【从零学C++11(上)】列表初始化、decltype关键字、委派构造等

107 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。


虽然现在C++迭代到C++20版本了,但是作为最近的经典还非C++莫属,因为它提供了很多好用,便捷的新特性,修复了历史版本很多的bug

历史

2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。

不过由于TC1主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0xC++11C++标准当真是十年磨一剑啊~

第二个真正意义上的标准珊珊来迟。相比于C++98/03C++11则带来了数量可观的变化,其中包含了约140个新特性!以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。 相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。


下面开始具体介绍:

1. 列表初始化

C++98中,标准允许使用花括号{}对数组元素进行统一的列表初始值设定。比如:

int arrA[] = {1,2,3,4,5,6,7,8,9};
int arrB[5] = {1};

但对于一些自定义的类型,却无法使用这样的初始化。比如如下代码就无法通过编译

vector<int> v{1,2,3,4,5};

所以每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常不方便。

C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。

内置类型

int main(){
	// 内置类型变量
	int x1 = {10};
	int x2{10};
	int x3 = 1+2;
	int x4 = {1+2};
	int x5{1+2};
	
	// 数组
	int arr1[5] {1,2,3,4,5};	
	int arr2[]{1,2,3,4,5};
	//列表初始化可以在 {} 之前使用等号,其效果与不使用 = 没有什么区别
	
	// 动态数组,在C++98中不支持
	int* arr3 = new int[5]{1,2,3,4,5};
	
	// 标准容器
	vector<int> v{1,2,3,4,5};
	map<int, int> m{{1,1}, {2,2},{3,3},{4,4}};
	return 0;
 }

自定义类型

  • 标准库支持单个对象的列表初始化。
class Point{
public:
	Point(int x = 0, int y = 0)
	 : _x(x)
	 , _y(y)
	{}
private:
	int _x;
	int _y;
};
int main(){
	Pointer p{ 1, 2 };
	return 0;
}
  • 多个对象的列表初始化。 多个对象想要支持列表初始化,需给该类(模板类)添加一个带有initializer_list类型参数的构造函数即可。

【注】:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()end()迭代器及获取区间中元素个数的方法size()

#include <initializer_list>
template<class T>
class Vector {
public:
	Vector(initializer_list<T> l)
	 : _capacity(l.size())
	 , _size(0)
	{
		_array = new T[_capacity];
		for(auto e : l)
			_array[_size++] = e;
	}
	
	Vector<T>& operator=(initializer_list<T> l) {
		delete[] _array;
		size_t i = 0;
		for (auto e : l)
			_array[i++] = e;
		return *this;
	}
private:
	T* _array;
	size_t _capacity;
	size_t _size;
};

2. 变量类型推导

  • 类型推导意义?

  • 在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎么设定,或者类型写起来特别复杂。比如:

#include <map>
#include <string>
int main(){
	short a = 32670;
	short b = 32670;
	
	short c = a + b;
	// c如果设为short,会造成数据丢失,如果能够让编译器根据a+b的结果推导c的实际类型,就不会存在问题
	
	std::map<std::string, std::string> m{{"apple", "苹果"}, {"banana","香蕉"}};
	
	 // 使用迭代器遍历容器, 迭代器类型太繁琐
	std::map<std::string, std::string>::iterator it = m.begin();
	while(it != m.end()){
		cout << it->first << " " << it->second << endl;
		++it;
	}
	return 0;
}

C++11中,可以使用auto来根据变量初始化表达式类型推导变量的实际类型,可以给程序的书写提供许多方便。将程序中cit的类型换成auto,程序可以通过编译,而且更加简洁。

decltype关键字

  • 为什么需要decltype关键字呢?
  • auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。 但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就无能为力。

请看代码:

template<class T1, class T2>
T1 Add(const T1& left, const T2& right){
	return left + right;
}

如果能用两参数加和之后结果的实际类型,作为函数的返回值类型就不会出错,但这需要程序运行完才能知道结果的实际类型,即RTTI(Run-Time Type Identification 运行时类型识别)。

C++98中确实已经支持RTTI

  1. typeid:但它只能查看类型不能用其结果类定义类型
  2. dynamic_cast:只能应用于含有虚函数的继承体系中。

运行时类型识别的缺陷是降低程序运行的效率

decltype】: decltype根据表达式的实际类型推演出定义变量时所用的类型,比如:

  1. 推演表达式类型作为变量的定义类型
int main(){
	int a = 10;
	int b = 20;
	
	// 用decltype推演a+b的实际类型,作为定义c的类型
	decltype(a+b) c;
	cout << typeid(c).name() << endl;
	return 0;
}
  1. 推演函数返回值的类型 【如果带参数列表,推导的是函数返回值的类型。注意:此处 只是推演,不会执行函数
void* GetMemory(size_t size){
	return malloc(size);
}
int main(){
	// 如果没有带参数,推导函数的类型
	cout << typeid(decltype(GetMemory)).name() << endl;
	
	// 如果带参数列表,推导的是函数返回值的类型,注意:此处 只是推演,不会执行函数
	cout << typeid(decltype(GetMemory(0))).name() <<endl;
	
	return 0;
}

3. 基于范围for的循环

范围for之前的博客里面也有使用过。

格式:

for(元素类型 元素对象:容器对象){
  循环体
}
  1. 如果循环体由单条语句或者单个结构块组成,可以省略花括号
  2. 用元素对象依次结合容器对象中的每一个元素,每结合一个元素,执行依次循环体,直至容器内的所有元素都被结合完为止。
  3. 不依赖于下标元素,通用。
  4. 看似不需要访问迭代器,透明,但其底层实现依然要借助于容器的迭代器,
  5. 不需要定义处理函数,简洁。

语法简单,不赘述~


4. final与override

  • final:禁止继承。
  • override:检查是否复写。

5. 智能指针

之前的博客中有专项讲解


6. 新增容器:unordered系列

之前的博客中有提及unordered系列底层原理


7. 委派构造函数

委派构造函数:是指委派函数将构造的任务委派给目标构造函数来完成的一种类构造的方式。

委派构造函数也是C++11中对C++的构造函数的一项改进,其目的是为了:减少程序员书写构造函数的时间。

请看代码:

class Info {
public:
	Info(): _type(0), _name('a')
	{ InitRSet();}
	
	Info(int type): _type(type), _name('a')
	{ InitRSet();}
	
	Info(char a): _type(0), _name(a)
	{ InitRSet();}

private:
	void InitRSet() {//初始化其他变量}
private:
	int _type;
	char _name;
};

上述构造函数除了初始化列表不同之外,其他部分都是类似的,代码重复

初始化列表可以通过:类内部成员初始化进行优化,但是构造函数体的重复在C++98中无法解决。 所以我们打算将构造函数体中重复的代码提出来,作为一个基础版本,在其他构造函数中调用:

class Info{
public:
	// 目标构造函数
	Info()
	 : _type(0)
	 , _a('a')
	{ 
		InitRSet();
	}

	 // 委派构造函数
	Info(int type) 
	 : Info()
	{
		_type = type;
	}

	 // 委派构造函数
	Info(char a)
	 : Info()
	{
	 	 _a = a;
	}

private:
	void InitRSet(){ 
		//初始化其他变量 
	}
private:
	int _type = 0;
	char _a = 'a';
};

在初始化列表中调用”基准版本”的构造函数称为委派构造函数,而被调用的”基准版本”则称为目标构造函数。通过委派其他构造函数,多构造函数的类编写更加容易。

【注】:构造函数不能同时”委派使用初始化列表