C++中 虚函数及包含多态的实现
2017-04-29 06:52
190 查看
我们分三个方面来说明虚函数以及用虚函数实现的包含多态。
第一个:什么是虚函数?
从语法上来说虚函数就是用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、虚函数应该是公有的,并且类层次之间的继承也应该是公有的,否则就没什么意义了,
或者有其他的特殊情况,再特殊处理。
第一个:什么是虚函数?
从语法上来说虚函数就是用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++多态实现(虚函数,成员函数覆盖、隐藏)
- C++中为什么要用虚函数、指针或引用才能实现多态?
- C++对象布局及多态实现探索之虚函数调用
- C++对象布局及多态实现之带虚函数的类
- c++类型兼容规则与虚函数实现多态的实现原理和区别
- c++中的重载操作符和虚函数实现多态
- C++ 虚函数实现多态浅析
- C++中的动态类型与动态绑定、虚函数、运行时多态的实现
- C++ 虚函数 多态的实现
- C++中的动态类型与动态绑定、虚函数、运行时多态的实现
- C++通过虚函数实现多态
- C++中的动态类型与动态绑定、虚函数、运行时多态的实现
- C++中为什么要用虚函数、指针或引用才能实现多态?
- C++对象布局及多态实现之带虚函数的类
- C++中多态怎样由虚函数实现。和纯虚函数介绍
- C++中的动态类型与动态绑定、虚函数、运行时多态的实现
- C++多态之 虚函数实现机制
- 虚函数实现多态---C++
- C++中的动态类型与动态绑定、虚函数、运行时多态的实现
- C++多态,虚函数作用及底层实现原理