C/C++ 入门:面向对象进阶

类解决了”数据和操作放一起”,但真实世界里的对象常常有”共同点 + 各自特点”,继承和多态就是为这种关系设计的。

📚 基本概念速读

名称 定义 省流
继承(inheritance) 派生类复用基类的成员 代码复用
基类(base class) 被继承的类 父类
派生类(derived class) 继承基类的类 子类
继承访问权限 public/protected/private 三种继承方式 继承时成员怎么放行
函数重写(override) 派生类重新实现基类虚函数 覆盖行为
虚函数(virtual function) virtual 声明的成员函数 支持动态绑定
多态(polymorphism) 基类指针/引用调用到派生类实现 一个接口多种行为
虚析构函数 基类析构函数加 virtual 安全删除派生类对象

🧩 继承:基类、派生类、代码复用

基本用法

如果多个类有共同的数据和行为,可以把共同部分抽到基类,派生类”继承”它们,再补充自己的部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <string>
using namespace std;

// 基类:共同部分
class Animal {
public:
string name;

void eat()
{
cout << name << " is eating" << endl;
}
};

// 派生类:继承 Animal,再添加自己的部分
class Dog : public Animal {
public:
void bark()
{
cout << name << " says woof!" << endl;
}
};

int main()
{
Dog d;
d.name = "Rex"; // 基类的成员
d.eat(); // 基类的方法
d.bark(); // 自己的方法
return 0;
}

输出:

1
2
Rex is eating
Rex says woof!

class Dog : public Animal 的含义是:Dog 继承 Animalpublic 表示继承方式。



flowchart TD
    A[基类 Animal<br/>name, eat] --> B[派生类 Dog<br/>+ bark]
    A --> C[派生类 Cat<br/>+ meow]

构造与析构顺序

派生类对象创建时,先构造基类部分,再构造派生类部分;销毁时顺序相反,先析构派生类,再析构基类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Base {
public:
Base() { cout << "Base 构造" << endl; }
~Base() { cout << "Base 析构" << endl; }
};

class Derived : public Base {
public:
Derived() { cout << "Derived 构造" << endl; }
~Derived() { cout << "Derived 析构" << endl; }
};

// 创建 Derived 对象时的输出:
// Base 构造
// Derived 构造
// Derived 析构
// Base 析构

继承带来的最直接好处是代码复用:DogCat 不用各自重复写 nameeat()

🔐 继承访问权限

继承时也可以指定访问权限,它决定基类成员在派生类中”降级”到什么可见性。

基类成员 public 继承后 protected 继承后 private 继承后
public public protected private
protected protected protected private
private 不可访问 不可访问 不可访问

基类的 private 成员无论哪种继承方式,派生类都不能直接访问(只能通过基类的 public/protected 接口间接访问)。

实际使用中,public 继承占绝大多数,它表达”派生类是一种基类”(is-a)关系:

1
class Dog : public Animal { ... };   // Dog 是一种 Animal

protected 继承和 private 继承主要用于特殊的设计场景,比如”实现复用但不暴露继承关系”。入门阶段记住三者的区别表,优先使用 public 继承即可。

🔁 函数重写

派生类可以重新实现基类里的函数,让同一个函数名在派生类里有不同的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

class Animal {
public:
virtual void speak() // virtual:允许被重写
{
cout << "some sound" << endl;
}
};

class Dog : public Animal {
public:
void speak() override // 重写基类的 speak
{
cout << "woof!" << endl;
}
};

这里有两个关键点:

  1. 基类函数要加 virtual:否则派生类的同名函数只是”隐藏”基类版本,不会参与多态;
  2. 派生类用 override 标记:C++11 起可写,让编译器检查”基类里确实有这样一个虚函数”,防止拼错签名。
1
2
3
4
5
class Dog : public Animal {
public:
void speak() override { ... } // 正确
// void speak(int x) override; // 错误:基类没有这个签名,编译器报错
};

🌀 虚函数:virtual 与动态绑定

静态绑定 vs 动态绑定

不加 virtual 时,调用哪个函数在编译期就确定了,这叫静态绑定;加 virtual 后,调用哪个函数在运行期根据对象的实际类型决定,这叫动态绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;

class Animal {
public:
void speak() // 非虚函数:静态绑定
{
cout << "some sound" << endl;
}
};

class Dog : public Animal {
public:
void speak() // 隐藏基类版本,不参与多态
{
cout << "woof!" << endl;
}
};

int main()
{
Animal* p = new Dog();
p->speak(); // 静态绑定:调用 Animal::speak()
delete p;
return 0;
}

输出是 some sound,因为编译期只看到指针类型是 Animal*

把基类函数加上 virtual

1
2
3
4
5
6
7
class Animal {
public:
virtual void speak()
{
cout << "some sound" << endl;
}
};

同样的调用:

1
2
Animal* p = new Dog();
p->speak(); // 动态绑定:运行期发现实际是 Dog,调用 Dog::speak()

输出变成 woof!

绑定方式 决定时机 依赖 virtual 典型场景
静态绑定 编译期,看指针/引用类型 普通成员函数
动态绑定 运行期,看对象实际类型 虚函数

🎭 多态:基类指针/引用调用派生类行为

多态就是”用基类的指针或引用,调用到派生类的实现”。把各种派生类对象当成基类来统一处理,运行时各自表现自己的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <vector>
using namespace std;

class Animal {
public:
virtual void speak()
{
cout << "some sound" << endl;
}
};

class Dog : public Animal {
public:
void speak() override { cout << "woof!" << endl; }
};

class Cat : public Animal {
public:
void speak() override { cout << "meow!" << endl; }
};

int main()
{
// 基类指针指向不同派生类对象
Animal* a = new Dog();
Animal* b = new Cat();

a->speak(); // woof!
b->speak(); // meow!

// 统一放进基类指针容器,逐个调用各自行为
vector<Animal*> zoo = {a, b};
for (Animal* p : zoo) {
p->speak();
}

delete a;
delete b;
return 0;
}


flowchart TD
    A[Animal* 基类指针] --> B[Dog 对象]
    A --> C[Cat 对象]
    B -->|p->speak 动态绑定| D[woof!]
    C -->|p->speak 动态绑定| E[meow!]

多态的价值在于:代码只需要面向基类写一遍,新增一个派生类时不用改调用方。

引用也能触发多态:Animal& ref = dog; ref.speak(); 效果相同。只有通过基类指针或引用调用虚函数才会动态绑定;通过对象直接调用永远是静态绑定。

🧹 虚析构函数

风险场景

如果基类析构函数不是 virtual,通过基类指针删除派生类对象时,只会调用基类的析构函数,派生类部分的资源可能泄漏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;

class Base {
public:
~Base() // 非虚析构
{
cout << "Base 析构" << endl;
}
};

class Derived : public Base {
public:
~Derived()
{
cout << "Derived 析构" << endl;
}
};

int main()
{
Base* p = new Derived();
delete p; // 危险:只调用 Base::~Base()
return 0;
}

输出只有:

1
Base 析构

Derived 的析构函数没被调用。如果 Derived 在析构函数里释放资源,资源就泄漏了。

正确写法

把基类析构函数声明为 virtual

1
2
3
4
5
6
7
class Base {
public:
virtual ~Base() // 虚析构
{
cout << "Base 析构" << endl;
}
};

这样 delete p 会先调用 Derived 析构,再调用 Base 析构:

1
2
Derived 析构
Base 析构

经验规则

场景 建议
类里有虚函数(会被继承和多态使用) 析构函数也声明为 virtual
类不会被继承 不必加 virtual
通过基类指针 delete 派生类对象 基类析构必须是 virtual

一个类只要设计了虚函数、打算被多态使用,就把析构函数也加上 virtual。这是 C++ 里的经典规则:有虚函数,析构函数通常也要虚

🏛️ 抽象类与纯虚函数

纯虚函数

在虚函数声明后面加 = 0,就成了纯虚函数(pure virtual function):

1
2
3
4
5
class Shape {
public:
virtual double area() const = 0; // 纯虚函数:不提供实现
virtual ~Shape() {} // 虚析构,配合多态
};

含纯虚函数的类叫抽象类(abstract class),它有两个特点:

  1. 不能实例化Shape s; 会编译报错,因为 area() 没有实现;
  2. 作为接口:派生类必须实现所有纯虚函数,才能创建对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Circle : public Shape {
private:
double r;

public:
Circle(double radius) : r(radius) {}

double area() const override // 必须实现纯虚函数
{
return 3.14159 * r * r;
}
};

int main()
{
// Shape s; // 错误:抽象类不能实例化
Circle c(2.0);
cout << c.area() << endl; // 12.5664

Shape* p = &c; // 可以:用基类指针指向派生类
cout << p->area() << endl; // 12.5664,多态调用
return 0;
}

抽象类的意义

抽象类定义的是一份”契约”:它规定”所有形状都能算面积”,但不规定怎么算。具体算法由每个派生类自己实现。这正好和第 8 章”对象职责”的思想衔接——基类只负责定义职责,实现下放给派生类。

特性 普通虚函数 纯虚函数
基类提供实现 提供 不提供(= 0
基类能否实例化 不能(抽象类)
派生类是否必须实现 可选 必须实现
典型用途 默认行为 + 可覆盖 定义接口契约


flowchart TD
    A[抽象类 Shape<br/>area() = 0] --> B[Circle<br/>实现 area]
    A --> C[Rectangle<br/>实现 area]
    B --> D[基类指针调用各自实现]
    C --> D

一个类里只要有一个纯虚函数,整个类就是抽象类。抽象类通常作为设计层级的”顶层接口”存在。

⚠️ 常见误区

误区 正解
继承只是为了代码复用 继承还表达 is-a 关系,与多态配合使用
派生类写同名函数就是重写 基类函数不加 virtual 时只是隐藏,不是重写
override 必须写 不写也能编译,但写上能让编译器帮你检查签名
基类析构函数加不加 virtual 无所谓 通过基类指针删除派生类对象时,非虚析构会漏掉派生类析构
多态就是函数重载 重载是编译期同名不同参;多态是运行期动态绑定
通过对象直接调用虚函数也是多态 直接通过对象调用是静态绑定,只有基类指针/引用才触发动态绑定
抽象类可以直接创建对象 抽象类含纯虚函数,不能实例化,只能作为接口被继承

✅ 总结

继承复用代码,虚函数开启动态绑定,两者结合就是多态;纯虚函数把基类变成抽象类,定义接口契约;设计基类时记住”有虚函数,析构函数也要虚”。

学到这里,C++ 面向对象的四大特性——封装、继承、多态、抽象——已经集齐。第 7 章的类与对象打底,这一章的继承、多态和抽象类让类与类之间有了层级和契约。

Happy Hacking! 🎉