[C++——lesson37.——「C++11『基础新特性』」]


🛸个人主页: dragoooon34
🚁所属专栏: C++
🚀操作环境: Visual Studio 2022

目录
🌞前言
自从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的构造函数,使其不能被编译器隐式优化构造函数
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, 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就是一个新建变量,此时就不会报错了重载了
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?总之,列表初始化 就像一个万金油,得益于 泛型编程,可以轻松进行初始化,并且是 万能初始化,可以在刷题过程中享受一下了
三、😂简化声明
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遍历哈希表时,实际获取的就是哈希表中的pairint 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 🍳RAII 风格
RAII风格由祖师爷 本贾尼 提出,他说 使用局部对象管理资源的技术通常称为“资源获取就是初始化”,这种通用技术依赖于构造函数和析构函数的性质以及它们与异常处理的交互作用
简单来说就是 构造即初始化,析构则销毁,利用对象创建时需要调用 构造函数,生命周期结束时会自动调用 析构函数 的特性
智能指针 就是一个对象,一个在构造时申请资源,析构时释放资源的小工具,仅此而已
5-2 🧇智能指针分类
C++11中的 智能指针 有unique_ptr、shared_ptr和weak_ptr,其中weak_ptr就是shared_ptr的小弟;而unique_ptr与shared_ptr的区别在于 是否支持拷贝如果想传递 智能指针 的话,选择
shared_ptr,否则选择unique_ptr就行了下面简单演示一下
unique_ptr是如何 智能 管理资源的,使用 智能指针 需要包含头文件memoryclass 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为例,谈谈几个升级点1.重载了
initializer_list<T>,使容器初始化更加方便
2.增加
const对象的迭代器获取,也就是cbegin和cend,这玩意其实很鸡肋,因为普通版的begin或end都已经重载了const版本
3.支持移动构造和移动赋值,可以极大提高效率(重点)
4.支持右值引用相关插入接口,同样可以提高效率(重点)
总的来看,
C++11还是更新了不少东西,不过万众期待的 网络库 仍迟迟没有更新,希望网络相关标准库可以尽快更新吧,让C++变得更加强大
C++11的重磅更新为 右值引用和移动语义、lambda表达式、线程库、包装器等,限于篇幅原因,这些重磅更新将会放到后面的文章中详细讲解
🔥总结🔥
以上就是关于 C++11『基础新特性』的全部内容了,在本文中首先介绍了
C++11的背景知识及更新内容,然后介绍了各种常用特性,比如 列表初始化、auto、范围for等,这些都是C++11中的普通特性,令人眼前一亮的特性将会在后续文章中详解
结束语
以上就是我对于【C++】——C++11『基础新特性』」的理解
感谢你的三连支持!!!

更多推荐






























所有评论(0)