C++类对象内存模型与成员函数调用分析(下)

AI权益加码!Claude Code、Cursor等20+工具免费用! 购周边限时加赠Coding Plan Lite,畅享主流AI工具!学习进阶更高效! 阅读详情

2.4.2 多重继承下的虚拟函数

多重继承下的虚拟函数主要有一下几个麻烦:

1.         几个父类都声明了相同原型的virtual函数;

2.         有不止一个父类将其析构函数声明为虚拟;

3.         一般的虚拟函数问题;

先给出代码段9

class Parent1{

public:

   Parent1() : data_parent1(0.0){}

   virtual ~Parent1(){cout<<"Parent1::~Parent1()"<<endl;}

   virtual void speakClearly(){cout<<"Parent1::speakClearly()"<<endl;}

   virtual Parent1* clone() const{cout<<"Parent1::clone()"<<endl; return null;}

protected:

   int data_parent1;

};

 

class Parent2{

public:

   Parent2() : data_parent2(1.0){}

   virtual ~Parent2(){cout<<"Parent2::~Parent2()"<<endl;}

   virtual void mumble(){cout<<"Parent2::mumble()"<<endl;}

   virtual Parent2* clone() const{cout<<"Parent2::clone()"<<endl; return null;}

protected:

   int data_parent2;

};

 

class Child : public Parent1, public Parent2

{

public:

   Child() : data_child(2.0){}

   virtual ~Child(){cout<<"Child::~Child()"<<endl;}

   virtual Child* clone() const{cout<<"Child::clone()"<<endl; return null;}

protected:

   int data_child;

};

就内存布局而言,有了前面的基础了,猜得出来大概是个什么样子了。好吧,我们就先猜一把,然后再写段代码验证验证。对于数据成员,多重继承使用的就是各自分配一段空间“叠放”在一起,如之前的图4所示。对于虚拟函数,其实就是多了个vptr嘛,也放进去不久结了吗?

嗯,所以我们可以猜想了,见图8

 

接下来就是调试验证了,调试代码段10如下:

typedef void(*Fun)(void);

 

int main()

{

    Child c;

    Fun pFun;

 

    int** pVtbl = (int**)&c;

 

    cout << "[0] Parent1::_vptr->" << endl;

    pFun = (Fun)pVtbl[0][0];

    cout << "     [0] ";

    pFun();

 

    pFun = (Fun)pVtbl[0][1];

    cout << "     [1] ";

    pFun();

 

    cout << "    Parent1.data_parent1 = " << (int)pVtbl[1] << endl;

 

    int s = sizeof(Parent1)/4;

    cout << "[" << s << "] Parent2::_vptr->"<<endl;

    pFun = (Fun)pVtbl[s][0];

    cout << "     [0] "; pFun();

 

    pFun = (Fun)pVtbl[s][1];

    cout << "     [1] "; pFun();

 

    s++;

    cout << "    Parent2.data_parent2 = " << (int)pVtbl[s] << endl;

 

    s++;

    cout << "[3] Child.data_child = " << (int)pVtbl[s] << endl;

}

需要的是,这段代码的运行要将虚析构函数注释掉,理由应该很好理解吧,对象都被析构掉了,指针也就成为悬挂指针了,SIGSEGV就会触发。Code::Blocks(GCC 4.5.2)下运行结果如下:

 

 

再次说明一下,因为我们注释掉了虚析构函数那一行,所以上面的输出中没有Child::~Child()之类的信息。所以,我们的猜想图8是正确的。

根据《Inside The C++ Object Model》一书,关于多重继承主要有三种情况要仔细考虑,对着图8,这三种情况其实都是浮云。

1.       通过一个“指向第二个父类,如Parent2”的指针,调用子类的虚拟函数。请看代码段11

Parent2* pP2 = new Child;

// 下面的代码将调用Child::~Child()

// 因此pP2必须被向后调整sizeof(Parent1)bytes,由编译器和运行期信息参与完成

delete pP2;

还是回到图8,注意到一个问题,Parent1Child指针所指向的位置是一样的(如果都是取的同一个Child对象的地址),但是Parent2不是,它与Parent1Child的指针之间存在一个偏移量,看下面的代码就知道了:

Child c;

Parent1* pP1 = &c;

Parent2* pP2 = &c;

Child* pC = &c;

cout<<pP1<<"/n"<<pC<<"/n"<<pP2<<"/n/n";

 

运行结果如下图:

 

 

pP1pC内容一样,pP2pP1pC之间存在8个字节的偏移量(8个字节是由一个4字节int变量和一个4字节指针引起的)。

对于代码段11,因为pP2指向Child对象中Parent2子对象处,为了能够正确执行,pP2必须调整到Child对象起始处。

1.       通过一个“指向Child类”的指针,调用Parent2中一个继承而来的虚拟函数。在这种情况下,子类指针必须再次被调整,以指向第二个父类Parent2处。例如:

Child* pC = new Child;

// 调用Parent2::mumble()

// pC必须被向前调整sizeof(Parent1)bytes,由编译器和运行期信息参与完成

pC->mumble();

2.       第三种情况发生在一个语言扩充性质之下:允许一个虚拟函数的返回值类型有所变化(注意,返回值类型不是激活C++重载机制的充分条件),可能是父类类型,也可能是子类类型,这一点通过clone()函数来描述,看下面代码:

Parent2* pP1 = new Child;

// 调用Child* Child::clone()

// 返回值必须被调整,以指向Parent2子对象,由编译器和运行期信息参与完成

Parent2* pP2 = pP1->clone();

当进行pP1->clone()时,pP1会被调整到指向Child对象的起始地址,于是cloneChild版会被调用,它会传回一个指针,指向一个新的Child对象,该对象的地址在被指定给pP2之前,必须经过调整,以指向Parent2子对象处。

之前的注释中都有一句话,“XXX必须被调整,以指向Parent2子对象,由编译器和运行期信息参与完成,确实,就是编译器会去做的事情,我们也不用管,因为这也是compiler-dependent的。

2.4.3 虚继承下的虚拟函数

虚拟继承的出现就是为了解决重复继承中多个间接父类的问题的,经典继承结构图就是环形继承链:

class PP {……};

class P1 : virtual public PP{……};

class P2: virtual public PP{……};

class C : public P1, public P2{…… };

虚拟继承是个有点麻烦乃至无聊的东西,实践中不被推荐,一般来说:

1)      先是P1,然后是P2,接着是C,而PP这个超类都放在最后的位置;

2)      各个类内部的布局与多重继承一样;

3C++对象模型总结

大道至简,如果理解了前面的文字,下面四句话应该就差不多了:

l  非静态数据成员都存放在对象所跨有的地址空间中,静态数据成员则存放于对象所跨有的地址空间之外;

l  非虚拟成员函数(静态和非静态)也存放于对象所跨有的地址空间之外,且编译器将其改写为普通的非成员函数的形式(以求降低调用开销);

l  对于虚拟成员函数,则借助vtblvptr支持。

l  对于继承关系,子类对象跨有的地址空间中包含了父类对象的实体,通过嵌入type-info信息进行识别和虚函数调用。

4、参考资料

[1] Inside The C++ Object ModelLippaman,124章。

[2] C++对象内存布局,陈皓专栏, http://blog.csdn.net/haoel/archive/2008/10/15/3081328.aspx

 

 

C++笔记(《C++新经典》) C++新经典第1章 C/C++1.1 C和C++语言的起源、特点、关系讲解范畴1.2 C/C++语言市场需求就业需求分析1.3 再谈C/C++就业1.4 搭建开发语言环境第2章 数据型、运算符表达式2.1 常量、变量、整形、实型和字符型第3章 程序的基本结构和语句第4章 程序的基本结构和语句第5章 循环控制第6章 数组第7章 函数第8章 编译预处理第9章 指针第10章 结构体公用体第11章 位运算第12章 文件第13章 C++基本语言第14章 第15章 模板泛型第16章 智能指针第17章 并发 阅读详情

相关推荐

C++类对象内存模型成员函数调用分析(上)

C++类对象内存模型是一个比较抓狂的问题,主要是C++特性太多了,所以必须建立一个清晰的分析层次。一般而言,讲到C++对象,都比较容易反应到以下这个图表:这篇文章,就以这个表格作为分析和行文的策略的纵向指导;横向上,兼以考虑无继承、单继承、多重继承及虚拟继承四方面情况,这样一来,

左手为你画情 1万+

C++对象模型详解[收集].pdf

C++对象模型详解[收集].pdf

图说C++对象模型.pdf

网上看到的一篇博客,对C++对象内存模式讲的详细易懂。

C++对象内存模型.pdf

C++对象内存模型.pdf

7本经典C++教程PDF打包.rar

7本经典C++教程PDF打包下载

C++ 新经典:对象模型——目录(先留框架,慢慢来~)

C++ 新经典:对象模型——目录

慢慢记录,慢慢更新。 949

道阻且长之C++对象模型

前言 本系列博客参考狂想老师网易云课程和其《C++新经典 对象模型》,《深度探索C++对象模型》; 强烈推荐想在C++领域进阶的同学进行深入的学习

doubleintfloat的博客 1528

C++类对象内存模型成员函数调用分析

C++类对象内存模型成员函数调用分析(上) C++类对象内存模型成员函数调用分析(中) C++类对象内存模型成员函数调用分析(下)

464

C++类对象内存模型成员函数调用分析(中)

2.4 虚拟成员函数这是本文中最复杂也最有趣的话题了。虚拟函数也是和继承这个话题相伴相生,所以本节将纳入对单继承、多重继承和虚拟继承,一起描述他们之间的关系,这样,对C++对虚拟函数调用,以及由此所变现出来的多态的理解,应该是非常清晰了。2.4.1 单继承下的虚拟成员函数对于虚

左手为你画情 5009

C++对象模型

C++模型之菱形虚拟继承中的内存分布

上一篇: C++类对象内存模型与成员函数调用分析(中)
下一篇: 关于C++的学习,对一位网友的回复
fairyroad
博客等级 码龄17年 82粉丝 50原创
评论 8
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值