zoukankan      html  css  js  c++  java
  • 从汇编看c++中的默认构造函数

    c++中,如果为一个类没有明确定义一个构造函数,那么,编译器就会自动合成一个默认的构造函数。下面,通过汇编程序,来看一下其真实情况。

    c++中的源程序:

    class X {
    private:
        int i;
    };
    
    int main() {
        X x;
    }
        

    上面的类X没有定义构造函数,仅仅有一个int i。

    下面为其汇编程序:

    ; 7    : int main() {
    
        push    ebp;ebp为一个寄存器,总是指向一个函数调用堆栈的栈底,作为基址,用偏移量来访问该调用栈上的变量,但这里没有任何变量要访问,因此不起作用 
        mov    ebp, esp;这两句的作用是为了保存调用main之前堆栈的基址ebp的值,并将ebp指向main调用栈的栈底
        push    ecx;将寄存器ecx的值压栈, 栈顶指针esp向前移动4byte
                   ;这句的作用,为即将要创建的对象预留了4byte的空间,并向里面写入ecx的值
    
    ; 8    :     X x;
    ; 9    : }
    
        xor    eax, eax;eax也是一个寄存器,这里不起作用
        mov    esp, ebp;将栈顶指针移动到push ecx前的位置,即释放了4byte的空间
        pop    ebp;恢复基址到main调用之前的状态
        ret    0;函数返回

    通过汇编发现,通过push ecx,编译器将堆栈栈顶移动4byte,并将寄存器的ecx的值写入,类X只含有一个int,大小刚好为4byte,因此这一句可以看成是为对象x分配空间。而接下来并没有任何函数的调用,来对这一块区域进行适当的初始化。所以,在没有明确定义一个构造函数的时候,不会有任何的初始化操作。

    这种情况下,在《深度探索c++对象模型》中,认为编译器只合成了一个无用的默认构造函数。

    下面再看一段c++程序:

    class X {
    private:
        int i;
        int j;//增加一个成员变量int j
    };
    
    int main() {
        X x;
    }
        

    与上面相比,在类X里面增加了一个成员变量int j,类的大小变为8字节。

    下面为对应汇编码:

    ; 8    : int main() {
    
        push    ebp
        mov    ebp, esp
        sub    esp, 8; 栈顶指针移动8byte,刚好等于类X的大小
    
    ; 9    :     X x;
    ; 10   : }
    
        xor    eax, eax
        mov    esp, ebp
        pop    ebp
        ret    0

    从汇编码看出,通过sub esp,8指令,堆栈确实留出了8byte的空间,刚好等于类X的大小,同样没有调用任何函数,来进行初始化操作。

    所以,综上所述,在一个类没有明确定义构造函数的时候,编译器不会有任何的函数调用来进行初始化操作,仅仅是移动栈顶留出对象所需空间,也就是说,这种情况下,编译器根本不会提供默认的构造函数。

    那么,书上说的由编译器提供默认的构造函数到底是怎么一回事呢?

    下面看第一种情况,类里面有虚成员函数:

    c++源码如下:

    class X {
    private:
        int i;
        int j;//增加一个成员变量int j
    public:
        virtual ~X() {
            
        }
    };
    
    
    int main() {
        X x;
    }
        

    析构函数为虚函数

    下面是main函数对应的汇编码:

    ; 13   : int main() {
    
        push    ebp
        mov    ebp, esp
        sub    esp, 12                    ; 为对象x预留12byte的空间,成员变量int i,int j占8byte,由于有虚函数,因此vptr指针占4byte
    
    ; 14   :     X x;
    
        lea    ecx, DWORD PTR _x$[ebp];获取x对象的首地址,存入ecx寄存器
        call    ??0X@@QAE@XZ;这里调用x的构造函数
    
    ; 15   : }
    
        lea    ecx, DWORD PTR _x$[ebp];获取对象x的首地址
        call    ??1X@@UAE@XZ                ; 调用析构函数
        xor    eax, eax
        mov    esp, ebp
        pop    ebp
        ret    0

    可以看到,对象x的构造函数被调用了,编译器确实合成了默认的构造函数。

    下面是构造函数的汇编码:

    ??0X@@QAE@XZ PROC                    ; X::X, COMDAT
    ; _this$ = ecx
        push    ebp
        mov    ebp, esp
        push    ecx
        mov    DWORD PTR _this$[ebp], ecx;ecx寄存器存有对象x的首地址
        mov    eax, DWORD PTR _this$[ebp];将对象x的首地址给寄存器eax
        mov    DWORD PTR [eax], OFFSET ??_7X@@6B@;这里设置vptr指针的值,指向vtable (OFFSET ??_7X@@6B@是获得vtable的地址)
                                              ;并且通过这句,也可以证明vptr指针位于对象其实地址处
        mov    eax, DWORD PTR _this$[ebp]
        mov    esp, ebp
        pop    ebp
        ret    0

    可以看到,由于有虚函数,涉及到多态,因此构函数初始化了vptr指针,但是没有为另外两个变量int i,int j赋值。

    从上面可以看出,类里面含有虚函数时,在没有明确定义构造函数时,编译器确实会为我们提供一个默认的构造函数。

    接下来看第二种情况,类Y继承自虚基类X

    先来看c++源码:

    class X {
    private:
        int i;
    };
    
    class Y : public virtual  X {
    private: 
        int i;
    };
    
    int main() {
        Y y;
    }

    虚基类X和子类Y都没有显示定义构造函数

    下面是main的汇编码:

    _main    PROC
    
    ; 11   : int main() {
    
        push    ebp
        mov    ebp, esp
        sub    esp, 12                    ; 为对象y预留12byte空间
    
    ; 12   :     Y y;
    
        push    1;压入标志,避免重复调用虚基类的构造函数
        lea    ecx, DWORD PTR _y$[ebp];将对象y的首地址(this指针)传给eax 隐含传递给构造函数
        call    ??0Y@@QAE@XZ;调用y的构造函数
    
    ; 13   : }
    
        xor    eax, eax
        mov    esp, ebp
        pop    ebp
        ret    0
    _main    ENDP

    可以看到,编译器为y提供了默认的非无用构造函数

    下面是类Y构造函数的汇编码:

    ??0Y@@QAE@XZ PROC                    ; Y::Y, COMDAT
    ; _this$ = ecx
        push    ebp
        mov    ebp, esp
        push    ecx;ecx里面含有对象y的首地址(this指针),这里压栈的目的是为保留这个地址分配空间
        mov    DWORD PTR _this$[ebp], ecx;将对象首地址存到刚才分配的空间里面
        cmp    DWORD PTR _$initVBases$[ebp], 0;_$initVBases里面存的是调用构造函数之前的标志位,这里将标志位和0比较
                                            ;等于0就说明已经调用过虚基类的构造函数 跳到标号$LN2@Y处执行 不等于0就说明还没有调用 顺序执行
        je    SHORT $LN2@Y
        mov    eax, DWORD PTR _this$[ebp];这里将y对象首地址给eax
        mov    DWORD PTR [eax], OFFSET ??_8Y@@7B@;将内存??_8Y@@7B@的首地址写入y对象首地址所指向的内存 因此,y对象首地址处内存可以看成是一个指针
                                              ;这里将该指针叫vt_offset 因为该指针指向的内存存储的都是偏移量 第一项是本类的虚指针vptr相对于vt_offset指针的偏移量(这里本类无vptr 因此该项为0)
                                              ;第二项为虚基类首地址相对于vt_offset的偏移量(这里,该项为8)
    $LN2@Y:
        mov    eax, DWORD PTR _this$[ebp];将对象y的首地址给eax
        mov    esp, ebp
        pop    ebp
        ret    4
    ??0Y@@QAE@XZ ENDP    

    可以看到,类Y的构造函数并没有调用类X的构造函数,因为类X没有显示定义构造函数,而且编译器也不会为它合成默认的非无用构造函数。

    接下来是第三种情形,类Y继承自类X,X明确定义了一个默认的构造函数(并非编译器提供),而类Y不定义任何构造函数:

    先来看看c++源码:

    class X {
    private:
        int i;
        int j;
    public: 
        X() {//X显示定义的默认构造函数
            i = 0;
            j = 1;
        }
    };
    
    class Y : public X{//Y继承自X
    private:
        int i;
    };
    
    
    int main() {
        Y y;
    }
        

    类Y里面没有显示定义任何构造函数

    下面是main函数对应的汇编码:

    ; 19   : int main() {
    
        push    ebp
        mov    ebp, esp
        sub    esp, 12                    ; 为对象y预留12byte空间,y自身成员变量int i占4byte 父类中的成员变量int i int j占8byte
    
    ; 20   :     Y y;
    
        lea    ecx, DWORD PTR _y$[ebp];获取对象y的首地址,存入寄存器ecx
        call    ??0Y@@QAE@XZ;调用对象y的构造函数
    
    ; 21   : }
    
        xor    eax, eax
        mov    esp, ebp
        pop    ebp
        ret    0

    main函数中调用了由编译器提供的默认y对象的默认构造函数。

     下面是编译器提供的y对象默认构造函数的汇编码:

    ??0Y@@QAE@XZ PROC                    ; Y::Y, COMDAT
    ; _this$ = ecx
        push    ebp
        mov    ebp, esp
        push    ecx
        mov    DWORD PTR _this$[ebp], ecx;ecx中存有对象y的首地址
        mov    ecx, DWORD PTR _this$[ebp]
        call    ??0X@@QAE@XZ                ; 调用父类X的构造函数
        mov    eax, DWORD PTR _this$[ebp]
        mov    esp, ebp
        pop    ebp
        ret    0
    ??0Y@@QAE@XZ ENDP    

    可以看到y对象的构造函数又调用了父类的构造函数来初始化继承自父类的成员变量,但自身成员变量依然没有初始化。

    下面是父类X的构造函数汇编码:

    ; 7    :     X() {
    
        push    ebp
        mov    ebp, esp
        push    ecx
        mov    DWORD PTR _this$[ebp], ecx; ecx中存有对象y的首地址
    
    ; 8    :         i = 0;
    
        mov    eax, DWORD PTR _this$[ebp];对象y首地址给寄存器eax
        mov    DWORD PTR [eax], 0;初始化父类中的变量i
    
    ; 9    :         j = 1;
    
        mov    ecx, DWORD PTR _this$[ebp];对象y首地址给寄存器ecx
        mov    DWORD PTR [ecx+4], 1;初始化父类中的变量j,在对象y的内存空间中,从首地址开始的8比特用来存储继承自父对象的成员变量,后4byte用来存储自己的成员变量
                                ;由于首地址存储了父类成员变量i,因此内存地址要从对象y的首地址要移动4byte,才能找到父类成员变量j所处位置
    
    ; 10   :     }
    
        mov    eax, DWORD PTR _this$[ebp]
        mov    esp, ebp
        pop    ebp
        ret    0

    可以看到,y对象继承自父类的成员变量由父类构造函数初始化。父对象包含在子对象中,并且this指针,即寄存器ecx存储的首地址始终是子对象y的首地址。

    如果父类X中也没有定义任何构造函数会怎样?

    下面是c++源码:

    class X {
    private:
        int i;
        int j;
    
        
    };
    
    class Y : public X{//Y继承自X
    private:
        int i;
    };
    
    
    int main() {
        Y y;
    }
        

    父类和子类都没有任何构造函数。

    下面是main函数汇编码:

    ; 16   : int main() {
    
        push    ebp
        mov    ebp, esp
        sub    esp, 12                    ; 和刚才一样,为对象y预留12byte
    
    ; 17   :     Y y;
    ; 18   : }
    
        xor    eax, eax
        mov    esp, ebp
        pop    ebp
        ret    0

    可以看到main中根本没有任何函数的调用,也就是说,编译器没有为子对象y提供默认构造函数。

    那么,要是父类中带参数的构造函数,而子类中没有构造函数呢?这时候编译器会报错。

    下面看第四种情况,类Y中包含成员对象X,成员对象有显示定义的默认构造函数,而类Y没有任何构造函数:

    先看c++源码:

    ; 16   : int main() {
    
        push    ebp
        mov    ebp, esp
        sub    esp, 12                    ; 和刚才一样,为对象y预留12byte
    
    ; 17   :     Y y;
    ; 18   : }
    
        xor    eax, eax
        mov    esp, ebp
        pop    ebp
        ret    0

    类X为类Y的成员对象
    下面是main函数的汇编码:

    ; 21   : int main() {
    
        push    ebp
        mov    ebp, esp
        sub    esp, 12                    ; 为对象y预留12byte 成员对象的变量占8byte 对象y自身占变量占4byte 成员对象包含在对象y中
    
    ; 22   :     Y y;
    
        lea    ecx, DWORD PTR _y$[ebp];对象y的首地址存入ecx
        call    ??0Y@@QAE@XZ;调用对象y的构造函数,由编译器提供的默认构造函数
    
    ; 23   : }
    
        xor    eax, eax
        mov    esp, ebp
    
        pop    ebp
        ret    0

    对象y的构造函数被调用,即编译器提供了默认的构造函数

    对象y的构造函数汇编码:

    ??0Y@@QAE@XZ PROC                    ; Y::Y, COMDAT
    ; _this$ = ecx
        push    ebp
        mov    ebp, esp
        push    ecx
        mov    DWORD PTR _this$[ebp], ecx;ecx中存有对象y的首地址
        mov    ecx, DWORD PTR _this$[ebp]
        add    ecx, 4;加4是因为对象y首地址起始处存储的是自身成员变量i
        call    ??0X@@QAE@XZ                ; 调用成员对象x的构造函数
        mov    eax, DWORD PTR _this$[ebp]
        mov    esp, ebp
        pop    ebp
        ret    0

    对象y的构造函数调用了成员对象x的构造函数,用来初始化成员对象中的成员变量,对象y自身的成员变量没有初始化。

    成员对象x的构造函数汇编码:

    ??0X@@QAE@XZ PROC                    ; X::X, COMDAT
    ; _this$ = ecx
    
    ; 7    :     X() {
    
        push    ebp
        mov    ebp, esp
        push    ecx
        mov    DWORD PTR _this$[ebp], ecx;ecx中存有成员对象x的起始地址
    
    ; 8    :         i = 0;
    
        mov    eax, DWORD PTR _this$[ebp];成员对象x的起始地址给eax寄存器
        mov    DWORD PTR [eax], 0;初始化成员对象x中额成员变量i
    
    ; 9    :         j = 0;
    
        mov    ecx, DWORD PTR _this$[ebp];成员对象x的起始地址给ecx寄存器
        mov    DWORD PTR [ecx+4], 0;初始化成员对象x中额成员变量j 加4的原因是j的地址偏离了成员对象x起始地址4byte(即成员对象x的成员变量i的字节数)
    
    ; 10   :     }
    
        mov    eax, DWORD PTR _this$[ebp]
        mov    esp, ebp
        pop    ebp
        ret    0

    但是,如果成员对象x也没有任何构造函数,情形会怎样呢?

    下面是c++源码:

    class X {
    private:
        int i;
        int j;
    
        
    };
    
    class Y {
    private:
        int i;
        X x;//x成员对象
    };
    
    
    int main() {
        Y y;
    }
        

    下面是main函数汇编码:

    ; 17   : int main() {
    
        push    ebp
        mov    ebp, esp
        sub    esp, 12                    ; 为对象预留12byte空间
    
    ; 18   :     Y y;
    ; 19   : }
    
        xor    eax, eax
        mov    esp, ebp
        pop    ebp
        ret    0

    可以看到,main函数里面没有任何函数调用,也就是说编译器没有提供默认构造函数。

    那要是成员对象x有带参数的构造函数(即非默认构造函数),而对象y没有任何构造函数呢?此时,编译器会报错。

    这种情形和前一种情形很相似。

    综合以上的情况,可以总结出,对于一个类不含任何构造函数,而编译器会提供非无用默认的构造函数,有一下4种情形:

    1 类本身函数虚成员函数(或者继承的基类中有虚成员函数也属于这中情况)

    2 类继承自一个虚基类(不管虚基类有没有构造函数,编译器都会为该类提供一个非无用的默认构造函数)

    3 类的基类显示定义有默认构造函数(如果类的基类没有显示定义默认构造函数,但是编译器会为基类提供一个非无用的默认构造函数,也属于这种情况。也就是说,只要基类有非无用默认构造函数,不管这个非无用的默认构造函数时编译器提供,还是自定义的都行)。但是若基类的构造函数带有参数(即非默认构造函数),编译器报错

    4  这种情况和上一种相似,类的成员对象显示定义有默认构造函数(若果类的成员对象没有显示定义默认的构造函数,但是编译器会为这个成员对象提供一个非无用的默认构造函数,也属于这种情况。也就是说,只要成员对象有一个非无用的默认构造函数,不管这个非无用的构造函数时自定义的,还是编译器自动提供的都行);但是若成员对象的构造函数带有参数(即非默认构造函数),编译器报错。

    并且,编译器提供的默认构造函数,无论是有用的还是无用的,都不会执行初始化操作。

    以上参考了《VC++深入详解》里面的知识点,还有自己的分析,欢迎指正

  • 相关阅读:
    如何掌握所有的程序语言
    程序与证明
    卫星通信地面系统构成
    SCIP 环境搭建
    Homebrew install.sh
    macOS 内核之从 I/O Kit 电量管理开始
    matlab练习程序(空间椭圆拟合)
    多进程抢票加锁
    进程间数据传递
    队列用法
  • 原文地址:https://www.cnblogs.com/chaoguo1234/p/3061785.html
Copyright © 2011-2022 走看看