C++11
前言
C++11是C++继98后的更新,其更新了许多内容,是非常重要的,除了我们前文所提到的范围for和auto,还有许多新的东西,对于C++11完全可以把它当成新的语言进行学习
列表初始化
{}进行初始化
C++11以后想统⼀初始化⽅式,试图实现⼀切对象皆可⽤{}初始化,{}初始化也叫做列表初始化。
内置类型⽀持,⾃定义类型也⽀持,⾃定义类型本质是类型转换,中间会产⽣临时对象,最后优化了以后变成直接构造。
{}初始化的过程中,可以省略掉=
int x2 { 2 };
Date d6 { 2024, 7, 25 };
C++11列表初始化的本意是想实现⼀个⼤统⼀的初始化⽅式,其次他在有些场景下带来的不少便利,如容器push/inset多参数构造的对象时,{}初始化会很方便
initializer_list
initializer_list是C++11之后提到的一个类,这个类的本质是底层开⼀个数组,将数据拷贝过来,initializer_list内部有两个指针分别指向数组的开始和结束。initializer_list与刚刚提到的{}初始化是不一样的,{}是基于构造函数,也就是说需要初始化每一个值都是需要对应的构造函数的。这样对于初始化容器就很不方便,例如初始化vector里的数,总不能写N个构造函数吧
vector<int> v1 ={1,2,3}
容器⽀持⼀个std::initializer_list的构造函数,也就⽀持任意多个值构成的 {x1,x2,x3…} 进⾏初始化。STL中的容器⽀持任意多个值构成的 {x1,x2,x3…} 进⾏初始化,就是通过initializer_list的构造函数⽀持的。
右值引用和移动语义
左值与右值
左值是⼀个表⽰数据的表达式(如变量名或解引用的指针),⼀般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
右值也是⼀个表⽰数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
左值与右值的核心区别就是能否取地址
左值引用与右值引用
int& a = x;int&& b = y;
第一个语句就是左值引用,第二个就是右值引用。
左值引用不能直接引用右值,但是const左值引用可以引用右值
右值引用不能直接引用左值,但是右值引用可以引用move(左值)
move的作用就是强制转化为右值,但是属性还是左值的属性,只是类型强制转换。但是除非确定需要将左值转为右值,否则不要用move,因为move之后左值的资源会被窃取掉,那么左值又是会一直存在的,就会导致左值是一个悬空指针
需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值,不然在后续进行移动语义的时候,如果右值引用后的属性是右值属性,那就无法修改了,也就无法移动了。
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 = min(x, y);
string&& rr4 = string("11111");
// 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = min(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;
引用延长生命周期
右值引⽤可⽤于为临时对象延⻓⽣命周期,const 的左值引⽤也能延⻓临时对象⽣存期,但const对象⽆法被修改。
右值引用和移动语义的使用场景
左值引用
左值引⽤主要使⽤场景是在函数中左值引⽤传参和左值引⽤传返回值时减少拷⻉,同时还可以修改实参和修改返回对象的值。左值引⽤已经解决⼤多数场景的拷⻉效率问题,但是有些场景不能使⽤传左值引⽤返回,如addStrings和generate函数,C++98中的解决⽅案只能是被迫使⽤输出型参数解决。那么C++11以后这⾥可以使⽤右值引⽤做返回值解决吗?显然是不可能的,因为这⾥的本质是返回对象是⼀个局部对象,函数结束这个对象就析构销毁了,右值引⽤返回也⽆法改变对象已经析构销毁的事实。
移动构造和移动赋值
移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。
移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤。
对于像string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是要“窃取”引⽤的右值对象的资源,⽽不是像拷⻉构造和拷⻉赋值那样去拷⻉资源,从提⾼效率。
必须得有移动构造函数才可以出现移动构造
string(string&& s)
{
cout << "string(const string& s) -- 移动构造" << endl;
this->swap(s);
}
移动构造是将右值与当前值进行交换,将有用的资源交换给需要的变量,这样相比与之前的拷贝构造出一个临时变量,再将临时变量拷贝构造给所需要的变量,效率要高很多
右值引用在容器插入的提效
• 查看STL文档我们发现C++11以后容器的push和insert系列的接⼝否增加的右值引⽤版本
• 当实参是⼀个左值时,容器内部继续调⽤拷贝构造进⾏拷贝,将对象拷贝到容器空间中的对象
• 当实参是⼀个右值,容器内部则调⽤移动构造,右值对象的资源到容器空间的对象上
引用折叠
• C++中不能直接定义引⽤的引⽤如 int& && r = i; ,这样写会直接报错,通过模板或 typedef中的类型操作可以构成引用的引用。
• 通过模板或 typedef 中的类型操作可以构成引用的引用时,这时C++11给出了⼀个引用折叠的规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。
typedef int& lref;
typedef int&& rref;
int n = 0;
lref& r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref& r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&
万能折叠
template<class T>
void Function(T&& t)
{}
以上代码就是万能折叠,如果传的是int右值,T就会被识别成int,如果传的是int左值,T就会被识别成int&,这样就实现了传左值就是左值引用的参数,传右值就是右值引用的参数
完美转发
因为变量表达式的属性都是左值,即虽然传的是右值,在万能折叠下,t被识别成右值引用的类型,但由于t是变量表达式,所以如果把t传给其他函数,就会把其他函数的形参的属性变为左值,所以出现完美转发来解决这一问题。
template<class T>
void Function(T&& t)
{
// 完美保持他的属性传参
Fun(forward<T>(t));
}
完美转发forward是一个函数模版,可以把他与move相对应,move是将左值强制转化为右值,但是属性并不会发生改变,只是告诉编译器该左值是可以被窃取的,而forward是可以保持该对象的属性的,即t如果是右值引用,就会保持为右值引用,如果是左值,就会保持为左值。