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