首先申明的一点是:默认构造函数既可以是编译器自动合成的,也可以是用户自己定义的。当我们定义一个类对象时,如果没有显式地提供构造函数的参数,则会调用默认构造函数,然而,这一切的前提是我们有默认构造函数,因为编译器不会总给我们合成默认构造函数,只在“必需”的时候合成一个默认构造函数,一旦这个默认构造函数被合成了,其地位也是等同于用户自己定义的默认构造函数的,以后我会一一讲解这段话里涉及的知识点。
1、编译器没有合成默认构造函数,代码示例如下:
#include <iostream>
using namespace std;
class A {
public:
int i;
//A(int ii = 1) : i(ii) {}
};
void setStack(){
int a = 2;
cout<<"addr of int a: "<<&a<<endl;
}
void printA(){
A a;
cout<<"addr of A a: "<<&a<<endl;
cout<<"content of a.i: "<<a.i<<endl;
}
int main()
{
setStack();
printA();
return 0;
}
这段代码定义了一个类 A ,且没有提供用户自定义的默认构造函数,这段代码的目的是看看编译器有没有为我们合成一个默认构造函数(有的小伙伴认为编译器总是会合成一个默认构造函数来将类对象的成员初始化为零),代码中有两个函数,第一个函数中有一个 int 局部变量,我们给它初始化为 2 ,第二个函数里有一个 A 的对象,它的大小与 int 相同,如果这两个函数先后被调用的话,int 型的 a 与 A类型的 a 占用同一块内存(在栈上的同一块内存),所以我们可以在 main 中先调用 setStack 来初始化这块内存,再调用printA,这样就可以看到 A 的构造函数是否被合成出来了,代码的执行结果如下:
可以看到,int 型的 a 与 A型的 a 确实在同一块内存上,而且 A 的默认构造函数没有被合成,因此 A类的 a 中的数据成员 i 保留了之前内存中的值,故为 2, 我们可以给代码做一点点修改,就可以看到不同,现在我们给 A 定义一个默认构造函数(其余代码没有改动),如下:
class A {
public:
int i;
A(int ii = 1) : i(ii) {}
};
这时的输出结果变成了:
说明默认构造函数被调用了。现在我们可能就会问:到底什么时候编译器会为我们合成一个默认构造函数呢?在四种情况下编译器会给我们合成默认构造函数,分别如下:
a) 类中有其他类的对象(成员对象),而且该对象有默认构造函数;
b) 类继承与某个基类,而且该基类有默认构造函数;
c) 类中有虚函数;
d) 类虚继承于某个基类;
现在举个有虚函数的例子,代码如下:
#include <iostream>
using namespace std;
class A {
public:
int i;
//A(int ii = 1) : i(ii) {}
virtual void fun() {}
};
void setStack(){
int a = 3,b=4;
cout<<"addr of int a: "<<&a<<endl;
cout<<"addr of int b: "<<&b<<endl;
}
void printA(){
A a;
cout<<"addr of A a.vptr: "<<&a<<"-----content of a.vptr: "<<*((unsigned int*)&a)<<endl;
cout<<"addr of A a.i : "<<&a.i<<"-----content of a.i : "<<a.i<<endl;
}
int main()
{
setStack();
printA();
return 0;
}
这段代码给 A 定义了一个虚函数,众所周知,现在A的类对象中将包含一个指向虚函数表的指针,所以A的a将会占用8个字节的内存,同样地,我们在setStack中初始化8个字节的内存,代码的执行结果如下:
可以看到,默认构造函数是被合成了,但是该函数只初始化了指针的内容,没有初始化 i 的内容,因此依赖于自动合成的默认构造函数来初始化 i 仍然是不行的,换言之,对 i 的初始化并不是编译器必须做的,编译器认为应该由用户来负责对 i 的初始化,因此编译器只是初始化了虚函数表指针,所以我们不应该依赖编译器做很多事情,否则可能陷入不知所以的bug