文章目录
- 引言
- 1. 多态是如何实现的
- 2. 动态绑定和静态绑定
- 3. 虚函数表
- 4. 虚函数表指针
- 5. 总结
引言
书接上回,本文将重点讨论多态的实现原理和虚函数。
1. 多态是如何实现的
class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:string _name;
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-打折" << endl; }
private:string _id;
};
class Soldier : public Person {
public:virtual void BuyTicket() { cout << "买票-优先" << endl; }
private:string _codename;
};
void Func(Person* ptr)
{// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。ptr->BuyTicket();
}
int main()
{// 其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后// 多态也会发⽣在多个派⽣类之间。Person ps;Student st;Soldier sr;Func(&ps);Func(&st);Func(&sr);return 0;
}
通过下图我们可以看到,满足多态条件后,底层不再是编译时通过调用对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。
2. 动态绑定和静态绑定
- 对不满足多态条件(指针或引用 + 调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定。
- 满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也叫做动态绑定。
// ptr是指针+BuyTicket是虚函数满⾜多态条件。
// 这⾥就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调⽤函数地址
ptr->BuyTicket();
00EF2001 mov eax,dword ptr [ptr]
00EF2004 mov edx,dword ptr [eax]
00EF2006 mov esi,esp
00EF2008 mov ecx,dword ptr [ptr]
00EF200B mov eax,dword ptr [edx]
00EF200D call eax
// BuyTicket不是虚函数,不满⾜多态条件。
// 这⾥就是静态绑定,编译器直接确定调⽤函数地址
ptr->BuyTicket();
00EA2C91 mov ecx,dword ptr [ptr]
00EA2C94 call Student::Student (0EA153Ch)
3. 虚函数表
- 基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同一张虚表,不同类型的对象各自有独立的虚表,所以基类和派生类有各自独立的虚表。
- 派生类由两部分构成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意在这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个,就像基类对象的成员和派生类对象中的基类对象成员也是独立的。
- 派生类中重写的基类虚函数,派生类虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。
- 派生类的虚函数表中包含,(1)基类的虚函数地址,(2)派生类重写或完成覆盖的虚函数地址,派生类自己的虚函数地址三个部分。
- 虚函数表本质是一个存放虚函数指针的指针数组,一般情况下这个数组最后面放了一个 0x00000000 标记(这个C++并没有进行规定,各个编译器自行定义的,vs系列编译器会在后面放个 0x00000000 标记,g++系列编译不会放)。
class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }void func5() { cout << "Base::func5" << endl; }protected:int a = 1;
};class Derive : public Base
{
public:// 重写基类的func1virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func1" << endl; }void func4() { cout << "Derive::func4" << endl; }protected:int b = 2;
};int main()
{Base b;Derive d;return 0;
}
- 虚函数存在哪?虚函数和普通函数⼀样,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址又存到了虚表中。
- 虚函数表存在哪?这个问题严格说并没有标准答案,C++标准并没有规定,我们写下面的代码可以对比验证⼀下。vs下是存在代码段(常量区)。
int main()
{int i = 0;static int j = 1;int* p1 = new int;const char* p2 = "xxxxxxxx";printf("栈:%p\n", &i);printf("静态区:%p\n", &j);printf("堆:%p\n", p1);printf("常量区:%p\n", p2);Base b;Derive d;Base* p3 = &b;Derive* p4 = &d;printf("Person虚表地址:%p\n", *(int*)p3);printf("Student虚表地址:%p\n", *(int*)p4);printf("虚函数地址:%p\n", &Base::func1);printf("普通函数地址:%p\n", &Base::func5);return 0;
}
4. 虚函数表指针
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}protected:int _b = 1;char _ch = 'x';
};int main()
{Base b;cout << sizeof(b) << endl;return 0;
}
上⾯代码运行结果16bytes(64位操作系统下的结果),除了 _b 和 _ch 成员,还多⼀个 _vfptr 放在对象的前⾯(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针( v 代表virtual,f 代表 function )。一个含有虚函数的类中都至少有⼀个虚函数表指针,因为⼀个类所有虚函数的地址都要被放到这个类对象的虚函数表中,虚函数表也简称虚表。
5. 总结
C++多态是面向对象编程的强大工具,它提供了代码的灵活性和可扩展性。理解多态的实现原理和适用场景,能够帮助我们设计出更优雅、更易维护的代码。无论是编译时多态还是运行时多态,都有其独特的优势和适用场景,在实际开发中应根据需求合理选择。
希望本文能帮助你更好地理解和应用C++多态特性。如果你有任何问题或想法,欢迎在评论区留言讨论!