2292 字
11 分钟
虚函数:C++ 多态的核心

前言#

在面向对象编程中,多态是一种强大的特性,它允许我们通过基类的接口调用派生类的实现。C++ 中的虚函数(Virtual Function)是实现多态的关键机制。虚函数的设计理念源自于“重载函数”,但它们的目标和实现方式有所不同。重载函数主要用于编译时的多态(静态绑定),而虚函数则用于运行时的多态(动态绑定)。

虚函数的实现依赖于一个隐藏的数据结构——虚表(Virtual Table,简称 vtable)。虚表是一个函数指针数组,存储了类中所有虚函数的地址。每个对象都有一个指向其类虚表的指针(vptr),通过 vptr 可以访问虚表。

理解虚函数和虚表的工作原理,对于深入掌握 C++ 的对象模型和性能优化至关重要。


虚函数的设计理念#

虚函数的设计理念源自于“重载函数”,但它们的目标和实现方式有所不同:

  • 重载函数(Overloading)

    • 目标:通过相同的函数名和不同的参数列表,提供多种函数实现。
    • 机制:编译时多态(静态绑定)。编译器根据函数的参数类型和数量选择正确的函数版本。
    • 示例
      void print(int x) { cout << "print(int)" << endl; }
      void print(double x) { cout << "print(double)" << endl; }
  • 虚函数(Virtual Function)

    • 目标:通过基类的接口调用派生类的实现,实现运行时多态(动态绑定)。
    • 机制:运行时多态。通过虚表(vtable)和虚指针(vptr)动态选择正确的函数版本。
    • 示例
      class Base {
      public:
          virtual void print() { cout << "Base::print()" << endl; }
      };
      
      class Derived : public Base {
      public:
          void print() override { cout << "Derived::print()" << endl; }
      };

虚函数的设计理念是为了解决以下问题:

  • 如何在运行时动态选择函数的实现?
  • 如何通过基类的指针或引用调用派生类的函数?

虚函数的定义与使用#

虚函数的定义#

虚函数通过在函数声明前加上 virtual 关键字来定义。例如:

class Base {
public:
    virtual void print() { cout << "Base::print()" << endl; }
};

虚函数的声明表示该函数可以在派生类中被重写(override)。如果派生类没有重写虚函数,则会继承基类的实现。

虚函数的使用#

虚函数的使用依赖于基类的指针或引用。通过基类的指针或引用调用虚函数时,实际调用的是派生类的实现(如果派生类重写了该虚函数)。这种机制称为动态绑定(Dynamic Binding)。

class Base {
public:
    virtual void print() { cout << "Base::print()" << endl; }
};

class Derived : public Base {
public:
    void print() override { cout << "Derived::print()" << endl; }
};

int main() {
    Base* ptr = new Derived();
    ptr->print(); // 输出 Derived::print()
    delete ptr;
    return 0;
}

虚函数的继承#

当派生类继承自基类时,派生类会继承基类的虚函数。如果派生类重写了基类的虚函数,虚表中的相应条目会被更新为派生类的函数地址。如果派生类没有重写某些虚函数,虚表中的条目则保持基类的函数地址。

虚函数的覆盖(Override)#

派生类可以通过重写基类的虚函数来覆盖其行为。使用 override 关键字可以显式地表示覆盖:

class Derived : public Base {
public:
    void print() override { cout << "Derived::print()" << endl; }
};

虚函数的隐藏(Hide)#

如果派生类定义了一个与基类虚函数同名但参数列表不同的函数,这将隐藏基类的虚函数,而不是覆盖它。例如:

class Base {
public:
    virtual void print(int x) { cout << "Base::print(int)" << endl; }
};

class Derived : public Base {
public:
    void print() { cout << "Derived::print()" << endl; } // 隐藏了 Base::print(int)
};

纯虚函数与抽象类#

纯虚函数#

纯虚函数是一种特殊的虚函数,它没有具体的实现,仅在基类中声明。纯虚函数的语法如下:

class Base {
public:
    virtual void print() = 0; // 纯虚函数
};

纯虚函数的定义表示该函数必须在派生类中被实现。如果派生类没有实现纯虚函数,则该派生类也是抽象类。

抽象类#

包含纯虚函数的类称为抽象类。抽象类不能被实例化,只能作为基类被派生。例如:

class Base {
public:
    virtual void print() = 0; // 纯虚函数
};

class Derived : public Base {
public:
    void print() override { cout << "Derived::print()" << endl; }
};

int main() {
    // Base b; // 错误!抽象类不能被实例化
    Derived d;
    d.print(); // 输出 Derived::print()
    return 0;
}

纯虚函数的实现#

虽然纯虚函数没有默认实现,但可以在基类中提供一个默认实现。派生类可以选择性地覆盖该实现。例如:

class Base {
public:
    virtual void print() = 0 {
        cout << "Base::print()" << endl;
    }
};

class Derived : public Base {
public:
    void print() override { cout << "Derived::print()" << endl; }
};

虚表(Virtual Table)的实现#

虚表是 C++ 编译器为每个包含虚函数的类生成的一个隐藏的数据结构。虚表是一个函数指针数组,存储了类中所有虚函数的地址。每个对象都有一个指向其类虚表的指针(vptr),通过 vptr 可以访问虚表。

虚表的结构#

假设我们有以下类定义:

class Base {
public:
    virtual void f1() { cout << "Base::f1" << endl; }
    virtual void f2() { cout << "Base::f2" << endl; }
};

class Derived : public Base {
public:
    void f1() override { cout << "Derived::f1" << endl; }
    void f2() override { cout << "Derived::f2" << endl; }
};

编译器会为 BaseDerived 类生成虚表,其结构如下:

  • Base 类的虚表

    • Base::f1 的地址
    • Base::f2 的地址
  • Derived 类的虚表

    • Derived::f1 的地址
    • Derived::f2 的地址

虚表的内存布局#

假设 BaseDerived 类的对象在内存中的布局如下:

  • Base 对象

    • vptr(指向 Base 的虚表)
    • 其他成员变量
  • Derived 对象

    • vptr(指向 Derived 的虚表)
    • 其他成员变量

当通过基类指针调用虚函数时,程序通过 vptr 查找虚表,找到对应的函数地址并调用。这种机制确保了多态的实现。

虚表的初始化#

当对象被创建时,构造函数会初始化对象的 vptr,使其指向正确的虚表。如果对象是派生类的实例,vptr 会被初始化为指向派生类的虚表。

虚表的继承#

当派生类继承自基类时,派生类会继承基类的虚表。如果派生类重写了基类的虚函数,虚表中的相应条目会被更新为派生类的函数地址。如果派生类没有重写某些虚函数,虚表中的条目则保持基类的函数地址。

虚表的大小#

虚表的大小取决于类中虚函数的数量。每个虚函数占用一个指针大小的空间(通常是 4 或 8 字节,取决于平台)。

虚表的性能开销#

虽然虚表引入了一定的性能开销(主要是通过 vptr 查找虚表的额外操作),但这种开销通常是可以接受的。虚表的查找时间是常数时间 (O(1)),因此虚函数的调用效率仍然很高。


虚函数的高级用法#

虚析构函数#

虚析构函数是虚函数的一个重要应用。当通过基类指针删除派生类对象时,需要确保调用派生类的析构函数。通过将析构函数声明为虚函数,可以实现这一点。例如:

class Base {
public:
    virtual ~Base() { cout << "Base destructor" << endl; }
};

class Derived : public Base {
public:
    ~Derived() { cout << "Derived destructor" << endl; }
};

int main() {
    Base* ptr = new Derived();
    delete ptr; // 输出 Derived destructor 和 Base destructor
    return 0;
}

虚函数与模板#

虚函数和模板可以结合使用,但需要注意模板函数不能是虚函数。例如:

class Base {
public:
    virtual void print() { cout << "Base::print()" << endl; }
};

class Derived : public Base {
public:
    void print() override { cout << "Derived::print()" << endl; }
};

template <typename T>
void print(T* obj) {
    obj->print();
}

int main() {
    Base* ptr = new Derived();
    print(ptr); // 输出 Derived::print()
    delete ptr;
    return 0;
}

小结#

虚函数是 C++ 中实现多态的核心机制。通过虚函数,派生类可以重写基类的函数实现,从而实现动态绑定。虚函数的设计理念源自于“重载函数”,但它们的目标和实现方式有所不同。虚函数的实现依赖于虚表(vtable)和虚指针(vptr)。

纯虚函数和抽象类是虚函数的高级应用,它们允许我们定义接口类,确保派生类实现特定的函数。虚表是虚函数实现的基础,它存储了类中所有虚函数的地址。每个对象都有一个指向其类虚表的指针(vptr),通过 vptr 可以访问虚表。

理解虚函数和虚表的工作原理,有助于我们更好地掌握 C++ 的对象模型和性能优化。

虚函数:C++ 多态的核心
https://loners.site/posts/cpp-虚函数/
作者
Mongxin Chan
发布于
2024-11-23
许可协议
CC BY-NC-SA 4.0