目录
C++11发展历史
列表初始化
C++11中的std::initializer_list
右值引用和移动语义
左值和右值
左值引用和右值引用
引用延长生命周期
左值和右值的参数匹配
移动构造和移动赋值
返回问题
优化过程
引用折叠
完美转发
应用
可变参数模板
基本语法和原理
包扩展
empalce系列接口
新的类功能
默认的移动构造和移动赋值
成员变量声明时给缺省值
defult和delete
final与override
在了解C++11之前,我们先了解C++的历史!
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中⼀般数组和结构体可以⽤{}进⾏初始化。
图:
C++11中的{ }初始化
- C++11以后想统⼀初始化⽅式,试图实现⼀切对象皆可⽤{}初始化,{}初始化也叫做列表初始化。
- 内置类型⽀持,⾃定义类型也⽀持,⾃定义类型本质是类型转换,中间会产⽣临时对象,最后优化 了以后变成直接构造。
- {}初始化的过程中,可以省略掉=
- C++11列表初始化的本意是想实现⼀个⼤统⼀的初始化⽅式,其次他在有些场景下带来的不少便利,如容器push/inset多参数构造的对象时,{}初始化会很⽅便
如:
C++11中的std::initializer_list
- 上⾯的初始化已经很⽅便,但是对象容器初始化还是不太⽅便,⽐如⼀个vector对象,我想⽤N个 值去构造初始化,那么我们得实现很多个构造函数才能⽀持, vector v1 = {1,2,3};
- C++11库中提出了⼀个std::initializer_list的类,这个类的本质是底层开⼀个数组,将数据拷⻉ 过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。std::initializer_list⽀持迭代器遍历。
- 容器⽀持⼀个std::initializer_list的构造函数,也就⽀持任意多个值构成的 初始化。STL中的容器⽀持任意多个值构成的 {x1,x2,x3...} 进⾏ {x1,x2,x3...} 进⾏初始化,就是通过 std::initializer_list的构造函数⽀持的。
右值引用和移动语义
C++98的C++语法中就有引⽤的语法,⽽C++11中新增了的右值引⽤语法特性,C++11之后我们之前学 习的引⽤就叫做左值引⽤。⽆论左值引⽤还是右值引⽤,都是给对象取别名。
左值和右值
- 左值是⼀个表⽰数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我 们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const 修饰符后的左值,不能给他赋值,但是可以取它的地址。
- 右值也是⼀个表⽰数据的表达式,要么是字⾯值常量、要么是表达式求值过程中创建的临时对象 等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
左值引用和右值引用
- Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引⽤,左值引⽤就是给左值取别 名,第⼆个就是右值引⽤,同样的道理,右值引⽤就是给右值取别名。
- 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值。
- 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)。
- move是库里面的⼀个函数模板,本质内部是进行强制类型转换,当然他还涉及⼀些引用折叠的知识。
- 语法层⾯看,左值引⽤和右值引⽤都是取别名,不开空间。从汇编底层的⻆度看下⾯代码中r1和rr1 汇编层实现,底层都是⽤指针实现的,没什么区别。
需要注意的是:rrx1,rrx2,rrx3,rrx4,rrx5变量都是左值,不是右值,它们开辟了空间去存储相对应的右值!
引用延长生命周期
右值引⽤可⽤于为临时对象延⻓⽣命周期,const的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆法被修改。
因为本来这些临时对象在运行完所在行后,就没了,找不到了,而引用可将生命周期扩大到相应的作用域!
左值和右值的参数匹配
- C++98中,我们实现⼀个const左值引⽤作为参数的函数,那么实参传递左值和右值都可以匹配。
- C++11以后,分别重载左值引⽤、const左值引⽤、右值引⽤作为形参的f函数,那么实参是左值会 匹配f(左值引⽤),实参是const左值会匹配f(const左值引⽤),实参是右值会匹配f(右值引⽤)。
有这么几个函数:
调用情况:
移动构造和移动赋值
- 移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引 ⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。
- 移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函 数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤。
- 对于像string/vector这样的深拷⻉的类或者包含深拷⻉的成员变量的类,移动构造和移动赋值才有 意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是要“窃取”引⽤的 右值对象的资源,⽽不是像拷⻉构造和拷⻉赋值那样去拷⻉资源,从提⾼效率。
下面我将举几个例子看清构造,拷贝构造,移动构造,移动赋值的深层调用逻辑!
有了移动构造和移动赋值,会高效很多,再加上编译器自己会优化构造过程,来达到高效率!
我们将会在linux(优化前)中和vs2022(优化后)中进行比较!
在linux中,运行一个C++文件步骤(假设有一个test.cpp文件):
1.编译C++文件,使用g++
编译器编译你的C++源文件,并指定你希望使用的C++标准。例如,如果你希望使用C++11标准,可以使用-std=c++11
选项。类似地,你可以使用-std=c++14
、-std=c++17
或-std=c++20
来指定其他标准。
指令:(以C++11为例)(优化了)
g++ -std=c++11 -o test test.cpp
指令:(没有优化)
g++ -fno-elide-constructors test.cpp -o test -std=c++11
2.运行编译后的可执行文件
指令:
./test
下面,我们将使用我们自制的string类进行演示:
这里只演示主要函数:
讲解:
优化前:
如果既有拷贝构造也有移动构造,那么当然走移动构造(走最优的),如果没有移动构造,走拷贝构造!
注意:string("11111")和move(左值),虽然都是右值,但是还是不一样!
优化后:
当然也有优化不了的,因为已经是最高效了的了!
返回问题
这里有一个函数:
有这样一行代码:
当没有移动构造,只有拷贝构造时:(优化前)
验证:
当有移动构造,也有拷贝构造时:(优化前)调用最合适的函数!
调用拷贝构造的地方,调用了拷贝构造:
优化过程
没有移动构造:
有移动构造:
在优化的第二过程,在addString只有一个构造过程,str和ret指向同一块空间,只不过出了addString作用域,str就被销毁了,但是空间没被销毁!
优化检测:
验证:
我们继续看一个示例:
有这样几行代码:
没有移动构造和移动赋值:(优化前)
验证:
没有移动构造和移动赋值:(优化后)
有移动构造和赋值构造:(优化前)
在调用了拷贝构造的地方,调用了移动构造或者移动赋值:
有移动构造和赋值构造:(优化后)
引用折叠
- C++中不能直接定义引⽤的引⽤如 int& && r = i; ,这样写会直接报错,通过模板或typedef 中的类型操作可以构成引⽤的引⽤。
- 通过模板或typedef中的类型操作可以构成引⽤的引⽤时,这时C++11给出了⼀个引⽤折叠的规 则:右值引⽤的右值引⽤折叠成右值引⽤,所有其他组合均折叠成左值引⽤。
- 像f2这样的函数模板中,T&&x参数看起来是右值引⽤参数,但是由于引⽤折叠的规则,他传递左 值时就是左值引⽤,传递右值时就是右值引⽤,有些地⽅也把这种函数模板的参数叫做万能引⽤。
有这么两个函数:
继续看,有这么个函数:
看推导:
完美转发
- Function(T&&t)函数模板程序中,传左值实例化以后是左值引⽤的Function函数,传右值实例化 以后是右值引⽤的Function函数。
- 变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定 后,右值引⽤变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传 递给下⼀层函数Fun,那么匹配的都是左值引⽤版本的Fun函数。这⾥我们想要保持t对象的属性, 就需要使⽤完美转发实现。
下面我们来看看应用场景:
我们调用Function函数:
Fun(t)语句时:
因为不管T被推导成什么,形参 t都是左值!
Fun(forward<T>(t))语句时:
它会延续T类型!会将原来的数据类型传递下去!
应用
所以我们可以提供右值版本:
可以继续改善:使用万能引用
可变参数模板
基本语法和原理
- C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称 为参数包,存在两种参数包:模板参数包,表⽰零或多个模板参数;函数参数包:表⽰零或多个函 数参数。
- 我们⽤省略号来指出⼀个模板参数或函数参数的表⽰⼀个包,在模板参数列表中,class...或 typename...指出接下来的参数表⽰零或多个类型列表;在函数参数列表中,类型名后⾯跟...指出 接下来表⽰零或多个形参对象列表;函数参数包可以⽤左值引⽤或右值引⽤表⽰,跟前⾯普通模板 ⼀样,每个参数实例化时遵循引⽤折叠规则。
- 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
- 这⾥我们可以使⽤sizeof...运算符去计算参数包中参数的个数。
我们来看以下原理:
包扩展
- 对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个 包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元 素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。底层 的实现细节如图1所⽰。
- C++还⽀持更复杂的包扩展,直接将参数包依次展开依次作为实参给⼀个函数去处理。
同样的:
但是我们可以这样遍历:
//包扩展(解析出参数包的内容)
void ShowList()
{// 编译器时递归的终止条件,参数包是0个时,直接匹配这个函数cout << endl;
}
template <class T, class ...Args>
void ShowList(T&& x, Args&&... args)
{cout << x << " ";// args是N个参数的参数包// 调用ShowList,参数包的第一个传给x,剩下N-1传给第二个参数包ShowList(args...);
}
template <class ...Args>
void Print(Args&&... args)
{ShowList(args...);
}
也可以这样:
template <class T>
int GetArg(const T& x)
{cout << x << " ";return 0;
}
template <class ...Args>
void Arguments(Args... args)
{}template <class ...Args>
void Print(Args... args)
{// 注意GetArg必须返回或者到的对象,这样才能组成参数包给ArgumentsArguments(GetArg(args)...);
}
empalce系列接口
- C++11以后STL容器新增了empalce系列的接⼝,empalce系列的接⼝均为模板可变参数,功能上 兼容push和insert系列,但是empalce还⽀持新玩法,假设容器为container,empalce还⽀持 直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。
- emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列
我们来看看push_back和emplace的区别:
看看pair类型:
新的类功能
默认的移动构造和移动赋值
- 原来C++类中,有6个默认成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重 载/const 取地址重载,最后重要的是前4个,后两个⽤处不⼤,默认成员函数就是我们不写编译器 会⽣成⼀个默认的。C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
- 如果你没有⾃⼰实现移动构造函数,且没有实现析构函数、拷⻉构造、拷⻉赋值重载中的任意⼀ 个。那么编译器会⾃动⽣成⼀个默认移动构造。默认⽣成的移动构造函数,对于内置类型成员会执 ⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤ 移动构造,没有实现就调⽤拷⻉构造。
- 如果你没有⾃⼰实现移动赋值重载函数,且没有实现析构函数、拷⻉构造、拷⻉赋值重载中的任意 ⼀个,那么编译器会⾃动⽣成⼀个默认移动赋值。默认⽣成的移动构造函数,对于内置类型成员会 执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调 ⽤移动赋值,没有实现就调⽤拷⻉赋值。(默认移动赋值跟上⾯移动构造完全类似)。
- 如果你提供了移动构造或者移动赋值,编译器不会⾃动提供拷⻉构造和拷⻉赋值。
成员变量声明时给缺省值
defult和delete
C++11可以让你更好的控制要使⽤的默认函数。假设你要使⽤某个默认的函数,但是因为⼀些原因 这个函数没有默认⽣成。⽐如:我们提供了拷⻉构造,就不会⽣成移动构造了,那么我们可以使⽤ default关键字显⽰指定移动构造⽣成。
如果能想要限制某些默认函数的⽣成,在C++98中,是该函数设置成private,并且只声明补丁已, 这样只要其他⼈想要调⽤就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语 法指⽰编译器不⽣成对应函数的默认版本,称=delete修饰的函数为删除函数。
final与override
在继承和多态中应用,注意跳转我其他文章!
由于C++11篇幅过长,我们下期见!