继承是C++面向对象编程的核心机制之一,它允许我们在已有类的基础上创建新类,新类自动拥有基类的成员变量和成员函数,并可以追加或改写自己的行为。这种关系通常称为基类(父类)与派生类(子类)。通过继承,既能减少重复代码,也能为多态提供语法基础。

一、继承的基本语法与三种方式
C++中使用冒号语法声明继承,格式为 class Derived : access_specifier Base。访问说明符可以是 public、protected 或 private,它决定了基类成员在派生类中的可见性。绝大多数业务场景使用 public 继承,表示“是一个”的语义关系。
下面的示例展示了一个最简单的 public 继承结构。基类 Animal 提供通用接口,Dog 类继承后增加了独有方法。
#include <iostream>
using namespace std;
class Animal {
public:
void eat() {
cout << "animal eat" << endl;
}
};
class Dog : public Animal {
public:
void bark() {
cout << "dog bark" << endl;
}
};
int main() {
Dog d;
d.eat(); // 来自基类
d.bark(); // 派生类自身
return 0;
}
当使用 protected 继承时,基类的 public 成员在派生类中变为 protected;private 继承则全部变为 private。这意味着外部代码无法通过派生类对象调用基类原有 public 接口,常用于实现层面的复用而非接口暴露。
不同继承方式直接影响封装边界。如果仅想复用基类代码而不希望用户把派生类当作基类使用,private 继承更合适;若需要构建可替换的类型层次,则必须选择 public 继承以满足里氏替换原则。
二、访问控制与成员可见性
基类中的 private 成员无论何种继承方式,派生类都不可直接访问,只能通过基类提供的 protected 或 public 接口操作。将需要给子类用的内部状态声明为 protected,是平衡封装与扩展的常见做法。
考虑以下例子,子类通过 protected 成员完成计数功能,而外部仍无法随意修改该字段。
#include <iostream>
using namespace std;
class Base {
protected:
int count;
public:
Base() : count(0) {}
void show() { cout << count << endl; }
};
class Sub : public Base {
public:
void inc() { count++; }
};
int main() {
Sub s;
s.inc();
s.show(); // 输出 1
// s.count = 5; // 编译错误,外部不可访问 protected
return 0;
}
如果错误地将本应保护的字段设为 public,会导致任何调用方都能破坏对象内部逻辑;反之全部 private 又迫使子类写大量转发函数。合理划分访问级别是继承设计的第一步。
在大型项目中,建议基类只暴露必要的 public 接口,把可扩展钩子以 protected 虚函数形式留给子类,这样既约束了行为又保留了定制空间。
三、函数重写与虚函数多态
当派生类定义与基类同名、同参数、同返回类型的函数,且基类函数声明为 virtual 时,就发生了重写(override)。通过基类指针或引用调用该函数,实际执行的是对象真实类型对应的版本,这就是运行时多态。
虚函数的实现依赖于虚函数表(vtable)。每个含虚函数的类有一张表,对象内部藏着一个指向表的指针;调用时通过查表找到目标函数地址。下面代码演示多态调用:
#include <iostream>
using namespace std;
class Shape {
public:
virtual void draw() { cout << "shape" << endl; }
virtual ~Shape() {}
};
class Circle : public Shape {
public:
void draw() override { cout << "circle" << endl; }
};
int main() {
Shape* s = new Circle();
s->draw(); // 输出 circle
delete s; // 虚析构保证调用 Circle 析构
return 0;
}
注意基类析构函数也应声明为 virtual。否则用基类指针 delete 派生对象时,只调用基类析构,派生部分资源泄漏。C++11 提供的 override 关键字能让编译器检查是否真的重写了虚函数,减少拼写错误。
除了虚函数,也可以使用纯虚函数定义接口类,例如 virtual void run() = 0;,此时基类成为抽象类,不能实例化,强迫派生类给出实现。这是构建插件式架构的常用手段。
四、多重继承与歧义处理
C++允许一个类同时继承多个基类,这带来灵活性的同时也引入了命名冲突和菱形继承问题。若两个基类有同名函数,派生类调用时必须加作用域限定,否则编译报错。
菱形继承指派生类经由不同路径继承同一个祖类,导致祖类成员在对象中存在多份。使用 virtual 继承可让共享基类只有一份实例。
#include <iostream>
using namespace std;
class A { public: int val; };
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};
int main() {
D d;
d.val = 10; // 只有一份 A::val
cout << d.val << endl;
return 0;
}
虚拟继承会在对象布局中插入间接指针,构造顺序也变为先构造共享虚基类。虽然解决了数据重复,但增加了模型复杂度,日常开发中应尽量用组合替代多重继承,仅在确实需要多接口聚合时谨慎使用。
在跨平台库设计中,常把接口拆分为多个纯虚类,具体类以 public 方式多重继承这些接口,再配合工厂函数返回基类指针,既清晰又避免深层的菱形结构。
五、构造、析构顺序与对象切片
创建派生类对象时,先调用基类构造函数,再执行派生类构造体;析构顺序相反。若基类构造需要参数,应在派生类初始化列表中显式传递。
另一个容易忽视的问题是对象切片:把派生类对象赋值给基类变量(非指针或引用)时,多余部分被截断,仅保留基类子对象。以下示例说明该现象:
#include <iostream>
using namespace std;
class Base { public: int a = 1; };
class Der : public Base { public: int b = 2; };
void print(Base b) { cout << b.a << endl; }
int main() {
Der d;
print(d); // 仅拷贝 Base 部分,b 成员丢失
return 0;
}
为避免切片并保持多态,函数参数应写为基类引用或指针。同时,在返回对象时优先使用智能指针如 shared_ptr<Base>,既管理生命周期又保留动态类型信息。
理解构造析构顺序还有助于排查资源初始化竞争。例如派生类依赖基类打开的文件句柄,就必须在基类构造完成后再于派生构造中操作,不能提前使用未初始化的成员。
六、继承与组合的选择
继承表达“是一个”关系,组合表达“有一个”关系。当仅为了复用代码而没有语义上的类别归属时,优先使用成员变量组合,可降低耦合,避免继承层次膨胀。
举例来说,若实现一个带有日志功能的网络客户端,把 Logger 作为成员比让客户端继承 Logger 更合理,因为客户端并不是一种日志器。组合也让替换实现更容易,不影响外部类型体系。
总的来看,C++实现继承不仅是写一行冒号语法,还涉及访问控制、虚表机制、构造语义和设计权衡。掌握这些细节,才能写出既安全又易维护的面向对象代码。