转向现代C++

创建对象时注意区分()和{}

  1. 大括号初始化可以用来指定容器的初始内容,也可以用来为非静态成员指定默认初始化值(此时使用=号初始化也可行,但不能使用小括号)。

  2. 不可复制的对象(std::atomic等类型对象)可以使用大括号和小括号来进行初始化,却不能使用“=”。在大括号、小括号和=三种初始化表达式写法中只有大括号适用于所有场合。

  3. 大括号初始化的一项新特性是禁止内建类型之间进行隐式窄化类型转换,如果大括号内的表达式无法保证能够采用进行初始化的对象来表达,则代码不能通过编译,比如:

    double x,y,z;
    int sum1{x+y+z};//错误!double类型之和可能无法用int表达
    

    而采用小括号和=号的初始化则不会进行窄化类型检查,因为会破坏太多遗留代码。

优先使用nullptr,而非0或NULL

  1. 字面常量0的类型是int,而非指针,当C++在只能使用指针的语境中发现了一个0,它也会勉强解释为空指针,但说到底还是一个不得已而为之的行为。以上结论对于NULL也成立,0和NULL都不具备指针类型。因此,在指针类型和整数类型之间重载时可能会发生意外。
    void f(int);
    void f(bool);
    void f(void*);
    f(0);//调用的是f(int),而不是f(void*)
    f(NULL);//可能不通过编译,但一般会调用f(int)。从来不会调用f(void*)
    
  2. nullptr的优点在于它不具备整型类型,实话实话它也不具备指针类型,它的实际类型是std::nullptr_t,可以隐式转换为所有的裸指针类型。并且使用nullptr可以提升代码的清晰性。
  3. 在函数模板中传入0或者NULL会被模板自动推导为是int整型,而如果此时需要指针类型就会编译不通过,而使用nullptr就不会出现这种问题。
  4. 因此,想要表示空指针时,使用nullptr而非0或NULL,另外避免在整型和指针类型之间重载。

优先使用别名声明,而非typedef

  1. 别名声明可能在处理涉及函数指针的时候比较容易理解:
    typedef void (*FP)(int,const std::string&);//FP类型是一个指向形参为int和const std::string&,
    using FP=void (*)(int,const std::string&);//没有返回值的函数指针
    
  2. 别名声明可以模板化(这种情况下它们被称为别名模板,alias template),typedef就不行。另外如果想要在模板内使用typedef来创建一个链表,它容纳的对象类型由模板形参指定的话,那你就要给typedef的名字加一个typename前缀。依赖于模板类型形参(T)的类型::type称为带依赖类型,而C++规定带依赖类型前必须加上typename。而使用别名的话就不会产生带依赖类型。
  3. 别名模板可以让人免写”::type“后缀,并且在模板内,对于内嵌typedef的引用经常要求加上typename前缀。

优先选用限定作用域的枚举类别,而非不限作用域的枚举类型

  1. 如果在一对大括号里声明一个名字,则该名字的可见性就被限定在括号括起来的作用域内。但是这个规则不适用于C++98风格的枚举类型中定义的枚举量。
    enum Color{black,white,red};//black、white、red所在作用域和Color相同
    auto white=false;//错误,white已在范围内声明过了
    
  2. 上述枚举类型被称为不限范围的枚举类型,而C++11中有限定作用域的枚举类型。
    enum class Color{black,white,red};//三个元素被限定在了Color中
    auto white=false;//没问题
    
  3. 不限范围的枚举类型中的枚举量可以隐式转换到整数级别(并能够进一步转换到浮点类型),而限定作用域的枚举类型到任何其他类型都不存在隐式转换路径。
  4. 限定作用域的枚举类型可以进行前置声明,而不限作用域的则不行。
    enum Color;//错误
    enum class Color;//正确
    

    这是因为限定作用域的枚举类型的底层类型是已知的,而不限范围的枚举类型的底层类型,我们可以进行指定。当我们指定过后,不限范围的枚举类型也能够进行前置声明了。限定作用域的默认底层类型是int。

  5. 枚举类型的底层类型可以通过std::underlying_type::type(C++11)或者std::underlying_type_t(C++14)来取得。

优先使用删除函数,而非private未定义函数

  1. 为了让输入输出流类成为不可复制的,在C++98中将他们的拷贝构造函数和拷贝赋值函数都声明为private,这就阻止了客户去调用他们,并且故意不去定义它们,这就阻止了友元或者其他成员函数调用它们。
  2. 在C++11中,使用“=delete”将拷贝构造函数和拷贝赋值函数都标识为删除函数,是达成相同效果的更好途径。删除函数无法通过任何方式使用,因此友元或是成员函数也会因试图复制类对象而无法工作,而在C++98中这种不当使用要到链接阶段才能诊断出来。
  3. 将删除声明为public而非private能得到更好的错误信息。删除函数的另一个优点在于:任何函数都能声明为delete,但是只有成员函数能被声明为private。因此重载函数并定义为删除函数能够帮我们阻止编译器所带来的隐式类型转换。
    bool islucky(int number);
    islucky(100);//right
    islucky('a');//将‘a’转换为其ascii码在判断。也没错
    islucky(3.5);//double类型截断后再调用也没错
    bool islucky(char)=delete;//此时islucky('a')调用就会出错
    
  4. 删除函数的另一作用而private做不到的就是可以阻止那些不应该的模板具现(模板特例化)。
    template<typename T>void processPointer(T* ptr);
    template<> 
    void processPointer(void* ptr)=delete;//这样就阻止了使用void*来特例化此模板
    

为意在改写的函数添加override声明

  1. 正是虚函数改写,使得通过基类接口调用派生类函数成了可能。成员函数改写需要满足要求:
    基类中的函数必须是虚函数,基类和派生类中的函数名字、形参类型、函数常量性必须完全相同(析构函数除外),基类和派生类的函数返回值和异常规格必须兼容(C++98)
    more:基类和派生类的函数引用饰词必须完全相同,引用饰词是为了实现限制成员函数仅用于左值或右值。带有引用饰词的函数不一定是虚函数(C++11)。
  2. 由于对于声明派生类中的改写,保证正确性很重要,而出错又很容易,C++11提供了一种方法来显式地标明派生类中的函数是为了改写基类版本:为其加上override声明。这样不止让编译器在你想要改写的函数实际上未改写时提醒你,还可以在打算更改基类中的虚函数的签名时,衡量一下波及的影响面。
  3. override这类语境关键词仅于出现在成员函数的声明的末尾时才有保留意义,其余地方并没有“改写”的意义。而另一个语境关键词就是final,将final应用于虚函数,会阻止它在派生类中被改写,final也可以用于一个类,禁止该类用作基类。
  4. 成员引用饰词使得对于左值和右值对象(*this)的处理能够区分开来。

优先使用const_iterator,而非iterator

  1. 从const_iterator到iterator并不存在可移植的类型转换,static_cast不行,即使reinterptet_cast也不行。
  2. 在C++11中,容器的成员函数cbegin和cend都返回const_iterator类型,甚至对于非const的容器也是如此。但是在C++11中只提供了非成员函数版本的begin和end。
  3. C++14中添加了非成员函数版本的cbegin、cend、rbegin、rend、crbegin、crend。
  4. 在最通用的代码中,优先选用非成员函数版本的begin、end和rbegin等,而非成员函数版本。

只要函数不发生异常,就为其加上noexcept声明

  1. 函数是否会发射异常,是调用方关注的核心,而函数是否带有noexcept声明就事关接口设计。当函数不会发射异常却没有加上noexcept声明的话就是接口规格缺陷。
  2. 在带有noexcept声明的函数中,优化器不需要在异常传出函数的前提下将执行期栈保持在开解状态,也不需要在异常逸出函数定位前提下,保证其中的对象以其被构造的顺序的逆序完成析构。
  3. C++11后标准库中很多函数都把C++98中的复制操作替换成了C++11中的移动操作,但仅在已知移动操作不会发生异常的前提下。怎么知道移动操作不会发生异常呢,就只要校验一下移动操作是否带有noexcept声明即可。
  4. swap函数是极其需要noexcept声明的一个例子。标准库中的swap是否带有noexcept声明,取决于用户定义的swap是否带有noexcept声明。
  5. 一般仅当构建它的低阶数据结构具备noexcept性质时,高阶数据结构的swap行为才能具有noexcept性质,这一事实促使我们在只要有可能的情况下都让函数带有noexcept声明。
  6. 默认地,在C++11中,内存释放和所有的析构函数都隐式地具备noexcept性质,这样一来他们就无需加上noexcept声明了。析构函数未隐式具备noexcept性质的唯一场合就是所在类中有数据成员的类型显式将其析构函数声明为可能发射异常的[即为其加上”noexcept(false)”声明]。
  7. noexcept性质对于移动操作、swap、内存释放函数和析构函数最有价值。
  8. 大多数函数都是异常中立的(调用的函数可能会发生异常),不具备noexcept性质。

只要有可能使用constexpr,就使用它

  1. constexpr对象,具备const属性,而且其值在编译阶段就已知(严格来说是在翻译阶段但是没差)。而在编译阶段已知的整型常量值可以用在C++要求整形常量表达式的语境中,比如数组大小、整型模板实参、枚举量的值等等。
  2. const并未提供和constexpr同样的保证,因为const对象不一定经由编译期已知值来初始化。因此,所有constexpr对象都是const对象,而非所有的const对象都是constexpr对象,如果想让编译器提供保证让变量拥有一个值,用于要求编译期常量的语境,那么这个工具就是constexpr。
  3. constexpr函数在调用时要是传入的是编译器常量,则产出编译期常量,传入直至运行期才知晓的值就产出运行期值。因此constexpr函数可以用在要求编译期常量的语境中,前提是传入的是编译期已知的实参值。在调用constexpr函数时要是传入的实参编译器未知,则它的运作方式和普通函数无异。
  4. 由于constexpr函数必须在传入编译期常量时能够返回编译期结果,它们的实现必须有所限制。C++11中,constexpr函数不得包含多于一个可执行语句,即一条return语句。而在14中,constexpr函数仅限于传入和返回字面类型,C++11中所有内置类型除了void都符合这个条件。用户类型同样也可能是字面类型,因为它的构造函数和其他成员函数可能也是constexpr函数。
  5. C++11中constexpr函数都隐式声明为const了(const成员函数),另外在C++11中void不是字面类型,而在14中这两个限制都被解除了,因此设置器也可以声明为constexpr。
  6. 比起非constexpr对象或constexpr函数而言,constexpr对象或是constexpr函数可以用在一个作用域更广的语境中。

保证const成员函数的线程安全性

  1. 当const成员函数中需要修改某些mutable的值时,需要考虑其在多线程情况下的线程安全问题,可以考虑互斥量和std::atomic类型的计数器(相比较互斥量开销更小),但是由于互斥量mutex和atomic都是只移类型(只能移动不能复制)整个类型都会因此变为只移类型。
  2. 对于单个要求同步的变量或内存区域,使用std::atomic就足够了,而如果有两个或更多个变量或内存区域需要作为一整个单位进行操作时,就要使用互斥量了。

理解特种成员函数的生成机制

  1. 特种成员函数是指那些C++会自行生成的成员函数,C++98有四种特种成员函数:默认构造函数、析构函数、复制构造函数以及复制赋值函数,这些函数仅在需要时才会生成。
  2. 仅在一个类没有声明任何构造函数时才会生成默认构造函数(只要指定了一个要求传参的构造函数,就会组织编译器生成默认构造函数。)
  3. 生成的特种成员函数都具有public访问层级并且是inline的,并且除了有虚函数的基类的析构函数都是非虚的(此时编译器为派生类生成的析构函数也是虚函数)。
  4. C++11增加了移动构造函数和移动赋值函数,那些不可移动的类型将使用复制来实现“移动”。如果发生了复制操作的情况,移动操作就不会在已有声明的前提下被生成。
  5. 类的两种复制操作是彼此独立的:声明了其中一个,并不会阻止编译器生成另外一个。当声明了复制构造函数,未声明复制赋值运算符,并写了要求复制赋值的代码,编译器将自动生成复制赋值运算符。复制构造函数也一样。
  6. 两种移动操作并不是相互独立的,声明了其中一种,就会阻止编译器生成另外一个。因为实际表明移动操作的实现方式将会与编译器生成的默认按成员移动的移动构造函数多少有些不同,因此按成员进行的移动赋值函数极有可能也有不合用之处。
  7. 一旦显式声明了复制操作,类也就不会生成移动操作了。反之,一旦显式声明了移动操作,编译器就会废除复制操作(废除的方式是删除它们)。
  8. 大三律:如果你声明了复制构造函数、复制赋值函数运算符或析构函数的任何一个,你就得同时声明所有这三个。推论1:如果存在用户声明的析构函数,则平凡的按成员复制也不适用于该类,因此复制操作就不该自动生成,但是在实际过程中用户声明的析构函数即使存在也不会影响编译器生成复制操作的意愿。
  9. C++11规定:只要用户声明了析构函数,就不会生成移动操作。移动操作的生成条件:
    (1)该类未声明任何复制函数 (2)该类未声明任何移动操作 (3)该类未声明任何析构函数
  10. C++11规定在已经存在复制操作或析构函数的情况下,仍然生成复制操作已经成为了被废弃的行为。此时按之前方式写的类如果依赖于编译器生成的复制操作,可以显式使用=default来显式声明。
  11. 成员函数模板在任何情况下都不会抑制特种成员函数的生成。