`
guoyiqi
  • 浏览: 1019046 次
社区版块
存档分类
最新评论

(转)c++对象内存分析4

 
阅读更多

前言本章节是4个课题的最后一个,我们将讨论多重继承情况下,对象内存的布局。阅读本文,请思考下面的问题:当子类从多个基类继承,虚函数指针和成员变量将如何布局?编译器如何进行子类和基类之间类型转换?如果多个基类具有同样的虚函数,子类选择哪个实现来调用?如果子类重写该虚函数,那么它覆盖的是哪个基类的实现呢?
多重继承我们将分析这样的例子:CFinal类继承自CBasic类和CBasic1类;CBasic类和CBasic1类都定义有虚函数add和minus;CBasic类和CBasic1类都定义有成员变量int i;子类CFinal重写了虚函数add;子类CFinal增加了新的虚函数AVG。类图如下:


代码:
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 CBasic1
{
public:
virtual int add(int a, int b)
{
cout<<"CBasic1::Add"<<endl;
return a+b;
}
virtualint minus(int a, int b)
{
cout<<"CBasic1::Minus"<<endl;
return a-b;
}
int i;
int iBasic1;
};

class CFinal:public CBasic,public CBasic1
{
int add(int a, int b)
{
cout<<"CFinal::Add"<<endl;
return a+b;
}
virtualint AVG(int a, int b)
{
cout<<"CFinal::AVG"<<endl;
return (a+b)/2;
}
int iFinal;
};
构造CFinal类对象:
CFinal *f=new CFinal;

我们还是用Watch窗口来观察对象的布局:



我们发现,在Watch窗口中打印f->__vfptr是不允许的,这是因为f中有2个虚函数指针,编译器不知道你想引用的是哪一个,因此我们需要把f转换为基类类型才能打印__vfptr,对于成员变量int i也是同样的。通过对内存布局的观察,我们得到这样的CFinal类内存结构图:

我们发现:
1)CBasic类对象位于CFinal类对象的前端,相应的,CBasic类的虚函数指针位于CFinal类对象的最前端。这是由于在定义CFinal类时,我们把CBasic类写在前面,编译器把它作为主基类。因此,编译器将在CBasic类的虚函数表表尾增加一个元素,来储存子类新增加的虚函数AVG的地址(请参考分析(2)中关于虚函数AVG在虚函数表中的位置的分析)。
2)CBasic1对象开始于紧接着CBasic对象结束的位置,CFinal类新增的成员变量存储在CFinal对象的尾端。
3)对于子类CFinal重写的add方法,在CBasic的虚函数表和CBasic1的虚函数表中,对应的元素都重定向为指向CFinal类的实现。对于CBasic1类来说,这种重定向是通过Thunk技术来实现的(特指VC++。本文将不讨论Thunk技术)。
下面我们讨论章节开始提出的问题,如果我们用f对象调用minus方法,哪个基类的实现会被调用?同样我们也尝试调用成员变量i,因为2个基类都定义了它。运行下面的代码:
int _tmain(int argc, _TCHAR* argv[])
{
CFinal *f=new CFinal;
CBasic *b=(CBasic*)f;
f->minus(4,3); //编译错误,错误码C2385
int x=f->i; //编译错误,错误码C2385
b->minus(4,3); //成功
int y=b->i; //成功 return 0;
}

我们发现,直接调用minus函数或者i是不被允许的,因为编译器不知道你想调用的是哪个基类的实现!然而,通过类型转换来指定特定的基类再进行调用,则可以成功。
下一个问题,编译器如何进行子类和基类间的类型转换。对于主基类CBasic来说,这不成问题,因为它位于CFinal对象的最前端,不需要进行指针调整。那么转换为CBasic1类型呢?编译器会在CFinal对象指针的基础上加上12字节,跳过CBasic类对象从而指向CBasic1对象。你可能有这样的问题,为什么编译器知道要加上12字节,而不是13,14字节呢?这是因为编译器知道CFinal对象的布局,它清楚的知道CBasic1对象在CFinal对象中的偏移地址。如果CBasic对象的长度改变了,比如长度增加到16,需要重新编译整个程序,这样使用了CFinal对象的部分在分配地址和类型转换时,也将做出相应的改变。
关于多态。对于子类中重写(override)的虚函数,在子类所有的虚函数表中对应的元素都被重定向为指向子类的新实现(如果基类有此虚函数的话),因此,无论是转换为哪一个基类,多态都能被实现。

结论让我们试着为多重继承的情况做出结论(如果你对上面的内容重复一遍没有兴趣,跳过这段):
当子类从2个(或多个)带虚函数的基类继承时
1)子类中,主基类的对象内存位于子类对象内存的最前端,相应地,主基类的虚函数指针地址等于子类对象地址。
2)子类新增加的虚函数,将在主基类的虚函数表尾增加新的元素元素,来指向其实现。
3)子类中其他基类的对象位于紧接着主基类结束后的地址。
4)子类新增的成员变量位于子类对象内存的尾端。
5)在子类中重写(override)的基类虚函数,在其所有基类的虚函数表中,对应的元素都覆盖为指向子类新实现的地址(通过THUNK技术实现)。
6)对于多个基类中都定义过的虚函数,如果子类没有重写它,子类对象是不能直接调用的,因为编译器不知道你希望调用的是具体哪个基类的实现。同样,在多个子类中定义的成员变量,也不能被子类对象直接调用。
5)子类对象转换为基类类型时,除非是主基类,需要进行指针调整,指针加上若干字节,跳过位于其前端的其他基类占用的地址,从而指向需要转换的基类。编译器之所以精确的知道需要偏移的地址,因为它通过类定义清楚地知道子类对象的内存布局。
6)关于多态。对于子类中重写(override)的虚函数,在子类所有的虚函数表中对应的元素都被重定向为指向子类的新实现(如果基类有此虚函数的话),因此,无论是转换为哪一个基类,多态都能被实现。

其他情况 至此,本文已经把所有经典常见的对象布局情况进行了研究,下面我们再简要的看几个更复杂的情形。
1,菱形多重继承。CFinal类的基类CBasic类和CBasic1类有共同的基类CInitial。
类图:

分析:这种情况和Subject4中多重继承的情况没有不同。对于编译器来说,CFinal类对2个基类的处理方式没有因为他们有共同的基类而有什么不同,只是在CFinal类对象中的CBasic类和CBasic类的内部又都分别包含了一个CInitial类。我们在Subject4种得到的所有结论在此都是适用的。

2,主基类没有虚函数的多重继承。考虑在Subject4的情况中,把CBasic类中的2个虚函数去掉,使其没有虚函数,在定义CFinal时仍把CBasic写在前面作为主虚函数。
类图:

分析:这种情况下,虽然我们把CBasic写在前面,但是CFinal类事实上把CBasic1类,即带虚函数指针和虚函数表的基类作为主基类,把它布局在对象的最前端。内存结构图:

分享到:
评论

相关推荐

    C++对象内存模型.pdf

    本文将通过实验和分析来探索 C++ 对象内存模型,并讨论对象内存结构、简单类型相关数据、包含虚函数类的对象内存结构、继承下的多态性等问题。 1. 实验基础 在 C++ 中,我们可以使用 sizeof 运算符来获取对象的...

    C++对象内存布局

    ### C++对象内存布局 #### 1. 最简单的类 在C++中,理解对象的内存布局对于深入学习语言特性非常关键。通过分析一个简单的类`CTest`,我们可以更好地了解对象是如何在内存中分配和组织的。 ##### 1.1.1 赋值语句...

    C++对象内存分析

    对C++模型的认识可以从本质上提高对语言和各种机制的理解,如果对底层机制一无所知,那么很多高级的机制都只能通过死记硬背的方式来运用,而且有时候有错误,也很难找出原因。C++相对与C语言,编译器做了很多的对...

    C++对象内存池 ---- C++侦探改写.rar

    本资源"ObjPool.h"可能是一个实现了C++对象内存池的头文件,由"C++侦探改写",可能是对原内存池实现的分析和改进。下面我们将深入探讨C++对象内存池的原理、设计以及可能的优化策略。 内存池的基本思想是预先分配一...

    C++对象模型在内存中的实现

    C++对象模型在内存中的实现,讲述了类,继承以及虚继承的内存布局;成员变量和成员函数的访问已经访问时的开销情况,包含虚函数的情况,考察构造函数,析构函数,以及特殊的赋值操作符成员函数是如何工作的,数组是...

    深入探索c++对象模型 设计模式c++ pdf

    C++对象模型是C++语言的核心,它涉及到内存管理、类结构、对象生命周期、继承、多态等关键概念。在《Inside The C++ Object Model》这本书中,作者深入浅出地解析了这些概念,让读者能了解C++编译器如何将源代码转化...

    内存管理 c++ C++内存回收

    在C++中,当一个对象创建在堆上时,程序员负责在适当的时间调用`delete`来释放内存。如果忘记释放或者无法确定何时释放,就会导致内存泄漏。内存泄漏虽然可能不会立即引发问题,但随着程序运行时间的增长,累积的未...

    深度探索C++对象模型(简体中文版).pdf

    特别地,本书还分析了C++对象模型在运行时的语义,以及对象模型的一些边缘问题。 C++作为一门强类型语言,它的编译器对于程序员编写的代码会做出很多“手脚”,例如自动进行内存管理、调用构造函数和析构函数、处理...

    C++内存泄露检测器

    4. **内存泄漏检测库**:一些第三方库,如LeakSanitizer(部分集成于GCC和LLVM编译器),可以集成到项目中,为程序提供内存泄漏检测。这些库在运行时监控内存分配,当程序结束时报告未释放的内存。 5. **自定义内存...

    深度探索C++对象模型

    《深度探索C++对象模型》是一本专注于C++编程语言底层机制的专业书籍,它揭示了C++对象在内存中的表示方式以及对象模型的工作原理。这本书是面向已经对C++有一定基础理解的开发者,旨在帮助他们深入理解C++的内部...

    C++内存池实现

    4. **内存回收**:当对象不再使用时,不是直接归还给操作系统,而是返回给内存池,放入相应的空闲链表,以便后续再次使用。 5. **碎片控制**:由于内存池内部管理内存,可以避免碎片问题,提高内存利用率。 在C++...

    分析c++对象在内存中的布局情况.pdf

    标题中的“分析c++对象在内存中的布局情况”是指探讨C++编程中对象在内存中的存储方式,包括成员变量的排列、内存对齐原则以及如何通过特定编译器选项(如VS2010的/d1reportSingleClassLayout)来查看这种布局。...

    pascal转c++_pascal转c++教程_

    Pascal,作为一种结构化编程语言,强调清晰的代码结构和严格的类型检查,而 C++ 则是面向对象的编程语言,以其灵活性、性能和广泛的库支持而闻名。当我们需要将 Pascal 代码转换为 C++ 时,我们需要理解两者之间的...

    C++对象模型.pdf

    《深度探索C++对象模型》这本书,由Stanley B. Lippman撰写,侯捷翻译,由华中科技大学出版社出版。本书致力于深入解析C++编译器在处理C++代码时所采取的复杂对象模型及其背后的底层机制,特别是针对构造函数、解构...

    几种内存池的实现(c/c++ 源码)

    在C/C++编程中,内存池常用于频繁创建和销毁小对象的场景,如网络编程、数据库连接等。本文将深入探讨几种内存池的实现方式及其源码分析。 1. **静态内存池**: 静态内存池在程序启动时就分配好内存,且在程序运行...

    C++ 内存管理算法和实现

    C++中可以使用静态分析工具如Valgrind,或者在代码中加入特定的检测机制,如引用计数,来帮助检测内存泄漏。 七、C++11及以后的内存管理改进 从C++11开始,标准库增加了对内存管理的支持,例如引入了右值引用...

    C++对象池源码示例

    总的来说,C++对象池是一种优化技术,它通过集中管理对象的生命周期,减少了内存分配和释放的开销,提升了程序运行效率。理解和掌握对象池的设计与实现,对于提升C++编程能力、优化系统性能具有重要意义。

    C++转C#的自动工具

    C++和C#虽然都是面向对象的编程语言,但它们在语法、内存管理、类型系统和库支持等方面存在显著差异。 C++是一种静态类型的、编译式的、通用的、大小写敏感的、不仅支持过程化编程,也支持面向对象编程的编程语言。...

    C++代码转java工具

    5. **内存管理**:C++使用手动内存管理,而Java有自动垃圾回收,工具需要处理这两者之间的差异。 6. **函数调用和库集成**:如果C++代码使用了库,工具可能需要处理库的迁移问题,或者提供Java的等价实现。 标签...

    C++内存检测器

    标题 "C++内存检测器" 指向的是一个用于检测C++程序中内存泄漏问题的工具或技术。在C++编程中,由于手动管理内存的特性,开发者需要自行负责内存的分配与释放。如果不小心忘记释放已分配的内存,就会导致内存泄漏,...

Global site tag (gtag.js) - Google Analytics