【C++篇】C++11:从列表初始化到移动语义

74 阅读9分钟

前言 C++11的发展历史

C++11是C++的第⼆个主要版本,并且是从C++98起的最重要更新。它引入了大量更改,标准化了既有实践,并改进了对C++程序员可用的抽象。在它最终由ISO在2011年8月12日采纳前,⼈们曾使⽤名称“C++0x”,因为它曾被期待在?2010?年之前发布。C++03与C++11期间花了8年时间,故而这是迄今为止最长的版本间隔。从那时起,C++有规律地每3年更新⼀次。

列表初始化 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; } 运行本项目

C++11中的{}

C++11以后想统⼀初始化⽅式,试图实现⼀切对象皆可⽤{}初始化,{}初始化也叫做列表初始化

c++11开始,自定义类型支持用初始化列表,c++98只有内置类型支持初始化列表

#include #include 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 };

    // ⾃定义类型⽀持
    // 这⾥本质是⽤{ 2025, 1, 1}构造⼀个Date临时对象
    // 临时对象再去拷⻉构造d1,编译器优化后合⼆为⼀变成{ 2025, 1, 1}直接构造初始化
    Date d1 = { 2025, 1, 1};

    //C++98⽀持单参数时类型转换,也可以不⽤{}
    Date d3 = { 2025};//c++11
    Date d4 = 2025;//c++98


    //可以省略掉= 
    Point p1 { 1, 2 };
    int x2 { 2 };
    Date d6 { 2024, 7, 25 };
    const Date& d7 { 2024, 7, 25 };
     
    //只有初始化列表才支持省略=
    Date d8 2025//会报错

} 运行本项目

C++11中的std::initializer_list

上⾯的初始化已经很⽅便,但是对象容器初始化还是不太⽅便,⽐如⼀个vector对象,我想⽤N个 值去构造初始化,那么我们得实现很多个构造函数才能⽀持:vector v1 = {1,2,3};vector v2 = {1,2,3,4,5};

C++11库中提出了⼀个std::initializerlist的类, auto il = { 10, 20, 30 }; // the type of il is an initializerlist ,这个类的本质是底层开⼀个数组,将数据拷⻉ 过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。

vector v1={1,2,3,4}; initializer_listl1={10,20,30};//本质是底层在栈上开一个数组, 运行本项目

//这里在语义上表示构造+拷贝构造+优化,,,但编译器会优化成直接构造 //本质也可以理解为隐式类型转换 vector v1={1,2,3,4}; vector v2{1,2,3,4}; //这里在语义上表示直接进行构造 vector v3({1,2,3,4});//调用initializer_list进行构造 //上述两种方式在语义上表示的意思不同,但最后的结果是相同的 运行本项目 右值引用和移动语义 左值,是一个数据表达式,一般以持久的状态存储在内存中,可以获取到它的地址。左值可以出现在赋值符号的左边也可以出现在右边

右值,是一个数据表达式,要么是字面值常量,要么是表达式求值过程中创建的临时对象。右值不能出现在表达式的左边且无法获取地址。

左值引用和右值引用 Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引⽤,左值引⽤就是给左值取别 名,第⼆个就是右值引⽤,同样的道理,右值引⽤就是给右值取别名。

左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值

右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)

move是库⾥⾯的⼀个函数模板,本质内部是进⾏强制类型转换,当然他还涉及⼀些引⽤折叠的知识

是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值

double x = 1.1, y = 2.2; const int& rx1 = 10; const double& rx2 = x + y;

int* p = new int(0); int b = 1; string s("111111"); int&& rr1 = move(b); int*&& rr2 = move(p); string&& rr3 = move(s); string&& rr4 = (string&&)s;//move本质是进行强转

int& tt1 = rr1;//用左值引用来引用右值引用表达式 运行本项目

左值引用与右值引用在底层其实就是指针

引用延长生命周期

右值引用可用于为临时对象延长生命周期,const的左值引用也能延长临时对象生存期,但这些对象无法被修改。

如果想用引用来延长被调用的函数内部局部变量的生命周期,这是不被允许的。第一点:引用不会改变变量的存储位置。第二点:局部变量是创建在函数栈帧中的,当函数调用结束栈帧销毁,局部变量也会随之销毁。

string s1 = "test"; //string&& r1 = s1;//右值引用无法引用左值

const string& r2 = s1 + s1; //r2 += s1;//const左值可以引用右值,但无法进行修改

string&& r3 = s1 + s1; r3 += s1; cout << r3 << endl; 运行本项目 左值和右值的参数匹配 C++98中,我们实现⼀个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。

C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的f函数,那么实参是左值会匹配f(左值引用),实参是const左值会匹配f(const左值引用),实参是右值会匹配f(右值引用)。

void func(int& x) { cout << "左值引用" << x <<endl; }

void func(const int& x) { cout << "const左值引用" << x << endl; }

void func(int&& x) { cout << "右值引用" << x<<endl; }

int main() { int i = 1; const int ci = 2; func(i); func(ci); func(3); func(move(i));

int&& x = 1;
func(x);
func(move(x));

return 0;

} 运行本项目

左值引用与右值引用最终目的是减少拷贝、提高效率。

左值引用还可以修改参数或者返回值,方便使用

左值引用的不足:

在部分函数场景,只能传值返回,不能传引用返回。比如:当前函数的局部对象,出了当前函数的作用域就销毁

移动构造和移动赋值 移动构造函数是⼀种构造函数,类似拷⻉构造函数,要求第⼀个参数是该类类型的引⽤,不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。

移动构造是进行指针交换,其本质是“掠夺资源”。被掠夺的右值的指针则指向”空“

所以一个左值不能轻易的去move,因为这会导致左值的资源被掠夺

右值对象构造,只有拷贝构造,没有移动构造的场景

vs2019debug环境下编译器对拷贝进行了优化。当移动构造与拷贝构造同时存在时,编译器会选择代价小的移动构造。优化前,需要进行两次移动构造,优化后只需进行一次移动构造 需要注意的是在vs2019的release和vs2022的debug和release,下面代码优化为非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为⼀,变为直接构造。变为直接构造。要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解 linux下可以将下面代码拷贝到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elide- constructors 的方式关闭构造优化,运行结果可以看到图1左边没有优化的两次拷贝。

右值对象构造,有拷贝构造,也有移动构造的场景

展示了vs2019debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次移动构造,右边为编译器优化的场景下连续步骤中的拷⻉合⼆为⼀变为⼀次移动构造。 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码优化为非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为⼀,变为直接构造。要理解这个优化要结合局部对象⽣命周期和栈帧的角度理解 linux下可以将下⾯代码拷贝到test.cpp⽂件,编译时用g++ test.cpp -fno-elide- constructors 的方式关闭构造优化,运行结果可以看到图1左边没有优化的两次移动。

右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景

左边展示了vs2019?debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,一次拷贝构造,一次拷贝赋值。

需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。

右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景

展示了vs2019debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,⼀次移动构造,⼀次移动赋值。 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。

如果想看未优化的场景,在Linux下通过:g++ test.cpp -fno-elide-constructors关闭构造优化来观察。

移动赋值 移动赋值是⼀个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第⼀个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。

移动赋值也是对资源进行掠夺

只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景