05:了解C++默默编写并调用了哪些函数

  • 如果没有自定义相应拷贝控制成员,而且需要使用该拷贝控制成员,则编译器进行合成
  • 有时编译器不会进行合成,因为一些操作非法
1
2
3
4
class MyClass {};
MyClass m1; // 生成默认构造函数和析构函数
MyClass m2(m1); // 生成复制构造函数
m2 = m1; // 生成赋值构造运算符
  • 默认构造函数和析构函数
    • 作用:调用{基类和non-static成员变量}的构造函数和析构函数
    • 当自定义构造函数后,编译器就不会自动生成构造函数
    • 生成的析构函数是non-virtual的,除非基类的析构函数是virtual的
  • 复制构造函数
  • 赋值构造运算符
    • 自动生成赋值构造运算符的条件是,相关操作必须合法
      • 比如成员变量是const或引用,则不能进行赋值
      • 比如基类中赋值构造运算符是private的,则派生类中无法调用父类相应的赋值构造运算符对父类成员进行赋值

06:若不想使用编译器自动生成的函数,就该明确拒绝

在声明中将拷贝控制成员标记为=delete,将不会自动生成该拷贝控制成员

  • 背景:有时不希望类具有拷贝等行为(语义要求)
  • 三种方法:将不需要自动生成的拷贝控制成员
    • 在private中进行定义
      • 虽然类外部无法访问,但不是绝对安全,可以在成员函数和友元中使用
      • 写为空函数体,使其在链接过程中报错
    • 在基类中声明为private
      • 这样即使在成员函数和友元中使用相应拷贝控制成员,也会因为无法拷贝控制相应基类成员,从而将报错从链接期提前到编译期
    • 在声明中标记为=delete

07:为多态基类声明virtual析构函数

声明多态性质的基类的析构函数为virtual的

  • 背景:当delete一个指向派生类的基类指针时,只会调用non-virtual的基类析构函数,派生类中成员无法释放
  • 只有当类中至少包含一个除析构函数外的virtual函数时(多态性质),才将析构函数声明为virtual的
    • 为了保持可移植性
    • 如果该类不包含virtual函数,则通常该类不会作为基类
  • 将基类析构函数声明为pure virtual函数,从而将基类构造为抽象基类(避免了考虑将其他哪个函数声明为pure virtual函数)
  • 所有的STL容器都不包含virtual析构函数,因此不要将STL容器作为基类
    • 因为STL容器设计不是用来作为基类,不带有多态语义要求,只有多态性质的基类才需要声明一个virtual析构函数
    • 不是所有基类都带有多态性质

08:别让异常逃离析构函数

  • 析构函数不要抛出异常,但是析构函数中可以使用try catch进行异常处理
  • C++11中,默认将析构函数声明为noexcept,防止在析构函数中抛出异常
    • try语句块中抛出异常时,会将作用域中对象依次调用析构函数,然后进入catch语句块中
      • 如果此时调用的析构函数中继续报错,则core dumped
  • 可以在析构函数中使用try catch捕获异常,或者重新设计接口,使得防止抛出异常的函数在析构函数中被调用

09:绝不在构造和析构过程中调用virtual函数

不要再构造/析构函数(及其调用的函数中)中调用virtual函数,因为这样虚函数不会呈现多态

  • 当派生类中的基类部分被构造时,其调用的虚函数只会调用基类中的版本,不会调用派生类中的版本,即不会呈现多态
    • 从安全性角度看,因为此时派生类部分还未构造,使用派生类的虚函数版本可能产生未定义的行为,所以C++规定使用基类的版本
    • 从原理角度看,在构造基类部分时,对象的类型实际上是基类类型
  • 当派生类中的基类部分被析构时,同样不会呈现多态
    • 从安全性角度看,此时派生类部分已经析构,调用派生类的虚函数版本产生未定义的行为
    • 从原理角度看,此时对象为基类类型
  • 构造函数/析构函数内调用的函数,也要保证其中不调用虚函数

10:令operator=返回一个reference to *this

令赋值运算符返回一个*this的引用

11:在operator=中处理自我赋值

进行重新排列赋值或者copy and swap

  • 背景:有时可能很隐蔽的进行了自赋值的操作,特别是类管理资源时,很可能被意外delete掉
  • 进行重新排列赋值:先保存当前资源副本,然后new,最后delete原来的资源;可以保证异常安全性,而且identity test没有必要
    1
    2
    3
    4
    5
    6
    
    MyClass& operator= (const MyClass& rhs){
        Resource* tmp = MyResource;
        MyResource = new Resource(); // 如果new失败,则当前资源不会被释放 
        delete tmp; // new成功
        return *this;
    }
    
  • copy and swap

12:复制对象时勿忘记其每一个成分

  • 派生类复制时,不要忘记将基类部分也复制
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <cstdio>
#include <string>
using namespace std;

class Base{
public:
    Base(int id): base_id(id) {}
    Base(const Base& b): base_id(b.base_id) {}
    Base& operator= (const Base& b) { base_id = b.base_id; return *this;}
private:
    int base_id;
};
 
class Derived: public Base{
public:
    Derived(int id, string name): Base(id), myname(name) {}
    Derived(const Derived& d): Base(d), myname(d.myname) {} // 将派生类直接赋值给基类,派生类被切掉
    Derived& operator= (const Derived& d){
        Base::operator=(d); // 调用基类operator=
        myname = d.myname;
        return *this;
    }
private:
    string myname;
};