【C++】C++继承是个啥?咋用的?

79 阅读6分钟

C++继承是个啥?咋用的?

C++ 是一门面向对象的语言,提到 OOP(Object-Oriented Programming),有三个核心特性:封装、继承、多态。 其中,“继承”是连接代码复用与扩展的桥梁。今天,我们就聊聊 C++ 的继承到底是个啥,怎么用,底层机制又是怎么回事。


一、继承是干啥的?

简单说,继承让一个类可以“沿用”另一个类的成员和方法,就像现实生活中“子承父业”: 父亲有的房子、存款、技能,儿子可以直接用,不用重新造一遍。

在 C++ 中,继承用于代码复用和建立类之间的层次关系。 例如:

class Animal {
public:
    void eat() { std::cout << "吃东西" << std::endl; }
};

class Dog : public Animal {
public:
    void bark() { std::cout << "汪汪" << std::endl; }
};

int main() {
    Dog d;
    d.eat();  // 继承自 Animal
    d.bark(); // 自己的
}

这里,Dog 继承了 Animal,于是 Dog 对象不仅能 bark(),还能 eat(),因为这两个类之间形成了**“is-a”关系**:狗也是动物。


二、C++ 继承的语法长啥样?

继承的基本语法:

class 派生类 : 继承方式 基类 {
    // 派生类自己的成员
};

比如:

class B : public A { ... };

继承方式有三种

  • public:公有继承(最常用)
  • protected:保护继承(少用)
  • private:私有继承(特殊用途)

这三种继承有啥区别?

关键在于基类的成员访问权限在派生类中的变化

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

简单记:公有继承保留原有访问性,其他两种继承都“降级”

公有继承常用于表达“is-a”关系;私有继承更多是实现复用,但不暴露接口。


三、构造和析构是怎么执行的?

当一个类继承另一个类时,先构造基类,再构造派生类;析构时先派生类,再基类,就像盖房子:先打地基,再盖楼,拆的时候反过来。

class A {
public:
    A() { std::cout << "A构造" << std::endl; }
    ~A() { std::cout << "A析构" << std::endl; }
};

class B : public A {
public:
    B() { std::cout << "B构造" << std::endl; }
    ~B() { std::cout << "B析构" << std::endl; }
};

int main() {
    B b;
}
// 输出:A构造 -> B构造 -> B析构 -> A析构

注意:如果基类有带参构造,需要在派生类初始化列表显式调用。


四、继承的“隐藏”和“重写”

继承后,派生类可以:

  • 直接用基类的成员
  • 重写基类的成员(虚函数)
  • 隐藏基类的同名成员

示例:

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

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

int main() {
    Derived d;
    d.func();   // 调用 Derived 的
    // d.func(10); // 编译错误:Base 的 func 被隐藏
}

如果你想“用回”基类的方法,可以 using Base::func;


五、虚函数与多态:继承的灵魂

前面都只能说是面向对象语言的通性。

如果继承只是代码复用,还不够强大,C++ 的继承配合 虚函数(virtual function) 才能实现运行时多态

class Animal {
public:
    virtual void sound() { std::cout << "动物叫" << std::endl; }
};

class Dog : public Animal {
public:
    void sound() override { std::cout << "汪汪" << std::endl; }
};

int main() {
    Animal* p = new Dog();
    p->sound();  // 输出:汪汪(而不是“动物叫”)
    delete p;
}

这就是虚函数表(vtable)发挥作用的地方:程序运行时根据对象的真实类型调用对应的函数

你可以理解为虚函数能够让不同的派生类发挥各自不同的作用,但又都在父类的管控之下。


重点提示:

  • 虚函数表是编译器生成的一张指针表,用来实现动态绑定。
  • 基类析构函数最好是虚的,否则可能内存泄漏。

六、继承的坑点

  • 菱形继承问题:多个派生类继承同一个基类,再有一个类继承这两个派生类,会出现重复基类成员 → 需要 虚继承
  • 构造顺序要注意:先基类,再派生类。
  • 私有继承不是 is-a,而是 implemented-in-terms-of
  • 滥用继承会让设计僵化,很多时候组合比继承更好(“组合优于继承”原则)。

七、底层是怎么实现的?

说了那么多,底层都是咋实现的呢?

  • 普通继承:就是类成员的线性拼接,派生类对象里先放基类的成员,再放自己的。
  • 虚函数:编译器为每个类生成一张虚函数表(vtable),对象里存一个虚表指针(vptr)。
  • 多重继承:对象可能有多个 vptr,需要编译器做偏移调整。
  • 虚继承:引入虚基表(vbtable)来解决菱形继承问题。

八、总结

说白了,如果你只是想使用,只需要记住它的名字来理解它的作用,然后记住语法即可。

下面是摘自网络的总结:

  1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设 计出菱形继承。否则在复杂度及性能上都有问题。
  2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
    • 继承和组合 public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
    • 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。    
    • 优先使用对象组合,而不是类继承 。
    • 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称 为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的 内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很 大的影响。派生类和基类间的依赖关系很强,耦合度高。
    • 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象 来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
    • 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有 些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用 继承,可以用组合,就用组合。

但是继承作为三大特性之一,要说的东西还有好多哇~