一、继承——代码复用的核心武器
说到面向对象编程,继承绝对是个绕不开的话题。我刚开始学C++那会儿,总觉得继承就是个“儿子继承老爸财产”的概念,挺简单的。但真正写起项目来才发现,这里面的门道比想象中多得多。
继承说白了,就是让一个类拥有另一个类的成员。你写了一个Animal类,里面有eat()、sleep()这些通用方法。现在要写Dog类,难道再把eat()和sleep()重写一遍?没必要。让Dog继承Animal就行了。
继承的核心价值:复用已有代码,建立类之间的层次关系,实现多态的基础。
二、继承的语法——怎么写?
语法其实很直观。看个例子你就明白了:
// 基类(父类)
class Animal {
public:
void eat() {
cout << "吃东西" << endl;
}
void sleep() {
cout << "睡觉" << endl;
}
};
// 派生类(子类)
class Dog : public Animal {
public:
void bark() {
cout << "汪汪叫" << endl;
}
};
这里Dog继承了Animal,所以Dog的对象既能调用bark(),也能调用eat()和sleep()。嗯,这就是继承最基础的样子。
继承的通用语法是:
class 派生类名 : 继承方式 基类名 {
// 派生类新增的成员
};
继承方式有三种:public、protected、private。这个我们后面细说。
三、单继承与多继承
3.1 单继承
单继承就是一个派生类只有一个直接基类。就像你只有一个亲生父亲一样。大部分情况下,单继承就够用了。
class A { /* ... */ };
class B : public A { /* ... */ }; // B单继承自A
class C : public B { /* ... */ }; // C单继承自B(形成继承链)
我在项目中遇到过,很多人一上来就想着用多继承,其实单继承配合组合模式,能解决绝大多数问题。别把简单问题复杂化。
3.2 多继承
多继承就是一个派生类有多个直接基类。C++是少数支持多继承的语言之一,Java、C#都不支持。
class Teacher {
public:
void teach() { cout << "教书" << endl; }
};
class Researcher {
public:
void research() { cout << "搞科研" << endl; }
};
// 多继承:一个教授既是老师又是研究员
class Professor : public Teacher, public Researcher {
public:
void publish() { cout << "发论文" << endl; }
};
多继承看起来很强大,但用不好就是灾难。你想想看,如果两个基类有同名成员,到底用谁的?这就引出了菱形继承问题。
四、菱形继承与虚继承
4.1 什么是菱形继承?
菱形继承长这样:
A
/ \
B C
\ /
D
B和C都继承自A,然后D同时继承B和C。这时候D的对象里会有两份A的成员。两份!
我曾经在一个图形库项目中踩过这个坑。当时有个Shape基类,Rectangle和Ellipse都继承它,然后RoundedRect又同时继承Rectangle和Ellipse。结果RoundedRect对象里有两份Shape的数据,调试的时候人都麻了。
4.2 虚继承解决菱形问题
解决方案就是虚继承。在继承时加上virtual关键字:
class A {
public:
int value;
};
class B : virtual public A { }; // 虚继承
class C : virtual public A { }; // 虚继承
class D : public B, public C { };
这样D的对象里只有一份A的成员。虚继承的原理是让编译器在对象布局中增加一个虚基类指针,指向共享的基类子对象。嗯,底层细节有点复杂,但用起来很简单——记住在可能形成菱形的继承链上加上virtual就行。
避坑指南:虚继承会增加内存开销和访问时间。不是所有多继承都需要虚继承,只有出现菱形结构时才用。我曾经见过有人把所有继承都写成虚继承,结果性能惨不忍睹。
五、继承中的构造函数与析构函数
5.1 构造顺序
继承体系下,构造函数的调用顺序是有讲究的:
- 先调用基类的构造函数(按继承顺序)
- 再调用派生类的构造函数
析构函数正好相反:先析构派生类,再析构基类。
class Base {
public:
Base() { cout << "Base构造" << endl; }
~Base() { cout << "Base析构" << endl; }
};
class Derived : public Base {
public:
Derived() { cout << "Derived构造" << endl; }
~Derived() { cout << "Derived析构" << endl; }
};
// 创建Derived对象时输出:
// Base构造
// Derived构造
// Derived析构
// Base析构
5.2 派生类如何初始化基类成员?
如果基类有带参数的构造函数,派生类必须在初始化列表中显式调用:
class Base {
public:
Base(int x) { /* ... */ }
};
class Derived : public Base {
public:
// 必须在初始化列表中调用Base的构造函数
Derived(int a, int b) : Base(a), derivedValue(b) { }
private:
int derivedValue;
};
我个人习惯在写派生类时,总是显式写出基类的构造函数调用,哪怕基类有默认构造函数。这样代码更清晰,别人一看就知道基类是怎么初始化的。
六、继承中的访问控制
访问控制是继承里最容易搞混的部分。我画个表帮你理清楚:
| 基类中的访问级别 | public继承后在派生类中 | protected继承后在派生类中 | private继承后在派生类中 |
|---|---|---|---|
| public | public | protected | private |
| protected | protected | protected | private |
| private | 不可访问 | 不可访问 | 不可访问 |
说白了,继承方式决定了基类成员在派生类中的“降级”程度:
- public继承:最常用。基类的public成员在派生类中还是public。
- protected继承:基类的public和protected都变成protected。外部不能访问了。
- private继承:基类所有成员都变成private。这其实更像是一种“实现复用”,而不是“接口继承”。
我的建议:99%的情况下用public继承就够了。protected继承偶尔用用。private继承?我工作十年就用过两次,而且都有更好的替代方案。
七、知识体系总览
下面这张图帮你把本章的核心知识点串起来:
八、写在最后
继承是C++面向对象的三大特性之一,用好了能让代码优雅又高效。但记住一句话:继承是为了复用接口,而不是复用实现。如果只是为了复用代码,组合(composition)往往是更好的选择。
嗯,这一章的内容就到这儿。代码多敲几遍,尤其是虚继承和构造顺序那部分,光看是记不住的。
公众号:蓝海资料掘金营,微信 deep3321