跳到主要内容 C++11 详解:列表初始化、右值引用与移动语义 | 极客日志
C++ 算法
C++11 详解:列表初始化、右值引用与移动语义 C++11 引入了列表初始化和右值引用等核心特性。列表初始化通过{}统一了对象创建方式,配合 std::initializer_list 简化了容器初始化。右值引用允许绑定临时对象,结合移动语义实现了资源的高效转移而非拷贝,显著提升了性能。理解左值与右值的区别、引用延长生命周期机制以及移动构造/赋值函数的实现,是掌握现代 C++ 内存管理的关键。文章详细阐述了列表初始化的原理、右值引用的语法规则、参数匹配机制以及移动构造和赋值的具体应用场景,并通过代码示例演示了如何避免不必要的深拷贝开销。
一、C++11 的发展历史 C++11 是 C++ 的第二个主要版本,并且是从 C++98 起的最重要的更新。它引入了大量更改,标准化了既有实践,并改进了对 C++ 程序员可用的抽象。在它最终由 ISO 在 2011 年 8 月 12 日采纳前,人们曾使用名称'C++0x',因为它曾被期待在 2010 年之前发布。C++03 与 C++11 期间花了 8 年时间,故而这是迄今为止最长的版本间隔。从那时起,C++ 有规律地每 3 年更新一次。
二、列表初始化
1. C++98 传统的 { } C++98 中一般数组和结构体可以用{}进行初始化。
struct Point { int _x; int _y; };
int main () {
int array1[] = { 1 , 2 , 3 , 4 , 5 };
int array2[5 ] = { 0 };
Point p = { 1 , 2 };
return 0 ;
}
2. C++11 中的{}
C++11 以后想统一初始化方式,试图实现一切对象皆可用{}初始化,{}初始化也叫做列表初始化。
内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化后变成直接构造。
{}初始化的过程中,可以省略掉=。
C++11 列表初始化的本意是想实现一个大统一的初始化方式,其次它在有些场景下带来的不少便利,如容器 push/insert 多参数构造的对象时,{}初始化会很方便。
#include <iostream>
#include <vector>
using namespace std;
struct Point { int _x; int _y; };
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;
};
int main () {
int a1[] = { 1 , 2 , 3 , 4 , 5 };
int a2[5 ] = { 0 };
Point p = { 1 , 2 };
int x1 = { 2 };
Date d1 = { 2025 , 1 , 1 };
Date d11 = 2008 ;
const Date& d2 = { 2024 , 7 , 25 };
Date d3 = { 2025 };
Date d4 = 2025 ;
Point p1{ 1 , 2 };
int x2{ 2 };
Date d6{ 2024 , 7 , 25 };
const Date& d7{ 2024 , 7 , 25 };
vector<Date> v;
v.push_back (d1);
v.push_back (Date (2025 , 1 , 1 ));
v.push_back ({ 2025 , 1 , 1 });
return 0 ;
}
3. C++11 中的 std::initializer_list
上面的初始化已经很方便,但是对象容器初始化还是不太方便,比如一个 vector 对象,我想用 N 个值去构造初始化,那么我们得实现很多个构造函数才能支持,例如这样的情况:vector v1 = {1,2,3}; vector v2 = {1,2,3,4,5};(这就是 initializer_list 的主要作用!)
C++11 库中提出了一个 std::initializer_list 的类,auto il = { 10, 20, 30 }; // the type of il is an initializer_list,这个类的本质是底层开一个数组,将数据拷贝过来,std::initializer_list 内部有两个指针分别指向数组的开始和结束(结束位置不是 30,而是 30 的下一个位置)。
容器支持一个 std::initializer_list 的构造函数,也就支持任意多个值构成的 {x1,x2,x3...} 进行初始化。STL 中的容器支持任意多个值构成的 {x1,x2,x3...} 进行初始化,就是通过 std::initializer_list 的构造函数支持的。
template <class T > class vector {
public :
typedef T* iterator;
vector (initializer_list<T> l) {
for (auto & e : l) push_back (e);
}
private :
iterator _start = nullptr ;
iterator _finish = nullptr ;
iterator _endofstorage = nullptr ;
};
#include <iostream>
#include <vector>
#include <string>
#include <map>
using namespace std;
int main () {
std::initializer_list<int > mylist;
mylist = { 10 , 20 , 30 };
cout << sizeof (mylist) << endl;
int i = 0 ;
cout << mylist.begin () << endl;
cout << mylist.end () << endl;
cout << &i << endl;
vector<int > v1 ({ 1 ,2 ,3 ,4 ,5 }) ;
vector<int > v2 = { 1 ,2 ,3 ,4 ,5 };
const vector<int >& v3 = { 1 ,2 ,3 ,4 ,5 };
map<string, string> dict = { {"sort" , "排序" }, {"string" , "字符串" } };
pair<string, string> kv1 ("sort" , "排序" ) ;
pair<string, string> kv2 ("string" , "字符串" ) ;
map<string, string> m1 = { kv1, kv2 };
v1 = { 10 ,20 ,30 ,40 ,50 };
return 0 ;
}
任意类型(内置类型,自定义类型)都支持列表初始化,自定义类型的列表初始化是借助构造支持的,构造里面有几个参数列表里面就可以有几个参数;
vector<int> v1({ 1,2,3,4,5 });
initializer_list 主要是为 STL 容器所准备的,让容器都支持任意多个类型的参数去初始化。本质上是将列表里面的值传给容器里面 initializer_list 的那个构造函数,用 initializer_list 去构造一个 vector 的一个临时对象,再将这个临时对象拷贝构造给 v1,编译器优化为直接构造。
三、右值引用和移动语义(重点) C++98 的 C++ 语法中就有引用的语法,而 C++11 中新增了的右值引用语法特性,C++11 之后我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
1. 左值和右值
左值是一个表示数据的表达式 (如变量名或解引用的指针),一般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时 const 修饰符后的左值,不能给它赋值,但是可以取它的地址。
右值也是一个表示数据的表达式,要么是字面值常量(例如:10, 1.1)、要么是表达式求值过程中创建的临时对象,匿名对象等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址。
值得一提的是,左值的英文简写为 lvalue,右值的英文简写为 rvalue。传统认为它们分别是 left value、right value 的缩写。现代 C++ 中,lvalue 被解释为 location value 的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,而 rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字面量常量,存储于寄存器中的变量等,也就是说左值和右值的核心区别就是能否取地址。
#include <iostream>
using namespace std;
int main () {
int * p = new int (0 );
int b = 1 ;
const int c = b;
*p = 10 ;
string s ("111111" ) ;
s[0 ] = 'x' ;
cout << &c << endl;
cout << (void *)&s[0 ] << endl;
double x = 1.1 , y = 2.2 ;
10 ;
x + y;
fmin (x, y);
string ("11111" );
return 0 ;
}
2. 左值引用和右值引用
Type& r1 = x; Type&& rr1 = y; 第一个语句就是左值引用,左值引用就是给左值取别名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名;
左值引用不能直接引用右值,但是 const 左值引用可以引用右值;
右值引用不能直接引用左值,但是右值引用可以引用 move(左值),本质就是将左值强转为右值;
template typename remove_reference::type&& move(T&& arg);
move 是库里面的一个函数模板,本质内部是进行强制类型转换,当然他还涉及一些引用折叠的知识,这个我们后面会细讲。
需要注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值;
语法层面看,左值引用和右值引用都是取别名,不开空间。从汇编底层的角度看下面代码中 r1 和 rr1 汇编层实现,底层都是用指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要然到一起去理解,互相佐证,这样反而是陷入迷途。
#include <iostream>
using namespace std;
int main () {
int * p = new int (0 );
int b = 1 ;
const int c = b;
*p = 10 ;
string s ("111111" ) ;
s[0 ] = 'x' ;
double x = 1.1 , y = 2.2 ;
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 int & rx1 = 10 ;
const double & rx2 = x + y;
const double & rx3 = fmin (x, y);
const string& rx4 = string ("11111" );
int && rrx1 = move (b);
int *&& rrx2 = move (p);
int && rrx3 = move (*p);
string&& rrx4 = move (s);
string&& rrx5 = (string&&)s;
cout << &b << endl;
cout << &r1 << endl;
cout << &rr1 << endl;
int & r6 = r1;
int && rrx6 = move (rr1);
return 0 ;
}
3. 引用延长生命周期 右值引用可用于为临时对象延长生命周期;const 的左值引用也能延长临时对象生存期,但这些对象无法被修改。
#include <iostream>
#include <string>
using namespace std;
int main () {
std::string s1 = "Test" ;
const std::string& r2 = s1 + s1;
std::string&& r3 = s1 + s1;
r3 += "Test" ;
std::cout << r3 << '\n' ;
return 0 ;
}
4. 左值和右值的参数匹配
C++98 中,我们实现一个 const 左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
C++11 以后,分别重载左值引用、const 左值引用、右值引用作为形参的 f 函数,那么实参是左值会匹配 f(左值引用),实参是 const 左值会匹配 f(const 左值引用),实参是右值会匹配 f(右值引用)。
右值引用变量在用于表达式时属性是左值,这个设计这里会感觉很怪,等我们讲右值引用的使用场景时,就能体会这样设计的价值了。
#include <iostream>
using namespace std;
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 (ci);
f (3 );
f (std::move (i));
int && x = 1 ;
f (x);
f (std::move (x));
return 0 ;
}
5. 右值引用和移动语义的使用场景
(1)左值引用主要使用场景回顾 左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,提高效率,同时还可以修改实参和修改返回对象的价值。左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如下面的 addStrings 和 generate 函数(对象出了作用域还在才可以用左值引用返回,如果出了作用域对象就销毁了就不能使用左值引用返回),C++98 中的解决方案只能是被迫使用输出型参数解决。
那么 C++11 以后这里可以使用右值引用做返回值解决吗?显然是不可能的,因为这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法改变对象已经析构销毁的事实。
class Solution {
public :
string addStrings (string num1, string num2) {
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 ());
return str;
}
};
class Solution {
public :
vector<vector<int >> generate (int numRows) {
vector<vector<int >> vv (numRows);
for (int i = 0 ; i < numRows; ++i) {
vv[i].resize (i + 1 , 1 );
}
for (int i = 2 ; i < numRows; ++i) {
for (int j = 1 ; j < i; ++j) {
vv[i][j] = vv[i - 1 ][j] + vv[i - 1 ][j - 1 ];
}
}
return vv;
}
};
(2)移动构造和移动赋值
移动构造函数是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函 数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
对于像 string/vector 这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,他的本质是要'窃取'引用的 右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率。下面的 lrq::string 样例实现了移动构造和移动赋值,我们需要结合场景理解。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <assert.h>
#include <string.h>
#include <algorithm>
using namespace std;
namespace lrq {
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 *) :_size(strlen (str)), _capacity(_size) {
cout << "string(char* str)-构造" << endl;
_str = new char [_capacity + 1 ];
strcpy (_str, str);
}
void swap (string& s) {
::swap (_str, s._str);
::swap (_size, s._size);
::swap (_capacity, s._capacity);
}
string (const string& s) :_str(nullptr ) {
cout << "string(const string& s) -- 拷贝构造" << endl;
reserve (s._capacity);
for (auto ch : s) {
push_back (ch);
}
}
string (string&& s) {
cout << "string(string&& s) -- 移动构造" << endl;
swap (s);
}
string& operator =(const string& s) {
cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;
if (this != &s) {
_str[0 ] = '\0' ;
_size = 0 ;
reserve (s._capacity);
for (auto ch : s) {
push_back (ch);
}
}
return *this ;
}
string& operator =(string&& s) {
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap (s);
return *this ;
}
~string () {
cout << "~string() -- 析构" << endl;
delete [] _str;
_str = nullptr ;
}
char & operator [](size_t pos) {
assert (pos < _size);
return _str[pos];
}
void reserve (size_t n) {
if (n > _capacity) {
char * tmp = new char [n + 1 ];
if (_str) {
strcpy (tmp, _str);
delete [] _str;
}
_str = tmp;
_capacity = n;
}
}
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; }
private :
char * _str = nullptr ;
size_t _size = 0 ;
size_t _capacity = 0 ;
};
}
int main () {
lrq::string s1 ("xxxxx" ) ;
lrq::string s2 = s1;
lrq::string s3 = lrq::string ("yyyyy" );
cout << "******************************" << endl;
return 0 ;
}
(3)右值引用和移动语义解决传值返回问题 namespace lrq {
string addStrings (string num1, string num2) {
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 << "******************************" << endl;
return str;
}
}
int main () {
lrq::string ret = lrq::addStrings ("11111" , "2222" );
cout << ret.c_str () << endl;
return 0 ;
}
int main () {
lrq::string ret;
ret = lrq::addStrings ("11111" , "2222" );
cout << ret.c_str () << endl;
return 0 ;
}
右值对象构造,只有拷贝构造,没有移动构造的场景
图 1 展示了 vs2019 debug 环境下编译器对拷贝的优化,左边为不优化的情况下,两次拷贝构造,右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次拷贝构造。
需要注意的是在 vs2019 的 release 和 vs2022 的 debug 和 release,下面代码优化,会直接将 str 对象的构造,str 拷贝构造临时对象,临时对象拷贝构造 ret 对象,合三为一,变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解。
linux 下可以将下面代码拷贝到 test.cpp 文件,编译时用 g++ test.cpp -fno-elideconstructors 的方式关闭构造优化,运行结果可以看到图 1 左边没有优化的两次拷贝。
右值对象构造,有拷贝构造,也有移动构造的场景
图 2 展示了 vs2019 debug 环境下编译器对拷贝的优化,左边为不优化的情况下,两次移动构造,右边为编译器优化的场景下连续步骤中的移动构造合二为一变为一次移动构造。
需要注意的是在 vs2019 的 release 和 vs2022 的 debug 和 release,下面代码优化,合三为一。要理解这个优化要结合局部对象生命周期和栈帧的角度理解。
linux 下可以将下面代码拷贝到 test.cpp 文件,编译时用 g++ test.cpp -fno-elideconstructors 的方式关闭构造优化,运行结果可以看到图 2 左边没有优化的两次移动。
右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
图 4 左边展示了 vs2019 debug 和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,一次拷贝构造,一次拷贝赋值。
需要注意的是在 vs2019 的 release 和 vs2022 的 debug 和 release,下面代码会进一步优化,直接构造要返回的临时对象,str 本质是临时对象的引用,底层角度用指针实现。运行结果的角度,我们可以看到 str 的析构是在赋值以后,说明 str 就是临时对象的别名。
右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景
图 5 左边展示了 vs2019 debug 和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,一次移动构造,一次移动赋值。
需要注意的是在 vs2019 的 release 和 vs2022 的 debug 和 release,下面代码会进一步优化,直接构造要返回的临时对象,str 本质是临时对象的引用,底层角度用指针实现。运行结果的角度,我们可以 看到 str 的析构是在赋值以后,说明 str 就是临时对象的别名。
四、总结 本文详细介绍了 C++11 引入的两个核心特性:列表初始化和移动语义。
列表初始化 :通过 {} 语法统一了对象创建方式,配合 std::initializer_list 简化了 STL 容器的初始化过程,使得代码更加简洁且类型安全。
右值引用与移动语义 :右值引用 (&&) 允许绑定临时对象,结合移动构造函数和移动赋值运算符,实现了资源的'窃取'而非拷贝。这显著减少了深拷贝类(如 std::string, std::vector)的性能开销,特别是在函数返回值和容器操作中。
生命周期管理 :右值引用和 const 左值引用均可延长临时对象的生命周期,但前者允许修改,后者只读。
编译器优化 :现代编译器会对返回值进行 RVO/NRVO 优化,但在没有移动语义的情况下,仍可能产生不必要的拷贝。正确实现移动语义能确保性能最大化。
掌握这些特性有助于编写更高效、更现代的 C++ 代码。
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online