当前位置: 首页 > article >正文

深入理解 C++ 左值右值、std::move 与函数重载中的参数传递

在 C++ 编程中,左值和右值的概念以及std::move的使用,常常让开发者感到困惑。特别是在函数重载场景下,如何合理利用这些特性来优化代码性能、确保语义正确,更是一个值得深入探讨的话题。

在开始之前,先提出几个问题:

  1. 当我定义一个函数func(int c) 时,我在调用这个函数 func(3)和int a = 3; func(a)有什么区别,形参在被实参赋值时调用拷贝构造吗,如果形参不是内置变量类型而是类类型又是什么情况?
  2. 那当函数不是值传递而是左值或右值的引用传递时会调用拷贝构造吗?
  3. 当我的类有一个私有成员变量cb_接受一个可执行类型数据作为回调函数,类提供一个公有成员方法setcallback来设置这个回调函数,setcallback有两种重载分别接受const T& t和右值引用 T&& t,但是两个的实现都是直接cb_ = t ,那么这里时调用拷贝赋值还是移动构造?
  4. 既然const T&可以接受右值,那我在现实时无论实参是左值还是右值,实现都用cb_ = std::move(t)不行吗,为什么好多开源代码中还要分别实现两个方法接收左值和右值?

如果回答不上来,请看下文讲解,并且在文章最后我们来回答这些问题。

一、左值与右值:

在 C++ 中,表达式可以分为左值(lvalue)和右值(rvalue)。简单来说,左值表示有标识符(如变量名)、能获取地址可被多次使用的对象;而右值通常是临时的、无名称的,无法获取地址,生命周期短暂。​

1.1 左值的特点​

  • 有标识符:例如int a = 10;中的a,通过变量名可以访问。​
  • 可寻址:可以使用&运算符获取其地址,如&a。​
  • 可多次使用:在程序的不同位置多次引用该变量。​

1.2 右值的特点​

  • 无标识符:像字面量10,没有名称标识。​
  • 不可寻址:无法对其使用&运算符获取地址。​
  • 临时性:通常出现在表达式求值过程中,用完即销毁,例如a + 5中的a + 5计算结果就是一个右值 。

1.3 左值与右值的判定示例 

int main() {int x = 10;  // x是左值int y = x + 5;  // x + 5是右值,y是左值int&& rref = 20;  // 20是右值,rref是右值引用return 0;
}

二、std::move:

std::move它的作用是将左值强制转换为右值引用。但需要明确的是,std::move本身并不会移动任何数据,它只是提供了一种让编译器按照右值来处理对象的方式。​

2.1 std::move 的实现原理

template<class T>
typename remove_reference<T>::type&& move(T&& t) {return static_cast<typename remove_reference<T>::type&&>(t);
}

通过模板推导和类型转换,std::move将传入的参数T&&(万能引用,可绑定左值或右值)转换为右值引用。​

2.2 std::move 的使用场景​

移动语义:在类中实现移动构造函数和移动赋值运算符时,使用std::move转移资源所有权,避免不必要的深拷贝,提升性能。例如std::vector在扩容时,就会利用移动语义高效地转移元素。

class MyString {
private:char* data;
public:MyString(const char* str) {// 深拷贝构造}MyString(MyString&& other) noexcept {data = other.data;other.data = nullptr;}
};

函数参数传递:当函数参数为右值引用时,将左值通过std::move转换后传入,触发移动操作。

三、函数重载中的参数传递:左值引用、右值引用与 const T&​

在函数重载时,常常会定义不同参数类型的版本,如左值引用T&、右值引用T&&以及常量左值引用const T& ,以适配不同类型的实参,实现最佳的性能和语义。​

3.1 左值引用T&​

左值引用T&只能绑定左值(如果传一个右值会报错),常用于需要修改实参的场景。例如:

void modifyValue(int& num) {num++;
}

modifyValue函数通过左值引用接收参数,能够直接修改传入的变量。​

3.2 右值引用T&&​

右值引用T&&专门用于绑定右值,在函数内部可以安全地移动右值的资源。在移动语义中应用广泛:

void processRValue(int&& num) {// 可以安全地移动num
}

当传入右值(如字面量或临时对象)时,processRValue函数会高效地处理而不产生额外拷贝。​

3.3 常量左值引用const T&​

const T&既可以绑定左值,也可以绑定右值。它的优势在于能够以只读方式访问对象,避免不必要的拷贝,并且可以处理临时对象。但如果在使用const T&接收右值后,使用std::move进行移动操作,可能会出现问题。因为const修饰的对象不能被移动(移动构造函数通常不接受const参数)。​

3.4 实际案例分析​

假设我们有一个类MyClass,其中有一个私有成员变量cb_用于存储回调函数,并提供setCallback方法来设置回调。

class MyClass {
private:std::function<void()> cb_;
public:// 左值引用重载:拷贝赋值template<typename T>void setCallback(const T& t) {std::cout << "拷贝赋值" << std::endl;cb_ = t;}// 右值引用重载:移动赋值template<typename T>void setCallback(T&& t) {std::cout << "移动赋值" << std::endl;cb_ = std::move(t);}
};

当传入左值时,const T&版本的setCallback函数会执行拷贝操作;当传入右值时,T&&版本的函数会执行移动操作,实现了性能优化和语义正确。​

如果只使用const T&版本并统一使用std::move,会对右值实参强制执行拷贝,违背移动语义;而如果只使用T&&版本并对所有参数使用std::move,可能会意外移动左值,导致数据丢失。​

四、完美转发:更优雅的解决方案​

泛型编程中,我们可以使用完美转发来解决上述问题。通过std::forward函数,能够在函数模板中保留实参的左值 / 右值属性,从而实现更灵活高效的参数传递(这里的&&不再代表右值引用)。

template<typename T>
void setCallback(T&& t) {cb_ = std::forward<T>(t);
}

当传入左值时,std::forward返回左值引用,触发拷贝;当传入右值时,返回右值引用,触发移动,仅需一个函数即可处理所有情况,同时保留了实参的原始属性。​

五、问题的回答

1.当我定义一个函数func(int c) 时,我在调用这个函数 func(3)和int a = 3; func(a)有什么区别,形参在被实参赋值时调用拷贝构造吗,如果形参不是内置变量类型而是类类型又是什么情况?

分析与回答:

右值实参(像临时对象)和左值实参(如变量)在传递给按值传递的形参时,处理机制是相同的

这意味着无论实参是左值还是右值,都会进行一次拷贝操作。不过,对于像 int 这样的内置类型,这种拷贝操作仅仅是进行位复制,并不会调用拷贝构造函数,如果是类类型才会调用拷贝构造函数。

#include <iostream>
using namespace std;// 内置类型参数(不会调用拷贝构造函数)
void func(int c) {cout << "func(int): " << c << endl;
}// 类类型参数(会调用拷贝构造函数)
class MyClass {
public:int value;MyClass(int v) : value(v) { cout << "构造函数" << endl; }MyClass(const MyClass& other) : value(other.value) {cout << "拷贝构造函数" << endl;}
};void func(MyClass c) {cout << "func(MyClass): " << c.value << endl;
}int main() {// 情况1:内置类型参数func(3);        // 实参是右值int a = 3;func(a);        // 实参是左值// 情况2:类类型参数func(MyClass(4)); // 实参是右值(临时对象)MyClass obj(5);func(obj);      // 实参是左值
}

//结果
func(int): 3
func(int): 3
构造函数
拷贝构造函数
func(MyClass): 4
构造函数
拷贝构造函数
func(MyClass): 5

2.那当函数不是值传递而是左值或右值的引用传递时会调用拷贝构造吗?

分析与回答:

在 C++ 中,当函数采用引用传递时,形参直接绑定到实参,不会发生拷贝操作。但左值引用和右值引用在绑定规则上存在差异,左值引用只能绑定到左值,无法绑定到右值(除非是 const T&);右值引用专门用于绑定右值(临时对象),不能直接绑定左值(将左值传递给右值引用参数,需要使用 std::move 将左值强制转换为右值引用)。

class MyClass {
public:MyClass(int v) { cout << "构造函数" << endl; }MyClass(const MyClass& other) { cout << "拷贝构造函数" << endl; }
};void funcByValue(MyClass c) {  // 值传递// 会调用拷贝构造函数
}void funcByRef(MyClass& c) {  // 左值引用传递// 不会调用拷贝构造函数
}void funcByRRef(MyClass&& c) {  // 右值引用传递// 不会调用拷贝构造函数
}int main() {MyClass obj(1);  // 调用构造函数funcByValue(obj);  // 调用拷贝构造函数funcByRef(obj);    // 不调用拷贝构造函数funcByValue(MyClass(2));  // 调用构造函数和拷贝构造函数funcByRRef(MyClass(3));   // 只调用构造函数(右值引用直接绑定临时对象)
}
构造函数        // MyClass obj(1)
拷贝构造函数    // funcByValue(obj)
构造函数        // MyClass(2)
拷贝构造函数    // funcByValue(MyClass(2))
构造函数        // MyClass(3)

3.当我的类有一个私有成员变量cb_接受一个可执行类型数据作为回调函数,类提供一个公有成员方法setcallback来设置这个回调函数,setcallback有两种重载分别接受const T& t和右值引用 T&& t,但是两个的实现都是直接cb_ = t ,那么这里时调用拷贝赋值还是移动赋值?

分析与回答:

先看一个实例代码:

class MyClass {
private:std::function<void()> cb_;  // 回调函数类型public:// 左值引用重载:接受可拷贝的回调template<typename T>void setCallback(const T& t) {cb_ = t;  // 拷贝赋值}// 右值引用重载:接受可移动的回调template<typename T>void setCallback(T&& t) {cb_ = t;  // 仍为拷贝赋值!}
};
  1. 右值引用重载中的 t 是左值
    尽管参数 T&& t 可以绑定右值,但在函数体内,t 是一个命名变量(左值)。因此 cb_ = t 会调用 std::function 的拷贝赋值运算符operator=(const T&)),而非移动赋值运算符。

  2. 移动语义需要显式调用 std::move
    若要触发移动赋值,必须将 t 转换为右值引用cb_ = std::move(t)

无论使用左值引用还是右值引用重载,cb_ = t 这行代码确实会导致拷贝赋值操作,而非移动赋值。

4.既然const T&可以接受右值,那我在现实时无论实参是左值还是右值,实现都用cb_ = std::move(t)不行吗,为什么好多开源代码中还要分别实现两个方法接收左值和右值?

在实现中只提供一个 const T& 参数的重载并统一使用 std::move 确实能处理所有情况,但是右值被 const 引用绑定后会失去可移动性。此时使用 std::move 会强制调用拷贝操作(因为移动构造函数通常不接受 const 参数)。

class ExpensiveResource {
public:ExpensiveResource() = default;ExpensiveResource(const ExpensiveResource&) { std::cout << "深拷贝(耗时)" << std::endl; }ExpensiveResource(ExpensiveResource&&) noexcept { std::cout << "移动(高效)" << std::endl; }
};template<typename T>
void setCallback(const T& t) {cb_ = std::move(t);  // 对 const 对象使用 move,触发拷贝!
}// 调用示例
setCallback(ExpensiveResource{});  // 本应移动,但实际触发深拷贝

移动语义的核心是高效转移资源所有权,而 const T& 的设计目的是安全地读取对象。混用两者会导致:

  • 语义混乱:用户传入临时对象(右值)是期望资源被移动,但实际执行了拷贝。
  • 性能损失:对于不可拷贝但可移动的类型(如 std::unique_ptr),const T& 会直接编译失败。

为什么需要同时提供 const T& 和 T&& 重载?

目标:根据实参类型自动选择最优操作
  • 左值实参(如变量):通过 const T& 接收,执行拷贝(因为左值可能被后续使用,不能直接移动)。
  • 右值实参(如临时对象):通过 T&& 接收,执行移动(右值即将销毁,可安全转移资源)。

相关文章:

深入理解 C++ 左值右值、std::move 与函数重载中的参数传递

在 C 编程中&#xff0c;左值和右值的概念以及std::move的使用&#xff0c;常常让开发者感到困惑。特别是在函数重载场景下&#xff0c;如何合理利用这些特性来优化代码性能、确保语义正确&#xff0c;更是一个值得深入探讨的话题。 在开始之前&#xff0c;先提出几个问题&…...

【大厂机试题解法笔记】矩阵匹配

题目 从一个 N * M&#xff08;N ≤ M&#xff09;的矩阵中选出 N 个数&#xff0c;任意两个数字不能在同一行或同一列&#xff0c;求选出来的 N 个数中第 K 大的数字的最小值是多少。 输入描述 输入矩阵要求&#xff1a;1 ≤ K ≤ N ≤ M ≤ 150 输入格式 N M K N*M矩阵 输…...

java 局域网 rtsp 取流 WebSocket 推送到前端显示 低延迟

众所周知 摄像头取流推流显示前端延迟大 传统方法是服务器取摄像头的rtsp流 然后客户端连服务器 中转多了&#xff0c;延迟一定不小。 假设相机没有专网 公网 1相机自带推流 直接推送到云服务器 然后客户端拉去 2相机只有rtsp &#xff0c;边缘服务器拉流推送到云服务器 …...

免费批量Markdown转Word工具

免费批量Markdown转Word工具 一款简单易用的批量Markdown文档转换工具&#xff0c;支持将多个Markdown文件一键转换为Word文档。完全免费&#xff0c;无需安装&#xff0c;解压即用&#xff01; 官方网站 访问官方展示页面了解更多信息&#xff1a;http://mutou888.com/pro…...

【Redis】Redis从入门到实战:全面指南

Redis从入门到实战:全面指南 一、Redis简介 Redis(Remote Dictionary Server)是一个开源的、基于内存的键值存储系统,它可以用作数据库、缓存和消息代理。由Salvatore Sanfilippo于2009年开发,因其高性能、丰富的数据结构和广泛的语言支持而广受欢迎。 Redis核心特点:…...

LeetCode 0386.字典序排数:细心总结条件

【LetMeFly】386.字典序排数&#xff1a;细心总结条件 力扣题目链接&#xff1a;https://leetcode.cn/problems/lexicographical-numbers/ 给你一个整数 n &#xff0c;按字典序返回范围 [1, n] 内所有整数。 你必须设计一个时间复杂度为 O(n) 且使用 O(1) 额外空间的算法。…...

智能体革命:企业如何构建自主决策的AI代理?

OpenAI智能代理构建实用指南详解 随着大型语言模型&#xff08;LLM&#xff09;在推理、多模态理解和工具调用能力上的进步&#xff0c;智能代理&#xff08;Agents&#xff09;成为自动化领域的新突破。与传统软件仅帮助用户自动化流程不同&#xff0c;智能代理能够自主执行工…...

以太网PHY布局布线指南

1. 简介 对于以太网布局布线遵循以下准则很重要&#xff0c;因为这将有助于减少信号发射&#xff0c;最大程度地减少噪声&#xff0c;确保器件作用&#xff0c;最大程度地减少泄漏并提高信号质量。 2. PHY设计准则 2.1 DRC错误检查 首先检查DRC规则是否设置正确&#xff0c;然…...

linux设备重启后时间与网络时间不同步怎么解决?

linux设备重启后时间与网络时间不同步怎么解决&#xff1f; 设备只要一重启&#xff0c;时间又错了/偏了&#xff0c;明明刚刚对时还是对的&#xff01; 这在物联网、嵌入式开发环境特别常见&#xff0c;尤其是开发板、树莓派、rk3588 这类设备。 解决方法&#xff1a; 加硬件…...

若依项目部署--传统架构--未完待续

若依项目介绍 项目源码获取 #Git工具下载 dnf -y install git #若依项目获取 git clone https://gitee.com/y_project/RuoYi-Vue.git项目背景 随着企业信息化需求的增加&#xff0c;传统开发模式存在效率低&#xff0c;重复劳动多等问题。若依项目通过整合主流技术框架&…...

零基础在实践中学习网络安全-皮卡丘靶场(第十一期-目录遍历模块)

经过前面几期的内容我们学习了很多网络安全的知识&#xff0c;而这期内容就涉及到了前面的第六期-RCE模块&#xff0c;第七期-File inclusion模块&#xff0c;第八期-Unsafe Filedownload模块。 什么是"遍历"呢&#xff1a;对学过一些开发语言的朋友来说应该知道&…...

mcts蒙特卡洛模拟树思想

您这个观察非常敏锐&#xff0c;而且在很大程度上是正确的&#xff01;您已经洞察到了MCTS算法在不同阶段的两种不同行为模式。我们来把这个关系理得更清楚一些&#xff0c;您的理解其实离真相只有一步之遥。 您说的“select是在二次选择的时候起作用”&#xff0c;这个观察非…...

华为云Flexus+DeepSeek征文 | 基于Dify构建具备联网搜索能力的知识库问答助手

华为云FlexusDeepSeek征文 | 基于Dify构建具备联网搜索能力的知识库问答助手 一、构建知识库问答助手引言二、构建知识库问答助手环境2.1 基于FlexusX实例的Dify平台2.2 基于MaaS的模型API商用服务 三、构建知识库问答助手实战3.1 配置Dify环境3.2 创建知识库问答助手3.3 使用知…...

Qt学习及使用_第1部分_认识Qt---Qt开发基本流程

前言 学以致用,通过QT框架的学习,一边实践,一边探索编程的方方面面. 参考书:<Qt 6 C开发指南>(以下称"本书") 标识说明:概念用粗体倾斜.重点内容用(加粗黑体)---重点内容(红字)---重点内容(加粗红字), 本书原话内容用深蓝色标识,比较重要的内容用加粗倾…...

[特殊字符] Spring Boot底层原理深度解析与高级面试题精析

一、Spring Boot底层原理详解 Spring Boot的核心设计哲学是约定优于配置和自动装配&#xff0c;通过简化传统Spring应用的初始化和配置流程&#xff0c;显著提升开发效率。其底层原理可拆解为以下核心机制&#xff1a; 自动装配&#xff08;Auto-Configuration&#xff09; 核…...

MeanFlow:何凯明新作,单步去噪图像生成新SOTA

1.简介 这篇文章介绍了一种名为MeanFlow的新型生成模型框架&#xff0c;旨在通过单步生成过程高效地将先验分布转换为数据分布。文章的核心创新在于引入了平均速度的概念&#xff0c;这一概念的引入使得模型能够通过单次函数评估完成从先验分布到数据分布的转换&#xff0c;显…...

【2D与3D SLAM中的扫描匹配算法全面解析】

引言 扫描匹配(Scan Matching)是同步定位与地图构建(SLAM)系统中的核心组件&#xff0c;它通过对齐连续的传感器观测数据来估计机器人的运动。本文将深入探讨2D和3D SLAM中的各种扫描匹配算法&#xff0c;包括数学原理、实现细节以及实际应用中的性能对比&#xff0c;特别关注…...

【Vue】scoped+组件通信+props校验

【scoped作用及原理】 【作用】 默认写在组件中style的样式会全局生效, 因此很容易造成多个组件之间的样式冲突问题 故而可以给组件加上scoped 属性&#xff0c; 令样式只作用于当前组件的标签 作用&#xff1a;防止不同vue组件样式污染 【原理】 给组件加上scoped 属性后…...

Docker环境下安装 Elasticsearch + IK 分词器 + Pinyin插件 + Kibana(适配7.10.1)

做RAG自己打算使用esmilvus自己开发一个&#xff0c;安装时好像网上没有比较新的安装方法&#xff0c;然后找了个旧的方法对应试试&#xff1a; &#x1f680; 本文将手把手教你在 Docker 环境中部署 Elasticsearch 7.10.1 IK分词器 拼音插件 Kibana&#xff0c;适配中文搜索…...

第14节 Node.js 全局对象

JavaScript 中有一个特殊的对象&#xff0c;称为全局对象&#xff08;Global Object&#xff09;&#xff0c;它及其所有属性都可以在程序的任何地方访问&#xff0c;即全局变量。 在浏览器 JavaScript 中&#xff0c;通常 window 是全局对象&#xff0c; 而 Node.js 中的全局…...

构建Docker镜像的Dockerfile文件详解

文章目录 前言Dockerfile 案例docker build1. 基本构建2. 指定 Dockerfile 路径3. 设置构建时变量4. 不使用缓存5. 删除中间容器6. 拉取最新基础镜像7. 静默输出完整示例 docker runDockerFile 入门syntax指定构造器FROM基础镜像RUN命令注释COPY复制ENV设置环境变量EXPOSE暴露端…...

Shell 解释器​​ bash 和 dash 区别

bash 和 dash 都是 Unix/Linux 系统中的 ​​Shell 解释器​​&#xff0c;但它们在功能、语法和性能上有显著区别。以下是它们的详细对比&#xff1a; ​​1. 基本区别​​ ​​特性​​​​bash (Bourne-Again SHell)​​​​dash (Debian Almquist SHell)​​​​来源​​G…...

从0开始学习R语言--Day17--Cox回归

Cox回归 在用医疗数据作分析时&#xff0c;最常见的是去预测某类病的患者的死亡率或预测他们的结局。但是我们得到的病人数据&#xff0c;往往会有很多的协变量&#xff0c;即使我们通过计算来减少指标对结果的影响&#xff0c;我们的数据中依然会有很多的协变量&#xff0c;且…...

ABAP设计模式之---“Tell, Don’t Ask原则”

“Tell, Don’t Ask”是一种重要的面向对象编程设计原则&#xff0c;它强调的是对象之间如何有效地交流和协作。 1. 什么是 Tell, Don’t Ask 原则&#xff1f; 这个原则的核心思想是&#xff1a; “告诉一个对象该做什么&#xff0c;而不是询问一个对象的状态再对它作出决策。…...

Oracle实用参考(13)——Oracle for Linux物理DG环境搭建(2)

13.2. Oracle for Linux物理DG环境搭建 Oracle 数据库的DataGuard技术方案,业界也称为DG,其在数据库高可用、容灾及负载分离等方面,都有着非常广泛的应用,对此,前面相关章节已做过较为详尽的讲解,此处不再赘述。 需要说明的是, DG方案又分为物理DG和逻辑DG,两者的搭建…...

CentOS 7.9安装Nginx1.24.0时报 checking for LuaJIT 2.x ... not found

Nginx1.24编译时&#xff0c;报LuaJIT2.x错误&#xff0c; configuring additional modules adding module in /www/server/nginx/src/ngx_devel_kit ngx_devel_kit was configured adding module in /www/server/nginx/src/lua_nginx_module checking for LuaJIT 2.x ... not…...

IP选择注意事项

IP选择注意事项 MTP、FTP、EFUSE、EMEMORY选择时&#xff0c;需要考虑以下参数&#xff0c;然后确定后选择IP。 容量工作电压范围温度范围擦除、烧写速度/耗时读取所有bit的时间待机功耗擦写、烧写功耗面积所需要的mask layer...

虚拟机网络不通的问题(这里以win10的问题为主,模式NAT)

当我们网关配置好了&#xff0c;DNS也配置好了&#xff0c;最后在虚拟机里还是无法访问百度的网址。 第一种情况&#xff1a; 我们先考虑一下&#xff0c;网关的IP是否和虚拟机编辑器里的IP一样不&#xff0c;如果不一样需要更改一下&#xff0c;因为我们访问百度需要从物理机…...

SOC-ESP32S3部分:30-I2S音频-麦克风扬声器驱动

飞书文档https://x509p6c8to.feishu.cn/wiki/SKZzwIRH3i7lsckUOlzcuJsdnVf I2S简介 I2S&#xff08;Inter-Integrated Circuit Sound&#xff09;是一种用于传输数字音频数据的通信协议&#xff0c;广泛应用于音频设备中。 ESP32-S3 包含 2 个 I2S 外设&#xff0c;通过配置…...

比较数据迁移后MySQL数据库和ClickHouse数据仓库中的表

设计一个MySQL数据库和Clickhouse数据仓库的表数据比较的详细程序流程,两张表是相同的结构,都有整型主键id字段,需要每次从数据库分批取得2000条数据,用于比较,比较操作的同时可以再取2000条数据,等上一次比较完成之后,开始比较,直到比较完所有的数据。比较操作需要比较…...