0%

C++对象的默认初始化和值初始化

注:如果赶时间可以直接跳到下一小结阅读。

今天一个朋友问我一个问题,由此引发了我对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的时候加括号可以得到想要的结果: pic1 pic2

但是,直接在函数内部定义结构体变量,gcc和vs得到了不同的结果: pic3

这说明结构体内部的成员没有初始化。

为了搞清所有的情况,查了下cppreference,并按照C++11的标准写了下面的内容:

下面介绍C++对象两种重要的初始化方式:默认初始化和值初始化。主要参考cppreference写成的。并且删除了其中过时的部分(主要都是C++03的特性)。

默认初始化

默认初始化的语法:

T object;
new T;

在以下情形,会使用默认初始化:

  1. 当变量具有自动,静态,线程生存周期,并且没有初始化时。
  2. 使用new动态分配的对象没有初始化
  3. 在一个类中,当某个基类或者非静态成员在构造函数初始化列表中没有出现,并且该构造函数被调用的时候。

默认初始化的效果:

  • 如果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) 在某个类中,当某个非静态成员或基类使用了空的花括号或圆括号初始化时。

初始化的效果:

  1. 如果类型T是有至少1个用户定义的构造函数,那么调用默认构造函数。
  2. 如果T没有默认构造函数,或者有用户定义的构造函数,或者默认构造函数是删除的,那么使用默认初始化。
  3. 如果T不是Union且无用户的构造函数,那么所有的非静态成员和基类采用值初始化。
  4. 如果T是个有默认构造函数的类,且这个构造函数不是用户定义的也不是删除的,那么该对象被零初始化。然后,如果它有一个non-trivial的默认构造函数,它将被默认初始化。
  5. 如果T是数组,每个元素都被值初始化。
  6. 否则都执行零初始化

关于第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里包含默认构造器被删除的情况),只要用户指定了默认构造函数,那么就执行默认初始化。并且,如果编译器合成了构造函数,执行零初始化。另外,对于数组和其它情况是值初始化。而且排除掉数组后,基本类型的值初始化都是零初始化。综上,可以简单理解为:有用户定义构造函数,就执行用户定义的构造函数,否则都使用零初始化。