[C++]C++11(上)
一、 统一的初始化列表
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 << "Point(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
那么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原来的用法,将其用于实现自动类型推断。同时它要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<map>
using namespace std;
int main()
{
auto i = 1;
auto p = &i;
auto pf = strcpy;
cout << typeid(i).name() << endl;
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
auto it = dict.begin();
//cout << typeid(it).name() << endl;
return 0;
}
2.decltype
关键字decltype将变量的类型声明为表达式指定的类型。
有时候我们需要用一个变量的类型,来声明另外一个变量
但是我们用的是auto我们很难确定它的变量类型
为了达到我们的目的,我们可以这样做:
不过这样做的缺陷就是它还必须得进行定义,但是我们有时候是不需要进行赋值的。
所以就有了这个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的缺陷
void func(int)
{
cout << "int" << endl;
}
void func(void* p)
{
cout << "int* p" << endl;
}
int main()
{
func(NULL);
return 0;
}
主要原因还是在于NULL使用宏定义的
在effective中也提到了尽量使用const enum inline去替代宏
三、范围for
在之前实现STL的时候已经用过了,所以不做介绍了,只需要知道它是C++11的就可以了
四、智能指针
这里我们后面介绍,这里仅需知道它是C++11的
五、STL中的一些变化
1.新容器
圈的是C++11中的一些几个新容器,但是实际最有用的是unordered_map和unordered_set。
array对标的其实就是普通的数组,它两在用法上几乎没有任何区别,甚至是字节大小都一样,唯一不同的就是他们两个的类型不同。这俩个都是静态的数组,但是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实现为单链表;单向链表可以将它们包含的每个元素存储在不同且不相关的存储位置。通过与 link 到 sequence 中下一个元素的每个元素的关联来保持排序。
forward_list容器和列表容器之间的主要设计区别在于,前者在内部仅保留指向下一个元素的链接,而后者为每个元素保留两个链接:一个指向下一个元素,一个指向前一个元素,允许在两个方向上进行高效迭代,但每个元素会消耗额外的存储空间,并且插入和删除元素的时间开销略高。因此,forward_list 对象比 List 对象更高效,尽管它们只能向前迭代。
与其他基本标准序列容器(array、vector 和 deque)相比,forward_list通常在容器内任何位置插入、提取和移动元素方面表现更好,因此在密集使用这些元素的算法(如排序算法)中也表现更好。
与其他序列容器相比,forward_lists 和 lists 的主要缺点是它们无法通过其位置直接访问元素;例如,要访问forward_list中的第六个元素,必须从开头迭代到该位置,这需要在这些元素之间的距离内花费线性时间。它们还会消耗一些额外的内存来保持与每个元素关联的链接信息(对于大型小尺寸元素列表来说,这可能是一个重要因素)。
forward_list类模板在设计时就考虑到了效率:根据设计,它与简单的手写 C 样式单链表一样高效,实际上,它是唯一一个出于效率考虑而故意缺少 size 成员函数的标准容器:由于它作为链表的性质,具有需要恒定时间的大小成员将要求它为其大小保留一个内部计数器(如 list does) 的这将消耗一些额外的存储空间,并使插入和删除作的效率略有降低。要获取 forward_list 对象的大小,可以使用距离算法及其 begin 和 end,这是一个需要线性时间的作。
它的接口如下:
它只支持头插和头删除,因为尾插尾删效率太低。
如果非要用可以使用insert和erase。但是这两个是往该节点后面插入或删除的
2.新接口
第一大变化就是增加了cbegin系列的迭代器
这些迭代器可以返回const迭代器,但是实际begin也可以返回const迭代器,所以这个也是比较鸡肋的,新接口的第二大变化就是所有容器均支支持{}列表初始化的构造函数
这个主要是由initializer_list容器支持的,第三大变化就是emplce接口,不过这里涉及到右值引用,和模板的可变参数,后序我们再介绍。
除了emplace以外,还升级了push_back接口,因为使用了右值引用,使得性能提高了
第四大变化就是,新容器增加了移动构造和移动赋值,部分接口的性能得到了极大的提升