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

C++拷贝语义和移动语义,左值引用与右值引用

一般的左右值

//函数的返回值是右值

//函数的形参是左值

//一般来说字面量是右值,但字符串字面量是左值

//x++返回值是一个临时变量(右值)

//++x是左值

//临时运算(a+b)是右值

核心:函数重载决议

C++ 编译器根据你传递的参数的值类别(是左值还是右值)来决定调用哪个重载版本的函数(拷贝构造函数还是移动构造函数)。


1. 常左值引用 (const T&)

  • 它能绑定到什么? 几乎任何东西
    • 左值(有名字的、有持久状态的对象)
    • 右值(临时对象、字面量)
  • 为什么是 const
    • 因为拷贝操作的目的是创建副本,而不应该修改源对象。使用 const 引用保证了这一点,并且使得它可以绑定到临时对象(右值)。
  • 何时触发?
    • 当你传递一个左值(例如一个具名变量)时,编译器必须选择这个版本,因为左值不能绑定到非常量的右值引用 (T&&)。
    • 当你传递一个右值时,编译器也可以选择这个版本(因为 const T& 能绑定右值),但如果有更匹配的(即接受 T&& 的),它会优先选择更匹配的。

简单说:const T& 是一个“万能”的引用,用于表示“我只需要读你的数据,用来做一个副本”。

左值引用在拷贝构造函数中的必要性

如果拷贝构造函数的参数不是引用,确实会导致无限递归

核心原因:值传递(Pass-by-value)本身就是在调用拷贝构造函数

在 C++ 中,当你以的方式(而不是引用或指针)传递一个对象给函数时,编译器必须在内存中创建这个参数的一个副本。创建这个副本的标准方式就是调用该对象的拷贝构造函数

现在,让我们看看如果拷贝构造函数本身采用值传递会发生什么。

错误示例分析

假设我们错误地这样定义拷贝构造函数:

class MyClass {
public:int data;// 错误的拷贝构造函数!参数是值传递 (MyClass other)MyClass(MyClass other) { // 注意:这里没有 & this->data = other.data;std::cout << "Copy Constructor (wrongly) called\n";}// 正常的构造函数MyClass(int d) : data(d) {}
};

现在,当我们尝试用一个 MyClass 对象来初始化另一个时:

MyClass objA(10);
MyClass objB = objA; // 这里会发生什么?

编译器看到 MyClass objB = objA;,它需要调用 MyClass 的拷贝构造函数来创建 objB

让我们一步步模拟编译器的行为:

  1. 第一次调用:为了调用拷贝构造函数 MyClass(MyClass other),它需要将实参 objA 传递给形参 other
  2. 创建形参:形参 other值传递 的。这意味着需要创建 objA 的一个副本来初始化 other
  3. 如何创建副本? 创建 objA 的副本,需要调用 MyClass 的… 拷贝构造函数
  4. 第二次调用:于是,编译器准备调用 MyClass(MyClass other) 来创建 other 这个形参。同样,为了这次调用,它需要将 objA 传递给新的 other 形参。
  5. 创建新的形参:这个新的 other 形参又是值传递的,所以需要再次创建 objA 的副本…
  6. 第三次调用:于是,编译器再次准备调用 MyClass(MyClass other)
  7. 无限循环:这个过程会永无止境地进行下去,直到堆栈空间被耗尽(Stack Overflow)。

这个过程可以可视化如下:

MyClass objB = objA;-> 调用 MyClass(MyClass other) 【第一次调用】-> 为了传递参数,需要创建 objA 的副本给 other-> 调用 MyClass(MyClass other) 【第二次调用】-> 为了传递参数,需要创建 objA 的副本给 other-> 调用 MyClass(MyClass other) 【第三次调用】-> ... (无限递归,直到栈溢出)
正确的解决方案:使用引用传递

为了解决这个问题,拷贝构造函数的参数必须是引用。引用本质上是一个别名,是对象的另一个名字。传递引用不会创建对象的副本,因此也不需要调用拷贝构造函数。

class MyClass {
public:int data;// 正确的拷贝构造函数!参数是常左值引用 (const MyClass& other)MyClass(const MyClass& other) { // 注意:这里有 &this->data = other.data;std::cout << "Copy Constructor (correctly) called\n";}MyClass(int d) : data(d) {}
};

现在,同样执行 MyClass objB = objA;

  1. 调用:编译器需要调用 MyClass(const MyClass& other)
  2. 传递参数:形参 other 是一个引用,它直接绑定到 objA不需要创建任何副本
  3. 执行函数体:函数体正常执行,用 objA.data 来初始化 objB.data
  4. 完成:只有一次拷贝构造调用,成功创建 objB
为什么是 const 引用?
  • 保证不修改源对象:拷贝操作的目的是创建副本,不应该修改原始对象。const 关键字确保了这一点。
  • 允许绑定到右值const 引用可以绑定到临时对象(右值),虽然这在拷贝构造的场景中不常见,但增加了灵活性。(例如 MyClass obj = MyClass(10);,虽然这里更可能触发移动构造或优化)。
特性错误的方式 (值传递)正确的方式 (引用传递)
参数声明MyClass(MyClass other)MyClass(const MyClass& other)
传递机制创建实参的副本创建实参的别名(引用)
初始化形参 other需要调用拷贝构造函数不需要调用任何构造函数
结果无限递归,导致栈溢出正常工作,只调用一次

所以,拷贝构造函数的参数必须是一个引用,以避免在传递参数时发生无限递归的自我调用。这是 C++ 语法规则中一个非常基础和强制性的要求。


2. 右值引用 (T&&)

  • 它能绑定到什么? 只能绑定到右值(临时对象、被 std::move 转换后的对象)。
  • 为什么不是 const
    • 因为移动操作的目的是**“偷”走源对象的资源**!这必然需要修改源对象(例如,将它的内部指针设为 nullptr)。所以它不能是 const
  • 何时触发?
    • 只有当你传递一个右值时,编译器才会选择这个版本。这是一个更精确、更匹配的选项。

简单说:T&& 是一个“挑剔”的引用,专门用于表示“你是一个即将消亡的临时对象,我被允许掏空你”。


实战对比:编译器如何选择?

假设我们有一个类 MyClass,它完整定义了拷贝和移动构造函数:

class MyClass {
public:// 拷贝构造函数 (参数:常左值引用)MyClass(const MyClass& other) {std::cout << "Copy Constructor called\n";// ... 深拷贝逻辑 ...}// 移动构造函数 (参数:右值引用)MyClass(MyClass&& other) noexcept {std::cout << "Move Constructor called\n";// ...“偷”资源的逻辑,例如:// this->data_ptr = other.data_ptr;// other.data_ptr = nullptr;}// ... 其他成员 ...
};

现在看几个不同的初始化场景:

场景 1:用一个左值初始化

MyClass obj1;
MyClass obj2 = obj1; // 源是左值 obj1
  • obj1 是一个左值(有名字的变量)。
  • 可以匹配 MyClass(const MyClass&) (完美匹配)。
  • 不能匹配 MyClass(MyClass&&) (因为左值不能绑定给 MyClass&&)。
  • 结果:调用拷贝构造函数。

场景 2:用一个显式转换的右值初始化

MyClass obj1;
MyClass obj2 = std::move(obj1); // std::move(obj1) 将左值转换为右值
  • std::move(obj1) 的结果是一个右值
  • std::move 是一个“强制搬家许可”。你对编译器说:“虽然 obj1 是我的常住地址,但我现在授权你,可以把它当成一个临时的快递盒来处理。我保证以后不用它了。”
  • 可以匹配 MyClass(const MyClass&) (但没那么匹配)。
  • 更匹配 MyClass(MyClass&&) (精确匹配)。
  • 结果:调用移动构造函数。

场景 3:用一个临时对象(纯右值)初始化

MyClass obj2 = MyClass(); // MyClass() 创建一个临时对象(右值)
  • MyClass() 产生一个右值(临时对象)。
  • 可以匹配 MyClass(const MyClass&)
  • 更匹配 MyClass(MyClass&&)
  • 结果:调用移动构造函数。 (注意:编译器可能会直接优化掉这次构造,但理论上移动构造是可用的最佳选择)。

场景 4:从一个返回值的函数初始化

MyClass createMyClass() {return MyClass(); // 返回一个临时对象
}MyClass obj2 = createMyClass(); // 函数返回值是右值
  • createMyClass() 的返回值是一个右值
  • 同样,它更匹配 MyClass(MyClass&&)
  • 结果:调用移动构造函数。 (同样,RVO/NRVO 优化可能会发生,但语言标准保证移动是底线)。

noexcept 是一个承诺。
你把它加在函数后面,就是在对编译器和所有调用你这个函数的代码做出一个庄严的承诺:
“我保证,这个函数绝对、绝对不会抛出任何异常!”

参数类型可绑定的实参类型语义典型用途
const T&左值、右值“借我来读一下,做个副本”拷贝构造函数拷贝赋值运算符
T&&右值“你快要死了,把资源给我吧!”移动构造函数移动赋值运算符

这种参数类型的区分,是 C++ 标准委员会设计的一种非常巧妙的重载机制。它让编译器能根据你提供的参数是“持久的”还是“临时的”,自动选择最合适、最高效的操作:是安全地拷贝,还是高效地移动。

专业术语我的比喻一句话解释
拷贝语义复印机模式创建一个全新的、一模一样的副本,原件不变。成本高,但安全。
移动语义搬家模式掏空原件,把内部资源(如内存)转移给新主人,原件变空壳。成本极低。
左值 (T&)常住地址名字、有持久身份的对象(如变量)。可以放在赋值语句的左边。
右值 (T&&)临时快递盒没名字马上要消失的临时对象(如函数返回值、字面量)。用完即弃。
const T& 参数复印专线我只读不改”,可以接收任何类型的对象(左值、右值都可以),用来安全地创建副本。
T&& 参数搬家专线我要掏空你”,只接收临时对象(右值),用来高效地转移资源。

文章转载自:

http://LeNqEtxC.qmrsf.cn
http://Y9Y3ZRhG.qmrsf.cn
http://0TPnyWth.qmrsf.cn
http://SQiEwx93.qmrsf.cn
http://IagA9vPC.qmrsf.cn
http://kwhaqqzC.qmrsf.cn
http://wfhs3tcg.qmrsf.cn
http://KpIrZ5nb.qmrsf.cn
http://yJd6TOZ1.qmrsf.cn
http://3kgt2V80.qmrsf.cn
http://3kGGPfv5.qmrsf.cn
http://pDueB3Vd.qmrsf.cn
http://k7QRyrWY.qmrsf.cn
http://fZuAD6Z9.qmrsf.cn
http://e0yiMT38.qmrsf.cn
http://SqssPwdn.qmrsf.cn
http://yOHIvwLM.qmrsf.cn
http://IVTuyF9r.qmrsf.cn
http://XWmKlWyf.qmrsf.cn
http://qhCZaNes.qmrsf.cn
http://jXT4XZ2w.qmrsf.cn
http://xbPsqxfJ.qmrsf.cn
http://lX4FccLR.qmrsf.cn
http://KKxVANIw.qmrsf.cn
http://FAqglJHV.qmrsf.cn
http://ENBaZXPX.qmrsf.cn
http://Fig7gSGB.qmrsf.cn
http://7VYPpu4r.qmrsf.cn
http://JEUVWNJw.qmrsf.cn
http://QOVFTiSV.qmrsf.cn
http://www.dtcms.com/a/363244.html

相关文章:

  • 汉得H-AI飞码智能编码助手V1.2.4正式发布!
  • Turso数据库:用Rust重构的下一代SQLite——轻量级嵌入式数据库的未来选择
  • 三维重建——基础理论(四):三维重建基础与极几何原理(三维重建基础、单视图回忆、双目视觉、极几何、本质矩阵与基础矩阵、基础矩阵估计)
  • 虚实交互新突破:Three.js融合AR技术的孪生数据操控方法
  • 什么是 AWS 和 GCE ?
  • 解决Mac电脑连接蓝牙鼠标的延迟问题
  • 对于牛客网—语言学习篇—编程初学者入门训练—复合类型:BC140 杨辉三角、BC133 回型矩阵、BC134 蛇形矩阵题目的解析
  • A-Level课程选择与机构报名指南
  • 净利润超10亿元,智能类产品18倍增长!顾家家居2025年半年报业绩:零售增长强劲,整家定制多维突破,全球深化布局!|商派
  • Selenium核心技巧:元素定位与等待策略
  • 苹果内部 AI聊天机器人“Asa”曝光,为零售员工打造专属A
  • 【国内外云计算平台对比:AWS/阿里云/Azure】
  • react用useImages读取图片,方便backgroundImage
  • 硬件开发_基于物联网的自动售卖机系统
  • Spring Boot数据校验validation实战:写少一半代码,还更优雅!
  • arm架构本地部署iotdb集群
  • 物联网开发学习总结(1)—— IOT 设备 OTA 升级方案
  • 没有天硕工业级SSD固态硬盘,物联网痛点如何解决?
  • Sping Web MVC入门
  • Spring MVC BOOT 中体现的设计模式
  • Web基础学习笔记01
  • 我的项目我做主:Focalboard+cpolar让团队协作摆脱平台依赖
  • 【Vue2 ✨】 Vue2 入门之旅(五):组件化开发
  • 2024年全国研究生数学建模竞赛华为杯D题大数据驱动的地理综合问题求解全过程文档及程序
  • 【硬核干货】把 DolphinScheduler 搬进 K8s:奇虎 360 商业化 900 天踩坑全记录
  • 复杂PDF文档如何高精度解析
  • 【Flask + Vue3 前后端分离管理系统】
  • GitHub 热榜项目 - 日榜(2025-09-02)
  • 详解 C++ 中的虚析构函数
  • 电机控制(二)-控制理论基础