C++笔记21•C++11•
相比于
C++98/03
,
C++11
则带来了数量可观的变化,其中
包含了约140个新特性
,以及对
C++03
标准中
约
600
个缺陷的修正,这使得
C++11
更像是从
C++98/03
中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更
强大,而且能提升程序员的开发效率。
1.统一的列表初始化
1.1 {}初始化C++11 扩大了用大括号括起的列表 ( 初始化列表 ) 的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号 (=) ,也可不添加 。struct Point { int _x; int _y; }; int main() { int x1 = 1; int x2{ 2 }; int array1[]{ 1, 2, 3, 4, 5 }; int array2[5]{ 0 }; Point p{ 1, 2 }; // C++11中列表初始化也可以适用于new表达式中 int* pa = new int[4]{ 0 }; return 0; }
int main() { //Date是一个日期类 Date d1(2022, 1, 1); // 旧格式 //C++11支持的列表初始化,这里会调用构造函数初始化 Date d2{ 2022, 1, 2 };//新格式 Date d3 = { 2022, 1, 3 }; return 0; }
2.关键字声明
2.1 auto:在 C++98 中 auto 是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto 就没什么价值了。 C++11 中废弃 auto 原来的用法,将 其用于实现自动类型推断。int main() { int i = 10; auto p = &i;// auto可以自动推断出p是一个指针 map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} }; map<string, string>::iterator it = dict.begin(); auto it = dict.begin();// auto可以自动推断出it是一个迭代器 return 0; }
2.2decltype关键字 decltype 将变量的类型声明为表达式指定的类型。//关键字decltype 将变量的类型 声明为 表达式指定的类型。 int main() { const int x = 1; double y = 2.2; decltype(x * y) ret; // decltype可以推断出ret的类型是double decltype(&x) p; // decltype可以推断 p的类型是int* cout << typeid(ret).name() << endl; //typeid(ret).name()可以打印出ret的类型名字 cout << typeid(p).name() << endl; //typeid(p).name()可以打印出p的类型名字 return 0; }
3.3 nullptr由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示 整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
3.右值引用和左值引用
C++11
中新增了的右值引用语法特性,所以从现在开始之前学习的引用就叫做左值引用。无论左值引用还是右值引用,
都是给对象取别名
。
3.1左值和左值引用概念
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但可以取它的地址。左值引用就是给左值的引用,给左值取别名。
int main() { // 以下的p、b、c、*p都是左值 int* p = new int(1); int b = 1; const int c = 2; // 以下几个是对上面左值的左值引用 int*& rp = p; int& rb = b; const int& rc = c; int& pvalue = *p; //特别注意:左值可以出现在赋值符号的右边 但是右值不能出现出现在赋值符号的左边 return 0; }
3.2右值和右值引用概念
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值 ( 这个不能是左值引用返回) 等等, 右值可以出现在 赋值符号的右边 ,但是 不能出现出现在赋值符号的左边 , 右值不能取地址 。 右值引用就是对右值的引用,给右值取别名 。int main() { double x = 1.1, y = 2.2; // 以下几个都是常见的右值 10; //字面常量 x + y;//表达式的返回值 fmin(x, y);//函数的返回值 // 以下几个都是对右值的右值引用 int&& rr1 = 10; double&& rr2 = x + y; double&& rr3 = fmin(x, y); // 这里编译会报错:error C2106: “=”: 左操作数必须为左值 10 = 1; x + y = 1; fmin(x, y) = 1; return 0; } //左值引用:单 & //右值引用:双 &&
3.3左值引用和右值引用的比较左值引用总结: 1. 左值引用只能引用左值,不能引用右值。 2. 但是const左值引用既可引用左值,也可引用右值。 int main() { // 左值引用只能引用左值,不能引用右值。 int a = 10; int& ra1 = a; // ra为a的别名 //int& ra2 = 10; // 编译失败,因为10是右值 // const左值引用既可引用左值,也可引用右值。 const int& ra3 = 10; const int& ra4 = a; return 0; } 右值引用总结: 1. 右值引用只能引用右值,不能引用左值。 2. 但是右值引用可以move以后的左值。 int main() { // 右值引用只能右值,不能引用左值。 int&& r1 = 10; // error C2440: “初始化”: 无法从“int”转换为“int &&” // message : 无法将左值绑定到右值引用 int a = 10; int&& r2 = a;//编译不通过 // 右值引用可以引用move以后的左值 int&& r3 = std::move(a);//编译通过 return 0; }
3.4上面看似右值引用没有太大用处,但其实也有它的用武之地, 右值引用的使用场景如果s被传入的是 右值,如果既有拷贝构造又有移动构造,优先使用移动构造这样可以减少拷贝构造的次数,增加效率。3.5 左值引用的使用场景做参数和做返回值都可以提高效率void func1(bit::string s) {} void func2(const bit::string& s) {} int main() { bit::string s1("hello world"); // func1和func2的调用我们可以看到"左值引用做参数减少了拷贝",提高效率的使用场景和价值 func1(s1); func2(s1); //"左值引用做返回值减少了拷贝" // string operator+=(char ch) 传值返回存在深拷贝,降低效率 // string& operator+=(char ch) 传左值引用没有拷贝提高了效率 s1 += '!'; return 0; }
左值引用的短板:但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如:bit::string to_string(int value) 函数中可以看到,这里只能使用传值返回,传值返回会导致至少1 次拷贝构造 ( 如果是一些旧一点的编译器可能是两次拷贝构造 ) 。右值引用和移动语义解决上述问题:在 bit::string 中增加移动构造, 移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不 用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己 。总结: 右值引用做参数和做返回值减少拷贝的本质是利用了移动构造和移动赋值 左值引用和右值引用本质的作用都是减少拷贝,右值引用本质可以认为是弥补左值引用不足的地方,他们两个是相辅相成的 左值引用:解决的是传参过程中和返回值过程中的拷贝 做参数: void push(T x) -> void push(const T& x)解决的是传参过程中减少拷贝 做返回值:T f() -> T& f()解决的返回值过程中的拷贝 Ps :但是要注意这里有限制,如果返回对象出了作用域不在了就不能用传引用,这个左值引用无法解决,等待C++11右值引用解决 右值引用:解决的是传参后,push/insert函数内部将对象移动到容器空间上的问题+传值返回接收返回值的拷贝 做参数:void push(T&& x)解决的push内部不再使用拷贝构造x到容器空间上,而是移动构造过去 做返回值:T f();解决的外面调用接收f()返回对象的拷贝,T ret = f(),这里就是右值引用的移动构造,减少了拷贝 Ps:右值引用实际上不建议写成T&& f() 和 T&& ret = f(),写成 T&& f() 和 T&& ret = f() 会带来潜在的风险,正确且安全的做法是返回一个局部对象(T f()),这样编译器能够自动处理移动语义或返回值优化,避免不必要的拷贝。 ·使用 T f() 的方式返回对象,配合右值引用和移动构造函数,已经可以避免拷贝。编译器会自动选择移动构造 函数来高效地传递对象,因此没有必要显式返回右值引用。 ·如果你写成 T&& ret = f();,实际上是将右值引用绑定到临时对象,这可能导致不安全的代码。
3.6移动赋值
不仅仅有移动构造,还有移动赋值:// 移动赋值 string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 移动语义" << endl; swap(s); return *this; } int main() { xcn::string ret1; ret1 = xcn::to_string(1234); return 0; } // 运行结果: // string(string&& s) -- 移动语义(移动构造) // string& operator=(string&& s) -- 移动语义(移动赋值)
这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象接收,编译器就没办法优化了。xcn::to_string函数中会先用str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时对象做为xcn::to_string函数调用的返回值赋值给ret1,这里调用的移动赋值。
STL 中的容器都是增加了移动构造和移动赋值:例string
4.C++11增加新的类功能
1.默认成员函数
原来 C++ 类中,有 6 个默认成员函数:(1). 构造函数(2). 析构函数(3). 拷贝构造函数(4). 拷贝赋值重载(5). 取地址重载(6). const 取地址重载重要的是前 4 个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。 C++11 新增了两个:移动构造函数和移动赋值运算符重载2.强制生成默认函数的关键字 defaultC++11 可以让你更好的控制要使用 的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default 关键字显示指定移动构造生成。class Person { public: Person(const char* name = "", int age = 0) :_name(name) , _age(age) {} Person(const Person& p) :_name(p._name) ,_age(p._age) {} Person(Person&& p) = default; private: xcn::string _name; int _age; };
3.禁止生成默认函数的关键字 delete:如果能想要限制某些默认函数的生成,在 C++98 中,是该函数设置成 private ,并且只在类外进行声明,这样只要其他人想要调用就会报错。在C++11 中更简单,只需在该函数声明加上 =delete 即可,该语法指示编译器不生成对应函数的默认版本,称=delete 修饰的函数为删除函数。class Person { public: Person(const char* name = "", int age = 0) :_name(name) , _age(age) {} Person(const Person& p) = delete; private: xcn::string _name; int _age; };
4.继承和多态中的 final 与 override 关键字C++11 override 和 final 检查重写的关键字 // final:修饰虚函数,表示该虚函数不能再被重写 // override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
详细见:C++笔记13•面向对象之多态•_c++ 面相对象 多态体现-CSDN博客