1.虚函数简介
2.虚函数的核心作用:运行时动态绑定
3.虚函数的底层原理
4.虚函数的关键规则
1.虚函数简介
csharp
复制代码
C++的虚函数(Virtual Function)是实现面向对象"多态"的核心机制, 能让程序在运行时根据对象的实际类型, 调用对应的方
法, 而非编译时的类型
为什么需要虚函数?
如果父类指针指向子类对象, 调用的方法会是父类的版本, 而非子类重写的版本
csharp
复制代码
#include <iostream>
using namespace std;
// 父类:游戏物体
class GameObject {
public:
// 普通方法(非虚函数)
void Move() {
cout << "游戏物体通用移动" << endl;
}
};
// 子类:玩家(重写Move方法)
class Player : public GameObject {
public:
void Move() {
cout << "玩家快速移动" << endl;
}
};
int main() {
// 父类指针指向子类对象
GameObject* obj_ptr = new Player();
obj_ptr->Move(); // 期望输出"玩家快速移动",实际输出"游戏物体通用移动"
delete obj_ptr;
return 0;
}
csharp
复制代码
问题根源: "编译时, 编译器根据obj_ptr的声明类型绑定方法(静态绑定), 而非对象的实际类型"
2.虚函数的核心作用:运行时动态绑定
csharp
复制代码
给父类的Move()方法加virtual关键字, 就能让方法绑定从"编译时"变成"运行时", 程序会根据对象的实际类型, 调用对应的
方法
1).虚函数的基本用法
class 父类名 {
public:
// 声明虚函数
virtual 返回值类型 方法名(参数) {
// 父类实现
}
};
class 子类名 : public 父类名 {
public:
// 子类重写(override)虚函数(可加override关键字显式声明)
返回值类型 方法名(参数) override {
// 子类实现
}
};
csharp
复制代码
2).示例
#include <iostream>
using namespace std;
class GameObject {
public:
// 声明为虚函数
virtual void Move() {
cout << "游戏物体通用移动" << endl;
}
// 析构函数建议也声明为虚函数(下文解释)
virtual ~GameObject() {}
};
class Player : public GameObject {
public:
// 重写虚函数(加override更规范,编译器会检查是否真的重写)
void Move() override {
cout << "玩家快速移动" << endl;
}
};
class Enemy : public GameObject {
public:
void Move() override {
cout << "敌人缓慢移动" << endl;
}
};
int main() {
// 父类指针指向不同子类对象,调用对应子类的Move方法
GameObject* ptr1 = new Player();
GameObject* ptr2 = new Enemy();
ptr1->Move(); // 输出:玩家快速移动
ptr2->Move(); // 输出:敌人缓慢移动
delete ptr1;
delete ptr2;
return 0;
}
3.虚函数的底层原理
csharp
复制代码
虚函数的实现依赖虚函数表(vtable)和虚表指针(vptr)
a.每个包含虚函数的类, 编译器会生成一个"虚函数表"(vtable) ------ 存储该类所有虚函数的地址
b.每个对象会包含一个隐藏的"虚表指针(vptr)", 指向所属类的虚函数表
c.运行时, 程序通过vptr找到对应类的vtable, 再调用表中的方法地址 ------ 这就是动态绑定
csharp
复制代码
结合之前的GameObject/Player示例, 一步步拆解内存中的实际过程
a.编译器的预处理(编译阶段)
当你写了含虚函数的类, 编译器会做两件事
csharp
复制代码
// 父类:含虚函数
class GameObject {
public:
virtual void Move() { cout << "通用移动" << endl; }
virtual ~GameObject() {}
};
// 子类:重写虚函数
class Player : public GameObject {
public:
void Move() override { cout << "玩家移动" << endl; }
};
csharp
复制代码
关键: 子类的虚表会"继承 + 覆盖"父类虚表 ------ 重写的方法替换地址, 未重写的沿用父类地址
b.对象的内存布局(创建对象时)
当你创建Player对象时, 内存中是这样的
csharp
复制代码
每个含虚函数的对象, 第一个字节一定是vptr(隐藏的)
vptr的值是"所属类的vtable的内存地址", 比如Player对象的vptr指向Player vtable
c.运行时调用方法(动态绑定核心)
当你执行GameObject* ptr = new Player(); ptr->Move(); CPU 执行的步骤是:
- 无虚函数时, 编译器直接把ptr->Move()翻译成"调用 GameObject::Move () 的地址"(编译时就定死)
- 有虚函数时, 编译器只翻译"先取vptr -> 找vtable -> 取方法地址 -> 调用"(运行时才确定具体地址)
4.虚函数的关键规则
csharp
复制代码
1).析构函数必须声明为虚函数
如果父类析构函数不是虚函数, 用父类指针删除子类对象时, 只会调用父类的析构函数, 导致子类的资源(比如堆内存、文件
句柄)无法释放, 造成内存泄漏
csharp
复制代码
class GameObject {
public:
~GameObject() { // 非虚析构
cout << "GameObject析构" << endl;
}
};
class Player : public GameObject {
public:
~Player() {
cout << "Player析构" << endl; // 不会被调用!
}
};
int main() {
GameObject* ptr = new Player();
delete ptr; // 只输出"GameObject析构",Player析构未执行
return 0;
}
正确做法: 父类析构函数加virtual, 子类析构函数会自动成为虚函数("无需显式加")
csharp
复制代码
2).override关键字的作用
子类重写虚函数时加override, 编译器会检查:
a.父类是否有该虚函数
b.方法签名(返回值、参数、const 等)是否完全一致
c.如果不一致, 编译器会报错, 避免手误
csharp
复制代码
3).纯虚函数(抽象类)
如果父类只想定义接口, 不想提供实现, 可以声明纯虚函数, 这样的类称为"抽象类" ------ 不能实例化, 只能作为父类被继承
语法: "virtual 返回值类型 方法名(参数) = 0;"
csharp
复制代码
// 抽象类:所有可交互物体必须实现Interact方法
class Interactable {
public:
virtual void Interact() = 0; // 纯虚函数
virtual ~Interactable() {}
};
// 子类必须实现Interact,否则无法实例化
class Chest : public Interactable {
public:
void Interact() override {
cout << "打开宝箱,获得道具" << endl;
}
};