17、继承(上):继承概念、基类与派生类、访问控制与构造析构顺序
继承,是C++面向对象编程的三大核心之一。说白了,就是让一个类“拥有”另一个类的特性,同时还能添加自己的新东西。我刚开始学的时候,觉得这玩意儿不就是代码复用吗?后来在项目中踩过坑才明白——继承远不止“复用”这么简单,它关乎设计、关乎架构、关乎你代码的生命力。
17.1 继承的基本概念
继承允许我们基于已有的类创建新类。已有的类叫基类(Base Class),新创建的类叫派生类(Derived Class)。派生类会继承基类的成员(变量和函数),同时可以添加自己的成员。
举个例子:
// 基类:动物
class Animal {
public:
void eat() {
cout << "动物在吃东西" << endl;
}
};
// 派生类:狗
class Dog : public Animal {
public:
void bark() {
cout << "狗在汪汪叫" << endl;
}
};
这里 Dog 继承了 Animal,所以 Dog 的对象既能 eat(),又能 bark()。嗯,很直观。
我个人习惯:在设计继承关系时,先问自己“是不是一个”的关系。比如“狗是一个动物”,那就用继承。如果是“狗有一个尾巴”,那就用组合。这个原则能帮你避免很多设计上的坑。
17.2 基类与派生类的定义
定义派生类的语法很简单:
class 派生类名 : 继承方式 基类名 {
// 派生类自己的成员
};
继承方式有三种:public、protected、private。这个我们后面细说。
派生类可以访问基类中 public 和 protected 的成员,但不能访问 private 的成员。这一点很重要——基类的私有成员,派生类也看不见。
我曾经在项目中犯过这个错:以为派生类能访问基类的私有成员,结果编译报错,查了半天才发现。记住:私有就是私有,亲儿子也不给看。
17.3 访问控制:三种继承方式
继承方式决定了基类成员在派生类中的访问级别。我画了一张图,帮你理清这个关系:
你看,这张图把三种继承方式的影响说得很清楚。我简单总结一下:
- public继承:基类的public还是public,protected还是protected。这是最常用的方式,表达“是一个”的关系。
- protected继承:基类的public变成protected。也就是说,派生类的使用者(外部代码)看不到这些成员了,但派生类的子类还能看到。
- private继承:基类的所有成员都变成private。这相当于“实现继承”,更多是复用实现,而不是表达接口关系。
实际项目中,我几乎只用public继承。protected继承偶尔用在框架设计中,private继承嘛……说实话,我工作五年只用过一次。你想想看,大部分场景下,public继承就够了。
17.4 继承中的构造函数与析构函数
这是继承里最容易出问题的地方之一。我直接说结论:
- 构造顺序:先构造基类,再构造派生类
- 析构顺序:先析构派生类,再析构基类
为什么会这样?你想想看:派生类是在基类的基础上“加东西”,所以构造时当然要先打好地基(基类),再往上盖楼(派生类)。析构时反过来,先拆楼,再拆地基。
看个例子:
class Base {
public:
Base() { cout << "Base构造" << endl; }
~Base() { cout << "Base析构" << endl; }
};
class Derived : public Base {
public:
Derived() { cout << "Derived构造" << endl; }
~Derived() { cout << "Derived析构" << endl; }
};
int main() {
Derived d;
return 0;
}
// 输出:
// Base构造
// Derived构造
// Derived析构
// Base析构
输出顺序完全符合我们的预期。嗯,这里要注意:如果基类没有默认构造函数,派生类必须在初始化列表中显式调用基类的构造函数。
class Base {
public:
Base(int x) { cout << "Base with " << x << endl; }
};
class Derived : public Base {
public:
// 必须显式调用基类构造函数
Derived() : Base(42) {
cout << "Derived构造" << endl;
}
};
我曾经在重构一个老项目时,忘了给派生类加初始化列表,结果编译报错。当时还纳闷:明明基类有构造函数啊?后来才反应过来——基类没有默认构造函数,必须显式调用。这个坑,你记住就好。
17.5 实际项目中的继承设计建议
说了这么多,我分享几个实际经验:
- 优先用组合,而不是继承。很多人一上来就想着继承,其实很多场景用组合更灵活。继承是强耦合,组合是松耦合。
- public继承才叫“继承”。protected和private继承更多是“实现复用”,不要滥用。
- 基类析构函数要声明为virtual。这个我们下一章会详细讲,但你现在就要记住:如果基类析构函数不是virtual,通过基类指针删除派生类对象时,行为是未定义的。
- 继承层次不要太深。我见过一个项目,继承链有七八层,改一个基类,所有派生类都要跟着改。那叫一个酸爽。一般来说,三层以内比较合理。
我个人习惯:写继承之前,先画个类图。哪怕只是草稿,也能帮你理清关系。别急着写代码,设计清楚了再动手,事半功倍。
好了,继承的上半部分就到这里。记住:继承是工具,不是目的。用对了,代码优雅;用错了,代码灾难。下一章我们继续聊继承的进阶话题——虚函数、多态、以及那个让人又爱又恨的virtual析构函数。
公众号:蓝海资料掘金营,微信deep3321