前言
我们知道C++多态实现有两个条件——一是基类的指针或引用调用虚函数,另一个是基类中有虚函数并且在派生类中实现虚函数重写;这两个条件缺一不可,这与多态实现的底层原理有关,今天我们就来学习一下多态实现的原理🥳🥳
目录
1.虚函数表
虚函数表(Virtual Function Table,VTable)是C++中实现动态多态性的一种机制。每个包含虚函数的类都有一个对应的虚函数表,用于存储该类的虚函数地址。
虚函数表是一个包含函数指针的数组,每个函数指针指向相应虚函数的实现。
例如:
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
cout << sizeof(Base) << endl;
return 0;
}
如下图所示:
注意是虚函数表的指针,而不是直接存储虚函数表。
2.派生类中的虚表
如果基类中没有虚函数,派生类中有虚函数,那么它的虚函数表和上面的一致。
例如:
class Base
{
private:
int _b = 1;
};
class Derive :public Base
{
public:
virtual void Func2()
{
cout << "Func2()" << endl;
}
private:
int _d = 2;
};
int main()
{
Derive d;
return 0;
}
如下图所示:
但是如果基类中有虚函数表,那么派生类该如何继承呢?
情况一:只有基类有虚函数,派生类没有
例如:
//情况一:只有基类有虚函数
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
class Derive :public Base
{
private:
int _d = 2;
};
int main()
{
Derive d;
return 0;
}
情况二:基类和派生类中都有虚函数,并且虚函数没有被重写
例如:
//情况二:基类和派生类中都有虚函数,并且虚函数没有被重写
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
class Derive :public Base
{
public:
virtual void Func2()
{
cout << "Func2()" << endl;
}
private:
int _d = 2;
};
int main()
{
Derive d;
return 0;
}
虚函数的地址存放在虚函数表中,而对象中前四个字节存放的是虚函数表的指针,所以我们可以使用强制类型转换取出对象的前四个字节,但是int
类型与Base
和Derive
类型不兼容,不能相互转换,但是指针之间可以相互转换,所以我们考虑先取Base
和Derive
类对象的地址然后强制转换成int*
类型,然后再解引用就得到了虚函数表的地址🥳🥳
代码如下:
//情况二:基类和派生类中都有虚函数,并且虚函数没有被重写
//基类和派生类代码如上
//先定义一个函数指针类型
typedef void(*VFPTR) ();
//打印函数指针数组中存放的函数地址
void PrintVTable(VFPTR vTable[])
{
// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; i++)
{
printf(" 第%d个虚函数地址 :0X%x->", i, vTable[i]);
VFPTR f = vTable[i];
f();//调用该函数
}
cout << endl;
}
void test()
{
Base b;
Derive d;
//打印b对象中虚函数的地址
VFPTR* vTableb = (VFPTR*)(*(int*)&b);
PrintVTable(vTableb);
//打印d对象中虚函数的地址
VFPTR* vTabled = (VFPTR*)(*(int*)&d);
PrintVTable(vTabled);
}
结果如下:
综上所述,如果派生类和基类都定义了自己的虚函数,并且基类的虚函数没有在派生类中重写的话,那么派生类中虚函数的地址会存放在派生类继承的基类那部分的虚函数表中的末尾,并且基类定义的对象和派生类定义的对象的虚函数表的地址是不同的。
同一类定义的不同对象使用的基函数表是同一个。
如下图所示:
情况三:基类中定义虚函数,并且派生类中对该虚函数进行了重写
例如:
//情况三:基类中定义虚函数,并且派生类中对该虚函数进行了重写
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
private:
int _b = 1;
};
class Derive :public Base
{
public:
virtual void Func1()//重写虚函数Fun1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
结果如下图:
综合这三种情况
- 如果基类没有虚函数,子类有虚函数,那么子类会自己生成一个虚函数表来存放自己的虚函数;
- 如果基类有虚函数,子类也有自己的虚函数,那么子类中虚函数的地址会存放在子类继承的基类那部分的虚函数表中的末尾;
- 如果基类有虚函数,并且子类对该虚函数进行了重写,那么子类虚函数表中基类被重写的虚函数地址就会被子类重写的虚函数地址覆盖,而不再和第二点一样写在虚函数表的尾部。
例如:
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;
}
虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr
。
总结一下派生类的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
✨✨这里还有一个很容易混淆的问题:虚函数存在哪的?虚表存在哪的?
代码如下:
int main()
{
int i = 0;
static int j = 1;
int* p1 = new int;
const char* p2 = "aaaaa";
Base b;
Derive d;
printf("栈:%p\n", &i);
printf("静态区:%p\n", &j);
printf("堆:%p\n", p1);
printf("常量区:%p\n", p2);
printf("Base虚表地址:%p\n", *(int*)&b);
printf("Derive虚表地址:%p\n", *(int*)&d);
return 0;
}
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 Black;
Func(Black);
Student tutu;
Func(tutu);
return 0;
}
当使用Person类对象调用函数Func时:
当使用Student类对象调用函数Func时:
反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是基类对象的指针或引用调用虚函数。具体原因,我们先要了解一下动态绑定。
动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
那么当我们直接使用对象调用成员函数时走的是静态绑定,是指编译期间就确定的程序行为;当我们使用基类指针或引用调用虚函数时走的是动态绑定,需要通过虚函数表来确定不同对象调用不同的函数,根据具体拿到的类型确定程序的具体行为。
如果只是完成了虚函数的覆盖而没有通过基类对象的指针或引用调用,或者只有第二个条件都无法完成多态的实现。
4.多继承中的虚函数表
在多继承中,派生类会继承多个基类,每个基类都有自己的虚表。因此,派生类会有多个虚表,每个虚表对应于一个基类。
例如:
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; }//重写Fun1()
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
//打印虚表地址以及虚函数地址
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
//第一个虚表地址
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
//找到第二个虚表的地址
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
PrintVTable(vTableb2);
return 0;
}
结果如下:
5.结语
虚函数表的存在是为了实现动态绑定也就是实现多态,当派生类对基类的虚函数进行重写时,通过基类对象指针和引用调用虚函数时,就会通过虚函数表来确定不同对象调用不同的函数,根据具体拿到的类型确定程序的具体行为,所以多态实现的两个条件缺一不可。以上就是今天所有的内容啦~ 完结撒花🥳🎉🎉