
前言
本文主要向您介绍有关C ++初始化方法的相关内容. 分享它供您参考和学习. 我不会谈论太多. 让我们看一下详细的介绍.
C ++小实验测试: 以下程序中主函数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编译器还是Microsoft 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为int类型,则全局变量和使用值初始化形式的T型变量将被初始化为0(因为int是内置类型,而不是类类型或数组,因此它将为零. ,因为int是算术类型,所以如果执行零初始化,则初始值为0),其他默认初始化为未定义的值.
首先回到示例,我们现在已经具有理解该示例所需的基本知识. 结果不同的根本原因是foo和bar受它们在不同位置的默认构造函数影响.
最初要求foo的构造函数是在最初声明时综合的,而不是由我们提供的,因此它属于编译器综合的默认构造函数. 酒吧的建设者是不同的. 它需要在定义时进行综合,因此它属于我们用户定义的默认构造函数.

前面提到的有关值初始化的规则指出: 如果类型T的默认构造函数不是用户定义的,则在默认初始化之前执行零初始化. 由于foo的默认构造函数不是我们的自定义,它是由编译器合成的,因此,在初始化foo类型的对象的值时,将首先执行零初始化,然后调用默认构造函数,这将导致值的aa的初始化为0,bar的默认构造函数是用户定义的,因此不会被初始化为零,而是直接调用默认构造函数,这会导致bb的值未初始化,因此它是随机的每次都有价值.
此陷阱迫使我们注意: 如果您不希望默认构造函数是用户定义的,则必须在类的内部声明中使用“ = default”,而不是在类的外部定义中使用.
对于类类型,用户提供了带有一些其他“副作用”的自定义默认构造函数. 例如,对于缺少用户提供的自定义默认构造函数的类,无法定义该类的const对象. 示例如下:
class exec
{
int i;
};
const exec e; //错误!缺少用户自定义默认构造函数,不允许定义const类对象
通过开头的示例,我们对某些C ++初始化方法有直观的体验. C ++中有6种初始化类型: 零初始化,默认初始化,值初始化,直接初始化,副本初始化和列表初始化.
零初始化与变量的类型和位置有关,例如它是静态的还是聚集类型. 可以初始化的类型为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 *,因此将首先调用字符串类的字符串(const char *)构造函数以创建一个临时对象,然后copy构造函数将其复制临时反对但是该标准还规定,为了提高效率,允许编译器跳过创建临时对象的步骤,而直接调用构造函数以构造要创建的对象,从而忽略了对复制构造函数的调用以进行优化,即完全等同于直接初始化,当然,您可以使用-fno-elide-constructors选项禁用优化.
如果我们将字符串类型copy构造函数定义为private或delete编译 初始化,则无法对其进行编译. 尽管可以优化它以省略复制构造函数的调用,但是复制构造函数必须在语法上可访问. ,这就是为什么C ++入门第五版第13章的复制控件的13.1.1节结尾处的第442页最后一段说:
“即使编译器跳过了复制/移动构造函数,在此程序点,复制/移动构造函数也必须存在并且可以访问(例如,它不能是私有的).
复制初始化不仅在使用=定义变量时发生,而且在以下特殊情况下也会发生:

1. 将对象作为实际参数传递给未引用的形式参数;
2. 从函数的返回类型为非引用的对象中返回对象;
3. 用花括号初始化数组中的元素或聚合类中的成员.
实际上还有另一种情况,例如当异常被值抛出或捕获时.
另一个令人困惑的部分是vector
组合:
只要使用括号(括号或大括号),但没有给出特定的初始值,它就是值初始化. 可以简单地理解为括号告诉编译器您要初始化该对象.
不使用括号,这意味着默认的初始化可以简单地理解,您不用理会它,并允许编译器使用默认行为. 通常这是不良行为,除非您真正了解自己在做什么.
4. 列表初始化是新的C ++标准提供的一种初始化方法. 它可以用于内置类型或自定义对象. 前者是数组,后者是向量. 典型:
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;
}
在上面的代码中,主程序的输出值是多少?我们不要先使用编译器来编译程序,而要根据之前介绍的知识来推断一些程序:
首先,我们需要了解,对于类而言,构造函数用于初始化类对象,无论如何都将初始化类对象. 也就是说,在实例化类对象时,无论构造函数是否实际初始化数据成员,都将调用构造函数. 因此,对于没有定义任何构造函数的自定义类,此类的默认构造函数没有“必需/不需要”,并且将不可避免地进行合成.
由于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.
以上是我们的预测. 结果会是这样吗?不幸的是,事实并非一定如此. 我们哪里错了?我们没有错. 上述程序的结果取决于您使用的操作系统,编译器版本(例如gcc-5.0和gcc-7.0)和发行版(例如gcc和clang). 有些人可能会得到与投机完全相同的结果,而另一些人则可能不会. 例如,在经常因批评不符合C ++标准而受到批评的Microsoft VC ++编译器(VS 2017,调试模式)下,结果是完全一致的(可能是由于Microsoft开始接受开源和Linux,并逐渐严格遵守) GCC的结果也完全符合要求,而广受赞誉的Clang也符合要求. 当然,在Mac和Ubuntu上,同一Clang编译器的结果甚至不一致. 在某些时候,GCC比Clang更人性化. 警告会通知您使用了未初始化的数据成员.
尽管由于操作系统和编译器的原因,上述程序中的某些地方与预期结果有所不同,但也必然有相同的地方. 例如,使用构造函数初始化列表的最后一个类的行为符合预期. . 在合成的默认构造函数之前,还有一个初始化零构造函数的地方,它不可避免地会初始化为0.
至此,我们对C ++的初始化方法和规则有了清晰的了解,即: 由于平台和编译器的差异以及对语言标准的不同程度的遵守,我们一定不能依赖合成的默认构造函数. 这就是C ++ Primer反复强调我们不应该依赖综合默认构造函数的原因. 它还显示C ++ Primer告诉我们有关手动分配动态内存的信息. 对于我们的自定义类类型,为什么它不需要值初始化?的.
C ++语言设计的基本思想是``自由''. 它对某些事物给出了特定要求,并留有余地. 未指定的区域是语言的“暗区”. 我们需要小心避免. 在对象的初始化中,建议的方法是删除默认构造函数,让我们的用户定义自己的构造函数,并对每个成员进行合理的初始化. 如果需要保留默认构造函数,则必须执行我知道的行为.
摘要
以上是本文的全部内容. 希望本文的内容对每个人的学习或工作都有一定的参考价值. 如有任何疑问,可以留言和交流. 感谢您对脚本库的支持.
本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/jisuanjixue/article-241943-1.html
否则就是蛮干