Inheritance in C++

An Easy Introduction to Inheritance

继承是面向对象技术得以发扬光大的原因之一。本节课,我们来学习 C++ 中最基础的继承知识,简单了解一下继承是什么。我们一般将被继承的类称为基类(也称为父类、祖先类),将继承其他类的类称为派生类(也称为子类、扩展类)。下面用一个例子展示这种关系:

class Base{ // parent class or ancestor class
// Some implementation
};
class Derived : public Base{
// Some implementation
};

一个类可以被继承和派生,正如上面基类 Base 和派生类 Derived 的关系。这里的继承可以理解为派生类接收到来自基类的传承,脉脉相承就是继承最主要的思想。类与类之间有两种关系:"is-a" 和 "has-a",请不要混淆,我们下面就来解释这两种关系。

"is-a" and "has-a" Relationship

我们所说的继承是 "is-a" 的关系,也就是“是一种”的关系。而 "has-a" 表示的是“有一个”的关系,即“composition”。我们用下面的示例来直观地感受这两种关系:

class Widget {};
class Base {};
class Derived : public Base { // class Derived is-a Base
    // Class data member
    Widget w; // class Derived has-a Widget
};

在这个例子中,我们说派生类 Derived 继承了基类 Base,所以 Derived 是一种 Base。而派生类内有个 Widget 成员,所以我们说派生类 Derived 中有一个 Widget,也就是 "has-a" 的关系。

And by the way, we also have another 'has-a' relationship called aggregation, which has a weaker relationship than compositional 'has-a'. For example, you may say a car has an engine, this is compositional 'has-a'. And also, a school could have many students, this is aggregational 'has-a'.

What Can You Get

通过继承,你能得到什么呢?我们说派生类 is-a 基类。那么,我们是否可以将派生类中的一些共性提炼出来放在基类中?实际上,我们的确就是这么做的。通过继承,派生类重用基类的代码,减少了代码的重复,提高了代码的可维护性。在功能扩展时,只需要让派生类在基类的基础上增加新的功能。

此外,继承使得基类提供不同的接口,使得派生类可以通过相同的接口做出不同的实现,这被称为多态(或动态多态)。这与我们在 静态多态 中介绍的“假”多态不同。多态的出现可以使代码在运行时通过不同的接口调用不同的派生类方法。我们将在 Virtual Dispatch in C++ 介绍这种动态多态。

Inheritance Access

在 C++ 中,我们有三种不同的继承访问控制方式: publicprotectedprivate。不同的继承访问方式决定了派生类中对基类成员的不同访问权限。我们用一个例子来展示这三种不同的继承访问控制方式的作用:

class Base {
public:
    int pub_i; // Public members are accessible from outside
protected:
    int prot_i; // Protected members are accessible to derived classes
private: 
    int pri_i; // Private members are only accessible within Base class
};

class Pub_Derived : public Base {
public:
    void accessBase() {
        pub_i = 1; // Accessible
        prot_i = 2; // Accessible
        // pri_i = 3; // Not accessible
    }
};

class Prot_Derived : protected Base {
public:
    void accessBase() {
        pub_i = 1; // Accessible
        prot_i = 2; // Accessible
        // pri_i = 3; // Not accessible
    }
};

class Pri_Derived : private Base {
public:
    void accessBase() {
        pub_i = 1; // Accessible
        prot_i = 2; // Accessible
        // pri_i = 3; // Not accessible
    }
};

int main() {
    Pub_Derived pubDerived;
    pubDerived.pub_i = 1; // Accessible from outside
    // pubDerived.prot_i = 2; // Not accessible from outside
    // pubDerived.pri_i = 3; // Not accessible from outside
    
    Prot_Derived protDerived;
    // protDerived.pub_i = 1; // Not accessible from outside
    // protDerived.prot_i = 2; // Not accessible from outside
    // protDerived.pri_i = 3; // Not accessible from outside

    Pri_Derived priDerived;
    // priDerived.pub_i = 1; // Not accessible from outside
    // priDerived.prot_i = 2; // Not accessible from outside
    // priDerived.pri_i = 3; // Not accessible from outside
    return 0;
}

从例子中,你可以看到这三种不同的继承访问控制方式的作用。我们发现,无论是哪种继承关系,派生类都不能访问基类中的 private 成员。对于 public 继承,基类中的 publicprotected 成员在派生类中的访问权限不变。对于 protected 继承,基类中的 public 成员在派生类中的访问权限会变为 protected。对于 private 继承,基类中所有类型的成员在派生类中都会变为 private 成员。

Pasted image 20250225102217.png

Derived Constructing Process

  • 构造顺序:基类 → 派生类成员变量(按声明顺序)→ 派生类构造函数体。
  • 析构顺序:派生类析构函数体 → 派生类成员变量(逆声明顺序)→ 基类析构函数
class Base {
public:
    Base() { std::cout << "Base constructed\n"; }
    ~Base() { std::cout << "Base destroyed\n"; }
};

class Derived : public Base {
public:
    Derived() { std::cout << "Derived constructed\n"; }
    ~Derived() { std::cout << "Derived destroyed\n"; }
};

int main() {
    Derived d;
}

Which to Call

当有多个不同的基类构造函数时,派生类需要在构造函数初始化列表中显式指定要调用的基类构造函数,不然编译器尝试调用基类默认构造函数。

class Base {
public:
    Base() { std::cout << "Base()\n"; }
    Base(int x) { std::cout << "Base(" << x << ")\n"; }
};

class Derived : public Base {
public:
    Derived() { std::cout << "Derived()\n"; }
    Derived(int x) : Base(x) { std::cout << "Derived(int)\n"; }

    Derived(int x, double y) : Base(x), y_(y) { 
        std::cout << "Derived(int, double)\n"; 
    }
private:
    double y_;
};

int main() {
    Derived d1;        // Base() → Derived()
    Derived d2(10);    // Base(10) → Derived(int)
    Derived d3(5, 3.14);// Base(5) → Derived(int, double)
}

Function Hiding

如果我们在派生类中定义了与基类同名的函数(non-virtual),无论该函数的参数列表是否相同,基类中的函数都会被隐藏,而不是简简单单的覆盖(override)。这种隐藏现象是名称级别的,即基类的同名函数被“隐藏”。因而,你不能直接通过派生类对象调用基类的同名函数。

#include <iostream>
class Base {
public:
    void func() {
        std::cout << "Base::func()" << std::endl;
    }
};

class Derived : public Base {
public:
    void func(int x) {
        std::cout << "Derived::func(int)" << std::endl;
    }
};

int main() {
    Derived d;
    d.func(42);  // call Derived::func(int)
    // d.func(); // error, because Base::func() was hiden
    d.Base::func(); // call Base::func()
    return 0;
}

上面的例子中,如果你想重写基类中的同名函数,你就需要用到虚函数,我们会在虚多态介绍这种机制,这是 C++ 中多态实现的方式。