03 | 右值和移动究竟解决了什么问题?
该思维导图由 AI 生成,仅供参考
值分左右
- 深入了解
- 翻译
- 解释
- 总结
移动语义是C++11引入的重要概念,解决了处理左值和右值的复杂性。文章首先介绍了C++中的值类别,包括lvalue、rvalue、glvalue、xvalue和prvalue。右值引用的引入和`std::move`的作用也得到了讨论。此外,文章解释了临时对象的生命周期延长规则,以及可能导致的潜在问题。移动语义的意义在于减少运行开销,特别是在使用容器类的情况下。文章通过比较C++11之前和之后的写法,阐述了移动语义对性能的提升和代码简洁性的影响。实现移动的几个步骤,包括对象的拷贝构造和移动构造函数、`swap`成员函数的设计也得到了介绍。总的来说,本文通过具体的例子和技术细节,生动地阐述了移动语义的重要性和实现方法,对于想要提高C++代码性能和可读性的程序员具有很高的实用价值。
《现代 C++ 编程实战》,新⼈⾸单¥59
全部留言(85)
- 最新
- 精选
- 中年男子第二题: 左值和右值都有效是因为构造参数时,如果是左值,就用拷贝构造构造函数,右值就用移动构造函数 无论是左值还是右值,构造参数时直接生成新的智能指针,因此不需要判断
作者回复: 理解满分。👍
2019-12-03348 - NEVER SETTLE请教下老师,字符串字面量是左值,是不是在C++中 字符串其实是const char[N],其实是个常量表达式,在内存中有明确的地址。
作者回复: 是。👍
2019-12-02225 - NEVER SETTLE老师,我这个初学者看的比较慢,目前只看了右值与右值引用,下面是我总结了的学习心得,请您指点下: **背景: C++11为了支持移动操作,引用了新的引用类型-右值引用。 所谓右值引用就是绑定到右值的引用。 为了区分右值引用,引入左值引用概念,即常规引用。 那左值与右值是是什么?** ## 1、左值与右值 **左值 lvalue 是有标识符、可以取地址的表达式** * 变量、函数或数据成员的名字 * 返回左值引用的表达式,如 ++x、x = 1、cout << ' ' * 字符串字面量如 "hello world" 表达式是不是左值,就看是否可以取地址,或者返回类型是否可以用(常规)引用来接收: ``` int x = 0; cout << "(x).addr = " << &x << endl; cout << "(x = 1).addr = " << &(x = 1) << endl; //x赋值1,返回x cout << "(++x).addr = " << &++x << endl; //x自增1,返回x ``` > 运行结果: (x).addr = 0x22fe4c (x = 1).addr = 0x22fe4c (++x).addr = 0x22fe4c ``` cout << "hello world = " << &("hello world") << endl; ``` > 运行结果: hello world = 0x40403a C++中的字符串字面量,可以称为字符串常量,表示为const char[N],其实是地址常量表达式。 在内存中有明确的地址,不是临时变量。 ``` cout << "cout << ' ' = " << &(cout << ' ') << endl; ``` > 运行结果: cout << ' ' = 0x6fd0acc0 **纯右值 prvalue 是没有标识符、不可以取地址的表达式,一般称为“临时对象”** * 返回非引用类型的表达式,如 x++、x + 1、make_shared(42) * 除字符串字面量之外的字面量,如 42、true ``` //cout << "(x++).addr = " << &x++ << endl; //返回一个值为x的临时变量,再把x自增1 //cout << "(x + 1).addr = " << &(x + 1) << endl; //返回一个值为x+1的临时变量 //cout << "(42).addr = " << &(42) << endl; //返回一个值为42的临时变量 //cout << "(true).addr = " << &(true) << endl; //返回一个值为true的临时变量 ``` > 编译出错: 每行代码报错:表达式必须为左值或函数指示符 因为以上表达式都返回的是“临时变量”,是不可以取地址的 ---
作者回复: 你在学校的时候,都是让老师来看你的笔记记得好不好的吗?😂 对不起,如果有明确的问题,我可以回答。否则,我只能暂时忽略了。
2019-12-03624 - doge我感觉我有点理解完美转发的意思了,对于一个函数,如果形参是右值引用,但在函数体内,这个“右值引用”实际上是一个左值变量,然后函数内再有一个函数传入这个参数,那么就会调用对应的左值引用版本,而完美转发的意义就相当于做一次类型转换,让这个参数保持一开始传入时的左值右值类别。 不知道理解的对不对?
作者回复: 对。
2021-02-2222 - 禾桃"请查看一下标准函数模板 make_shared 的声明,然后想一想,这个函数应该是怎样实现的。" template <class T, class... Args> std::shared_ptr<T> make_shared (Args&&... args) { T* ptr = new T(std::forward<Args...>(args...)); return std::shared_ptr<T>(ptr); } 我的考虑是: make_shared声明里的(Args&&...) 是universal reference, 所以在函数体里用完美转发(std::forward)把参数出入T的构造函数, 以调用每个参数各自对用的构造函数(copy or move)。 肯定还有别的需要考量的地方,请指正。 谢谢!
作者回复: 对,最主要就是这点,用完美转发来正确调用构造函数。
2019-12-03521 - NEVER SETTLE老师,留言有字数限制,我是接着上个留言来的,上面那个总结了下左值与右值,这个是右值引用的学习心得: ## 2、右值引用 **针对以上的所说的“临时变量”,如何来“接收”它呢?** * 最直白的办法,就是直接用一个变量来“接收” 以x++为例: ``` void playVal(int y) { cout << "y = " << y << ", (y).adrr = " << &y << endl; } int x = 0; playVal(x++); cout << "x = " << x << ", (x).adrr = " << &x << endl; ``` >运行结果: y = 0, (y).adrr = 0x22fe20 x = 1, (x).adrr = 0x22fe4c 这是一个值传递过程,相当于 int y = x++,即x++生成的临时变量给变量y赋值,之后临时变量就“消失”,这里发生是一次拷贝。 如何避免发生拷贝呢? 通常做法是使用引用来“接收”,即引用传递。 上面说过,使用一个(常规)引用来“接收”一个临时变量,会报错: ``` void playVal(int& y) ``` > error : 非常量引用的初始值必须为左值 * 普遍的做法都是使用常量引用来“接收”临时变量(C++11之前) ``` void playVal(const int& y) ``` 这里编译器做了处理: int tmp = x++; const int& y = tmp; 发生内存分配。 其实还是发生了拷贝。 * 使用右值引用来“接收”临时变量(C++11之后) 上面说过,“临时变量”是一个右值,所以这里可以使用右值引用来“接收”它 右值引用的形式是 T&& : ``` void playVal(int&& y) { cout << "y = " << y << ", (y).adrr = " << &y << endl; } int x = 0; playVal(x++); cout << "x = " << x << ", (x).adrr = " << &x << endl; ``` > 运行结果: y = 0, (y).adrr = 0x22fe4c x = 1, (x).adrr = 0x22fe48 这是一个(右值)引用传递的过程,相当于 int&& y = x++,这里的右值引用 y 直接“绑定”了“临时变量”,因为它就会有了命名,变成“合法”的,就不会“消失”。 **注意:这里的变量 y 虽然是右值引用类型,但它是一个左值,可以正常对它取地址** (如上例所示)
作者回复: 再多说一句,如果每个人都这么让我来看笔记的话,我是不可能满足所有人的。只看你的,也对别人不公。在这儿回答(不重复的)问题则不同,问题一般是有共性的,回答之后,大家都能看到,都能从中受益。
2019-12-03617 - 可爱的小奶狗老师,为什么对临时对象不能使用取地址符&,比如&shape(),我知道这会编译报错:不能对右值取地址。我困惑的是:既然有对象,肯定有地址存放嘛,那一定能取地址才对。c++为什么要这样设计?或者说从堆栈的使用机制上看是为啥?
作者回复: 因为危险。临时对象在当前语句执行完成之后就被析构了。你握着这个已经不存在的对象的指针,想干嘛?
2020-05-2112 - NEVER SETTLE“返回左值引用的表达式,,如 x++、x + 1 ”不太清楚原因,后来我就试了下: ``` int x = 0; cout << "(x).addr = " << &x << endl; cout << "(x = 1).addr = " << &(x = 1) << endl; cout << "(++x).addr = " << &++x << endl; //cout << "(x++).addr = " << &x++ << endl; ``` > 运行结果: (x).addr = 0x22fe4c (x = 1).addr = 0x22fe4c (++x).addr = 0x22fe4c 最后一行注释掉的代码报错:表达式必须为左值或函数指示符
作者回复: 对,x = 1 和 ++x 返回的都是对 x 的 int&。x++ 则返回的是 int。
2019-12-0212 - 糖又是看不懂的一节。。。老师讲的课程太深刻了。。。 1. 本来感觉自己还比较了解左右值的区别,但是,文中提到:一个 lvalue 是通常可以放在等号左边的表达式,左值,然后下面说:字符串字面量如 "hello world",但字符串字面量貌似不可以放到等号左边,搞晕了。 2. 内存访问的局域性是指什么呢?又有何优势呢?老师能提供介绍的链接吗 3. 为何对于移动构造函数来讲不抛出异常尤其重要呢? 希望老师能指点一下
作者回复: 1. “通常”。字符串字面量是个继承自C的特殊情况。 2. 这个搜索一下就行。这是CPU的缓存结构决定的。 3. 其他类,尤其容器类,会期待移动构造函数无异常,甚至会在它有异常时选择拷贝构造函数,以保证强异常安全性。
2019-12-02511 - 安静的雨Obj simple_with_move() { Obj obj; // move 会禁止 NRVO return std::move(obj); } move后不是类型转换到右值引用了吗? 为啥返回值类型还是obj?
作者回复: 文中已经说了,禁止返回本地对象的引用。 需要生成一个 Obj,给了一个 Obj&&,不就是调用构造函数而已么。所以(看文中输出),就是多产生了一次Obj(Obj&&) 的调用。
2019-12-03410