C++虚函数的一点分析与思考

简介:
以下是自己看过的书籍以及自己思考的流程和总结,主要是对C++虚函数分析了,分析并不算足够深入,但相信对理解c++的虚函数会有些帮助。现在仅仅写到了单继承下的一些皮毛,后面还要继续挖掘一下,希望自己能以淡定一点的心做好一块,不负自己。
以下内容适合了解一些C++虚函数以及对指针操作相对来说有点基础的朋友,因为里面为了验证自己的思考进行了很多指针的强转,下面的测试需要有实际的代码操作,不希望大家仅仅看结论就算完了,那很没有意思。我在本地建了一个vs工程,先从最简单的测试做起,然后一点点的加入代码.一点点的深入,代码写的比较乱,仅仅是为了自己的测试,望朋友包涵一下了。

代码如下:
Shape.h
log.h
typedef.h
main.cpp
Shape.h:
#ifndef __SHAPE__HEAD_H
#define __SHAPE__HEAD_H


#include "typedef.h"
#include "log.h"


class CShape
{
public:
CShape(){
TRACE_FUCTION_AND_LINE("");
m_color = 15;
}
~CShape(){
TRACE_FUCTION_AND_LINE("");
}
void SetColor(int colore){
TRACE_FUCTION_AND_LINE("");
m_color = colore;
}
protected:
private:
int m_color;
};


class CRect : public CShape
{
public:
CRect(){TRACE_FUCTION_AND_LINE(""); m_width = 0; m_height = 255;}
~CRect(){TRACE_FUCTION_AND_LINE("");}
void PrintMemory(){
TRACE_FUCTION_AND_LINE("this: %p", this);
int *p = (int*)this;
TRACE_FUCTION_AND_LINE("4: %d", *p);
TRACE_FUCTION_AND_LINE("4: %d", *(p+1));
TRACE_FUCTION_AND_LINE("4: %d", *(p+2));
}
protected:
private:
int m_width;
int m_height;
};
#endif


log.h:
#define TRACE_FUCTION_AND_LINE(fmt, ...) printf("[%30s:%4d] "fmt"\n",__FUNCTION__, __LINE__, ##__VA_ARGS__)


typedef.h:
//仅仅是一些跨平台的宏定义,就不列出来了,针对我们的问题不起主要作用。


main.cpp:
#include <iostream>
using namespace  std;
#include "Shape.h"


int main()
{
CRect rect1;

TRACE_FUCTION_AND_LINE("sizeof(CShape):%d", sizeof(CShape));
TRACE_FUCTION_AND_LINE("sizeof(CRect):%d, %p", sizeof(CRect), &rect1);
rect1.PrintMemory();
rect1.SetColor(10);
rect1.PrintMemory();
return 0;
}

问题1:
派生类和基类的内存如何排布?
通过main.cpp中rect1的打印内存的操作我们可以看出,派生类占用12字节内存,分别是基类的m_color,以及自己的两个int成员。
基类占有4个字节的内存,SetColor函数本身不占用任何内存。

真理:对象所占用内存包含3个部分,非静态数据成员(自身的和父类的),vptr(后面介绍),字节对齐。
因此不要武断的说,c++占用内存比C多,其实就一个vptr的问题,字节对齐在结构体中同样会出现,字节对齐对上层来说是透明的,因此不用太过于例会。

派生类如何调用基类的非虚public函数,例如本例的SetColor?
1,对于SetColor方式,编译器会将其编译成SetColor(int colore, CShape* pShape); rect进行调用的时候采用的纯C的方式,也就没有任何多余的开销。
rect1.SetColor(color)将会被展开为 SetColor(color, &rect1); 于是rect1的地址被传入到pShape中,继而调用pShape->m_color给m_color赋值。
对于编译器来说,它只看到传过来的参数地址为&rect1, 它并不知道它的实际类型是什么,对于任何类型都将会被SetColor强转为CShape的类型,于是这就引出一个问题,编译器怎么知道实际的rect1的m_color地址偏移是多少呢?实际上它压根就不知道它是CRect类型.在SetColor这个函数指令运行的时候,它仅仅是根据传入pShape的地址,以CShape的方式偏移特定的地址(对于本例子偏移为0),然后赋值。可以判断对于子类CRect来说, 也是以偏移为0的地址进行赋值的;换句话说,子类对象的内存有一块内存是父类的,而且父类的内存必须在内存块的前半部分,要不对rect1的地址偏移为0的地址赋值时,就有可能赋值到另一个数据成员上,而不是m_color。

2,如何验证此想法?
1)根据上面例子的内存打印可以看出,rect1的m_color的内存确实在地址的前半部分。
2)可以给SetColor传递一个假的CShape类型,观测其是否确实是对假的对象的前四个字节赋值?
测试代码如下:
struct FakeCRect{
int fake1;
int fake2;
int fake3;
int fake4;
}fakerect = {1,1,1,1};
TRACE_FUCTION_AND_LINE("fakerect:%d, %d, %d, %d", fakerect.fake1, fakerect.fake2, fakerect.fake3, fakerect.fake4);
CRect* pfakerect = (CRect*)&fakerect;
pfakerect->SetColor(20);
TRACE_FUCTION_AND_LINE("fakerect:%d, %d, %d, %d", fakerect.fake1, fakerect.fake2, fakerect.fake3, fakerect.fake4);

打印结果如下:
main:  23] fakerect:1, 1, 1, 1
main:  26] fakerect:20, 1, 1, 1
可以看到确实是第一个字节变了,传入一个假的CRect类型,它依然是对其第一个int变量处理,你甚至可以传递一个char型的数组来测试都行。

3,拓展延伸,这种情况的例外。
1)上面的例子没有考虑带有虚函数的情况,现在给父类和子类分别加入一个虚函数,display方法。子类继承父类的虚函数,并重写此方法。
virtual void display(){
TRACE_FUCTION_AND_LINE("");//打印当前函数的名字和行号,便于判断是调用哪一个类的display方法。
}
这个时候可以看到,父类和子类的对象内存大小改变了,分别增加了四个字节,分别是8, 16,而且根据子类的PrintMemory可以清楚的看到添加的内存是占用的对象最前面四个字节,其他不变。虚函数机制使得使用基类的指针指向不同的对象来实现多态成为现实。pShape->display();
通过pShape指向不同的对象, 将会调用不同方法,可以再创建一个CCircle类继承于CShape类来观测这种结果。
CShape* pShape = new CCircle();
pShape->display();//调用CCircle的display方法
pShape = new CRect;
pShape->display();//调用CShape的display方法
如果你还不是很清楚虚函数的用法或者说你不打算深入探究虚函数的实现原理,建议一下内容就不要看了。
只要一个类有虚函数(继承下来的或者是本身的),它就会有一个vptr,vptr是一个指针,执行一个vbtl虚函数表。你可以把vbtl想象成一个指针数组,它的数组的元素是一个个的函数指针,指向它自己的虚函数,一般还会有一个指向typeinfo的结构的指针,以实现运行时刻类型识别。
简单说来,现在CRect有一个虚函数display,那么他的vbtl会有两个元素,一个是typeinfo指针,一个是dispaly方法指针。

2)简单看看typeinfo:
CShape* pShape = &rect1;
if(typeid(rect1) == typeid(CRect) && typeid(*pShape) == typeid(CRect)){
TRACE_FUCTION_AND_LINE("rect1 is type CRect");
TRACE_FUCTION_AND_LINE("rect1 name:(%s) raw_name:(%s)", typeid(rect1).name(), typeid(rect1).raw_name());
}
通过上述代码可以清楚看到可以在运行时候判断一个对象时什么类型,即使将rect1的地址转换父类指针,依然可以判断出它实际是什么类型。
typeinfo是一个类一个,编译器编译的时候静态静态分配,每一个带有虚函数的类的对象都会有一个指针指向它,同一个类的对象指针应该一致下面开始测试这一猜想。
看有些书上说的是typeinfo的结构指针位于vtbl虚表的第0个item上,但是我在vc++编译调试没能找到,第0个item上是虚函数display的地址。
于是又改在仿linux环境,MINGW下测试,在第-1个item选项上找到了typeinfo的地址,很是欣慰。但是在windows上还是始终找不到,猜测是在-1的item选项上,不过这个选项应该还有其他的东西,我这么推断的主要原因是除了-1和0item这两个位置,它们的前后地址都是0. 只是-1这个item的位置应该被微软又封装了一下,而不是单纯的指向typeinfo结构。
以下为测试代码:
const type_info* ptypeidinfo = &(typeid(rect1));
TRACE_FUCTION_AND_LINE("ptypeidinfo: %p", ptypeidinfo);
int *p = (int*)&rect1;
int *pp = (int*)(p[0]);//vptr
type_info *prectinfo = (type_info*) (*(pp-1));//pp-0: virtual function address
TRACE_FUCTION_AND_LINE("prectinfo: %p", prectinfo);
vs2008输出:
main:  36] ptypeidinfo: 003A9004
main:  40] prectinfo: 003A7748
MINGW输出:
main:  36] ptypeidinfo: 004085a4
main:  40] prectinfo: 004085a4
在vs2008上对pp-2和pp+1都查看了,都是0地址,因此可以猜测typeinfo肯定跟pp-1有关,只是被封装了而已。

3)回到问题 (上面的例子没有考虑带有虚函数的情况):
父类和子类都有一个虚函数,这样每一个子类对象的父类部分也就自动向下偏移,因此可以看到现在的fakerect是第二个int变量被改变了,这个比较容易理解,对于SetColor方法来说,它的参数是CShape,那么他就CShape的m_color的偏移地址(此处为4)进行赋值。试想另外一种情况,父类没有虚函数,子类有虚函数,这样父类占用的内存为4,子类的内存占用仍有vptr的4个字节内存。
测试:可以将父类的display虚函数注释掉,打印结果如下:
fakerect:1, 1, 1, 1
fakerect:1, 20, 1, 1
此时父类对象部分就不是位于子类对象起始部分,子类对象最起始是vptr,而SetColor还根据它本身的参数CShape类型来进行偏移。对于CShape来说,它看到的仅仅是四个字节的m_color,它对m_color进行赋值的时候,就会对pShape指针的最开始四个字节赋值,然而传递过去的fackrect对象的首地址却是fake1变量,而被改变的确是fake2变量,这个到底是怎么回事呢??
传递过去的首地址是fake1,SetColor改变也是传递过去的首地址,而最终被改变的是fake2,这个肯定是有一些内部转换机制在作怪,猜测应该讲传递过去的( CRect* pfakerect = (CRect*)&fakerect; pfakerect->SetColor(20);) pfakerect指针在被强转为CShape*的时候地址被自动偏移了4个字节,下面进行测试:

CShape类的SetColor方法,打印this指针地址:
void CShape::SetColor(int colore)
{
//TRACE_FUCTION_AND_LINE("");
TRACE_FUCTION_AND_LINE("pShape:%p", this);
m_color = colore;
}
main.cpp程序,打印传递过去的pfakerect地址:
CRect* pfakerect = (CRect*)&fakerect;
TRACE_FUCTION_AND_LINE("fakerect:%p", pfakerect);
pfakerect->SetColor(20);
输出结果:
           main:  27] fakerect:002DF958
CShape::SetColor:  12] pShape:002DF95C
 可以清楚的看到传递过去的地址是002DF958,而setColor在运行的时候处理的首地址却是002DF95C,这个应该是pfakerect转化为pShape时候的自动偏移的。
 
 为了更清楚的感觉到这种变化,现在直接在main.cpp加入测试代码:
main.cpp:
  CRect* pfakerect = (CRect*)&fakerect;
TRACE_FUCTION_AND_LINE("fakerect:%p", pfakerect);
pfakerect->SetColor(20);
CShape* pfakeshape = pfakerect;
TRACE_FUCTION_AND_LINE("pfakeshape:%p", pfakeshape);
输出结果如下:
           main:  27] fakerect:003EF77C
CShape::SetColor:  12] pShape:003EF780
           main:  30] pfakeshape:003EF780
可以看出fakerect的地址为003EF77C,在SetColor的时候进行强转的时候偏移了四个字节,当然我们通过CShape* pfakeshape = pfakerect直接进行强转的时候,还是会偏移4个字节。
结论:
对于SetColor这种很纯粹的方法来说,它位于CShape类,它仅仅看到CShape类的成员,进行成员赋值的时候,它也是根据自身的类的描述结果进行赋值,但是在将CRect*赋值给CShape*的时候,编译器会查看这个类的描述,发现CShape类没有虚函数,CRect类有虚函数,因此在CRect*转换为CShape*的时,编译器会进行自动4个字节的偏移。我认为这个应该是在编译器编译的时候自动做好的。由于我现在还没有达到能看懂编译器对代码所生成的目标代码的地步,也就没有继续向下追踪,如果你可以读懂它生成的目标代码,可以跟踪一下,是不是对于pfakerect转换为pfakeshape这一句的目标代码中,pfakeshape已经自动进行了偏移4个字节,我认为应该是编译器编译期的行为。


想做一个简单总结了:
OK,通过以上描述(说的比较乱,本身语言表达能力不行,还是边写边思考边测试的,望你理解,有问题可留言),上面的基本的讨论大部分都是针对普通成员变量m_color的赋值,非虚函数SetColor的运行机制。
1,首先讲述了继承的父类和子类的内存排布,主要包含了普通成员变量,vptr,以及字节对齐的内存(欲验证字节对齐的朋友,可以在CRect去掉虚函数再加一个double的成员变量就可以看到CRect的大小变化了。),也许你会问静态数据成员是不是位于对象里面,可以明确的答复你,不会,静态对象成员是位于类,不是位于对象的,因此不可能每一个对象存储一份copy。上面的三种其实就是整个对象的内存占用。 
2,然后讲述了普通成员函数SetColor的运行机制,它被编译成的目标代码实际上是以C的机制进行的,在进行rect1.SetColor(10);这样的调用的时候,实际上也是以C的方式进行的调用SetColor(10, &rect1),这个通过加入一个普通的SetColor函数,与它对比做了验证,还测试了时间消耗,也相差不大。还讨论了通过fakerect也确定SetColor的运行机制,它就可以当成很普通的C赋值,不管你传入的实际类型是什么,它所要做的就是对传递过来的地址进行一定m_color的偏移,然后赋值。
3,然后又讨论了拓展延伸一部分,简单描述了虚函数,虚函数对于对象内存大小的影响。一不小心又扯远了,讲到了typeinfo结构,讲到了typeinfo同样是占用了vptr所指向的vbtl指针表的一个表项,可以通过类型的强转从vbtl中读取typeinfo的结构与通过typeid取到的结构的地址进行对比,讨论了Vs2008的不一致和MINGW的一致,之所以可以比较,是基于一个类的typeinfo只有一份内存来进行的。每一个对象(含有虚函数的)会存有一个指针指向它(以实现运行期的动态类型识别),typeid可以获取到它(这个应该是编译器设置的,编译器就分配好typeinfo的内存,然后给typeid指令返回它的地址)。
4,然后又讨论了父类对象无虚函数,子类对象有虚函数,在进行SetColor的时候,内部机制到底是如何运行的。再次验证了对于SetColor来说,它只根据自己自身对象的CShape结果来决定对m_color所进行的偏移,对于父类没有虚函数,子类有虚函数,于是将CRect*赋值给CShape*时外部负责默认偏移了4个字节的地址,这样可以保证SetColor赋值时仅仅考虑自身的CShape的结构,那么这个SetColor就可以在编译期根据自身的结构来对m_color进行自身的偏移,编译生成一份唯一的目标代码,外部传递的参数来保证传递过来的有效性。于是我们看到了将CRect*结构传递给CShape*的时候,外部负责进行4个字节的偏移传递给SetColor的pShape,对于SetColor方法来说,它只考虑自身的结构。还阐述了这个是编译器的工作,不过仅仅是个人猜测,理论上会是这样,为了高效而言。
5,简单测试了一下,对于偏移和不偏移的情况,cpu耗费时间基本相同,可以大概确定是编译期进行的偏移,运行期进行偏移的话,时间消耗应该会大的多。不过这个需要能观测到目标代码的大牛来查看确定。


---------------------------2013/5/12 12:22:33---------------------------------------------------------
1,对虚函数的着迷,让我禁不住再看看虚函数表:
1)既然虚函数表中有一个虚函数位于上述的pp[0]的位置,那么我们可否取出来尝试调用一下以验证一些正确性呢,说实话,有点不合常理,不过上
述的取typeinfo的时候,不就是那么的不合常理吗?好东西就是用来折腾的。
main.cpp加入以下代码:
const type_info* ptypeidinfo = &(typeid(rect1));
TRACE_FUCTION_AND_LINE("ptypeidinfo: %p", ptypeidinfo);
int *p = (int*)&rect1;
int *pp = (int*)(p[0]);//vptr
type_info *prectinfo = (type_info*) (*(pp-1));//virtual function address
TRACE_FUCTION_AND_LINE("prectinfo: %p", prectinfo);

typedef void* pDisplayFunc(CRect* rect);
TRACE_FUCTION_AND_LINE("test begin:");
pDisplayFunc* pfunc = (pDisplayFunc*)(*pp);
pfunc(&rect1);
TRACE_FUCTION_AND_LINE("test end..");
pp是vptr的值,它指向的是一个vtbl,因此可以取出它的表的第0项,*pp就是它的第0项的值,它是一个函数指针,是指向display函数,因此根据上面的讨论可以判断它的C函数原型应该是void display(CRect*);的,故而将它强转为pDisplayFunc,然后通过pfunc(&rect1)进行调用,然后你会发现它真的就是调用的CRect的display方法。
输出如下,可以看到确实调用的CRect的display方法:
[                          main:  47] test begin:
[                CRect::display:  44]
[                          main:  50] test end..
2)再玩点传参的吧
Shape.h中CRect类再加入一个虚函数:
virtual void display1(int i){
TRACE_FUCTION_AND_LINE("i=%d", i);
}
main.cpp:
typedef void* pDisplayFunc1(int i, CRect* rect);
TRACE_FUCTION_AND_LINE("test begin:");
pDisplayFunc1* pfunc1 = (pDisplayFunc1*)(*(pp+1));
(*pfunc1)(8888, &rect1);
TRACE_FUCTION_AND_LINE("test end..");
前面关于虚函数表还没有详细说明,再加一个虚函数,它的位置是位于第1个item表项上,可以通过取*(pp+1)获得,调用传递的参数为8888.
打印结果如下:
[                          main:  53] test begin:
[               CRect::display1:  47] i=8888
不过我的vs2008随后报告了内存访问错误了,不知道为什么,但是可以看到函数还是调用了,证明预想不错。采用MINGW仿linux倒没有问题正常打印出了结果,应该两个环境的安全检查程度不一样。


3)再玩点C函数的感觉
函数的地址应该是编译期确定,不知道如何打印出来?
再看个更洋气的:既然pfunc1是display1的函数地址,那么我完全就不需要传入&rect1的参数了,传递一个NULL验证猜想。在(*pfunc1)(8888, &rect1);下面再加上一个调用(pfunc1)(8899, NULL);   如我所愿,还是正常打印出了8899,
打印结果(先注释掉前面那一句话,因为此方法引起内存访问错误):
[                          main:  53] test begin:
[               CRect::display1:  47] i=8899
4)继续新花样,更改vtbl看看效果。
将vtbl的第0个item和第1个item的内容交换一下,那么再调用display会发生什么情况呢?迫不及待啊。
很不幸,由于内存保护不让写,很是郁闷。但是有什么东西能够阻挡我们的兴趣呢?你保护,我自己制造一个rect,自己制造一个vtbl。基本思想是创建一个rectbuf的char数组保存rect1的内存,创建一个rectvtbl保存vtbl的项目,而且将rectbuf数组的前四个字节变成一个vptr指针指向rectvtbl的item0地址,由于这个vbtl表是我们自己的内存,因此是可以访问的,然后交换一下rectvtbl表项的两个函数指针的值即可.
测试中发现将display和display1交换,vs2008总是说内存访问错误,猜想是因为两个函数一个有参数一个无参数导致的,于是又增加了一个虚函数display2,它和display1的内容一模一样。
Shape.cpp:
virtual void display2(int i){
TRACE_FUCTION_AND_LINE("i=%d", i);
}
main.cpp:
char rectbuf[sizeof(rect1)];
memcpy(rectbuf, &rect1, sizeof(rect1));//分配内存保存rect1的内存
char rectvtbl[sizeof(int*) * 4];
memcpy(rectvtbl, pp-1, sizeof(int*)*4);//分配内存报错rectvtbl的内存
int *prectbuf = (int*)rectbuf;
int *pvtbl = (int*)((int*)rectvtbl + 1);//将pvtbl指向虚表的item0项目,因为前四个字节是typeinfo指针
prectbuf[0] = (int)(pvtbl);

CRect* fakecharRect = (CRect*)rectbuf;
//fakecharRect->display();
TRACE_FUCTION_AND_LINE("before deal: fakecharRect.display.....");
fakecharRect->display1(100);
fakecharRect->display2(100);//打印,正常调用。先调用display1,再调用display2

long temp = *(pvtbl+1);
*(pvtbl+1) = *(pvtbl+2);
*(pvtbl+2) = temp;//交换虚表的item1和item2项目,也就是display1和display2

TRACE_FUCTION_AND_LINE("after deal: fakecharRect.display.....");
//fakecharRect->display();
fakecharRect->display1(100);
fakecharRect->display2(100);//打印,惊讶调用。先调用display2,再调用display1(交换表项目所致)
输出结果如下:
[                          main:  61] before deal: fakecharRect.display....
[               CRect::display1:  54] i=100
[               CRect::display2:  57] i=100
[                          main:  69] after deal: fakecharRect.display.....
[               CRect::display2:  57] i=100
[               CRect::display1:  54] i=100
[                 CRect::~CRect:  34]
从日志打印的函数名字可以看到没有处理之前先调用diplay1,在调用display2。处理之后就变了。
上面的测试可以验证几个结论,不要把对象之类的东西看的过于神秘了,对象就是内存,你可以通过CRect rect1来创建对象,你也可以通过char数组来创建对象,完全取决于你。函数是什么?函数就是一个地址而已,那个地址是编译时确定的。由于我们知道了虚函数的一些机制,虚函数表项目中的内存实际代表的是哪一个函数,我们就可以通过交换表项目的内存,来进行一些匪夷所思的操作,将display1函数实际上调用的是display2,如果你想,可以自己随便写一个函数,然后将函数地址赋值到表的项目中,然后当你进行类的diplay函数调用的时候,你会惊讶的发现它竟然调用的是一个外部C函数,这个绝对会另你周围的人惊讶不已。当然玩这个可不是 为了哗众取宠,理解它可以让我们更懂得控制我们程序的行为,当程序出现一些百思不得解的诡异状况时,仔细思考一下程序的内部执行逻辑,你就会豁然开朗(相信我,你碰到的一般都不会涉及到如此的深入)。

想总结一下了:
OK,通过虚函数的描述,让我们知道了可以通过取虚函数表项目来进行函数调用,它可以如真实的C函数一样,我们可以通过更改虚函数的表项来操纵函数的调用,可以交换,可以给虚函数表项赋值。这样你将不再对虚函数感到很大的神秘性,它其实就是一个函数指针数组,里面存储着一堆当前类的虚函数而已。通过存储的虚函数可以达到运行时刻的动态调用。对于非虚函数,是编译器确定具体调用哪一个函数,因此会在目标代码中直接写死调用。还有前面说到不知道取类的函数地址,后面突然想到了一个方式,测试了一下还行。
代码如下:
char buf[100];
sprintf(buf, "%d", &(CRect::PrintMemory));//sprintf的目的主要是因为编译器编译期禁止我对函数指针强转为int,我绕了一个弯,做到了。
*(pvtbl) = atoi(buf);
TRACE_FUCTION_AND_LINE("after set CRect::PrintMemory function......");
fakecharRect->display();//打印,惊讶调用。调用PrintMemory方法
确定如此,调用的PrintMemory方法。此外我还简单测试了一下不同CRect对象的vptr,确实都是一样的,一个类一份内存。

-------------------------------------2013/5/12 15:07:42------------------------------------------------
虚函数的描述:
虚函数就是一个函数,它为了实现运行时的多态和类型识别,于是引入了虚函数表和typeinfo。所谓运行多态,就是基类指针通过指向不同的派生类对象,可以触发不同的函数调用,而函数调用代码却一样,真正的内部处理的时候,它就去查它的虚函数表。虚函数仅仅发生在通过指着或者引用上,直接的对象的调用操作不会引发虚函数。虚函数建立在基类的公有虚函数被派生类重写的情况,通过基类指针的不同指向,在运行期查表获取不同的函数执行。
在前面讲到的指针的赋值时,提到派生类指针赋值给基类指针会有一些编译器隐含操作。这让我不由对对象赋值产生一些兴趣。
CShape shape1 = rect1;
shape1.display();
由于前面说过,虚函数仅仅发生在指针和引用中,可以确定是调用CShape的display方法,但是具体shape1的内存又是如何呢?
先假设复制仅仅将rect1的基类部分切割给了shape1,也就是说shape1拥有了rect1的vptr,这个可以想象肯定会有问题,甚至如果CShape没有虚函数的时候,shape1就相当于多了一个vptr的内存,可以预见当父类有vptr,编译器会默认写入父类的vptr,如果父类没有vptr,编译器会将子类的vptr切割掉。这些都测试通过了印证,代码很简单就不贴出来了,通过在父类加入一个PrintMemory方法,可以很容易观察到这种变化。


未完待续:
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值