一、 统一的初始化列表
1.{}列表初始化
在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扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,一切皆可使用{}初始化,使用{}初始化时,可添加等号(=),也可不添加。
struct Point { Point(int x,int y) :_x(x) ,_y(y) { cout << "Ponint(int x, int y)" << endl; } int _x; int _y; }; int main() { int x = 1; int y = { 2 }; int z{ 3 }; int a1[] = { 1,2,3 }; int a2[]{ 1,2,3 }; Point p0(1, 2); Point p1 = { 1, 2 }; Point p2{ 1,2 }; return 0; }
而且使用这样的{}会去调用构造函数
同时这样的话就可以应用于new中
int* ptr1 = new int[3] {1, 2, 3}; Point* ptr2 = new Point[2]{p0,p1}; Point* ptr3 = new Point[2]{ {1,1},{0,0} };
当然建议日常定义的时候,还是不要去掉=,因为可能显得比较奇怪,降低了一点可读性。
其实上面这些用{}的本质是一个多参数的隐式类型转换,因为之前string中的单参数的隐式类型转换
string s = "xxxxx";
如果我们在类的构造函数前加一个explicit关键字,那么就无法使用{}这样进行初始化了,因为explicit关键字可以防止隐式类型转换
再比如,我们可以使用引用进行验证,如果没有explicit关键字,这个引用还可以编译通过
这里我们还必须要加const,因为隐式类型转换要产生一个临时对象,这个临时对象具有常性
2.initializer_list
我们先来看一下下面两段代码是同一个语法吗?
struct Point { Point(int x, int y) :_x(x) ,_y(y) { cout << "Ponint(int x, int y)" << endl; } int _x; int _y; }; int main() { vector<int> v = { 1,2,3,4,5,6,7 }; Point p = { 1,2 }; return 0; }
其实不是的,对于vector,它后面的花括号参数是可以改变的,而对于Point,它后面的花括号参数是不可以改变的。
所以说,这两个其实是利用不同的规则进行初始化的。
第二个我们好理解,就是多参数的隐式类型转换。
那么第一个是咋回事呢?其实C++11做了这样一件事。它新增了一个类型,叫做initializer_list
它只有三个成员函数,也就是迭代器和size
那么initializer_list是如何实现的呢?
其实我们可以认为它的底层是这样实现的
template<class T> class initializer_list { private: const T* _start; const T* _finish; }
然后我们赋值时候所给的数组其实是存储在常量区的,当我们赋值的时候,这两个指针其实一个指向常量区的开始,一个指向常量区的结尾
所以当我们打印这个类型的大小的时候,我们会发现,在32位下是8字节
还有一点需要切记的是,这样做编译器是不支持的,虽然字符串支持这样操作,我们可以认为这样会与initializer_list产生冲突,因为{}已经被识别为了initializer_list了
其实上面的{}赋值给initializer_list本质上还是调用它的构造函数
那么vector为什么可以直接接收initializer_list的类型呢?
其实本质上是vector写了一个构造函数,即支持使用initializer_list初始化的构造函数。
这个构造函数也是非常好想的
vector(initializer_list<value_type> il) { reserve(il.size()); for(auto& e : il) { push_back(e); } }
所以现在也解释了为什么vector看上去使用{}初始化可以有任意个类型,其实是两次构造函数得到的
如下是在我们原本的vector容器中进行改造的。
不仅仅是vector中可以这样使用,在map中也有initializer_list初始化
这样在map中这样用其实比较有点意思,首先map的插入需要的是pair类型,所以实际上里层的两个花括号是多参数的隐式类型转换,将两个字符串转化为pair类型,然后外层的花括号就是initializer_list了
二、声明
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(); //cout << typeid(dict).name() << endl; return 0; }
2.decltype
关键字decltype将变量的类型声明为表达式指定的类型。
有时候我们需要用一个变量的类型,来声明另外一个变量
但是我们千万不可以这样做,因为这个typeid出来的仅仅只是一个字符串,而不是类型
为了达到我们的目的,我们可以这样做,不过这样做的缺陷就是它还必须得进行定义,但是我们有时候是不需要进行赋值的。
所以就有了这个decltype关键字,它可以取出类型
还有一种场景是在类里面的
还有这样的场景,在类模板的场景
而且decltype还可以推导表达式的类型
总结:
- typeid推出的类型仅仅是一个字符串,只能看不能用
- decltype推出对象的类型,可以定义变量,模板传参
3.nullptr
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif
如下就是NULL的缺陷
主要原因还是在于NULL使用宏定义的
在effective中也提到了尽量使用const enum inline去替代宏
三、范围for
关于这一点,在之前实现STL的时候已经十分熟练了,所以就不做介绍了,我们只需要知道它是C++11的就可以了
四、智能指针
这里我们后面介绍,这里仅需知道它是C++11的
五、STL中的一些变化
1.新容器
用橘色圈起来是C++11中的一些几个新容器,但是实际最有用的是unordered_map和unordered_set。
array对标的其实就是普通的数组,它两在用法上几乎没有任何区别,甚至是字节大小都一样,唯一不同的就是他们两个的类型不同。这俩个都是静态的数组
int main() { int a1[10]; array<int, 10> a2; cout << sizeof(a1) << endl; cout << sizeof(a2) << endl; cout << typeid(a1).name() << endl; cout << typeid(a2).name() << endl; return 0; }
虽然这两个用起来没有任何区别,但是array对于[]运算符重载要比普通的更严格一些
下面代码是普通数组,越界却没有任何报错,因为其本质是指针的解引用
下面代码是对于array的使用,其越界后会强制报错,主要原因就是它的[]运算符本质是operator[]函数的调用,内部会有检查的
不过总体说array还是比较鸡肋的,因为我们更喜欢使用vector,而且它还可以初始化
vector<int> v(10,0);
还有一个就是forward_list
Forward_list是序列容器,允许在序列中的任何位置进行常量时间的插入和擦除操作。
转发列表被实现为单链表;单链表可以将它们包含的每个元素存储在不同且不相关的存储位置。顺序是通过链接到序列中下一个元素的每个元素的关联来保持的。
forward_list容器和列表容器在设计上的主要区别在于,前者在内部只保留一个指向下一个元素的链接,而后者为每个元素保留两个链接:一个指向下一个元素,一个指向前一个元素,允许在两个方向上进行有效的迭代,但每个元素都要消耗额外的存储空间,插入和删除元素的时间开销略高。因此,Forward_list对象比列表对象更有效,尽管它们只能向前迭代。
与其他基本标准序列容器(array、vector和deque)相比,forward_list在插入、提取和移动容器内任意位置的元素方面通常表现更好,因此在大量使用这些元素的算法(如排序算法)中也表现更好。
与其他序列容器相比,forward_lists和lists的主要缺点是它们无法通过位置直接访问元素;例如,要访问forward_list中的第六个元素,必须从开始迭代到该位置,这需要在两者之间的距离上花费线性时间。它们还消耗一些额外的内存来保存与每个元素相关联的链接信息(对于包含小元素的大型列表来说,这可能是一个重要因素)。
forward_list类模板在设计时考虑到了效率:通过设计,它与简单的手写c风格单链表一样高效,事实上,它是唯一一个出于效率考虑而故意缺少size成员函数的标准容器:由于其作为链表的性质,拥有一个花费常量时间的size成员将要求它为其大小保留一个内部计数器(就像list一样)。这将消耗一些额外的存储空间,并使插入和删除操作的效率稍微降低。要获取forward_list对象的大小,可以使用带有开始和结束的距离算法,这是一个需要线性时间的操作。
它的接口如下
它只支持头插和头删除,因为尾插尾删效率太低。
如果非要用可以使用insert和erase。但是这两个是往该节点后面插入或删除的。
2.新接口
第一大变化就是增加了cbegin系列的迭代器
这些迭代器可以返回const迭代器,但是实际上begin也可以返回const迭代器,所以这个也是比较鸡肋的
新接口的第二大变化就是所有容器均支支持{}列表初始化的构造函数
这个主要是由initializer_list容器支持的。
第三大变化就是emplce接口,不过这里涉及到右值引用,和模板的可变参数,后序会介绍
除了emplace以外,还升级了push_back接口,因为使用了右值引用,使得性能提高了
第四大变化就是,新容器增加了移动构造和移动赋值,部分接口的性能得到了极大的提升