前言

本节内容,将会用到前面类的相关知识以及C++基础教程以及C++类&对象,需要你具备前面的知识,如果你还有不会的或者说不熟悉的地方,请重新温习一下


C++ 多态:让对象具备“多种形态”的能力

多态,顾名思义,就是“多种形态”。在 C++ 中,多态是面向对象编程(OOP)的三大特性之一(另两个是封装和继承)。多态使得程序可以使用统一的接口来操纵不同类型的对象,从而实现代码的灵活性和可扩展性。

目录


1. 什么是多态?

多态是指程序中对象的多种表现形态,具体来说,就是使用基类指针或引用,在运行时根据对象的实际类型,调用对应的重写方法。这意味着同一个函数调用,根据对象类型的不同,会表现出不同的行为

举个生活中的例子:

假设有一个“动物”这个概念,动物会“发出声音”。不同的动物,发出的声音不同。猫会“喵喵叫”,狗会“汪汪叫”。在程序中,我们可以通过一个统一的接口(如 speak() 方法)来让不同的动物发出各自的声音。


2. 虚函数的概念

2.1 什么是虚函数?

虚函数(virtual function)是使用 virtual 关键字声明的成员函数,目的是为了允许子类重写该函数,并在运行时通过基类指针或引用调用子类的实现

2.2 为什么需要虚函数?

在基类中,如果某个函数可能会被子类重写,并且你希望通过基类指针调用子类的实现,就需要将该函数声明为虚函数。

2.3 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Animal {
public:
virtual void speak() { // 虚函数
std::cout << "Animal makes a sound." << std::endl;
}
};

class Dog : public Animal {
public:
void speak() override { // 重写虚函数
std::cout << "Dog barks." << std::endl;
}
};

class Cat : public Animal {
public:
void speak() override {
std::cout << "Cat meows." << std::endl;
}
};

2.4 如何使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void makeAnimalSpeak(Animal* animal) {
animal->speak();
}

int main() {
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();

makeAnimalSpeak(animal1); // 输出:Dog barks.
makeAnimalSpeak(animal2); // 输出:Cat meows.

delete animal1;
delete animal2;
return 0;
}

在上述代码中,尽管我们使用的是 Animal* 类型的指针,但实际调用的是 DogCat 各自的 speak() 方法。


3. 动态绑定 vs 静态绑定

3.1 静态绑定

  • 定义:函数调用在编译时就已经确定,称为静态绑定早期绑定
  • 特点:编译器根据对象的静态类型决定调用哪个函数。

3.2 动态绑定

  • 定义:函数调用在运行时根据对象的实际类型来决定,称为动态绑定晚期绑定
  • 特点:需要通过基类指针或引用调用虚函数。

3.3 示例对比

静态绑定

1
2
Dog dog;
dog.speak(); // 编译时决定调用 Dog::speak()

动态绑定

1
2
Animal* animal = new Dog();
animal->speak(); // 运行时决定调用 Dog::speak()

如果 speak() 不是虚函数,那么即使使用基类指针,也会调用 Animal::speak(),这是因为没有动态绑定,函数调用在编译时已经确定。


4. 纯虚函数与抽象类

4.1 纯虚函数

  • 定义:没有实现的虚函数,声明时在函数签名后加 = 0
  • 目的:要求派生类必须实现该函数,否则派生类也将是抽象类,无法实例化。

4.2 抽象类

  • 定义:包含纯虚函数的类称为抽象类
  • 特点
    • 不能创建抽象类的实例。
    • 可以包含成员变量和已经实现的函数。
    • 用于定义接口,供子类继承和实现。

4.3 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Shape {
public:
virtual void draw() = 0; // 纯虚函数
};

class Circle : public Shape {
public:
void draw() override {
std::cout << "Drawing a circle." << std::endl;
}
};

class Rectangle : public Shape {
public:
void draw() override {
std::cout << "Drawing a rectangle." << std::endl;
}
};

4.4 使用抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void renderShape(Shape* shape) {
shape->draw();
}

int main() {
Shape* shape1 = new Circle();
Shape* shape2 = new Rectangle();

renderShape(shape1); // 输出:Drawing a circle.
renderShape(shape2); // 输出:Drawing a rectangle.

delete shape1;
delete shape2;
return 0;
}

5. 多态的实现原理

5.1 虚函数表(V-Table)

  • 概念:编译器为每个包含虚函数的类创建一个虚函数表,记录该类的虚函数地址。
  • 作用:用于在运行时动态决定调用哪个函数。

5.2 虚函数指针(V-Ptr)

  • 概念:每个对象都有一个隐藏的指针,指向所属类的虚函数表。
  • 作用:通过虚函数指针找到虚函数表,从而调用正确的函数实现。

5.3 调用过程

当通过基类指针或引用调用虚函数时:

  1. 程序根据对象的虚函数指针找到对应的虚函数表。
  2. 在虚函数表中查找对应函数的地址。
  3. 调用该地址对应的函数实现。

5.4 示例图解

  • 类结构

    1
    2
    3
    4
    5
    Animal
    ├── speak() [virtual]

    Dog : public Animal
    └── speak() [override]
  • 对象内存布局

    • Animal 对象

      1
      2
      [ V-Ptr ] ---> [ Animal V-Table ]
      ├── speak() --> Animal::speak()
    • Dog 对象

      1
      2
      [ V-Ptr ] ---> [ Dog V-Table ]
      ├── speak() --> Dog::speak()

6. 为什么要使用多态?

6.1 代码复用性

  • 统一接口:通过基类指针或引用,可以使用同一个函数操作不同类型的对象。
  • 减少重复代码:避免为每个子类编写重复的函数调用代码。

6.2 扩展性

  • 易于维护和扩展:添加新的子类无需修改现有代码,只需确保新子类实现了必要的虚函数。
  • 降低耦合度:模块之间依赖于抽象的接口,而非具体的实现。

6.3 灵活性

  • 运行时决定行为:程序可以在运行时根据实际对象类型执行不同的操作。
  • 支持多态容器:可以创建包含基类指针的容器,存储不同类型的对象。

7. 使用多态需要注意的问题

7.1 析构函数应为虚函数

  • 原因:当通过基类指针删除子类对象时,如果基类析构函数不是虚函数,可能导致资源未正确释放。

  • 示例

    1
    2
    3
    4
    5
    6
    class Animal {
    public:
    virtual ~Animal() {
    std::cout << "Animal destroyed." << std::endl;
    }
    };

7.2 对象切片

  • 概念:当将子类对象赋值给基类对象时,子类特有的数据会被“切掉”。
  • 避免方法:尽量使用指针或引用来处理对象。

7.3 性能开销

  • 动态绑定的成本:多态机制需要在运行时查找函数地址,略微增加了执行时间和内存开销。
  • 权衡:通常,这些开销是可以接受的,不会对性能产生显著影响。

7.4 虚函数不能是内联函数

  • 原因:虚函数在运行时决定调用哪个版本,无法在编译时内联展开。

8. 总结

多态是 C++ 面向对象编程的核心特性之一,它通过虚函数和动态绑定机制,使得程序可以在运行时根据对象的实际类型执行相应的操作。

多态的优点

  • 提高代码的复用性和可维护性。
  • 增强程序的扩展性和灵活性。
  • 使代码更符合面向对象的设计思想。

使用多态的关键

  • 使用虚函数来允许子类重写基类方法。
  • 通过基类指针或引用来调用虚函数。
  • 当需要定义统一接口时,使用纯虚函数和抽象类。

希望以上内容能够帮助您更深入地理解 C++ 中的多态机制,并在实际编程中有效地应用它。多态虽然概念简单,但在大型项目中能够大大简化代码结构,提高开发效率。

如果您有任何疑问,或者需要进一步的解释,请随时提问!