C++中 虚函数及包含多态的实现
来源:互联网 发布:单片机控制舵机程序 编辑:程序博客网 时间:2024/06/07 14:18
我们分三个方面来说明虚函数以及用虚函数实现的包含多态。
第一个:什么是虚函数?
从语法上来说虚函数就是用virtual声明的函数。所以定义一个虚函数很简单。重点是你需要知道我们如何用虚函数解决实际的问题。
第二个:编译器是如何解析函数调用语句的?
通常我们是用一个类型定义一个对象,或者new一个对象,然后用这个类型的指针指向它,然后用对象或者指针来调用它所拥有的函数。某些时候(其实是经常)我们会遇到在子类中覆盖父类方法的情况,根据我们前面所说的指针赋值兼容性规则,我们用下面的例子详细说明一下这种语法情况,然后和后面的多态进行对比:
class A
{
public:
voidfun()
{
cout << "A的hello" << endl;
}
};
class B : public A
{
public:
voidfun()
{
cout << "B的hello" << endl;
}
};
int _tmain(int argc,_TCHAR* argv[])
{
//现在主要说明赋值兼容性规则
Aa;
a.fun(); // A的hello
Bb;
b.fun(); // B的hello
//以上代码不会有任何问题或歧义
Bb1;
b1.fun(); // B的hello
A a1 = b1; //用子类对象给父类对象赋值
a1.fun(); // A的hello
Bb2;
b2.fun(); // B的hello
A &a2 = b2; //用子类对象给一个父类对象的引用赋值
a2.fun(); // A的hello
Bb3;
b3.fun(); // B的hello
A *pa3 = &b3; //用子类对象地址给父类对象的指针赋值
pa3->fun(); // A的hello
B*pb4 = new B();
pb4->fun(); // B的hello
A *pa4 = pb4;//用子类的指针给父类的指针赋值。
pa4->fun();// A的hello
return0;
}
仔细观察一个,编译器在编译一个方法调用语句时,总是根据调用方法的对象或者指针的类型来调用对应的方法。那么就引出了我们的第三个问题。
第三:我们如何根据父类的指针来调用子类的方法呢?
答案就是我们前面所说的虚函数。
下面我们用实际的代码来看一下虚函数的作用,以及其内存模型。
class A
{
public:
virtualvoid fun()
{
cout << "A的hello" << endl;
}
};
class B : public A
{
public:
virtualvoid fun()
{
cout << "B的hello" << endl;
}
};
int _tmain(int argc,_TCHAR* argv[])
{
//现在主要说明赋值兼容性规则
Aa;
a.fun(); // A的hello
Bb;
b.fun(); // B的hello
//以上代码不会有任何问题或歧义
Bb1;
b1.fun(); // B的hello
A a1 = b1; //
a1.fun(); // A的hello
Bb2;
b2.fun(); // B的hello
A&a2 = b2;
a2.fun();// B的hello
Bb3;
b3.fun(); // B的hello
A*pa3 = &b3;
pa3->fun();// B的hello
B*pb4 = new B();
pb4->fun(); // B的hello
A*pa4 = pb4;
pa4->fun();// B的hello
return0;
}
第一种情况,用B的对象给A的对象赋值,实际上是调用了A的拷贝构造函数,也就是用b1中A的部分去给a1赋值,所以我们可以认为B的对象中包含一个A对象(可能这就是为什么叫做包含多态吧)。此时用a1调用fun()就确实是用A的对象来调用fun()方法
其他三种情况a2,a3,a4实际上表示或者指向的都是一个B类型的对象,所有后面输出的就都是“B的hello”
为了弄清楚程序究竟是如何工作的,我们从反汇编和内存的角度来看一个它的内存模型。
在前面所说的对象的内存模型中,我们知道程序在运行时会先把方法代码载入到代码区,然后把方法的入口地址以jmp地址的方式给出方法入口表。而我们在代码中对方法的调用会被编译器自动转换为call入口表中对应入口地址
的方式。
现在再来看一下含有虚函数的情况:
pb4->fun(); // B的hello
00FE6A91 mov eax,dword ptr [pb4] //1- 把对象的首地址复制到eax
00FE6A94 mov edx,dword ptr [eax] // 2-把以eax的内容为地址的4个字节的内存空间的内容复制到edx,实际上它就是虚指针。
00FE6A96 mov esi,esp
00FE6A98 mov ecx,dword ptr [pb4] // ecx存放对应对象的首地址
00FE6A9B mov eax,dword ptr [edx] // 3-然后把以edx的内容为地址的内存空间的内容复制到eax,现在eax就是函数入口表中对应的函数入口地址了。
00FE6A9D call eax //4-调用,跳转
00FE6A9F cmp esi,esp
00FE6AA1 call __RTC_CheckEsp (0FE136Bh)
A *pa4 = pb4;
00FE6AA6 mov eax,dword ptr [pb4]
00FE6AA9 mov dword ptr [pa4],eax
pa4->fun();// B的hello
00FE6AAC mov eax,dword ptr [pa4]
00FE6AAF mov edx,dword ptr [eax]
00FE6AB1 mov esi,esp
00FE6AB3 mov ecx,dword ptr [pa4]
00FE6AB6 mov eax,dword ptr [edx]
00FE6AB8 call eax
如果你看完上面加粗的说明之后还没有头晕,那么恭喜你,你不是正常人^^
作为正常人,我需要去看看内存中的具体数据。
第一步:mov eax, dword ptr [pb4] [pb4] == pb4 =0x0061D7E0,执行之后eax =0x0061D7E0
它所对应的前四个字节的内容为0x00FED998
第二步:mov edx,dword ptr [eax] ; [eax] =以0x0061D7E0为地址的内存单元的内容,也就是0x00FED998
也就是虚指针,此时edx = 0x00FED998
我们看一下0x00FED998地址处的内容:
可以看到,此处的内容都是00fe开头的一些内存空间的地址。
第三步:moveax,dword ptr [edx]; [edx] =以0x00FED998为首地址的内存单元的内容,也就是0x00FE151E
此时,EAX = 00FE151E
第四步:call eax;在call后面,说明eax保存的是可执行代码了,所以我们查看一下此处的反汇编
A::A:
00FE1519 jmp A::A (0FE68F0h)
B::fun:
00FE151E jmp B::fun (0FE2DD0h)
A::fun:
00FE1523 jmp A::fun (0FE2D70h)
B::B:
00FE1528 jmp B::B (0FE98F0h)
A::A:
00FE152D jmp A::A (0FE6950h)
可以看到,从call开始,进入到普通函数调用的函数入口表中的函数入口地址。
(ps:虚表和函数入口表是在一段内存空间中存放)
总结一下,对于含有虚函数的类,在生成对象时,会在对象的前四个字节保存一个虚指针,我们称虚指针指向的内存空间是一个虚表,它是由一系列虚函数的入口的地址组成的,然后用call进行程序的跳转,回到普通函数的调用方法上。
对应的我们可以和虚基类对比一下,虚继承之后的类在生成对象时,同样会在对象开始保存一个虚指针,只不过虚指针指向的虚表中保存的不是函数的入口地址,而是对象中,基类的属性成员的偏移地址。
现在我们已经说明了虚函数用途和使用方式,以及包含多态的意义,需要明确以下几点:
1、虚函数会造成额外的内存开支,所以只应该在类层次结构中并且需要使用多态时才使用
2、虚函数应该是公有的,并且类层次之间的继承也应该是公有的,否则就没什么意义了,
或者有其他的特殊情况,再特殊处理。
- C++中 虚函数及包含多态的实现
- 包含激活函数的多层神经元网络及matlab实现
- string.h 中包含的函数及实例
- C语言中全局变量的定义及重复包含问题
- c语言常用头文件中包含的函数
- 【C】结构体中包含函数
- C语言中不用宏实现变长参数函数的原理及实现
- C语言中不用宏实现变长参数函数的原理及实现
- C语言中不用宏实现变长参数函数的原理及实现
- C++/C中防止头文件的重复包含的解决办法及区别
- C++/C中防止头文件的重复包含的解决办法及区别
- conio包含的函数及作用
- wordpress所包含的函数及应用
- 常用头文件及包含的函数
- 常用头文件及包含的函数
- C中struct的函数的实现
- C语言中自带头文件(.h)中包含的函数
- C语言常用函数的包含文件
- 安装vue.js的方法,安装nodejs,安装cnpm
- 【哈佛商评】人工智能的创造力总会有极限
- gVim, gVim Easy, gVim Read-only 的简单区别
- Leetcode 338 Counting Bits
- Java 中继承之后同名静态变量的问题分析
- C++中 虚函数及包含多态的实现
- 背包九讲详解
- 机器学习基础——BP算法
- const 修饰函数参数,返回值,函数体
- C++中 异常处理的 实现方式
- 关于 强制类型转换的探究
- [TravelNotes] ZJOI 2017 DAY2 酱油记
- reason about thread is blocked
- 241. Different Ways to Add Parentheses