【C/C++ 多态的关键】【虚函数表】

文章目录

  • C++虚函数表
  • 1、引言
  • 2、概述
  • 3、特点
  • 4、虚表指针
  • 5、动态绑定
    • 5.1、动态绑定的三个条件
    • 5.2、动态绑定的流程
    • 5.3、Upcasting的相关概念
  • 6、虚函数表对于多态的重要性
  • 7、虚析构函数的重要性
  • 8、虚函数表如何影响程序性能
  • 9、代码示例:使用虚函数表实现多态
  • 10、多重继承下虚函数表的结构和实现
    • 10.1、虚函数表结构
    • 10.2、类层次结构设计影响
      • 10.2.1性能考虑
      • 10.2.2代码维护
      • 10.2.3避免菱形继承问题
  • 11、总结
  • 12、参考文章

C++虚函数表

1、引言

为了实现C++的多态,C++使用了一种动态绑定的技术,而这个技术的核心是虚函数表。每个包含了虚函数的类都包含一个虚函数表,同一个类的对象共同使用同一张虚表。

2、概述

类的虚函数表是一块连续的内存,每个内存单元中记录一个JMP指令的地址。
注意的是,编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享。类的每个虚成员占据虚函数表中的一行。如果类中有N个虚函数,那么其虚函数表将有N*4字节的大小。
虚函数(Virtual Function)是通过一张虚函数表来实现的。简称为V-Table。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。这样,在有虚函数的类的实例中分配了指向这个表的指针的内存,所以,当用父类的指针来操作一个子类的时候,这张虚函数表就显得尤为重要了,它就像一个地图一样,指明了实际所应该调用的函数。

3、特点

  • 每一个基类都会有自己的虚函数表
  • 派生类的虚函数表的顺序,和继承时的顺序相同;
  • 派生类自己的虚函数放在第一个虚函数表的后面,顺序也是和定义时顺序相同;
  • 对于派生类如果要覆盖父类中的虚函数,那么会在虚函数表中代替其位置;
  • 没有虚函数的C++类,是不会有虚函数表的;
  • 虚函数表是在编译过程中创建的

稍微总结一下,虚函数在虚函数表中的顺序与定义时的顺序相同,后继有虚函数,顺次写入,因为虚函数表是一块连续的内存,所以虚函数表中虚函数的位置也是连续的;
虚函数表是和虚函数关联的,有虚函数则有虚函数表,否测没有。

4、虚表指针

虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表
为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。
【C/C++ 多态的关键】【虚函数表】_第1张图片
上面指出,一个继承类的基类如果包含虚函数,那个这个继承类也有拥有自己的虚表,故这个继承类的对象也包含一个虚表指针,用来指向它的虚表。

5、动态绑定

5.1、动态绑定的三个条件

  • 通过指针来调用函数
  • 指针upcast向上转型
  • 调用的是虚函数

5.2、动态绑定的流程

  • 取出类的虚函数表的地址
  • 根据虚函数表的地址找到虚函数表
  • 根据找到的虚函数的地址调用虚函数

5.3、Upcasting的相关概念

在C++中,upcast是指将派生类对象的指针或引用转换为基类指针或引用的过程。这是C++中实现多态的一种方式。通过upcast,我们可以使用基类指针或引用访问派生类对象的成员,同时保留了多态性。
当我们将一个派生类对象的指针或引用转换为基类指针或引用时,这个过程被称为upcast。upcast可以在运行时动态地进行,因为编译器不知道实际指向的对象是什么类型。因此,upcast允许我们使用基类指针或引用访问派生类对象的成员,同时保留了多态性。
需要注意的是,upcast不会改变实际指向的对象,只是改变了指针或引用的类型。因此,在使用upcast时需要小心处理对象的实际类型和指针或引用的类型之间的关系,以避免出现类型不匹配的问题。
upcast使用例子:

#include   
  
class Animal {  
public:  
    virtual void sound() {  
        std::cout << "Animal makes a sound" << std::endl;  
    }  
};  
  
class Dog : public Animal {  
public:  
    void sound() override {  
        std::cout << "Dog barks" << std::endl;  
    }  
};  
  
class Cat : public Animal {  
public:  
    void sound() override {  
        std::cout << "Cat meows" << std::endl;  
    }  
};  
  
int main() {  
    Dog dog;  
    Cat cat;  
    Animal* animalPtr1 = &dog; // upcast from Dog to Animal  
    Animal* animalPtr2 = &cat; // upcast from Cat to Animal  
    animalPtr1->sound(); // output: Dog barks  
    animalPtr2->sound(); // output: Cat meows  
    return 0;  
}

在上面的例子中,我们定义了一个基类Animal和两个派生类Dog和Cat。Dog和Cat都重写了sound()函数,以实现不同的声音输出。在main()函数中,我们创建了一个Dog对象和一个Cat对象,并将它们的地址分别存储在animalPtr1和animalPtr2指针变量中。这两个指针变量都是Animal类型的指针,因此我们可以使用它们来调用sound()函数。在调用时,由于animalPtr1指向的是Dog对象,而animalPtr2指向的是Cat对象,所以它们分别输出了"Dog barks"和"Cat meows"。这个过程就是通过upcast实现的。我们将派生类对象的指针转换为基类指针,然后使用基类指针来访问派生类对象的成员函数,实现了多态性。
总之,upcast是C++中实现多态的一种方式,它可以将派生类对象的指针或引用转换为基类指针或引用,以便在运行时动态地访问派生类对象的成员。

6、虚函数表对于多态的重要性

虚函数表在实现C++多态中起着至关重要的作用。多态是面向对象编程的一个核心特性,它允许我们使用基类指针或引用操作派生类对象,并根据对象的实际类型动态调用适当的成员函数。这种运行时的动态调用机制使得代码更加灵活和可扩展,从而有助于创建更具可维护性和可重用性的程序。
虚函数表(Virtual Function Table,简称V-Table)正是支持多态的关键所在。当一个类包含虚函数时,编译器会为该类生成一个虚函数表。虚函数表是一个包含指向虚函数地址的指针数组,其中每个指针都对应一个虚函数。同时,编译器还会在类的对象中添加一个指向虚函数表的指针,称为虚表指针。
在运行时,当我们使用基类指针或引用来调用虚函数时,程序会通过虚表指针找到正确的虚函数表。接着,它会根据虚函数在虚函数表中的索引定位到相应的虚函数地址,并调用该函数。这个过程称为动态绑定,它使得程序能够根据对象的实际类型来决定调用哪个类的成员函数。
举个简单的例子,假设我们有一个基类Shape和两个派生类Circle和Rectangle。假设Shape类中有一个虚函数area(),Circle和Rectangle类都重写了这个函数。当我们使用一个Shape指针或引用来调用area()函数时,虚函数表使得程序能够根据指针或引用指向的实际对象类型来调用Circle或Rectangle类的area()函数。这样一来,我们就可以在不了解具体对象类型的情况下,编写处理不同类型对象的通用代码。

7、虚析构函数的重要性

虚析构函数在面向对象编程中起到了关键作用,特别是在涉及继承关系的类中。虚析构函数允许基类析构函数在删除派生类对象时正确调用,这有助于防止资源泄漏和其他潜在问题。以下是虚析构函数的重要性的详细解释:

  • 防止资源泄漏
    派生类可能会分配额外的资源,例如动态内存。当通过基类指针删除派生类对象时,如果基类的析构函数不是虚函数,那么只有基类的析构函数会被调用,而派生类的析构函数不会被调用。这可能导致派生类中分配的资源无法正确释放,从而引发资源泄漏。
class Base {
public:
    // 没有使用virtual关键字
    ~Base() { cout << "Base destructor called." << endl; }
};

class Derived : public Base {
public:
    ~Derived() {
        cout << "Derived destructor called." << endl;
        // 释放派生类分配的资源
    }
};

int main() {
    Base* basePtr = new Derived();
    delete basePtr; // 只会调用基类的析构函数,导致资源泄漏
    return 0;
}
  • 保持多态行为一致
    在面向对象编程中,多态是一个关键概念。当使用指向基类的指针或引用来操作派生类对象时,我们希望能够在运行时正确地调用派生类的成员函数。将基类的析构函数声明为虚函数,可以确保在删除派生类对象时,析构函数的行为与其他虚成员函数的行为保持一致。
class Base {
public:
    // 使用virtual关键字
    virtual ~Base() { cout << "Base destructor called." << endl; }
};

class Derived : public Base {
public:
    ~Derived() {
        cout << "Derived destructor called." << endl;
        // 释放派生类分配的资源
    }
};

int main() {
    Base* basePtr = new Derived();
    delete basePtr; // 调用派生类的析构函数,然后调用基类的析构函数
    return 0;
}

总之,虚析构函数是一个重要的概念,可以确保在删除派生类对象时正确地调用基类和派生类的析构函数,从而避免资源泄漏。在设计涉及继承关系的类时,如果预计基类可能被作为接口使用,那么将基类的析构函数声明为虚函数是一种良好的编程实践。

8、虚函数表如何影响程序性能

说到对程序性能的影响,这是相对于模板来说的。虽然虚函数表实现了多态,但是因为虚函数表本身是一块连续的内存,在存储与运行时就会有多余的开销。

  • 内存开销:每个包含虚函数的类都会有一个虚函数表,虚函数表中存储了指向虚函数的指针。此外每个包含虚函数的类的对象都会有一个指向虚函数表的指针。这意味着每个对象的大小都会增加,因为他需要额外的空间来存储虚函数表指针。同事,虚函数表本身也会占用内存。
  • 运行开销:当调用一个虚函数时,编译器需要通过虚函数表来查找正确的函数地址,然后才能调用他。这个过程比直接调用非虚函数要慢,因为需要额外的间接寻址操作。虽然这个开销相对较小,但是在性能关键的场景中,这可能会成为一个瓶颈。

【C/C++ 多态的关键】【虚函数表】_第2张图片
虚函数表并不是唯一的选择,也可以考虑使用模板来实现多态。模板在编译时实现多态,不需要在运行时运行时查找虚函数表,以下是模板实现多态的一些优势。

  • 编译时多态:模板是一种编译时多态技术,这意味着函数调用是在编译时确定的,而不是在运行时调用,这样可以消除运行时的间接寻址开销。
  • 无额外内存开销:模板在编译过程中是实例化的,所以不需要虚函数表和虚函数表指针。这样减少了内存的开销。

模板也有一些缺点,如代码膨胀(每个模板实例都会生成一份代码),编译时间增加。因此在选择时要权衡利弊。
虚函数表确实会对程序性能产生一定的影响,但是在大多数情况下,这种开销是负担得起的。在对性能要求比较苛刻的情况下,可以考虑使用其他技术,例如模板。

9、代码示例:使用虚函数表实现多态

#include 

// 基类 Shape
class Shape {
public:
    virtual void area() const {
        std::cout << "This is the area method in the base class Shape." << std::endl;
    }
};

// 派生类 Circle
class Circle : public Shape {
public:
    void area() const override {
        std::cout << "This is the area method in the derived class Circle." << std::endl;
    }
};

// 派生类 Rectangle
class Rectangle : public Shape {
public:
    void area() const override {
        std::cout << "This is the area method in the derived class Rectangle." << std::endl;
    }
};

void printArea(const Shape& shape) {
    shape.area(); // 调用虚函数,实现动态绑定
}

int main() {
    Shape shape;
    Circle circle;
    Rectangle rectangle;

    printArea(shape);      // 输出:This is the area method in the base class Shape.
    printArea(circle);     // 输出:This is the area method in the derived class Circle.
    printArea(rectangle);  // 输出:This is the area method in the derived class Rectangle.

    return 0;
}

在上面的示例中,我们有一个基类Shape和两个派生类Circle和Rectangle。基类Shape中定义了一个虚函数area(),而派生类Circle和Rectangle都重写了这个函数。
我们定义了一个printArea函数,该函数接受一个Shape引用作为参数。在printArea函数中,我们调用了area()函数,由于area()是虚函数,这里的调用会实现动态绑定。
在main函数中,我们创建了Shape、Circle和Rectangle对象,并分别将它们传递给printArea函数。虽然printArea函数接受的参数类型是Shape引用,但由于多态的存在,我们可以传递Circle和Rectangle对象。
当我们调用printArea函数时,程序会根据传入对象的实际类型,通过虚函数表来动态调用合适的成员函数。因此,printArea(shape)会调用基类Shape的area()函数,printArea(circle)会调用派生类Circle的area()函数,而printArea(rectangle)会调用派生类Rectangle的area()函数。

10、多重继承下虚函数表的结构和实现

在C++中,多重继承允许一个派生类继承多个基类。在这种情况下,虚函数表的结构和实现将更加复杂。本段将深入探讨多重继承下虚函数表的结构和实现,以及这对程序员在设计类层次结构时的影响。

10.1、虚函数表结构

在多重继承的情况下,派生类需要维护一个虚函数表数组。数组中的每个元素都指向一个虚函数表,这些虚函数表分别对应每个基类。由于派生类需要处理多个基类的虚函数,虚函数表数组的顺序与基类的声明顺序相同。
以下是一个简单的多重继承示例,说明虚函数表数组的结构:

class Base1 {
public:
    virtual void func1() {}
    virtual void func2() {}
};

class Base2 {
public:
    virtual void func3() {}
    virtual void func4() {}
};

class Derived : public Base1, public Base2 {
public:
    virtual void func1() override {}
    virtual void func4() override {}
};

在这个例子中,Derived 类继承了 Base1 和 Base2。Derived 类将有两个虚函数表,一个来自 Base1,另一个来自 Base2。Derived 类覆盖了基类的部分虚函数。最终,Derived 类的虚函数表数组如下:

Derived::vftable[0]:
    Derived::func1()
    Base1::func2()

Derived::vftable[1]:
    Base2::func3()
    Derived::func4()

10.2、类层次结构设计影响

10.2.1性能考虑

由于多重继承引入了额外的虚函数表指针和数组,这可能会导致额外的内存开销和间接访问成本。因此,程序员需要在性能和设计灵活性之间做出权衡。

10.2.2代码维护

在多重继承的情况下,代码的维护可能变得更加复杂。程序员需要仔细跟踪每个基类的虚函数,以确保正确覆盖和调用它们。此外,修改基类可能会影响多个派生类,需要谨慎处理。

10.2.3避免菱形继承问题

在多重继承中,可能会遇到菱形继承问题,即一个类从两个或多个类继承,而这些类又从同一个基类继承。这会导致二义性和资源浪费。为解决这个问题,C++在多重继承下,派生类可能继承多个基类,这样就需要为每个基类创建一个虚函数表。因此,派生类的虚函数表数量取决于它继承的基类数量。

11、总结

书山有路勤为径,学海无涯苦作舟。

12、参考文章

11.1【C/C++ 多态核心 】C++虚函数表:让多态成为可能的关键
11.2 虚函数表详解

你可能感兴趣的:(#,C++多态的精彩实现,c语言,c++)