`

C++类对象创建过程揭密(转载)

 
阅读更多

转载:http://blog.csdn.net/houdy/article/details/1714906

 

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

分配空间(Allocation)
       创建C++类对象的第一步就是为其分配内存空间。对于全局对象,静态对象以及分配在栈区域内的对象,对它们的内存分配是在编译阶段就完成了,而对于分配在堆区域内的对象,它们的分配是在运行是动态进行的。内存空间的分配过程涉及到两个关键的问题:

  • 需要分配空间的大小,即类对象的大小。这个问题对于编译器来说并不是什么问题,因为类对象的大小就是由它决定的,对于要分配多少内存,它最清楚不过了。
  • 是否有足够的内存空间来满足分配。对于不同的情况我们需要具体问题具体分析:
    • 全局对象和静态对象。编译器会为他们划分一个独立的段(全局段)为他们分配足够的空间,一般不会涉及到内存空间不够的问题。
    • 分配在栈区域的对象。栈区域的大小由编译器的设置决定,不管具体的设置怎样,总归它是有一个具体的值,所以栈空间是有限的,在栈区域内同时分配大量的对象会导致栈区域溢出,由于栈区域的分配是在编译阶段完成的,所以在栈区域溢出的时候会抛出编译阶段的异常
    • 分配在堆区域的对象。堆内存空间的分配是在运行是进行的,由于堆空间也是有限的,在堆区域内试图同时分配大量的对象会导致导致分配失败,通常情况会抛出运行时异常或者返回一个没有意义的值(通常是0)。

初始化(Initialization)
       这一阶段是对象创建过程中最神秘的一个阶段,也是最容易被忽视的一个阶段。要想知道这一阶段具体完成那些任务,关键是要区分两个容易混淆的概念:初始化 (Initialization)和赋值(Assignment)。初始化早于赋值,它是随着对象的诞生一起进行的。而赋值是在对象诞生以后又给予它一个新的值。这里我想到了一个很好的例子:任何一个在医院诞生的婴儿,在它诞生的同时医院会给它一个标识,以防止和其他的婴儿混淆,这个标识通常是婴儿母亲所在床铺的编号,医院给婴儿一个标识的过程可以看作是初始化。当然当婴儿的父母拿到他们会为他们起个名字,起名字的过程就可以看作是赋值。经过初始化和赋值后,其他人就可以通过名字来标识他们的身份了。区分了这两个概念后,我们再转到对对象初始化的分析上。对类对象的初始化,实际上是对类对象内的所有数据成员进行初始化。C++已经为我们提供了对类对象进行初始化的能力,我们可以通过实现构造函数的初始化列表(member initialization list)来实现。具体的情况是否是这样的呢?下面我们就看看具体的情况是什么样的吧。我写了两个简单的类:

class CInnerClass {
public:
    CInnerClass(int id):m_iID(id) {}
    CInnerClass& operator = (const CInnerClass& rb) {
        m_iID = rb.m_iID;
        return *this;
    }
private:
    int m_iID;
};

class CJdBase {
public:
    CJdBase::CJdBase(int id):m_innerObj(id),m_iID(id){
        m_innerObj = 10;
    }
private:
    CInnerClass m_innerObj;
    int m_iID;
};

 

       我们重点是看看CJdBase类的构造函数。CJdBase类的构造函数提供了初始化列表,用来初始化其成员变量,其相应的汇编代码如下(注:我只保留了关键的代码):

    mov    DWORD PTR _this$[ebp], ecx
    mov    eax, DWORD PTR _id$[ebp]
    push    eax
    mov    ecx, DWORD PTR _this$[ebp]
    call    
??0CInnerClass@@QAE@H@Z            ; CInnerClass::CInnerClass
    mov    eax, DWORD PTR _this$[ebp]
    mov    ecx, DWORD PTR _id$[ebp]
    mov    DWORD PTR [eax
+4], ecx

5    :     m_innerObj = 10;

    push    
10                    ; 0000000aH
    lea    ecx, DWORD PTR $T1359[ebp]
    call    
??0CInnerClass@@QAE@H@Z            ; CInnerClass::CInnerClass
    lea    eax, DWORD PTR $T1359[ebp]
    push    eax
    mov    ecx, DWORD PTR _this$[ebp]
    call    
??4CInnerClass@@QAEAAV0@ABV0@@Z        ; CInnerClass::operator=

       从这段汇编代码中我们可以看到一些有意义的内容:

  • 初始化列表先于构造函数体内的代码执行
  • 初始化列表确实执行的是数据成员的初始化过程,这个可以从成员对象的构造函数被调用看的出来。

 

赋值(Assignment)
       对 象经过初始化以后,我们仍然可以对其进行赋值。和类对象的初始化一样,类对象的赋值实际上是对类对象内的所有数据成员进行赋值。C++也已经为我们提供了 这样的能力,我们可以通过构造函数的实现体(即构造函数中由"{}"包裹的部分)来实现。这一点也可以从上面的汇编代码中成员对象的赋值操作符 (operator =)被调用得到印证。

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

虚函数表指针(VTable Pointer)
       我们怎么可能会忽视虚函数表指针呢?如果没有它的话,C++世界会清净很多。我们最关心的是对于那些拥有虚函数的类,它们的类对象中的虚函数表指针是什么时 候赋值的?我们没有任何代码,也没有任何能力(当然暴力破解的方法除外)能够在类对象创建的时候给其虚表指针赋值,给虚表指针赋值是编译器偷偷完成的。这 里有一个细节可能经常会被我们忽略:编译器给虚表指针赋值是发生在进入到构造函数体之前还是在构造函数体内部?下面我们就看看具体的情况是什么样的吧。在 上面的CJdBase类的基础上再添加一个虚函数:

 

    class CJdBase {
    
public:
        CJdBase::CJdBase(
int id):m_innerObj(id),m_iID(id){
            m_innerObj = 10;
        }
    
public:
        
virtual void dumpMe() {}
    
private:
        CInnerClass m_innerObj;
        
int m_iID;
    };

       使用VS2002编译获得这个构造函数的汇编代码,其中最关键的一些代码如下:

    mov    DWORD PTR _this$[ebp], ecx
    mov    eax, DWORD PTR _this$[ebp]
    mov    DWORD PTR [eax], OFFSET FLAT:
??_7CJdBase@@6B@
    mov    eax, DWORD PTR _id$[ebp]
    push    eax
    mov    ecx, DWORD PTR _this$[ebp]
    add    ecx, 
4
    call    
??0CInnerClass@@QAE@H@Z            ; CInnerClass::CInnerClass
    mov    eax, DWORD PTR _this$[ebp]
    mov    ecx, DWORD PTR _id$[ebp]
    mov    DWORD PTR [eax
+8], ecx

5    :     m_innerObj = 10;

    push    
10                    ; 0000000aH
    lea    ecx, DWORD PTR $T1368[ebp]
    call    
??0CInnerClass@@QAE@H@Z            ; CInnerClass::CInnerClass
    lea    eax, DWORD PTR $T1368[ebp]
    push    eax
    mov    ecx, DWORD PTR _this$[ebp]
    add    ecx, 
4
    call    
??4CInnerClass@@QAEAAV0@ABV0@@Z        ; CInnerClass::operator=

从这些代码中的

mov    DWORD PTR [eax], OFFSET FLAT:??_7CJdBase@@6B@

我们可以清晰的看到,在构造函数的最开始,在进入构造函数体内部,甚至是在进入初始化列表之前,编译器会插入代码用当前正在被构造的类的虚表地址给虚表指针赋值

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

历史记录
07/29/2007   v1.0
原文的第一版

分享到:
评论

相关推荐

    json字符串转换c++类对象

    在C++编程中,将JSON字符串转换为C++类对象是一项常见的任务,特别是在处理网络通信、数据存储或配置文件时。JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析...

    Java调用c++类对象

    这里,`new CppClass()`创建了C++类的对象,`cppObject->doSomething()`则调用了C++类的方法。注意,由于内存管理在不同语言间可能存在差异,所以必须手动管理C++对象的生命周期,防止内存泄漏。 在完成C++的实现后...

    c++ rtti 根据类名创建对象

    实现c++根据类名创建c++ 对象,一个文件简单明了,,,,,

    c++面向对象编程实例大全

    首先,C++的面向对象特性主要包括类(Class)、对象(Object)、封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism)。类是面向对象编程的基础,它定义了一组数据(属性)和操作这些数据的方法(函数...

    C++面向对象程序设计 经典例题 附练习题

    1. **类**:类是创建对象的模板或蓝图,它定义了对象的状态(数据成员)和行为(成员函数)。例如,你可以创建一个名为“Student”的类,包含姓名、年龄等属性和学习、参加活动等方法。 2. **对象**:对象是类的...

    c++面向对象程序设计课后习题答案

    在C++中,面向对象的核心概念包括类(Class)、对象(Object)、继承(Inheritance)、多态(Polymorphism)和封装(Encapsulation)。以下是对这些概念的详细解释: 1. **类(Class)**:类是面向对象编程的基础,...

    c++面向对象程序设计(第6版)Walter Savitch 书中的 c++题库

    1. **类(Class)**: C++中的类是面向对象编程的基础,它定义了一组数据和操作这些数据的方法。通过类,我们可以创建具有特定属性和行为的对象。类的设计需要遵循封装原则,将数据和方法隐藏在内部,仅通过公共接口与...

    深度探索c++对象模型(2012版本)

    C++的继承机制允许创建新的类(派生类)来扩展已存在的类(基类)。派生类不仅可以拥有基类的所有特性,还可以添加新的成员或重写基类的方法。这种设计模式支持代码复用和面向对象编程的原则,如封装和多态。 多态...

    C++动态生成对象

    在C++编程中,动态生成对象是指在程序运行时创建对象,而不是在编译时确定。这通常是通过使用new运算符来实现的。动态生成对象的主要优点是可以在运行时根据需要分配内存,增加代码的灵活性和可扩展性。在描述中提到...

    《C++面向对象程序设计》第2版编程题答案

    在C++中,类是创建对象的蓝图,它定义了对象的状态(数据成员)和行为(成员函数)。构造函数是类的一个特殊函数,用于初始化新创建的对象,而析构函数则在对象生命周期结束时自动调用,用于释放资源。继承是类之间...

    C++new动态创建对象简单易懂的实例

    C++大学课本中非常实用又非常难理解的动态创建对象,我做了个简单的实例供大家参考学习

    c++面向对象程序设计答案 陈维新 林小茶

    C++是一种支持OOP的强类型、静态类型的编程语言,它结合了过程化编程的效率和面向对象编程的灵活性。 1. 类(Class):类是面向对象编程的核心,它是对象的蓝图或模板。一个类定义了一组属性(数据成员)和方法...

    Visual C++ 面向对象编程教程——王育坚

    在《Visual C++ 面向对象编程教程——王育坚》中,你可能会学到如何创建和管理MFC应用程序,如何定义和使用类,以及如何利用MFC的控件库构建用户界面。教程中的例题将帮助你理解如何在实际项目中运用这些概念。例如...

    Java创建对象与C++创建对象的比较

     1、C++创建对象方式  在C++中我们可以采用如下两种方式来创建对象, 1 Dog dog;//Dog为类名 2 Dog *p = new Dog();  这两种方式在C++中都能完成对象的创建,但是在内存中的处理却完全不同。  对于...

    深度探索C++对象模型 PDF

    这本书旨在帮助开发者更好地理解C++中的内存管理、类型系统、类层次结构以及对象生命周期等关键概念。通过阅读本书,你可以提升对C++底层机制的洞察力,从而编写出更高效、更稳定的代码。 C++对象模型是C++编程的...

    C#调用C++DLL导出类

    3. 创建一个C#托管类,持有C++对象的指针,并提供相应的C#方法调用C++方法。 4. 确保正确处理对象生命周期,防止内存泄漏。 请注意,这只是一个基本示例,实际应用中可能需要处理更复杂的情况,如错误处理、数据...

    Visual C++面向对象与可视化程序设计 黄维通 课后习题答案程序及debug

    通过分析和运行这些代码,你可以更直观地理解书中的理论概念,如类的设计、对象的创建与操作、继承、多态性、虚函数等面向对象特性。同时,解题过程中的debug信息有助于你识别和解决常见的编程错误,如内存泄漏、...

    C++面向对象程序设计习题解析与上机指导

    它旨在帮助读者深入理解C++中的类、对象、继承、多态等核心概念,并通过习题解答和上机实践来巩固理论知识。 在C++语言中,面向对象编程(Object-Oriented Programming,OOP)是一种重要的编程范式,其核心思想是将...

    C++ 面向对象程序设计(第七版) 周靖 译

    C++支持过程化编程和面向对象编程,是多范式语言。了解C语言的基础是学习C++的前提,如变量、数据类型、控制结构、函数等。 2. **面向对象编程**:面向对象编程的三大核心概念是封装、继承和多态。封装是将数据和...

    C++类和对象(2013级-C++程序设计)

    在C++程序设计中,类和对象的应用非常广泛,从简单的数据结构如链表、栈、队列等,到复杂的系统如图形用户界面、游戏对象、网络通信协议等,都用到了类和对象的概念。例如,可以创建一个学生类,包含学生姓名、年龄...

Global site tag (gtag.js) - Google Analytics