转载: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 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类的基础上再添加一个虚函数:
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 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=
从这些代码中的
我们可以清晰的看到,在构造函数的最开始,在进入构造函数体内部,甚至是在进入初始化列表之前,编译器会插入代码用当前正在被构造的类的虚表地址给虚表指针赋值。
后记
如果不是亲自实践和分析,很难想象一个简单的类对象创建过程竟然蕴涵了这么多秘密。了解了这些秘密为我们解决其他的一些问题打开了胜利之门。
试试下面的一些问题,不知道在你看完本文后是否能够有一种豁然开朗的感觉:
1. 为什么C++需要提供初始化列表?那些情况下必须实现初始化列表? (提示:有些情况下只能初始化不能赋值)
2. 构造函数可以是虚函数呢?在构造函数中调用虚函数会有什么样的结果? (提示:虚表指针是在构造函数的最开始初始化的)
3. 构造函数和赋值操作符operator=有什么区别? (提示:区分初始化和赋值)
历史记录
07/29/2007 v1.0
原文的第一版
相关推荐
C++ 类与对象习题,都是入门知识,给初学者参考^-^
json 字符串转成c++类对象,json 字符串转成c++类对象
c++ 类和对象 c++ 类和对象 c++ 类和对象
c++类与对象实习举例。
C++类对象的拷贝构造函数 C++类对象的拷贝构造函数 C++类对象的拷贝构造函数 C++类对象的拷贝构造函数 C++类对象的拷贝构造函数 C++类对象的拷贝构造函数
实现c++根据类名创建c++ 对象,一个文件简单明了,,,,,
c++中由类名(string)创建这个类对象(RTTI)
使用Jni实现对C++类库调用的实例,主要方法是利用了C++中的指针机制和jlong类型的对接,从而在java中能够调用c++。
C++类与对象C++类与对象C++类与对象C++类与对象C++类与对象C++类与对象C++类与对象C++类与对象C++类与对象C++类与对象C++类与对象C++类与对象
c++ 类和对象讲解
C++ 类的思想,面向对象是实现迷宫C++ 类实现迷宫C++ 类的思想,面向对象是实现迷宫C++ 类实现迷宫C++ 类的思想,面向对象是实现迷宫C++ 类实现迷宫
C++大学课本中非常实用又非常难理解的动态创建对象,我做了个简单的实例供大家参考学习
1、C++创建对象方式 在C++中我们可以采用如下两种方式来创建对象, 1 Dog dog;//Dog为类名 2 Dog *p = new Dog(); 这两种方式在C++中都能完成对象的创建,但是在内存中的处理却完全不同。 对于...
C++类与对象课件 C++类与对象课件 C++类与对象课件 C++类与对象课件 C++类与对象课件 C++类与对象课件 C++类与对象课件 C++类与对象课件 C++类与对象课件
你从C#中很容易在运行时得到某个引用的类型信息。但是C++也有运行时类型信息(奇怪,C++编译后是二进制代码,怎么会有类型信息呢)。本文档就是从PE文件和内存揭示C++到底如何实现运行时类型信息和类型转换。
C++ 创建线程互斥对象的实例源码下载,声明线程函数,创建线程,程序睡眠,释放互斥对象,设置事件对象为无信号状态,生成控制台程序,仅供参考。
涉及各种情况下C++对象的sizeof大小,包括单一类对象,继承,重复继承 多继承 单一虚继承 等各种情况下的对象大小。对C++对象内存布局有清楚了解。
C++类C++类C++类C++类C++类C++类C++类C++类C++类