设为首页 加入收藏

TOP

C++的各种初始化方式(一)
2017-10-10 21:06:06 】 浏览:7059
Tags:各种 初始 方式

C++小实验测试:下面程序中main函数里a.a和b.b的输出值是多少?


答案是a.a是0,b.b是不确定值(不论你是gcc编译器,还是clang编译器,或者是微软的msvc++编译器)。为什么会这样?这是因为C++中的初始化已经开始畸形发展了。


 


接下来,我要探索一下为什么会这样。在我们知道原因之前,先给出一些初始化的概念:默认初始化,值初始化,零初始化。


  上面这些不同形式的初始化方式有点复杂,我会对这些C++11的初始化做一下简化:


 看一下上面的例子,如果T是int类型,那么global和那些T类型的使用值初始化形式的变量都会初始化为0(因为int是内置类型,不是类类型,也不是数组,将会零初始化,又因为int是算术类型,如果进行零初始化,则初始值为0)而其他的默认初始化都是未定义值。


 


 回到开头的例子,现在我们已经有了搞明白这个例子所必要的基础知识。造成结果不同的根本原因是:foo和bar被它们不同位置的默认构造函数所影响。


foo的构造函数在起初声明时是要求默认合成,而不是我们自定义提供的,因此它属于编译器合成的默认构造函数。而bar的构造函数则不同,它是在定义时被要求合成,因此它属于我们用户自定义的默认构造函数


前面提到的关于值初始化的规则时,有说明到:如果T类型的默认构造函数不是用户自定义的,默认初始化之前先进行零初始化。因为foo的默认构造函数不是我们自定义的,是编译器合成的,所以在对foo类型的对象进行值初始化时,会先进行一次零初始化,然后再调用默认构造函数,这导致a.a的值被初始化为0,而bar的默认构造函数是用户自定义的,所以不会进行零初始化,而是直接调用默认构造函数,从而导致b.b的值是未初始化的,因此每次都是随机值。


这个陷阱迫使我们注意:如果你不想要你的默认构造函数是用户自定义的,那么必须在类的内部声明处使用"=default",而不是在类外部定义处使用。


对于类类型来说,用户提供自定义的默认构造函数有一些额外的“副作用”。比如,对于缺少用户提供的自定义默认构造函数的类,是无法定义该类的const对象的。示例如下:


 


通过开头的例子,我们已经对C++的一些初始化方式有了直观的感受。 C++中的初始化分为6种:零初始化、默认初始化、值初始化、直接初始化、拷贝初始化、列表初始化。


零初始化和变量的类型和位置有关系,比如是否static,是否aggregate聚合类型。能进行0初始化的类型的对象的值都是0,比如int为0,double为0.0,指针为nullptr;


 现在我们已经了解了几种初始化的规则,下面则是几种初始化方式的使用形式:


1. 默认初始化是定义对象时,没有使用初始化器,也即没有做任何初始化说明时的行为。典型的:


 


2. 值初始化是定义对象时,要求初始化,但没有给出初始值的行为。典型的:


 


3. 直接初始化和拷贝初始化主要是相对于我们自定义的对象的初始化而言的,对于内置类型,这两者没有区别。对于自定义对象,直接初始化和拷贝初始化区别是直接调用构造函数还是用"="来进行初始化。典型的:


对于书本中给出的示例:


这里s的初始化书本说是直接初始化,看起来似乎像是拷贝初始化,其实的确是直接初始化,因为直接初始化是用参数来直接匹配某一个构造函数,而拷贝构造函数和其他构造函数形成了重载,以至于刚好调用了拷贝构造函数。


事实上,C++语言标准规定复制初始化应该是先调用对应的构造函数创建一个临时对象,然后拷贝构造函数再将构造的临时对象拷贝给要创建的对象。例如:


上面代码中,因为“hello"的类型是const char *,所以string类的string(const char *)构造函数会被首先调用,创建一个临时对象,然后拷贝??造函数将这个临时对象复制到a。但是标准还规定,为了提高效率,允许编译器跳过创建临时对象这一步,直接调用构造函数构造要创建的对象,从而忽略调用拷贝构造函数进行优化,这样就完全等价于直接初始化了,当然可以使用-fno-elide-constructors选项来禁用优化。


如果我们将string类型的拷贝构造函数定义为private或者定义为delete,那么就无法通过编译,虽然能够进行优化省略拷贝构造函数的调用,但是拷贝构造函数在语法上还是要能正常访问的,这也是为什么C++ primer第五版第13章拷贝控制13.1.1节末尾442页最后一段话中说:


拷贝初始化不仅在使用=定义变量时会发生,在以下几种特殊情况中也会发生:


1.将一个对象作为实参传递给一个非引用的形参;


2.从一个返回类型为非引用的函数返回一个对象;


3.用花括号列表初始化一个数组中的元素或一个聚合类中的成员。


其实还有一个情况,比如:当以值抛出或捕获一个异常时。


 


另外还有比较让人迷惑的地方在于vector<string> v2(10),在《C++ Primer 5th》中说这是值初始化的方式,但是仔细看书本,这里的值初始化指的是容器中string元素,也就是说v2本身是直接初始化的,而v2中的10个string元素,由于没有给出初始值,因此标准库对容器中的元素采用了值初始化的方式进行初始化。


结合来说:


 


4. 列表初始化是C++新标准给出的一种初始化方式,可用于内置类型,也可以用于自定义对象,前者比如数组,后者比如vector。典型的:


 


文章写到这里,读者认真的看到这里,似乎已经懂了C++的各种初始化规则和方式,下面用几个例子来检测一下:


 


试问上面代码中,main程序中的各个输出值是多少?先不忙使用编译器编译程序,根据之前介绍的知识先推断一番:


首先,我们需要明白,对于类来说,构造函数是用来负责类对象的初始化的,一个类对象无论如何一定会被初始化。也就是说,当实例化类对象时,一定会调用构造函数,不论构造函数是否真的初始化了数据成员。故而对于没有定义任何构造函数的自定义类来说,该类的默认构造函数不存在“被需要/不被需要”这回事,它必然会被合成。


由于Init1和Init2它们拥有类似的合成默认构造函数,因此它们的ia1.i和ib1.i值相同,应该都是随机值,而ia2.i和ib2.i被要求值初始化,因此它们的值都是0。


由于Init3和Init4它们拥有类似的用户自定义默认构造函数,因此它们的ic1.i和id1.i值相同,应该都是随机值,而ic2.i和id2.i虽然被要求值初始化,但也是随机值。


由于Init5我们为它显式提供了默认构造函数,并且手动的初始化了数据成员,因此它的ie1.i和ie2.i都会被初始化为0。


虽然,上面程序中有一些地方因为操作系统和编译器的原因和我们预期的结果不相同,但也有必然相同的地方,比如最后一个使用了构造函数初始化列表的类的行为就符合预期。还有在合成的默认构造函数之前会先零初始化的地方,必然会初始化为0。


至此,我们已经对C++的初始化方式和规则已经有了一个了然于胸的认识,那就是:由于平台和编译器的差异,以及对语言标准的遵守程度不同,我们决不能依赖于合成的默认构造函数。这也是为什么C++ Primer中多次强调我们不要依赖合成的默认构

首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇关于Java的“找不到或无法加载主.. 下一篇JavaWeb中监听器

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目