作者:爱写代码的刚子
时间:2023.9.24
前言:本篇博客主要介绍C++11的新特性,了解C++11这座里程碑。lambda表达式、线程库、智能指针将分别会在C++11系列第二篇,以及智能指针专篇博客介绍
在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为 C++11之前的最新C++标准名称。不过由于TC1主要是对C++98标准中的漏洞进行修复,语言的核心部分则没 有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑, 第二个真正意义上的标准珊珊来迟。相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语 言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
多个对象想要支持列表初始化,需给该类(模板类)添加一个带有initializer_list类型参数的构造函数即可。注意:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器以及获取区间中元素个数的方法size()。
为什么要进行类型推导?
在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂,或者防止由于给错类型造成数据丢失
为什么需要使用decltype进行类型推导?
auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就 无能为力。
如果能用加完之后结果的实际类型作为函数的返回值类型就不会出错,但这需要程序运行完才能知道结果的 实际类型,即RTTI(Run-Time Type Identification 运行时类型识别)。
C++98中确实已经支持RTTI: typeid只能查看类型不能用其结果类定义类型
dynamic_cast只能应用于含有虚函数的继承体系中 运行时类型识别的缺陷是降低程序运行的效率。
C++98中提出了引用的概念,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通 过指针来实现的,因此使用引用,可以提高程序的可读性。
为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用。
为了与C++98中的引用进行区分,C++11将该种方式称之为右值引用。
左值与右值是C语言中的概念,但C标准并没有给出严格的区分方式,一般认为:可以放在=左边的,或者能 够取地址的称为左值,只能放在=右边的,或者不能取地址的称为右值,但是也不一定完全正确。
因此关于左值与右值的区分不是很好区分,一般认为:
1. 普通类型的变量,因为有名字,可以取地址,都认为是左值。(例如常量字符串是左值)
2. const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),C++11认为其是左值。
3. 如果表达式的运行结果是一个临时变量或者对象,认为是右值。
4. 如果表达式运行结果或单个变量是一个引用则认为是左值。
总结:
1. 不能简单地通过能否放在=左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质 判断,比如上述:c常量
2. 能得到引用的表达式一定能够作为引用,否则就用常引用。
C++11对右值进行了严格的区分:
C语言中的纯右值,比如:a+b, 100 将亡值。比如:表达式的中间结果、函数按照值的方式进行返回。
在C++98中的普通引用与const引用在引用实体上的区别:
普通引用:只能引用左值,不能引用右值,const引用既可引用左值,也可引用右值。
C++11中右值引用:只能引用右值,一般情况不能直接引用左值。
但是我们可以使用std::move将左值变为右值:
注意以上代码中,作者犯了一个错误:std和STL中都含有move函数,使用move函数时最好使用域作用限定符指定类域(如:std::move)
如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值,就会出错,比如:
如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将
会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值,就会出错,比如:
同时,编译器还会进行优化:
问题概括:
在operator+中:strRet在按照值返回时,必须创建一个临时对象,临时对象创建好之后,strRet就被销毁了,最后使用返回的临时对象构造s3,s3构造好之后,临时对象就被销毁了**。仔细观察会发现:strRet、临时对象、**s3每个对象创建后,都有自己独立的空间,而空间中存放内容也都相同,相当于创建了三个内容完 全相同的对象,对于空间是一种浪费,程序的效率也会降低,而且临时对象确实作用不是很大
C++11提出了移动语义概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解该问题。
在C++11中如果需要实现移动语义,必须使用右值引用。上述String类增加移动构造:
String(String&& s)//右值引用
: _str(s._str)
{
s._str = nullptr;
}
因为strRet对象的生命周期在创建好临时对象后就结束了,即将亡值,C++11认为其为右值,在用strRet构造 临时对象时,就会采用移动构造,即将strRet中资源转移到临时对象中。而临时对象也是右值,因此在用临 时对象构造s3时,也采用移动构造,将临时对象中资源转移到s3中,整个过程,只需要创建一块堆内存即 可,既省了空间,又大大提高程序运行的效率。
注意:
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用 右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右 值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功 能就是将一个左值强制转化为右值引用,然后实现移动语义。
注意:
注意:以上代码是move函数的经典的误用,因为move将s1转化为右值后,在实现s2的拷贝时就会使用移动 构造,此时s1的资源就被转移到s2中,s1就成为了无效的字符串。
使用move的一个例子:
str在存储在栈帧中,原本为左值,但相当于将亡值,利用std::move函数将str转化为右值,编译器识别右值,调用移动构造,提高效率。
附:添加以下代码以便理解右值引用:
完美转发:指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
void Func(int x)
{
// ......
}
template<typename T>
void PerfectForward(T t)
{
Fun(t);
}
PerfectForward为转发的模板函数,Func为实际目标函数**,但是上述转发还不算完美,**完美转发是目标函 数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销,就好像转发者不存在 一样。
所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相 应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进 行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。
C++11通过forward函数来实现完美转发, 比如:
void Fun(int &x){
cout << "lvalue ref" << endl;
}
void Fun(int &&x){
cout << "rvalue ref" << endl;
}
void Fun(const int &x){
cout << "const lvalue ref" << endl;
}
void Fun(const int &&x){
cout << "const rvalue ref" << endl;
}
template<typename T>
void PerfectForward(T &&t){
Fun(std::forward<T>(t));
}
int main() {
PerfectForward(10); // rvalue ref
int a;
PerfectForward(a); // lvalue ref
PerfectForward(std::move(a)); // rvalue ref
const int b = 8;
PerfectForward(b); // const lvalue ref
PerfectForward(std::move(b)); // const rvalue ref
return 0;
}
C++98中引用作用:因为引用是一个别名,需要用指针操作的地方,可以使用指针来代替,可以提高代码的
可读性以及安全性。 C++11中右值引用主要有以下作用:
int main() {
string s1("hello");
string s2(" world");
string s3 = s1 + s2; // s3是用s1和s2拼接完成之后的结果拷贝构造的新对象
stirng&& s4 = s1 + s2; // s4就是s1和s2拼接完成之后结果的别名
return 0;
}