C++ - C++11历史 - 统一列表初始化 - aotu - decltype - nullptr - C++11 之后 STL 的改变

目录

C++的发展史了解

 统一列表初始化

 {}初始化

 std::initializer_list

在 vector 的模拟实现当中加上 initializer_list 作为参数的构造函数

声明

aotu  

decltype 

nullptr

 C++11 之后 STL 的一些变化

新容器 

 容器中的一些新方法


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%。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chihiro1122

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值