介绍
        初看到这个题目,你可能会有些疑惑: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
原文的第一版
文章出处:飞诺网(www.firnow.com):http://dev.firnow.com/course/3_program/c++/cppjs/200896/139707_2.html

转载于:https://my.oschina.net/alphajay/blog/5029

C++类对象创建过程揭密相关推荐

  1. python class用法_python原类、类的创建过程与方法

    [小宅按]今天为大家介绍一下python中与class 相关的知识-- 获取对象的类名 python是一门面向对象的语言,对于一切接对象的python来说,咱们有必要深入的学习与了解一些知识 首先大家 ...

  2. java方法区对象类型_浅谈Java内存区域与对象创建过程

    一.java内存区域 Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域.这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有的区域则 ...

  3. python原类、类的创建过程与方法

    今天为大家介绍一下python中与class 相关的知识-- 获取对象的类名 python是一门面向对象的语言,对于一切接对象的python来说,咱们有必要深入的学习与了解一些知识 首先大家都知道,要 ...

  4. Mybatis--SqlSession对象创建过程

    Mybatis--SqlSession对象创建过程 一. 源码阅读 二. 文字总结 三. 图像总结 mybatis是个很方便的框架,其中最重要的无疑就是session对象了.知其然,知其所以然才能不断 ...

  5. Java类加载及对象创建过程详解

    类加载过程 类加载的五个过程:加载.验证.准备.解析.初始化. 加载 在加载阶段,虚拟机主要完成三件事: 通过一个类的全限定名来获取定义此类的二进制字节流. 将这个字节流所代表的静态存储结构转化为方法 ...

  6. java 创建对象的init_Java 对象创建过程。init 方法和 clinit方法。

    Java 对象创建过程 判断是否加载.分配内存(指针碰撞或者空闲链表).初始化为零值.设置对象头(实例是哪个类的实例.类的元信息位置.GC 分代年龄等).init 方法. Java 虚拟机创建一个对象 ...

  7. 深入理解JVM的对象创建过程

    关于对象创建,有很多种方法.比如可以通过反射,或者通过 new关键字来创建.不管是何种方式,最终都是会创建一个对象.而我们平常工作中最常用的就是通过new关键字来创建对象.对于我们而言,只要new一下 ...

  8. JVM篇--详解对象创建过程-对象结构-对象访问方式

    hello,hello,刚学过的东西瞬间忘记,是我年龄大了还是年龄大了,可我明明才20出头啊(凑不要脸),其实25了,偏题了....今天整理一下关于JVM对象篇的结构,会记录对象创建过程,还有对象的内 ...

  9. JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配

    文章目录 前言 零.排序规范 1.happens-before原则 2.找文档位置 一.一线互联网企业关于对象面试题: (后面回答的就是这几个问题) 二.对象创建过程 三.对象在内存中的存储布局 1. ...

  10. python 元类的call_【原创】Python 对象创建过程中元类, __new__, __call__, __init__ 的处理...

    原始type: type是最原始的元类,其__call__方法是在你使用" t_class = type(classname_string, base_classes_tuple, attr ...

最新文章

  1. LeetCode简单题之换酒问题
  2. 单片机18b20c语言程序,AVR单片机控制DS18B20的示例C程序
  3. 正则表达式基础知识及应用(用于个人学习以及回顾)
  4. 列表、元组、字典与集合
  5. 【Linux系统编程】Linux 可执行文件结构与进程结构
  6. [JS性能优化专篇]
  7. android于src和background差额
  8. vmware挂载san存储_戏说 块存储 文件存储 对象存储
  9. Linux命令学习教程【建议收藏】
  10. 条件语句与循环语句:将数字一二三四五六七八九十转化成汉字大写的壹, 贰,叁,肆,伍,陆,柒,捌,玖,拾
  11. 蓝桥杯真题:等差素数列
  12. https的安全密钥
  13. 判断一个double类型的数是不是整数
  14. Android使用自带的文件管理器选择文件并读取内容
  15. 一步一回头撞在了南墙上
  16. 【Gulimall+】第三方服务:对象存储OSS、短信验证、社交登录、支付宝支付
  17. 2021年教你增加拿到BAT等大厂offer几率,分享PDF高清版
  18. tkinter的一些控件设置——listbox放大字体
  19. 【机器学习】数值分析02——任意方程求根
  20. 专利缴费后如何获取发票

热门文章

  1. (附源码)Flutter Demo
  2. Android图片的三级缓存整理
  3. C语言 rtmp测试代码,在mac本地搭建rtmp服务器用于测试
  4. vue中接收到后台数据再回显到输入框时无法修改
  5. mgr未同步 mysql_MySQL MGR--数据同步原理
  6. 新建模块 pom.xml依赖无法识别_使用模块依赖关系,第2部分
  7. C# 使用RabbitMQ(二)安装
  8. 安卓应用,在吾手机上正常,在另外手机上崩溃,因为缺少so库
  9. 一句“哭什么哭”,说得好
  10. 研究表明,越早退休越长寿