提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
基于以下代码讲解
定义一个基类human,一个派生类kangknag
class human
{
public:
int a;
virtual void eat()
{
std::cout << "human" << std::endl;
}
virtual void onlyhuman()
{
std::cout << "human" << std::endl;
}
};
class kangkang:public human
{
public:
int b;
void eat()
{
std::cout << "kangkang" << std::endl;
}
void play()
{
std::cout << "play" << std::endl;
}
virtual void onlykangkang()
{
std::cout << "狠狠的学........." << std::endl;
}
};
先讲一讲虚函数的本质
1,先看不含虚函数的普通类
以下是虚函数表:
类所占内存只有成员变量。
main加两行代码:
human pure_human;
kangkang pure_kangkang;
pure_kangkang.eat();
(&pure_kangkang)->eat();
能够发现,也是只能利用指针和引用实现多态的本质原因:类实例调用对象时候,直接调用是直接去call这个函数地址,利用指针调用相当于是间接通过虚函数表去查地址调用,结果是一样的。而且,要明白无论是虚函数还是非虚函数,都只是存在代码区某个固定地址。
2,虚函数表
human* human1 = new kangkang();
类中有了虚函数之后,类内存会在首地址加一个_vfptr的虚函数表指针,该指针指向一个虚函数表。如下所示:
下面看_vfptr指针指向的虚函数表内存的实际情况:
但是根据上图发现一个问题,human1的实际内存里的虚函数表里是正常的,第四个字节就是onlykangkang虚函数的地址,但是vs给出的却没显示出来??????有点奇怪可能是编译器原因、
基类直接在首地址创建虚函数表指针。
派生类虚函数表特性:
1.先拷贝基类的虚函数表
2.如果派生类对基类中的虚函数进行重写,使用派生类的虚函数替换相同偏移量位置的基类虚函数,如下图所示:派生类重写的虚函数表确实覆盖了原虚函数表。
3,跟上派生类自己的虚函数
具体过程:pure_kangkang继承human,所以把human所以拷贝过来,然后派生类重写虚函数,生成自己的虚函数表覆盖掉继承来的。得到红框1;
派生类自己有其成员变量,接着开辟内存进行存储,得到红框2;
3,虚函数的性质
1,同一个类的多个实例都指向同一个虚函数表;
kangkang pure_kangkang;
kangkang pure_kangkang2;
pure_kangkang.eat();
(&pure_kangkang)->eat();
(&pure_kangkang2)->eat();
可见,pure_kangkang和pure_kangkang2的虚函数表指针是一样的,其虚函数存放在代码区的地址也是完全一样的。
2,只有通过指针访问函数才会调用虚函数表,通过对象调用函数肯定是自己的函数,不涉及指针就不涉及多态,编译器在编译时候就能确定函数地址?????,动态多态就是编译好了不确定
多态
封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。静态多态,将同一个接口进行不同的实现,根据传入不同的参数(个数或类型不同)调用不同的实现。动态多态,则不论传递过来的哪个类的对象,函数都能够通过同一个接口调用到各自对象实现的方法。
静态多态和动态多态的区别其实只是在什么时候将函数实现和函数调用关联起来,是在编译时期还是运行时期,即函数地址是早绑定还是晚绑定的。静态多态是指在编译期间就可以确定函数的调用地址,并生产代码,这就是静态的,也就是说地址是早绑定。静态多态往往也被叫做静态联编。动态多态则是指函数调用的地址不能在编译器期间确定,需要在运行时确定,属于晚绑定,动态多态往往也被叫做动态联编。
静态多态和动态多态区别的理解:
是两种多态的体现,静态多态是利用重载和模版技术,调用不同重载函数时call的地址是不同的,可以直接根据函数参数类型和数量确定函数地址,模板也是根据T生成新的函数,调用其地址。因此几个重载代码区就有几个内存区域存放函数,代码冗余度高。
在 C++ 中,动态多态(Dynamic Polymorphism)是通过虚函数(Virtual Functions)和指针或引用来实现的,是根据指针指向不同对象的虚函数表来实现不同的方法。
静态多态
函数重载 ,函数模版和 运算符重载属于静态多态,复用函数名。
静态多态:也称为编译期间的多态,编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。
静态多态类中函数一般是调用object.func()直接去call代码区的函数地址,而没有查虚函数表等等使用多态带来的性能损失。当然,有得必有失,静态多态会牺牲空间造成代码膨胀(无论是函数重载还是模板函数,都是在不同的地址存放)。
动态多态
human* human1 = new kangkang();
human1->eat();
human1->play();
具体的多态本质如下:
如图所示:含虚函数的类,类内存首先有个_vfptr指针,指向一张_vfptr表,表中是虚函数的地址,
无论是虚函数还是非虚函数,都只是存在代码区某个固定地址,类实例调用对象时候,直接调用是直接去call这个函数地址,利用指针调用相当于是间接通过虚函数表去查地址调用,结果是一样的。但是后者有个好处,因为指针可以指向不同类型的地址,
eg 有个全局函数 void func(human* human1){};基类指针可以指向各种派生类new的内存,这样利用指针或引用取调用函数时候_vfptr指针指向的是派生类各自的虚函数表,这样就实现了多态!!!
visual studio编译给出human1结构如上所示,我的理解:
human* human1 = new kangkang();
先new一个kangkang类型–对应红框1的第一行kangkang类,然后转为human类型–对应转为第二行的human类,但此时human下的东西仍然是原来kangkang的。
对于红框2的理解:有点迷惑,按上面那个逻辑已经结束了,难不成是因为向上转型时候编译器直接也给了human1在human*类型下的一张表,但其实表都是一样的,编译器的问题应该是!