C++多态原理
多态的原理
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
通过观察测试我们发现b对象是8bytes,除了_b
成员,还多一个_vfptr
放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。
虚函数表指针,在32位下是四个字节,64位下是八个字节,这个指针会指向一张表,这个表叫虚函数表,虚函数表本质是一个指针数组。
一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
函数编译好了之后都是一串指令,这些指令都是放到代码段里,所以,虚函数并不是放到虚表里,而是把虚函数的地址放到虚表里,函数的地址就是编译完后的第一条指令的地址。
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
virtual void Func2()
{
cout << "Func2()" << endl;
}
void Func3()
{
cout << "Func3()" << endl;
}
private:
int _b = 1;
};
因为,Fun1函数Func2函数是虚函数,所以Fun1函数Func2函数的地址会放到虚表里面。Func3不是虚函数,F所以un3函数的地址没有放到虚表里面。
虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
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;
};
void Func1(Base* p)
{
//动态绑定/运行时绑定
//运行的时候确定函数的地址,通过指向对象的虚指针找到函数的地址
p->Func1();
//静态绑定/编译时绑定
//普通函数在编译时就能确定函数的地址,跟指向的对象没有关系
p->Func3();
}
可以看到对于Func1函数传一个Base对象的指针调用的就是Base的Fun1函数,传一个Derive对象的指针调用的就是Deirve的Fun1函数,但是对于Func3函数传入的无论是Base对象的指针还是Derive对象的指针调用的都是Base的指针。
那这里是怎么实现的呢?通过调试窗口可以看到
可以观察到基类b对象和派生类d对象虚表是不一样的,这样可以避免混淆,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
另外Func2继承下来后是虚函数,所以放进了虚表,因为虚函数Func2并没有完成重写所以,两个虚表中,Func2函数的地址是一样的,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
派生类的虚表生成:
- 先将基类中的虚表内容拷贝一份到派生类虚表中
- 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
- 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
静态多态
int main()
{
int i = 0;
double d = 1.1;
//给不同的类型实现不同的效果
//通过运算符重载,在编译的时候就确定了
cout << i;
cout << d;
int a = 1, b = 2;
char x = 'x', y = 'y';
//通过函数模板,不同参数实例化不同的函数
//在编译时通过不同的参数就实例化出不同函数
swap(a, b);
swap(x, y);
}
动态多态
在运行时去虚表里面去找,实现运行的多种形态,根据指向的对象不同,达到不同的效果。
虚函数存在哪的?虚表存在哪的?
虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。虚表的指针是存在对象里面的,那么虚表存在那的呢?
下面是一个测试。
int main()
{
int i = 0;
static int j = 1;
int* p1 = new int;
const char* p2 = "xxxxx";
printf("栈:%p\n", &i);
printf("静态区:%p\n", &j);
printf("堆:%p\n", p1);
printf("常量区:%p\n", p2);
Base b;
Derive d;
printf("Base虚表地址:%p\n", *(int*)&b);
printf("Deirve虚表地址:%p\n", *(int*)&d);
}
所以,可以看到虚表与常量区更接近一点,所以虚表一般存在常量区,或者代码段。
同一个类型的对象,虚表是一样的,共享一张虚表。
一个题目
多继承中指针偏移问题?下面说法正确的是( )
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main(){
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3
对象模型就是说,编译器通过编译后,编译器生成这个对象,这个对象里的各个变量的是通过成员变量声明顺序分布的,先声明的在前面,对于继承的类来说,先继承的基类成员在派生类对象的内存布局的前面。
原文地址:https://blog.csdn.net/qq_74319491/article/details/142532179
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!