c++中的源程序:
int main() {
X x;
}
下面为其汇编程序:
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;函数返回
下面再看一段c++程序:
int main() {
X x;
}
下面为对应汇编码:
push ebp
mov ebp, esp
sub esp, 8; 栈顶指针移动8byte,刚好等于类X的大小
; 9 : X x;
; 10 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
所以,综上所述,在一个类没有明确定义构造函数的时候,编译器不会有任何的函数调用来进行初始化操作,仅仅是移动栈顶留出对象所需空间,也就是说,这种情况下,编译器根本不会提供默认的构造函数。
那么,书上说的由编译器提供默认的构造函数到底是怎么一回事呢?
下面看第一种情况,类里面有虚成员函数:
c++源码如下:
int main() {
X x;
}
下面是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
下面是构造函数的汇编码:
从上面可以看出,类里面含有虚函数时,在没有明确定义构造函数时,编译器确实会为我们提供一个默认的构造函数。因此当一个类继承自虚基类时,也满足上面的情形。
接下来是第二种情形,类Y继承自类X,X明确定义了一个默认的构造函数(并非编译器提供),而类Y不定义任何构造函数:
先来看看c++源码:
class Y : public X{//Y继承自X
private:
int i;
};
int main() {
Y y;
}
下面是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
下面是编译器提供的y对象默认构造函数的汇编码:
下面是父类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
如果父类X中也没有定义任何构造函数会怎样?
下面是c++源码:
};
class Y : public X{//Y继承自X
private:
int i;
};
int main() {
Y y;
}
下面是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
那么,要是父类中带参数的构造函数,而子类中没有构造函数呢?这时候编译器会报错。
下面看第三种情况,类Y中包含成员对象X,成员对象有显示定义的默认构造函数,而类Y没有任何构造函数:
先看c++源码:
push ebp
mov ebp, esp
sub esp, 12 ; 和刚才一样,为对象y预留12byte
; 17 : Y y;
; 18 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
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的构造函数汇编码:
成员对象x的构造函数汇编码:
; 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
下面是c++源码:
};
class Y {
private:
int i;
X x;//x成员对象
};
int main() {
Y y;
}
push ebp
mov ebp, esp
sub esp, 12 ; 为对象预留12byte空间
; 18 : Y y;
; 19 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
那要是成员对象x有带参数的构造函数(即非默认构造函数),而对象y没有任何构造函数呢?此时,编译器会报错。
这种情形和前一种情形很相似。
综合以上的情况,可以总结出,对于一个类不含任何构造函数,而编译器会提供默认的构造函数,有一下3种情形:
1 类本身函数虚成员函数或者继承自虚基类
2 类的基类有构造函数,并且基类构造函数还是显示定义的默认构造函数(非编译器提供),若基类的构造函数带有参数(即非默认构造函数),编译器报错
3 这种情况和上一种相似,类的成员对象有构造函数,并且成员对象的构造函数还是显示定义的默认构造函数(非编译器提供);若成员对象的构造函数带有参数(即非默认构造函数),编译器报错。
以上参考了《VC++深入详解》里面的知识点,还有自己的分析,欢迎指正
新闻热点
疑难解答
图片精选