虚函数vptr,vtable与运行多态

最后更新于 2024-07-23 5731 次阅读


手机上整理的不够详细...当时被拷打的很难受,还是要好好学下学下这种基础的东西

原文

基础理论

vtable

为了实现虚函数,C ++使用一种称为虚拟表的特殊形式的后期绑定。该虚拟表是用于解决在动态/后期绑定方式的函数调用函数的查找表

虚拟表有时会使用其他名称,例如“vtable”,“虚函数表”,“虚方法表”或“调度表”。

虚拟表实际上非常简单,虽然用文字描述有点复杂。首先,每个使用虚函数的类(或者从使用虚函数的类派生)都有自己的虚拟表该表只是编译器在编译时设置的静态数组。虚拟表包含可由类的对象调用的每个虚函数的一个条目。此表中的每个条目只是一个函数指针指向该类可访问的派生函数

vptr

其次,编译器还会添加一个隐藏指向基类的指针,我们称之为vptr。vptr会在创建类实例时自动设置,以便指向该类的虚拟表。与this指针不同,this指针实际上是编译器用来解析自引用的函数参数,vptr是一个真正的指针。

因此,它使每个类对象的分配一个指针的大小。这也意味着vptr由派生类继承,这很重要。

实现与内部结构

C++的动态多态性是通过虚函数来实现的,通过virtual函数,指向子类的基类指针可以调用子类的函数。

具体实现看原文

其过程为:

  • 首先程序识别出fun1()是个虚函数
  • 其次程序使用pt->vptr来获取Derived的虚拟表
  • 第三,它查找Derived虚拟表中调用哪个版本的fun1()。这里就可以发现调用的是Derived::fun1()。因此pt->fun1()被解析为Derived::fun1()!

运行多态

虚函数的调用取决于指向或者引用的对象的类型,而不是指针或者引用自身的类型。

默认参数

默认参数是静态绑定的,虚函数是动态绑定的。 默认参数的使用需要看指针或者引用本身的类型(反过来了),而不是对象的类型

(重要)可以不可以

静态函数可以声明为虚函数吗?

原因主要有两方面:

静态函数不可以声明为虚函数,同时也不能被const 和 volatile关键字修饰

static成员函数不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义

虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,静态成员函数没有this指针,所以无法访问vptr。

构造函数可以为虚函数吗?

构造函数不可以声明为虚函数。同时除了inline|explicit之外,构造函数不允许使用其它任何关键字。

为什么构造函数不可以为虚函数?

尽管虚函数表vtable是在编译阶段就已经建立的,但指向虚函数表的指针vptr是在运行阶段实例化对象时才产生的。 如果类含有虚函数,编译器会在构造函数中添加代码来创建vptr。 问题来了,如果构造函数是虚的,那么它需要vptr来访问vtable,可这个时候vptr还没产生。 因此,构造函数不可以为虚函数。

我们之所以使用虚函数,是因为需要在信息不全的情况下进行多态运行。而构造函数是用来初始化实例的,实例的类型必须是明确的。 因此,构造函数没有必要被声明为虚函数。

析构函数可以为虚函数吗?

析构函数可以声明为虚函数。

如果我们需要删除一个指向派生类的基类指针时,应该把析构函数声明为虚函数。 事实上,只要一个类有可能会被其它类所继承, 就应该声明虚析构函数(哪怕该析构函数不执行任何操作)。

虚函数可以为私有函数吗?

基类指针指向继承类对象,则调用继承类对象的函数;

int main()必须声明为Base类的友元,否则编译失败。 编译器报错: ptr无法访问私有函数。 当然,把基类声明为public, 继承类为private,该问题就不存在了。

虚函数可以被内联吗?

通常类成员函数都会被编译器考虑是否进行内联。 但通过基类指针或者引用调用的虚函数必定不能被内联。 当然,实体对象调用虚函数或者静态调用时可以被内联,虚析构函数的静态调用也一定会被内联展开。

  • 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
  • 内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
  • inlline virtual唯一可以内联的时候是:编译器知道所调用的对象是哪个类,这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。

RTTI与dynamic_cast

RTTI(Run-Time Type Identification),通过运行时类型信息程序能够使用基类的指针或引用来检查这些指针或引用所指的对象的实际派生类型。

在面向对象程序设计中,有时我们需要在运行时查询一个对象是否能作为某种多态类型使用。与Java的instanceof,以及C#的as、is运算符类似,C++提供了dynamic_cast函数用于动态转型。相比C风格的强制类型转换和C++ reinterpret_cast,dynamic_cast提供了类型安全检查,是一种基于能力查询(Capability Query)的转换,所以在多态类型间进行转换更提倡采用dynamic_cast。

纯虚函数和抽象类

C++中的纯虚函数(或抽象函数)是我们没有实现的虚函数!我们只需声明它! 通过声明中赋值0来声明纯虚函数

  • 纯虚函数:没有函数体的虚函数
  • 抽象类:包含纯虚函数的类

抽象类只能作为基类来派生新类使用,不能创建抽象类的对象,抽象类的指针和引用->由抽象类派生出来的类的对象

实现抽象类

抽象类中:在成员函数内可以调用纯虚函数,在构造函数/析构函数内部不能使用纯虚函数。

如果一个类从抽象类派生而来,它必须实现了基类中的所有纯虚函数,才能成为非抽象类。

重要点

  • 纯虚函数使一个类变成抽象类
// 抽象类至少包含一个纯虚函数
class Base{
public:
    virtual void show() = 0; // 纯虚函数
    int getX() { return x; } // 普通成员函数

private:
     int x;
}; 
  • 抽象类类型的指针和引用
class Derived : public Base {
public:
    void show() { cout << "In Derived \n"; } // 实现抽象类的纯虚函数
    Derived(){} // 构造函数
};

int main(void)
{
    //Base b;  // error! 不能创建抽象类的对象
    //Base *b = new Base(); error!

    Base *bp = new Derived(); // 抽象类的指针和引用 -> 由抽象类派生出来的类的对象
    bp->show();
    return 0;
}
  • 如果我们不在派生类中覆盖纯虚函数,那么派生类也会变成抽象类
// Derived为抽象类
class Derived: public Base
{
public:
//    void show() {}
}; 
  • 抽象类可以有构造函数
// 抽象类
class Base {
    protected:
        int x;
    public:
        virtual void fun() = 0;
        Base(int i) { x = i; }  // 构造函数
};
// 派生类
class Derived: public Base
{
    int y;
public:
    Derived(int i, int j) : Base(i) { y = j; } // 构造函数
    void fun() { cout << "x = " << x << ", y = " << y; }
}; 
  • 构造函数不能是虚函数,而析构函数可以是虚析构函数
// 抽象类
class Base  {
public:
    Base(){ cout << "Constructor: Base" << endl; }
    virtual ~Base(){ cout << "Destructor : Base" << endl; }

    virtual void func() = 0;
};

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

    void func(){cout << "In Derived.func()." << endl;}
};

当基类指针指向派生类对象并删除对象时,我们可能希望调用适当的析构函数。 如果析构函数不是虚拟的,则只能调用基类析构函数。