注:如果赶时间可以直接跳到下一小结阅读。
今天一个朋友问我一个问题,由此引发了我对C++对象初始化方式的深入研究。他问我这段代码有什么问题没有?
tm *t = new tm;
t->tm_year = 2016;
t->tm_mon = 5;
t->tm_min = 9;
t->tm_hour = 9;
t->tm_sec = 30;
cout << mktime(t)<<endl;
我第一反应是time_t格式不能直接cout,直接cout得不到格式化的时间。当然这个不是重点,他告诉我每次结果都是随机的!我这才反应过来是new的问题。因为new在堆上分配的空间是没有初始化的。
然后我打算告诉他在main函数外的全局部分定义一个变量,但是在我消息发送出去前,他又告诉我他把代码的第一行修改成了下面这个形式,然后就初始化成固定值了:
tm t1;
他问我是不是这样做就总是固定值呢?我认为这个对象是栈上的,印象里这个是没有初始化的。所以回答只是编译器的问题。然后我提供了一个使用指针的的方法:
tm *t = new tm();
之后他问了一个关键的问题:“如果不加括号是不是一定就不会去自动的调用相关的初始化部分?”
我开始回答的是:如果是个类的话,会调用。基本类型不会。
但是后来想想不太对,因为我印象中确实有的在函数里定义的临时类是没有经过初始化的。但当时在外边没有电脑。回去后在 Windows 环境下用 gcc(codeblocks) 和 VS2015 都试了一下,然后查了cppreference,大概明白是怎么回事了。
首先经验证,new的时候加括号可以得到想要的结果:
但是,直接在函数内部定义结构体变量,gcc和vs得到了不同的结果:
这说明结构体内部的成员没有初始化。
为了搞清所有的情况,查了下cppreference,并按照C++11的标准写了下面的内容:
下面介绍C++对象两种重要的初始化方式:默认初始化和值初始化。主要参考cppreference写成的。并且删除了其中过时的部分(主要都是C++03的特性)。
默认初始化
默认初始化的语法:
T object;
new T;
在以下情形,会使用默认初始化:
- 当变量具有自动,静态,线程生存周期,并且没有初始化时。
- 使用
new
动态分配的对象没有初始化 - 在一个类中,当某个基类或者非静态成员在构造函数初始化列表中没有出现,并且该构造函数被调用的时候。
默认初始化的效果:
- 如果T是一个非POD (Plain Old Data)类型,就会在列表为空的构造函数,以及重载的构造函数中选择一个(注:可能有构造函数参数列表非空且所有的参数都有默认值)。被选中的这个构造函数会初始化变量。
- 如果T是数组类型,每个元素都被默认初始化。
- 否则,什么也不做:自动生存周期的对象(包括他们的子对象)值都是未定义的。
#值初始化
值初始化的语法:
T(); (1)
new T (); (2)
Class::Class(...) : member() { ... } (3)
T object {}; (4) (since C++11)
T{}; (5) (since C++11)
new T {}; (6) (since C++11)
Class::Class(...) : member{} { ... } (7) (since C++11)
在以下情形使用值初始化:
- 1,5) 当使用内部为空的圆括号或花括号创建一个无名的临时对象时。
- 2,6) 当用
new
创建一个动态对象,且后面使用了内部为空的圆括号或花括号时。 - 3,7) 在某个类中,当某个非静态成员或基类使用了空的花括号或圆括号初始化时。
初始化的效果:
- 如果类型T是有至少1个用户定义的构造函数,那么调用默认构造函数。
- 如果T没有默认构造函数,或者有用户定义的构造函数,或者默认构造函数是删除的,那么使用默认初始化。
- 如果T不是Union且无用户的构造函数,那么所有的非静态成员和基类采用值初始化。
- 如果T是个有默认构造函数的类,且这个构造函数不是用户定义的也不是删除的,那么该对象被零初始化。然后,如果它有一个non-trivial的默认构造函数,它将被默认初始化。
- 如果T是数组,每个元素都被值初始化。
- 否则都执行零初始化
关于第3条很难理解。首先,trivial的定义参考这个链接。明确了这个定义,看看下面的代码:
#include <bits/stdc++.h>
#include <type_traits>
using namespace std;
class A{//non-trivial
int a;
int b;
char c;
public:
A(){
cout << "it works!"<<endl;
};
};
class B{//trivial
int a;
int b;
char c;
};
int main()
{
A *a = new A();
B *b = new B();
cout<<is_pod<A>::value<<endl;
return 0;
}
运行结果表明a指向的对象的成员是随机的,并且输出了"it works",而b指向的成员的对象全部是0。
A的构造函数尽管是空的,但是它是用户定义的,因此是non-trivial,进行默认初始化。参考默认初始化条的效果,由于A不是POD类型,那么就会执行A的默认构造函数(我们定义的那个)。由于我们定义的构造函数没有初始化任何数据成员,因此A成员的值是随机的,并且输出了信息"it works"。
B的构造函数有编译器合成,是trivial的,它符合第三条,执行零初始化。
总结
根据以上内容,我总结了2点帮助记忆:
第一点
第一点是关于两种初始化发生的条件的:
只要使用了括号(圆括号或花括号),就是值初始化。可以简单理解为括号提醒编译器你想要用某个值赋给对象。
没有使用括号,就是默认初始化。可以简单理解成,你不加任何东西,编译器就会使用默认的行为。
第二点
第二点是关于两种初始化的效果的:
默认初始化:总是试图使用默认构造函数初始化对象。但是它对于POD类型则不这么做。比如:C基本类型,聚合类型,POD类型的数组。C语言的struct以及基本类型如果不初始化也是随机的值,和这个POD类型在C++类似。我们可以简单理解为:总使用默认构造函数,同时兼容C。
值初始化:我们观察几个条件可以发现,大部分情况下(效果2里包含默认构造器被删除的情况),只要用户指定了默认构造函数,那么就执行默认初始化。并且,如果编译器合成了构造函数,执行零初始化。另外,对于数组和其它情况是值初始化。而且排除掉数组后,基本类型的值初始化都是零初始化。综上,可以简单理解为:有用户定义构造函数,就执行用户定义的构造函数,否则都使用零初始化。