前言本章节讨论单继承情况下类对象的内存特性。阅读时请思考这几个问题:从子类到基类的类型转换,编译器做了什么?多态是怎么实现的?类的成员函数(包括虚函数)和普通函数有什么区别吗?
Subject2:从带虚函数的基类继承的子类类CFinal是我们要分析的目标,它从CBasic中继承而来,重写(override)了虚函数add;增加了一个新的虚函数;增加了一个成员变量iFinal,类图如下:
代码:
class CBasic
{
public:
CBasic()
{
Array=new int[2];
}
int i;
int *Array;
virtual int add(int a, int b)
{
return a+b;
}
virtualint minus(int a, int b)
{
return a-b;
}
void HelloWorld()
{
cout<<"hello world"<<endl;
}
};
class CFinal:public CBasic
{
public:
int add(int a, int b)
{
cout<<"CFinal::add"<<endl;
return a+b;
}
virtualint AVG(int a, int b)
{
return (a+b)/2;
}
int iFinal;
};
构造CFinal类的对象
CFinal *f=new CFinal;
我们还是采用上一节中使用Wacth窗口来观察对象内存的办法,如下图所示:
仔细观察上图打印的元素的地址,我们发现:1)虽然CFinal增加了一个新的虚函数,但是子类并没有因此增加一个新的虚函数指针来指向新的虚函数表,CFinal类仍然只有一个虚函数指针位于对象的最前端,因此,虚函数表”应该“在表尾增加一个单元来存储新增加的虚函数AVG。 2)由于虚函数add被重写(override),虚函数表第一个元素也从指向CBasic::add方法覆盖为指向CFinal::add方法。我们很容易画出对象的内存结构图:
前面我们提到虚函数AVG被增加到了虚函数表的表尾单元时,我们说“应该”是这样的。这是因为我们现在还缺乏证据,在watch窗口中打印f->__vfptr[2]的值,显示错误码CXX0072:Error:type information missing or unknown。 难道我们的分析错误了吗? 不是的。现在我们来试图证明这点。
阅读下面的代码:
typedef int (*Fun)(int a,int b);
int _tmain(int argc, _TCHAR* argv[]);
{
CFinal *f=new CFinal;
Fun fun1=NULL;
Fun fun2=NULL;
Fun fun3=NULL;
fun1=(Fun)(int*)*((int *)(*(int*)f)+ 0);
fun2=(Fun)(int*)*((int *)(*(int*)f)+ 1);
fun3=(Fun)(int*)*((int *)(*(int*)f)+ 2);
return 0;
}
注意11行,我们知道,虚函数指针的地址和对象的地址是相同的,虚函数指针指向虚函数表的起始地址,然后我们再从虚表的起始地址加上2个4字节,就索引到虚函数表的第三个元素。
Watch窗口:
果然,虚函数表的第三个元素储存AVG方法的地址。
我们试着用函数指针fun3来调用AVG方法,看看是不是能成功:
fun3(1,3);
跟踪代码发现,断点确实进入了AVG函数中,但是很不幸,接着发生了异常。这是由于在C++中,类成员函数和普通函数的实现是有差别的,我们这里定义的是普通函数的函数指针来调用类成员函数,所以会失败。[1]
在总结本文之前,我们再来分析最后2个问题:
1)CFinal类的对象转换为CBasic类型时,编译器做了什么呢?
2)多态是怎么实现的?(在本课题的讨论范围之内)
先来看第一个问题。从CFinal类的内存结构图我们已经可以很清楚的看到,CFinal对象内存空间的前12个字节和虚函数表的前2个元素,正好是CBasic类对象的结构!很神奇不是嘛,这也正是C++编译器这么布局的目的。当CFinal类对象转换为CBasic类型时,对象指针地址不变,只是对象被限制只允许访问内存的前12个字节和虚函数表的前2个元素即完成了转换。
回答了第一个问题,第二个问题的答案就很简单了。当CFinal类对象转换为CBasic类型后,调用add方法时,由于事实上使用的是CFinal类的虚表,虚表的第一个元素指向CFinal::add而不是CBasic::add。于是,CBasic类对象调用了CFinal类的add实现,是为多态。
Subject2:结论和上一节一样,让我们试着为本章做个结论:
对于一个从带虚函数的基类继承的子类:
1)对象内存中依然只有一个虚函数指针处于最前端,虚函数表的末尾将增加新的元素来储存子类新虚函数的地址。
2)对于重写(override)的虚函数,虚函数表中对应的元素将从指向基类方法覆盖为指向子类新实现的方法地址。
3)子类新添加的成员变量,将添加在对象内存的尾端。
4)当把子类对象转换为基类类型时,指针指向的地址不发生变化[2],但是对象访问的内存和虚表范围被缩小了,限制在基类对象能访问的范围内。编译器能做到这一点,是由于子类对象内存和虚函数表的前面部分刚好和基类对象完全吻合。
5)根据2)和4)我们很容易得到多态是如何实现的:子类对象转换为基类类型后,指向的虚函数表其实是子类的虚函数表,由于重写(override)的虚函数在该虚函数表中指向了子类新实现的方法,所以,基类对象(从子类转换得到)调用该方法即调用子类的实现,是为多态。
注释[1]在默认情况下,C++类成员函数使用的函数调用约定是__thiscall,而普通函数使用的是__cdecl。__thiscall方式被使用时,调用者(caller)把this指针传递给ECX寄存器(当CPU是x86构架),然后从右向左把参数压入堆栈,函数结束时,由函数本身(被调用者,callee)清理堆栈;__cdecl方式,调用者从右向左把参数压入堆栈,函数结束时,由调用者清理堆栈。我们这里使用普通函数指针调用类成员函数,将会造成2个错误:1)this指针没有被调用者压入堆栈。 2)函数体内堆栈已经被清理,但是函数结束后,caller又试图清理堆栈。
解决方案:把AVG函数定义为
virtual int __cdecl AVG(int a, int b)
这时,将由caller清理堆栈,this指针将作为隐含的第一个参数传入。
把函数指针定义为:
typedef int (*Fun)(CFinal *f,int a,int b);
调用改为:
(*fun3)(f,1,3);
[2]当子类从不带虚函数的子类继承,或者多重继承的时,可能要进行指针调整,后2个章节将涉及到这2种情况。
分享到:
相关推荐
本文将通过实验和分析来探索 C++ 对象内存模型,并讨论对象内存结构、简单类型相关数据、包含虚函数类的对象内存结构、继承下的多态性等问题。 1. 实验基础 在 C++ 中,我们可以使用 sizeof 运算符来获取对象的...
### C++对象内存布局 #### 1. 最简单的类 在C++中,理解对象的内存布局对于深入学习语言特性非常关键。通过分析一个简单的类`CTest`,我们可以更好地了解对象是如何在内存中分配和组织的。 ##### 1.1.1 赋值语句...
对C++模型的认识可以从本质上提高对语言和各种机制的理解,如果对底层机制一无所知,那么很多高级的机制都只能通过死记硬背的方式来运用,而且有时候有错误,也很难找出原因。C++相对与C语言,编译器做了很多的对...
本资源"ObjPool.h"可能是一个实现了C++对象内存池的头文件,由"C++侦探改写",可能是对原内存池实现的分析和改进。下面我们将深入探讨C++对象内存池的原理、设计以及可能的优化策略。 内存池的基本思想是预先分配一...
C++对象模型在内存中的实现,讲述了类,继承以及虚继承的内存布局;成员变量和成员函数的访问已经访问时的开销情况,包含虚函数的情况,考察构造函数,析构函数,以及特殊的赋值操作符成员函数是如何工作的,数组是...
C++对象模型是C++语言的核心,它涉及到内存管理、类结构、对象生命周期、继承、多态等关键概念。在《Inside The C++ Object Model》这本书中,作者深入浅出地解析了这些概念,让读者能了解C++编译器如何将源代码转化...
2. **动态分析工具**:如Valgrind、LeakSanitizer等,它们在程序运行时监测内存分配和释放,找出未释放的内存块。 3. **自定义内存管理**:编写智能指针(如`std::unique_ptr`、`std::shared_ptr`)或其他内存管理类...
2. **动态分析**:在程序运行过程中进行,实时监测内存分配和释放。比如Valgrind,这是一个强大的内存错误检测工具,其中的Memcheck子工具专门用于检测内存泄漏。它会记录每个内存块的分配和释放,如果程序结束时仍...
特别地,本书还分析了C++对象模型在运行时的语义,以及对象模型的一些边缘问题。 C++作为一门强类型语言,它的编译器对于程序员编写的代码会做出很多“手脚”,例如自动进行内存管理、调用构造函数和析构函数、处理...
《深度探索C++对象模型》是一本专注于C++编程语言底层机制的专业书籍,它揭示了C++对象在内存中的表示方式以及对象模型的工作原理。这本书是面向已经对C++有一定基础理解的开发者,旨在帮助他们深入理解C++的内部...
通过阅读和分析这个项目,开发者可以深入理解内存池的实现细节,学习如何在C++中有效地管理内存,这对于优化程序性能、减少系统资源消耗是非常有价值的。同时,这也为理解和实现其他内存管理技术,如智能指针、垃圾...
标题中的“分析c++对象在内存中的布局情况”是指探讨C++编程中对象在内存中的存储方式,包括成员变量的排列、内存对齐原则以及如何通过特定编译器选项(如VS2010的/d1reportSingleClassLayout)来查看这种布局。...
Pascal,作为一种结构化编程语言,强调清晰的代码结构和严格的类型检查,而 C++ 则是面向对象的编程语言,以其灵活性、性能和广泛的库支持而闻名。当我们需要将 Pascal 代码转换为 C++ 时,我们需要理解两者之间的...
《深度探索C++对象模型》这本书,由Stanley B. Lippman撰写,侯捷翻译,由华中科技大学出版社出版。本书致力于深入解析C++编译器在处理C++代码时所采取的复杂对象模型及其背后的底层机制,特别是针对构造函数、解构...
在C/C++编程中,内存池常用于频繁创建和销毁小对象的场景,如网络编程、数据库连接等。本文将深入探讨几种内存池的实现方式及其源码分析。 1. **静态内存池**: 静态内存池在程序启动时就分配好内存,且在程序运行...
C++中可以使用静态分析工具如Valgrind,或者在代码中加入特定的检测机制,如引用计数,来帮助检测内存泄漏。 七、C++11及以后的内存管理改进 从C++11开始,标准库增加了对内存管理的支持,例如引入了右值引用...
总的来说,C++对象池是一种优化技术,它通过集中管理对象的生命周期,减少了内存分配和释放的开销,提升了程序运行效率。理解和掌握对象池的设计与实现,对于提升C++编程能力、优化系统性能具有重要意义。
C++和C#虽然都是面向对象的编程语言,但它们在语法、内存管理、类型系统和库支持等方面存在显著差异。 C++是一种静态类型的、编译式的、通用的、大小写敏感的、不仅支持过程化编程,也支持面向对象编程的编程语言。...
2. **类型映射**:理解C++和Java类型之间的差异,并进行适当的转换,如C++的int到Java的int,或者C++的指针到Java的对象引用。 3. **结构转换**:处理C++的类和对象到Java的类和对象的转换,包括构造函数、继承、...
标题 "C++内存检测器" 指向的是一个用于检测C++程序中内存泄漏问题的工具或技术。在C++编程中,由于手动管理内存的特性,开发者需要自行负责内存的分配与释放。如果不小心忘记释放已分配的内存,就会导致内存泄漏,...