status
Published
slug
modern-cpp-1
type
Post
category
Technology
date
May 18, 2023
tags
C++
笔记
summary
第1-15章节的重点知识记录
第1章 新基础类型(C++11~C++20)第2章 内联和嵌套命名空间(C++11~C++20)第3章 auto占位符(C++11~C++17)第4章 decltype说明符(C++11~C++17)第5章 函数返回类型后置(C++11)第6章 右值引用第7章 lambda表达式(C++11~C++20)第8章 非静态数据成员默认初始化(C++11 C++20)第9章 列表初始化第10章 默认和删除函数第11章 非受限联合类型(C++11)第12章 委托构造函数(C++11)第13章 继承构造函数(C++11)第14章 强枚举类型(C++11 C++17 C++20)第15章 扩展的聚合类型(C++17 C++20)
第1章 新基础类型(C++11~C++20)
- 整数类型long long
- 新字符类型char8_t,char16_t,char32_t
- 明确规定了所占内存空间的大小,让代码在任何平台上都能有一致的表现
- 字符串新类型:u16string,u32string,wstring
第2章 内联和嵌套命名空间(C++11~C++20)
- 内联命名空间的定义和使用
- 内联命名空间的使用:如上所示,可以用于帮助库作者无缝升级库代码,使用命名空间管理接口版本
- 内联命名空间只能有一个(避免二义性)
- 嵌套命名空间的简化语法
第3章 auto占位符(C++11~C++17)
- auto关键字:声明变量时根据初始化表达式自动推断变量类型、声明函数时函数返回值的占位符
- 使用auto占位符声明变量时必须初始化变量
- 注意要点
- 声明多变量时,从左往右推导,不一致报错
- 使用表达能力更强类型
- 不能声明非静态成员变量
- C++20前,不能在形参中使用auto
- auto推导规则:
- auto声明变量按值初始化,推导出的类型会忽略const和volatile限定符
- auto声明变量初始化时,目标对象如果是引用,引用属性会被忽略
- auto和万能引用声明变量时,对于左值会将auto推导为引用类型
- 使用auto声明变量,如果目标对象是一个数组或者函数,则auto会被推导为对应的指针类型
- 当auto关键字与列表初始化组合时,这里的规则有新老两个版本,这里只介绍新规则(C++17标准)。
- 直接使用列表初始化,列表中必须为单元素,否则无法编译,auto类型被推导为单元素的类型。
- 用等号加列表初始化,列表中可以包含单个或者多个元素,auto类型被推导为std::initializer_list<T>,其中T是元素类型。请注意,在列表中包含多个元素的时候,元素的类型必须相同,否则编译器会报错。
- 什么时候使用auto
- 一眼就能看出声明变量初始化类型的时候
- 对于复杂类型(lambda表达式、bind等),直接使用auto
- 返回类型推导
- 保证所有返回值的类型是相同的
- lambda表达式中使用auto类型推导
- 实现泛型的lambda表达式
- 非类型模板形参占位符
第4章 decltype说明符(C++11~C++17)
- typeof:用于获取对象类型
- typeid:用于获取与目标操作数类型有关的信息
- typeid返回值是一个左值,且其生命周期一直被扩展到程序生命周期结束
- typeid返回的std::type_info删除了复制构造函数,若想保存std::type_info,只能获取其引用或指针
- typeid的返回值总是忽略类型的cv限定符,typeid(const T)==typeid(T))
- decltype说明符:获取对象或者表达式的类型
- decltype的推导规则:
- 如果e是一个未加括号的标识符表达式(结构化绑定除外)或者未加括号的类成员访问,则decltype(e)推断出的类型是e的类型T。如果并不存在这样的类型,或者e是一组重载函数,则无法进行推导。
- 如果e是一个函数调用或者仿函数调用,那么decltype(e)推断出的类型是其返回值的类型。
- 如果e是一个类型为T的左值,则decltype(e)是T&。
- 如果e是一个类型为T的将亡值,则decltype(e)是T&&。
- 除去以上情况,则decltype(e)是T。
- C++中的左值和右值:
- 左值可以被修改,是可寻址的变量,有持久性
- 右值不能修改,一般为不可寻址的常量,或在表达式求值过程中的无名临时对象,是短暂性的
- decltype(e),当e是加括号的数据成员时,父对象表达式的cv限定符会同步到推断结果,反之cv限定符会被忽略
- decltype(auto)
第5章 函数返回类型后置(C++11)
- 函数返回类型后置声明函数
- 可以用于返回比较复杂的类型,例如函数指针类型(
int(*)(int)
)
- 推导返回类型的函数
- decltype说明符不能写在函数声明前
第6章 右值引用
右值引用是C++11标准提出的一个非常重要的概念,它的出现不仅完善了C++的语法,改善了C++在数据转移时的执行效率,同时还增强了C++模板的能力。
我们应该灵活运用右值引用,避免在程序里出现无谓的复制,提高程序的运行效率。
- 左值和右值:
- 左值:一般是指一个指向特定内存的具有名称的值(具名对象),它有一个相对稳定的内存地址,并有一段较长的生命周期
- 右值:是不指向稳定内存地址的匿名值(不具名对象),生命周期很短,是暂时性的
- 通常字面量都是右值(字符串字面量除外)
x++
是右值,++x
是左值- 常量左值引用可以绑定(引用)右值
- 缺点:一旦使用常量左值引用,表明无法在函数内修改对象的内容
⇒ 右值引用,可以帮助完成这项工作
- 右值引用:一种引用右值且只能引用右值的方法
- 右值引用的特点:
- 延长右值的生命周期
- 使用目的,减少对象复制,提升程序性能
- 移动语义:可以帮助我们把临时对象的内存移动到其他对象中,以避免内存数据的复制
- 可以大幅度提高性能,减少内存复制对性能的消耗
- 移动构造函数:接受的参数为右值,核心思想是通过转移实参对象的数据以达成构造目标对象的目的
- 值类别:
- C++11标准中引入的新概念,值类别是表达式的一种属性,该属性将表达式分为三种类别
- 左值(lvalue)
- 右值(rvalue)
- 将亡值(xvalue)
- C++17中的分类
- 泛左值:指一个通过评估能够确定对象、位域或函数的标识表达式,确定了对象或函数的标识(具名对象)
- 将亡值:标识资源可以被重用的对象和位域,通常接近其生命周期的末尾,也可能是经过右值引用转换产生的
- 使用类型转换将泛左值转换为该类型的右值引用
- 临时量实质化,将纯右值转换到临时对象
- 左值:指非将亡值的泛左值
- 右值:
- 纯右值:指一个通过评估能够用于初始化对象和位域,能够计算运算符操作数的值的表达式
- 将亡值
将亡值的产生
- 将左值转换为右值
- 将左值通过static_cast转化为将亡值,之后再绑定到右值引用
- 可以使用std::move函数模板将左值转换为右值
- 会根据传参类型自动推导返回类型,省去了指定转换类型的代码
- 万能引用:使用了类型推导
- 引用折叠的推导规则:只要有左值引用参与,那么结果就是左值引用,实际类型是一个非引用类型或者右值引用类型,最后的结果才是右值引用
- 完美转发:万能引用最典型的用途
- 无论实参类型为何,都可以正确转发,且不会发生多余的临时复制
- 在C++11的标准库中提供了std::forward函数目标,实现了完美转发功能。
- std::forward会根据左值和右值的实际情况进行转发,在使用的时候需要指定模板实参。
- 针对局部变量和右值引用的隐式移动操作
- 编译器完成,提高运行效率
- 隐式移动的发生:新标准中编译器隐式地采用移动构造函数完成数据交换
- 可隐式移动的对象必须是一个非易失或一个右值引用的非易失自动存储对象,在以下情况下可以使用移动代替复制。
- return或者co_return语句中的返回对象是函数或者lambda表达式中的对象或形参。
- throw语句中抛出的对象是函数或try代码块中的对象。
两种情况
第7章 lambda表达式(C++11~C++20)
- lambda表达式的语法
[ captures ]
捕获列表:可以捕获当前函数作用域的零个或多个变量,变量之间用逗号分隔- 捕获方式:
- 按值捕获
- 引用捕获
( params )
可选参数列表specifiers
可选限定符exception
可选异常说明符ret
可选返回值类型{body}
lambda表达式的函数体
- 捕获列表
- 作用域:捕获的变量必须要是自动存储类型,即非静态的局部变量
- 捕获值和捕获引用:
- 主要差别在需进行赋值操作时,捕获的变量默认为常量,只有捕获引用时才能修改捕获变量
- 可选说明符mutable可以移除lambda表达式的常量性,因此带上mutable说明符的lambda表达式可以修改捕获值的变量
- 捕获值的变量在lambda表达式定义的时候已经固定下来了,无论函数在lambda表达式定义后如何修改外部变量的值,lambda表达式捕获的值都不会变化
- 特殊的捕获方法:lambda表达式的捕获列表除了指定捕获变量之外还有3种特殊的捕获方法。
- [this] —— 捕获this指针,捕获this指针可以让我们使用this类型的成员变量和函数。
- [=] —— 捕获lambda表达式定义作用域的全部变量的值,包括this。
- 隐式捕获this对象,C++20标准中引入了[=, this]捕获this指针的语法,实际上表达的意思和[=]相同
- [&] —— 捕获lambda表达式定义作用域的全部变量的引用,包括this。
- 广义捕获:
- 简单捕获
- 初始化捕获
- 可以捕获表达式
- 使用场景1:使用移动操作减少代码运行开销
- 使用场景2:异步调用时复制this对象,防止lambda表达式被调用时因原始this对象被析构造成未定义的行为
- C++17标准对捕获*this进行了增强,上述代码可以替换为
- 泛型lambda表达式
第8章 非静态数据成员默认初始化(C++11 C++20)
非静态数据成员默认初始化在一定程度上解决了初始化列表代码冗余的问题,代码可读性高
- 对非静态数据或成员使用=或者{}初始化
- 位域的默认初始化
- 当表示位域的常量表达式是一个条件表达式时需要注意解析顺序:
第9章 列表初始化
- 变量初始化:
- 直接初始化:使用括号进行初始化、new运算符和类构造函数的初始化列表
- 拷贝初始化(复制初始化):使用等号初始化、函数传参、return返回
- 拷贝初始化隐式调用了构造函数
- 列表初始化:C++11标准中引入,支持一次初始化多个参数(变量)
- 隐式缩窄转换规则:
- 从浮点类型转换整数类型
- 从long double转换到double或float,或从double转换到float,除非转换源是常量表达式以及转换后的实际值在目标可以表示的值范围内。
- 从整数类型或非强枚举类型转换到浮点类型,除非转换源是常量表达式,转换后的实际值适合目标类型并且能够将生成目标类型的目标值转换回原始类型的原始值。
- 从整数类型或非强枚举类型转换到不能代表所有原始类型值的整数类型,除非源是一个常量表达式,其值在转换之后能够适合目标类型。
- 列表初始化的优先级问题:
- 如果有一个类同时拥有满足列表初始化的构造函数,且其中一个是以std::initializer_list为参数,那么编译器将优先以std::initializer_ list为参数构造函数
- 指定初始化:C++20标准中引入了该特性
- 指定初始化要求对象必须是一个聚合类型
- 指定的数据成员必须是非静态数据成员。这一点很好理解,静态数据成员不属于某个对象。
- 每个非静态数据成员最多只能初始化一次:
- 非静态数据成员的初始化必须按照声明的顺序进行。
- 针对联合体中的数据成员只能初始化一次,不能同时指定
- 不能嵌套指定初始化数据成员。
- 在C++20中,一旦使用指定初始化,就不能混用其他方法对数据成员初始化了
规则:
第10章 默认和删除函数
帮助用户精确控制类特殊成员函数的生成以及删除
- 类的特殊成员函数
- 没有自定义构造函数时,编译器会为类添加默认的构造函数,类似的函数一共有6个:
- 默认构造函数。
- 析构函数。
- 复制构造函数。
- 复制赋值运算符函数。
- 移动构造函数(C++11新增)
- 移动赋值运算符函数(C++11新增)。
- 带来的麻烦:
- 声明任何构造函数都会抑制默认构造函数的添加。
- 一旦用自定义构造函数代替默认构造函数,类就将转变为非平凡类型。
- 没有明确的办法彻底禁止特殊成员函数的生成(C++11之前)。
- 显式默认和显式删除
- 直接在声明函数的尾部添加=default和=delete
- 显式删除的其他用法:显式删除不仅适用于类的成员函数,也适用于普通函数
- explicit关键字:只能用于修饰只有一个参数的类构造函数,取消隐式转换
第11章 非受限联合类型(C++11)
- 联合类型:Union,成员变量共享一块内存
- 局限性:
- 成员类型不能是非平凡类型(不能有自定义构造函数,如string,自定义了构造函数的类等)
- 非受限联合类型:C++11标准之后,联合类型的成员可以是除了引用类型外的所有类型
- 当成员存在非平凡类型,使用时需要提供联合类型的构造和析构函数
- 推荐让联合类型的构造和析构函数为空,将其成员的构造和析构函数放在需要使用联合类型的地方
- 非受限联合类型对静态成员变量的支持:联合类型的静态成员不属于联合类型的任何对象,不是队形构造时被定义的,不能在联合类型内部初始化
第12章 委托构造函数(C++11)
- 构造函数:
- 构造函数重复代码多,维护困难
- 使用函数,减轻初始化列表代码冗余
- 成员函数中若包括复杂对象,可能会影响类的构造效率
- 有些情况不同使用函数主体对成员赋值
- 为构造函数提供默认参数
- 容易引发二义性
- 委托构造函数:某个类型的一个构造函数可以委托同类型的另一个构造函数对对象进行初始化
- 使用规则:
- 每个构造函数都可以委托另一个构造函数为代理。
- 不要递归循环委托!
- 如果一个构造函数为委托构造函数,那么其初始化列表里就不能对数据成员和基类进行初始化
- 委托构造函数的执行顺序是先执行代理构造函数的初始化列表,然后执行代理构造函数的主体,最后执行委托构造函数的主体
- 如果在代理构造函数执行完成后,委托构造函数主体抛出了异常,则自动调用该类型的析构函数。
- 委托模板构造函数:代理构造函数是一个函数模板
- 捕获委托构造函数的异常
第13章 继承构造函数(C++11)
- 继承关系中,当基类提供了很多不同的构造函数时,派生类中不得不定义相同多的构造函数,目的仅仅是转发构造参数
- 代码冗余繁杂
- 容易引入错误
- C++标准对using关键字进行了扩展,使其可以引入基类的构造函数:
- 使用规则:
- 派生类是隐式继承基类的构造函数,所以只有在程序中使用了这些构造函数,编译器才会为派生类生成继承构造函数的代码。
- 派生类不会继承基类的默认构造函数和复制构造函数。
- 继承构造函数不会影响派生类默认构造函数的隐式声明,也就是说对于继承基类构造函数的派生类,编译器依然会为其自动生成默认构造函数的代码。
- 在派生类中声明签名相同的构造函数会禁止继承相应的构造函数。
- 派生类继承多个签名相同的构造函数会导致编译失败(需要避免二义性)
- 继承构造函数的基类构造函数不能为私有
第14章 强枚举类型(C++11 C++17 C++20)
强枚举类型不仅修正了枚举类型的缺点并且全面地扩展了枚举类型的特性
- 枚举类型破坏了C++的类型安全,存在一定的使用问题
- 枚举类型可以隐式转换为整型,但整型不能转换为枚举类型
- 枚举类型会把其内部的枚举标识符导出到枚举被定义的作用域,若不同的枚举类型中使用了相同的枚举标识符,会发生重复定义,无法编译
- 无法指定枚举类型的底层类型,不同的编译器对于相同枚举类型可能会有不同的底层类型
⇒ 可以使用命名空间,区分作用域
- 强枚举类型:
- 枚举标识符属于强枚举类型的作用域。
- 枚举标识符不会隐式转换为整型。
- 能指定强枚举类型的底层类型,底层类型默认为int类型。
- 强枚举类型不允许匿名
- 定义强枚举类型:
- 指明底层类型
- 从C++17标准开始,对有底层类型的枚举类型对象可以直接使用列表初始化。
- 没有指定底层类型的枚举类型是无法使用列表初始化
- 列表初始化禁止缩窄转换
- 使用using打开强枚举类型:C++20标准扩展的using功能
using还可以指定引入的标识符
第15章 扩展的聚合类型(C++17 C++20)
- C++17标准对聚合类型给出了新定义:
- 聚合类型需要满足的常规条件:
- 没有用户提供的构造函数
- 没有私有和受保护的非静态数据成员
- 没有虚函数
- 需要满足的新条件
- 必须是公开的基类,不能是私有或受保护的基类
- 必须是非虚继承
- 聚合类型(Aggregate Classs)的特性:
- 可以使用{}像数组一样进行初始化
- C++20标准中规定聚合类型对象的初始化可以用小括号列表来完成,结果和大括号列表相同
- 带小括号的列表初始化支持缩窄转换
- 聚合类型的初始化
- 使用聚合类型方法初始化派生类的基类
- 删除用户提供的构造函数,使派生类为聚合类型,直接使用{}初始化
- 派生类存在多个基类,那么其初始化的顺序与继承的顺序相同
- 由于标准的改变,会出现相同类型使用不同声明方式表现不一致的问题:
- 最简单明确的处理方法:禁止聚合类型使用用户声明的构造函数