
🔥小叶-duck:个人主页
❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《算法题讲解指南》--优选算法
《算法题讲解指南》--递归、搜索与回溯算法
《算法题讲解指南》--动态规划算法
✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游
目录
[二. 列表初始化:万物皆可 {} 的统一初始化方案](#二. 列表初始化:万物皆可 {} 的统一初始化方案)
3、C++11中的std:initializer_list(初始化列表)
[三. 右值引用与移动语义:彻底解决拷贝效率问题](#三. 右值引用与移动语义:彻底解决拷贝效率问题)
[2.1 概念](#2.1 概念)
[2.2 左值引用、右值引用可以相互交叉](#2.2 左值引用、右值引用可以相互交叉)
[2.3 引用延迟生命周期](#2.3 引用延迟生命周期)
[2.4 左值和右值的参数匹配问题](#2.4 左值和右值的参数匹配问题)
[2.5 右值引用和移动语义的使用场景](#2.5 右值引用和移动语义的使用场景)
[2.5.1 左值引用主要使用场景](#2.5.1 左值引用主要使用场景)
[2.5.2 移动构造和移动赋值](#2.5.2 移动构造和移动赋值)
[2.6 右值引用和移动语义解决传值返回问题](#2.6 右值引用和移动语义解决传值返回问题)
[2.6.1 两种场景实践](#2.6.1 两种场景实践)
[2.6.2 传值返回没有拷贝了,是否意味设计右值引用和移动语义就没意义?](#2.6.2 传值返回没有拷贝了,是否意味设计右值引用和移动语义就没意义?)
[2.6.3 右值对象构造,只有拷贝构造,没有移动构造的场景](#2.6.3 右值对象构造,只有拷贝构造,没有移动构造的场景)
[2.6.4 右值对象构造,既有拷贝构造,也有移动构造的场景](#2.6.4 右值对象构造,既有拷贝构造,也有移动构造的场景)
[2.6.5 右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景](#2.6.5 右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景)
[2.6.6 右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景](#2.6.6 右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景)
前言
C++11 作为 C++ 历史上最具里程碑意义的版本,引入了众多革命性特性,彻底改变了 C++ 的编程范式。其中,列表初始化 实现了初始化方式的大一统 ,右值引用与移动语义 则解决了长期存在的拷贝效率问题。这两大特性不仅简化了代码编写,更在性能优化上带来质的飞跃。本文结合核心知识点、示例代码,从列表初始化的用法与原理,到右值引用、移动语义的落地实践,层层拆解 C++11 这两大核心特性。
一、C++11的发展历史
C++11 是C++的第二个主要版本,并且是从C++98起的最重要更新。它引入了大量更改,标准化了既有实践,并改进了对C++程序员可用的抽象。在它最终由ISO在2011年8月12日采纳前,人们曾使用名称"C++0X",因为它曾被期待在2010年之前发布。C++03与C++11期间花了8年时间,故而是迄今为止最长的版本间隔。从那时起,C++有规律地每3年更新一次。

二. 列表初始化:万物皆可 {} 的统一初始化方案
C++98 中初始化方式杂乱(如数组用 {}:int arr[3] = { 1, 2, 3 }、类用构造函数:Date d1(2000, 1, 1)、内置类型直接赋值:int x = 1),C++11 引入列表初始化 (统一初始化) ,通过 {} 实现 "一切对象皆可初始化" ,兼顾简洁性与安全性 。
1、C++98中的{}
C++98中一般数组和结构体可以用0进行初始化。
cpp
#include<iostream>
using namespace std;
//===========================列表初始化===========================
//C++98中的{}
struct Point
{
int _x = 0;
int _y = 0;
};
int main()
{
//C++98
int arr1[] = { 1, 2, 3 };
int arr2[] = { 0 };
Point p = { 1, 2 };
return 0;
}

2、C++11中的{}
C++11以后,想统一初始化方式:试图实现一切对象皆可用{}初始化 ,{}初始化 也叫列表初始化;
cpp
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date(const Date& d)" << endl;
}
private:
int _year;
int _month;
int _day;
};
void Insert(const Date& d)
{
//......
}
- 内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化变成直接构造
cpp
//C++11中的{}
int x1 = { 2 }; //完全等同于:int x1 = 2;
Date d1 = { 2025, 1, 1 };
// 自定义类型支持
// 这里本质是用{2025, 1, 1}构造一个 Date临时对象
// 临时对象再去拷贝构造d1,编译器优化后合二为一变成{2025, 1, 1}直接构造初始化d1
// 运行一下,我们可以验证上面的理论,发现是没调用拷贝构造的
// 这里d2引用的是{ 2000, 1, 1 }构造的临时对象
const Date& d2 = { 2000, 1, 1 };
// 需要注意的是C++98支持单参数时类型转换,也可以不用{}
Date d3 = { 2026 };
Date d4 = 2026;
- {}初始化的过程中,可以省略掉=
cpp
// 可以省略掉=
Point p1{ 1, 2 };
int x2{ 2 };
Date d5{ 2024, 7, 25 };
const Date& d6{ 2024, 7, 25 };
// 不支持,只有{}初始化,才能省略=
// Date d7 2025; //error
- C++11列表初始化的本意是实现一个大统一的初始化方法,其次他在有些场景下带来的不少便利,如容器push多参数构造的对象时,{}初始化会很方便。
cpp
vector<Date> v;
//Date d1 = { 2025, 1, 1 };
v.push_back(d1);
v.push_back(Date(2025, 1, 1));
// 比起上面的有名对象和匿名对象传参,这里{}就更体现出性价比
v.push_back({ 2025, 1, 1 });
Insert({ 2025,11,15 });
3、C++11中的std:initializer_list(初始化列表)
std::initializer_list 是 C++11 新增的轻量级容器,本质存储两个指针 (指向数组首尾),数组存储初始化列表中的数据(位于栈上)。其核心接口包括 begin()、end() 和 size(),支持迭代器遍历。
- 上面的初始化已经很方便,但是对象是容器初始化还是不太方便,比如一个vector对象,我想用N个值去构造初始化,那么我们得实现多个构造函数才能支持。
cpp
vector<int> v1= { 1, 2, 3 };
vector<int> v2 = { 1, 2, 3, 4, 5 };
- C++库中提出std::initializer_list这个类,这个类的本质 是底层开⼀个数组 ,将数据拷贝过来,std::initializer_list 内部由两个指针分别指向数组的开始和结束。
cpp
auto il = { 10, 20, 30 }; // the type of il is an initializer_list
- std::initializer_list支持迭代器遍历
- 容器支持一个 std::initializer_list 的构造函数,也就支持任意多个值构成的{x1,x2,x3......}进行初始化。STL中的容器支持任意多个值构成的{x1,x2,x3......}进行初始化,就是通过std::initializer_list 的构造函数支持的。如下图所示:
cpp
//=============================={}:initializer_list<T>==============================
int main()
{
//构造 + 拷贝构造(优化 -> 构造)
vector<int> v1 = { 1, 2, 3, 4 }; //列表初始化
vector<int> v2 = { 1, 2, 3, 4, 5, 6, 7, 7 };
const vector<int>& v4 = { 1, 2, 3, 4 }; //引用就需要加上const修饰,因为临时对象具有常性
//构造
vector<int> v3({ 1, 2, 3, 4, 5 });
//构造走的是初始化列表:vector (initializer_list<value_type> il,
map<string, string> dict = { {"sort","排序"},{"string","字符串"} };
// 里面的括号是pair列表初始化,外面的是initialize_list
auto il = { 10,20,30 };
cout << typeid(il).name() << endl;
std::initializer_list<int> mylist;
mylist = { 10,20,30 };
cout << sizeof(mylist) << endl;
// 这里begin和end返回的值initializer_list对象中存的两个指针
// 这两个指针的值跟i的地址跟接近,说明数组存在栈上
int i = 0;
cout << mylist.begin() << endl; //如:000000A1646FFA18
cout << mylist.end() << endl; //000000A1646FFA24
cout << &i << endl; //000000A1646FF5A4
return 0;
}

三. 右值引用与移动语义:彻底解决拷贝效率问题
C++98 的C++语法中就有引用的语法,我们在前面的学习中已经讲解了,C++11之后,我们之前学习的引用就叫做左值引用 。左值引用 (Type&)是无法直接绑定右值 的(如int& rx = 1),大量临时对象的拷贝导致性能浪费 。
C++11 新增右值引用(Type&&) 和移动语义 ,通过 "窃取" 右值对象的资源 ,替代拷贝操作 ,大幅提升效率 。而无论是左值引用还是右值引用,都是给对象取别名(给对象取别名,不开空间)。
1、基础概念:左值和右值
- 左值 :是⼀个表示数据的表达式(如变量名 或解引用的指针 ),⼀般是有持久状态 ,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
- 右值 :右值也是⼀个表示数据的表达式,要么是字面值常量 、要么是表达式求值过程中创建的临时对象 等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
- 核心区别 :能否取地址是左值和右值的核心区别。
cpp
//==============================右值引用与移动语义==============================
int main()
{
// -------左值,可以取地址-------
// 左值:左值是一个表示数据的表达式(如变量名或解引用的指针),一般是有持久状态,存储在内存中,
// 我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。
// 定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址(区别)。
// 验证左值都可以取地址
// 以下的p、b、c、*p、s、s[0]就是常见的左值
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("11111111111111");
s[0] = 'x';
cout << &p << endl;
cout << &b << endl;
cout << &c << endl;
cout << &(*p) << endl;
cout << &s << endl;
//cout << &s[0] << endl; //x1111111111111
//这里回顾一个知识点:s[0]对应类型为char,char类型取地址类型为char*,
//而在C++中cout打印char*是被视为字符串而不是地址,所以这里直接取地址打印的结果就不是我们认为的地址而是字符串s
//要想打印字符的地址就需要强转成(void*)类型
cout << (void*)&s[0] << endl;
// -------右值,不可以取地址-------
// 右值:右值也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象
// 右值可以出现在赋值符号的右边,但是一般不能出现出现在赋值符号的左边,右值不能取地址(区别)
double x = 1.1, y = 2.2;
// 以下几个情况:10、x + y、fmin(x, y)、string("11111")都是常见的右值
10; //字面量常量
x + y; //表达式返回的结果
fmin(x, y); //fmin传值返回,返回的是临时对象,临时对象具有常性
string("11111"); //匿名对象
// 编译报错(右值不可以取地址)
//cout << &10 << endl;
//cout << &(x+y) << endl;
//cout << &(fmin(x, y)) << endl;
//cout << &string("11111") << endl;
return 0;
}

2、左值引用和右值引用
2.1 概念
cpp
int x = 1;
int & r1 = x;
int && rr1 = 1;
第二个语句就是左值引用 ,左值引用就是给左值取别名 ;第二个语句就是右值引用 ,同样的道理,右值引用就是给右值取别名 ------这个左值和右值就是上面说的能不能取到地址的区别。
左值引用不能直接引用右值,但是const左值引用可以引用右值(但无法修改);
右值引用也不能直接引用左值,但可通过 std::move() 强制转换左值为右值(move本质是类型转换,不移动数据)。
cpp
template typename remove_reference::type&& move (T&& arg);
move(强转)是库里面的一个函数模板,本质内部是进行强制类型转换 ,当然这里还涉及一些引用折叠的知识,这个我们后面会详细介绍的,现在先了解一下。
值得注意的是变量表达式都是左值属性 ,也就意味着一个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值。
语法层面看,左值引用和右值引用都是取别名,不开空间。
2.2 左值引用、右值引用可以相互交叉
左值引用、右值引用可以"相互交叉", 如下面代码所示**:**
cpp
//==============================左值引用、右值引用可以相互交叉==============================
int main()
{
// 以下的p、b、c、*p、s、s[0]就是常见的左值
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("11111");
s[0] = 'x';
double x = 1.1, y = 2.2, z = 3.3;
// 左值引用给左值取别名
int& r1 = b;
int*& r2 = p;
int& r3 = *p;
string& r4 = s;
char& r5 = s[0];
// 右值引用给右值取别名
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
string&& rr4 = string("11111");
// 左值引用不能直接引用右值,但是const左值引用可以引用右值
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("11111");
// 右值引用不能直接引用左值,但是右值引用可以引用move(左值)
int&& rrx1 = move(b);
int*&& rrx2 = move(p);
int&& rrx3 = move(*p);
string&& rrx4 = move(s);
string&& rrx5 = (string&&)s;
// b、r1、rr1都是变量表达式,不管是引用左值还是引用右值,其本身属性都是左值
cout << &b << endl;
cout << &r1 << endl;
cout << &rr1 << endl;
// 这里要注意的是,上面已经说了右值引用后rr1本身的属性是左值,所以本身也是不能被右值引用绑定,除非move一下
// int&& rrx1 = move(b);
// int&& rrx6 = rr1; //error
int&& rrx6 = move(rr1);
return 0;
}
2.3 引用延迟生命周期
我们知道引用可以延长对象生命周期 ,那么表达式返回值、临时对象 和匿名对象 的生命周期可以通过右值引用来延长,const的左值引用也行,但是不能修改。
cpp
//==============================右值引用延长生命周期==============================
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
int main()
{
std::string s1 = "Test";
//std::string&& r1 = s1; // 错误:右值引用不能绑定到左值
const std::string& r2 = s1 + s1; // const 的左值引用延长生命周期(表达式的返回值为右值)
std::string&& r3 = s1 + s1; // 右值引用延⻓延长生命周期
// const 的左值引用 和 右值引用都可以引用表达式、临时对象或者匿名对象,
//但是 const左值引用 则不能进行修改;而右值引用可以进行修改(上面讲了右值引用变量本身属性是左值)
A aa1;
// 延长匿名对象的生命周期
const A& ref1 = A();// const左值引用,但是这样就不能修改了
A&& ref2 = A();// 右值引用
cout << "main end()" << endl;
return 0;
}

2.4 左值和右值的参数匹配问题
- C++98 中,我们实现一个 const 左值引用 作为参数的函数,那么实参传递左值和右值都可以匹配
- C++11之后,分别重载左值引用 ,const左值引用 ,右值引用 作为形参的f函数,那么实参是左值会匹配f(左值引用) ,实参是const左值 会匹配(const左值引用), 实参是右值会匹配f(右值引用)。
- 右值引用变量在用于表示式时本身属性是左值(这是非常巧而且牛的设计),在这里直接告诉大家会感觉反人类的逻辑,但是这个我们后面还会讲到的,到时候大家就会对它有更直观的认知了。
cpp
//==============================左值和右值的参数匹配问题==============================
void f(int& x)
{
std::cout << "左值引用重载 f(" << x << ")\n";
}
void f(const int& x)
{
std::cout << " const 的左值引用重载 f(" << x << ")\n";
}
void f(int&& x)
{
std::cout << "右值引用重载 f(" << x << ")\n";
}
int main()
{
int i = 1;
const int ci = 2;
f(i); // 调用 f(int&)
f(ci); // 调用 f(const int&)
f(3); // 调用 f(int&&),如果没有 f(int&&) 重载则会调用 f(const int&) (编译器会优先选择最适合的那个)
f(std::move(i)); // 调用 f(int&&)
// 右值引用变量在用于表达式时是左值
int&& x = 1;
f(x); // 调用 f(int& x)
f(std::move(x)); // 调用 f(int&& x)
return 0;
}

2.5 右值引用和移动语义的使用场景
2.5.1 左值引用主要使用场景
在讲解 右值引用和移动语义的使用场景 之前我们先来回顾一下 左值引用主要使用场景:
函数中 左值引用传参 和 左值引用传返回值 时减少拷贝 ,同时还具有可以修改实参和修改返回对象价值 的作用。左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回 :如 addStrings 和 generate 函数,C++98中的解决方案只能是被迫使用输出型参数解决。C++11以后这里可以使用右值引用作为返回值来解决吗?显然这是不可能的,因为这里的本质和左值引用一样是返回对象是一个局部对象 ,函数结束 这个对象就析构销毁 了,右值引用返回也无法概念对象已经析构销毁的事实。
2.5.2 移动构造和移动赋值
- 移动构造函数 是一种构造函数 ,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
- 移动赋值 是一个赋值运算符重载 ,他跟拷贝赋值构成函数重载 ,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
- 对于像string/vector这样的深拷贝的类或者深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,他的本质是要**"窃取"引用的右值对象的资源**,而不是像拷贝构造函数和拷贝赋值那样去额外开辟空间进行拷贝资源,提高效率。下面的 xiaoye::string 样例实现了移动构造和移动赋值,我们需要结合场景去理解。
这里**"窃取"引用的右值对象的资源** 大部分情况下就是通过交换两者的指针 。
所以这就解释了为什么要出现右值引用变量在用于表示式时本身属性是左值这个在逻辑上感觉比较反人类的设计,原因就在这里,如果我们想要交换引用的右值对象的资源 ,如果这个对象属性也是右值,那我们还能进行修改吗?显然是不可以的。
所以正是有了这个设计,我们就可以对引用的右值对象进行修改操作,也就能实现两者指针的交换了。
cpp
namespace xiaoye
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str)-构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
~string()
{
//cout << "~string() -- 析构" << endl;
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
{
cout << "string(const string& s) -- 拷贝构造" << endl;
reserve(s._capacity); //拷贝构造需要额外再开辟空间进行拷贝数据
for (auto e : s)
{
push_back(e);
}
}
// 移动构造
string(string&& rs)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(rs); //移动构造只需要"掠夺"右值引用形参的资源即可实现构造
}
//拷贝赋值
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;
if (this != &s)
{
_str[0] = '\n';
_size = 0;
reserve(s._capacity); //拷贝赋值需要额外再开辟空间进行拷贝赋值数据
for (auto e : s)
{
push_back(e);
}
}
return *this;
}
//移动赋值
string& operator=(string&& rs)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(rs); //移动赋值只需要"掠夺"右值引用形参的资源即可实现赋值
return *this;
}
void reserve(size_t new_capacity)
{
if (new_capacity > _capacity)
{
char* tmp = new char[new_capacity + 1];
if (_str)
{
strcpy(tmp, _str);
delete[]_str;
}
_str = tmp;
_capacity = new_capacity;
}
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
//右值引用和移动语义解决传值返回问题
// 传值返回需要拷贝
string addStrings(string num1, string num2, string& ret)
{
string str;
int end1 = num1.size() - 1, end2 = num2.size() - 1;
// 进位
int next = 0;
while (end1 >= 0 || end2 >= 0)
{
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if (next == 1)
str += '1';
reverse(str.begin(), str.end());
cout << &str << endl;
return str;
}
}
int main()
{
xiaoye::string s1("xxxxx");
// 拷贝构造
xiaoye::string s2 = s1;
// 构造 + 移动构造,优化后直接构造
xiaoye::string s3 = xiaoye::string("yyyyy"); //匿名对象走移动构造
// 移动构造
xiaoye::string s4 = move(s1);
return 0;
}

2.6 右值引用和移动语义解决传值返回问题
2.6.1 两种场景实践

cpp
int main()
{
xiaoye::string s1("xxxxx");
// 拷贝构造
xiaoye::string s2 = s1;
// 构造 + 移动构造,优化后直接构造
xiaoye::string s3 = xiaoye::string("yyyyy"); //匿名对象走移动构造
// 移动构造
xiaoye::string s4 = move(s1);
cout << "******************************" << endl;
//场景一:
//移动构造
xiaoye::string ret = xiaoye::addStrings("1111", "2222");
cout << ret.c_str() << endl;
cout << &ret << endl;
cout << "******************************" << endl;
//场景二:
//移动赋值
xiaoye::string ret2;
ret2 = xiaoye::addStrings("1111", "2222");
cout << ret2.c_str() << endl;
cout << &ret2 << endl;
return 0;
}

2.6.2 传值返回没有拷贝了,是否意味设计右值引用和移动语义就没意义?
通过上面的打印结果我们发现,对于场景一的移动构造而言只有构造了,二对于场景二的移动赋值而言也只有构造和移动赋值了,那么都没有移动拷贝了设计右值引用和移动语义还有意义吗?
当然是有非常大的意义的,但是我们需要结合下面几个场景才能解释清楚:
2.6.3 右值对象构造,只有拷贝构造,没有移动构造的场景
如下左边图 为 linux下可以将下面代码拷贝到test.cpp文件,编译时用 g++ test.cpp -fno-elide-constructors的方式关闭构造优化,运行结果可以看到左边没有优化的两次拷贝 ;
中间图 为展示了 vs2019 的 debug 环境下编译器对拷贝的优化,编译器优化的场景下连续步骤中的拷贝合二为一变为一次拷贝构造****;
右边图展示为 vs2019 的 release 版本或者 vs2022 的 debug和release 版本,代码优化非常恐怖------会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为一,变为直接构造。要理解这个优化就要结合局部对象生命周期和栈帧的角度理解。


2.6.4 右值对象构造,既有拷贝构造,也有移动构造的场景
如下左边图 为 linux下可以将下面代码拷贝到test.cpp文件,编译时用 g++ test.cpp -fno-elide-constructors的方式关闭构造优化,运行结果可以看到左边没有优化的两次移动 ;
中间图 为展示了 vs2019 的 debug 环境下编译器对拷贝的优化,编译器优化的场景下连续步骤中的移动构造合二为一变为一次移动构造****;
右边图展示为 vs2019 的 release 版本或者 vs2022 的 debug和release 版本,代码优化非常恐怖------会直接将str对象的构造,str移动构造临时对象,临时对象r移动构造ret对象,合三为一,变为直接构造。要理解这个优化就要结合局部对象生命周期和栈帧的角度理解。


2.6.5 右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
如下左边图 为 linux下可以将下面代码拷贝到test.cpp文件,编译时用 g++ test.cpp -fno-elide-constructors的方式关闭优化环境下编译器的处理,一次拷贝构造,一次拷贝赋值 。
中间图 为展示了 vs2019 的 debug 环境下编译器对拷贝赋值的优化,编译器优化的场景下只对前面实参到形参的构造+拷贝构造 合二为一变为一次构造**,** 并没有对返回过程进行优化 ;
右边图展示为 vs2019 的 release 版本或者 vs2022 的 debug和release 版本,代码进一步优化,直接构造要返回的临时对象,str本质是临时对象的引用,底层角度用指针实现。从运行结果的角度来看,我们能发现str 的析构是在拷贝赋值以后,说明 str 就是临时对象的别名。在下面我会把打印结果(附析构解释)展示出来。


2.6.6 右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景
如下左边图 为 linux下可以将下面代码拷贝到test.cpp文件,编译时用 g++ test.cpp -fno-elide-constructors的方式关闭优化环境下编译器的处理,一次移动构造,一次移动赋值 。
中间图 为展示了 vs2019 的 debug 环境下编译器对移动赋值的优化,编译器优化的场景下只对前面实参到形参的构造+移动构造 合二为一变为一次构造**,** 并没有对返回过程进行优化 ;
右边图展示为 vs2019 的 release 版本或者 vs2022 的 debug和release 版本,代码进一步优化,直接构造要返回的临时对象,str本质是临时对象的引用,底层角度用指针实现。从运行结果的角度来看,我们能发现str 的析构是在移动赋值以后,说明 str 就是临时对象的别名。在下面我会把打印结果(附析构解释)展示出来。


cpp
string addStrings(string num1, string num2, string& ret)
{
string str;
int end1 = num1.size() - 1, end2 = num2.size() - 1;
// 进位
int next = 0;
while (end1 >= 0 || end2 >= 0)
{
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if (next == 1)
str += '1';
reverse(str.begin(), str.end());
cout << &str << endl;
return str;
}
int main()
{
//场景一:
//移动构造
xiaoye::string ret = xiaoye::addStrings("1111", "2222");
cout << ret.c_str() << endl;
cout << &ret << endl;
cout << "******************************" << endl;
//场景二:
//移动赋值
xiaoye::string ret2;
ret2 = xiaoye::addStrings("1111", "2222");
cout << ret2.c_str() << endl;
cout << &ret2 << endl;
return 0;
}

小总结

结束语
到此,C++11的列表初始化、右值引用与移动语义就讲解完了。**列表初始化统一了初始化方式,减少错误;移动语义则解决了临时对象拷贝的性能痛点,尤其适用于深拷贝类和容器操作。在实际开发中,应优先使用列表初始化简化代码,对深拷贝类主动实现移动构造 / 赋值,充分利用移动语义优化性能。**希望对大家学习C++能有所收获!
C++参考文档:
https://legacy.cplusplus.com/reference/
https://zh.cppreference.com/w/cpp
https://en.cppreference.com/w/