通过继承机制,可以利用已有的数据类型来定义新的数据类型。所定义的新的数据类型不仅拥有新定义的成员,而且还同时拥有旧的成员。

基类负责定义所有类共同拥有的成员,而每个派生类定义各自特有的成员。

继承格式

单继承:

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的成员依次排列,然后整体进行字节对齐

派生类的构造顺序

若一个类是由多个基类派生出来的,则在派生类构造函数的调用顺序:

  1. 按继承顺序,依次调用基类的构造函数。
  2. 最后调用派生类自身的构造函数。

派生类的析构顺序

若一个类是由多个基类派生出来的,则在派生类析构函数的调用顺序:

  1. 先调用派生类自身的析构函数。
  2. 按继承顺序的逆序,依次调用基类的析构函数。

显式调用基类成员

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

派生类的拷贝控制

构造函数

  1. 派生类的构造函数中只能调用直接基类的构造函数,不能调用间接基类的构造函数。

  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(指针)或抛出异常(引用)。