C++进阶:详解多态(多态、虚函数、抽象类以及虚函数原理详解)

C/C++
173
0
0
2024-06-13
  • 在虚函数的继承中,派生类继承了基类的虚函数的接口,即函数的声明。
  • 派生类必须重新实现基类的虚函数,并且可以通过多态性实现运行时的动态绑定。
  • 这种继承关系强调了对函数接口的统一定义,为实现多态提供了基础。

4.虚函数原理

4.1虚函数表(vtable)

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

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

通过观察测试我们发现b对象中除了 _base**成员,还多一个** _vfptr 放在前面(注意有些平台可能会放到对象的最后面,跟平台有关),对象中的这个指针我们叫做虚函数表指针 (v代表 virtual,f代表 function)。 一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中(其实是一个函数指针数组),虚函数表也简称虚表

利用下面的代码进一步讨论:

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 _base = 1;
};

class Derive : public Base
{
public:
    virtual void Func1()
    {
        cout << "虚函数Derive::Func1()" << endl;
    }
private:
    int _derive = 2;
};

int main()
{
    Base bb;
    Derive dd;

    return 0;
}


  1. 虚函数表(Virtual Function Table)是在编译期间生成的。编译器在编译每个包含虚函数的类时,会在该类的内部生成一个虚函数表,其中包含了该类中所有虚函数的地址。这个过程是在编译期间静态地完成的,因为编译器可以确定每个类中虚函数的数量和排列顺序。
  2. 虚函数表指针(vptr)的赋值是在对象的构造函数中完成的。具体来说,当创建一个对象时,首先会调用该对象的构造函数,而构造函数的初始化列表是在对象实际构造之前执行的。

在构造函数的初始化列表中,虚表指针(vptr)会被赋值为该类的虚函数表的首地址。这样,在对象的构造期间,虚表指针就已经指向了正确的虚函数表,从而确保在对象的构造期间就可以正确调用虚函数

  1. 派生类对象dd中也有一个虚表指针,dd对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在这一部分,另一部分是自己的成员
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以dd的虚表中存的是重写的**Derive::Func1**,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法
  3. Func2**继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表**
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr(这个也是看平台)
  5. 总结一下派生类的虚表生成:
  • 先将基类中的虚表内容拷贝一份到派生类虚表中
  • 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
  • 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
  • 虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的存的是虚表指针。那么虚表其实在vs下是也存在代码段的

4.2多态的原理

class Person {
public:
    virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person {
public:
    virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

void Func(Person& p)
{
    p.BuyTicket();
}

void test4()
{
    Person Mike;
    Func(Mike);

    Student Johnson;
    Func(Johnson);
}

int main()
{
    test4();
    return 0;
}

  1. 观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。
  2. 观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。
  3. 满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象中找的。不满足多态的函数调用时编译时确认好的

其实这里还是利用了切割,编译器看到的都是父类,不过指向子类时里的父类是切割过去的而已,里面的虚表也是子类覆盖后的,找到的地址也是子类的虚函数的

两个问题:

  • 基类对象的指针 / 引用调用虚函数的原理是什么? 基类指针或引用调用虚函数时,编译器生成的机器代码确实会先访问对象的虚函数指针(vptr),再通过虚函数表(vtable)找到实际要调用的虚函数的地址,最终进行调用。这种动态绑定的过程使得程序在运行时能够根据对象的实际类型来调用正确的虚函数,实现了多态性
  • 为什么多态必须要用基类的指针 / 引用来调用虚函数,而用基类对象调用却不行 当派生类对象赋值给基类对象时,只会拷贝对象中的数据成员,而不会拷贝派生类的虚表指针。因此,基类对象中的虚函数调用会绑定到基类的虚函数表上,而无法访问派生类的虚函数。 多态必须使用基类的指针/引用来调用虚函数的原因主要是因为基类指针/引用可以在运行时指向派生类对象,而且能正确地调用派生类的虚函数。这是因为在运行时,基类指针/引用会指向实际对象的虚表,从而实现了动态绑定,根据对象的实际类型调用正确的虚函数

4.3动态绑定与静态绑定


  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,

比如:函数重载

  1. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

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

在单继承和多继承关系中,下面来主要研究的是派生类对象的虚表模型,因为基类的虚表模型没什么需要特别研究的

5.1单继承中的虚函数表

class A {
public:
    virtual void func1()
    {
        cout << "A::func1" << endl;
    }
    virtual void func2()
    {
        cout << "A::func2" << endl;
    }
private:
    int _a;
};

class B :public A {
public:
    virtual void func1()
    {
        cout << "B::func1" << endl;
    }
    //这里B自己又多加了两个虚函数
    virtual void func3()
    {
        cout << "B::func3" << endl;
    }
    virtual void func4()
    {
        cout << "B::func4" << endl;
    }
private:
    int _b;
};

int main()
{
    B bb;
    return 0;
}

我们也可利用下面函数来打印虚表内容进行验证

typedef void(*vfptr)();

void printvf(vfptr* ptr)
{
 for (int i = 0; ptr[i] != nullptr; ++i)//以空指针结束
 {
     // 依次打印虚表各元素
     printf(" 第%d个虚函数地址 :%p", i+1, ptr[i]);
     // 把虚表各元素赋值给函数指针f
     vfptr f = ptr[i];
     // 调用函数
     f();
 }
 cout << endl;
}

int main()
{
 B bb;
 printvf((vfptr*)(*(int*)&bb));
 //先&bb地址出来,因为vs下虚表指针在最前面四个字节,利用强转为int*取到前四个字节
 //然后*解引用得到地址,再强转为vfptr* ,进行调用函数
 return 0;
}

5.2多继承中的虚函数表

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; }
    //自己又加上两个fun3,会在哪个虚表里填上呢
    virtual void func3() { cout << "Derive::func3" << endl; }
private:
    int d1;
};

int main()
{
    Derive dd;
    Base1* b1 = &dd;
    Base1* b2 = &dd;
    printvf((vfptr*)(*(int*)b1));

    printvf((vfptr*)(*(int*)b2));
    return 0;
}

是有两个虚表的,一个基类一个

观察上图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

好啦,这次知识的内容就先到这里啦!多态在笔试当中选择题经常考察,在面试中也会问。以后大概率会对这部分进行梳理,感谢大家支持!!!