`
weiyinchao88
  • 浏览: 1235183 次
文章分类
社区版块
存档分类
最新评论

C++类对象创建过程揭密

 
阅读更多

介绍
初 看到这个题目,你可能会有些疑惑:C++类对象的创建还有什么好说的,不就是调用构造函数么?实际上情况并不是想象中的那么简单,大量的细节被隐藏或者被 忽略了,而这些细节又是解决一些其他问题的关键,所以我们很有必要深入到这块"神秘"的区域,去探索鲜为人知的秘密。

分配空间(Allocation)
创建C++类对象的第一步就是为其分配内存空间。对于全局对象,静态对象以及分配在栈区域内的对象,对它们的内存分配是在编译阶段就完成了,而对于分配在堆区域内的对象,它们的分配是在运行是动态进行的。内存空间的分配过程涉及到两个关键的问题:
  • 需要分配空间的大小,即类对象的大小。这么问题对于编译器来说并不是什么问题,因为类对象的大小就是由它决定的,对于要分配多少内存,它最清楚不过了。
  • 是否有足够的内存空间来满足分配。对于不同的情况我们需要具体问题具体分析:
    • 全局对象和静态对象。编译器会为他们划分一个独立的段(全局段)为他们分配足够的空间,一般不会涉及到内存空间不够的问题。
    • 分配在栈区域的对象。栈区域的大小由编译器的设置决定,不管具体的设置怎样,总归它是有一个具体的值,所以栈空间是有限的,在栈区域内同时分配大量的对象会导致栈区域溢出,由于栈区域的分配是在编译阶段完成的,所以在栈区域溢出的时候会抛出编译阶段的异常。
    • 分配在堆区域的对象。堆内存空间的分配是在运行是进行的,由于堆空间也是有限的,在栈区域内试图同时分配大量的对象会导致导致分配失败,通常情况会抛出运行时异常或者返回一个没有意义的值(通常是0)。
初始化(Initialization)
这 一阶段是对象创建过程中最神秘的一个阶段,也是最容易被忽视的一个阶段。要想知道这一阶段具体完成那些任务,关键是要区分两个容易混淆的概念:初始化 (Initialization)和赋值(Assignment)。初始化早于赋值,它是随着对象的诞生一起进行的。而赋值是在对象诞生以后又给予它一个 新的值。这里我想到了一个很好的例子:任何一个在医院诞生的婴儿,在它诞生的同时医院会给它一个标识,以防止和其他的婴儿混淆,这个标识通常是婴儿母亲所 在床铺的编号,医院给婴儿一个标识的过程可以看作是初始化。当然当婴儿的父母拿到他们会为他们起个名字,起名字的过程就可以看作是赋值。经过初始化和赋值 后,其他人就可以通过名字来标识他们的身份了。区分了这两个概念后,我们再转到对对象初始化的分析上。对类对象的初始化,实际上是对类对象内的所有数据成 员进行初始化。C++已经为我们提供了对类对象进行初始化的能力,我们可以通过实现构造函数的初始化列表(member initialization list)来实现。具体的情况是否是这样的呢?下面我们就看看具体的情况是什么样的吧。我写了两个简单的类:
classCInnerClass{
public:
CInnerClass(
intid):m_iID(id){}
CInnerClass&
operator=(constCInnerClass&rb){
m_iID=rb.m_iID;
return*this;
}
private:
intm_iID;
};

classCJdBase{
public:
CJdBase::CJdBase(
intid):m_innerObj(id),m_iID(id){
m_innerObj=10;
}
private:
CInnerClassm_innerObj;
intm_iID;
};
我们重点是看看CJdBase类的构造函数。CJdBase类的构造函数提供了初始化列表,用来初始化其成员变量,其相应的汇编代码如下(注:我只保留了关键的代码):
movDWORDPTR_this$[ebp],ecx
moveax,DWORDPTR_id$[ebp]
pusheax
movecx,DWORDPTR_this$[ebp]
call
??0CInnerClass@@QAE@H@Z;CInnerClass::CInnerClass
moveax,DWORDPTR_this$[ebp]
movecx,DWORDPTR_id$[ebp]
movDWORDPTR[eax
+4],ecx

;
5:m_innerObj=10;

push
10;0000000aH
leaecx,DWORDPTR$T1359[ebp]
call
??0CInnerClass@@QAE@H@Z;CInnerClass::CInnerClass
leaeax,DWORDPTR$T1359[ebp]
pusheax
movecx,DWORDPTR_this$[ebp]
call
??4CInnerClass@@QAEAAV0@ABV0@@Z;CInnerClass::operator=
从这段汇编代码中我们可以看到一些有意义的内容:
  • 初始化列表先于构造函数体内的代码执行;
  • 初始化列表确实执行的是数据成员的初始化过程,这个可以从成员对象的构造函数被调用看的出来。
赋值(Assignment)
对 象经过初始化以后,我们仍然可以对其进行赋值。和类对象的初始化一样,类对象的赋值实际上是对类对象内的所有数据成员进行赋值。C++也已经为我们提供了 这样的能力,我们可以通过构造函数的实现体(即构造函数中由"{}"包裹的部分)来实现。这一点也可以从上面的汇编代码中成员对象的赋值操作符 (operator =)被调用得到印证。

结束
随着构造函数执行完最后一行代码,可以说类对象的创建过程也就顺利完成了。由以上的分析可以看出,构造函数实现了对象的初始化和赋值两个过程:对象的初始化是通过初始化列表来完成,而对象的赋值则才是通过构造函数,或者更准确的说应该是构造函数的实现体。

虚函数表指针(VTable Pointer)
我 们怎么可能会忽视虚函数表指针呢?如果没有它的话,C++世界会清净很多。我们最关心的是对于那些拥有虚函数的类,它们的类对象中的虚函数表指针是什么时 候赋值的?我们没有任何代码,也没有任何能力(当然暴力破解的方法除外)能够在类对象创建的时候给其虚表指针赋值,给虚表指针赋值是编译器偷偷完成的,具 体的时机是在进入到虚函数后,在给对象的数据成员初始化和赋值之前,编译器偷偷的给虚表指针赋值。下面我们就看看具体的情况是什么样的吧。在上面的 CJdBase类的基础上再添加一个虚函数:
classCJdBase{
public:
CJdBase::CJdBase(
intid):m_innerObj(id),m_iID(id){
m_innerObj=10;
}
public:
virtualvoiddumpMe(){}
private:
CInnerClassm_innerObj;
intm_iID;
};
使用VS2002编译获得这个构造函数的汇编代码,其中最关键的一些代码如下:
movDWORDPTR_this$[ebp],ecx
moveax,DWORDPTR_this$[ebp]
movDWORDPTR[eax],OFFSETFLAT:
??_7CJdBase@@6B@
moveax,DWORDPTR_id$[ebp]
pusheax
movecx,DWORDPTR_this$[ebp]
addecx,
4
call
??0CInnerClass@@QAE@H@Z;CInnerClass::CInnerClass
moveax,DWORDPTR_this$[ebp]
movecx,DWORDPTR_id$[ebp]
movDWORDPTR[eax
+8],ecx

;
5:m_innerObj=10;

push
10;0000000aH
leaecx,DWORDPTR$T1368[ebp]
call
??0CInnerClass@@QAE@H@Z;CInnerClass::CInnerClass
leaeax,DWORDPTR$T1368[ebp]
pusheax
movecx,DWORDPTR_this$[ebp]
addecx,
4
call
??4CInnerClass@@QAEAAV0@ABV0@@Z;CInnerClass::operator=
从这些代码中的
movDWORDPTR[eax],OFFSETFLAT:??_7CJdBase@@6B@
我们可以清晰的看到,在构造函数的最开始,在进入构造函数体内部,甚至是在进入初始化列表之前,编译器会插入代码用当前正在被构造的类的虚表地址给虚表指针赋值。

后记
如果不是亲自实践和分析,很难想象一个简单的类对象创建过程竟然蕴涵了这么多秘密。了解了这些秘密为我们解决其他的一些问题打开了胜利之门。
试试下面的一些问题,不知道在你看完本文后是否能够有一种豁然开朗的感觉:
1. 为什么C++需要提供初始化列表?那些情况下必须实现初始化列表? (提示:有些情况下只能初始化不能赋值)
2. 构造函数可以是虚函数呢?在构造函数中调用虚函数会有什么样的结果? (提示:虚表指针是在构造函数的最开始初始化的)
3. 构造函数和赋值操作符operator=有什么区别? (提示:区分初始化和赋值)

历史记录
07/29/2007 v1.0
原文的第一版
分享到:
评论

相关推荐

    ATL 揭秘之“对象创建”篇

    对象创建揭秘之旅 ATL(Active Template Library)在幕后帮助我们完成了这些工作。但要深入理解ATL的工作机制,我们需要探究其内部细节。本文将逐步揭示ATL如何创建和管理COM对象的过程。 #### 2. “对象”探讨 ...

    超级玛丽C++编程揭秘源码+文档,超级玛丽c语言源代码,C,C++源码.zip

    《超级玛丽C++编程揭秘源码+文档》是一份珍贵的学习资源,包含了使用C++语言重制经典游戏“超级玛丽”的全过程。这份资料不仅提供了源代码,还搭配了详细的文档,使得学习者能够深入理解C++编程在游戏开发中的应用。...

    超级玛丽C++编程揭秘源码+文档

    1. **面向对象编程(OOP)**:C++支持面向对象编程,通过类和对象来组织代码。游戏中,可以创建不同的类来代表角色(如玛丽、敌人、道具等),每个类包含其属性(如位置、速度、生命值等)和行为(如移动、跳跃、...

    .net2.0面向对象编程揭秘

    《.NET 2.0 面向对象编程揭秘》是一本深入探讨.NET框架2.0版本中面向对象编程技术的书籍。这本书旨在揭示面向对象编程在.NET平台上的实现细节和最佳实践,帮助开发者掌握核心的面向对象概念,并将其有效地应用到实际...

    逆向C++(中文版).pdf

    3. **构造函数的识别**:构造函数通常会在对象创建后立即被调用,可以通过查找new操作符后面紧跟的函数调用来识别构造函数。此外,构造函数往往包含初始化类成员的操作。 ##### B. 识别类 1. **多态类的识别**: ...

    超级玛丽制作揭秘 C++学习文件

    在C++中,你可以使用类来定义这些对象,每个类包含其属性(如位置、速度)和方法(如移动、跳跃)。 5. **音频管理**:游戏音效和背景音乐也是不可或缺的一部分。你可以利用OpenAL或SDL Mixer等库来管理和播放音频...

    超级玛丽C++编程揭秘源码+文档,超级玛丽c语言源代码,C,C++

    1. **C++语言基础**:C++是一种面向对象的编程语言,它在C语言的基础上增加了类、模板、命名空间等特性。在这个项目中,开发者将使用C++的面向对象特性来构建游戏中的角色、场景、物理系统等对象,通过继承、封装和...

    C++开发的超级玛丽

    在实现超级玛丽的过程中,我们可能需要创建一个`GameEngine`类来管理游戏循环,处理用户输入,并调用各游戏对象的方法进行更新和渲染。 游戏中的角色和物体需要能够移动和响应碰撞。C++中的坐标系统和数学运算可以...

    Professional C++(第4版)

    - **第十三章:揭秘C++输入/输出** 讲解了C++中的输入输出机制,包括文件流、格式化输出等。 - **第十四章:错误处理** 探讨了如何在C++中优雅地处理异常情况,确保程序稳定运行。 - **第十五章:重载C++运算符*...

    超级玛丽源码c++(附文档)

    1. **面向对象编程**:C++支持面向对象编程,源码中可以看到各种类的定义,如角色类(Mario)、敌人类(Goomba)、砖块类(Brick)等,这些类封装了各自的行为和属性,体现了OOP的核心思想。 2. **图形渲染**:源码...

    visual C++超级玛丽 完整源代码

    在《超级玛丽制作揭秘》中,你将有机会学习到游戏开发的基本流程,包括游戏循环(Game Loop)、对象的状态管理、碰撞检测算法以及游戏规则的实现。通过分析和实践这份源代码,你不仅可以掌握C++编程基础,还能对游戏...

    深入编程内幕——Visual C++

    1. **建立应用程序**:创建一个MFC应用程序通常从派生自CWinApp的类开始,这是整个应用的核心,负责初始化和退出过程。 2. **窗口类**:CWnd类是所有窗口类的基类,通过派生CWnd创建特定类型的窗口,如对话框、视图...

    MATLAB揭秘修订版(2011-3-4)_matlab揭秘_matlab_

    此外,MATLAB支持面向对象编程,通过类和对象的概念,用户可以构建复杂的程序结构和模块化设计。 MATLAB的文件输入/输出功能也是实用的工具,包括读写文本文件、二进制文件以及与Excel等其他软件的数据交换。这使得...

    Visual C++应用框架揭密

    ### Visual C++应用框架揭秘:深入理解文档管理与文件操作 #### 一、引言 在Visual C++中,Microsoft提供了强大的应用框架,极大地简化了应用程序的开发过程。通过使用这些框架,开发者能够快速构建出具有基本功能...

    揭秘 Kotlin 编程原理

    2. **继承**:继承允许创建新的类来继承现有类的属性和方法。在Kotlin中,通过关键字`open`和`class`来实现继承。值得注意的是,Kotlin默认不允许类被继承(除非显式声明为`open`),这一设计有助于减少继承带来的...

    andrid框架揭秘 中文

    《Android框架揭秘》是一本深入探讨Android系统框架的著作,主要涵盖了从系统初始化到Java框架服务的全过程。这本书由一位韩国作者撰写,以其独特的视角和深入浅出的讲解,为读者揭示了Android操作系统背后的复杂...

    浅谈C++中虚函数实现原理揭秘

    在C++中,虚函数是实现多态性的重要机制,允许通过基类指针调用派生类重写的成员函数。虚函数的实现原理主要涉及虚拟表(VTABLE)和虚指针(VPTR)的概念。 首先,让我们理解虚拟表(VTABLE)。每个具有虚函数的类...

    揭秘Java虚拟机

    其中,堆用于存储对象实例,栈用于存储方法调用,方法区存储类信息。垃圾回收器负责自动回收不再使用的内存,防止内存泄漏。 5. **垃圾回收**:JVM的垃圾回收机制是自动内存管理的关键,它识别并清理无引用的对象。...

Global site tag (gtag.js) - Google Analytics