在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(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增加的语法特性非常篇幅非常多,这里没办法壹壹讲解,所以本博客主要讲解实际中比较实用的语法。
1. 列表初始化initializer_list
- 列表:花括号:{ }就被叫做列表。
之前可以使用列表来初始化数组,初始化结构体变量,初始化元素类型为结构体变量的数组等等。
- C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
#include <iostream> using namespace std; class Date { public: Date(int year, int month, int day) :_year(year) , _month(month) , _day(day) { cout << "Date(int year, int month, int day)" << endl; } protected: int _year = 1; int _month = 1; int _day = 1; }; int main() { int x1 = 1; int x2 = { 2 }; // 要能看懂,但是不建议使用 int x3{ 2 }; Date d1(2023, 1, 1); // 都是在调用构造函数 Date d2 = { 2023, 2, 2 }; // 要能看懂,但是不建议使用 Date d3{ 2023, 3, 3 }; return 0; }
可以不加等号进行初始化,如上图代码所示,但是强烈不建议使用。
这其实很鸡肋,没有什么价值,继续使用C++98中的方式就挺好的,而且容易理解,C++11中的方式反而不太好理解了。C++中这种鸡肋的语法被很多人吐槽,理性看待。
可以不加等号进行初始化,如上图代码所示,但是强烈不建议使用。
这其实很鸡肋,没有什么价值,继续使用C++98中的方式就挺好的,而且容易理解,C++11中的方式反而不太好理解了。C++中这种鸡肋的语法被很多人吐槽,理性看待。
列表初始化真正有意义的地方是用于初始化STL中的容器:
之前提到:vector和list以及map等STL中的容器也可以像普通数组一样使用初始化列表来初始化了。这是因为列表初始化本身就是一个类模板:
如上图所示,这是C++11才有的一个类型,该类型叫做列表初始化,而且还有自己的成员函数,包括构造函数,计算列表大小的接口,获取列表迭代器位置。(但几乎都不用)
C++11为这些容器提供了新的构造函数,该构造函数是使用列表来初始化对象的,它的形参就是initializer_list,所以列表初始化才可以初始化STL中的容器。
赋值运算符重载函数也有一个列表的重载版本:
#include <iostream> #include <vector> #include <list> #include <map> using namespace std; class Date { public: Date(int year, int month, int day) :_year(year) , _month(month) , _day(day) { cout << "Date(int year, int month, int day)" << endl; } protected: int _year = 1; int _month = 1; int _day = 1; }; int main() { int x1 = 1; int x2 = { 2 }; // 要能看懂,但是不建议使用 int x3{ 2 }; Date d1(2023, 1, 1); // 都是在调用构造函数 Date d2 = { 2023, 2, 2 }; // 要能看懂,但是不建议使用 Date d3{ 2023, 3, 3 }; // 调用支持list (initializer_list<value_type> il)类似这样的构造函数 vector<int> v1 = { 1, 2, 3, 4, 5, 6 }; vector<int> v2 { 1, 2, 3, 4, 5, 6 }; list<int> lt1 = { 1, 2, 3, 4, 5, 6 }; list<int> lt2{ 1, 2, 3, 4, 5, 6 }; auto x = { 1, 2, 3, 4, 5, 6 }; cout << typeid(x).name() << endl; // 打印初始化列表的类型 vector<Date> v3 = {d1, d2, d3}; vector<Date> v4 = { { 2022, 1, 1 }, {2022, 11, 11} }; string s1 = "11111"; map<string, string> dict = { { "sort", "排序" }, { "insert", "插入" } }; // 构造 initializer_list<pair<const string, string>> kvil = { { "left", "左边" }, { "right", "右边" } }; // 赋值重载 dict = kvil; // 上面的类型就不能用auto推导,编译器不知道那里是一个pair return 0; }
2. 前面提到的一些知识点
2.1 小语法
C++11提供了一些新的小语法,很多我们都接触过甚至是使用过,这里系统讲讲。
c++11提供了多种简化声明的方式,尤其是在使用模板时。这里讲auto和decltype
auto:这个关键字我们已经使用过很多了
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
#define _CRT_SECURE_NO_WARNINGS 1 #include <iostream> #include <map> using namespace std; int main() { int i = 10; auto p = &i; auto pf = strcpy; cout << typeid(p).name() << endl; cout << typeid(pf).name() << endl; map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} }; //map<string, string>::iterator it = dict.begin(); auto it = dict.begin(); return 0; }
decltype:
关键字decltype将变量的类型声明为表达式指定的类型。
使用typeid().name()只能打印出类型的名称,并不能用这个名称继续创建变量,而decltype可以:
template<class T1, class T2> void F(T1 t1, T2 t2) { decltype(t1 * t2) ret; cout << typeid(ret).name() << endl; } int main() { const int x = 1; double y = 2.2; decltype(x * y) ret; // ret的类型是double decltype(&x) p; // p的类型是int* cout << typeid(ret).name() << endl; cout << typeid(p).name() << endl; F(1, 'a'); return 0; }
使用decltype可以自动推演类型,并且可以用推演出的结果继续创建变量,如上图所示,对于一些不同类型直接的运算结果,decltype有奇效。
nullptr:
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
在C++中存在条件编译:(以后用nullptr就行了)这算是修复了一个bug
#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void*)0) #endif #endif
范围for循环
范围for我们也一直都在使用,这是C++11提供的语法糖,使用起来非常方便,它的底层就是迭代器,只是编译器给自动替换了,这里就不再详细讲解了。一般是这么用的:
#include<iostream> using namespace std; int main() { int arr[] = { 1, 2, 3, 4, 5 }; for (auto& e : arr) { cout << e << " "; } cout << endl; return 0; }
2.2 STL中的一些变化
新容器:
红色框中的是C++11增加的新容器,基本只有unordered_map和unordered_set有用,其他很鸡肋。容器array对标的是静态数组,array也是一个静态的,也就是在栈区上的,大小是通过一个非类型模板参数确定的。容器forward_list是一个单链表,也很鸡肋,因为绝大部分场景双链表都可以满足要求,而且更加方便,唯一使用到单链表的地方就是哈希桶中。前面都提到过。
至于unordered_map和unordered_set,这两个容器的底层是哈希桶,虽然不能实现排序,但是可以降重。而且在查找时具有其他容器无法比拟的效率。这两个容器是非常实用的,而且也是我们经常使用的。
容器中的使用新方法:
1. 使用列表构造
在前面就讲解过了,几乎每个容器都增加了新的接口,使用std::initializer_list类型来构造。
2. 移动构造和移动赋值
在下面讲解了右值引用就可以明白了。
3. emplace_xxx插入接口或者右值引用版本的插入接口。
同样在后面才能学习到。
3. 右值和右值引用
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性, 之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
3.1 右值和右值引用概念
什么是左值?什么是右值?
- 左值:一个表示数据的表达式,如变量名或者指针解引用。
- 特点:可以对左值取地址 + 可以对左值赋值。
上图代码中所示的变量都属于左值,要牢记左值可以取地址这一个特性。
- 定义时const修饰符后的左值,不能给它赋值,但是可以取它的地址。
- 左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边。
- 右值:也是一个表示数据的表达式。如:字面常量,表达式返回值,函数返回值,类型转换时的临时变量等等。
- 特点:右值不可以取地址,不可以赋值。
要牢记右值特性:不能取地址不能赋值。
- 右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边。
什么是右值引用?
左值引用是给左值取别名,右值引用显而易见就是给右值取别名。
- 右值引用使用两个&符号。
上图代码中的rr1,rr2,rr3就是三个右值的别名,也就是右值引用。
3.2 右值引用类型的左值属性
- 右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址。
对于内置类型的右值,如字面常量,一旦右值引用以后,就会被存储到特定的位置,并且可以取到该地址,而且还可以修改。
int main() { int&& rr1 = 10; cout << rr1 << endl; rr1 = 5; cout << rr1 << endl; const double&& rr2 = (1.1 + 2.2); //rr2 = 5.5; // 不能修改 return 0; }
字面常量10原本是不可以被修改的,但是右值引用以后,在特定的位置开辟了变量来存放10,所以就可以被修改了。
表达式或者函数的返回值,会有一个临时变量来存放返回值,我们知道这样的临时变量具有常性,也是右值。对于这种右值引用,编译器会修改它的属性,将常性修改,并且存储在特定位置。注意const类型的右值,即便开辟了变量存放该右值也是不可以被修改的,因为被const修饰了。
内置类型的右值被称为纯右值。
自定义类型的右值被称为将亡值。
对于自定义类型的右值,如容器的临时变量,它确确实实会被销毁,而不会被存放。自定义类型的右值才能体现出右值存在的意义,后面会详细讲解。
右值引用是右值的别名,它所指向的右值是不可以被修改的。
但是右值引用本身也是一种类型,并且它的属性是左值,可以取地址,可以赋值。
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(中):https://developer.aliyun.com/article/1522391