0 绪
本篇是看完《深入理解C 11:C 11新特性解析与应用》后做的笔记的上半部分. 这本书可以看作是《C Primer》的进阶版, 主要是更加详细地介绍了C 11的一些常用设计和标准库设施, 很多知识点都在面试中会遇到, 值得一读.
阅读并笔记的途中我跳过了一些之前已经总结过的内容, 而对于一些自己看书后依然没搞清楚的内容(例如SFINAE和内存模型)搜索资料进行了扩展, 还补充了一些原书没有介绍但稍微有所相关的内容, 参考文献在每一段的开头给出. 全文6.6k字, 慢慢来吧.
才疏学浅, 错漏在所难免, 后续若有所修改会同步存于我的Github仓库, 点击底部"阅读原文"可跳转(Study-Notes/Content/《深入理解C 11》笔记/上 at main · ZFhuang/Study-Notes (github.com))
1 新标准的诞生
- C 11的目的是成为更适合系统开发的语言, 成为更易入门的语言, 且保留稳定性和兼容性. 理解这个初衷就能理解C 11很多新内容的设计缘由
- 核心改进点:
- 并行编程: 内存模型, 线程, 原子操作
- 泛型编程: 统一初始化表达式, auto, decltype, move
- 系统编程: constexpr, POD
- 库编程: 内联命名空间, 继承构造, 右值引用
- C 11标准的设计准则:
- 在稳定性和兼容性之间抉择: auto, using, nullptr
- 用库而非扩展语言语法来实现特性: std::regex, 库尽量以头文件实现, 但也有并行库这种必须深入编译器的库
- 用通用而非特殊的手段来实现特性: 显式类型转换
- 让特性对专家和新手都适用: 统一初始化表达式
- 增强类型安全: enum class
- 与硬件紧密合作, 但保持透明: 并行库
- 开发改变思维方式的新特性: lambda
- 融入编程现实: 引入少数的几个通用属性
2 保证稳定性和兼容性
预定义宏 https://docs.microsoft.com/zh-cn/cpp/preprocessor/predefined-macros?view=msvc-170
预处理器
__func__
所在处的函数名称, 可以用在初始化成员列表中__DATE__
编译日期__FILE__
当前文件的名称__LINE__
所在处的行号__TIME__
编译时间_Pragma()
和#pragma
一样, 用来指示编译器进行一些行为, 不过前者属于操作符因此可以用在宏中调用__VA__ARGS__
变长宏参数, 用于替换掉宏函数中省略号代表的字符串__cplusplus
返回cpp版本, C语言则无定义, 用于混合编译
新编译特性
<cassert>
中提供assert()
宏, 用于运行时断言;static_assert()
用于编译期断言, 接受表达式和提示信息两个参数- 由于
static_assert()
是静态的, 能在任何命名空间中使用, 因此最好写在外部作为提示并防止用到局部变量 noexcept
和noexcept()
指明某函数不能抛出异常(若抛出则直接terminate), 参数可以用bool值来决定是否允许抛出, ture就不允许. 此举是为了节省抛出异常的额外开销. C 11的delete操作和析构函数默认是noexcept的- 窄字符串
char
和宽字符串wchar_t
连接的时候会统一位宽字符串然后连接 long long
至少64位的长整型, 字面值以LL结尾- C 11放松了就地初始化(类内直接赋值)的使用限制并引入了构造函数后面的初始化列表设置. 初始化列表的效果总是慢于就地初始化, 但也快过在构造函数中进行赋值
- 注意: 非常量的静态变量依然要在头文件外定义从而保证在程序中只存在一个
sizeof()
可以对类成员表达式使用了- 类模板也可以声明友元了(
friend T;
) - 函数声明的尾部加上
final
可以阻止后续派生类的覆盖, 函数声明的尾部加上override
可以强制派生类进行覆盖 - 模板函数也可以有默认参数了, 且不一定要和模板类一样从右到左指定, 只要能够从实参被合理推导即可. 但是如要注意模板实际参数始终都以实际类型优先, double和int是这个特性的常见例子, 例如1, 如果默认参数是double就会被推导为double
- C 11引入了extern模板. 原因和extern变量一样, 普通的模板只存在于对应文件的
.o
中, 如果一个模板文件被多个文件实例化就会产生多份重复代码, 没有extern的话此时重复的模板会冲突. 有了extern后编译器会自动删除重复的实例化模板, 不但节省内存还节省了多余的实例化时间 - 注意被其他文件调用的外部模板一定要在要用到的类实例化之前实例化
- 局部和匿名成员可以作为模板实参了, 但仍要注意匿名类型的声明不能在参数位置
3 通用为本, 专用为末
继承构造
- 为了减少派生类层层透传函数以复用基类代码的情况, 可以在类内用
using Base::Func;
来获取基类被隐藏的同名成员函数(包括构造函数, 此时被称为继承构造函数)的访问, 从而在外部可以自动调用基类构造 - C 11中继承构造函数和其他默认函数一样, 存在隐式声明的默认版本, 且如果不被使用就不会生成
- 继承构造函数的默认参数不会被继承, 反而会生成多个不同声明的构造函数的产生, 所以当发生冲突的时候应该显式声明构造函数来因此冲突的函数
- 当派生类是虚继承了基类时, 不能使用继承构造函数
- 一旦使用了继承构造函数(用
using Base::Base;
)暴露出来, 自身的默认构造函数就和之前的隐藏规则一样, 不会被自动生成
委派构造
- 在初始化列表中可以调用构造函数了, 此时称为委派构造函数. 不但可以调用基类的构造函数, 也可以调用当前类的其他构造函数, 这样就能进一步减少重复代码
- 但要注意委派构造不能和普通的初始化列表共用, 因为目标构造(初始化列表)总是先于委派构造被调用, 这会导致目标构造的参数无效. 如果使用委派构造, 就必须在构造函数体中进行其余成员的初始化
- 一种解决方案是修改构造的顺序, 让参数最多的构造函数作为委派构造的最终目标, 然后在这个构造函数的初始化列表中完成成员初始化. 其他构造函数通过带有默认值的委派构造来调用这个目标构造函数
- 千万小心环形委派, 会导致编译错误
- 委派构造函数使得构造函数的模板编程也成为一种可能, 通过让模板构造函数成为委派构造函数, 我们可以很容易地接受多种不同类型的参数进行相同的底层初始化
右值引用
- "右值"没有严谨的定义, 通常来说, 出现在等号右边的内容就是右值, 可以取地址, 有名字的就是左值
- 不跟对象关联的字面值等称为纯右值, 其他右值称为将亡值
- 右值引用是为了优化对临时对象进行深拷贝的效率
- 右值引用形成的拷贝/赋值函数称为移动拷贝/赋值, 核心是直接窃走目标对象内部指针指向的内存内容, 然后置空目标对象的指针
- 无论声明了左值引用还是右值引用, 都必须在声明时立即初始化(参数列和初始化列表也算立即初始化). 但注意右值引用不能被绑定到左值
- 右值引用能够延长右值的声明周期, 常量左值引用属于万能引用, 也能够延迟生命周期, 但是常量左值引用后的值不能修改所以一般用来减少临时对象的开销而已
- 当类没有定义移动构造时, 如果定义了常量左值引用构造也能正确执行, 只不过会变为拷贝形式
- 常量右值引用存在但是没有实际用途
std::move()
能强制使一个左值变为右值, 但是不会改变其生命周期- 右值引用本身是个左值, 因此需要配合
std::forward()
来完美转发(或者直接用std::move()
) - 默认的移动构造函数和拷贝构造函数是一样的, 只是执行按位拷贝操作. 因此只要需要移动语义就一定要自己实现移动构造
- 拷贝构造/赋值和移动构造/赋值两大类函数是同地位的, 只要声明了其中一种另一种就不会产生默认版本, 因此只要声明了一种就一定要手动编写其余函数
- 移动构造时抛出异常是很危险的, 因此应该用noexcept修饰移动构造函数
- move也有
std::move_if_noexcept
的版本, 其在当前函数没有noexcept时主动转换为左值应用从而调用拷贝构造版本. 但显然这种做法损失性能 - 只要定义中出现了左值引用, 引用折叠规则就会将其变为左值引用, 这是
std::forward()
的核心原理 - 编译器优化的时候本身就打开了返回值优化功能, 因此返回右值并不是很必要的事
语法扩展
- 函数前面加上
explict
可以防止参数发生隐式类型转换, 用于构造函数和operator中 - 不要将explict与delete共用, 因为这相当于删去了显式转换版本的函数, 可能会留下默认的隐式转换的实现.
- 大括号初始化会制止类型收窄
- 大括号的返回值是
initializer_list
, 可以用作函数的一种重载参数 - 大括号也可以在return, 一般用来构造临时变量, 具体构造出来的临时变量还是依靠声明的返回值决定
- 新增
ReturnType operator "" X(Args)
作为后缀操作符, 其允许我们将字符串字面值加上自定义的后缀X来转换为目标字面类型 - 但是对于这个操作符的编写有相关的参数限制:
- 字面值是整型: 参数只能是unsigned long long或const char*, 当unsigned long long无法容纳字面值时会自动转为字符串, 以
- 字面值是整型: 参数只能是unsigned long long或const char*, 当unsigned long long无法容纳字面值时会自动转为字符串, 以