C++ 继承完全指南

1. 概述

继承(Inheritance)是面向对象编程的三大特性之一(封装、继承、多态)。在 C++ 中,继承允许我们创建一个新类(派生类, derived class)基于另一个已有的类(基类, base class),从而重用、扩展或修改基类的行为。

继承的主要作用:

  • 代码复用:派生类自动拥有基类的成员(数据和方法)。

  • 建立层次结构 :表达"is-a"关系,如 DerivedBase 的一种。

  • 支持多态:通过虚函数实现运行时多态。

2. 继承的基本语法

cpp

复制代码
class Base {
    // ...
};

class Derived : access-specifier Base {
    // ...
};

其中 access-specifier 可以是 publicprotectedprivate,决定了基类成员在派生类中的访问级别。

3. 继承方式与成员访问控制

继承方式直接影响基类 publicprotectedprivate 成员在派生类中的可见性。

基类成员 public 继承 protected 继承 private 继承
public public protected private
protected protected protected private
private 不可访问 不可访问 不可访问

三种继承方式的特点:

3.1 public 继承(最常用)

  • 基类的 public 成员在派生类中仍为 public。

  • 基类的 protected 成员在派生类中仍为 protected。

  • 体现 is-a 关系(派生类是基类的一种)。

3.2 protected 继承

  • 所有基类的 public 和 protected 成员在派生类中都变为 protected。

  • 通常用于实现层面的继承,而不是接口继承。

3.3 private 继承

  • 所有基类成员在派生类中都变为 private。

  • 体现 is-implemented-in-terms-of(根据基类实现)的关系,更接近于组合。

  • 在决定使用 private 继承时,应优先考虑组合(composition)。

4. 派生类的构造与析构

4.1 构造顺序

  1. 先调用基类的构造函数(按继承顺序从左到右)。

  2. 再按照类中成员声明的顺序初始化成员对象(包括成员变量和子对象)。

  3. 最后执行派生类的构造函数体。

4.2 析构顺序

  • 与构造完全相反:先执行派生类的析构函数体,然后析构成员对象,最后调用基类的析构函数。

4.3 显式调用基类构造函数

cpp

复制代码
class Base {
public:
    Base(int x) : data(x) {}
private:
    int data;
};

class Derived : public Base {
public:
    Derived(int x, int y) : Base(x), derivedData(y) {}
private:
    int derivedData;
};

注意:如果基类没有默认构造函数,派生类的每个构造函数必须在初始化列表中显式调用基类的某构造函数。

5. 成员覆盖与隐藏

5.1 函数覆盖(override)

派生类可以重新定义基类中已有的非虚 成员函数,这叫隐藏 (hiding);对于虚函数,重新定义称为覆盖(override),是实现多态的基础。

cpp

复制代码
class Base {
public:
    virtual void func() { cout << "Base::func()\n"; }
    void other() { cout << "Base::other()\n"; }
};

class Derived : public Base {
public:
    virtual void func() override { cout << "Derived::func()\n"; } // 覆盖
    void other() { cout << "Derived::other()\n"; }               // 隐藏
};

5.2 override 关键字(C++11)

  • 显式标记派生类的虚函数要覆盖基类的虚函数。

  • 如果基类没有对应的虚函数,编译会报错,避免意外创建新函数。

5.3 final 关键字(C++11)

  • final 用在类:禁止其他类继承该类。

  • final 用在虚函数:禁止派生类再覆盖该虚函数。

cpp

复制代码
class Base final { };   // 不可以被继承

class Base2 {
    virtual void func() final;   // 派生类不可覆盖 func
};

6. 多继承(Multiple Inheritance)

C++ 支持一个派生类同时继承多个基类。

cpp

复制代码
class A { };
class B { };
class C : public A, public B { };

6.1 二义性问题

如果多个基类含有同名成员,派生类访问时必须使用作用域运算符明确指定来自哪个基类。

cpp

复制代码
class A { public: void f(); };
class B { public: void f(); };
class C : public A, public B {
    void call() {
        A::f();   // 明确调用 A 的 f
        B::f();
    }
};

6.2 菱形继承问题

text

复制代码
   Base
  /    \
 A      B
  \    /
   Derived

如果 A 和 B 都继承自 Base,Derived 多重继承 A 和 B,会导致 两份 Base 的副本,访问 Base 成员时产生二义性,并且构造和析构次序也变得复杂。

解决方案:虚继承(virtual inheritance)。

7. 虚继承

虚继承确保在菱形继承中,最顶层的基类只被保留一份共享副本。

cpp

复制代码
class Base { };
class A : virtual public Base { };
class B : virtual public Base { };
class Derived : public A, public B { };

7.1 构造顺序(虚继承)

  • 虚基类的构造函数优先于非虚基类。

  • 虚基类只在最终派生类的构造函数中被初始化一次。

  • 所有虚基类按照它们出现在继承列表中的顺序初始化,再按普通继承顺序。

7.2 虚继承的代价

  • 增加了间接访问和内存开销(通常通过虚基类指针表)。

  • 构造和析构规则复杂,应谨慎使用。

8. 继承与多态

  • 多态:基类指针或引用指向派生类对象,通过虚函数调用实际类型的成员。

  • 虚函数表(vtable):每个含有虚函数的类都有一个虚函数表,通过虚指针(vptr)访问。

  • 纯虚函数与抽象类

    • virtual void func() = 0; 表示纯虚函数。

    • 含有纯虚函数的类称为抽象类,不能实例化。

    • 派生类必须实现所有纯虚函数,否则也是抽象类。

cpp

复制代码
class Shape {
public:
    virtual void draw() = 0;
};

class Circle : public Shape {
public:
    void draw() override { /* 绘制圆形 */ }
};

9. 继承与访问控制总结

成员访问性 在类内部 在派生类内部(public 继承) 在外部(通过对象)
private
protected
public
  • protected 成员的引入正是为了支持继承:允许派生类访问,但禁止外部访问。

  • 在类的设计中,通常将数据成员设为 private,而将需要被派生类定制的方法设为 protected 或 public virtual。

10. 最佳实践与建议

  1. 优先使用 public 继承,只有当确实需要实现层面的隔离时才考虑 private/protected 继承。

  2. 析构函数应为 virtual:如果通过基类指针删除派生类对象,基类的析构函数必须是虚函数,否则会产生未定义行为(只调用基类析构函数)。

  3. 避免过深的继承层次:多层继承增加理解难度和耦合度,优先考虑组合而不是继承(Composition over Inheritance)。

  4. 使用 overridefinal:明确虚函数覆盖意图,增加代码安全性。

  5. 尽量不使用多继承,除非必要,如果必须使用,注意避免菱形继承和名字冲突。

  6. 虚继承只在解决菱形继承时使用,不要滥用。

11. 一个完整示例

cpp

复制代码
#include <iostream>
using namespace std;

class Animal {
public:
    Animal(const string& n) : name(n) {}
    virtual void speak() const {
        cout << "Animal " << name << " speaks." << endl;
    }
    virtual ~Animal() = default;   // 虚析构
protected:
    string name;
};

class Dog : public Animal {
public:
    Dog(const string& n) : Animal(n) {}
    void speak() const override {
        cout << "Dog " << name << " barks." << endl;
    }
};

class Cat : public Animal {
public:
    Cat(const string& n) : Animal(n) {}
    void speak() const override {
        cout << "Cat " << name << " meows." << endl;
    }
};

int main() {
    Animal* zoo[2];
    zoo[0] = new Dog("Buddy");
    zoo[1] = new Cat("Kitty");

    for (auto a : zoo)
        a->speak();

    for (auto a : zoo)
        delete a;
    return 0;
}

输出:

text

复制代码
Dog Buddy barks.
Cat Kitty meows.

12. 总结

C++ 的继承机制功能强大而灵活,它支持单继承、多继承、虚继承,并提供了不同的访问控制级别。正确使用继承可以提高代码的复用性和可扩展性,但滥用继承会导致设计复杂、维护困难。理解继承的底层语义(尤其是对象模型、构造/析构顺序、虚函数机制)是成为 C++ 高手的重要一步。

在实际工程中,应牢记:组合优先于继承,继承用于表达稳定的"is-a"关系;当关系不明确时,组合往往带来更低的耦合。

相关推荐
tankeven1 小时前
动态规划专题(11):区间动态规划之三角剖分问题
c++·算法·动态规划
zhangrelay1 小时前
三分钟云课实践速通--C/C++程序设计--
linux·c语言·c++·笔记·学习·ubuntu
小此方2 小时前
Re:从零开始的 C++ STL篇(十二)深度解析哈希函数设计、负载因子调节与两种冲突处理策略
c++·算法·哈希算法
Karle_2 小时前
为AI编辑器准备c++编译环境,onnxruntime、cmake、cl,网上坑太多备份记录后续方便使用。
开发语言·c++·编辑器
lcj25112 小时前
【数据结构精讲】堆与二叉树从底层原理到代码落地:堆的构建 / 调整 / 排序 + 二叉树遍历 / 操作(附完整 C++ 源码 + LeetCode 题解)
数据结构·c++·leetcode
努力努力再努力wz2 小时前
【MySQL 进阶系列】C/C++ 如何通过客户端库访问 MySQL?从连接原理到 API 调用流程详解(附完整demo代码)
服务器·c语言·数据结构·数据库·c++·b树·mysql
CSCN新手听安2 小时前
【Qt】Qt窗口(七)QColorDialog颜色对话框,QFileDialog文件对话框的使用
开发语言·c++·qt
A charmer2 小时前
从 C++ 到 Objective-C:零基础平滑转学专栏【总目录】
开发语言·c++·objective-c
cookies_s_s2 小时前
C++ 内存模型与无锁编程:从底层原理到实战
linux·服务器·开发语言·c++