C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++虚函数与多态的实现原理

C++之虚函数与多态的实现原理分析

作者:一枝小雨

文章讲解了C++多态原理,通过虚函数表(虚表)实现动态绑定,探讨了单继承、多继承及菱形继承中的虚表结构,并指出虚函数不能是内联或静态函数,因需函数地址存储于虚表

1.多态的原理

1.1 虚函数表(简称虚表)

class Base
{
public:
        virtual void Func1()
        {
                cout << "Func1()" << endl;
        }
private:
        int _b = 1;
};

问:sizeof(Base) 是多少?

int main()
{
        Base a;
        cout << sizeof(a) << endl;    // 8
        return 0;
}

观察监视窗口,我们发现除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(一般是__vftptr,即virtual function table ptr)。

一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

针对上面的代码我们做出以下改造:

class Base
{
public:
        virtual void Func1(){cout << "Base::Func1()" << endl;}
        virtual void Func2(){cout << "Base::Func2()" << endl;}
        void Func3(){cout << "Base::Func3()" << endl;}

private:
        int _b = 1;
};
class Derive : public Base
{
public:
        virtual void Func1(){cout << "Derive::Func1()" << endl;}

private:
        int _d = 2;
};

int main()
{
        Base b;
        Derive d;
        return 0;
}

观察监控窗口的信息,我们可以发现:

1.2 虚函数表本质

虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个 nullptr 标志数组结束。

派生类的虚表生成:

1.3 多态的原理

多态是如何实现指向谁就调用谁的虚函数的?

在运行时,多态会到指向对象的虚表中查找要调用的虚函数的地址,父类对象的虚表中的虚函数指针指向的是父类虚函数,而子类对象的虚表中的虚函数指针指向的是子类重写后的虚函数。

class Person {
public:
        virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
        virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
        p.BuyTicket();
}
int main()
{
        Person Mike;
        Func(Mike);
        Student Johnson;
        Func(Johnson);
        return 0;
}

也就是说,调用函数时,他其实并不知道自己要调用的是子类还是父类的虚函数,他只需要到这个对象的虚表里面找就行了,找到是哪个就是哪个。如果是父类对象,那就直接通过虚表指针到虚表里找。

如果是子类对象,我们知道:

派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。

子类对象中的父类部分会被切割赋给 p,p 还是按照父类对象找虚函数的方式去虚表里找。这样就实现了不同对象去完成同一行为时,展现出不同的形态。

PS:满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到指向对象的虚函数表中查找对应的虚函数的地址。不满足多态的函数调用是编译时直接确定的,通过 p 的类型确定要调用函数的地址

1.4 动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为也称为静态多态,比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态
int i = 0;
double d = 1.1;
// 静态绑定 静态的多态(静态:编译时确定函数)
f1(i);
f1(d);

// 动态绑定 动态的多态(一般的多态指的就是动态多态)(动态:运行时去虚表找函数)
Base* p = new Base;
p->Func1();
p = new Derive;
p->Func1();

2. 单继承和多继承关系的虚函数表

需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类的虚表模型前面我们已经看过了,没什么需要特别研究的

2.1 单继承中的虚函数表

class Base {
public:
        virtual void func1() { cout << "Base::func1" << endl; }
        virtual void func2() { cout << "Base::func2" << endl; }
private:
        int a;
};
class Derive :public Base {
public:
        virtual void func1() { cout << "Derive::func1" << endl; }
        virtual void func3() { cout << "Derive::func3" << endl; }
        virtual void func4() { cout << "Derive::func4" << endl; }
private:
        int b;
};

int main()
{
        Base b;
        Derive d;

        return 0;
}

从监视窗口我们可以看到虚表的内容,但是我们 d 对象的 func3 和 func4 呢?监视窗口好像没有,实际上不是虚表中没有,而是监视窗口没有展示,它认为不需要展示,就没有显示出来。

我们可以自己打印虚表,看看到底有没有 func3 和 func4 :

typedef void(*VF_PTR)();   // 函数指针类型重定义
                           // 定义完成后可以使用 VF_PTR p;来创建一个函数指针对象

void PrintVFTable(VF_PTR* pTable)
{
        for (size_t i = 0; pTable[i] != 0; ++i)
        {
                printf("vfTable[%d]:%p->", i, pTable[i]);
                VF_PTR f = pTable[i];
                f();        // 调用函数指针指向的这个函数
        }
        cout << endl;
}

int main()
{
        Base b;
        Derive d;
        // 取对象中前四个字节存的虚表指针打印虚表
        PrintVFTable((VF_PTR*)(*(int*)&b));
        PrintVFTable((VF_PTR*)(*(int*)&d));

        return 0;
}

事实证明 func3 和 func4 确实存在于虚表中,我们成功打印并且调用了。

2.2 多继承中的虚函数表

typedef void(*VF_PTR)();        // 函数指针类型重定义
                                                        // 定义完成后可以使用 VF_PTR p;来创建一个函数指针对象

void PrintVFTable(VF_PTR pTable[])
{
        for (size_t i = 0; pTable[i] != 0; ++i)
        {
                printf("vfTable[%d]:%p->", i, pTable[i]);
                VF_PTR f = pTable[i];
                f();        // 调用函数指针指向的这个函数
        }
        cout << endl;
}

class Base1 {
public:
        virtual void func1() { cout << "Base1::func1" << endl; }
        virtual void func2() { cout << "Base1::func2" << endl; }
private:
        int b1;
};
class Base2 {
public:
        virtual void func1() { cout << "Base2::func1" << endl; }
        virtual void func2() { cout << "Base2::func2" << endl; }
private:
        int b2;
};
class Derive : public Base1, public Base2 {
public:
        virtual void func1() { cout << "Derive::func1" << endl; }
        virtual void func3() { cout << "Derive::func3" << endl; }
private:
        int d1;
};

int main()
{
        // base1虚表4 + int4 + base2虚表4 + int4 + int4 = 20字节
        cout << sizeof(Derive) << endl;    // 20
        
        Derive d;

        // base1 的虚表
        PrintVFTable((VF_PTR*)(*(int*)&d));
        // base2 的虚表
        PrintVFTable((VF_PTR*)(*(int*)((char*)&d + sizeof(Base1))));

        return 0;
}

2.3 菱形继承、菱形虚拟继承

实际中不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。

3. Q&A

3.1 内联函数为什么不能是虚函数?

内联函数会在调用位置直接展开,所以内联函数没有地址,也不需要地址,没有函数地址就无法放入虚表,所以内联函数不能是虚函数。

3.2 静态函数为什么不能是虚函数?

1. 调用方式与对象绑定的根本差异

静态函数:

虚函数:

2. 虚函数机制依赖于对象实例

虚函数的实现依赖于:

静态函数没有this指针,因此无法访问对象的vPtr,也就无法实现动态绑定。如果静态函数是虚的,编译器无法知道应该使用哪个类的虚函数表。

3.3 虚表?虚基表?虚基类?

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文