C++面向对象:继承
通过继承机制,可以利用已有的数据类型来定义新的数据类型。所定义的新的数据类型不仅拥有新定义的成员,而且还同时拥有旧的成员。
基类负责定义所有类共同拥有的成员,而每个派生类定义各自特有的成员。
继承格式
单继承:
class <派生类名> : <继承权限> <基类名> {
    <派生类新定义成员>
};
继承权限
- 
    public 表示公有继承 基类的public, protected属性的成员在派生类中保持原有的访问属性。 基类的private属性的成员在派生类中不能访问。 
- 
    private 表示私有继承 基类的public, protected属性的成员在派生类中为private属性。 基类的private属性的成员在派生类中不能访问。 
- 
    protected 表示保护继承 基类的public, protected属性的成员在派生类中为protected属性。 基类的private属性的成员在派生类中不能访问。 
注意:
- 
    基类的构造函数和析构函数无法被继承。 
- 
    基类的自定义的new、delete、赋值运算符无法被继承。 
- 
    基类的友元函数无法被继承。 
- 
    派生类可以访问基类的非private静态成员。 派生类和基类中的static成员是公有的,共享内存空间。 
派生类的内存
派生类对象的内存布局:基类成员 + 派生类成员 。
- 派生类中,基类成员变量的内存位置位于派生类成员变量之前。
- 
    即使派生类不能直接访问基类的 private成员,但基类的private成员仍是派生类对象内存的一部分。
- 
    静态成员变量不占类的内存。 
- 虚函数会引入额外的虚函数指针的内存。
- 空类占1字节内存。
- 计算类的内存时,要考虑内存对齐。
class Base {
private:
    int a;
    short b;
};
class Derived : Base {
private:
    short c;
};
int main()
{
    std::cout << sizeof(Base) << std::endl;    // 输出8
    std::cout << sizeof(Derived) << std::endl; // 输出8
}
从上例中看,Derived的大小并不是Base字节对齐后的size加上Derived字节对齐后的size,而是将按顺序将Base和Derived的成员依次排列,然后整体进行字节对齐。
派生类的构造顺序
若一个类是由多个基类派生出来的,则在派生类构造函数的调用顺序:
- 按继承顺序,依次调用基类的构造函数。
- 最后调用派生类自身的构造函数。
派生类的析构顺序
若一个类是由多个基类派生出来的,则在派生类析构函数的调用顺序:
- 先调用派生类自身的析构函数。
- 按继承顺序的逆序,依次调用基类的析构函数。
显式调用基类成员
class Base
{
public:
    int a {1};
    void show() {
        std::cout << "Base a: " << a << std::endl;
    }
};
class Derived : Base
{
public:
    int a {2};
    void show() {
        Base::show();
        std::cout << "Base a: " << Base::a << std::endl;
        std::cout << "Derived a: " << a << std::endl;
    }
};
int main()
{
    Derived d;
    d.show();
}
output:
Base a: 1
Base a: 1
Derived a: 2
派生类的拷贝控制
构造函数
- 
    派生类的构造函数中只能调用直接基类的构造函数,不能调用间接基类的构造函数。 
- 
    通过派生类创建对象时必须要调用基类的构造函数,如果没有显式调用,则调用基类的默认构造函数。 
class Base {
public:
    Base() {}
    Base(int val = 0) : base_val(val) {}
private:
    int base_val;
};
class Derived : public Base {
public:
    // 没有显式调用基类构造函数:则会自动调用基类的默认构造函数
    Derived(int val = 0) : derived_val(val) {}
    // 显式调用基类构造函数
    Derived(int b_val = 0, int d_val = 0) : Base(b_val), derived_val(d_val) {}
private:
    int derived_val;
};
显式调用基类构造函数时,只能使用参数初始化表进行构造,在函数体内无法显式调用基类的构造函数,因为基类的构造函数无法被派生类访问。
拷贝构造与赋值
拷贝构造函数
- 如果派生类没有显式定义拷贝构造函数,则会自动调用基类的拷贝构造函数。
- 如果派生类显式定义拷贝构造函数,那么不会再自动调用基类的拷贝控制函数,若要调用基类的拷贝构造函数需要显式调用。
拷贝赋值运算符
- 派生类的拷贝赋值运算符不会自动调用基类的拷贝赋值运算符,必须显式调用基类的拷贝赋值运算符来处理基类部分的赋值。
class Base {
public:
    int base_val;
    Base& operator=(const Base& other) {
        if (this != &other) {
            base_val = other.base_val;
            std::cout << "Base assignment operator called." << std::endl;
        }
        return *this;
    }
};
class Derived : public Base {
public:
    int derived_val;
    Derived& operator=(const Derived& other) {
        if (this != &other) {
            // 显式调用基类的拷贝赋值运算符
            Base::operator=(other);
            derived_val = other.derived_val;
            std::cout << "Derived assignment operator called." << std::endl;
        }
        return *this;
    }
};
移动构造与赋值
拷贝构造函数
- 如果派生类没有显式定义移动构造函数,则会自动调用基类的移动构造函数。
- 如果派生类显式定义移动构造函数,那么不会再自动调用基类的移动控制函数,若要调用基类的移动构造函数需要显式调用。
移动赋值运算符
- 派生类的移动赋值运算符不会自动调用基类的移动赋值运算符,必须显式调用基类的移动赋值运算符来处理基类部分的移动赋值。
class Derived : public Base {
public:
    Derived (Derived&& other) noexcept : Base(std::move(other)) { // 显式调用基类的移动构造函数
        derived_data = other.derived_data;
        other.derived_data = nullptr;
    }
};
析构函数
当派生类对象被销毁时,先调用派生类的析构函数,然后自动调用基类的析构函数。无需显式调用基类的析构函数。
基类和派生类的转换
派生类转基类:向上转型
- 隐式转换
- 安全转换
基类转派生类:向下转型
- 无法隐式转换
- 不安全转换
    - static_cast:不安全,转换失败会导致未定义行为。
- dynamic_cast:相对安全,转换失败返回nullptr(指针)或抛出异常(引用)。