前言
自从C++98以来,C++11无疑是一个相当成功的版本更新。它引入了许多重要的语言特性和标准库增强,为C++编程带来了重大的改进和便利。C++11的发布标志着C++语言的现代化和进步,为程序员提供了更多工具和选项来编写高效、可维护和现代的代码
一、C++11 简介
1.1 起源
1998
年C++标准委员会
成立后,计划每五年进行一次更新
在 2003
年 C++标准委员会
提交了一份 技术勘误表(简称为 TC1
),TC1
主要是对 C++98
标准中的漏洞进行修复,其语言的核心部分并没有大改动,这次提交可以看作一次小小的语法更新,即 C++03
,但因此人们总是习惯性的将 C++98/03
看作一个标准,多年以来,C++98/03
标准是市面上主要被使用的 C++
版本
C++标准委员会
计划在 2007
年发布下一个语法版本,并计划命名为 C++07
,但是很遗憾,在 2006
年,官方觉得无法在 2007
年如期发布 C++07
,并且觉得 2008
年可能也无法完成,于是官方干脆将下一个 C++
标准命名为 C++0X
(X
表示有可能在 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++14
、C++17
、C++20
标准,最新的C++23
也已经发布,新标准意味着新特性,是需要慢慢适应的,并且C++14/17
也只是对C++11
的修复和补充,所以我们着重学习C++11
即可
以下是不同的编译器对 C++11
语法的支持情况(绿色表示最低支持版本,红色表示不支持)
主流的编译器有:GCC
、Clang
、MSVC
,其中 GCC
就是在 Linux
中使用的编译器,基本上 GCC 4.6
及后续版本就能对 C++11
进行很好的支持,而 MSVC
是微软 VS
系列的编译器,从 VS 2015
及后续版本对 C++11
语法支持较好
推荐使用 VS 2019
或 VS 2022
进行 C++11
新标准的学习
注:C++11
中的新特性众多,本文以及后续文章只是列举常用语法
二、列表初始化
列表初始化 { }
是我们学习的第一个 C++11
新特性,这玩意其实我们在 C语言
阶段就已经使用过了,比如对数组进行初始化 int arr[] = {1, 2, 3}
C++11
中对 { }
进行了全面升级,使其不仅能初始化数组,还能初始化自定义类型,比如 STL
中的容器,这对于编码时初始化是十分友好的
2.1 对于内置类型
首先需要明白,为了适应 泛型编程,C++
中的内置类型(比如 int
、double
等)就已经全部配备了 构造函数,方便在进行模板传参时,传递默认构造值
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
的构造函数,使其不能被编译器隐式优化
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, 5
的 vector
如果在 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
就是一个新建变量,此时就不会报错了
// 供列表初始化调用 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
?
总之,列表初始化 就像一个万金油,得益于 泛型编程,可以轻松进行初始化,并且是 万能初始化,可以在刷题过程中享受一下了
三、简化声明
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
也得注意以下几点:
auto
定义的变量必须是显示实例化的,也就是=
右边的变量类型是可知的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; }
decltype
比 auto
方便的一点是 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
四、范围 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
中的值已经被修改了
五、智能指针
智能指针 这个名词听着挺唬人,其实也没啥,无非就是会自动销毁 new
出来的对象,对于日常使用来说,还是挺方便的,毕竟 C/C++
可没有隔壁 Java
的垃圾回收机制 GC
,得自己清理垃圾, 智能指针 可以自动完成垃圾清理这个工作
5.1 RALL 风格
RAII
风格由祖师爷 本贾尼 提出,他说 使用局部对象管理资源的技术通常称为“资源获取就是初始化”,这种通用技术依赖于构造函数和析构函数的性质以及它们与异常处理的交互作用
简单来说就是 构造即初始化,析构则销毁,利用对象创建时需要调用 构造函数,生命周期结束时会自动调用 析构函数 的特性
智能指针 就是一个对象,一个在构造时申请资源,析构时释放资源的小工具,仅此而已
5.2 智能指针分类
C++11
中的 智能指针 有 unique_ptr
、shared_ptr
和 weak_prr
,其中 weak_ptr
就是 shared_ptr
的小弟;而 unique_ptr
与 shared_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; }
可以看到析构函数确实被调用了,证明资源已经被销毁了
关于 智能指针 还有很多知识,后面会专门出一篇文章来详谈 智能指针,这里就不再赘述
六、STL容器变化
C++11
不仅更新了 C++
语法,还更新了 STL
库,作为 C++
联邦中的重要成员,STL
库是编程时必不可少的利器,不仅好用,而且高效
6.1 新增容器
C++11
为 STL
增加了几种新容器,比如之前已经模拟实现过的 unordered_map
和 unordered_set
就是新增的容器,C++11
中共新增了这四种容器
array
是一个静态数组,使用时需要像 C
语言 中的数组一样确定大小,后续使用时无法插入或删除数据,array
提供的接口如下
对比 C
语言 传统静态数组,进行了以下升级
- 面向对象,成为一个单独的类
- 提供迭代器,支持通过迭代器遍历
- 可以更轻易获取大小信息
- 对于数据的访问方式更加丰富,同时下标随机访问时,安全性更高
- 支持其他功能:判满、交换
这么看来似乎是全面升级,但别忘了,vector
是全面碾压 array
,vector
配合 resize
或者 reserve
,也能做到提前开辟容量,同时 vector
接口更加丰富,兼容性也更好
所以实际上 array
很少用,这种东西仁者见仁智者见智吧
再来说说另一个新增容器 forward_list
,传统的 list
是一个双向循环链表,支持 首尾操作,而 forward_list
是一个很单纯的 单链表,并且是一个不支持尾部操作的 单链表,尽管它提供任意位置插入/删除的接口,但就是没有明着提供尾部操作接口
forward_list
只有一个指针,节省空间,同时头部操作效率不错,但是我们日常中都是不缺内存的,所以 list
会更加方便
至于 unordered_map
和 unordered_set
就不再细谈了,无非就是 哈希表 的实际运用,效率极高
6.2 新增接口
除了新增容器,还给原来的容器进行了接口方面的升级,这里以 vector
为例,谈谈几个升级点
- 重载了
initializer_list<T>
,使容器初始化更加方便
- 增加
const
对象的迭代器获取,也就是cbegin
和cend
,这玩意其实很鸡肋,因为普通版的begin
或end
都已经重载了const
版本
- 支持移动构造和移动赋值,可以极大提高效率(重点)
- 支持右值引用相关插入接口,同样可以提高效率(重点)
总的来看,C++11
还是更新了不少东西,不过万众期待的 网络库 仍迟迟没有更新,希望网络相关标准库可以尽快更新吧,让 C++
变得更加强大
C++11
的重磅更新为 右值引用和移动语义、lambda
表达式、线程库、包装器等,限于篇幅原因,这些重磅更新将会放到后面的文章中详细讲解