【C++】C++11特性学习(1)——列表初始化 | 右值引用与移动语义
目录
前言
1.列表初始化
1.1传统{}与新{}
1.2initializer_list
1.3initializer_list与列表初始化
2.右值引用
2.1左值与右值
2.2左值引用与右值引用
2.3右值引用特性
3.移动语义
3.1左值引用主要使用场景回顾
3.2移动语义
3.3移动构造与移动赋值
前言
C++11标准作为C++重大更新之一,为C++带来很多实用的新功能,下面将对C++11中的重要实用模块进行详细讲解。更多C++内容看准>| C++专栏 <|
1.列表初始化
1.1传统{}与新{}
在C++98中延用C语言的{},可为数组和结构体进行初始化:
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;
}
而在C++11之后,为统一初始化方式,C++实现了一切对象均可用{}初始化,这种用{}初始化的方式也叫列表初始化(list initialization),分为直接列表初始化和拷贝列表初始化。内置类型支持,自定义类型也支持。
#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()
{//C++98支持的int a1[] = { 1, 2, 3, 4, 5 };int a2[5] = { 0 };Point p = { 1, 2 };/*C++11后支持的*///内置类型int x1 = { 2 };//自定义类型Date d1 = { 2025, 10, 01 }; //拷贝列表初始化Date d2{ 2025, 10, 02 }; //直接列表初始化return 0;
}
直接列表初始化场景
1.使用大括号包围的初始化列表对命名变量进行初始化
2.使用大括号包围的初始化列表对未命名临时对象进行初始化
3.在 new 表达式中初始化具有动态存储期的对象,其中初始化器是大括号包围的初始化列表
4.在不使用等号的非静态数据成员初始化器中
5.在构造函数的成员初始化列表中,如果使用了大括号包围的初始化列表
拷贝列表初始化场景
1.在等号后使用大括号包围的初始化列表对命名变量进行初始化
2.在函数调用表达式中,使用大括号包围的初始化列表作为参数,列表初始化用于初始化函数形参
3. 在 return 语句中使用大括号包围的初始化列表作为返回表达式,列表初始化用于初始化返回的对象
4. 在下标表达式中使用用户定义的 operator[],其中列表初始化用于初始化重载运算符的形参
5. 在赋值表达式中,列表初始化用于初始化重载运算符的形参
6. 函数式转换表达式或其他构造函数调用中,使用大括号包围的初始化列表代替构造函数参数。拷贝列表初始化用于初始化构造函数的形参
7.在使用等号的非静态数据成员初始化器中
但值得注意的是对于拷贝列表初始化,只能利用可进行类型转换的构造函数(non-explicit)进行初始化;直接列表初始化则都可以使用。
拷贝列表初始化的底层逻辑可理解为先利用列表中的值进行初始化临时对象,再利用拷贝构造进行初始化对象,但一般编译器会对其进行优化为直接构造。特别的对于直接进行的拷贝构造初始化会优先匹配构造函数,不会拷贝出临时对象。例如:
Date d1 = { 2025, 10, 01 };
1.2initializer_list
C++11中给出了一个叫initializer_list的容器,initializer_list容器一般用于构造函数,对容器进行初始化。例如在STL的vector中,C++11给出了用initializer_list作为参数的构造函数。基本逻辑参考如下:
class my_array
{
public:my_array(initializer_list<int> i1){int i = 0;for (auto& e : i1){if (i < 10){_arr[i] = e;++i;}}}private:int _arr[10];int _size;
};
initializer_list的底层是由两个指针来实现数据列表对象的创建,进而用列表中的数据来构建构造函数。但这种构造函数一般用于储存多数据的容器之中,如STL中的vector,list,map等,进而实现一次用多个数据来实现初始化。
1.3initializer_list与列表初始化
在语法层面initializer_list和列表初始化的语法是类似的,但这并不冲突。列表初始化的原理是根据{}中的参数,编译器去匹配相近的构造函数来进行对象的初始化。而initializer_list只是其中构造函数的一种。
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(initializer_list<int> i1){cout << "initializer_list构造:Date(initializer_list<int> i1)" << 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()
{Date d1{};Date d2{ 2025 };Date d3 = { 2025, 10, 01 };Date d4{ d3 };return 0;
}
相应的,C++11中规定:当使用列表初始化时,编译器会优先匹配带有initializer_list的构造函数。但对于像日期类这种不进行数据存储的类,一般不会进行initializer_list构造函数的编写。
最后,列表初始化实际是提供了一种初始化的语法形式,预期对于的有类似Date d1,Date d1(2025, 10, 01)这样的语法。而initializer_list则是提供了一种语法方法,可以用({})形式的传统初始化语法调用,也可以用列表初始化的方式调用。
//调用initializer_list构造函数
Date d1({ 2025, 01, 01 });
Date d2{ 2025, 01, 01 };
Date d3 = { 2025, 01, 01 };
2.右值引用
2.1左值与右值
左值是⼀个表示数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
右值也是⼀个表⽰数据的表达式,要么是字⾯值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
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("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")都是常⻅的右值10;x + y;fmin(x, y);string("11111");//匿名对象return 0;
}
2.2左值引用与右值引用
在C++11之前,一般我们可以对非临时对象进行引用,对于临时对象我们可以用const引用来接收;在C++11中,我们可以通过右值引用来引用临时对象。
#include<string>
int main()
{//左值引用int x = 1;int& a = x;string s("111111");string& ss = s;const int& b = 11;//右值引用double y = 1.0, z = 2.1;int&& i = 0;double&& rr2 = y + z;string&& rr4 = string("11111");return 0;
}
左值引用不能直接引⽤右值,但是const左值引⽤可以引⽤右值,右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值) 。
// 左值引⽤不能直接引⽤右值,但是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(a);
int*&& rrx2 = move(&x);
string&& rrx4 = move(s);
string&& rrx5 = (string&&)s;
move函数本质将传递的参数值强制类型转换为右值,进而能被右值引用接收。
语法层⾯看,左值引⽤和右值引⽤都是取别名,不开空间。从汇编底层的⻆度看下⾯代码中r1和rr1汇编层实现,底层都是⽤指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要然到⼀起去理解,互相佐证,这样反⽽是陷⼊迷途。
2.3右值引用特性
变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量的属性是左值。
可以看到当用右值引用去接收右值引用变量时,是会报错的。这里是因为右值引用的汇编层实现方式用指针去指向那块临时对象的空间,并且对临时对象内存空间的数据进行生命周期的延长,等到使用完毕后再进行空间的销毁。
右值引用变量实际代表的还是右值的那块内存空间数据,不过右值引用对其进行了生命周期的延长,让我们可以对那块即将销毁的空间进行操作,一般我们用于移动语句。
3.移动语义
3.1左值引用主要使用场景回顾
左值引⽤主要使⽤场景是在函数中左值引⽤传参和左值引⽤传返回值时减少拷⻉,同时还可以修改实参和修改返回对象的价值。左值引⽤已经解决⼤多数场景的拷⻉效率问题,但是有些场景不能使⽤传左值引⽤返回,如addStrings和generate函数,C++98中的解决⽅案只能是被迫使⽤输出型参数解决。
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;}
};
那么C++11以后这⾥可以使⽤右值引⽤做返回值解决吗?显然是不可能的,因为这⾥的本质是返回对象是⼀个局部对象,函数结束这个对象就析构销毁了,右值引⽤返回也⽆法概念对象已经析构销毁的事实。
3.2移动语义
C++11中通过右值来处理传值传参和传值返回的方法是利用传值传参和传值返回过程中会产生中间常量的特性,通过右值引用来延长其生命周期,再将常量中的数据与接收对象进行交换。即将需要但即将销毁的数据和接收对象的数据进行交换,以此达到真正需要数据的移动。
3.3移动构造与移动赋值
移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。下面将以my_string类进行代码演示
#include<assert.h>
#include<string.h>
#include<algorithm>
using namespace std;
namespace my_string
{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);}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;}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& 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(){cout << "~string() -- 析构" << endl;delete[] _str;_str = nullptr;}size_t size() const{return _size;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;};
}
进行移动构造的重写:移动构造的逻辑是将右值中的数据给到对象中数据。
//移动构造
string(string&& s)
{cout << "string(string&& s):移动构造" << endl;swap(s);
}
int main()
{my_string::string s1("xxxxx");// 拷⻉构造my_string::string s2 = s1;// 构造+移动构造,优化后直接构造my_string::string s3 = my_string::string("yyyyy");// 移动构造my_string::string s4 = move(s1);cout << "******************************" << endl;return 0;
}
通过调试,我们也能看到其中的交换逻辑:
可以看到s4在利用s1移动构造之后,s1中的值被置空了。这里也反映对于左值,不要轻易进行move传递。
对于移动赋值,逻辑也与移动构造类似:
//移动赋值
string& operator=(string&& s)
{cout << "string& operator=(const string& s):移动赋值" << endl;swap(s);return *this;
}
对于像string/vector这样的深拷⻉的类或者包含深拷⻉的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是要“窃取”引⽤的右值对象的资源,⽽不是像拷⻉构造和拷⻉赋值那样去拷⻉资源,从提⾼效率。