编程语言cppC++11 Standards (上)左右值引用与初始化列表
星野暗涌1. C++11 发展的历史
c++11是 c++的第二个主要的版本, 并且是从 c++98 最重要的更新。他最终由 ISO在2011 年8月12日采纳,
人们曾经使用名称 c++0x,用为他曾经被期待在2010年之前更新。 c++11 与 c++03 之间的更新相差8年,
是更新间隔最长的一次版本。从那时起,c++有规律地每3年更新一次。
学习内容
初始化列表
现在我们就来认真地学习一下这个被称为改变 c++ 历史的更新
2. 列表初始化
2.1 c++98 传统的
💡
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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.2 c++中的 {}
c++11之后,标准制定组织试图实现一切对象皆可用 {} 初始化为, 因此他也被称为 初始化列表。
内置类型支持,自定义类型也支持, 自定义类型本质上就是类型转换, 中间会产生临时对象,最后优化了以后变为 直接构造;
{}的过程可以省略 =;
c++11 列表的初始化本意是实现一种大一统的初始化方式,其次,在其他不少的场景下带来便利,比如 push/insert参数时,构造对象,{} 就会十分的方便。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| #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}; 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; }
|
2.3 c++11 中的 std::initializer_list
前面的初始化方式就已经很方便了,但是对象容器初始化还是不够方便,比如一个 vector 对象,我们呢想要使用 n 个值去初始化,那么我们==需要实现很多个构造函数才能实现,vector v1 = {1,2,3};vector v2 = {1,2,3,4,5};
c++11的库中提出了一个 std::initializer_list 的类
1 2
| auto li = {10, 20, 30}; vector<int> v = {1, 2, 3, 4};
|
这个类本质上就是在底层开一个数组,将数据拷贝过来,std::initializer_list 内部有两个指针,一个指向数组的开始一个指向数组的末尾!
initializer_list的参考文档在这里, std::initializer_list支持迭代器遍历。
容器支持一个 std::initializer_list的构造函数,也支持任意多个值构成 {x1, x2,...xn} 进行初始化,就是通过 std::initializer_list 的构造函数来支持的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| vector (initializer_list<value_type> il, const allocator_type& alloc = allocator_type()); list (initializer_list<value_type> il, const allocator_type& alloc = allocator_type()); map (initializer_list<value_type> il,const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());
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; };
vector& operator= (initializer_list<value_type> il); map& operator= (initializer_list<value_type> il);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #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", "字符串"}};
v1 = { 10,20,30,40,50 }; return 0; }
|
3. 右值应用和移动语义
c++98 的 c++语法中就支持了引用语法, 而在 c++11中就支持了 右值引用的语法特性, 无论是之前的左值应用还是现在我们涉及到的右值引用 本质上都是给对象取别名。
3.1 左值和右值
左值是一个表扣是数据的表达式,一般是持久状态, 可以获取地址
右值也是一个表达数据的表达式, 要么时字面量,要么是表达式求值过程中产生的临时对象,
- 右值只能出现在
= 的右边,不能出现 = 的右边,同时不能进行取地址操作
左值和右值最核心的一点就是能否进行取地址操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #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; }
|
3.2 左右值引用
type& r1 = x; type&& rr1 = y; 第一个语句就是 左值引用 ,即给左值取别名; 同样第二条语句就是右值引用,即给右值取别名!
左值应用不能直接应用右值, 但是 const 左值可以引用右值。
右值引用不能直接引用左值,但是右值引用可以引用 move后的左值
template<class T> typename remove_reference<T> :: type&& move(T&& arg)
move 本质上就是库里面的一个函数模板,内部就是在进行 强制类型转换 ,当然他还涉及到一些折叠的知识,这个我们到了后面再慢慢展开!
需要注意的是, 变量表达式都是左值属性, 这意味着,右值引用在引用之后就是 左值的属性。
从语义上面看,左值引用和右值引用本质上都是在取别名,不会另外开辟空间,从汇编的角度来看,他们的底层都市使用指针来实现的,没有什恶魔区别。底层汇编的实现和上层的用法语义表有时会有所差异,所以没有必要去相互佐证,有时候这样的思路可能还会系那个大家带偏!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| emplate <class _Ty> remove_reference_t<_Ty>&& move(_Ty&& _Arg) { return static_cast<remove_reference_t<_Ty>&&>(_Arg); } #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.3 延长生命周期
右值引用可以用于延长生命周期,左值的 const 引用也可以用于延长生命周期,但是不能够进行修改操作
1 2 3 4 5 6 7 8 9 10 11
| 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; }
|
3.4 左值和右值的参数匹配
C++98中, 我们实现了一个 const 左值作为参数的引用函数,那么 传递左值还是右值都是可以的!
在 C++11之后,分别重载了,左值引用, const 左值引用,右值引用。在实际传递参数时,就会在按照最和自己情况相符的情况来传递参数!
右值引用变量在用于表达式的时候,属性为左值, 我感觉这个设计怪怪的, 后面我会为大家讲解,右值引用的使用场景, 这样大家就能够体会到右值引用的设计价值了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #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; }
|

3. 5 右值引用和移动语义的使用场景
3.5.1 左值引用的使用场景回顾
左值引用的使用场景主要是在 函数中的左值引用传参和左值引用传值时,减少拷贝操作。
左值引用已经解决了,在实际开发中的大多数问题,到那时有一些场景是左值引用没办法解决的, 比如在一些场景下,没有办法传递左值引用返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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; } };
|
向上面的情况不能传递左值引用返回。那么在 C++11之后这就可以传递左值引用返回了吗?
显然还不行的,因为这里本质上就是对这个函数栈帧中的一个临时对象进行返回,这个和函数结束时,这个对象就被销毁了,右值引用也不能掩盖这个对象已经被销毁的事实。
3.5.2 移动构造和移动赋值
他和拷贝赋值构成函数重载
- 对于像
stringvector 需要进行深拷贝的类或者是包含着有需要深拷贝资源的类,移动构造和移动赋值才有意义, 因为移动构造和移动的复制的第一个参数都是右值引用!他们的本质是窃取右值引用对象的资源, 不是像左值引用那般,对类对象进行拷贝,这样更利于效率的提升!
下面的代码就是我们的一个样例,以此来验证右值引用对效率的提升! 🌈
温馨提示: 关于 string类的模拟实现我们在前面学习 stl容器时已经详细讲解了,这里就不再进行赘述。 🐼
感兴趣的uu可以去参考一下这篇文章 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
| #define _CRT_SECURE_NO_WARNINGS1 #include<iostream> #include<assert.h> #include<string.h> #include<algorithm> using namespace std; namespace bit { 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); } 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] = ''; _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] = ''; } 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() { bit::string s1("xxxxx"); bit::string s2 = s1; bit::string s3 = bit::string("yyyyy"); bit::string s4 = move(s1); cout << "******************************" << endl; return 0; }
|
3.5.3 右值引用和移动语义解决传值返回的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| namespace bit { 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() { bit::string ret = bit::addStrings("11111", "2222"); cout << ret.c_str() << endl; return 0; }
int main() { bit::string ret; ret = bit::addStrings("11111", "2222"); cout << ret.c_str() << endl; return 0; }
|
右值对象构造,只有拷贝构造,没有移动构造的场景

在图中展示了 在 vs2019 debug 的环境下编译器对拷贝的优化, 左边为不优化的情况,两次拷贝构造,而右边的情况,编译器会将这两次的拷贝合二为一,达到优化的效果.
在 vs2019 release和 vs2022 release and debug 编译器优化回答道一个非常恐怖的阶段, 编译器会直接将 str对象的构造和,str 对象的构造,str 拷贝临时对象,临时对象拷贝构造 ret 对象的三个步骤和三为一!想要理解这个优化我们可以看看下面的函数栈帧图:
stateDiagram
direction LR
[*] –> str对象
str对象–> B
B
state B {
direction LR
临时对象 –> ret对象
}
stateDiagram
direction LR
[*] –> str对象
state B {
direction LR
str对象 –> ret对象
}
3.5.4 右值引用引用左值及更加深入的场景
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能 真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move 函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性, 它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
1 2 3 4 5 6 7 8 9 10 11
| int main() { bit::string s1("hello world"); bit::string s2(s1); bit::string s3(std::move(s1)); return 0; }
|

在 STL 库种也大部分的容器也做增加了右值引用的版本:
如 list :
如 vector :
3.5.6 完美转发
✨ 模板中的 && 万能引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| void Fun(int &x){ cout << "左值引用" << endl; } void Fun(const int &x){ cout << "const 左值引用" << endl; }std::forward 完美转发在传参的过程中保留对象原生类型属性 void Fun(int &&x){ cout << "右值引用" << endl; } void Fun(const int &&x){ cout << "const 右值引用" << endl; }
template<typename T> void PerfectForward(T&& t) { Fun(t); } int main() { PerfectForward(10); int a; PerfectForward(a); PerfectForward(std::move(a)); const int b = 8; PerfectForward(b); PerfectForward(std::move(b)); return 0; }
|
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
模板的万能引用只是提供了能够同时接收左值引用和右值引用的能力,
但是引用类型的唯一作用就是解除了接收类型的限制,后续使用中都退化成了左值。
我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
🎆 std::forward 完美转发在传参的过程中保留对象原生类型属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| void Fun(int &x){ cout << "左值引用" << endl; } void Fun(const int &x){ cout << "const 左值引用" << endl; } void Fun(int &&x){ cout << "右值引用" << endl; } void Fun(const int &&x){ cout << "const 右值引用" << endl; }
template<typename T> void PerfectForward(T&& t) { Fun(std::forward<T>(t)); } int main() { PerfectForward(10); int a; PerfectForward(a); PerfectForward(std::move(a)); const int b = 8; PerfectForward(b); PerfectForward(std::move(b)); return 0;完美转发实际中的使用场景: }
|