# 多态与虚函数

> [!info] 学习导航
> 返回：[[C++ 学习指南]]
> 前置知识：[[计算机系/C++/10 继承与赋值兼容|继承与赋值兼容]]

**多态**按字面的意思就是多种形态。

当类之间存在层次结构，并且类之间是通过继承关联时，就会用到多态。

在 C++ 中，多态（Polymorphism）是面向对象编程的重要特性之一。

C++ 多态允许使用基类指针或引用来调用子类的重写方法，从而使得同一接口可以表现不同的行为。

多态使得代码更加灵活和通用，程序可以通过基类指针或引用来操作不同类型的对象，而不需要显式区分对象类型。这样可以使代码更具扩展性，在增加新的形状类时不需要修改主程序。

## 多态的关键点

**虚函数（Virtual Functions）**：

- 在基类中声明一个函数为虚函数，使用关键字virtual。
- 派生类可以重写（override）这个虚函数。
- 调用虚函数时，会根据对象的实际类型来决定调用哪个版本的函数。

**动态绑定（Dynamic Binding）**：

- 也称为晚期绑定（Late Binding），在运行时确定函数调用的具体实现。
- 需要使用指向基类的指针或引用来调用虚函数，编译器在运行时根据对象的实际类型来决定调用哪个函数。

**纯虚函数（Pure Virtual Functions）**：

- 一个包含纯虚函数的类被称为抽象类（Abstract Class），它不能被直接实例化。
- 纯虚函数没有函数体，声明时使用= 0。
- 它强制派生类提供具体的实现。

**多态的实现机制**：

- 虚函数表（V-Table）：C++运行时使用虚函数表来实现多态。每个包含虚函数的类都有一个虚函数表，表中存储了指向类中所有虚函数的指针。
- 虚函数指针（V-Ptr）：对象中包含一个指向该类虚函数表的指针。

**使用多态的优势**：

- **代码复用**：通过基类指针或引用，可以操作不同类型的派生类对象，实现代码的复用。
- **扩展性**：新增派生类时，不需要修改依赖于基类的代码，只需要确保新类正确重写了虚函数。
- **解耦**：多态允许程序设计更加模块化，降低类之间的耦合度。

**注意事项**：

- 只有通过基类的指针或引用调用虚函数时，才会发生多态。
- 如果直接使用派生类的对象调用函数，那么调用的是派生类中的版本，而不是基类中的版本。
- 多态性需要运行时类型信息（RTTI），这可能会增加程序的开销。

```cpp
#include <iostream>
using namespace std;
// 基类 Animal
class Animal {
public:
// 虚函数 sound，为不同的动物发声提供接口
  virtual void sound() const {
    cout << "Animal makes a sound" << endl;
  }
// 虚析构函数确保子类对象被正确析构
  virtual ~Animal() {
    cout << "Animal destroyed" << endl;
  }
};
// 派生类 Dog，继承自 Animal
class Dog : public Animal {
public:
// 重写 sound 方法
  void sound() const override {
    cout << "Dog barks" << endl;
  }
  ~Dog() {
    cout << "Dog destroyed" << endl;
  }
};
// 派生类 Cat，继承自 Animal
class Cat : public Animal {
public:
// 重写 sound 方法
  void sound() const override {
    cout << "Cat meows" << endl;
  }
  ~Cat() {
    cout << "Cat destroyed" << endl;
  }
};
// 测试多态
int main() {
  Animal* animalPtr; // 基类指针
// 创建 Dog 对象，并指向 Animal 指针
  animalPtr = new Dog();
  animalPtr->sound(); // 调用 Dog 的 sound 方法
  delete animalPtr; // 释放内存，调用 Dog 和 Animal 的析构函数
// 创建 Cat 对象，并指向 Animal 指针
  animalPtr = new Cat();
  animalPtr->sound(); // 调用 Cat 的 sound 方法
  delete animalPtr; // 释放内存，调用 Cat 和 Animal 的析构函数
  return 0;
}
```

**主函数 main()**：

- 创建一个基类指针 animalPtr。
- 使用 new Dog() 创建 Dog 对象，将其地址赋给 animalPtr。此时，调用 animalPtr->sound() 会输出"Dog barks"，因为 animalPtr 实际指向的是 Dog 对象。
- 释放 Dog 对象时，先调用 Dog 的析构函数，再调用 Animal 的析构函数。
- 使用 new Cat() 创建 Cat 对象并赋给 animalPtr，再调用 animalPtr->sound()，输出"Cat meows"，显示多态行为。

- **虚函数**：通过在基类中使用 virtual 关键字声明虚函数，派生类可以重写这个函数，从而使得在运行时根据对象类型调用正确的函数。
- **动态绑定**：C++ 的多态通过动态绑定实现。在运行时，基类指针 animalPtr 会根据它实际指向的对象类型（Dog 或 Cat）调用对应的 sound() 方法。
- **虚析构函数**：在具有多态行为的基类中，析构函数应该声明为 virtual，以确保在删除派生类对象时调用派生类的析构函数，防止资源泄漏。

## 虚函数

**虚函数**是在基类中使用关键字 **virtual** 声明的函数。

**虚函数**允许子类重写它，从而在运行时通过基类指针或引用调用子类的重写版本，实现动态绑定。

我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数，这种操作被称为**动态链接**，或**后期绑定。**

**特点：**

- **在基类中可以有实现**。通常虚函数在基类中提供默认实现，但子类可以选择重写。
- **动态绑定**：在运行时根据对象的实际类型调用相应的函数版本。
- **可选重写**：派生类可以选择性地重写虚函数，但不是必须。

虚函数不能是静态成员函数，因为静态成员函数属于类而非对象，无法实现运行时多态。


核心概念解析

"继承下来的不管加不加都是虚的"


当基类声明了一个虚函数（virtual），所有派生类中同名、同签名的函数自动成为虚函数，无论是否显式添加virtual关键字。


这是C++的默认行为，目的是保持多态性的一致性。


```cpp
class Base {
public:
  virtual void show() { cout << "Base\n"; } // 显式virtual
};
class Derived : public Base {
public:
  void show() { cout << "Derived\n"; } // 自动成为虚函数（即使不加virtual）
};
```

"不做成虚的就是基类虚函数"


如果派生类中定义的函数与基类虚函数签名不同（如参数类型不同），则不会覆盖基类虚函数，而是成为一个独立的新函数。


此时基类的虚函数仍然存在，但派生类的新函数不会参与多态。


```cpp
class Base {
public:
  virtual void show(int x) { cout << "Base " << x << "\n"; }
};
class Derived : public Base {
public:
  void show() { cout << "Derived\n"; } // ❌ 不是虚函数覆盖，而是新函数
};
// 使用时：
Base* b = new Derived();
b->show(1); // 调用Base::show(int)，而非Derived::show()
```

关键规则总结

情况    是否覆盖虚函数  是否需要override关键字（C++11推荐）

派生类函数与基类虚函数完全同签名   ✅ 是  建议加override

派生类函数与基类虚函数签名不同  ❌ 否  编译错误（若误用override）

基类函数非虚，派生类同名函数   ❌ 不是虚函数  不应加override

为什么需要override关键字？（C++11引入）

为了避免意外隐藏（而非覆盖）基类虚函数，强烈建议在派生类中显式标记override：


```cpp
class Derived : public Base {
public:
  void show(int x) override { cout << "Derived " << x << "\n"; } // ✅ 明确覆盖
  void show() const override; // ❌ 编译错误：签名不匹配基类虚函数
};
```

多态行为的必要条件

基类函数必须声明为virtual。


派生类函数必须签名完全一致（包括const和引用限定符）。


通过基类指针/引用调用才能触发多态。


```cpp
Base* b = new Derived();
b->show(); // 多态调用Derived::show()（如果覆盖正确）
```

常见误区纠正

误区："派生类不加virtual就不是虚函数"

事实：只要基类有virtual，派生类同名同签名函数自动继承虚属性。


误区："函数名相同就能覆盖"

事实：必须严格匹配签名（参数类型、const等）。


最佳实践

基类虚函数显式用virtual。


派生类覆盖函数显式用override（C++11起）。


使用final禁止进一步覆盖（如果需要）。


```cpp
class Base {
public:
  virtual ~Base() = default; // 虚析构函数（多态基类必须）
  virtual void foo(int) const; // 虚函数
};
class Derived : public Base {
public:
  void foo(int) const override; // 明确覆盖
  void bar() final; // 禁止派生类覆盖
};
```

## 纯虚函数

您可能想要在基类中定义虚函数，以便在派生类中重新定义该函数更好地适用于对象，但是您在基类中又不能对虚函数给出有意义的实现，这个时候就会用到纯虚函数。

纯虚函数是没有实现的虚函数，在基类中用 **= 0** 来声明。

纯虚函数表示基类定义了一个接口，但具体实现由派生类负责。

纯虚函数使得基类变为抽象类（abstract class），无法实例化。

**特点：**

- **必须在基类中声明为 = 0**，表示没有实现，子类必须重写。
- **抽象类**：包含纯虚函数的类不能直接实例化，必须通过派生类实现所有纯虚函数才能创建对象。
- **接口定义**：纯虚函数通常用于定义接口，让派生类实现具体行为。

**虚函数与纯虚函数的对比**

| **特性** | **虚函数（Virtual Function）**  | **纯虚函数（Pure Virtual Function）** |
| -------- | ------------------------------- | ------------------------------------- |
| 定义     | 基类中使用 virtual 声明，有实现 | 基类中使用 = 0 声明，无实现           |
| 子类重写 | 子类可以选择重写                | 子类必须实现                          |
| 抽象性   | 可以实例化类                    | 使类变为抽象类，无法实例化            |
| 用途     | 提供默认行为，允许子类重写      | 定义接口，强制子类实现具体行为        |

1、纯虚函数声明如下： virtual void funtion1()=0; 纯虚函数一定没有定义，纯虚函数用来规范派生类的行为，即接口。包含纯虚函数的类是抽象类，抽象类不能定义实例，但可以声明指向实现该抽象类的具体类的指针或引用。

2、虚函数声明如下：virtual ReturnType FunctionName(Parameter) 虚函数必须实现，如果不实现，编译器将报错、

3、对于虚函数来说，父类和子类都有各自的版本。由多态方式调用的时候动态绑定。

4、实现了纯虚函数的子类，该纯虚函数在子类中就变成了虚函数，子类的子类即孙子类可以覆盖该虚函数，由多态方式调用的时候动态绑定。

5、虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。

6、在有动态分配堆上内存的时候，析构函数必须是虚函数，但没有必要是纯虚的。

7、友元不是成员函数，只有成员函数才可以是虚拟的，因此友元不能是虚拟函数。但可以通过让友元函数调用虚拟成员函数来解决友元的虚拟问题。

8、析构函数应当是虚函数，将调用相应对象类型的析构函数，因此，如果指针指向的是子类对象，将调用子类的析构函数，然后自动调用基类的析构函数。

1、必须存在继承关系；

2、继承关系必须有同名虚函数（**其中虚函数是在基类中使用关键字Virtual声明的函数，在派生类中重新定义基类中定义的虚函数时，会告诉编译器不要静态链接到该函数**）；

3、存在基类类型的指针或者引用，通过该指针或引用调用虚函数；

**动态联编的实现机制 VTABLE**

编译器对每个包含虚函数的类创建一个虚函数表VTABLE，表中每一项指向一个虚函数的地址，即VTABLE表可以看成一个函数指针的数组，每个虚函数的入口地址就是这个数组的一个元素。

每个含有虚函数的类都有各自的一张虚函数表VTABLE。每个派生类的VTABLE继承了它各个基类的VTABLE，如果基类VTABLE中包含某一项（虚函数的入口地址），则其派生类的VTABLE中也将包含同样的一项，但是两项的值可能不同。如果派生类中重载了该项对应的虚函数，则派生类VTABLE的该项指向重载后的虚函数，如果派生类中没有对该项对应的虚函数进行重新定义，则使用基类的这个虚函数地址。

在创建含有虚函数的类的对象的时候，编译器会在每个对象的内存布局中增加一个vptr指针项，该指针指向本类的VTABLE。在通过指向基类对象的指针（设为bp）调用一个虚函数时，编译器生成的代码是先获取所指对象的vtb1指针，然后调用vtb1所指向类的VTABLE中的对应项（具体虚函数的入口地址）。

当基类中**没有定义虚函数**时，其长度=数据成员长度；派生类长度=自身数据成员长度+基类继承的数据成员长度；

当基类中**定义虚函数后**，其长度=数据成员长度+虚函数表的地址长度；派生类长度=自身数据成员长度+基类继承的数据成员长度+虚函数表的地址长度。

包含一个虚函数和几个虚函数的类的长度增量为0。含有虚函数的类只是增加了一个指针用于存储虚函数表的首地址。

派生类与基类同名的虚函数在VTABLE中有相同的索引号（或序号）。

A 类继承 B 类，A 类重写了 B 类的虚函数 B2，而 B 类的 B1 中调用了 B2，则 A 类使用 B1 时，运行时 B1 中的 B2 将执行 A 类中重写的虚函数B2。

```cpp
#include <iostream>
using namespace std;
class B
{
public:
void B1(void)
{
 B2();
}
virtual void B2(void)
{
 cout << "Hello, B2" << endl;
}
};
class A:public B
{
public:
virtual void B2(void)
{
 cout << "Hello, A" << endl;
}
};
```

定义一个函数为虚函数，不代表函数为不被实现的函数。

定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。

定义一个函数为纯虚函数，才代表函数没有被实现。

定义纯虚函数是为了实现一个接口，起到一个规范的作用，规范继承这个类的程序员必须实现这个函数。


### final 关键字

父类的虚函数或纯虚函数在子类中依然是虚函数。有时我们并不希望父类的某个函数在子类中被重写，在 C++11 及以后可以用关键字 final 来避免该函数再次被重写。

如果不希望一个类被继承，也可以使用 final 关键字。

格式如下：

```cpp
class Class_name final
{
```

  ...

```cpp
};
```

则该类将不能被继承。

## 相关内容
- [[计算机系/C++/02 类型转换、变量与生命周期|类型转换、变量与生命周期]]

---

## 学习导航
- 上一篇：[[计算机系/C++/10 继承与赋值兼容|继承与赋值兼容]]
- 返回目录：[[C++ 学习指南]]
- 下一篇：[[计算机系/C++/12 模板与模板元编程|模板与模板元编程]]
