【C++从0到王者】第三十九站:C++11(全文三万字,超详解)(上)

简介: 【C++从0到王者】第三十九站: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 << "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还可以推导表达式的类型

总结:

  1. typeid推出的类型仅仅是一个字符串,只能看不能用
  2. 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接口,因为使用了右值引用,使得性能提高了

第四大变化就是,新容器增加了移动构造和移动赋值,部分接口的性能得到了极大的提升

相关文章
|
1月前
|
安全 程序员 Linux
【C++】—— c++11之智能指针
【C++】—— c++11之智能指针
|
1月前
|
并行计算 安全 程序员
【C++】—— C++11之线程库
【C++】—— C++11之线程库
|
1月前
|
编译器 C++
【C++】—— c++11新的类功能
【C++】—— c++11新的类功能
|
1月前
|
存储 安全 编译器
【C++】—— 简述C++11新特性
【C++】—— 简述C++11新特性
|
1月前
|
存储 C++
【C++初阶】第三站:类和对象(中) -- 日期计算器
【C++初阶】第三站:类和对象(中) -- 日期计算器
|
1月前
|
算法 编译器 C++
【C++】—— c++11新特性之 lambda
【C++】—— c++11新特性之 lambda
|
1月前
|
安全 算法 程序员
【C++ 空指针的判断】深入理解 C++11 中的 nullptr 和 nullptr_t
【C++ 空指针的判断】深入理解 C++11 中的 nullptr 和 nullptr_t
50 0
|
1月前
|
安全 编译器 程序员
【C++ 11 模板和泛型编程的应用以及限制】C++11 模板与泛型深度解析:从基础到未来展望
【C++ 11 模板和泛型编程的应用以及限制】C++11 模板与泛型深度解析:从基础到未来展望
78 0
|
1月前
|
编译器
C++11 静态断言(static_assert)的介绍:介绍静态断言(static assert)在C++11 中的作用和使用方法
C++11 静态断言(static_assert)的介绍:介绍静态断言(static assert)在C++11 中的作用和使用方法
14 0
|
1月前
|
安全 Java Unix
【C++ 包裹类 std::thread】探索C++11 std::thread:如何使用它来创建、销毁和管理线程
【C++ 包裹类 std::thread】探索C++11 std::thread:如何使用它来创建、销毁和管理线程
44 0