15 【C++11 新特性】统一的列表初始化和变量类型推导
文章目录
- C++11简介
- 1. 统一的列表初始化
- 1.1 统一初始化的原理
- (1)多参数的隐式类型转换
- 禁用类型转换
- (2)std::initializer_list
- 大概了解initializer_list
- 容器实现接收initializer_list类型参数的构造函数和赋值重载函数
- 1.2 统一列表初始化的总结
- 2. 统一的声明
- 2.1 auto
- 2.2 decltype
- 总结
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增加的语法特性非常篇幅非常多,我们这里没办法一 一讲解,所以本文主要讲解实际中比较实用的语法:
- 列表初始化
- 变量类型推导
- 范围for循环
- 可变模板参数
- lambda表达式
- 包装器
- 新增 新容器—静态数组array、forward_list以及unordered系列
- 新增 新接口—右值引用和移动语义
- 新增 类功能: 禁止生成默认函数的关键字delete、 final与override
- 智能指针
- 线程库
C++参考手册
小故事:
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。
1. 统一的列表初始化
C++11的第一个新特性:一切皆可用{}初始化,并且可以不用加=
class Point
{
public:Point(int x, int y):_x(x), _y(y){}int _x;int _y;
};int main()
{int x = 1;int y = { 2 };int z{ 3 };int* p1 = new int[3]{1, 2, 3}; //有了统一列表初始化之后就可以这样,下面其他初始化可以不加=的原因,或许是为了和这里统一。//int* pa = new int[3] = {1, 2, 3}; // 错误: “=”: 无法从“initializer list”转换为“int *”int p2[]{ 1, 2, 3 };int p3[] = { 1, 2, 3 };Point p4(4, 4);Point p5{ 5,5 };Point p6 = { 6, 6 };vector<int> v1{ 1,2,3,4,5 };vector<int> v2 = { 1,2,3,4,5 };return 0;
}
1.1 统一初始化的原理
(1)多参数的隐式类型转换
我们上面对Point类对象的构造:
Point p4(4, 4);
Point p5{ 5,5 };
Point p6 = { 6, 6 };
这三种Point初始化,本质都是调用了构造函数,也就是这三个是等价的。
我们知道单参数隐式类型转换:string s = "xxxx";
对于对象p6,它们其实本质是一个多参数的隐式类型转换:将{ 6, 6 },隐式转换成了构造函数刚好接收两个参数的类Point,构造出了一个临时对象,然后将临时对象赋值给了p6,
由于编译器的优化行为,可能将p6的构造优化的和p5一样直接调用了Point的构造函数。
证明:
int main()
{//Point& p = { 1,2 }; //报错:“初始化”: 无法从“initializer list”转换为“Point &”const Point& p1 = { 1,2 }; //正确return 0;
}
因为临时变量有常性,所以我们无法直接将其赋给一个非常性的引用,而加了const就可以正常引用。
侧面证明了这是多参数的隐式类型转换。
至于报错中的initializer list是什么,暂且不管。
在堆上new对象也是同样原理:
//堆上new对象:
Point* po1 = new Point<int>{1, 2}; //new类对象,隐式类型转换
std::vector<int>* vec3 = new std::vector<int>{1, 2, 3, 4}; //new类对象,调用initializer_list参数构造函数
禁用类型转换
和单参数的隐式类型转换一样,如果我们不想隐式类型转换发生,我们可以在类的构造函数前加上explicit禁用类型装换:
class Point
{
public:explicit Point(int x, int y):_x(x), _y(y){}int _x;int _y;
};int main()
{Point p4(4, 4);Point p5{ 5,5 };//Point p6 = { 6, 6 }; //报错: "Point" 的复制列表初始化不能使用显式构造函数return 0;
}
(2)std::initializer_list
我们来看看上面v2和p6的初始化:
Point p6 = { 6, 6 };
//Point p6 = { 6, 6 ,6}; // 报错。
vector<int> v2 = { 1,2,3,4,5 };
它们都是利用{}初始化,两个有什么不同?他们用的是同一个语法吗?
很明显不是,因为我们清除vector内部的实现规则,{}中的值明显是给了vector中一个数组成员(实际是指针实现)初始化了。而我们Point后面的{}是给了两个成员初始化。
我们Point调的是构造为成员初始化,它的构造只需要两个参数,超过就会报错。但是我们vector可以用很多值,因为最终它是放到一个数组中的。
我们的p6是多参数隐式类型转换,那么v2是什么呢?
它其实也是调用构造函数,是这样实现的:
C++11中新增了一个类型initializer_list,编译器可以将直接写的常量列表{}识别成initializer_list类型。
然后我们的容器如vector,再去实现对应使用initializer_list初始化的构造函数,就实现了以上使用{}给容器初始化的现象。
注意,正常的给数组使用{}初始化并不使用initializer_list转换,而是 C++ 核心语言特性的一部分,换句话说就是编译器的行为:
Point p6 = { 6, 6 };
//栈上的数组:
Point p2[2]{ {1,1}, {2,2} };
//堆上new数组:
Point* pt = new Point[2]{ p6, p6 }; //initializer_list给数组一一初始化
至于为什么可以不加=,这是编译器的行为。
大概了解initializer_list
int main()
{auto il = { 10, 20 ,30 };cout << typeid(il).name() << endl; //输出: class std::initializer_list<int>cout << sizeof(il) << endl; //输出: 8return 0;
}
我们可以认为,initializer_list内部是用两个指针实现的,因为32位下地址大小为4字节,所以两个指针成员就是8字节。
常量数组识别成initializer_list,那么:编译器会想办法取到数组的开始地址和结尾地址给到initializer_list的成员(本质还是调用initializer_list的构造函数),这里是编译器特殊处理的,因为我们常量列表返回的并不是指针。
所以像我们上面出现的错误,就是因为编译器隐式类型转换不成功,从而尝试了转换成initializer_list而产生的报错。
而且由于数组列表被识别成了initializer_list,这样初始化不可以:
const int* p1 = { 1,2,3 }; //“初始化”: 无法从“initializer list”转换为“const int *”
容器实现接收initializer_list类型参数的构造函数和赋值重载函数
我们很多容器构造函数,C++11都支持了用initializer_list做参数的构造函数和赋值重载,就是为了支持我们的列表初始化。
如果我们给我们自己的类加上可以接收initializer_list参数类型的构造函数和赋值重载,那么我们的自己的类,就也可以使用{}初始化了。
template<class T>
class myVector {
public:typedef T* iterator;myVector(initializer_list<T> l){_start = new T[l.size()];_finish = _start + l.size();_endofstorage = _start + l.size();iterator vit = _start;typename initializer_list<T>::iterator lit = l.begin();while (lit != l.end()){*vit++ = *lit++;}//for (auto e : l)// *vit++ = e;}myVector<T>& operator=(initializer_list<T> l) {myVector<T> tmp(l);std::swap(_start, tmp._start);std::swap(_finish, tmp._finish);std::swap(_endofstorage, tmp._endofstorage);return *this;}
private:iterator _start;iterator _finish;iterator _endofstorage;
};
大括号给容器初始化元素的过程:编译器自动将常量列表转换为initializer_list类对象,然后根据去调用对应容器接收initializer_list参数的构造或者赋值重载。
1.2 统一列表初始化的总结
统一列表初始化,可以给类初始化,也可以给容器元素初始化,也可以适用于new表达式中,也可以用于数组的元素初始化(不过不使用initializer_list)。
给类初始化是多参数的隐式类型转换;给容器初始化元素是编译器自动将常量列表转换成了initializer_list,各个容器再去支持使用initializer_list初始化或者赋值;
而用于原生数组的元素初始化,是 C++ 核心语言特性的一部分,并不转换为initializer_list。
有了统一列表初始化,就可以做到这种事:
map<string, string> dict = { {"sort","排序"},{"left","左边"}};
内层括号:是多参数隐式类型装换而成的pair类型。
外层括号:是一个pair列表转换的initializer_list,map有对应接收initializer_list类型参数的构造函数。
2. 统一的声明
我们介绍的C++11中的第二个特性:统一的声明
C++11提供了多种简化声明的方式,尤其是在使用模板时。
2.1 auto
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器可以自动的将定义对象的类型设置为初始化值的类型。
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;
}
2.2 decltype
关键字decltype将变量的类型声明为表达式指定的类型。
// 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的类型是doubledecltype(&x) p; // p的类型是int*cout << typeid(ret).name() << endl;cout << typeid(p).name() << endl;F(1, 'a');return 0;
}
decltype和auto区别就是,我们的auto一定要用一个类型推导并且必须为我们定义的新变量初始化,而我们的decltype,也可以推导一个类型去定义一个变量,并且可以不为我们的新变量初始化。
如有一些特殊场景:
我们需要只是单纯的定义一个变量
class A
{decltype(malloc) ptr;
};
类模板需要的类型
template<class T>
class B
{T _num;
};int main()
{int x = 1, y = 2;B<decltype(x * y)> b;return 0;
}
总结
- 统一列表初始化:
可以给类初始化,也可以给容器元素初始化,也可以适用于new表达式中,也可以用于数组的元素初始化(不过不使用initializer_list)。
给类初始化是多参数的隐式类型转换;给容器初始化元素是编译器自动将常量列表转换成了initializer_list,各个容器再去支持使用initializer_list初始化或者赋值;
而用于原生数组的元素初始化,是 C++ 核心语言特性的一部分,并不转换为initializer_list。
- 统一的声明变量方式:
auto自动类型推导去定义变量
decltype自动推导表达式的类型去定义变量
都是编译器的行为。
本文章为作者的笔记和心得记录,顺便进行知识分享,有任何错误请评论指点 😃。