前言:
继承让我们可以在已有类的基础上,快速构建出功能更加丰富的类,实现代码复用,同时表达“是一种”这样的层次关系。可以说,不会用继承,就无法真正驾驭 C++ 面向对象编程。
1. 为什么需要继承?
假设正在开发一个学校管理系统,需要表示“学生”和“教师”。两个类都有姓名、年龄、ID 等属性,都有“显示信息”的行为。如果各自写一遍,代码高度重复,而且以后要统一添加“联系方式”字段时,就必须修改多个类。
继承的思路是:抽象出共性,放在一个基类中;特有部分,交给派生类自己处理。例如“人”就是基类,学生和教师都是“人”的派生类。这样,基类中的代码只需要写一次。
2. 基本语法
// 基类
class Person {
public:
std::string name;
int age;
void showInfo() {
std::cout << "姓名:" << name << ",年龄:" << age;
}
};
// 派生类,继承自 Person
class Student : public Person { // public 继承
public:
std::string studentID; // 派生类特有的成员
void study() {
std::cout << name << " 在学习。\n";
}
};
这里 class Student : public Person 的意思是:Student 以 public 方式 继承 Person。继承方式决定了基类成员在派生类中的访问权限。
继承方式如下:
| 基类成员权限 | public 继承 | protected 继承 | private 继承 |
| public | 仍为 public | 变为 protected | 变为 private |
| protected | 仍为 protected | 变为 protected | 变为 private |
| private | 不可直接访问 | 不可直接访问 | 不可直接访问 |
实战中 99% 的情况都使用 public 继承,表达“Student 是一种 Person”。
3. 访问权限回顾与 protected
private:只有本类的成员函数和友元可以访问。
protected:本类、派生类的成员函数和友元可以访问,但类的使用者不能访问。
public:所有人都能访问。
正因为基类的 private 成员在派生类中“不可直接访问”,所以我们通常会把派生类可能需要用到的内部成员设为 protected,而不是直接设为 public(破坏封装)。
class Person {
protected:
std::string id; // 派生类可以访问,外部不能直接访问
public:
Person(const std::string& id) : id(id) {}
};
class Student : public Person {
public:
Student(const std::string& id, const std::string& sid)
: Person(id), studentID(sid) {}
void showID() {
std::cout << "身份证:" << id << ",学号:" << studentID << "\n";
}
private:
std::string studentID;
};
注意:派生类的构造函数必须在初始化列表中显式调用基类的构造函数,否则会使用基类的默认构造函数。
4. 构造与析构顺序
当创建派生类对象时,构造函数的调用顺序是:
1.基类构造函数(先构造基类部分)
2.派生类自己的成员对象(按声明顺序)
3.派生类构造函数体
析构时顺序完全相反:先析构派生类,再析构基类部分。这是递归的,如果有多层继承,就沿着继承链逐层向上构造、向下析构。
class A {
public:
A() { std::cout << "A 构造\n"; }
~A() { std::cout << "A 析构\n"; }
};
class B : public A {
public:
B() { std::cout << "B 构造\n"; }
~B() { std::cout << "B 析构\n"; }
};
int main() {
B obj;
return 0;
}
// 输出:A 构造 B 构造 B 析构 A 析构
5. 名字隐藏与重定义
如果派生类定义了一个和基类同名的函数(或同名变量),会隐藏基类中所有同名函数(包括同名的重载版本)。
class Base {
public:
void func() { std::cout << "Base::func()\n"; }
void func(int x) { std::cout << "Base::func(int)\n"; }
};
class Derived : public Base {
public:
void func() { std::cout << "Derived::func()\n"; } // 隐藏了基类的所有 func
};
Derived d;
d.func(); // OK,调用派生类版本
// d.func(10); // 编译错误!Base::func(int) 被隐藏了
如果想在派生类中同时保留基类的重载版本,需要用 using Base::func; 引入名字。
class Derived : public Base {
public:
using Base::func; // 把基类的 func 重载纳入作用域
void func() { std::cout << "Derived::func()\n"; }
};
这个点很容易踩坑。
6. 多态与虚函数
6.1 静态绑定与动态绑定
看一个场景:
class Animal {
public:
void speak() { std::cout << "动物叫\n"; }
};
class Dog : public Animal {
public:
void speak() { std::cout << "汪汪\n"; }
};
int main() {
Animal* a = new Dog();
a->speak(); // 输出??
delete a;
}
输出结果是“动物叫”,而不是我们期望的“汪汪”。因为编译器在编译时根据指针的静态类型(Animal*)确定了调用哪个函数,这就是静态绑定,没有实现多态。
要使它能根据对象的实际类型调用正确的函数,必须使用虚函数:
class Animal {
public:
virtual void speak() { std::cout << "动物叫\n"; }
};
class Dog : public Animal {
public:
void speak() override { std::cout << "汪汪\n"; }
};
这样再执行 a->speak(),就会输出“汪汪”。virtual 关键字告诉编译器:这个函数是动态绑定的,运行时根据对象的真实类型决定调用哪个版本。
6.2 override 和 final
override:显式声明这个函数意图重写基类的虚函数。如果函数签名不匹配,编译器会报错(非常推荐加上,避免粗心失误)。
final:可以修饰虚函数或类,表示禁止被进一步重写,或禁止被继承。
class Cat : public Animal {
public:
void speak() override final { std::cout << "喵喵\n"; }
// 该类不能再被重写 speak
};
class Kitten : public Cat {
public:
// void speak() override; // 编译错误!Cat::speak 标记为 final
};
7. 纯虚函数与抽象类
如果一个基类中的虚函数无法给出有意义的实现,我们可以把它声明为纯虚函数,类就变成抽象类。抽象类不能直接创建对象,只能作为基类派生出具体类。
class Shape {
public:
virtual double area() const = 0; // 纯虚函数
virtual ~Shape() {} // 虚析构函数(重要!)
};
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
};
为什么要将基类的析构函数设为虚函数?
当通过基类指针删除派生类对象时,如果析构函数不是虚的,则只会调用基类的析构函数,派生类部分可能无法正确释放资源。所以,只要一个类可能被继承,就应该将它的析构函数声明为虚函数(即使为空)。
8. 继承与组合:is-a 与 has-a
面向对象设计中有一个重要的准则:多用组合,少用继承。
继承表达的是“是一种”关系:Dog 是一种 Animal,所以用继承。
组合表达的是“有一个”关系:Car 有一个 Engine,所以把 Engine 作为 Car 的成员对象。
如果不加区分地滥用继承,会导致耦合过强,代码难以维护。比如,想让 Bird 具有飞翔的能力,如果直接用继承,可能会写出 FlyingBird 类,但随着需求变化(鸡也是鸟但不能飞,飞机不是鸟但能飞),继承层次会变得混乱。此时更好的方式是将“飞翔”抽象成一个接口(纯抽象类),如 IFlyable,然后让需要飞翔的类继承这个接口。这实际上是一种组合设计——实现多个接口。
9. 多重继承
C++ 支持从多个基类同时继承(多重继承),语法如下:
class A { ... };
class B { ... };
class C : public A, public B { ... };
多重继承容易引发菱形继承问题(两个基类有共同的祖父类,派生类会包含两份祖父部分),C++ 提供了虚继承来解决,但整体较复杂。多数项目会通过接口(纯抽象类)来规避多重继承的复杂性。
10. 常见误区与注意事项
忘记虚析构函数
凡是作为基类的类,析构函数应该写成 virtual ~Base() {} 或 virtual ~Base() = default;,否则通过基类指针删除派生对象会导致资源泄漏。
隐藏而非重写
派生类定义了与基类同名的非虚函数,不会产生多态,只是隐藏。一定要区分“重定义(隐藏)”和“重写(覆盖)”。
重写时缺少 override
不写 override,函数签名稍有不同就会变成隐藏而非重写,且编译器不提示错误,排查困难。养成重写时加 override 的好习惯。
protected 的使用
不要为了派生类方便把不该暴露的成员设为 public,可以设为 protected 对外隐藏,但也要考虑 protected 破坏封装的程度,尽量保持数据成员的 private,通过 protected 成员函数提供受控访问。
派生类对象切片
如果把派生类对象赋值给基类对象(不是指针或引用),会发生对象切片,只保留基类部分,派生类特有的部分被切掉,且多态失效。
11. 小结
1.继承是实现代码复用和多态的基础,表达“is-a”关系。
2.派生类以 public 继承基类,可以访问 public 和 protected 成员,不能直接访问 private 成员。
3.构造顺序:基类 → 派生类成员 → 派生类构造体;析构顺序相反。
4.通过 virtual 虚函数实现多态,用基类指针/引用调用派生类重写的函数。
5.重写虚函数请加 override,基类析构函数务必为虚函数。
6.纯虚函数让类变成抽象类,抽象类不能实例化。
7.优先使用组合,继承只在确实满足“is-a”关系时使用。
741

被折叠的 条评论
为什么被折叠?



