## 一.虚函数
### 1.作用
允许在派生类中重新定义与基类同名的函数,并且通过 **基类指针或引用** 来访问派生类中与基类同名的函数。
### 2.虚函数的定义声明
在基类 `A` 声明时,在函数名 `f` 前添加 `virtual` 关键字。
此时若有派生类 `B` 继承了 `A` 类,则与基类同名的函数 `f` 会自动加上 `virtual` 关键字。
#### 例1 使用虚函数与不使用虚函数的案例对比
不使用虚函数:
```c++
#include <iostream>
using namespace std;
struct A
{
//这不是虚函数哦
void hi()
{
cout << "Hi, A" <<endl;
}
};
struct B :public A
{
//这不是虚函数哦
void hi()
{
cout << "Hi, B" <<endl;
}
};
int main()
{
B b;
A &ra = b;
b.hi();
ra.hi();
return 0;
}
```
>[test]
>Hi, B
>Hi, A
使用虚函数:
```c++
#include <iostream>
using namespace std;
struct A
{
//虚函数
virtual void hi()
{
cout << "Hi, A" <<endl;
}
};
struct B :public A
{
//虚函数
void hi()
{
cout << "Hi, B" <<endl;
}
};
int main()
{
B b;
A &ra = b;
b.hi();
ra.hi();
return 0;
}
```
>[test]
>Hi, B
>Hi, B
#### 例2 虚析构函数
当我们使用基类指针去接收一个动态创建的派生类对象时,直接 `delete` 基类指针,则只会执行基类的析构函数,而不会删除派生类新增部分。这就会产生内存垃圾:
```c++
#include <iostream>
using namespace std;
struct A
{
~A()
{
cout << "析构 A" <<endl;
}
};
struct B :public A
{
~B()
{
cout << "析构 B" <<endl;
}
};
int main()
{
A *pa = new B;
delete pa;
return 0;
}
```
>[test]
>析构 A
为了避免这个问题,我们一般把析构函数声明虚函数,即使析构函数是空的:
```c++
#include <iostream>
using namespace std;
struct A
{
//虚析构函数
virtual ~A()
{
cout << "析构 A" <<endl;
}
};
struct B :public A
{
//虚析构函数
~B()
{
cout << "析构 B" <<endl;
}
};
int main()
{
A *pa = new B;
delete pa;
return 0;
}
```
>[test]
>析构 B
>析构 A
>[warning]不可以将构造函数设置为虚函数,因为构造函数执行完毕后,对象创建才算完成,没有对象,就不能完成匹配。
### 3.通过虚函数实现动态联编的条件
+ 虚函数
+ 通过 **基类指针或引用** 访问派生类对象
如果直接通过派生类、派生类的指针、派生类的引用、作用域运算符(包括基类和派生类)来访问与基类同名的函数,则只进行 **静态联编** 。
### 4.虚函数的实现原理
编译器会为每个含有虚函数的类提供一个虚函数表(vtable),它实际上就是一个函数指针数组,用于存放该类中所有虚函数的入口地址;而每当用多态类创建一个对象时,编译器就会自动生成一个虚函数表指针(vptr),由构造函数正确对其初始化,使其指向该对象所属类的虚函数表,最后将它放置在对象结构的开头。
虚函数表中的函数地址可能并不是真正的虚函数地址,而可能是中间过渡函数 `thunk` 的地址,而这个中间过渡函数用于跳转到虚函数的真实地址,并对传入的对象指针 `this` 进行调整。
#### 例3
假设有以下类的声明:
```c++
struct A1
{
int x_;
A1(int x = 1) :x_(x) {}
};
struct A2
{
int x_;
A2(int x = 2) :x_(x) {}
virtual void test() { cout << "Call base A2 vf"; }
};
struct A3
{
int x_;
A3(int x = 3) :x_(x) {}
virtual void test() { cout << "Call base A3 vf"; }
};
struct B : public A1, public A2, public A3 {
int y_;
void test() { cout << "Call derived vf"; }
};
```
使用 `Visual Studio` 打开上述代码,并在命令行选项中添加 `/d1 reportSingleClassLayoutB `,这样,在编译时,通过输出窗口可以看到 类 `B` 的内存布局和虚函数表:
```
class B size(24):
+---
0 | +--- (base class A2)
0 | | {vfptr}
4 | | x_
| +---
8 | +--- (base class A3)
8 | | {vfptr}
12 | | x_
| +---
16 | +--- (base class A1)
16 | | x_
| +---
20 | y_
+---
B::$vftable@A2@:
| &B_meta
| 0
0 | &B::test
B::$vftable@A3@:
| -8
0 | &thunk: this-=8; goto B::test
B::test this adjustor: 0
```
- 阅读说明
- 1.1 概述
- C++基础
- 1.2 变量与常量
- 1.2.1 变量
- 1.2.2 字面值常量
- 字符型常量
- 数值型常量
- 1.2.3 cv限定符
- 1.3 作用域
- 1.3.1 标识符
- 1.3.2 *命名空间
- 1.3.3 作用域
- 1.3.4 可见性
- 1.4 数据类型
- 1.4.1 概述
- 1.4.2 处理类型
- 类型别名
- * auto说明符
- * decltype说明符
- 1.4.3 数组
- 1.4.4 指针
- 1.4.5 引用
- 1.5 表达式
- 1.5.1 概述
- 1.5.2 值的类别
- 1.5.3 *初始化
- 1.5.4 运算符
- 算术运算符
- 逻辑和关系运算符
- 赋值运算符
- 递增递减运算符
- 成员访问运算符
- 位运算符
- 其他运算符
- 1.5.5 *常量表达式
- 1.5.6 类型转换
- 第2章 面向过程编程
- 2.1 流程语句
- 2.1.1 条件语句
- 2.1.2 循环语句
- 2.1.3 跳转语句
- 2.1.4 *异常处理
- 2.2 函数
- 2.2.1 概述
- 2.2.2 函数参数
- 2.2.3 内置函数
- 2.2.4 函数重载
- 2.2.5 * 匿名函数
- 2.3 存储和生命期
- 2.3.1 生命周期与存储区域
- 2.3.2 动态内存
- 2.4 *预处理命令
- 第3章 面向对象编程
- 3.1 概述
- 3.2 类和对象
- 3.3 成员
- 3.3.1 访问限制
- 3.3.2 常成员
- 3.3.3 静态成员
- 3.3.4 成员指针
- 3.3.5 this指针
- 3.4 特殊的成员函数
- 3.4.1 概述
- 3.4.2 构造函数
- 3.4.3 析构函数
- 3.4.4 拷贝语义
- 3.4.5 * 移动语义
- 3.5 友元
- 3.6 运算符重载与类型转换
- 3.6.1 概述
- 3.6.2 重载方法
- 3.6.3 类型转换
- 3.7 继承与多态性
- 3.7.1 概述
- 3.7.2 派生类
- 3.7.3 子类型
- 3.7.4 虚基类
- 3.7.5 虚函数
- 3.7.6 抽象类
- 3.8 模板与泛型
- 3.8.1 概述
- 3.8.2 模板类型
- 3.8.3 *模板参数
- 3.8.4 *模板编译
- 3.8.5 *模板推断
- 3.8.6 *实例化与特例化
- 第4章 C++标准库
- 4.1 概述
- 4.2 输入输出流
- 4.2.1 概述
- 4.2.2 *流的状态
- 4.2.3 *常用流
- 4.2.4 *格式化I/O
- 4.2.5 *低级I/O
- 4.2.6 *随机访问
- 4.3 *C输入输出
- 4.3.1 *字符输入输出
- 4.3.2 *格式化输入输出
- 4.4 * 容器
- 4.4.1 * 概述
- 4.4.2 * 基本操作
- 4.4.3 * 顺序容器
- 4.4.4 * 迭代器
- 4.4.5 * 容器适配器
- 4.5 * 泛型算法
- 4.6 * 内存管理
- 4.6.1 * 自动指针
- 4.7 * 其他设施