1. C++11简介
在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增加的语法特性非常篇幅非常多,我们这里没办法一 一讲解,所以本节课程主要讲解实际中比较实用的语法。
小故事:
1998年是C++标准委员会成立的第一年,本来计划以后每5年视实际需要更新一次标准,C++国际标准委员会在研究C++ 03的下一个版本的时候,一开始计划是2007年发布,所以最初这个标准叫C++ 07。但是到06年的时候,官方觉得2007年肯定完不成C++ 07,而且官方觉得2008年可能也
完不成。最后干脆叫C++ 0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的时候也没完成,最后在2011年终于完成了C++标准。所以最终定名为C++11。
2. 统一的列表初始化
{} 初始化
在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扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
int main()
{
int x1 = 1;
int x2 = { 2 };
int x4(1);
//可以省略赋值符号
int x3{ 3 };
int array1[]{ 1, 2, 3, 4, 5 };
int array2[5]{ 0 };
//C++11中列表初始化也可以适用于new表达式中
int* pa = new int[4]{ 0 };
return 0;
}
创建对象时也可以使用列表初始化方式调用构造函数初始化
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;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 1, 1); // old style
// C++11支持的列表初始化,这里会调用构造函数初始化,但是一般不建议这么写
Date d2 = { 2022, 1, 2 };
Date d3 { 2022, 1, 3 };
return 0;
}
std::initializer_list
initializer_list
类是C++11中新增加的一个类(initializer_list文档介绍)
initializer_list这个类只有构造函数、迭代器begin()
和end()
和size()
接口。它可以将同一类型元素的集合即由相同元素构成的一个列表转化为一个 initializer_list 的对象;
这里需要注意的是,initializer_list 实际上是对常量区的封装 – 将列表中的数据识别为常量区的数据,然后用类似于迭代器的 begin 和 end 指针指向并访问这些数据,其自身并不会开辟空间,所以 initializer_list 中的数据也不能修改。
因此,在C++11后,STL中的容器就支持重载了一个参数为 initializer_list
类型的构造函数和赋值函数,从而使得这些容器支持使用列表来进行初始化和赋值;
当我们给我们自己封装实现的vector或者list加上initializer_list
的构造函数和赋值运算符重载函数时,就可以使用列表进行初始化了。
vector(initializer_list<T> lt)
{
reserve(lt.size());
for (auto e : lt)
push_back(e);
}
有了列表初始化,很多容器的初始化就变的很方便了:
int main()
{
vector<Date> vd1 = {d1, d2};
vector<Date> vd2 = { Date(2023,5,20), Date(2023,5,21) };
vector<Date> vd3 = { {2023,5,20}, {2023,5,20} };
map<string, string> dict = { {"sort", "排序"},{"string", "字符串"},{"Date", "日期"} };
pair<string, string> kv1 = { "Date", "日期" };
pair<string, string> kv2 { "Date", "日期" };
return 0;
}
这里我们还需要区分两种不同的场景:
- 当列表中的元素类型和元素个数符合构造函数的参数要求时,会直接调用构造函数来完成初始化;
- 当列表中的元素个数不符合构造函数的参数要求时,会先将列表转换为 initializer_list 类,然后再调用参数为 initializer_list 的构造函数完成初始化。
因此,在 C++11 及其过后,一切即可用 {}
完成初始化,初始化时皆可以省略赋值符号。(STL 中的所有容器都重载了参数类型为 initializer_list
的构造和赋值函数,但是不包括容器适配器,因为容器适配器本身不是一个容器,其只是对容器的封装)。
3. 声明
auto
在C++98中
auto
是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
int main()
{
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin();
return 0;
}
decltype
关键字
decltype
将变量的类型声明为表达式指定的类型。
int main()
{
const int x = 1;
double y = 2.2;
cout << typeid(x * y).name() << endl;
decltype(x * y) ret; // ret的类型是double
decltype(&x) p; // p的类型是const int*
cout << typeid(ret).name() << endl;
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
// vector存储的类型跟x*y表达式返回值类型一致
// decltype推导表达式类型,用这个类型实例化模板参数或者定义对象
vector<decltype(x* y)> v;
cout << typeid(v).name() << endl;
return 0;
}
注意: typeid 拿到的只是类型的字符串,不能用这个再去定义对象。
nullptr
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
4. STL中的一些变化
新容器
用橘色圈起来是C++11中的一些几个新容器,但是实际最有用的是unordered_map和unordered_set。这两个我们前面已经进行了非常详细的讲解,其他的大家了解一下即可。
在C语言中,数组越界读是检查不出来的,而越界写是抽查。新增的array
只要是越界就会报错,因为他的[ ]是调用operator[],在该函数里有越界的断言检查。
forward_list
是单链表,对于list而言,每个节点可以节省一个指针的空间,头插头删的效率不错,但是我们一般都不缺内存,所以用list
更香。
容器中新增的一些方法
如果我们再细细去看会发现基本每个容器中都增加了一些C++11的方法,但是其实很多都是用得比较少的。比如,提供了cbegin
和cend
方法返回const迭代器等等,但是实际意义不大,因为begin
和end
也是可以返回const迭代器的,这些都是属于锦上添花的操作。
(1) 所有支持 const 迭代器的容器都提供了 cbegin 和 cend 方法来返回 const 迭代器。
(2) 所有容器的插入接口都提供了 emplace 版本,包括容器适配器 – emplace 主要是可变参数模板和右值引用。
(3) 所有容器的构造函数都重载了移动构造和参数为 initializer_list 的构造 (注:容器适配器重载了移动构造,但没有重载initializer_list构造)。
(4) 所有容器的赋值重载函数都重载了移动赋值和参数为 initializer_list 的赋值,不包括容器适配器。