c++合成默认构造函数与new关键字带不带括号的分析

声明或定义一个类/对象的时候,会因为类本身的成员结构而会引起不同的构造函数的调用,之前的学习中或多或少有些总结。

《c++primer(第五版)》《深度探索c++对象模型》《More Effective C++》三本书中都有总结,自己也简单的理解了下

全篇总结:

一,声明一个类对象时,不一定是调用了默认的构造函数;只有在没有任何构造函数且AA xx{}声明的时候,编译器才会对内置类型进行“零值化”;其他情况按照四种情况进行分析
二,编译器有四种情况会合成默认的构造函数
1.包含了一个类的对象,这个对象有一个构造函数(包括编译器合成的默认构造函数)
2.继承自一些基类,其中某些基类有一个构造函数(包括编译器合成的默认构造函数)
3.有一个虚函数,或者继承到了虚函数
4.有虚基类
两种错误的观点:
a> 任何类如果没有定义构造函数,则编译器会帮我们合成一个默认构造函数。
b> 合成默认构造函数会对类中的每一个数据成员进行初始化。
三,new对象的时候带不带括号
对于自定义类类型:
如果该类没有定义构造函数(由编译器合成默认构造函数)也没有虚函数,那么class c = new class;将不调用合成的默认构造函数,而class c = new class();则会调用默认构造函数。
如果该类没有定义构造函数(由编译器合成默认构造函数)但有虚函数,那么class c = new class;将不调用合成的默认构造函数;class c = new class()会调用默认构造函数。
如果该类定义了默认构造函数,那么class c = new class;和class c = new class();一样,都会调用默认构造函数。
对于内置类型:
int a = new int;不会将申请到的int空间初始化,而int a = new int();则会将申请到的int空间初始化为0。

1,声明一个类的时候,不一定是调用了默认构造函数

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class HasExplicitCon{
public:
HasExplicitCon(){
cout<<"显式的定义一个默认构造函数"<<endl;
}
HasExplicitCon(int a){
cout<<"显式的定义一个有参数默认构造函数"<<endl;
}

int a;
};
int main(int argc, char **argv) {
HasExplicitCon hasObject1;
HasExplicitCon hasObject2(2);
HasExplicitCon hasObject3();

cout<<hasObject1.a<<endl;
cout<<hasObject2.a<<endl;
//cout<<hasObject3.a<<endl; 编译不通过
return 0;
}

结果为:

1
2
3
4
显式的定义一个默认构造函数
显式的定义一个有参数默认构造函数
4213675
7602016

是的,没看错,结果只输出了两行,因为hasObject3后面跟的是小括号,会引起c++歧义,误以为是定义的一个函数,这点在之前的文章中说过了http://blog.csdn.net/hll174/article/details/78309212。用中括号的话则不会,中括号实际是initializer_list,http://zh.cppreference.com/w/cpp/language/list_initialization
std::initializer_list 对象在这些时候自动构造:

花括号初始化器列表用于列表初始化,包括函数调用列表初始化和赋值表达式
花括号初始化器列表被绑定到 auto ,包括在带范围 for 循环中

这里默认构造函数没有对变量a进行初始化,且其在类中没有初始值,new出的对象在堆中,编译器进行初始化的时候是对其进行随机初始化的。
所以明显看到定义(也就是我们所说的声明时),会调用无参的构造函数(默认构造函数),如果自己定义了则使用自己的,自己没有定义,则因为不满足四种情况,编译器也不会生成合成的默认构造函数,因此值是随机的。
这里HasExplicitCon自定义了默认构造函数,但是没有对a进行初始化,因此,a的值还是编译器随机化的,这种随机化的与构造函数无关。a的值是程序的责任,而非编译器的责任,默认构造函数只会执行编译器的责任,这点下面会将具体介绍。
而如果隐去所有的默认构造函数,则a的值仍然是随机值,因为这时候编译器也不会生成合成的默认构造函数,其不满足下面要介绍的编译器合成默认构造函数的四种情况。
这里又发现一种情况
情况1:
A a;声明,且有显示的默认构造函数和带参数构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class HasExplicitCon{
public:
HasExplicitCon(){
cout<<"显式的定义一个默认构造函数"<<endl;
}
HasExplicitCon(int a){
cout<<"显式的定义一个有参数默认构造函数"<<endl;
}
char *str;
int a;
};
int main(int argc, char **argv) {
HasExplicitCon hasObject1;

if(hasObject1.str){
cout<<"str是非空值"<<endl;
cout<<*hasObject1.str<<endl;
cout<<hasObject1.a<<endl;
}else{
cout<<"str是空值"<<endl;
cout<<*hasObject1.str<<endl;
cout<<hasObject1.a<<endl;
}
return 0;
}

结果为:

1
2
3
4
显式的定义一个默认构造函数
str是非空值
?
4213819

调用了显示的默认构造函数,但是没有实例化a和str的值,因为这是程序的责任
情况2:
A a;声明,无显示的默认构造函数和带参数构造函数
也就是把上面情况的构造函数全部去掉,这里只贴结果

1
2
3
str是非空值
?
4213803

结果相同,但原因不同。不满足四种情况,因此实质是编译器没有为其合成默认的构造函数,因为也就是没有构造函数调用,编译器处理的随机值。
情况3:
A a{};声明,有显示的默认构造函数和带参数构造函数
在情况1的基础上,只将声明改为 HasExplicitCon hasObject1{};,结果为:

1
2
3
4
显式的定义一个默认构造函数
str是非空值
?
4213819

调用默认构造函数,但是认为str和a是程序的责任,情况与1相同。
情况4:
A a{};声明,无显示的默认构造函数和带参数构造函数
在情况3的基础上,去掉显示的构造函数,这个时候只输出了

1
str是空值

然后程序死亡,这里断点看到hasObject1.a的值为0。
情况2的时候是没有合成构造函数的,也没有对内置类型进行初始值。而这个时候内置类型的值都被“零化”,因为中括号{}的存在,而中括号本质是initializer_list,显示的构造函数中,编译器对于{}还是和普通的()一样,认为不是编译器的责任,例如情况3;而当没有构造函数的时候,编译器会合成了默认的构造函数且对内置类型的对象进行了初始化,这与initializer_list有很大关系,因此对于初始化的时候initializer_list还需要进一步研究。

2,《深度探索c++对象模型》关于默认构造函数的解释

文章:http://blog.csdn.net/ywending/article/details/51096547
https://www.cnblogs.com/QG-whz/p/4676481.html
http://blog.csdn.net/cn_wk/article/details/61921566
http://blog.csdn.net/spaceyqy/article/details/22730939
这四篇文章中都有详细的讨论,在此之前,先看看编译器合成的默认构造函数条件。
首先,《深度探索c++对象模型》这本书告诉我们有两个误解:
a) 任何类如果没有定义构造函数,则编译器会帮我们合成一个默认构造函数。
b) 合成默认构造函数会对类中的每一个数据成员进行初始化。
以及编译器为我们生成默认构造函数的四种情况:
1.包含了一个类的对象,这个对象有一个构造函数(包括编译器合成的默认构造函数)
2.继承自一些基类,其中某些基类有一个构造函数(包括编译器合成的默认构造函数)
3.有一个虚函数,或者继承到了虚函数
4.有虚基类

2.1 包含了一个类的对象,这个对象有一个构造函数(包括编译器合成的默认构造函数)

看下面这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Foo{
public:
Foo(){cout<<"Foo显示定义的默认构造函数"<<endl;};
Foo(int){};
};

class Bar{
public:
Foo foo;
char *str;
};

void f(){
Bar bar;
if(bar.str){
cout<<"编译器对str进行了默认优化"<<endl;
cout<<*bar.str<<endl;
}
}
int main(int argc, char **argv) {
f();
return 0;
}

我在gcc6.2的平台下结果为;

1
2
3
Foo显示定义的默认构造函数
编译器对str进行了默认优化
?

Bar类没有任何构造函数,含有类成员对象foo,且foo类有默认构造函数(显示自定义的),因此编译器会为Bar类生成合成的默认构造函数,对变量进行初始化,注意这里编译器生成的默认构造函数首先是扩展构造函数,先调用了基类的默认构造函数对foo进行了初始化,这就是其构造函数扩展规则
同时,编译器生成的默认构造函数初始化只会完成编译器责任的初始化,而不会完成程序责任的初始化,这里的foo对象就是编译器的责任,而str指针则是程序的责任,因此str在这里是一个栈区对象,编译器对其是进行随机值初始化(不同编译平台的处理可能不同),因此我们需要构造Bar自己的构造函数完成程序的初始化,即对str进行初始化,

1
Bar(){str=0;}

因此,上面文章作者帮我们总结了合成默认构造函数总是不会初始化类的内置类型及复合类型的数据成员,因为这是程序的责任,我们得分清楚编译器的责任与程序的责任。

另外,这篇文章https://www.cnblogs.com/QG-whz/p/4676481.html“这是不可能的,构造函数是用来负责类对象的初始化的,一个类对象无论如何一定会被初始化,不论是默认初始化,还是值初始化。也就是说,当实例化类对象时,一定会调用构造函数。那也就不存在“需要/不需要”这回事,必然会合成。”
这是我们之前普遍的想法,我们看到认为的“类实例化”,其实并没有真正的实例化出来,可能类没有构造函数,编译器也无法合成默认的构造函数,“类实例化”仅仅只是编译器随机化的一个垃圾值,我们的实例化是我们知道或者变量对象按照我们需要的去进行初始值,而不是随机值。没有一定会调构造函数,值被初始化有可能只是编译器的处理而非各种构造函数的处理。

2.2 继承自一些基类,其中某些基类有一个构造函数(包括编译器合成的默认构造函数)

这与上面类似,只是这里有构造函数的扩展规则
基类默认构造函数(按照基类被声明的顺序先声明优先,多层基类按照上层优先)—>类对象的默认构造函数(按照类对象在类中声明的先后顺序)—>类自身的默认构造函数

2.3 有一个虚函数,或者继承到了虚函数

同时还有可能:
(1)类声明或继承一个虚函数
(2)类派生自一个继承链,其中有一个或更多的虚基类
总之这种情况的就是类有虚函数了,因为在编译期会有虚函数表被生成出来,同时还有指向虚函数表的指针vptr被生成出来,vptr需要被初始化才能完成虚机制。这些都是在构造函数中完成的,因此没有构造函数的类会由编译器合成默认的构造函数。因此扩展构造函数的规则就是设置正确的虚函数表地址。

2.4 带有虚基类的类

这种典型的虚基类的继承有“菱形继承”,为了让派生类中只有一个虚基类的对象,以前编译器的做法是在派生类的每个虚基类中安插一个指针,所有由引用或指针来存取一个虚基类的操作都可以通过相关指针完成。这个指针也是在类构造期间完成的,因此若类没有任何构造函数,编译器会合成默认的构造函数。同样,扩展构造函数的规则是设置正确的虚基类指针的值。
因此需要理解编译器合成默认构造函数的四种情况,同时还得区分编译器初始化的责任与程序初始化的责任。

3. new创建一个对象时候带不带小括号的区别

如果不用new创建对象,最好是用{},而不是(),如上所述。当用()的时候,其实有一些误解。同时上面也介绍了编译器生成默认构造函数的四种情况,这里继续看看new一个对象的区别。
我们知道如果直接XX xx;则对象的内存是在栈区;
而XX xx =new XX;则是在堆区(这里先忽略带不带括号的事)
如果没有默认实例化,那么栈区和堆区的值都是随机初始化的,这种随机性很多时候是对程序有害的。

3.1内置类型的new

关于内置类型的new情况,这点没有争议:
带了()的时候,在分配内存的时候初始化零值(int的0或者string的0值);没有带()的时候只分配了内存,其值是随机性的。看下面代码:

1
2
3
4
5
6
7
int main(int argc, char **argv) {
int *b=new int[100];
for(int i=0;i<100;i++){
cout<<b[i]<<endl;
}
return 0;
}

结果为:

1
2
3
4
5
6
7
18353232
18382632
0
0
...
-2147450880
-2147450880

而加上()后的结果(建议还是用中括号好,前面小括号很多时候会有意想不到的Bug)

1
int *b=new int[100]{};

这样的结果为:

1
2
3
0
...
0

全都是0,符合默认初始化的预期。

3.2 自定义类型的new

对于自定义类型的new,这里貌似不同的博客还是有点分歧,然后自己尝试了下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A{
public:
int a;
};

int main(int argc, char **argv) {
A *a1=new A;
A *a2=new A();

cout<<a1->a<<endl;
cout<<a2->a<<endl;

A a3;
cout<<a3.a<<endl;

return 0;
}

结果为:

1
2
3
17465128
0
4213632

只有a2的a值为0,a1和a3的值是随机的。这证实了一点:
(1)如果该类没有定义构造函数(由编译器合成默认构造函数)也没有虚函数,那么class c = new
class;将不调用合成的默认构造函数,而class c = new class();则会调用默认构造函数。
(2)a1不调用合成的默认构造函数,因此值是随机的;a3也是是因为无法生成合成的默认构造函数,而是随机值。
然后文章说

如果该类没有定义构造函数(由编译器合成默认构造函数)但有虚函数,那么class c = new class;和class c = new class();一样,都会调用默认构造函数。

尝试了下只加虚函数,结果为

1
2
3
18481344
0
4213600

仍然是只有()的实例为0,而如果该类定义了默认构造函数,那么class c = new class;和class c = new class();一样,都会调用默认构造函数,这点毫无争议。

因此总结下自定义类型的new时候:
如果该类没有定义构造函数(由编译器合成默认构造函数)也没有虚函数,那么class c = new class;将不调用合成的默认构造函数,而class c = new class();则会调用默认构造函数。
如果该类没有定义构造函数(由编译器合成默认构造函数)但有虚函数,那么class c = new class;将不调用合成的默认构造函数;class c = new class()会调用默认构造函数。
如果该类定义了默认构造函数,那么class c = new class;和class c = new class();一样,都会调用默认构造函数。
另外,这里的new调用默认构造函数的时候,是对内置类型对象进行“零值化”的,这点与第一章和第二章说的非编译器的责任则不初始化,因此值是随机的是存在区别的,注意区分。


后续需要对initializer_list在构造函数初始化的情况进行分析,还有没有调用构造函数且自身没有定义构造函数内存的分配问题还需要学习。