目录
- 一、虚函数表
- 二、多态原理
- 三、关于动态绑定与静态绑定
一、虚函数表
先来看一段代码:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
cout << sizeof(Base) << endl;
return 0;
}
如果用以前的知识,想到的结果应该是4
但是:
打开调试窗口发现,除了b成员变量外,还有一个_vfptr放在对象的前面,(也有可能在后面,取决于编译器),对象中的这个指针叫做虚函数表指针(简称虚表指针)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
再增加派生类继承Base类,改造Base类,看是否有同样的效果:
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️⃣派生类的
通过调试,可总结以下几点:
- 派生类对象中也有一个虚表指针,是继承基类的,除了基类以外就是自己的
- Func1和Func2是虚函数,所以放进虚表中;Func3不是虚函数,所以没有在虚表中
- 在派生类对象中发现,Func1函数的地址被覆盖了,原因是派生类对Func1函数进行了重写。重写是语法的叫法,覆盖是原理层的叫法
- 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr
- 派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
两个问题:
虚函数存在哪里?
虚表存在哪里?
///
虚函数和普通函数一样的,都是存在常量区的。只是虚函数指针存到了虚表中。
虚表也是存在常量区的。验证:(vs下)
int main()
{
Base b;
Derive d;
int i = 0;
static int j = 1;
int* p1 = new int;
const char* p2 = "aaaa";
printf("栈:%p\n", &i);
printf("静态区:%p\n", &j);
printf("堆:%p\n", p1);
printf("常量区:%p\n", p2);
Base* p3 = &b;
Derive* p4 = &d;
printf("Base虚表地址:%p\n", *(int*)p3);
printf("Base虚表地址:%p\n", *(int*)p4);
return 0;
}
根据打印结果可知,虚表是存在常量区的
二、多态原理
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
void Func(Base* p)
{
p->Func1();
}
int main()
{
Base b;
Func(&b);
Derive d;
Func(&d);
return 0;
}
多态调用的条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类要对基类的虚函数进行重写
多态调用过程:
p指向的是b对象时,p->Func在b对象的虚表中找到对应的虚函数,完成调用。p指向的是d对象时,p->Func在d对象的虚表中找到对应的虚函数,完成调用。
多态调用与普通调用的区别:
- 多态调用是运行时在所指向对象的虚表中找到虚函数的地址,与对象有关
- 普通调用是编译时确定函数地址,与类型有关
多态调用:
普通调用:
为什么要通过基类对象的指针或者引用进行调用?
在这过程中,其实是切片行为。基类的指针或者引用得到派生类中继承基类的那一部分,然后再去对应的虚表中找到要调用的虚函数。
为什么基类对象调用不行,会变成普通调用?
用基类对象去调用,派生类对象给基类对象是切片,但注意:这过程中有拷贝构造出一个基类的对象,这个基类的对象所对应的虚表是基类的虚表,结果导致拷贝构造的基类对象都去基类对象的虚表中找虚函数,就没有调用派生类对象的虚表中的虚函数了。
注意:同一个类的对象,对应的是同一张虚表
为什么派生类要对基类的虚函数进行重写?
因为派生类继承基类得到基类的那部分的虚表,虚表里面的虚函数地址是基类的,如果不进行重写,在派生类对象的虚表中找到的虚函数依然是基类的,所以要对基类的虚函数进行重写,虚表中也同时完成了虚函数地址的覆盖,调用时就是派生类的了。
三、关于动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。