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

前言

本文主要给大家介绍了关于C++初始化方式的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧。

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

#include <iostream>

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

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

T global;    //T是我们的自定义类型,首先零初始化,然后默认初始化

void foo()
{
 T i;  //默认初始化
 T j{}; //值初始化(C++11)
 T k = T(); //值初始化
 T l = T{}; //值初始化(C++11)
 T m(); //函数声明

 new T; //默认初始化
 new T(); //值初始化
 new T{}; //值初始化(C++11)
}

struct A
{
 T t;
 A() : t() //t将值初始化
 {
 //构造函数
 }
};

struct B
{
 T t;
 B() : t{} //t将值初始化(C++11)
 {
 //构造函数
 }
};

struct C
{
 T t;
 C()  //t将默认初始化
 {
 //构造函数
 }
};

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

  • 默认初始化 :如果 T 是一个类,那么调用默认构造函数进行初始化;如果是一个数组,每个元素默认初始化,否则不进行初始化,其值未定义。至于 合成的 默认构造函数初始化数据成员的规则是:1.如果类数据成员存在类内初始值,则用该值初始化相应成员(c++11);2.否则,默认初始化数据成员。
  • 值初始化 :如果 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对象的。示例如下:

class exec
{
 int i;
};

const exec e;  //错误!缺少用户自定义默认构造函数,不允许定义const类对象

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

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

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

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

int i;
vector<int> v;

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

int i{};
new int();
new int{}; //C++11

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

vector<int>  v1(10); //直接初始化,匹配某一构造函数
vector<string> v2(10); //直接初始化,匹配某一构造函数
vector<int>  v3=v1;  //拷贝初始化,使用=进行初始化

对于书本中给出的示例:

string dots(10, '.'); //直接初始化
string s(dots);      //直接初始化

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

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

string 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.用花括号列表初始化一个数组中的元素或一个聚合类中的成员。

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

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

结合来说:

只要使用了括号(圆括号或花括号)但没有给出具体初始值,就是值初始化。可以简单理解为括号告诉编译器你希望该对象初始化。

没有使用括号,就是默认初始化。可以简单理解成,你放任不管,允许编译器使用默认行为。通常这是糟糕的行为,除非你真的懂自己在干什么。

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

int array[5]={1,2,3,4,5};
vector<int> v={1,2,3,4,5};

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

#include <iostream>

using namespace std;

class Init1
{
public:
 int i;

};

class Init2
{
public:
 Init2() = default;

 int i;

};

class Init3
{
public:
 Init3();
 int i;

};

Init3::Init3() = default;

class Init4
{
public:
 Init4();

 int i;

};

Init4::Init4()
{
 //constructor
}

class Init5
{
public:
 Init5(): i{}
 {

 }
 int i;

};

int main(int argc, char const *argv[])
{
 Init1 ia1;
 Init1 ia2{};
 cout << "Init1: " << " "
   << "i1.i: " << ia1.i << "\t"
   << "i2.i: " << ia2.i << "\n";

 Init2 ib1;
 Init2 ib2{};
 cout << "Init2: " << " "
   << "i1.i: " << ib1.i << "\t"
   << "i2.i: " << ib2.i << "\n";

 Init3 ic1;
 Init3 ic2{};
 cout << "Init3: " << " "
   << "i1.i: " << ic1.i << "\t"
   << "i2.i: " << ic2.i << "\n";

 Init4 id1;
 Init4 id2{};
 cout << "Init4: " << " "
   << "i1.i: " << id1.i << "\t"
   << "i2.i: " << id2.i << "\n";

 Init5 ie1;
 Init5 ie2{};
 cout << "Init5: " << " "
   << "i1.i: " << ie1.i << "\t"
   << "i2.i: " << ie2.i << "\n";

 return 0;
}

试问上面代码中,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 2017,DEBUG模式)下,结果却完全吻合(可能是由于微软开始接纳开源和Linux,逐渐的严格遵守了语言标准),GCC的结果也是完全符合,而广受好评的Clang却部分结果符合。当然,相同的Clang编译器在Mac和Ubuntu下结果甚至都不一致,GCC在某些时候甚至比Clang还人性化的Warning告知使用了未初始化的数据成员。

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

至此,我们已经对C++的初始化方式和规则已经有了一个了然于胸的认识,那就是:由于平台和编译器的差异,以及对语言标准的遵守程度不同,我们决不能依赖于合成的默认构造函数。这也是为什么C++ Primer中多次强调我们不要依赖合成的默认构造函数,也说明了C++ Primer在关于手动分配动态内存那里告诉我们,对于我们自定义的类类型来说,为什么要求值初始化是没有意义的。

C++语言设计的一个基本思想是“自由”,对于某些东西它既给出了具体要求,又留出了发挥空间,而那些未加以明确的地方是属于语言的“灰暗地带”,我们需要小心翼翼的避过。在对象的初始化这里,推荐的做法是将默认构造函数删除,由我们用户自己定义自己的构造函数,并且合理的初始化到每个成员,如果需要保留默认构造函数,一定要对它的行为做到心里有数。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • C++直接初始化与复制初始化的区别深入解析

    C++中直接初始化与复制初始化是很多初学者容易混淆的概念,本文就以实例形式讲述二者之间的区别.供大家参考之用.具体分析如下: 一.Primer中的说法 首先我们现来看看经典是怎么说的: "当用于类类型对象时,初始化的复制形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数,复制初始化总是调用复制构造函数.复制初始化首先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象" 还有一段这样说: "通常直接初始化和复制初始化仅在低级别优化

  • C++类的静态成员初始化详细讲解

    记住:通常静态数据成员在类声明中声明,在包含类方法的文件中初始化.初始化时使用作用域操作符来指出静态成员所属的类.但如果静态成员是整型或是枚举型const,则可以在类声明中初始化!!! 复制代码 代码如下: #include <iostream>using namespace std;class test{public:static int num;};int test::num = 0;void main(){cout<<test::num <<endl;test::

  • C++中COM组件初始化方法实例分析

    本文实例讲述了C++中COM组件初始化方法.分享给大家供大家参考.具体如下: 这里使用BCB 在使用TADOConnect等组件时需要进行初始化 调用接口 : CoInitialize(NULL);//初始化COM套件 CoUninitialize();//释放COM套件 在DLL入口中调用: static bool isCoInitialize = false; //是否是自己进行的初始化 int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned l

  • C++初始化列表学习

    何谓初始化列表与其他函数不同,构造函数除了有名字,参数列表和函数体之外,还可以有初始化列表,初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段.在C++中,struct和class的唯一区别是默认的克访问性不同,而这里我们不考虑访问性的问题,所以下面的代码都以struct来演示. 复制代码 代码如下: struct foo{    string name ;    int id ;    foo(string s, int i):name(s), id(i){} ; // 初始化列表}; 构

  • C++构造函数初始化顺序详解

    1.构造函数.析构函数与拷贝构造函数介绍 构造函数 1.构造函数不能有返回值 2.缺省构造函数时,系统将自动调用该缺省构造函数初始化对象,缺省构造函数会将所有数据成员都初始化为零或空 3.创建一个对象时,系统自动调用构造函数 析构函数 1.析构函数没有参数,也没有返回值.不能重载,也就是说,一个类中只可能定义一个析构函数 2.如果一个类中没有定义析构函数,系统也会自动生成一个默认的析构函数,为空函数,什么都不做 3.调用条件:1.在函数体内定义的对象,当函数执行结束时,该对象所在类的析构函数会被

  • 浅析C++中结构体的定义、初始化和引用

    定义:结构体(struct)是由一系列具有相同类型或不同类型的数据构成的数据集合,也叫结构. 声明一个结构体类型的形式是: 复制代码 代码如下: struct Student{      //声明一个结构体类型Student  int num;         //声明一个整形变量num  char name[20];   //声明一个字符型数组name  char sex;        //声明一个字符型变量sex  int age;         //声明一个整形变量age  float

  • c++ 构造函数的初始化列表

    首先,运行下图中的C++代码,输出是什么? 复制代码 代码如下: class A{private: int n1; int n2;public: A(): n2(0) , n1(n2 + 2) { } void Print() {  cout<<"n1:"<<n1<<",n2:"<<n2<<endl; }};int main(void){ A a; a.Print(); return 0;} 答案:输出n1

  • C++中结构体的类型定义和初始化以及变量引用

    C++结构体类型的定义和初始化 有时需要将不同类型的数据组合成一个有机的整体,以供用户方便地使用.这些组合在一个整体中的数据是互相联系的.例如,一个学生的学号.姓名.性别.年龄.成绩.家庭地址等项,都是这个学生的属性,见图 可以看到学号(num).姓名(name).性别(sex).年龄(age).成绩(score ).地址(addr)是与姓名为"Li Fun"的学生有关的.如果在程序中将num,name,sex,age,score,addr分别定义为互相独立的变量,就难以反映出它们之间

  • c++基础语法:构造函数初始化列表

    C++为类中提供类成员的初始化列表 类对象的构造 顺序是这样的:1.分配内存,调用构造函数 时,隐式/显示的初始化各数据 成员2.进入构造函数后在构造函数中执行一般计算 使用初始化列表有两个原因: 1.必须这样做:如果我们有一个类成员,它本身是一个类或者是一个结构,而且这个成员它只有一个带参数的构造函数,而没有默认构造函数,这时要对这个类成员进行初始化,就必须调用这个类成员的带参数的构造函数,如果没有初始化列表,那么他将无法完成第一步,就会报错. 复制代码 代码如下: class  ABC  .

  • C++初始化函数列表详细解析

    在以下三种情况下需要使用初始化成员列表: 一,需要初始化的数据成员是对象的情况: 二,需要初始化const修饰的类成员: 三,需要初始化引用成员数据: 原因:C++可以定义引用类型的成员变量,引用类型的成员变量必须在构造函数的初始化列表中进行初始化.对于类成员是const修饰,或是引用类型的情况,是不允许赋值操作的,(显然嘛,const就是防止被错误赋值的,引用类型必须定义赋值在一起),因此只能用初始化列表对齐进行初始化.成员类型是没有默认构造函数的类.若没有提供显示初始化式,则编译器隐式使用成

随机推荐