C++ 继承
继承(inheritance)是面向对象的核心机制之一。它让你基于已有的类创建新类,复用代码并扩展功能。更重要的是,继承配合虚函数实现了多态——同一个接口,不同的行为。这一章我们看 C++ 继承的全部要点。
一、继承的基本概念
继承就是"子类拥有父类的全部成员,再加自己的扩展":
#include <iostream>
#include <string>
using namespace std;
// 基类(父类)
class Animal {
protected: // protected:子类能访问,外部不行
string name;
public:
Animal(const string& n) : name(n) {}
void eat() {
cout << name << " is eating" << endl;
}
};
// 派生类(子类):public 继承 Animal
class Dog : public Animal {
public:
// 子类构造函数:必须调用基类构造函数
Dog(const string& n) : Animal(n) {}
void bark() {
cout << name << " says: Woof!" << endl;
}
};
int main() {
Dog d("Buddy");
d.eat(); // 继承自 Animal:Buddy is eating
d.bark(); // 自己的方法:Buddy says: Woof!
return 0;
}几个要点:
- 语法:
class 子类 : public 基类。冒号 + 继承方式。 - 子类自动获得基类所有成员(除构造函数、析构函数、operator= 不能继承)。
- 子类构造:必须调用基类构造函数,用初始化列表
: Animal(n)。 - protected 访问:子类能访问,但外部不能。继承体系的"内部接口"。
二、三种继承方式
C++ 继承有三种方式,改变基类成员在子类中的访问权限:
- public 继承(最常用):基类的 public 在子类还是 public,protected 还是 protected。表达 "is-a"(子类是基类的一种)。
- protected 继承:基类的 public/protected 在子类都变 protected。很少用。
- private 继承(默认):基类的 public/protected 在子类都变 private。表达 "implemented-in-terms-of"(用基类实现),但更推荐用组合替代。
实际工程 99% 用 public 继承。后两种很少用,知道存在即可。
三、虚函数与多态(核心!)
多态(polymorphism)是 OOP 的精髓:同一个调用,根据对象实际类型执行不同代码。C++ 用 virtual 实现运行时多态:
#include <iostream>
#include <vector>
using namespace std;
class Shape {
public:
// 没有 virtual:静态绑定(根据指针/引用的静态类型调用)
// 有 virtual:动态绑定(根据实际对象的类型调用)—— 这是多态的关键
virtual double area() {
return 0;
}
virtual ~Shape() {} // 虚析构!多态基类必须有
};
class Circle : public Shape {
double r;
public:
Circle(double r) : r(r) {}
double area() override { // override(C++11):显式标记重写
return 3.14159 * r * r;
}
};
class Rectangle : public Shape {
double w, h;
public:
Rectangle(double w, double h) : w(w), h(h) {}
double area() override {
return w * h;
}
};
int main() {
// 用基类指针指向子类对象——多态
Shape* s1 = new Circle(5);
Shape* s2 = new Rectangle(3, 4);
cout << s1->area() << endl; // 78.5398(调 Circle::area)
cout << s2->area() << endl; // 12(调 Rectangle::area)
// 用 vector 管理多个不同形状
vector<Shape*> shapes = {s1, s2};
for (Shape* s : shapes) {
cout << s->area() << endl;
}
delete s1;
delete s2;
return 0;
}关键点:
- virtual:标记为虚函数,启用动态绑定。运行时根据对象真实类型调用对应版本。
- 没有 virtual 就是静态绑定:编译期根据指针/引用的声明类型决定调用哪个版本——这通常不是你想要的多态。
- 用基类指针/引用调用虚函数,会自动调用子类的版本。
- 虚析构函数:多态基类的析构必须是 virtual。否则
delete 基类指针不会调用子类析构,造成内存泄漏。
四、override 与 final(C++11)
C++11 引入的两个关键字,让虚函数重写更安全:
#include <iostream>
using namespace std;
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
};
class Derived : public Base {
public:
// override 让编译器帮你检查"是否真的重写了基类的虚函数"
// 写错签名(参数、const、名字)会编译错误
void f() override { cout << "Derived::f" << endl; } // OK
// void g(int) override { } // 编译错误:签名不匹配(不是真正的重写)
// final(C++11):禁止子类再重写
void g() override final { cout << "Derived::g (final)" << endl; }
};
class FinalClass final : public Derived {
// 不能再重写 g(因为被标记 final)
// void g() override { } // 编译错误
};
// class SubFinal : public FinalClass {}; // 编译错误:FinalClass 是 final
int main() {
Derived d;
Base& b = d;
b.f(); // Derived::f
b.g(); // Derived::g (final)
return 0;
}- override:告诉编译器"我打算重写基类的虚函数"。如果你写错了签名(参数、const、名字),编译器会报错。强烈推荐养成习惯。
- final:禁止子类再重写。可以用于虚函数,也可以用于整个类(禁止被继承)。
- 没写 override 的坑:你以为重写了,其实签名不匹配,运行时多态失效,bug 难找。
五、抽象类与纯虚函数
抽象类(abstract class)是"不能被实例化"的类,只能作为基类被继承。包含纯虚函数的类就是抽象类:
#include <iostream>
using namespace std;
// 抽象类:包含至少一个纯虚函数
class Shape {
public:
// 纯虚函数:= 0 表示"没有实现,必须由子类提供"
virtual double area() = 0;
virtual double perimeter() = 0;
// 可以有普通成员函数(子类共享)
void describe() {
cout << "面积: " << area() << ", 周长: " << perimeter() << endl;
}
virtual ~Shape() {}
};
class Circle : public Shape {
double r;
public:
Circle(double r) : r(r) {}
double area() override { return 3.14159 * r * r; }
double perimeter() override { return 2 * 3.14159 * r; }
};
int main() {
// Shape s; // 编译错误:抽象类不能实例化
Circle c(5);
c.describe(); // 面积: 78.5398, 周长: 31.4159
return 0;
}纯虚函数语法:virtual 返回类型 函数名(参数) = 0;。它定义了"接口"——子类必须提供实现。这是 C++ 实现"接口"概念的方式(Java 有专门的 interface 关键字,C++ 用抽象类)。
- 抽象类不能实例化:
Shape s;编译错误。 - 子类必须实现所有纯虚函数才能实例化。
- 典型用途:定义接口(如
Comparable、Serializable),让具体类实现细节。
六、构造与析构的调用顺序
继承体系下,构造和析构的顺序很重要:
#include <iostream>
using namespace std;
class Base {
public:
Base() { cout << "Base 构造" << endl; }
virtual ~Base() { cout << "Base 析构" << endl; } // 虚析构
};
class Derived : public Base {
public:
Derived() { cout << "Derived 构造" << endl; }
~Derived() override { cout << "Derived 析构" << endl; }
};
int main() {
cout << "--- 局部对象 ---" << endl;
{
Derived d;
}
// 输出顺序:
// Base 构造 -> Derived 构造
// Derived 析构 -> Base 析构
cout << "--- 用基类指针 ---" << endl;
Base* p = new Derived();
delete p;
// 析构是 virtual:正确调用 Derived 析构,再调 Base 析构
// 若析构不是 virtual:只调 Base 析构!Derived 析构不调用,资源泄漏!
return 0;
}- 构造顺序:基类 → 成员对象 → 派生类(从内到外,从老到新)。
- 析构顺序:派生类 → 成员对象 → 基类(反过来,从外到内)。
- 多态基类的析构必须 virtual:否则
delete 基类指针只调用基类析构,子类资源泄漏。这是 C++ 最经典的坑之一。
七、多重继承
C++ 允许一个类继承多个基类,但容易引入复杂性:
#include <iostream>
using namespace std;
class A {
public:
void f() { cout << "A::f" << endl; }
};
class B {
public:
void f() { cout << "B::f" << endl; }
};
// 多重继承:同时继承 A 和 B
class C : public A, public B {
};
int main() {
C c;
// c.f(); // 编译错误:二义性!A 和 B 都有 f()
c.A::f(); // 显式指定:A::f
c.B::f(); // B::f
return 0;
}
// 多重继承的"菱形继承"问题:
// A
// / \
// B C
// \ /
// D
// D 里有 A 的两份拷贝,访问 A 的成员会二义
// 解决:用虚继承(virtual public A)多重继承的问题:
- 二义性:两个基类有同名成员,子类访问需显式指定
c.A::f()。 - 菱形继承:D 继承 B 和 C,B 和 C 都继承 A——D 里有 A 的两份拷贝。解决:用虚继承
virtual public A。 - 建议:能用单继承 + 组合就别用多重继承。需要"接口"时用抽象类(纯虚函数)做"接口继承"。
八、组合 vs 继承
Effective C++ 名言:"优先组合,而非继承"。组合(composition)比继承更灵活:
#include <iostream>
#include <string>
using namespace std;
// "优先组合,而非继承"——Effective C++ 名言
class Engine {
public:
void start() { cout << "引擎启动" << endl; }
};
class Wheel {
public:
void roll() { cout << "轮子转动" << endl; }
};
// Car 不是 Engine,但 Car "有一个" Engine —— 这是组合(has-a)
class Car {
Engine engine;
Wheel wheels[4];
public:
void drive() {
engine.start();
for (int i = 0; i < 4; i++) wheels[i].roll();
}
};
int main() {
Car car;
car.drive();
return 0;
}
// 继承(is-a):Dog 是 Animal
// 组合(has-a):Car 有 Engine
// 优先用组合,组合更灵活、解耦,继承会让子类依赖父类实现选择原则:
- 继承(is-a):Dog 是 Animal、Circle 是 Shape。表达"类型关系"。
- 组合(has-a):Car 有 Engine、Student 有 Address。表达"拥有关系"。
- 组合更灵活:可以在运行时换成员(如 Car 可以换 Engine),继承在编译时固定。
- 组合不会破坏封装:子类可能依赖父类实现细节(尤其 protected 成员),父类改动会破坏子类。
小结
这一章你掌握了 C++ 继承的全部要点:三种继承方式(实际只用 public)、虚函数与多态、override/final、抽象类(纯虚函数)、构造析构顺序、多重继承(慎用)、组合 vs 继承。下一篇我们看 C++ 的另一项大杀器——模板。
← 上一篇 C++ 类与对象
下一篇 C++ 模板 →