虚函数默认实参

C++默认实参是靠编译器实现的,因此默认实参不参与动态绑定,默认实参有静态类型决定。

访问控制

每个类分别控制自己的成员初始化过程,还分别控制其成员对于派生类来说是否可访问,友元不继承

成员:

  • protected:派生类可见、自己、friend可见
  • public:所有可见
  • private:自己、friend可见

继承:

public protected private
公有继承 public protected 不可见
私有继承 private private 不可见
保护继承 protected protected 不可见

继承中的作用域

  • 每个类定义自己的作用域
  • 当存在继承关系时,派生类的作用域嵌套在基类的作用域之内。
  • 名字查找首先发生在派生类的作用域内,若无法解析则编译器将继续在外层的基类作用域中寻找该名字的定义。

与其他作用域一样,派生类也能重用定义在其直接基类或间接基类中的名字,此时定义在内层作用域的名字将隐藏定义在外层作用域的名字。

函数调用的解析过程:假设调用p->mem()

  • 首先确定p的静态类型。
  • p的静态类型对应的类中查找mem。如果找不到,则以此在直接基类中不断查找直至到达继承链的顶端。如果依然找不到则编译器报错。
  • 若找到了mem,就进行常规的类型检查以确认本次调用是否合法。
  • 若调用合法,则编译器将根据mem是否是虚函数而产生不同的代码:
    • 如果mem是虚函数且通过指针或者引用进行的调用,则编译器产生的代码将在运行时确定到底运行该虚函数的哪个版本,依据对象的动态类型。
    • 否则,进行常规的函数调用。

构造函数与拷贝控制

虚析构函数

在C++中,析构函数应设为虚函数的主要原因是为了确保正确的资源释放,特别是在使用多态时。当基类的析构函数为虚函数时,通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,确保派生类对象的资源被正确释放。

如果基类的析构函数不是虚函数,通过基类指针删除派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数。这样,派生类的资源就可能无法正确释放,导致资源泄漏或其他问题。

#include <iostream>

class Base {
public:
    ~Base() {
        std::cout << "Base destructor called" << std::endl;
    }
};

class Derived : public Base {
public:
    ~Derived() {
        std::cout << "Derived destructor called" << std::endl;
    }
};

int main() {
    Base* ptr = new Derived();
    delete ptr;  // 只调用了Base的析构函数,没有调用Derived的析构函数
    return 0;
}

如果基类的析构函数不是虚函数,则delete一个指向派生类的基类指针将产生未定义的行为。

虚析构函数将阻止合成移动操作,即使通过=default的形式使用合成的版本。

合成的拷贝构造函数

  • 合成的拷贝构造函数首先会调用基类的拷贝构造函数,以拷贝派生类对象的基类子对象。这个调用会遵循基类中定义的拷贝构造函数的逻辑。
  • 对于派生类中定义的成员变量,合成的拷贝构造函数会按照成员的定义顺序逐个进行拷贝。这种拷贝是成员级的浅拷贝,即简单地将一个对象的成员值复制到另一个对象的对应成员中。

删除的拷贝控制

  • 如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符、析构函数、移动构造函数、移动赋值运算符是被删除的的或者不可访问,则派生类中对应的成员将是被删除的。原因是编译器不能使用基类成员来执行派生类对象基类部分的构造、赋值、移动或者销毁。
  • 默认构造、拷贝构造、移动构造涉及临时对象的析构,因此当基类有一个不可访问或者删掉的析构函数时,则派生类中合成的默认构造函数

派生类的拷贝控制成员

派生类的拷贝、移动成员在拷贝和移动自由成员的同时,也需要拷贝和移动基类部分的成员。当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象。

#include <iostream>
#include <string>

// 基类
class Base {
public:
    std::string name;
    Base(const std::string& n) : name(n) {}
    
    // 拷贝构造函数
    Base(const Base& other) : name(other.name) {
        std::cout << "Base 拷贝构造函数\n";
    }

    // 移动构造函数
    Base(Base&& other) noexcept : name(std::move(other.name)) {
        std::cout << "Base 移动构造函数\n";
    }
};

// 派生类
class Derived : public Base {
public:
    int value;
    
    Derived(const std::string& n, int v) : Base(n), value(v) {}

    // 拷贝构造函数
    Derived(const Derived& other) : Base(other), value(other.value) {
        std::cout << "Derived 拷贝构造函数\n";
    }

    // 移动构造函数
    Derived(Derived&& other) noexcept : Base(std::move(other)), value(other.value) {
        std::cout << "Derived 移动构造函数\n";
    }
};

int main() {
    Derived d1("Example", 42);
    Derived d2 = d1;  // 调用拷贝构造函数
    Derived d3 = std::move(d1);  // 调用移动构造函数
}

派生类的析构函数

派生类的析构函数只负责清理派生类自己分配的资源,而基类的资源清理是通过隐式调用基类的析构函数来完成的。

  • 当派生类的对象被销毁时,系统会自动调用派生类的析构函数。
  • 派生类的析构函数执行完后,系统会自动调用基类的析构函数来清理基类部分的成员。
#include <iostream>

// 基类
class Base {
public:
    Base() { std::cout << "Base 构造函数\n"; }
    ~Base() { std::cout << "Base 析构函数\n"; }
};

// 派生类
class Derived : public Base {
public:
    Derived() { std::cout << "Derived 构造函数\n"; }
    ~Derived() { std::cout << "Derived 析构函数\n"; }
};

int main() {
    Derived d;
}

继承构造函数

class Derived : public Base{
public:
    using Base::Base; 
}

通常using声明语句只是令某个名字在当前作用域中可见,而当作用域构造函数是将令编译器产生代码:

Derived(params): Base(args){}