C++11『基础新特性』

简介: C++11『基础新特性』

🌇前言

自从C++98以来,C++11无疑是一个相当成功的版本更新。它引入了许多重要的语言特性和标准库增强,为C++编程带来了重大的改进和便利。C++11的发布标志着C++语言的现代化和进步,为程序员提供了更多工具和选项来编写高效、可维护和现代的代码


🏙️正文

1.C++11 简介

1.1.起源

1998C++标准委员会 成立后,计划每五年进行一次更新

2003C++标准委员会 提交了一份 技术勘误表(简称为 TC1),TC1 主要是对 C++98 标准中的漏洞进行修复,其语言的核心部分并没有大改动,这次提交可以看作一次小小的语法更新,即 C++03,但因此人们总是习惯性的将 C++98/03 看作一个标准,多年以来,C++98/03 标准是市面上主要被使用的 C++ 版本

C++标准委员会 计划在 2007 年发布下一个语法版本,并计划命名为 C++07,但是很遗憾,在 2006 年,官方觉得无法在 2007 年如期发布 C++07,并且觉得 2008 年可能也无法完成,于是官方干脆将下一个 C++ 标准命名为 C++0XX 表示有可能在 07、08、09 年完成)。结果时间来到了 2010 年,官方还是没有完成新标准的制定,这时候大部分人觉得 C++ 新标准的发布已经遥遥无期了,最终官方在 2011 年终于完成了新标准的制定,并将新标准命名为 C++11,也就是本文中将要学习的新标准

C++11 足足鸽了六年才发布了一个新版本…要知道隔壁 Java 可是每两年乃至每六个月更新一次新标准,现在最新的版本已经来到了 JDK21

1.2.主要更新

C++11 相对于 C++98/03 来说,带来了数量可观的变化, 其中包含了约 140 个新特性,以及对 C++98/03 中约 600 个缺陷修正,这就使得 C++11 更像是一次变革,变成了一种 “新的语言”(因为 C++11 中的部分操作显得很不 C++

源于 C++11 官网:https://en.cppreference.com/w/cpp/11

相对于上一个标准来说,C++11 能更好的适用于系统开发和库开发:语法变得更加丰富和简单化、更加稳定和安全,总的来说,C++11 变得更强了,作为开发工具能提高程序员的开发效率,并且大多数公司项目都已支持 C++11,所以 C++11 需要重点学习和掌握

除了 C++11 外,后面还陆续推出了 C++14C++17C++20 标准,最新的 C++23 也已经发布,新标准意味着新特性,是需要慢慢适应的,并且 C++14/17 也只是对 C++11 的修复和补充,所以我们着重学习 C++11 即可

以下是不同的编译器对 C++11 语法的支持情况(绿色表示最低支持版本,红色表示不支持)

主流的编译器有:GCCClangMSVC,其中 GCC 就是在 Linux 中使用的编译器,基本上 GCC 4.6 及后续版本就能对 C++11 进行很好的支持,而 MSVC 是微软 VS 系列的编译器,从 VS 2015 及后续版本对 C++11 语法支持较好

推荐使用 VS 2019VS 2022 进行 C++11 新标准的学习

注:C++11 中的新特性众多,本文以及后续文章只是列举常用语法


2.列表初始化

列表初始化 { } 是我们学习的第一个 C++11 新特性,这玩意其实我们在 C语言 阶段就已经使用过了,比如对数组进行初始化 int arr[] = {1, 2, 3}

C++11 中对 { } 进行了全面升级,使其不仅能初始化数组,还能初始化自定义类型,比如 STL 中的容器,这对于编码时初始化是十分友好的

2.1.对于内置类型

首先需要明白,为了适应 泛型编程C++ 中的内置类型(比如 intdouble 等)就已经全部配备了 构造函数,方便在进行模板传参时,传递默认构造值

int main()
{
  // 内置类型基本都配备了构造函数
  int a(10);
  char b('x');
  cout << a << " " << b << endl;
  return 0;
}



C++11 中,扩大了 { } 的适用范围,使其不止能给数组初始化,也能给内置类型初始化

int main()
{
  // 不仅能给数组初始化,也能给内置类型初始化
  int arr[] = { 1, 2, 3 };
  int a = { 10 };
  char b = { 'x' };
  cout << arr[0] << " " << a << " " << b << endl;
  return 0;
}



如何做到的呢?

其实就是当内置类型使用 { } 初始化时,实际上是在调用它的构造函数进行构造

这就不奇怪了,无非就是让内置类型将 { } 也看做一种特殊的构造:构造 + 赋值 优化为 直接构造

我们可以通过一个简单的 日期类 来体现这一现象

简单日期类 Date

// 日期类
class Date
{
public:
  Date(int d, int m, int y)
    :_day(d), _month(m), _year(y)
  {}
private:
  int _day;
  int _month;
  int _year;
};


此时可以直接通过 列表初始化 { } 来初始化日期类对象

int main()
{
  Date d1 = { 2023, 11, 8 };
  return 0;
}


编译运行,并无报错或警告,C++11 中甚至允许省略 = 符号,使其与 拷贝构造函数 一样,直接通过对象构造对象(语法支持,但不推荐这样写,因为容易与 构造函数 混淆)

Date d2{ 2023, 11,8 };


言归正传,接下来证明 列表初始化 实际上就是 构造 + 赋值 优化为 构造,首先是使用 explicit 修饰 Date 的构造函数,使其不能被编译器隐式优化

构造函数 Date — 位于 Date

explicit Date(int d, int m, int y)
  :_day(d), _month(m), _year(y)
{}


接下来同样的代码,尝试编译,结果出现了错误

现在的情况是 d1 列表初始化失败,d2 列表初始化成功

这是因为 d1 是由 构造 + 赋值 优化后进行的构造,而 explicit 关键字可以杜绝编译器这种 隐式 优化行为,编译器无法优化,也就无法构造 d1 了;而 d2 相当于直接调用了 拷贝构造函数,不受优化的影响,也就没啥问题

这里主要是想说明一个东西:对于内置类型来说,列表初始化 { } 实际上就相当于调用了内置类型的构造函数,构造出了一个对象

2.2.对于自定义类型

列表初始化 对于内置类型来说显得多余了,但对自定义类型就不一样了,这玩意能让自定义类型的初始化变得更加简单

举个例子:想要一个内容为 1, 2, 3, 4, 5vector

如果在 C++11 之前,需要先构建一个 vector 对象,然后再 push_back 五次,非常的朴实无华

int main()
{
  // C++11 之前
  vector<int> arr;
  for (int i = 0; i < 5; i++)
    arr.push_back(i + 1);
  return 0;
}


足够麻烦吧?可能有的人会说我们都是直接使用 { } 初始化的,没错,你使用的正是 列表初始化 这个新特性,只是你没有发现罢了

int main()
{
  // C++11 之后
  vector<int> arr = { 1, 2, 3, 4, 5 };
  return 0;
}


不止可以初始化五个数,初始化十个乃至一百一千个都是可以的,显然此时的 列表初始化 调用的不是 vector 的构造函数,因为它的构造函数总不可能重载出 N 个吧?

所以对于诸如 vector 这种自定义类型来说,需要把 列表初始化 视作一个类型,然后重载对这个类型参数的构造函数就行了,于是 initializer_list<T> 类就诞生了,这是一个模板类,大概长这样

支持传入模型参数 T,当我们写出 { 1, 2, 3, 4, 5 } 时,实际上已经构建出了一个 initializer_list<int> 类的匿名对象,可以借助 typeid 查看类型名来证明

int main()
{
  // 自动推导类型
  auto arr = { 1, 2, 3, 4, 5 };
  cout << typeid(arr).name() << endl;
  return 0;
}


结果是 initializer_list<int> 吧?


所以说当我们写出这种东西时:{ T, T, T }

编译器实际已经特殊处理过了,生成了一个模板类型为 T 的匿名对象:initializer_list<T>

当然也是可以直接创建一个 initializer_list<T> 对象来初始化,initializer_list<T> 这个类的构成十分简单,其成员函数仅有 size()begin()end(),也就是支持迭代器遍历其中的数据

细节:initializer_list<T> 类支持迭代器,自然也就支持范围 for 这个新特性,可以试着用一下

格局打开,其他类中只需重载一个类型为 initializer_list<T> 的参数,并在其中通过 initializer_list<T> 对象的迭代器进行数据遍历,就能轻松获取 initializer_list<T> 对象中的数据,所以在 C++11 中,几乎对所有库中的容器进行了更新:新增参数类型为 initializer_list<T> 的构造函数,这里简单举出几个例子


但凡重载了 initializer_list<T> 的构造函数,就能轻松使用 列表初始化 来初始化对象,如果没重载呢?那就不支持,比如拿出我们之前模拟实现的 vector (代码太长了,这里就不放完整代码了,重点在于看现象)

直接就报了一个错误,前面说过,要先支持 列表初始化 也很简单,重载一个参数为 initializer_list<T> 的构造函数就好了,比如这样

重载了 initializer_list<T> 的构造函数 ---- 位于 vector 类(自己模拟实现的)

// 供列表初始化调用
vector(const std::initializer_list<T>& init)
{
  std::initializer_list<T>::iterator it = init.begin();
  while (it != init.end())
  {
    this->push_back(*it);
    ++it;
  }
}


这么一看没啥毛病,但如果一编译就会出问题


这是因为 C++11 提高了安全检查,对于具有二义性的行为是直接拒之门外的,比如这里的

std::initializer_list<T>::iterator it = init.begin();


此时编译器不知道 it 究竟是 std::initializer_list<T>::iterator 中的一个静态变量,还是一个迭代器类型,所以编译器直接选择了报错,如果是在 C++11 之前,可能可以成功编译,这是因为检查不严格

要想解决问题就需要使用 typename 关键字,直接告诉编译器:std::initializer_list<T>::iterator 就是一个类型,并且 it 就是一个新建变量,此时就不会报错了

重载了 initializer_list<T> 的构造函数 ---- 位于 vector 类(自己模拟实现的)

// 供列表初始化调用
vector(const std::initializer_list<T>& init)
{
  typename std::initializer_list<T>::iterator it = init.begin();
  while (it != init.end())
  {
    this->push_back(*it);
    ++it;
  }
}


此时再编译,我们自己模拟实现的 vector 就能支持 列表初始化 了,C++11 对库中类的更新也是如此,并不神秘

库中不仅新增了对 initializer_list<T> 的构造重载,也顺便更新了对 initializer_list<T> 的赋值重载,所以是可以直接将一个 initializer_list<T> 对象赋值给容器对象的


2.3.高效的玩法

为什么说 列表初始化 是个好东西呢?

因为它可以帮我省很多初始化方面的事,比如对 pair 对象的初始化

int main()
{
  // 快速构建一个词典
  unordered_map<string, string> hash =
  {
    {"banana", "香蕉"},
    {"apple", "苹果"},
    {"pear", "梨"}
  };
  // 亦或是快速插入
  hash.insert({ "watermelon", "西瓜" });
  return 0;
}

有了这玩意,还要什么 make_pair

总之,列表初始化 就像一个万金油,得益于 泛型编程,可以轻松进行初始化,并且是 万能初始化,可以在刷题过程中享受一下了


3.简化声明

C++11 省去了很多麻烦事,可以让用户在使用时更加轻松,这也让 C++ 显得不那么 C++(做了很多用户看不见的操作),顺应时代发展变味了,比如接下来这几个声明,就是 C++11 为了简化模板操作时的补丁


3.1.auto 自动推导类型

auto 意味自动,这个关键字早在 C++98 中就已经存在了,主要用来 表明变量是局部自动存储类型,但如今在局部域中定义的局部变量默认就是自动存储类型,因此原来的 auto 显得很没用

组委会在 C++11 中废弃原来的用法,对 auto 进行了重新设计,使其摇身一变,成为一个非常好用且实用的关键字:根据待赋给变量的参数,自动推导其参数类型,用户无需关心该变量要定义为什么类型

这就好比看见张三就知道这是一个人,不用带着人的概念去见张三

auto 常常用于推导 复杂类型

比如哈希表中的迭代器

int main()
{
  unordered_map<int, int> hash = { {1, 1} };
  auto it = hash.begin();
  cout << typeid(it).name() << endl;
  return 0;
}


可以看到 it 的类型非常非常长,就问你如果手动定义这么一个类型的变量,方便吗?


有了 auto 就不用担心了,直接从手动挡变成了自动挡,什么半坡起步不是轻松拿捏

不过使用 auto 也得注意以下几点:

  1. auto 定义的变量必须是显示实例化的,也就是 = 右边的变量类型是可知的
  2. auto 不能作为参数类型


3.2.decltype 获取推导类型

除了 auto 这个自动挡外,C++11 还提供了另一个自动挡 decltype,不过这个自动挡使用起来比较麻烦,需要指明参数,才能推导出类型

int main()
{
  unordered_map<int, int> hash = { {1, 1} };
  auto it = hash.begin();
  decltype(it) tmp;
  cout << typeid(tmp).name() << endl;
  return 0;
}



decltypeauto 方便的一点是 decltype 无需显式实例化,也就是单纯定义也行

decltype 还可以作为模板参数传递,而 auto 不行

// decltype 可以推导出参数类型,并进行传递
vector<decltype(it)> v1;


auto 方便,decltype 更强大,但使用更麻烦,可以根据具体需求灵活使用


3.3.nullptr 空值补丁

祖师爷在设计 C++ 时,留下了个空值 NULL 的坑,不小心把 0 设成了 指针空值,同时也设置成了 整型空值,这是典型的二义性,在进行参数传递时,编译器无法区别

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif


于是为了填补这个坑,组委会在 C++11 中推出了空值补丁 nullptr,专门用来表示 指针空值,以后想把指针赋为空指针时,可以使用 nullptr


4.范围 for

范围 for 是一块语法糖,使用起来及其舒适,可以一键遍历容器中的值,如此申请的语法,背后其实就是对迭代器遍历的封装

简单使用范围 for 遍历链表

int main()
{
  // 使用列表初始化
  list<int> l = { 1, 2, 3, 4, 5 };
  for (auto e : l)
    cout << e << " ";
  return 0;
}



范围 for 的语法为

for(类型 值 : 容器)
{
  // 对值进行操作(默认不可被修改)
}


配合 auto 自动推导类型,范围 for 就会变得非常香

范围 for 的本质其实就是 迭代器 遍历,只要容器支持 迭代器,那么就可以支持范围 for

比如使用 范围 for 遍历哈希表时,实际获取的就是哈希表中的 pair

int main()
{
  unordered_map<int, int> hash = { {1, 1}, { 2, 2 } };
  for (auto it : hash)
    cout << it.first << " " << it.second << endl;
  return 0;
}



注意:范围 for 中获取的值,默认是不可被修改的,如果想要修改,需要使用 引用类型 获取值

接下来演示使用 范围 for 修改容器中的值,并打印进行对比

int main()
{
  // 使用列表初始化
  list<int> l = { 1, 2, 3, 4, 5 };
  for (auto& e : l)
  {
    cout << e << " ";
    e++;
  }
  cout << endl;
  for (auto e : l)
    cout << e << " ";
  return 0;
}


可以看到 list 中的值已经被修改了


5.智能指针

智能指针 这个名词听着挺唬人,其实也没啥,无非就是会自动销毁 new 出来的对象,对于日常使用来说,还是挺方便的,毕竟 C/C++ 可没有隔壁 Java 的垃圾回收机制 GC,得自己清理垃圾, 智能指针 可以自动完成垃圾清理这个工作

5.1.RAII 风格

RAII 风格由祖师爷 本贾尼 提出,他说 使用局部对象管理资源的技术通常称为“资源获取就是初始化”,这种通用技术依赖于构造函数和析构函数的性质以及它们与异常处理的交互作用

简单来说就是 构造即初始化,析构则销毁,利用对象创建时需要调用 构造函数,生命周期结束时会自动调用 析构函数 的特性

智能指针 就是一个对象,一个在构造时申请资源,析构时释放资源的小工具,仅此而已

5.2.智能指针分类

C++11 中的 智能指针unique_ptrshared_ptrweak_prr,其中 weak_ptr 就是 shared_ptr 的小弟;而 unique_ptrshared_ptr 的区别在于 是否支持拷贝

如果想传递 智能指针 的话,选择 shared_ptr,否则选择 unique_ptr 就行了

下面简单演示一下 unique_ptr 是如何 智能 管理资源的,使用 智能指针 需要包含头文件 memory

class A
{
public:
  A()
  {
    cout << "调用了构造函数" << endl;
  }
  ~A()
  {
    cout << "调用了析构函数" << endl;
  }
};
int main()
{
  unique_ptr<A> ptr(new A);
  return 0;
}


可以看到析构函数确实被调用了,证明资源已经被销毁了

关于 智能指针 还有很多知识,后面会专门出一篇文章来详谈 智能指针,这里就不再赘述


6.STL容器变化

C++11 不仅更新了 C++ 语法,还更新了 STL 库,作为 C++ 联邦中的重要成员,STL 库是编程时必不可少的利器,不仅好用,而且高效


6.1.新增容器

C++11STL 增加了几种新容器,比如之前已经模拟实现过的 unordered_mapunordered_set 就是新增的容器,C++11 中共新增了这四种容器

array 是一个静态数组,使用时需要像 C语言 中的数组一样确定大小,后续使用时无法插入或删除数据,array 提供的接口如下

对比 C语言 传统静态数组,进行了以下升级

  • 面向对象,成为一个单独的类
  • 提供迭代器,支持通过迭代器遍历
  • 可以更轻易获取大小信息
  • 对于数据的访问方式更加丰富,同时下标随机访问时,安全性更高
  • 支持其他功能:判满、交换

这么看来似乎是全面升级,但别忘了,vector 是全面碾压 arrayvector 配合 resize 或者 reserve,也能做到提前开辟容量,同时 vector 接口更加丰富,兼容性也更好

所以实际上 array 很少用,这种东西仁者见仁智者见智吧


再来说说另一个新增容器 forward_list,传统的 list 是一个双向循环链表,支持 首尾操作,而 forward_list 是一个很单纯的 单链表,并且是一个不支持尾部操作的 单链表,尽管它提供任意位置插入/删除的接口,但就是没有明着提供尾部操作接口

forward_list 只有一个指针,节省空间,同时头部操作效率不错,但是我们日常中都是不缺内存的,所以 list 会更加方便

至于 unordered_mapunordered_set 就不再细谈了,无非就是 哈希表 的实际运用,效率极高

6.2.新增接口

除了新增容器,还给原来的容器进行了接口方面的升级,这里以 vector 为例,谈谈几个升级点

1.重载了 initializer_list<T>,使容器初始化更加方便

2.增加 const 对象的迭代器获取,也就是 cbegincend,这玩意其实很鸡肋,因为普通版的 beginend 都已经重载了 const 版本

3.支持移动构造和移动赋值,可以极大提高效率(重点)

4.支持右值引用相关插入接口,同样可以提高效率(重点)

总的来看,C++11 还是更新了不少东西,不过万众期待的 网络库 仍迟迟没有更新,希望网络相关标准库可以尽快更新吧,让 C++ 变得更加强大

C++11 的重磅更新为 右值引用和移动语义、lambda表达式、线程库、包装器等,限于篇幅原因,这些重磅更新将会放到后面的文章中详细讲解


🌆总结

以上就是关于 C++11『基础新特性』的全部内容了,在本文中首先介绍了 C++11 的背景知识及更新内容,然后介绍了各种常用特性,比如 列表初始化、auto、范围 for 等,这些都是 C++11 中的普通特性,令人眼前一亮的特性将会在后续文章中详解


相关文章推荐

C++ 进阶知识

C++ 哈希的应用【布隆过滤器】

C++ 哈希的应用【位图】

C++【哈希表的完善及封装】

C++【哈希表的模拟实现】

C++【初识哈希】

C++【一棵红黑树封装 set 和 map】
目录
相关文章
|
2月前
|
编译器 程序员 定位技术
C++ 20新特性之Concepts
在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。 在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪
134 59
|
1月前
|
安全 编译器 C++
【C++11】新特性
`C++11`是2011年发布的`C++`重要版本,引入了约140个新特性和600个缺陷修复。其中,列表初始化(List Initialization)提供了一种更统一、更灵活和更安全的初始化方式,支持内置类型和满足特定条件的自定义类型。此外,`C++11`还引入了`auto`关键字用于自动类型推导,简化了复杂类型的声明,提高了代码的可读性和可维护性。`decltype`则用于根据表达式推导类型,增强了编译时类型检查的能力,特别适用于模板和泛型编程。
24 2
|
2月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(三)
【C++】面向对象编程的三大特性:深入解析多态机制
|
2月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(二)
【C++】面向对象编程的三大特性:深入解析多态机制
|
2月前
|
编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(一)
【C++】面向对象编程的三大特性:深入解析多态机制
|
2月前
|
存储 安全 编译器
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(一)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
2月前
|
C++
C++ 20新特性之结构化绑定
在C++ 20出现之前,当我们需要访问一个结构体或类的多个成员时,通常使用.或->操作符。对于复杂的数据结构,这种访问方式往往会显得冗长,也难以理解。C++ 20中引入的结构化绑定允许我们直接从一个聚合类型(比如:tuple、struct、class等)中提取出多个成员,并为它们分别命名。这一特性大大简化了对复杂数据结构的访问方式,使代码更加清晰、易读。
44 0
|
3月前
|
编译器 C++ 计算机视觉
C++ 11新特性之完美转发
C++ 11新特性之完美转发
58 4
|
3月前
|
Java C# C++
C++ 11新特性之语法甜点1
C++ 11新特性之语法甜点1
37 4
|
3月前
|
安全 程序员 编译器
C++ 11新特性之auto和decltype
C++ 11新特性之auto和decltype
48 3