本篇笔记包含 Term 07~17。
Term 07:在创建对象时注意区分 () 和 {}
在 Term 2 中,我们了解到 {val...}
实际是 std::initializer_list<T>
类型,使用 {}
初始化需要注意:
- 阻止隐式窄化型别转换
Foo f{};
会调用 class Foo 的无参构造函数- 如果一个类实现了
std::initializer_list<T>
为参数的构造函数,则除了 2. 之外,所有使用{}
的情况都会最优先调用这个构造函数,因此除了明确调用std::initializer_list<T>
为参数的构造函数的时候,其它情况都应该使用()
Foo f({});
或者Foo f{{}};
会调用std::initializer_list<T>
为参数的构造函数,实参为空的std::initializer_list<T>
- 非静态成员变量不能使用
()
,不可复制对象不能使用=
操作符
Term 08:优先选用 nullptr,而非 0 或者 NULL
使用 nullptr
表示空指针(任意类型指针),可以避免 0 或 NULL 错误调用 int 类型的重载
Term 09:优先选用别名声明,而非 typedef
使用别名声明,在函数指针上有优势,并且别名声明支持模板化,而 typedef
不支持。可以通过构造一个模板结构体/类,内嵌 typedef
声明变量来达到类似效果,但是比较繁琐。
// using 写法
template<typename T>
using MyAllocList = std::list<T, MyAlloc<T>>;
MyAllocList<Widget> lw;
// typedef 写法
template<typename T>
struct MyAllocList {
typedef std::list<T, MyAlloc<T>> type;
}
MyAllocList<Widget>::type lw;
Term 10:优先选用限定作用域的枚举型别,而非不限作用域的枚举型别
不限作用域的枚举型别,变量作用域是在外层,造成污染,使用限定作用域的枚举型别(enum class
)可以避免这一情况。限定作用域的枚举型别需要强制类型转换,默认的底层类型是 int,可在声明式指定类型。
Term 11:优先选用删除函数,而非 private 未定义函数
当我们需要禁止编译器编写某个默认的类成员函数时(例如 copy 构造函数),我们可以使用 Foo(const Foo&) = delete
告知编译器禁止生成该成员函数。除了成员函数之外,我们还可以声明其它函数为删除函数,用以避免隐式转换或者时处理特殊边界情况。
Term 12:为意在改写的函数添加 override 声明
注意 overload 和 override 的区别 ,当我们意图 override 一个虚函数时,应该使用 override
让编译器检查这是否是合法的 override 行为。
Term 13:优先选用 const_iterator,而非 iterator
- 优先选用 const_iterator,而非 iterator
- 优先选用非成员函数版本的 begin/cbegin 等,而非成员函数版本
Term 14:只要函数不会发射异常,就为其加上 noexcept 声明
添加 noexcept 声明使得编译器能更好地优化。
Term 15:只要有可能使用 constexpr,就使用它
constexpr
对象具备 const 属性,并切在编译期就已经知道其值- 如果
constexpr
函数传入地实参是编译期已知的值,那么其结果会在编译期计算得出,否则退化为普通函数
Term 16:保证 const 成员函数的线程安全
通常来说,只读属性是线程安全的。const 成员函数可以保证对象本身不变(除了 mutable 属性),但是 mutable 属性可能造成在读取成员属性时线程不安全(比如实现了缓存)。我们需要编写线程安全的代码避免这种情况。
Term 17:理解特种成员函数的生成机制
- 当我们没有为类编写特种成员函数,而代码中使用到了,编译器会自动替我们生成。他们包括:默认构造函数、析构函数、复制构造函数、复制赋值运算符、移动构造函数和移动赋值运算符
- 移动操作仅当类中没有显式声明复制操作、析构函数、移动操作时才会自动生成
- 复制构造函数、复制赋值运算符和析构函数,三者应该同时自定义或者同时都不自定义
- 使用
=delete
和=default
声明特种成员函数是使代码更加明确的做法 - 成员函数模板不会抑制特种成员函数的生成