跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

C++ 继承机制详解:派生类函数、虚继承与菱形继承案例

C++ 继承是面向对象代码复用的核心机制,允许在保持原有类特性的基础上扩展新成员。涵盖继承的三种访问方式(public/protected/private)对成员可见性的影响,以及基类与派生类间的对象切片现象。重点解析了派生类默认成员函数的调用顺序、作用域隐藏规则及 final 关键字用法。针对多继承中的菱形继承问题,阐述了数据冗余与二义性成因,并给出虚继承解决方案。最后对比了继承与组合的设计权衡,强调高内聚低耦合原则。

1qazxsw2发布于 2026/3/24更新于 2026/10/864 浏览
C++ 继承机制详解:派生类函数、虚继承与菱形继承案例

1. 继承的概念和定义

概念

继承(Inheritance)是面向对象程序设计中代码复用的重要手段。它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),从而产生新的类,称为派生类。继承体现了由简单到复杂的认知过程,实现了类设计层次的复用。

假设我们有一个学生和一个教师类,它们都有公共的成员如名字、电话、地址等。如果不在基类中封装这些公共属性,就会导致代码冗余。通过继承,我们可以将公共成员封装到一个 Person 类中,Student 和 Teacher 继承 Person,即可复用这些成员。

class Person {
public:
    // 身份识别,比如进入图书馆打卡
    void Identify() { 
        cout << "void Identify()" << _name << endl; 
    }
protected:
    string _name;   // 名字
    string _tel;    // 电话
    string _address;// 地址
    int _age;       // 年龄
};

class Student : public Person {
public:
    void study() { /* ... */ }
protected:
    int _stuid;     // 学号 <- 学生特有
};

class Teacher : public Person {
public:
    void teaching() { /* ... */ }
protected:
    string _title;  // 职称 <- 教师特有
};

定义

在 C++ 中,Person 被称为基类(父类),Student 和 Teacher 被称为派生类(子类)。

C++ 支持三种继承方式:public、protected、private。不同的继承方式会影响基类成员在派生类中的访问权限。

  • public 继承:基类的 public/protected 成员在派生类中保持原有的访问权限。
  • protected 继承:基类的 public/protected 成员在派生类中变为 protected。
  • private 继承:基类的所有成员在派生类中均变为 private。

注意:基类的 private 成员无论以什么方式继承都是不可见的(虽然物理上存在于对象内存中,但语法上无法访问)。使用关键字 class 时默认继承方式是 private,使用 struct 时默认为 public,建议显式写出继承方式。实际开发中一般使用 public 继承。

继承类模板示例

当基类是模板类时,子类访问基类成员需要指定访问限定符,因为模板类是按需实例化。

template<class T>
class Stack : public vector<T> {
public:
    void push(const T& x) {
        // 必须指定作用域,否则找不到 push_back
        vector<T>::push_back(x);
    }
    void pop() {
        vector<T>::pop_back();
    }
};

2. 基类和派生类间的转换

public 继承的派生类对象可以赋值给基类的指针或引用。这通常被称为'切片'(Slicing)现象。寓意是把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。

基类对象不能直接赋值给派生类对象。基类的指针或引用可以通过强制类型转换赋值给派生类的指针或引用,但必须是基类指针确实指向派生类对象时才安全。若基类是多态类型,可使用 RTTI(Run-Time Type Information)的 dynamic_cast 进行安全转换。

class Person {
protected:
    string _name;
    string _sex;
    int _age;
};

class Student : public Person {
public:
    int _No; // 学号
};

int main() {
    Student s;
    // 子类切片给父类的指针或者引用
    Person* ptr = &s;
    Person& a = s;
    // 派生类对象可以赋值给基类的对象是通过调用基类的拷贝构造完成的
    Person per = s;
    // 父类对象不可以赋值给子类对象,编译会报错
    // s = per; 
    return 0;
}

3. 继承中的作用域

隐藏规则

在继承体系中,基类和派生类都有独立的作用域。派生类和基类中有同名成员时,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。

在派生类成员函数中,可以使用 基类::基类成员 显式访问被隐藏的基类成员。需要注意的是,如果是成员函数的隐藏,只需要函数名相同就构成隐藏。在实际开发中,继承体系里最好不要定义同名的成员,以免混淆。

class Person {
protected:
    string _name = "小李子";
    int _num = 111; // 身份证号
};

class Student : public Person {
public:
    void Print() {
        cout << "姓名:" << _name << endl;
        // 显式访问基类成员
        cout << "身份证号:" << Person::_num << endl;
        cout << "学号:" << _num << endl;
    }
protected:
    int _num = 999; // 学号
};

int main() {
    Student s1;
    s1.Print();
    return 0;
}

4. 派生类的默认成员函数

派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认构造函数,则必须在派生类构造函数的初始化列表中显式调用。

  • 拷贝构造函数:派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  • operator=:派生类的 operator= 必须要调用基类的 operator= 完成基类的复制。需要注意派生类的 operator= 隐藏了基类的 operator=,所以显式调用基类的 operator= 需要指定基类作用域。
  • 析构函数:派生类的析构函数在被调用完成后会自动调用基类的析构函数清理基类成员。顺序是先清理派生类成员,再清理基类成员(满足先定义的类后析构的设计要求)。
class Person {
public:
    Person(const char* name = "peter") : _name(name) {
        cout << "Person()" << endl;
    }
    Person(const Person& p) : _name(p._name) {
        cout << "Person(const Person& p)" << endl;
    }
    Person& operator=(const Person& p) {
        cout << "Person operator=(const Person& p)" << endl;
        if (this != &p) _name = p._name;
        return *this;
    }
    ~Person() {
        cout << "~Person()" << endl;
    }
protected:
    string _name;
};

class Student : public Person {
public:
    Student(const char* name, int num) : Person(name), _num(num) {
        cout << "Student()" << endl;
    }
    Student(const Student& s) : Person(s), _num(s._num) {
        cout << "Student(const Student& s)" << endl;
    }
    Student& operator=(const Student& s) {
        cout << "Student& operator=(const Student& s)" << endl;
        if (this != &s) {
            // 构成隐藏,所以需要显式调用
            Person::operator=(s);
            _num = s._num;
        }
        return *this;
    }
    ~Student() {
        cout << "~Student()" << endl;
    }
protected:
    int _num;
};

int main() {
    Student s1("jack", 18);
    Student s2(s1);
    Student s3("rose", 17);
    s1 = s3;
    return 0;
}

实现一个不能被继承的类

方法一:将基类的构造函数私有化,派生类无法访问基类构造函数,从而无法实例化。 方法二:C++11 新增了 final 关键字,修饰基类后,派生类就不能继承了。

class Base final {
public:
    void func5() { cout << "Base::func5" << endl; }
protected:
    int a = 1;
};
// class Derive : public Base { ... }; // 编译错误

5. 继承与友元

友元关系不能继承。也就是说,基类友元不能访问派生类私有和保护成员,但该友元依旧可以访问基类的私有和保护成员。

class Person {
public:
    friend void Display(const Person& p, const Student& s);
protected:
    string _name;
};

class Student : public Person {
protected:
    int _stuNum;
};

void Display(const Person& p, const Student& s) {
    cout << p._name << endl;
    // cout << s._stuNum << endl; // 编译报错,Display 不是 Student 的友元
}

6. 继承与静态成员

基类定义了 static 静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个派生类,都只有一个 static 成员实例。即基类实例化的对象和派生类实例化的对象调用 static 成员时,调用的是同一个。

7. 多继承及其菱形继承问题

继承模型

  • 单继承:一个派生类只有一个直接基类。
  • 多继承:一个派生类有两个或以上直接基类。多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员放在最后面。
  • 菱形继承:菱形继承是多继承的一种特殊情况。从对象成员模型可以看出,菱形继承有数据冗余和二义性的问题。

例如在 Assistant 的对象中,Person 成员会有两份。Assistant 实例化的对象访问 Person 类中的成员时,不知道访问的是由 Student 类继承下来的 Person 还是 Teacher 继承下来的 Person,这种歧义导致编译器无法区分。

class Person {
public:
    string _name;
};

class Student : public Person {
protected:
    int _num;
};

class Teacher : public Person {
protected:
    int _id;
};

class Assistant : public Student, public Teacher {
protected:
    string _majorCourse;
};

int main() {
    Assistant a;
    // 编译报错:对"_name"的访问不明确
    // a._name = "peter";
    // 需要显式指定访问哪个基类的成员可以解决二义性问题,但是数据冗余问题无法解决
    a.Student::_name = "xxx";
    a.Teacher::_name = "yyy";
    return 0;
}

虚继承

使用虚继承可以解决菱形继承的数据冗余和访问时的二义性。

class Person {
public:
    string _name;
};

class Student : virtual public Person {
protected:
    int _num;
};

class Teacher : virtual public Person {
protected:
    int _id;
};

class Assistant : public Student, public Teacher {
protected:
    string _majorCourse;
};

int main() {
    Assistant a;
    // 使用虚继承可以解决菱形继承导致的数据冗余和二义性
    a._name = "peter";
    return 0;
}

在实际开发中,菱形继承足够复杂时会出现各种问题。实践中不建议设计出菱形继承这样的模型,或者说就不要设计菱形继承。但在 C++ IO 库中可以看到虚继承的应用,例如 basic_ostream 和 basic_istream 都虚继承自 basic_ios。

8. 继承和组合

  • public 继承是一种 is-a 的关系。每个派生类对象都是一个基类对象。继承允许你根据基类的实现来定义派生类的实现,通常被称为白箱复用。继承一定程度破坏了基类的封装,基类的改变对派生类有很大影响,耦合度高。
  • 组合是一种 has-a 的关系。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口,这种复用风格被称为黑箱复用。组合类之间没有很强的依赖关系,耦合度低。

程序设计要求高内聚、低耦合。因此优先使用组合,而不是继承。实际尽量多用组合,组合的耦合度低,代码维护性好。不过也不太绝对,类之间的关系适合继承(is-a)那就用继承,另外要实现多态也必须要继承。类之间的关系既适合用继承也适合组合,就用组合。


本文梳理了 C++ 继承的核心知识点,包括访问权限控制、对象切片、默认成员函数调用顺序以及多继承带来的菱形问题解决方案。理解这些机制有助于编写更安全、可维护的 C++ 代码。

目录

  1. 1. 继承的概念和定义
  2. 概念
  3. 定义
  4. 继承类模板示例
  5. 2. 基类和派生类间的转换
  6. 3. 继承中的作用域
  7. 隐藏规则
  8. 4. 派生类的默认成员函数
  9. 实现一个不能被继承的类
  10. 5. 继承与友元
  11. 6. 继承与静态成员
  12. 7. 多继承及其菱形继承问题
  13. 继承模型
  14. 虚继承
  15. 8. 继承和组合

更多推荐文章

查看全部
  • C++ 类和对象:拷贝构造与赋值运算符重载详解
  • ChatGPT 核心功能与高级应用实战指南
  • TD3 算法详解:双延迟深度确定性策略梯度
  • Git LFS 安装教程:Linux、macOS 与 Windows 全平台指南
  • 《Agent Runtime 工程化》Agent Framework 会被模型原生 Tool Use 淘汰吗?
  • Vue3 设置 style 属性提示 type check failed 类型检查失败问题及解决
  • C++ 模板与内存管理详解
  • Python 基础语法详解:变量、类型、运算符与交互
  • Linux 基础开发工具:权限管理与软件安装
  • 提示词工程:大语言模型指令设计与优化实践
  • Java Spring 核心:Bean 作用域、生命周期与自动配置
  • Java JDK 21 安装与环境配置教程(Windows + macOS)
  • 点云预测作为 4D 占用预测代理(一)
  • 基于知识图谱的 RAG 应用指南:从零到完整实践
  • 链表经典算法实战:相交节点查找与回文结构判断
  • GoldenDB 分布式数据库架构与 MySQL 迁移实践
  • 华为 OD 机试:最佳信号覆盖问题解析与多语言实现
  • 数据结构详解:KMP 算法、Trie 树与并查集
  • 实测 ToClaw 信息检索与分析能力:AI 实现先找再写
  • HarmonyOS 视频关键帧提取与智能体图像分析实战

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online