从汇编看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++程序:


代码如下:

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,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没有任何构造函数呢?此时,编译器会报错。

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

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

1 类本身函数虚成员函数或者继承自虚基类

2 类的基类有构造函数,并且基类构造函数还是显示定义的默认构造函数(非编译器提供),若基类的构造函数带有参数(即非默认构造函数),编译器报错

这种情况和上一种相似,类的成员对象有构造函数,并且成员对象的构造函数还是显示定义的默认构造函数(非编译器提供);若成员对象的构造函数带有参数(即非默认构造函数),编译器报错。

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

(0)

相关推荐

  • 对C++默认构造函数的一点重要说明

    大多数C++书籍都说在我们没有自己定义构造函数的时候,编译器会自动生成默认构造函数.其实这句话我一直也是 深信不疑.但是最近看了一些资料让我有了一点新的认识. 其实我觉得大多数C++书籍之所以这样描述其实是玩了文字游戏的.如果说编译器自动产生的默认构造函数对于我们 的类没有任何作用,也就是说在编译器默认生成的这个构造函数里根本没有任何实质性的代码工作,那么这种默认构 造其实是可有可无的,所以不妨说编译器其实是为每个类生成了默认构造函数的. 在深度探索C++对象模型中讲了四种关于编译器自动生成默认

  • 解析C++中构造函数的默认参数和构造函数的重载

    C++构造函数的默认参数 和普通函数一样,构造函数中参数的值既可以通过实参传递,也可以指定为某些默认值,即如果用户不指定实参值,编译系统就使形参取默认值. [例] #include <iostream> using namespace std; class Box { public : Box(int h=10,int w=10,int len=10); //在声明构造函数时指定默认参数 int volume( ); private : int height; int width; int l

  • 从汇编看c++中默认构造函数的使用分析

    c++中的源程序: 复制代码 代码如下: class X {private:    int i;}; int main() {    X x;} 上面的类X没有定义构造函数,仅仅有一个int i. 下面为其汇编程序: 复制代码 代码如下: ; 7    : int main() { push    ebp;ebp为一个寄存器,总是指向一个函数调用堆栈的栈底,作为基址,用偏移量来访问该调用栈上的变量,但这里没有任何变量要访问,因此不起作用     mov    ebp, esp;这两句的作用是为了

  • 从汇编看c++的默认析构函数的使用详解

    c++中,如果没有为一个类提供析构函数,那么编译器会为这个类提供默认的析构的函数.由于析构函数的功能和构造函数相反,因此和默认的构造函数类似,编译器也会提供无用的默认的析构函数,和非无用的析构函数.两者的分析情况一样(对于默认的构造函数分析,请参看<从汇编看c++中默认构造函数的使用分析>).并且编译器会提供非无用的默认析构函数情形和默认构造函数类似: 1 类含有虚成员函数(类继承自虚基类或者继承的基类含有虚成员函数,也属于这种情况) 2 类继承自一个基类,基类含有自定义析构函数(如果基类没有

  • 从汇编看c++中的多态详解

    在c++中,当一个类含有虚函数的时候,类就具有了多态性.构造函数的一项重要功能就是初始化vptr指针,这是保证多态性的关键步骤. 构造函数初始化vptr指针 下面是c++源码: class X { private: int i; public: X(int ii) { i = ii; } virtual void set(int ii) {//虚函数 i = ii; } }; int main() { X x(1); } 下面是对应的main函数汇编码: _main PROC ; 16 : in

  • 从汇编看c++中多态的应用

    在c++中,当一个类含有虚函数的时候,类就具有了多态性.构造函数的一项重要功能就是初始化vptr指针,这是保证多态性的关键步骤.构造函数初始化vptr指针下面是c++源码: 复制代码 代码如下: class X {private:    int i;public:    X(int ii) {        i = ii;    }    virtual void set(int ii) {//虚函数        i = ii;    }};int main() {   X x(1);} 下面

  • 从汇编看c++中变量类型的深入分析

    全局变量的生命期和可见性是整个程序的运行期间,下面就来用汇编来看一下实际情况: c++源码: 复制代码 代码如下: int i = 2;//全局变量 int main() {    int j = i;} 下面是汇编代码: 复制代码 代码如下: PUBLIC    ?i@@3HA                        ; i_DATA    SEGMENT?i@@3HA    DD    02H                    ; 全局变量i内存空间_DATA    ENDSPUB

  • 从汇编看c++中extern关键字的使用

    在c++中,extern关键字用来声明变量和函数,在声明函数的时候,有和没有extern的效果一样,即下面两条语句具有同样的效果: 复制代码 代码如下: extern void fun(); void fun(); 但是对于变量,有和没有extern就有区别,当有extern时,只是告知编译器存在这个变量,编译器并不为该变量分配存储空间,即真正的声明:若没有extern,则在声明的同时,编译器也为该变量分配存储空间. 下面是有extern的情形时的c++源码: 复制代码 代码如下: int ma

  • 从汇编看c++中函数里面的static关键字的使用说明

    下面从汇编语言看其原理. 下面是c++源代码: 复制代码 代码如下: void add() {    static int i = 1;    i++;}int main() {   add(); } 下面是main对应的汇编码 复制代码 代码如下: ; 5    : int main() { push    ebp    mov    ebp, esp ; 6    :    add(); call    ?add@@YAXXZ                ; 调用add ; 7    : 

  • 从汇编看c++中引用与指针的使用分析

    首先是引用情形下的c++源码: 复制代码 代码如下: void add(int a, int b, int&c) {    c = a + b;} int main() {    int a = 1;    int b = 2;    int c = 0;    add(a, b, c); } 下面是main对应的汇编码: 复制代码 代码如下: ; 6    : int main() { push    ebp    mov    ebp, esp    sub    esp, 12      

  • 从汇编看c++函数的默认参数的使用说明

    在c++中,可以为函数提供默认参数,这样,在调用函数的时候,如果不提供参数,编译器将为函数提供参数的默认值.下面从汇编看其原理. 下面是c++源码: 复制代码 代码如下: int add(int a = 1, int b = 2) {//参数a b有默认值    return a + b;}int main() {   int c= add();//不提供参数 } 下面是mian函数里面的汇编码: 复制代码 代码如下: ; 4    : int main() { push    ebp    m

  • 浅谈C++中的构造函数分类及调用规则

    构造函数的分类 这里简单地将C++中的构造函数分一下类,直接看下面的代码表达,说明在注释中: #include <iostream> using namespace std; class Text { public: Text() // 无参数构造函数 { m_a = 0; m_b = 0; cout << "无参数构造函数" << endl; } Text(int a) // 有参数构造函数 { m_a = a; m_b = 0; cout <

随机推荐