ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

C++中各种初始化方式示例详解

C++中各种初始化方式示例详解 本文主要给大家介绍了关于C初始化方式的相关内容分享出来供大家参考学习下面话不多说了来一起看看详细的介绍吧。C小实验测试下面程序中main函数里a.a和b.b的输出值是多少1234567891011121314151617181920212223#include iostreamstruct foo{foo() default;int a;};struct bar{bar();int b;};bar::bar() default;int main(){foo a{};bar b{};std::cout a.a \t b.b;}答案是a.a是0b.b是不确定值(不论你是gcc编译器还是clang编译器或者是微软的msvc编译器)。为什么会这样这是因为C中的初始化已经开始畸形发展了。接下来我要探索一下为什么会这样。在我们知道原因之前先给出一些初始化的概念默认初始化值初始化零初始化。1234567891011121314151617181920212223242526272829303132333435363738394041T global;//T是我们的自定义类型首先零初始化然后默认初始化void foo(){T i;//默认初始化T j{};//值初始化(C11)T k T();//值初始化T l T{};//值初始化(C11)T m();//函数声明newT;//默认初始化newT();//值初始化newT{};//值初始化(C11)}struct A{T t;A() : t()//t将值初始化{//构造函数}};struct B{T t;B() : t{}//t将值初始化(C11){//构造函数}};struct C{T t;C()//t将默认初始化{//构造函数}};上面这些不同形式的初始化方式有点复杂我会对这些C11的初始化做一下简化默认初始化 如果 T 是一个类那么调用默认构造函数进行初始化如果是一个数组每个元素默认初始化否则不进行初始化其值未定义。至于 合成的 默认构造函数初始化数据成员的规则是1.如果类数据成员存在类内初始值则用该值初始化相应成员c112.否则默认初始化数据成员。值初始化 如果 T 是一个类那么类的对象进行默认初始化( 如果T类型的默认构造函数 不是 用户自定义的默认初始化之前先进行零初始化 )如果是一个数组每个元素值初始化否则进行零初始化。零初始化 对于static或者thread_local变量将会在其他类型的初始化之前先初始化。如果T是算数、指针、枚举类型将会初始化为0如果是类类型基类和数据成员会零初始化如果是数组数组元素也零初始化。看一下上面的例子如果T是int类型那么global和那些T类型的使用值初始化形式的变量都会初始化为0因为int是内置类型不是类类型也不是数组将会零初始化又因为int是算术类型如果进行零初始化则初始值为0而其他的默认初始化都是未定义值。回到开头的例子现在我们已经有了搞明白这个例子所必要的基础知识。造成结果不同的根本原因是foo和bar被它们不同位置的默认构造函数所影响。foo的构造函数在起初声明时是要求默认合成而不是我们自定义提供的因此它属于编译器 合成的 默认构造函数 。而bar的构造函数则不同它是在定义时被要求合成因此它属于我们用户 自定义的 默认构造函数 。前面提到的关于值初始化的规则时有说明到 如果T类型的默认构造函数不是用户自定义的默认初始化之前先进行零初始化 。因为foo的默认构造函数不是我们自定义的是编译器合成的所以在对foo类型的对象进行值初始化时会先进行一次零初始化然后再调用默认构造函数这导致a.a的值被初始化为0而bar的默认构造函数是用户自定义的所以不会进行零初始化而是直接调用默认构造函数从而导致b.b的值是未初始化的因此每次都是随机值。这个陷阱迫使我们注意如果你不想要你的默认构造函数是用户自定义的那么必须在类的内部声明处使用default而不是在类外部定义处使用。对于类类型来说用户提供自定义的默认构造函数有一些额外的“副作用”。比如对于缺少用户提供的自定义默认构造函数的类是无法定义该类的const对象的。示例如下123456classexec{inti;};constexec e;//错误缺少用户自定义默认构造函数不允许定义const类对象通过开头的例子我们已经对C的一些初始化方式有了直观的感受。 C中的初始化分为6种零 初始化、 默认初始化、值初始化、直接初始化、拷贝初始化、列表初始化。零初始化和变量的类型和位置有关系比如是否static是否aggregate聚合类型。能进行0初始化的类型的对象的值都是0比如int为0double为0.0指针为nullptr现在我们已经了解了几种初始化的规则下面则是几种初始化方式的使用形式1. 默认初始化是定义对象时没有使用初始化器也即没有做任何初始化说明时的行为。典型的12inti;vectorint v;2. 值初始化是定义对象时要求初始化但没有给出初始值的行为。典型的123inti{};newint();newint{};//C113. 直接初始化和拷贝初始化主要是相对于我们自定义的对象的初始化而言的对于内置类型这两者没有区别。对于自定义对象直接初始化和拷贝初始化区别是直接调用构造函数还是用来进行初始化。典型的123vectorint v1(10);//直接初始化匹配某一构造函数vectorstring v2(10);//直接初始化匹配某一构造函数vectorint v3v1;//拷贝初始化使用进行初始化对于书本中给出的示例12string dots(10,.);//直接初始化string s(dots);//直接初始化这里s的初始化书本说是直接初始化看起来似乎像是拷贝初始化其实的确是直接初始化因为直接初始化是用参数来直接匹配某一个构造函数而拷贝构造函数和其他构造函数形成了重载以至于刚好调用了拷贝构造函数。事实上C语言标准规定复制初始化应该是先调用对应的构造函数创建一个临时对象然后拷贝构造函数再将构造的临时对象拷贝给要创建的对象。例如1string a hello;上面代码中因为“hello的类型是const char *所以string类的string(const char *)构造函数会被首先调用创建一个临时对象然后拷贝构造函数将这个临时对象复制到a。但是标准还规定为了提高效率允许编译器跳过创建临时对象这一步直接调用构造函数构造要创建的对象从而忽略调用拷贝构造函数进行优化这样就完全等价于直接初始化了当然可以使用-fno-elide-constructors选项来禁用优化。如果我们将string类型的拷贝构造函数定义为private或者定义为delete那么就无法通过编译虽然能够进行优化省略拷贝构造函数的调用但是拷贝构造函数在语法上还是要能正常访问的这也是为什么C primer第五版第13章拷贝控制13.1.1节末尾442页最后一段话中说“即使编译器略过了拷贝/移动构造函数但在这个程序点上拷贝/移动构造函数必须是存在且可访问的例如不能是priviate的。拷贝初始化不仅在使用定义变量时会发生在以下几种特殊情况中也会发生1.将一个对象作为实参传递给一个非引用的形参2.从一个返回类型为非引用的函数返回一个对象3.用花括号列表初始化一个数组中的元素或一个聚合类中的成员。其实还有一个情况比如当以值抛出或捕获一个异常时。另外还有比较让人迷惑的地方在于vectorstring v2(10)在《C Primer 5th》中说这是值初始化的方式但是仔细看书本这里的值初始化指的是容器中string元素也就是说v2本身是直接初始化的而v2中的10个string元素由于没有给出初始值因此标准库对容器中的元素采用了值初始化的方式进行初始化。结合来说只要使用了括号圆括号或花括号但没有给出具体初始值就是值初始化。可以简单理解为括号告诉编译器你希望该对象初始化。没有使用括号就是默认初始化。可以简单理解成你放任不管允许编译器使用默认行为。通常这是糟糕的行为除非你真的懂自己在干什么。4. 列表初始化是C新标准给出的一种初始化方式可用于内置类型也可以用于自定义对象前者比如数组后者比如vector。典型的12intarray[5]{1,2,3,4,5};vectorint v{1,2,3,4,5};文章写到这里读者认真的看到这里似乎已经懂了C的各种初始化规则和方式下面用几个例子来检测一下123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990#include iostreamusingnamespacestd;classInit1{public:inti;};classInit2{public:Init2() default;inti;};classInit3{public:Init3();inti;};Init3::Init3() default;classInit4{public:Init4();inti;};Init4::Init4(){//constructor}classInit5{public:Init5(): i{}{}inti;};intmain(intargc,charconst*argv[]){Init1 ia1;Init1 ia2{};cout Init1: i1.i: ia1.i \ti2.i: ia2.i \n;Init2 ib1;Init2 ib2{};cout Init2: i1.i: ib1.i \ti2.i: ib2.i \n;Init3 ic1;Init3 ic2{};cout Init3: i1.i: ic1.i \ti2.i: ic2.i \n;Init4 id1;Init4 id2{};cout Init4: i1.i: id1.i \ti2.i: id2.i \n;Init5 ie1;Init5 ie2{};cout Init5: i1.i: ie1.i \ti2.i: ie2.i \n;return0;}试问上面代码中main程序中的各个输出值是多少先不忙使用编译器编译程序根据之前介绍的知识先推断一番首先我们需要明白对于类来说构造函数是用来负责类对象的初始化的一个类对象无论如何一定会被初始化。也就是说当实例化类对象时一定会调用构造函数不论构造函数是否真的初始化了数据成员。故而对于没有定义任何构造函数的自定义类来说该类的默认构造函数不存在“被需要/不被需要”这回事它必然会被合成。对于Init1由于我们对其没有做任何构造函数的声明和定义因此会合成默认构造函数。对于Init2我们在类内部声明处要求合成默认构造函数因此也会有合成的默认构造函数。由于Init1和Init2它们拥有类似的合成默认构造函数因此它们的ia1.i和ib1.i值相同应该都是随机值而ia2.i和ib2.i被要求值初始化因此它们的值都是0。对于Init3我们在类外部定义处要求编译器为我们生成默认构造函数此默认构造函数为用户自定义的默认构造函数。对于Init4我们显式的定义了用户自定义默认构造函数。由于Init3和Init4它们拥有类似的用户自定义默认构造函数因此它们的ic1.i和id1.i值相同应该都是随机值而ic2.i和id2.i虽然被要求值初始化但也是随机值。对于Init5我们显式的定义了用户自定义默认构造函数并且使用了构造函数初始化列表来值初始化数据成员。由于Init5我们为它显式提供了默认构造函数并且手动的初始化了数据成员因此它的ie1.i和ie2.i都会被初始化为0。以上是我们的预测结果会是这样吗遗憾的是结果不一定是这样。是我们哪里出错了我们并没有错误上面的程序结果取决于你使用的操作系统、编译器版本(比如gcc-5.0和gcc-7.0)和发行版(比如gcc和clang)。可能有的人能获得和推测完全相同的结果而有的人不能比如在经常被批不遵守C标准的微软VC编译器(VS 2017DEBUG模式)下结果却完全吻合可能是由于微软开始接纳开源和Linux逐渐的严格遵守了语言标准GCC的结果也是完全符合而广受好评的Clang却部分结果符合。当然相同的Clang编译器在Mac和Ubuntu下结果甚至都不一致GCC在某些时候甚至比Clang还人性化的Warning告知使用了未初始化的数据成员。虽然上面程序中有一些地方因为操作系统和编译器的原因和我们预期的结果不相同但也有必然相同的地方比如最后一个使用了构造函数初始化列表的类的行为就符合预期。还有在合成的默认构造函数之前会先零初始化的地方必然会初始化为0。至此我们已经对C的初始化方式和规则已经有了一个了然于胸的认识那就是由于平台和编译器的差异以及对语言标准的遵守程度不同我们决不能依赖于合成的默认构造函数。这也是为什么C Primer中多次强调我们不要依赖合成的默认构造函数也说明了C Primer在关于手动分配动态内存那里告诉我们对于我们自定义的类类型来说为什么要求值初始化是没有意义的。C语言设计的一个基本思想是“自由”对于某些东西它既给出了具体要求又留出了发挥空间而那些未加以明确的地方是属于语言的“灰暗地带”我们需要小心翼翼的避过。在对象的初始化这里推荐的做法是将默认构造函数删除由我们用户自己定义自己的构造函数并且合理的初始化到每个成员如果需要保留默认构造函数一定要对它的行为做到心里有数。
返回列表