C++中的virtual

368 阅读4分钟

在 C++ 中,virtual 关键字用于声明虚函数,表示该函数可以在派生类中被重写(override)。虚函数的主要作用是支持 多态(Polymorphism),即允许通过基类指针或引用调用派生类中重写的函数。

1. 虚函数的基本概念

当一个函数在基类中被声明为虚函数时,C++ 会为该函数提供一个机制,允许派生类重写该函数并且可以通过基类指针或引用调用派生类的实现。这样就实现了动态绑定,也就是在运行时决定调用哪个版本的函数。

2. virtual 的作用

  • 动态多态virtual 函数使得程序能够在运行时决定调用哪个函数,而不是在编译时就决定。
  • 支持函数重写:派生类可以重写基类的虚函数,以提供特定的实现。

3. 基本示例

#include <iostream>
using namespace std;

class Base {
public:
    virtual void speak() {  // 基类中的虚函数
        cout << "Base class speaking" << endl;
    }
};

class Derived : public Base {
public:
    void speak() override {  // 重写基类的虚函数
        cout << "Derived class speaking" << endl;
    }
};

int main() {
    Base* basePtr = new Derived();  // 基类指针指向派生类对象
    basePtr->speak();  // 调用的是 Derived 类中的 speak() 函数

    delete basePtr;
    return 0;
}

输出:

Derived class speaking

4. 如何工作:虚函数和多态

  • 在上面的示例中,speak() 函数在基类 Base 中被声明为 virtual。当我们通过 Base 类型的指针 basePtr 来调用 speak() 时,实际调用的是派生类 Derived 中重写的版本。这是因为 speak() 是虚函数,C++ 会根据对象的实际类型(即 Derived 类型)来选择合适的函数。

  • 如果没有使用 virtual,基类中的 speak() 会被调用,即使 basePtr 指向的是一个 Derived 类的对象。这样就没有实现多态。

5. 动态绑定与静态绑定的区别

  • 静态绑定:函数调用在编译时确定,函数的调用是基于编译时类型的。对于非虚函数,编译器根据指针或引用的声明类型来决定调用哪个函数。

  • 动态绑定:虚函数支持在运行时确定调用哪个函数。在运行时,程序根据指针或引用实际指向的对象类型来决定调用哪个版本的虚函数。

6. 虚函数表(Vtable)和虚函数指针(Vptr)

  • 每个类如果有虚函数,编译器会为该类生成一个虚函数表(Vtable)。这个表存储了该类的虚函数的地址。对于每个对象,编译器还会生成一个虚函数指针(Vptr),它指向该对象所属类的虚函数表。
  • 当通过基类指针调用虚函数时,程序会查找指针指向的对象的虚函数表,找到相应函数的地址并调用。

7. 虚析构函数

当一个类有虚函数时,通常该类也应该有虚析构函数。这是为了保证在删除基类指针指向派生类对象时,能够正确地调用派生类和基类的析构函数,避免内存泄漏或资源释放错误。

class Base {
public:
    virtual ~Base() {  // 虚析构函数
        cout << "Base class destructor" << endl;
    }
};

class Derived : public Base {
public:
    ~Derived() override {  // 重写析构函数
        cout << "Derived class destructor" << endl;
    }
};

int main() {
    Base* basePtr = new Derived();  // 基类指针指向派生类对象
    delete basePtr;  // 会调用 Derived 的析构函数,再调用 Base 的析构函数
    return 0;
}

输出:

Derived class destructor
Base class destructor

8. 虚函数的注意事项

  • 构造函数不能是虚函数:虽然类中的其他成员函数可以是虚函数,但构造函数不能是虚函数。构造函数是在对象创建时调用的,而虚函数是用于多态的,它需要依赖对象的完全构造。因此,构造函数不能进行多态调用。

  • 继承链中的虚函数:在继承层次中,派生类可以重写基类的虚函数。如果派生类没有重写某个虚函数,那么该虚函数在派生类中依然是虚的,并且会调用基类的版本。

  • 虚函数的性能开销:由于虚函数需要使用虚函数表来查找实际的函数地址,这可能会带来一些运行时开销,但通常这个开销是非常小的,不会影响性能。

总结

  • virtual 关键字用于声明虚函数,支持 多态
  • 通过基类指针或引用调用虚函数时,程序会在运行时根据对象的实际类型选择合适的函数版本。
  • virtual 使得类层次结构中的函数可以被重写,支持动态绑定。
  • 使用虚函数时,通常需要考虑虚析构函数,以确保正确的资源释放。

虚函数是面向对象编程中实现多态和灵活性的重要机制,广泛应用于设计模式、插件架构、事件处理等场景中。