C++的发展史了解
在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 - cppreference.com
而 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。
统一列表初始化
{}初始化
列表初始化,C 当中的列表初始化,支持的是 数组的列表初始化,在 C++11 之后,支持对 struct (结构体)。
这里的 列表初始化 和 构造函数当中的 初始化列表是两个语法,是不一样的,这里要区分开来。
struct point
{
point(int x, int y)
:_x(x)
, _y(y)
{}
int _x;
int _y;
};
int main()
{
// 这三种方式都可以
point p0(0, 0);
point p1 = { 0, 0 };
point p2{0, 0};
}
上述在主函数使用三种方式构造的 point 的对象,都是要调用其中的构造函数的,所以上述三种方式是等价的。
其实,point p1 = { 0, 0 }; 这种方式其实是一种 多参数的 隐式类型转换,而像 string str = "xxxx"; 这样的方式是一种 单参数的隐式类型转换。
验证多参数的隐式类型转换:
point& r = { 1,2 }; // 编译报错
const point& r = { 1,2 }; // 编译通过
之所以出现上述的情况,是因为 发生了隐式类型的转换,发生隐式类型的转换要 先根据其他类型来临时对象,然后在使用拷贝构造函数 构造 出我们想要的对象。而上述两种方式是使用 引用的方式来接收的,那么接收的就是 临时对象的 引用,一位临时对象具有常性,所以,普通的引用去引用临时对象,发生了 权限的放大,就会报错;所以我们上述加了 const 的引用去引用 这个 临时对象才没有报错。
在支持 列表初始化之后,不止可以对结构体来初始化,我们一般使用 int 等等的内置类型都可以用 列表来初始化了,而且可以不写 = 符号:
// 下面三种int 变量的初始化结果是一样的
int a = 1;
int b = {1};
int c {1};
// 下述两种数组的初始化方式是一样的
int arr[] = { 1,2,3 };
int arr[] { 1,2,3 };
支持内置类型数组,使用 new 的时候,使用 列表初始化:
int* prt1 = new int[3]{ 1, 2, 3 };
同样支持 自定义类型数组,使用 new 时,使用 列表初始化:
point p1 = { 0, 0 };
point p2{0, 0};
point* ptr2 = new point[2]{ {1,2}, {2,3} };
point* ptr2 = new point[2]{ point(1,2),point(2,3)};
point* ptr2 = new point[2]{ p1, p2 };
当然,上述使用 列表初始化的方式,本质上是调用 构造函数吗,如果不想别人使用这样的方式来初始化的对象的话,可以在构造函数之前加一个 explicit 修饰词来修饰构造函数。
explicit point(int x, int y)
:_x(x)
, _y(y)
{}
加上这个修饰词之后,下述的初始化方式就不支持了:
std::initializer_list
cplusplus.com/reference/initializer_list/initializer_list/
先来看下述的两行代码,使用的语法是否是一样的:
vector<int> v1 = { 1,2 };
point p = { 1,2 };
首先明确,这两行代码是使用不同的规则来实现的。
point 对象是使用上述的 列表初始化来实现的,调用的是构造函数;可能还有人认为vector 对象也是使用 列表初始化的方式来实现的,但是上述还可以这样写:
vector<int> v1 = { 1,2 ,3,4,5,5,6};
这种方式放在没有这么多参数的 point()当中就不行了:
实际上,是 C++ 当中创建一个新的容器叫做 initializer_list :
只要是用 "{}" 括起来,都可以识别成 initializer_list 。
他还有迭代器的实现,其实是一个 常量数组。这个常量数组是存储在 常量区的。
其实 initializer_list 的实现就是两个指针,指向 常量数组的起始位置和 终止位置:
auto il1 = { 10, 20, 30 };
initializer_list<int> il2 = { 10, 20, 30 };
cout << typeid(il1).name() << endl;
cout << sizeof(il1) << endl;
输出;
class std::initializer_list<int>
16
initializer_list<int> il2 = { 10, 20, 30 }; 这句代码的本质是调用 initializer_list 的构造函数,想办法取到 { 10, 20, 30 } 这段空间的 起始地址和 结束地址。
我们现在反观 vector 是如何实现 , vector<int> v1 = { 1,2 ,3,4,5,5,6}; 这样的方式来构造对象的。
其实 是 vector 当中专门写了 一个 支持传入 initializer_list 作为参数,来构造 vector 的构造函数:
所以,至此就知道了为什么 vector 支持 {1,2,3,4,5}这样的方式去构造 vector 的 对象,而 point 不行。
其实,在C++ 11 之后,就不止vector 容器支持 initializer_list 作为参数的方式来进行构造对象,而是基本都支持了 initializer_list 作为参数的构造函数。
比如 map :
map<string, string> dict = { {"sort", "排序"}, {"left", "左边"} };
这里的 { {"sort", "排序"}, {"left", "左边"} }; 会被识别为一个 initializer_list<pair> 。其中 {"sort", "排序"} 和 {"left", "左边"},是 一个 pair 对象,当然这里使用的是 列表初始化的方式,我们还可以使用 有名对象,和匿名对象的方式来使用。
在 vector 的模拟实现当中加上 initializer_list 作为参数的构造函数
其实实现也很简单,因为 initializer_list 是支持迭代器的,所以,我们可以直接先把 vector 开和 initializer_list 一样大的空间,然后使用范围for 来一一赋值就行:
vector(initializer_list<T> lt)
{
reserve(lt.size());
for (auto e : lt)
{
push_back(e);
}
}
声明
C++ 的类型书写比较麻烦,声明也麻烦,特别是在加上模版之后。C++11 之后,简化了很多的声明方式。
aotu
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型腿断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
aotu在范围for,和一些复杂的模版上经常使用;但是,如果是类型比较复杂的模版参数,使用 aotu的话当然语法是通过的,但是写上aotu之后,其他人,或者是以后自己再看这部分代码的时候可读性的就变得很差,不知道此处的 aotu 是什么类型的。所以在书写复杂模版参数的时候要把这点考虑进去。
int i = 10;
auto p = &i;
auto pf = malloc;
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
输出:
如上述例子,关于 C 当中非常难写的函数指针他也可以推导出来。
还有需要注意的是,使用 aotu 的话,必须显示的初始化,不能像下述一样书写:
auto x;
想想也知道,此时编译器不知道 x 是什么类型变量。
decltype
当我们像定义之前我们定义过的某个变量的同类型的变量,那么除了使用 aotu 之外,我们还可以使用什么来定义呢?
使用 typeid 是不行的,因为 typeid 取出来的是 类型的字符串:
auto ptr = malloc;
cout << typeid(ptr).name() << endl;
// 不行,编译器报错
typeid(ptr).name() ptr2;
在C++ 11 当中新推出了 decltype ,它可以推导出类型,然后使用这个类型进行初始化变量:
auto ptr = malloc;
cout << typeid(ptr).name() << endl;
decltype(ptr) ptr2;
decltype(malloc) ptr3;
而且,aotu 的话,必须显示初始化;但是使用 decltype 的话可以只是单纯定义一个变量出来。如上述代码所示。
它还可以作为模版的实参传入:
auto ptr = malloc;
vector<decltype(ptr)> v;
还可以是一个 表达式的结果的类型推导:
int x = 1;
double y = 2;
decltype(x * y) ret;
nullptr
在 c++11 当中为了弥补 C 当中的一个坑,所增加的一个 空指针。在C 当中的是这样的定义 NULL的:
#define NULL 0
发现,NULL 直接就替换成 0 了,不是指针类型,这是不对的,所以在C++ 11 对此进行修改:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
((void *)0) 指针类型,把 0 强转为 指针类型。
void func(int x)
{}
void func(int* p)
{}
int main()
{
int* p = NULL; // int* p = 0;
func(NULL); // func(0);
return 0;
}
如上所示,主函数的那种实际匹配的是 第一个func 函数,而不是 第二个。
其实上述 NULL 的坑,宏占了一大部分,我们在书写代码的过程当中尽量要用 const ,enum ,inline 取替代宏。
C++11 之后 STL 的一些变化
新容器
用橘色圈起来是C++11中的一些几个新容器。关于 unordered_map 和 unordered_set 的介绍和模拟实现请看 下述博客:
C++ - unordered系列关联式容器介绍 - 和 set map 的比较-CSDN博客
C++ - 封装 unordered_set 和 unordered_map - 哈希桶的迭代器实现_chihiro1122的博客-CSDN博客
C++ - 模版进阶 - array_chihiro1122的博客-CSDN博客
我们来简单介绍一下 forward_list,这个容器 和 array 容器一样鸡肋。
array 容器 只是相比于 C 当中的数组,在调用 operator[]() 函数时候,比如C 的 数组多了一些越界检查,但是,想要检查用 vector 不好吗?此处的 array 是静态的数组,他的本意是想要替换 C 当中的数组,但是 C++ 当中有了 vector 容器 ,array 容器就不好用了。
forward_list的本质也是一个单链表,使用的是单向迭代器。
这个容器鸡肋就鸡肋在,只支持 头插头删;尾差尾删不支持。因为单链表的尾删效率很低,因为删除最后一个要找到前一个;而且其中的 insert()和 erase()函数也是在指定位置之后插入/删除。在指定位置之前插入/删除,也是要找到上一个,效率也不高。
容器中的一些新方法
如果我们再细细去看会发现基本每个容器中都增加了一些C++11的方法,但是其实很多都是用得比较少的。
比如提供了cbegin和cend方法返回const迭代器等等,但是实际意义不大,因为begin和end也是可以返回const迭代器的,这些都是属于锦上添花的操作。
正常我们使用 const 版本的begin()和 end()就行了,上述的用处不大。
新增的方法接口,就是我们上述提到过的 支持 initializer_list 做参数的 构造函数。这个构造函数很多 容器都支持了。
不止上述的更新实际上C++11更新后,容器中增加的新方法最后用的插入接口函数的右值引用版本:
cplusplus.com/reference/vector/vector/emplace_back/
www.cplusplus.com/reference/vector/vector/push_back/
cplusplus.com/reference/map/map/insert/
cplusplus.com/reference/map/map/emplace/
这些接口的实现,提升了效率,使用的就是 右值引用和移动语义 ,具体我们在下述进行说明。其中的 emplace还涉及模板的可变参数。
而且,不是上述新增的接口,其中的 push_back ()也进行的了升级,上述的带来的性能提升是在 调用 emplace 的情况下,在 push_back()升级之后,我们调用 push_back ()也可以有性能提升。而且,在有些地方的提升还非常大。具体看下述对 右值引用和移动语义 的介绍。
在C++11 之后,所有的容器都提供了 移动构造 和 移动赋值的版本,可以到达在有些情况下,深拷贝的性能提升了 约 90%。