在C++ Primer第五版39页提到:“在C++语言中,初始化时一个异常复杂的问题”。

然后在第235页中又提到:“构造函数是一个非常复杂的问题”。

恰好这两个问题连在一起,就成了一个异常非常复杂的问题,把我折磨的够呛。

1.初始化

很多程序员对于用等号 = 来初始化变量的方式倍感困惑,这种方式容易让人认为初始化是赋值的一种。事实上,在C++语言中,初始化与赋值是两个完全不同的

操作。然而在很多编程语言中二者的区别几乎可以忽略不计,即使在C++语言有时这种区别也无关紧要,所以人们特别容易把二者混为一谈。需要强调的是,这个

概念至关重要。

初始化不是赋值,初始化的含义是创建一个变量时赋予其一个初始值,而赋值的含义是把对象当前的值擦除,而以一个新值来替代。

列表初始化

C++语言定义了初始化的好几种不同形式,这也是初始化问题复杂性的一个体现。例如,要想定义一个名为 units_sold 的 int 变量并初始化为 0 ,以下的 4 条语句

都可以做到这一点:

1 int units_sold = 0;
2 int units_sold = {0};
3 int units_sold{0};
4 int units_sold(0);

作为C++11新标准的一部分,用花括号来初始化变量得到了全面应用,而在此之前,这种初始化的方式仅在某些受限的场合才能使用。

默认初始化

如果定义变量时没有指定初值,则变量被默认初始化,此时变量被赋予了默认值,默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响。

如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于任何函数体(块)之外的变量被初始化为 0 ,而定义于函数体(块)内部的内置类型变量将不被

初始化。一个未被初始化的内置类型变量的值是未定义的,如果试图拷贝或以其他形式访问此类值将引发错误。

定义在块内部的局部变量有一个例外,即如果是static型变量,即使不显式地给予初始值,也会被默认初始化为零。

未初始化的变量含有一个不确定的值,使用未初始化变量的值是一种错误的编程行为并且很难调试。尽管大多数编译器都能对一部分使用未初始化变量的行为提出警告,

但严格来说,编译器并未被要求检查此类错误。

每个类各自决定其初始化对象的方式。而且,是否允许不经初始化就定义对象也由类自己决定。如果类允许这种行为,它将决定对象的初始值到底是什么。

绝大多数类都支持无须显式初始化而定义对象,这样的类提供一个合适的默认值。

例如,string 类规定如果没有指定初值则生成一个空串。

一些类要求每个对象都显式初始化,此时如果创建了一个类的对象而并未对其作出明确的初始化操作,将引发错误。

2.构造函数的初始化作用

构造函数特点

和类同名。没有返回值。形参列表可能为空。函数体可能为空。可以重载。不能声明为 const。

合成默认构造函数

如果我们的类没有显式地定义构造函数,那么编译器就会为我们隐式地定义一个默认构造函数,即合成默认构造函数。

合成默认构造函数将按照如下规则初始化类的数据成员:

· 如果存在类内的初始值,用它来初始化成员,即C++11新标准中新增的类内初始化。

· 否则,执行默认初始化该成员。

某些类不能依赖于合成的默认构造函数

合成默认构造函数只适合非常简单的类,对于大多数普通的类来说,必须定义它自己的默认构造函数,原因有三:第一个原因也是最容易理解的一个原因就是编译器只有

在发现类不包含任何构造函数的情况下才会替我们生成一个默认的构造函数。一旦我们定义了一些其他的构造函数,那么除非我们再定义一个默认的构造函数,否则类将

没有默认构造函数。

只有当类没有声明任何构造函数时,编译器才会自动的合成默认构造函数。

第二个原因是对于某些类来说,合成的默认构造函数可能执行错误的操作。含有内置类型或复合类型成员的类应该在类的内部初始化这些成员,或者定义一个自己的默认构造函数。否则,用户在创建类的对象时就可能得到未定义的值。

第三个原因是有的编译器不能为某些类合成默认的构造函数。例如,如果类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数,那么编译器将无法初始化该成员。对于这样的类来说,我们必须自定义默认构造函数,否则该类将没有默认的构造函数可用。

构造函数初始值列表

 当我们定义变量时习惯立即对其进行初始化,而非先定义、再赋值。

1 string foo = "Hello World";    //定义并初始化
2 string bar;                             //默认初始化成空string对象
3 bar = "Hello World";             //为bar赋一个新值

就对象的数据成员而言,初始化和赋值也有类似的区别。如果没有在构造函数的初始值列表中显式地初始化成员,则该成员将在构造函数体之前执行默认初始化。

例如:

1 Sales_data::Sales_data(const string &s, unsigned cnt, double price)
2 {
3        bookNo = s;
4        units_sold = cnt;
5        revenue = cnt * price;      
6 }

这段代码与下面这段代码的原始定义效果是相同的:

1 Sales_data(const std::string &s, unsigned n, double p):bookNo(s), units_sold(n),revenue(p*n)
2 {}

当构造函数完成后,数据成员的值相同。区别是下面的代码初始化了它的数据成员,而这个上面的版本是对数据成员执行了赋值操作。这一区别到底会有什么深层次的

影响完全依赖于数据成员的类型。

有时我们可以忽略数据成员初始化和赋值之间的差异,但并非总是这样。如果成员是const或者引用的话,必须将其初始化。类似的,当成员属于某种类类型且该类没有

默认的构造函数时,也必须将这个成员初始化。

随着构造函数体一开始执行,初始化就完成了。

我们初始化const或者引用类型的数据成员的唯一机会就是通过构造函数初始值。

如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值。

在很多类中,初始化和赋值的区别事关底层效率问题:前者直接初始化数据成员,后者则先初始化再赋值。

成员初始化的顺序

成员初始化的顺序与他们在类定义中出现的顺序一致。

3.C++ 11中关于构造函数的新语法

=default的含义

如:

1 Sales_data() = default;

我们希望这个函数的作用完全等同于之前使用的合成默认构造函数。

在C++11新标准中,如果我们需要默认的行为,那么可以通过在参数列表后面写上 = default 来要求编译器生成默认构造函数。

其中, = default 既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。和其他函数一样,如果 = default 在类的内部,则默认构造函数是内联的;

如果它在类的外部,则该默认构造函数默认不是内联的。

委托构造函数

C++ 11新标准扩展了构造函数初始值的功能,使得我们可以定义所谓的委托构造函数(delegating constructor)。

一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或者全部)职责委托给了其他构造函数。

构造函数与初始化
 1 class Sales_data
 2 {
 3 public:
 4 //非委托构造函数使用对应的实参初始化成员
 5 Sales_data(std::string s, unsigned cnt, double price):bookNo(s), units_sold(cnt), revenue(cnt *  price) {}
 6 //其余构造函数全都委托给另一个构造函数
 7 Sales_data(): Sales_data("", 0 , 0) {}
 8 Sales_data(std::string s): Sales_data(s, 0, 0) {}
 9 Sales_data(std::istream &is): Sales_data()
10 {
11 read(is, *this);
12 }
13 }
构造函数与初始化

4.合成默认构造函数再探

其实我先前大部分的纠结都是在与编译器合成的默认构造函数上面,正如《Inside the C++ Object Model》中所说,面向对象的编程过程中,编译器背着我们做了太多的事,以至于很多东西看起来很不自然。

而对于合成默认构造函数,首先,它是在程序员没有显式地定义任何一个构造函数时编译器自动生成的,也就是说,只要我们自定义了任何一个构造函数,编译器将不再自动添加。而关于合成默认构造函数的作用,C++ Primer 第五版中文版中这样说到:

· 如果存在类内的初始值,用它来初始化成员,即C++11新标准中新增的类内初始化。

· 否则,执行默认初始化该成员。

也就是说,如果存在类内初始值,一切都好办,编译器将利用类内初始化赋予数据成员初始值,这是合成默认构造函数的工作。

而如果不考虑C++ 11新标准,即如果没有类内初始值的情况下,执行默认初始化,即:

对于基本内置类型和指针,引用等复合类型,其值是未定义的。(注1:static 型变量除外,注2:有时可能也依赖于编译器的实现,有的编译器可能就直接赋初值0)。

对于含有默认构造函数的类类型成员变量,合成默认构造函数会调用该类的默认构造函数来初始化类类型的数据成员。

然而,合成默认构造函数可能并没有这么简单,因为牵扯到编译器的优化问题,所以在《Inside the C++ Model》中,又详细阐述了根据不同的情况,编译器会怎样来合成

默认构造函数:

例如,一个很典型的例子就是,如果一个类的所有数据成员都是内置类型或复合类型(非static),那么编译器将非常聪明地不再合成默认构造函数,因为即使合成了默认构造函数也会什么都不做,所以就会优化掉,成员变量的值还是未定义的。

(注:其实还是那样的,这只是C++标准的规定,针对具体的编译器实现,可能这时会有不同,也会合成默认构造函数来初始化成员为 0)。

而如果一个类中含有其他类类型的数据成员,这个数据成员的类类型又恰好有默认构造函数,这时,编译器合成默认构造函数就是义不容辞的了,因为在这个合成默认构造函数中有非常重要的工作来做:即调用数据成员类类型的默认构造函数来初始化数据成员。

对于其他的编译器必须合成默认构造函数的情况,《Inside the C++ Model》总结了四种情况:

含有类对象数据成员,该类对象类型有默认构造函数;类的基类提供了默认的构造函数;类内定义了虚函数;类使用了虚继承。

注:后面两种情况其实涉及到了深刻的C++中多态机制和虚继承机制的实现。

其实可以归结为一句话:只有编译器不得不为这个类生成函数的时候(nontrival),编译器才会真正的生成它。

参考:http://www.cnblogs.com/fanzhidongyzby/archive/2013/01/12/2858040.html

参考:http://www.tuicool.com/m/articles/iYveIjR

5. 前天晚上的疑惑

前天我一直疑惑于下面一段代码:

1 class Foo
2 {
3 std::string str_;
4 };

我当时的想法是在类内定义str_的时候已经调用了string的默认构造函数来完成了初始化,那么在定义一个对象 Foo a 时,编译器默认生成的构造函数为什么还要调用一次

string的默认构造函数呢?这样来讲,《Inside the C++ Model》中第一种情况就不对了啊。

后来经过在知乎私信两位陈硕,dong大神,问题才得以解决。

其实我的理解是没错的,string类 不管在任何地方定义一个对象(全局或者局部处),都会调用默认的构造函数。

然而正如陈硕大大所说,“定义” class member 和 “定义” class object是不一样的 “定义”,其实前者更像是声明一点。

也就是说,在定义一个Foo类的时候,其实是没有分配内存的,str_更没有存储空间,更不用提初始化的问题了。只有在实例化即定义一个对象的时候,才会完成内存分配,

此时合成默认构造函数就会调动string的默认构造函数来初始化str_,这和《Inside the C++ Model》中是一致的。

当运用sizeof运算符做如下运算的时候,sizeof(Foo),其实是调用默认构造函数生成了一个临时Foo对象,得到的也是临时对象的存储空间大小,类本身并没有存储空间。

关于类对象的存储机制:

参考:http://blog.csdn.net/zhangliang_218/article/details/5544802

参考:http://blog.sina.com.cn/s/blog_69c189bf0100mkeu.html#cmt_2743824

参考:http://www.cnblogs.com/jerry19880126/p/3616999.html

6. 其他自己的一些思考

 1)其实关于默认构造函数我觉得可能还依赖于不同的编译器的实现,尽管C++标准上说的是未定义、默认初始化什么的,还有《Inside the C++ Model》说的编译器的优化问题,但是我觉得可能还有赖于编译器的具体实现,会不会全部初始化为 0 什么的。

 2)关于C++ 11标准新增的类内初始化,我觉得可能会影响以往的一些编译器实现。

  首先如果类的所有的数据成员都进行了类内初始化,那么编译器可能也会将默认构造函数优化掉。

  然后就是如果存在类内初始化的话,就没有必要非要在构造函数初始值列表中初始化引用和const数据了,可以直接类内初始化。

  注:毕竟《Inside the C++ Model》是根据C++95标准写的,现在来看有些地方过时也很正常。

  我个人觉得,尽管C++的标准在变化,但是有一点肯定是不变的,就是在C++语言中,创建一个对象,首先就会调用构造函数。

  而针对于合成默认构造函数的编译器优化,则可能会随着标准的变化而变化。一个原则:只有在被需要的时候,编译器才会做。毕竟在C++中,效率很主要,

  而且编译器是很聪明的。

 3)关于对象数组。

  对象数组的定义的话,我觉得还是和每一个对象定义时一样,如果有默认构造函数才能定义对象数组,然后数组的每一个元素都会按照默认构造函数来初始化。

7. 感想

 坦白讲,一直在这上面纠结真的是很浪费时间的。而且我现在都快想的走火入魔了,其实我现在根本就不在学习编程的正确的轨道上,

学习编程实践很中重要,我却总是纠结于一些语法的具体实现,和一些看起来无伤大雅的东西。

真的,不要再纠结于这些,一来可能现在以你的知识你还搞不定这些东西,理解不了,想太多也是浪费时间,还有可能会走火入魔,自己跳不出来,

二来编程真的实践特别中重要,一定要多敲代码多敲代码!!!!!

还有就是关于这些问题,其实等你能力上去了,这些问题都是可以通过自己的实践来验证的。

所以现在纠结这些没有任何意义!!!多敲代码才是王道!!!

(以下内容均在VS2019下得到验证):

C++ Primer 第236页是对的,合成默认构造函数将:

· 如果存在类内的初始值,用它来初始化成员,即C++11新标准中新增的类内初始化。

· 否则,执行默认初始化该成员。

也就是说利用类内初始值进行对象的初始化是合成默认构造函数的工作,(唉本来还以为是primer写错了呢,现在发现不是,又更加加深了对C++ Primer的崇敬之感)。

那么在C++ 11的标准之下,《Inside the C++ Model》在原来的四种情况的基础上,应该再加一种情况了(原来的四种情况仍然成立):

就是即使类的数据成员都是内置类型或者复合类型,只要存在类内初始化(哪怕只有一个成员),编译器就会合成默认构造函数来初始化数据成员,而对于没有类内初始值的

其他成员,执行默认初始化(未定义的)。当然,这些都是建立在程序员没有显式地定义一个构造函数的前提下。

此外,在VS2013环境下得到的一些结论:

···如果存在构造函数,将优先执行构造函数初始值列表,然后执行类内初始化,如果初始值列表提供了一些数据成员初始值,编译器将不再调用这些成员的类内的初始化(如       果有的话),来提高效率。

···在创建一个对象的时候,首先会调用构造函数,任何情况下都是如此,没有例外。

···在构造函数体开始执行以前,对象的初始化其实已经完成了。

···在VS2013 的环境下,编译器似乎必须要生成一个默认构造函数,就是如果定义一个类数据成员都是内置类型,且没有类内初始化,且没有显式地定义构造函数,这时,

   编译器是不答应的,也就是说如果你不给它生成默认构造函数的机会,它是不愿意的。

···似乎别人都是在Linux下可以方便的查看合成默认构造函数可以调用,然而我在VS下找了半天也没找到。

参考:https://www.zhihu.com/question/30804314/answer/49894954

参考:http://www.cnblogs.com/fanzhidongyzby/archive/2013/01/12/2858040.html

至此为止,彻彻底底搞清楚了C++的构造函数与初始化问题!!!

神清气爽!!!实践出真知啊!!

——Wulnut

国家保护废材
最后更新于 2019-05-27