C++之虚函数
对基类中的方法进行重写;
主要是通过继承机制 + V 表实现;
虚函数的引入与不加入虚函数的主要区别在于 动态多态性。通过将 Entity
类的 GetName
函数声明为 virtual
,可以实现 运行时多态,这意味着程序会根据对象的实际类型调用相应的函数,而不是根据指针或引用的类型调用函数。
1. 没有虚函数的情况
如果你不使用虚函数(即 Entity
类中的 GetName
不是 virtual
),则即使 Entity
的指针指向了 Player
对象,调用 GetName()
时,依然会执行 Entity
类中的 GetName()
函数。也就是说,调用的是编译时决定的函数,而不是运行时根据实际对象类型选择的函数。
举个例子:
class Entity
{
public:
std::string GetName() { return "Entity"; } // 非虚函数
};
class Player : public Entity
{
private:
std::string m_Name;
public:
Player(const std::string& name) : m_Name(name) {}
std::string GetName() { return m_Name; } // 不会覆盖 Entity 中的 GetName
};
void PrintName(Entity* entity)
{
std::cout << entity->GetName() << std::endl; // 总是调用 Entity::GetName()
}
int main()
{
Entity* e = new Entity;
PrintName(e); // 输出 "Entity"
Player* p = new Player("Cherno");
PrintName(p); // 依然输出 "Entity",因为没有虚函数
}
在这种情况下,PrintName(p)
也会输出 "Entity"
,即使 p
实际上指向的是一个 Player
对象。
2. 引入虚函数后的情况
当 GetName
函数声明为 virtual
时,C++ 会使用 虚函数表(vtable) 来确定调用哪个函数。虚函数表在运行时动态决定哪个类的版本的 GetName
被调用。因此,当你调用 entity->GetName()
时,程序会根据实际指向的对象类型(无论是 Entity
还是 Player
)调用相应的函数。
修改代码后,程序的行为会有所不同:
class Entity
{
public:
virtual std::string GetName() { return "Entity"; } // 声明为虚函数
};
class Player : public Entity
{
private:
std::string m_Name;
public:
Player(const std::string& name) : m_Name(name) {}
std::string GetName() override { return m_Name; } // 重写虚函数
};
void PrintName(Entity* entity)
{
std::cout << entity->GetName() << std::endl; // 运行时多态,调用正确的函数
}
int main()
{
Entity* e = new Entity;
PrintName(e); // 输出 "Entity"
Player* p = new Player("Cherno");
PrintName(p); // 输出 "Cherno",因为 GetName 被 Player 重写了
}
现在,程序会根据指针的实际类型来选择调用的 GetName
函数:
PrintName(e)
输出Entity
,因为e
是Entity
类型的对象。PrintName(p)
输出Cherno
,因为p
实际上指向的是Player
类型的对象,GetName
被Player
重写了。
3. 虚函数的工作原理
在 C++ 中,虚函数的工作原理如下:
-
虚函数表(vtable): 每个含有虚函数的类都会生成一个虚函数表,虚函数表存储了类中所有虚函数的地址。在创建对象时,编译器会为每个类对象(包括派生类)设置一个指向该类虚函数表的指针。
-
运行时决定: 在调用虚函数时,程序会根据对象的实际类型(即动态类型)来选择虚函数表中的函数地址,进而调用正确的函数。这个过程称为 动态绑定(dynamic binding)或 运行时多态。
- 如果你使用
Entity* e = new Player("Cherno");
,即使指针类型是Entity*
,但由于虚函数的存在,程序会正确调用Player
类中的GetName
,而不是Entity
类中的GetName
。
- 如果你使用
-
虚析构函数: 虚函数不仅可以使类支持多态,还可以使类支持正确的析构过程,避免内存泄漏。例如,
Entity
类应该将析构函数声明为虚拟函数,以确保删除Player
类型的对象时能正确调用Player
的析构函数。class Entity { public: virtual ~Entity() {} // 虚析构函数 };
总结
- 没有虚函数:如果不使用虚函数,基类指针指向派生类对象时,调用的会是基类中的函数,无法实现多态。
- 使用虚函数:如果使用虚函数,基类指针指向派生类对象时,调用的会是派生类中的函数,实现了动态多态。
- 弊端:虚函数有着额外的内存消耗(存储V表),以及额外的查询查询时间,对于cpu很差的嵌入式设备可能会有影响
- 好处:实现了同一函数对于不同对象引用时候的不同方法;
下一节
纯虚函数