在 C++ 中,虚函数是实现多态的基础。多态是面向对象编程的重要特性之一,允许程序在运行时决定调用哪一个函数版本。通过虚函数,我们能够实现动态绑定,使得不同类型的对象可以通过相同的接口进行操作。
1 静态绑定与动态绑定
- 静态绑定 :在编译时确定函数的调用。它发生在非虚函数的情况下。静态绑定会根据对象的类型(在编译时确定)来调用相应的函数。
- 动态绑定:在运行时根据对象的实际类型决定调用哪个函数。动态绑定仅在虚函数的情况下发生。当基类指针或引用指向派生类对象时,调用的函数由对象的实际类型决定,而不是基类的类型。
2 虚函数工作原理
虚函数依赖于 C++ 中的虚函数表(vtable)。每个包含虚函数的类都会有一个虚函数表,虚函数表包含指向该类虚函数的指针。每个对象在内存中都有一个指向虚函数表的指针,这个指针通常称为 vptr。当通过基类指针调用虚函数时,程序会通过 vptr 查找对象实际的虚函数表,然后调用相应的函数。
本文中base
类有一个虚拟指针vptr
,它指向虚函数表vtable
vtable
:虚函数表vtable
是一个包含指向虚函数的指针的结构,在本例中,vtable
存储了derived
类重写的show
函数的地址
Derived
类中包含了show
函数,由于show
是虚函数,当基类指针指向派生类对象时,通过基类指针调用show函数时,实际会调用Derived类中的版本。
当通过基类指针或引用调用虚函数时,C++ 会使用 动态绑定 来决定具体调用哪个版本的函数。具体来说:
-
当 Base 类的指针(basePtr)指向 Derived 类的对象时,basePtr 会持有指向 Derived 类对象的虚函数表的指针(即 vptr)。
-
该虚函数表指向的是 Derived 类重写后的虚函数(比如 show())的地址。
-
当你通过 basePtr->show() 调用虚函数时,程序会查找 basePtr 所指向对象的 vptr,然后找到该对象的 虚函数表(vtable),并通过虚函数表中的函数指针调用 Derived 类中的 show() 函数。
3 实例
cpp
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() { // 虚函数
cout << "Base class show function called." << endl;
}
virtual ~Base() { // 虚析构函数,确保派生类对象能被正确析构
cout << "Base class destructor called." << endl;
}
};
class Derived : public Base {
public:
void show() override { // 重写基类的虚函数
cout << "Derived class show function called." << endl;
}
~Derived() override {
cout << "Derived class destructor called." << endl;
}
};
int main() {
Base* basePtr; // 基类指针
Derived derivedObj; // 派生类对象
basePtr = &derivedObj;
// 虽然basePtr是基类指针,但它指向派生类对象
// 因为show是虚函数,调用的是派生类的show函数
basePtr->show();
return 0;
}
- 输出
bash
Derived class show function called.
Derived class destructor called.
Base class destructor called.
- 解释
*- 在
Base
类中,我们声明了一个虚函数show()
。
-
- 在
Derived
类中,重写了这个虚函数。
- 在
-
Base
类的指针basePtr
指向Derived
类的对象时,通过该指针调用show()
函数时,实际调用的是Derived
类中的show()
,这是动态绑定的结果。
-
- 虚析构函数:
- 虚析构函数是确保派生类对象能够被正确析构的关键。如果基类指针指向派生类对象并且基类析构函数没有被声明为虚函数,派生类的析构函数将不会被调用,导致资源泄漏或未正确清理。
- 在本例中,基类的虚析构函数确保了派生类的析构函数能够被正确调用。
- 在
4 对象切割
对象切割指的是当派生类对象
被赋值给基类对象
时,派生类
特有的成员被"切掉",只保留基类的部分。
cpp
class Base {
public:
virtual void show() {
cout << "Base class show" << endl;
}
};
class Derived : public Base {
public:
void show() override {
cout << "Derived class show" << endl;
}
void derivedFunction() {
cout << "Derived class specific function" << endl;
}
};
int main() {
Derived derivedObj;
Base baseObj = derivedObj; // 对象切割发生
baseObj.show(); // 调用的是Base类的show,而不是Derived类的show
// baseObj.derivedFunction(); // 编译错误,因为基类没有该函数
return 0;
}
- 输出
bash
Base class show
- 解释
对象切割 :Base baseObj = derivedObj; 会导致对象切割。baseObj
只会保留Base
类的部分,Derived 类的部分被"切掉"了。因此,调用 baseObj.show() 时,实际上调用的是 Base 类的 show(),而不是 Derived 类的版本。
为了避免 对象切割,应该使用基类的指针
或引用
来存储派生类的对象。这样可以确保多态行为正确。
cpp
int main() {
Derived derivedObj;
Base* basePtr = &derivedObj; // 使用基类指针指向派生类对象
basePtr->show(); // 调用Derived类的show
return 0;
}
- 输出
bash
Derived class show