📜  C++虚函数(1)

📅  最后修改于: 2023-12-03 14:59:52.476000             🧑  作者: Mango

C++虚函数

虚函数是 C++ 中实现多态的关键。它使得在运行时确定使用哪个函数,而不是在编译时就静态地绑定了。在一个基类中声明一个虚函数,派生类可以覆盖它,并且通过基类指针或引用来调用派生类的实现。

基础语法

在函数声明中添加关键字 virtual 来声明虚函数。并在派生类中使用 override 来覆盖基类中的虚函数。

class Base {
public:
    virtual void foo() { std::cout << "Base::foo()\n"; }
};

class Derived : public Base {
public:
    void foo() override { std::cout << "Derived::foo()\n"; }
};

注意事项:

  • 虚函数必须是成员方法,也就是必须被定义在类中。
  • 虚函数可以是纯虚函数,用 = 0 声明即可。
  • 参数列表、返回类型和 const 限定符与基类中的虚函数应该完全匹配。
  • 只有通过指针或引用调用函数时,虚函数才能起作用。
虚函数表

虚函数解决了多态问题,但是如何维护静态类型和动态类型之间的关系呢?答案就是虚函数表。

在每个对象中,包含一个指针指向其对应的虚函数表。虚函数表是一个包含所有虚函数指针的数组,存储在只读静态存储区域中。每个实现类都有一个对应的虚函数表,虚函数表的首地址即为对象中的指针。

当派生类没有覆盖基类中的虚函数时,就继承了基类的虚函数表。当派生类覆盖了基类中的虚函数,那么在派生类的虚函数表中使用覆盖的函数指针来替代基类的函数指针。

虚函数表指针

虚函数表指针是对象的成员之一,是一个指向虚函数表的指针。在类的对象创建时,虚函数表指针就被初始化了指向自己的虚函数表。

class Base {
public:
    virtual void foo() { std::cout << "Base::foo()\n"; }
};

class Derived : public Base {
public:
    void foo() override { std::cout << "Derived::foo()\n"; }
};

int main() {
    Base* ptr = new Derived();
    ptr->foo(); // 使用指针调用虚函数
    delete ptr;
}

在上面的代码中,变量 ptrBase 类型的指针,指向一个 Derived 类型的对象。使用指针调用虚函数时,实际上是使用它所指向的对象中的虚函数表指针来调用虚函数,进而调用到其所指向的实际对象类型的虚函数。

获得虚函数表地址

可以通过调试信息,从对象中获得虚函数表指针,从而得到该对象的虚函数表地址。

下面是一个简单的例子:

struct Base { virtual void f() {}; };
struct Derived : Base { virtual void f() {}; };

int main() {
    Derived d;
    auto vtable = *(std::intptr_t*)&d;
    auto handler = reinterpret_cast<Base*>(vtable);
    std::cout << typeid(*handler).name() << std::endl;
}

通过 *(std::intptr_t*)&d 得到一个包含虚函数表地址的 intptr_t 类型指针,进而利用 reinterpret_cast 将其转换为 Base 类型,得到对应的对象。在这个例子中,我们实现了一个反射的功能。

性能影响

在运行时动态确定调用的函数,一定程度上增加了函数调用的开销。因此,该机制会在一定程度上影响程序性能。

但是,在维护可读性和可扩展性方面虚函数带来的职业生涯长期积累相对于性能的损失,这项特性仍然是不可或缺的。

总结
  • 虚函数是 C++ 中实现多态的关键,它可以使得在运行时确定使用哪个函数。
  • 派生类可以覆盖基类的虚函数,并且通过基类指针或引用来调用派生类的实现。
  • 每个对象中包含一个指针指向其对应的虚函数表。虚函数表是一个包含所有虚函数指针的数组,存储在只读静态存储区域中。
  • 虚函数表指针是对象的成员之一,是一个指向虚函数表的指针。
  • 虚函数会增加函数调用的开销,需要在设计时权衡性能和可读性、可扩展性等因素。