条款7 在创建对象时注意区分()和{}
初始化的方式包括使用小括号、等号、大括号。
不可复制对象(如std::atomic
类型的对象)可以采用大括号和小括号来初始化,而不能用”=”。
大括号初始化禁止内建类型之间进行隐式窄化类型转换。
double x,y,z;
int sum1{x+y+z}; //无法通过编译
C++规定:任何能够解析为声明的都要解析为声明。
所以可能本来想要以默认初始化方式构造一个对象,结果不小心声明了一个函数。
本意是想使用一个没有形参的构造函数声明一个对象。
Widget w2();这个语句声明了一个函数
所以使用大括号就没有歧义了:
Widget w2{};
在构造函数被调用时,只要形参中没有一个具备std::initializer_list
类型,那么花括号和小括号没有区别;如果有的话,那么花括号会优先调用它们。
空的化括号表示的是没有参数,而不是空的**std::initializer_list**
,所以调用默认构造而不是形参为空的std::initializer_list
的构造函数。如果要调用空的**std::initializer_list**
的构造函数,就要用两个花括号,或者将花括号嵌套在小括号内。
条款8 优先使用nullptr而不是0或NULL
- 0 和 NULL 都不属于指针类型
条款9 优先使用声明别名而不是typedef
- 使用 using 而不是 typedef
在运用模板的地方,typedef 就办不到了
条款10 优先使用作用域限制enums而不是无作用域的enum
无作用域的**enum**
会将枚举元素隐式的转换为整数类型。
在enum
后增加一个”class”,就可以将无作用域的enum
转换为一个有作用域的**enum**
。有作用域的enum不存在从枚举元素到其他类型的隐式转换。
编译器需要在枚举体之前知道它的大小。有作用域的潜在类型是已知的:int;对于没有作用域的枚举体,可以指定。
对有作用域的枚举体,默认的潜在的类型是int
:
enum class Status; //潜在类型是int
如果默认的类型不适用于你,可以重载:
enum class Status:std::uint32_t; //Status潜在类型是
//std::uint32_t
条款11 优先使用delete关键字删除函数而不是private却又不实现的函数
删除的函数不能通过任何方式被使用,即便是其他成员函数或者友元函数试图复制该 class 的对象时也会导致编译失败;而 C++98 中声明为 private 这种方式,直到链接的时候才会诊断出这个错误:会出现undefined reference to ...
的报错
HINT:如果给 float 一个转换为 int 或 double 的可能性,C++总倾向于转换为 double。
条款12 使用override关键字声明覆盖的函数
可以使用修饰词来针对构造函数进行重载:
此处修饰词有&
和&&
,都是针对调用该函数的对象*this
的。前者负责处理左值对象;后者负责处理右值对象。
条款13 优先使用const_iterator而不是iterator
const_iterator
在STL中等价于指向const
的指针。
如果为了通用的代码,优先使用非成员版本的begin()
和end()
函数,而不是成员函数版本的。但是C++11只有非 const 的begin()
和end()
,C++14才有 const 版本的。
条款14 如果函数不抛出异常请使用noexcept
int f(int x) throw(); //C++98风格,不抛出异常
int f(int x) noexcept; //C++11风格,不抛出异常
C++11中,移动操作会破坏push_back
的异常安全保证。如果n个元素已经从老内存区移动到新内存区,但异常在第n+1个元素时抛出,那么push_back
就不能完成。但是原始的std::vector
已经被修改:有n个元素被移动走了。恢复操作也不太可能,因为从新内存到老内存的移动又可能发生异常。
- 对于移动语义和
swap
非常有用。
对于一些允许内存释放(operator delete
和operator delete[]
)和析构函数抛出异常是十分糟糕的,需要声明为noexcept
。所以C++11将它们默认声明为隐式noexcept
的。
条款15 尽可能使用constexpr
constexpr
表明一个值不仅仅是常量,还是编译期可知。
编译期可知的值”享有特权”,他们可能被存放的只读存储空间。另一种用法是用于需要”整形常量表达式”的上下文中。
const
不提供constexpr
所能保证之事,因为const对象不一定通过编译期已知值来初始化。 ```cpp int sz;
const auto arraysize = sz; int a[arraysize]; //错误,arraysize非编译期可知
constexpr auto arraysize = sz; //错误,sz编译期不可知 constexpr auto arraysize = 10; //没问题,编译期可知 int a[arraysize]; //没问题 ```
C++11中,constexpr
函数的代码不能超过一行:一个return
语句。但是在C++14被放松了:
**constexpr**
函数只能传入和返回字面值类型。注意,std::string
不是字面量!
在C++11中,constexpr
是隐式const
的,所以如果作为成员函数,不能修改成员变量。而且void
返回类型不是字面值类型。但是在C++14中被放开,setter(赋值器)也能声明为constexpr
。
条款16 让const成员函数线程安全
const
不是线程安全的。
std::mutex
是一种只可移动但不能复制的类型,根据class的规则,类的属性取决于内含元素的属性,所以内含std::mutex
的class失去了被复制的能力。
某些情况下,互斥量的副作用会过大。可以使用std::atomic
修饰开销会更小。但是它只适合操作单个变量或内存位置。而且它也是只可移动但不能复制的类型。
对于单个要求同步的变量或内存区域,使用std::atomic
就足够了,但是如果有两个或更多个变量或内存区域需要作为一整个单位操作时,就需要使用互斥量。
条款17 理解特殊成员函数的生成
- 特殊函数指C++自己生成的函数,仅在需要的时候才生成。
拷贝操作和移动操作一样,如果自己声明了,编译器就不会生成。
移动操作和inline
一样,只是一个申请,编译器并不保证一定会移动,因为有些数据类型并不能移动。所以每个按成员进行移动操作,核心在于运用**std::move**
,其返回值被用于函数重载决议,最终决定执行移动还是复制操作。
- 两个拷贝操作是独立的:声明一个不会限制编译器生成另一个。声明一个拷贝构造函数但没有声明拷贝赋值运算符,当代码用到拷贝赋值,编译器会自动生成。反之亦然。
- 两个移动操作并不相互独立。如果声明其中一个,编译器就不会再生成另外一个。
如果一个类显式声明了拷贝操作,编译器就不会生成移动操作。解释:如果声明拷贝操作,那么说明平常拷贝对象的方法不适用于该类,编译器会明白:如果逐成员拷贝对拷贝操作来说不合适,逐成员移动也可能不合适。反之亦然,所以可以通过声明移动构造来禁止编译器自动生成拷贝函数。
声明移动操作会使得编译器不合成默认的拷贝函数。解释:因为逐成员的移动都不适用于该类,那么逐成员的拷贝也不合适。
Rule of Three:如果声明了拷贝构造函数、拷贝赋值运算符,或者析构函数三者之一,你应该也声明其余两个。用户接管拷贝操作的需求几乎都是因为该类会做其他资源的管理。这也意味着(针对上面的那三个):
- 无论哪种资源管理如果在拷贝操作内完成,也应该在另一个拷贝操作内完成。
- 类的析构函数也需要参与资源的管理(通常是释放)。
所以仅当下面条件成立时编译器才会合成默认的移动操作(当需要时):
- 类中没有拷贝操作;
- 类中没有移动操作;
- 类中没有用户定义的析构。