C++ 继承详解:访问控制、对象模型、菱形继承与设计取舍

C++ 继承详解:访问控制、对象模型、菱形继承与设计取舍

继承是 C++ 面向对象编程中很容易"会写语法,却说不清原理"的部分。写出 class Student : public Person 并不难,真正需要理解的是:基类成员怎样进入派生类对象、不同继承方式会改变什么、对象切片为何发生、构造与析构按什么顺序执行,以及多继承中的菱形结构为什么会带来二义性。

这篇文章从一个简单的人员关系模型出发,逐步拆开继承的语法、访问控制和对象模型,再讨论虚继承与组合等设计问题。示例使用 C++11,可在 Linux 下直接编译运行。

一、继承解决了什么问题

假设系统中有学生和教师两类对象,它们都具备姓名、年龄等属性。如果分别定义两个完全独立的类,就会产生明显的重复:

cpp 复制代码
class Student {
private:
    std::string name_;
    int age_;
    int student_id_;
};

class Teacher {
private:
    std::string name_;
    int age_;
    int employee_id_;
};

name_age_ 表达的是两者共有的"人"的特征,可以把这些共性提取到 Person 中,让 StudentTeacher 在此基础上增加自己的属性:

cpp 复制代码
#include <iostream>
#include <string>

class Person {
public:
    Person(const std::string& name, int age)
        : name_(name), age_(age) {}

    void PrintBasicInfo() const {
        std::cout << "name: " << name_
                  << ", age: " << age_ << '\n';
    }

protected:
    std::string name_;

private:
    int age_;
};

class Student : public Person {
public:
    Student(const std::string& name, int age, int student_id)
        : Person(name, age), student_id_(student_id) {}

    void PrintStudentInfo() const {
        PrintBasicInfo();
        std::cout << "student id: " << student_id_ << '\n';
    }

private:
    int student_id_;
};

int main() {
    Student student("Lin", 20, 1001);
    student.PrintStudentInfo();
    return 0;
}

编译并运行:

bash 复制代码
g++ -std=c++11 -Wall -Wextra -pedantic inheritance_basic.cpp -o inheritance_basic
./inheritance_basic

预期输出:

text 复制代码
name: Lin, age: 20
student id: 1001

这里的 Person 称为基类Student 称为派生类。派生类对象中包含一个基类子对象,同时还保存派生类新增的成员。

text 复制代码
Student 对象
+--------------------------+
| Person 基类子对象         |
|   name_                  |
|   age_                   |
+--------------------------+
| Student 自有成员          |
|   student_id_            |
+--------------------------+

这个示意图只表达逻辑组成,不代表编译器必须采用某种固定的内存排列。对齐、填充、虚函数以及虚继承都会影响实际布局。

二、继承方式与访问控制

继承语法由派生类名、继承方式和基类名组成:

cpp 复制代码
class Derived : public Base {
    // 派生类成员
};

继承方式可以是 publicprotectedprivate。它们不会改变基类成员原本的访问属性,而是决定这些成员以怎样的可见级别出现在派生类中。

基类成员属性 public 继承后 protected 继承后 private 继承后
public public protected private
protected protected protected private
private 派生类不可直接访问 派生类不可直接访问 派生类不可直接访问

可以把变化规律概括为:

  • 基类中的 private 成员始终不能被派生类直接访问;
  • 其他成员继承后的访问级别,不会比继承方式更开放;
  • public 继承完整保留基类对外接口,最适合表达"派生类也是一种基类"。

1. private 成员是否被继承

很多初学者会把"不能直接访问"理解为"没有进入派生类对象"。这是两个不同的问题。

基类的 private 数据仍然存在于派生类对象的基类子对象中,只是派生类成员函数不能越过基类的访问边界。访问它们需要调用基类提供的 publicprotected 接口。

cpp 复制代码
class Base {
public:
    explicit Base(int value) : value_(value) {}

    int GetValue() const {
        return value_;
    }

private:
    int value_;
};

class Derived : public Base {
public:
    explicit Derived(int value) : Base(value) {}

    int ReadValue() const {
        // return value_;  // 错误:不能直接访问 Base::value_
        return GetValue();
    }
};

2. classstruct 的默认继承方式

省略继承方式时,两者的默认值不同:

cpp 复制代码
class DerivedClass : Base {};   // 默认 private 继承
struct DerivedStruct : Base {}; // 默认 public 继承

实际开发中建议明确写出继承方式,避免读代码的人还要根据关键字推断。

3. 为什么最常见的是 public 继承

public 继承通常用来表达 is-a 关系,也就是"派生类是基类的一种"。如果 Student 公有继承 Person,那么需要 Person 的地方原则上也应当能够接收 Student

protectedprivate 继承更接近"借用基类实现",它们会收紧基类接口的对外可见性。在只需要复用实现时,组合往往比非公有继承更直观。

三、派生类到基类的转换与对象切片

公有继承建立了派生类到基类的天然转换关系。派生类对象可以转换为基类对象,也可以绑定到基类引用或赋给基类指针。

cpp 复制代码
Student student("Lin", 20, 1001);

Person person = student;       // 构造一个独立的 Person,发生切片
Person& person_ref = student;  // 引用绑定到 Student 内的基类子对象
Person* person_ptr = &student; // 指针指向 Student 内的基类子对象

1. 什么是对象切片

执行 Person person = student; 时,目标对象的类型就是 Person。复制过程只保留 Student 中的 Person 部分,student_id_ 不会进入新对象,这种现象称为对象切片

text 复制代码
Student: [ Person 部分 ][ student_id_ ]
                    |
                    | 复制到 Person 对象
                    v
Person : [ Person 部分 ]

引用和指针转换不会创建新的基类对象,因此不会切掉派生类部分。它们只是以基类视角访问原来的对象,这也是多态通常使用基类引用或指针的原因。

2. 基类不能自动转换为派生类

一个普通 Person 并不一定是 Student,因此下面的转换不成立:

cpp 复制代码
Person person("Lin", 20);
// Student student = person;       // 错误
// Student* ptr = &person;          // 错误
// Student& ref = person;           // 错误

向下转换只有在对象的真实类型确实是目标派生类时才可能安全。若基类是多态类型,也就是至少包含一个虚函数,应使用 dynamic_cast 做运行时检查,而不是仅凭开发者的猜测强制转换。

注意:继承语法允许转换,并不意味着所有继承关系在设计上都合理。公有继承还应满足"派生对象能够替代基类对象"的语义要求。

四、继承体系中的作用域与成员隐藏

基类和派生类拥有各自独立的作用域。如果派生类声明了与基类同名的成员,派生类成员会隐藏基类中的同名成员。

cpp 复制代码
#include <iostream>

class Base {
public:
    void Print(int value) const {
        std::cout << "Base::Print(int): " << value << '\n';
    }

    int value = 10;
};

class Derived : public Base {
public:
    void Print() const {
        std::cout << "Derived::Print()\n";
    }

    int value = 20;
};

int main() {
    Derived object;
    object.Print();
    object.Base::Print(5);

    std::cout << object.value << '\n';
    std::cout << object.Base::value << '\n';
}

预期输出:

text 复制代码
Derived::Print()
Base::Print(int): 5
20
10

这里需要特别注意:函数隐藏只看名字,不要求参数列表相同Derived::Print() 会隐藏基类中所有名为 Print 的函数,因此 object.Print(5) 无法直接通过编译。

隐藏也不是重载。重载要求多个同名函数处于同一个作用域,而这里的两个函数分别属于基类和派生类作用域。

如果希望把基类的同名重载集合引入派生类作用域,可以使用 using

cpp 复制代码
class Derived : public Base {
public:
    using Base::Print;

    void Print() const {
        std::cout << "Derived::Print()\n";
    }
};

此时 object.Print()object.Print(5) 都是合法调用。若只想在特定位置使用基类版本,写成 object.Base::Print(5) 更明确。

五、派生类对象如何构造、复制和销毁

派生类对象包含基类子对象,所以它的生命周期不是只运行派生类自己的函数。编译器必须先建立基类部分,再建立派生类部分。

1. 构造顺序

cpp 复制代码
#include <iostream>
#include <string>

class Person {
public:
    explicit Person(const std::string& name) : name_(name) {
        std::cout << "Person constructor\n";
    }

    ~Person() {
        std::cout << "Person destructor\n";
    }

private:
    std::string name_;
};

class Student : public Person {
public:
    Student(const std::string& name, int id)
        : Person(name), id_(id) {
        std::cout << "Student constructor\n";
    }

    ~Student() {
        std::cout << "Student destructor\n";
    }

private:
    int id_;
};

int main() {
    Student student("Lin", 1001);
}

预期输出:

text 复制代码
Person constructor
Student constructor
Student destructor
Person destructor

完整顺序可以理解为:
#mermaid-svg-Tk8BjQCCeiMEAeqH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Tk8BjQCCeiMEAeqH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Tk8BjQCCeiMEAeqH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Tk8BjQCCeiMEAeqH .error-icon{fill:#552222;}#mermaid-svg-Tk8BjQCCeiMEAeqH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Tk8BjQCCeiMEAeqH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Tk8BjQCCeiMEAeqH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Tk8BjQCCeiMEAeqH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Tk8BjQCCeiMEAeqH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Tk8BjQCCeiMEAeqH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Tk8BjQCCeiMEAeqH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Tk8BjQCCeiMEAeqH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Tk8BjQCCeiMEAeqH .marker.cross{stroke:#333333;}#mermaid-svg-Tk8BjQCCeiMEAeqH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Tk8BjQCCeiMEAeqH p{margin:0;}#mermaid-svg-Tk8BjQCCeiMEAeqH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Tk8BjQCCeiMEAeqH .cluster-label text{fill:#333;}#mermaid-svg-Tk8BjQCCeiMEAeqH .cluster-label span{color:#333;}#mermaid-svg-Tk8BjQCCeiMEAeqH .cluster-label span p{background-color:transparent;}#mermaid-svg-Tk8BjQCCeiMEAeqH .label text,#mermaid-svg-Tk8BjQCCeiMEAeqH span{fill:#333;color:#333;}#mermaid-svg-Tk8BjQCCeiMEAeqH .node rect,#mermaid-svg-Tk8BjQCCeiMEAeqH .node circle,#mermaid-svg-Tk8BjQCCeiMEAeqH .node ellipse,#mermaid-svg-Tk8BjQCCeiMEAeqH .node polygon,#mermaid-svg-Tk8BjQCCeiMEAeqH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Tk8BjQCCeiMEAeqH .rough-node .label text,#mermaid-svg-Tk8BjQCCeiMEAeqH .node .label text,#mermaid-svg-Tk8BjQCCeiMEAeqH .image-shape .label,#mermaid-svg-Tk8BjQCCeiMEAeqH .icon-shape .label{text-anchor:middle;}#mermaid-svg-Tk8BjQCCeiMEAeqH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Tk8BjQCCeiMEAeqH .rough-node .label,#mermaid-svg-Tk8BjQCCeiMEAeqH .node .label,#mermaid-svg-Tk8BjQCCeiMEAeqH .image-shape .label,#mermaid-svg-Tk8BjQCCeiMEAeqH .icon-shape .label{text-align:center;}#mermaid-svg-Tk8BjQCCeiMEAeqH .node.clickable{cursor:pointer;}#mermaid-svg-Tk8BjQCCeiMEAeqH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Tk8BjQCCeiMEAeqH .arrowheadPath{fill:#333333;}#mermaid-svg-Tk8BjQCCeiMEAeqH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Tk8BjQCCeiMEAeqH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Tk8BjQCCeiMEAeqH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Tk8BjQCCeiMEAeqH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Tk8BjQCCeiMEAeqH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Tk8BjQCCeiMEAeqH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Tk8BjQCCeiMEAeqH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Tk8BjQCCeiMEAeqH .cluster text{fill:#333;}#mermaid-svg-Tk8BjQCCeiMEAeqH .cluster span{color:#333;}#mermaid-svg-Tk8BjQCCeiMEAeqH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Tk8BjQCCeiMEAeqH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Tk8BjQCCeiMEAeqH rect.text{fill:none;stroke-width:0;}#mermaid-svg-Tk8BjQCCeiMEAeqH .icon-shape,#mermaid-svg-Tk8BjQCCeiMEAeqH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Tk8BjQCCeiMEAeqH .icon-shape p,#mermaid-svg-Tk8BjQCCeiMEAeqH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Tk8BjQCCeiMEAeqH .icon-shape .label rect,#mermaid-svg-Tk8BjQCCeiMEAeqH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Tk8BjQCCeiMEAeqH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Tk8BjQCCeiMEAeqH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Tk8BjQCCeiMEAeqH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 构造虚基类
构造普通基类
按声明顺序构造成员
执行派生类构造函数体
执行派生类析构函数体
按逆序销毁成员
按逆序销毁普通基类
销毁虚基类

没有虚继承时,可以忽略图中的虚基类步骤。

如果基类没有默认构造函数,派生类必须在初始化列表中明确调用合适的基类构造函数:

cpp 复制代码
Student(const std::string& name, int id)
    : Person(name), id_(id) {}

不能在派生类构造函数体中"重新构造"基类。进入函数体之前,所有基类和成员对象都已经完成初始化。

2. 拷贝构造与赋值

编译器生成的拷贝构造和赋值运算符会依次处理基类部分及派生类成员。只有在需要自定义复制逻辑时,才需要手动编写。

cpp 复制代码
class Student : public Person {
public:
    Student(const Student& other)
        : Person(other), id_(other.id_) {}

    Student& operator=(const Student& other) {
        if (this != &other) {
            Person::operator=(other);
            id_ = other.id_;
        }
        return *this;
    }

private:
    int id_;
};

这段代码的关键在于:派生类负责协调完整对象的复制,基类部分交给基类自己的复制函数处理。调用赋值运算符时写出 Person::operator=,也能避免派生类同名函数造成的查找问题。

对于只包含标准库对象和普通值成员的类,优先使用编译器生成的版本。无必要地手写特殊成员函数,反而容易漏掉基类或某个成员。

3. 通过基类指针销毁派生对象

如果类会被当作多态基类使用,析构函数应声明为 virtual

cpp 复制代码
class Person {
public:
    virtual ~Person() = default;
};

class Student : public Person {
public:
    ~Student() override = default;
};

这样执行 delete person_ptr; 时,即使 person_ptr 的静态类型是 Person*,只要它实际指向 Student,也会先调用 Student 的析构函数,再调用 Person 的析构函数。

注意:通过没有虚析构函数的基类指针删除派生对象会产生未定义行为。只要一个类准备承载运行时多态,虚析构函数通常就是接口的一部分。

六、类模板继承中的名称查找

类模板继承有一个常见编译错误:明明基类中存在某个成员函数,派生类模板却提示找不到名称。

下面是为了帮助理解而补充的示例:

cpp 复制代码
#include <cstddef>
#include <vector>

template <class T>
class StackByInheritance : private std::vector<T> {
public:
    void Push(const T& value) {
        this->push_back(value);
    }

    void Pop() {
        this->pop_back();
    }

    const T& Top() const {
        return this->back();
    }

    bool Empty() const {
        return this->empty();
    }

    std::size_t Size() const {
        return this->size();
    }
};

std::vector<T> 是依赖模板参数的基类。在模板定义的第一阶段名称查找中,编译器不会自动到依赖基类中寻找未限定名称,因此需要写成 this->push_back(value),也可以使用完整限定名。

这段代码适合说明语法,却不是栈的理想设计。栈并不是一种向量,它只是使用某种顺序容器保存元素,因此组合更符合 has-a 关系:

cpp 复制代码
template <class T>
class Stack {
public:
    void Push(const T& value) {
        data_.push_back(value);
    }

    void Pop() {
        data_.pop_back();
    }

    const T& Top() const {
        return data_.back();
    }

    bool Empty() const {
        return data_.empty();
    }

private:
    std::vector<T> data_;
};

组合版本不会把 vector 的类型关系带入 Stack,也不会让实现细节干扰栈的接口。

七、继承中的友元、静态成员与 final

1. 友元关系不会继承

一个函数是基类的友元,并不代表它自动成为派生类的友元:

cpp 复制代码
class Base {
    friend void Inspect(const Base& object);

private:
    int base_secret_ = 1;
};

class Derived : public Base {
private:
    int derived_secret_ = 2;
};

Inspect 可以访问 Base::base_secret_,但不能因为 Derived 继承了 Base 就访问 Derived::derived_secret_。如果确有需要,派生类必须单独声明友元。

友元关系也不具备传递性。A 是 B 的友元、B 是 C 的友元,并不能推出 A 是 C 的友元。

2. 静态成员在继承体系中只有一份

静态成员属于类本身,不属于某个具体对象。基类和所有派生类访问的是同一个静态成员:

cpp 复制代码
#include <iostream>

class Person {
public:
    Person() {
        ++count_;
    }

    static int Count() {
        return count_;
    }

private:
    static int count_;
};

int Person::count_ = 0;

class Student : public Person {};
class Teacher : public Person {};

int main() {
    Student student;
    Teacher teacher;
    std::cout << Person::Count() << '\n';
}

预期输出为 2。这里没有为 StudentTeacher 各复制一份 count_,它们共同使用 Person::count_

3. 用 final 禁止继续派生

C++11 可以在类名后使用 final,明确表示该类不能再作为基类:

cpp 复制代码
class ClosedType final {
public:
    void Run() {}
};

// class Child : public ClosedType {}; // 编译错误

与利用私有构造函数限制继承相比,final 的意图更清楚,编译器给出的诊断也更直接。它适用于类型语义已经封闭、不希望扩展破坏约束的场景。

八、多继承与基类子对象

C++ 允许一个派生类同时继承多个基类:

cpp 复制代码
class Scanner {
public:
    void Scan() const {}
};

class Printer {
public:
    void Print() const {}
};

class AllInOne : public Scanner, public Printer {};

AllInOne 中分别包含 ScannerPrinter 两个基类子对象。转换为不同基类指针时,编译器会让指针准确指向对应的子对象:

cpp 复制代码
AllInOne device;
Scanner* scanner = &device;
Printer* printer = &device;

由于两个基类子对象可能位于对象内的不同位置,scannerprinter 保存的数值地址可能不同。指针调整由编译器自动完成,不能根据某次运行观察到的地址推断所有平台上的固定布局。

多继承的直接二义性

如果两个基类具有同名成员,派生对象直接调用时会产生二义性:

cpp 复制代码
class Scanner {
public:
    void Start() const {}
};

class Printer {
public:
    void Start() const {}
};

class AllInOne : public Scanner, public Printer {};

AllInOne device;
// device.Start();          // 错误:不知道调用哪一个
device.Scanner::Start();
device.Printer::Start();

作用域限定符能够说明要访问哪条继承路径,但它只解决调用表达式的歧义,并不会改变对象中包含多个基类子对象的事实。

九、菱形继承为什么麻烦

菱形继承是多继承中最典型的结构:
#mermaid-svg-vK8eZGKmAUKT9fYS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vK8eZGKmAUKT9fYS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vK8eZGKmAUKT9fYS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vK8eZGKmAUKT9fYS .error-icon{fill:#552222;}#mermaid-svg-vK8eZGKmAUKT9fYS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vK8eZGKmAUKT9fYS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vK8eZGKmAUKT9fYS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vK8eZGKmAUKT9fYS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vK8eZGKmAUKT9fYS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vK8eZGKmAUKT9fYS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vK8eZGKmAUKT9fYS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vK8eZGKmAUKT9fYS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vK8eZGKmAUKT9fYS .marker.cross{stroke:#333333;}#mermaid-svg-vK8eZGKmAUKT9fYS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vK8eZGKmAUKT9fYS p{margin:0;}#mermaid-svg-vK8eZGKmAUKT9fYS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vK8eZGKmAUKT9fYS .cluster-label text{fill:#333;}#mermaid-svg-vK8eZGKmAUKT9fYS .cluster-label span{color:#333;}#mermaid-svg-vK8eZGKmAUKT9fYS .cluster-label span p{background-color:transparent;}#mermaid-svg-vK8eZGKmAUKT9fYS .label text,#mermaid-svg-vK8eZGKmAUKT9fYS span{fill:#333;color:#333;}#mermaid-svg-vK8eZGKmAUKT9fYS .node rect,#mermaid-svg-vK8eZGKmAUKT9fYS .node circle,#mermaid-svg-vK8eZGKmAUKT9fYS .node ellipse,#mermaid-svg-vK8eZGKmAUKT9fYS .node polygon,#mermaid-svg-vK8eZGKmAUKT9fYS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vK8eZGKmAUKT9fYS .rough-node .label text,#mermaid-svg-vK8eZGKmAUKT9fYS .node .label text,#mermaid-svg-vK8eZGKmAUKT9fYS .image-shape .label,#mermaid-svg-vK8eZGKmAUKT9fYS .icon-shape .label{text-anchor:middle;}#mermaid-svg-vK8eZGKmAUKT9fYS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vK8eZGKmAUKT9fYS .rough-node .label,#mermaid-svg-vK8eZGKmAUKT9fYS .node .label,#mermaid-svg-vK8eZGKmAUKT9fYS .image-shape .label,#mermaid-svg-vK8eZGKmAUKT9fYS .icon-shape .label{text-align:center;}#mermaid-svg-vK8eZGKmAUKT9fYS .node.clickable{cursor:pointer;}#mermaid-svg-vK8eZGKmAUKT9fYS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vK8eZGKmAUKT9fYS .arrowheadPath{fill:#333333;}#mermaid-svg-vK8eZGKmAUKT9fYS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vK8eZGKmAUKT9fYS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vK8eZGKmAUKT9fYS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vK8eZGKmAUKT9fYS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vK8eZGKmAUKT9fYS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vK8eZGKmAUKT9fYS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vK8eZGKmAUKT9fYS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vK8eZGKmAUKT9fYS .cluster text{fill:#333;}#mermaid-svg-vK8eZGKmAUKT9fYS .cluster span{color:#333;}#mermaid-svg-vK8eZGKmAUKT9fYS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vK8eZGKmAUKT9fYS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vK8eZGKmAUKT9fYS rect.text{fill:none;stroke-width:0;}#mermaid-svg-vK8eZGKmAUKT9fYS .icon-shape,#mermaid-svg-vK8eZGKmAUKT9fYS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vK8eZGKmAUKT9fYS .icon-shape p,#mermaid-svg-vK8eZGKmAUKT9fYS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vK8eZGKmAUKT9fYS .icon-shape .label rect,#mermaid-svg-vK8eZGKmAUKT9fYS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vK8eZGKmAUKT9fYS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vK8eZGKmAUKT9fYS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vK8eZGKmAUKT9fYS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Person
Student
Teacher
Assistant

假设助教既是学生也是教师,可以写成:

cpp 复制代码
class Person {
public:
    std::string name;
};

class Student : public Person {};
class Teacher : public Person {};
class Assistant : public Student, public Teacher {};

此时 Assistant 中存在两份 Person 子对象:一份沿 Student 路径继承而来,另一份沿 Teacher 路径继承而来。

text 复制代码
Assistant
+--------------------------+
| Student                  |
|   Person                 |
|     name                 |
+--------------------------+
| Teacher                  |
|   Person                 |
|     name                 |
+--------------------------+

因此下面的访问具有二义性:

cpp 复制代码
Assistant assistant;
// assistant.name = "Lin"; // 错误:是哪一份 Person::name?

assistant.Student::name = "student identity";
assistant.Teacher::name = "teacher identity";

限定继承路径虽然能通过编译,但对象里仍然有两份姓名。如果业务语义要求助教只拥有一份人员身份,就需要虚继承。

十、虚继承如何消除公共基类的重复

让菱形结构中间层虚继承公共基类:

cpp 复制代码
class Student : virtual public Person {};
class Teacher : virtual public Person {};
class Assistant : public Student, public Teacher {};

虚继承保证在一个最派生对象中,指定的虚基类只保留一份。此时 Assistant 的两条继承路径共同指向同一个 Person 子对象。
#mermaid-svg-erwaFaX9OTeN66p5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-erwaFaX9OTeN66p5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-erwaFaX9OTeN66p5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-erwaFaX9OTeN66p5 .error-icon{fill:#552222;}#mermaid-svg-erwaFaX9OTeN66p5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-erwaFaX9OTeN66p5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-erwaFaX9OTeN66p5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-erwaFaX9OTeN66p5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-erwaFaX9OTeN66p5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-erwaFaX9OTeN66p5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-erwaFaX9OTeN66p5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-erwaFaX9OTeN66p5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-erwaFaX9OTeN66p5 .marker.cross{stroke:#333333;}#mermaid-svg-erwaFaX9OTeN66p5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-erwaFaX9OTeN66p5 p{margin:0;}#mermaid-svg-erwaFaX9OTeN66p5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-erwaFaX9OTeN66p5 .cluster-label text{fill:#333;}#mermaid-svg-erwaFaX9OTeN66p5 .cluster-label span{color:#333;}#mermaid-svg-erwaFaX9OTeN66p5 .cluster-label span p{background-color:transparent;}#mermaid-svg-erwaFaX9OTeN66p5 .label text,#mermaid-svg-erwaFaX9OTeN66p5 span{fill:#333;color:#333;}#mermaid-svg-erwaFaX9OTeN66p5 .node rect,#mermaid-svg-erwaFaX9OTeN66p5 .node circle,#mermaid-svg-erwaFaX9OTeN66p5 .node ellipse,#mermaid-svg-erwaFaX9OTeN66p5 .node polygon,#mermaid-svg-erwaFaX9OTeN66p5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-erwaFaX9OTeN66p5 .rough-node .label text,#mermaid-svg-erwaFaX9OTeN66p5 .node .label text,#mermaid-svg-erwaFaX9OTeN66p5 .image-shape .label,#mermaid-svg-erwaFaX9OTeN66p5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-erwaFaX9OTeN66p5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-erwaFaX9OTeN66p5 .rough-node .label,#mermaid-svg-erwaFaX9OTeN66p5 .node .label,#mermaid-svg-erwaFaX9OTeN66p5 .image-shape .label,#mermaid-svg-erwaFaX9OTeN66p5 .icon-shape .label{text-align:center;}#mermaid-svg-erwaFaX9OTeN66p5 .node.clickable{cursor:pointer;}#mermaid-svg-erwaFaX9OTeN66p5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-erwaFaX9OTeN66p5 .arrowheadPath{fill:#333333;}#mermaid-svg-erwaFaX9OTeN66p5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-erwaFaX9OTeN66p5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-erwaFaX9OTeN66p5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-erwaFaX9OTeN66p5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-erwaFaX9OTeN66p5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-erwaFaX9OTeN66p5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-erwaFaX9OTeN66p5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-erwaFaX9OTeN66p5 .cluster text{fill:#333;}#mermaid-svg-erwaFaX9OTeN66p5 .cluster span{color:#333;}#mermaid-svg-erwaFaX9OTeN66p5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-erwaFaX9OTeN66p5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-erwaFaX9OTeN66p5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-erwaFaX9OTeN66p5 .icon-shape,#mermaid-svg-erwaFaX9OTeN66p5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-erwaFaX9OTeN66p5 .icon-shape p,#mermaid-svg-erwaFaX9OTeN66p5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-erwaFaX9OTeN66p5 .icon-shape .label rect,#mermaid-svg-erwaFaX9OTeN66p5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-erwaFaX9OTeN66p5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-erwaFaX9OTeN66p5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-erwaFaX9OTeN66p5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Assistant 对象
Student 子对象
Teacher 子对象
共享的 Person 虚基类子对象

1. 虚基类由最派生类初始化

虚继承最需要记住的规则是:虚基类由最派生类直接负责初始化

cpp 复制代码
#include <iostream>
#include <string>

class Person {
public:
    explicit Person(const std::string& name) : name_(name) {}

    const std::string& Name() const {
        return name_;
    }

private:
    std::string name_;
};

class Student : virtual public Person {
public:
    Student(const std::string& name, int student_id)
        : Person(name), student_id_(student_id) {}

private:
    int student_id_;
};

class Teacher : virtual public Person {
public:
    Teacher(const std::string& name, int employee_id)
        : Person(name), employee_id_(employee_id) {}

private:
    int employee_id_;
};

class Assistant : public Student, public Teacher {
public:
    Assistant(const std::string& name, int student_id, int employee_id)
        : Person(name),
          Student(name, student_id),
          Teacher(name, employee_id) {}
};

int main() {
    Assistant assistant("Lin", 1001, 9001);
    std::cout << assistant.Name() << '\n';
}

编译运行:

bash 复制代码
g++ -std=c++11 -Wall -Wextra -pedantic virtual_inheritance.cpp -o virtual_inheritance
./virtual_inheritance

预期输出:

text 复制代码
Lin

单独构造 Student 时,Student 是最派生类,所以它负责初始化 Person。构造 Assistant 时,Assistant 才是最派生类,中间层初始化列表中的 Person(name) 不负责最终那份虚基类对象,真正生效的是 AssistantPerson 的直接初始化。

2. 虚继承的成本与边界

编译器通常需要保存额外信息,以便不同继承路径找到共享的虚基类子对象。对象大小、成员偏移和访问方式都可能因此变化,具体实现由编译器和 ABI 决定。

虚继承解决的是共享公共基类子对象的问题,不是普通虚函数机制的替代品,也不会自动让设计变得合理。看到菱形结构时,应先确认它确实表达业务关系,再决定是否使用虚继承。

标准输入输出体系就是虚继承的典型应用之一:输入流和输出流共享基础状态,双向流需要避免保留两份重复的公共基类状态。这类层次结构经过严格设计,普通业务类不必为了展示技巧主动制造复杂继承图。

十一、继承还是组合

继承强调类型关系,组合强调对象协作。判断时可以问两个问题:

  1. 派生类是否真的是基类的一种,并且能够在所有基类使用场景中替代它?
  2. 当前类型是否只是需要另一个类型提供能力或保存状态?
对比维度 公有继承 组合
关系语义 is-a has-a
复用方式 复用基类接口与实现 把工作委托给成员对象
耦合程度 派生类依赖基类设计 主要依赖成员对象的公开接口
替换实现 容易受继承层次约束 通常更灵活
典型用途 类型扩展、运行时多态 功能拼装、状态持有

汽车和轮胎适合组合:

cpp 复制代码
class Tire {
public:
    void Rotate() {}
};

class Car {
public:
    void Drive() {
        tire_.Rotate();
    }

private:
    Tire tire_;
};

汽车有轮胎,但汽车不是轮胎。反过来,某种具体汽车若确实满足通用汽车接口,可以继承 Car

很多设计问题并不是"能不能继承",而是"继承是否准确表达了关系"。当继承和组合都能完成复用时,组合通常拥有更小的耦合面;当需要真正的类型替换和运行时多态时,公有继承才更自然。

十二、常见问题与易错点

1. 派生类能否直接访问基类的 private 成员

不能。成员仍存在于基类子对象中,但只能通过基类提供的接口访问。不要为了让派生类方便修改,就把所有数据都改成 protected;这会扩大耦合范围,使基类难以维护自身不变式。

2. public 成员经过 private 继承后还是 public

不是。它在派生类中会变成 private,派生类对象不能再从外部直接调用这个基类接口。

3. 派生类定义同名函数算重载吗

通常不算。它们位于不同作用域,派生类函数会隐藏基类同名函数。可以通过作用域限定符或 using Base::Function 恢复所需接口。

4. 派生类对象赋给基类对象后还能恢复吗

不能依赖原对象信息恢复。切片后得到的是独立的基类对象,派生类新增状态已经不在其中。需要保留动态类型时,应传递基类引用、指针或智能指针。

5. 构造函数初始化列表的书写顺序决定构造顺序吗

不决定。虚基类、普通基类和成员有语言规定的实际初始化顺序;成员对象按它们在类中声明的顺序构造,而不是按初始化列表的排列顺序。建议让初始化列表与实际顺序一致,减少误判和编译器警告。

6. 析构函数什么时候必须是虚函数

当对象可能通过基类指针被删除时,基类析构函数必须是虚函数。若基类根本不允许多态销毁,也可以采用受保护的非虚析构等设计,但这需要清楚限制对象的使用方式。

7. 虚继承和虚函数是一回事吗

不是。虚函数解决运行时调用哪个重写函数的问题;虚继承解决菱形结构中公共基类子对象重复的问题。两者都使用 virtual,目标完全不同。

8. 多继承一定不能使用吗

不是。多个基类代表相互独立的能力接口时,多继承可以很清晰。风险主要来自状态复杂、同名成员、公共基类重复和初始化责任不明确。继承层次越复杂,越需要确认每一条边都具有明确语义。

十三、总结

理解继承不能停在冒号后的 public。派生类对象包含基类子对象,访问权限、名称查找、类型转换和生命周期都围绕这一点展开。

公有继承应表达可靠的 is-a 关系;派生类到基类对象的值转换会发生切片,而引用和指针转换只是改变观察视角;同名成员产生的是隐藏,不等同于重载;基类先构造、后析构,多态基类还需要虚析构函数。

进入多继承后,要把"继承路径"和"实际基类子对象"区分开。普通菱形结构会形成两份公共基类,虚继承让最派生对象只保留一份,并把初始化责任交给最派生类。设计层面则要克制使用继承:真正的类型关系用继承,仅仅为了复用和协作用组合,代码通常会更稳定。

相关推荐
程序员爱钓鱼2 小时前
Go 运算符详解
后端·面试·go
JaguarJack2 小时前
Fiber 与 PHP 8.6 Polling API 异步初探
后端·php·服务端
爱敲代码的憨仔2 小时前
BM25全文检索
开发语言·python·全文检索
gbase_lmax2 小时前
深度递归与广度递归(DFS,BFS)实现
算法·深度优先·宽度优先
程序员爱钓鱼2 小时前
Rust 生命周期案例详解:从编译错误到真实业务场景
前端·后端·rust
SamChan903 小时前
用Python+Requests批量翻译PDF:从脚本到调度
后端·python·microsoft·ai·pdf·机器翻译
西柚研究生1234563 小时前
论文分析15:跨层特征交互的多尺度无人机小目标检测算法
人工智能·算法·目标检测
油丶酸萝卜别吃8 小时前
utils.js 说明文档
开发语言·javascript·ecmascript
zyplayer-doc10 小时前
VuePress类静态文档站和动态知识库怎么选:两种技术路线的适用场景
javascript·人工智能·后端·安全·智能手机