在C++里,继承允许我们基于已有类创建新类,新类自动拥有基类的成员并可以扩展或改写行为。这种机制是代码复用和多态实现的基石,理解它的语法细节直接影响程序设计质量。

一、继承的基本语法
C++继承的写法是让派生类在声明时通过冒号指定继承方式和一个或多个基类。最常见的形式是class Derived : public Base,其中public表示公有继承。除了public,还有protected和private继承,它们决定了基类成员在派生类中的访问权限。
下面的代码展示了一个最简单的继承结构:基类Animal提供通用方法,派生类Dog公有继承后可以直接使用这些方法,并添加自己的特性。注意派生类构造函数初始化列表中要先构造基类部分。
#include <iostream>
using namespace std;
class Animal {
public:
Animal(const string& name) : name(name) {}
void eat() {
cout << name << " is eating" << endl;
}
protected:
string name;
};
class Dog : public Animal {
public:
Dog(const string& name) : Animal(name) {}
void bark() {
cout << name << " says woof" << endl;
}
};
int main() {
Dog d("Tom");
d.eat();
d.bark();
return 0;
}
上述例子中,Dog通过public继承获得了Animal的eat函数和受保护的name成员。由于name是protected,派生类内部可以访问,但外部仍不可直接读取,这体现了继承中的封装控制。
二、不同继承方式对可见性的影响
继承方式会改变基类成员在派生类中的最低访问级别。public继承时,基类public成员在派生类仍为public,protected仍为protected;protected继承会把基类public降为protected;private继承则全部降为private。很多初学者误以为private继承就是组合,其实它仍属于is-implemented-in-terms-of关系。
我们用一张表来对比不同继承方式下,基类原有成员在派生类内的访问情况:
| 基类成员 | public继承 | protected继承 | private继承 |
|---|---|---|---|
| public | public | protected | private |
| protected | protected | protected | private |
| private | 不可访问 | 不可访问 | 不可访问 |
可以看到,无论哪种继承,基类的private成员对派生类都不可见,只能通过基类提供的public或protected接口间接操作。如果希望派生类能访问但又不想暴露给外部,应将其声明为protected。
三、构造函数、析构函数与继承链
创建派生类对象时,构造顺序从最顶层基类开始,依次向下直到最底层派生类;析构则完全相反。若基类没有默认构造函数,派生类必须在初始化列表中显式调用合适的基类构造函数,否则编译失败。
当通过基类指针删除派生类对象时,如果基类析构函数不是虚函数,则只会调用基类析构,导致派生类资源泄漏。因此,任何可能被多态使用的基类,其析构函数都应声明为virtual。
#include <iostream>
using namespace std;
class Base {
public:
Base() { cout << "Base ctor" << endl; }
virtual ~Base() { cout << "Base dtor" << endl; }
};
class Derived : public Base {
public:
Derived() { cout << "Derived ctor" << endl; }
~Derived() { cout << "Derived dtor" << endl; }
};
int main() {
Base* p = new Derived();
delete p;
return 0;
}
运行上面程序会依次输出Base ctor、Derived ctor、Derived dtor、Base dtor,证明虚析构函数保证了完整的清理链路。如果去掉virtual,则Derived的析构不会被调用。
四、用虚函数实现运行时多态
继承的另一个核心价值是多态:同一接口根据对象实际类型执行不同逻辑。在基类中用virtual声明函数,派生类用override关键字重写,编译器会通过虚函数表(vtable)在运行时分发调用。
以下示例里,Shape定义纯虚函数area,强迫派生类提供实现;Circle和Rectangle各自给出面积公式。通过基类指针遍历,就能统一处理不同图形。
#include <iostream>
using namespace std;
class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
double r;
public:
Circle(double r) : r(r) {}
double area() const override {
return 3.14159 * r * r;
}
};
class Rectangle : public Shape {
double w, h;
public:
Rectangle(double w, double h) : w(w), h(h) {}
double area() const override {
return w * h;
}
};
int main() {
Shape* s1 = new Circle(2.0);
Shape* s2 = new Rectangle(3.0, 4.0);
cout << "Circle area: " << s1->area() << endl;
cout << "Rectangle area: " << s2->area() << endl;
delete s1;
delete s2;
return 0;
}
纯虚函数让Shape成为抽象类,不能实例化,只作为接口契约。派生类必须实现所有纯虚函数,否则自身也是抽象类。使用override能帮助编译器检查签名是否匹配,避免无意中隐藏基类函数。
五、多层继承与注意事项
C++支持多层继承,即派生类再作为其他类的基类。链条过长会让代码耦合变重,也容易出现菱形继承导致的二义性。若确实需要共享同一个基类实例,应使用虚继承(virtual inheritance),但这会引入额外复杂度,日常业务中更推荐通过组合或接口抽象来替代深继承树。
写继承时还有几个易错点:不要在构造或析构函数中调用虚函数,因为此时对象类型并不完整,不会触发多态;避免暴露保护成员给过宽的层级;用final关键字可禁止类或函数被进一步派生或重写,利于性能与安全性。
class Base {
public:
virtual void foo() {}
};
class Middle : public Base {
public:
void foo() override {}
};
class Leaf final : public Middle {
public:
void foo() override {}
};
// 下面这行若取消注释会编译报错,因为Leaf是final
// class Another : public Leaf {};
通过合理选择继承方式、正确编写构造析构、利用虚函数实现多态,并控制继承深度,你就能在C++中写出清晰稳定的继承代码,既复用逻辑又保留扩展空间。