构造函数与初始化

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

而后在第235页中又提到:“构造函数是一个很是复杂的问题”。程序员

刚好这两个问题连在一块儿,就成了一个异常很是复杂的问题,把我折磨的够呛。编程


1.初始化数组

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

操做。然而在不少编程语言中两者的区别几乎能够忽略不计,即便在C++语言有时这种区别也可有可无,因此人们特别容易把两者混为一谈。须要强调的是,这个ide

概念相当重要。函数

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

列表初始化优化

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

均可以作到这一点:

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. 感想

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

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

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

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

还有就是关于这些问题,其实等你能力上去了,这些问题都是能够经过本身的实践来验证的。

因此如今纠结这些没有任何意义!!!多敲代码才是王道!!!

4月18日晚上更新(如下内容均在VS2013下获得验证):

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++的构造函数与初始化问题!!!

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

相关文章
相关标签/搜索