初始化

2018/9/19 17:46:55 这篇罗列概念很没水平,就别看了。

后记:这篇笔记限于罗列书中的概念,自己的理解并不多,组织章节也不好。后来又整理过一篇: 《再谈初始化》 ,两者不分先后,没有依赖关系。

参考《C++ Primer》和网上资源誊抄完正文后,自己总结如下:

  1. 定义一个变量/对象时,如果不进行显式地初始化,就是默认初始化(除了使用 extern 进行纯粹的声明的)。需要关注的是,默认初始化时编译器究竟赋予了变量/对象什么内容。

  2. 默认初始化变数太多,比如造成内置类型未定义行为,对于使用编译器合成的默认构造函数的类也可能产生不良影响,所以在定义变量时最好进行初始化!初始化的语法格式有好多种呢:

    • 根据是否使用等号分为拷贝初始化和直接初始化;
    • 根据是否使用了花括号,区分出列表初始化;
  3. 想在初始化时偷懒,不想写太多参数(vector 值初始化);但又操心默认初始不好使(new 动态分配时值初始化)。所以,又出现了值初始化。

值初始化和(拷贝初始化、直接初始化、列表初始化)是不同的概念。后者说明的是“用什么样的语法格式来初始化变量”,而值初始化不但要说明“用什么样的语法格式来初始化变量 “( C++中用 new 开辟的空间通过在要初始化的空间的类型名后跟 () 来启用值初始化,而库类型则自动对未初始化的变量启用值初始化 ),而且还要说明是”用什么值来初始化变量”。

  1. 当变量为内置类型,值初始化用 0 来对其初始化。
  2. 当变量为类类型,值初始化用该类的默认构造函数初始化。
  3. 当变量为类类型且没有默认构造函数时,值初始化要求程序员提供初始化值。

说明:值初始化通常用在初始化一段连续的内存区( 如 vector 容器,new 一段空间 )。

在个人编码过程中推荐列表初始化方式,整篇文章整理的知识点其实主要针对阅读代码。在 C++11 之后,列表初始化其实是最一般、最通用的初始化方式

1
[new] T [object] { arg1, arg2, ... };

Default Initialization 默认初始化(P39)

如果定义变量时没有指定初值,则变量被默认初始化,此时变量被赋予了“默认值”。默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响。

内置类型的变量未被显式初始化时:

  • 定义于任何函数体之外的变量被初始化为 0
  • 定义在函数体内部的内置类型变量将不被初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using std::cout;
using std::endl;
int global_default_initialization;
int main(void)
{
int list_initialization{3.14};
int inner_default_initialization;
cout << "list initialization: " << list_initialization << endl;
cout << "default initialization(inner): " << inner_default_initialization << endl;
cout << "default initialization(global): " << global_default_initialization << endl;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
niel@debian8light:~/code/test_initialize$ g++ --version
g++ (Debian 4.9.2-10) 4.9.2
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

niel@debian8light:~/code/test_initialize$ make test CXXFLAGS=--std=c++11
g++ --std=c++11 test.cpp -o test
test.cpp: In function ‘int main()’:
test.cpp:7:31: warning: narrowing conversion of ‘3.1400000000000001e+0’ from ‘double’ to ‘int’ inside { } [-Wnarrowing]
int list_initialization{3.14};
^
niel@debian8light:~/code/test_initialize$ ./test
list initialization: 3
default initialization(inner): 4196240
default initialization(global): 0
niel@debian8light:~/code/test_initialize$

类的对象如果没有显式地初始化,则其值由类确定

std::string 不是内置类型,而是类!

Initialization 初始化(P39)

C++语言定义了初始化的好几种不同方式:

1
2
3
4
int units_sold = 0;
int units_sold = {0};
int units_sold{0};
int units_sold(0);

写在前面:等号 = 可以用来初始化,也可以用来赋值。我们可能更熟悉、更习惯 = 的赋值操作。事实上在 C++ 语言中,初始化和赋值是两个完全不同的概念,所以 = 在这两种情况下的应用也是两个完全不同的操作。

显式初始化(P76):强调“显式”是为了与“默认初始化”的“未指定初值”区分。在大多情况下,谈及“初始化”都是指非“默认初始化”的情况。“初始化”一般不包括“默认初始化”!

按照初始化时是否使用等号分为拷贝初始化和直接初始化;按照是否使用了花括号,区分出列表初始化。

copy initialization 拷贝初始化

如果使用等号(=)初始化一个变量,实际上执行的是拷贝初始化(copy initialization),编译器把等号右侧的初始值拷贝到新创建的对象中去。

direct initialization 直接初始化

与拷贝初始化相反,如果不使用等号,则执行的是直接初始化(direct initialization)。

List Initialization 列表初始化(P39)

作为 C++11 新标准的一部分,用花括号来初始化变量得到了全面应用,而在此之前,这种初始化的形式仅在某些受限的场合下才能使用。

1
2
int units_sold = {0};
int units_sold{0};

注意事项(P88)

在大多数情况下这些初始化方式(显式的)可以相互等价地使用,不过也并非一直如此:

  1. 使用拷贝初始化时(即使用 = 时),只能提供一个初始值;

    当(显式地)初始化只使用一个值时,使用直接初始化或拷贝初始化都行。如果像 string s4(n, 'c'); 这样初始化时要用到的值有多个,一般来说只能使用直接初始化的方式:(P76)

    1
    2
    3
    string s5 = "hiya";  // 拷贝初始化
    string s6("hiya"); // 直接初始化
    string s7(5, 'c'); // 直接初始化,s7 的内容是 ccccc
  2. 如果提供的是一个类内初始值(in-class initializer),则只能使用拷贝初始化或使用花括号的形式初始化;

    创建对象时,类内初始值将用于初始化数据成员。没有初始值的成员将被默认初始化。对类内初始值的限制与之前介绍的类似(原文:In-class initializers are restricted as to the form we can use: They must either be enclosed inside …):或者放在花括号里,或者放在等号右边,记住不能使用圆括号。(P65)

    1
    2
    3
    4
    5
    struct S ales_data {
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
    };
  3. 如果提供的是初始元素值的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在圆括号里;

    1
    2
    vector<string> v1{"a", "an", "the"};  // 列表初始化
    vector<string> v1("a", "an", "the"); // 错误

Value initialization 值初始化

初始化 vector 对象(P88)

我们可以只提供 vector 对象容纳的元素数量而略去初始值(原文:We can usually omit the value and supply only a size. )。此时库会创建一个值初始化的元素初值(原文:a value-initialized element initializer),并把它赋给容器中的所有元素,这个初值由 vector 对象中元素的类型决定。

如果 vector 对象的元素是内置类型,比如 int,则元素初始值自动设为0。如果元素是某种类类型,比如 std::string,则元素由类默认初始化。

1
2
vector<int> ivec(10);     //10个元素,每个都初始化为0
vector<string> svec(10); //10个元素,每个都是空string对象

对这种初始化的方式有两个特殊限制:

  1. 有些类要求必须明确地提供初始值,如果 vector 对象中元素的类型不支持默认初始化,我们就必须提供初始的元素值。对这种类型的对象来说,只提供元素的数量而不设定初始值无法完成初始化工作。

  2. 如果只提供了元素的数量而没有设定初始值,只能使用直接初始化:

    1
    vector<int> vi = 10;  //错误,必须使用直接初始化的形式指定向量的大小

    这里的 10 是用来说明如何初始化 vector 对象的,我们用它的本意是想创建含有 10 个值初始化了的元素的 vector 对象,而非数字 10 “拷贝” vector 中。因此,此时不宜使用拷贝初始化。

使用 new 动态分配和初始化对象(P407)

默认初始化

默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值是未定义的,而类类型对象将执行默认构造函数进行初始化。

1
2
string *ps = new string;  //初始化为空string
int *pi = new int; //pi指向一个未初始化的int

直接初始化

我们可以使用直接初始化方式来初始化动态分配的对象。我们可以使用传统的构造方式,在新标准下,也可以使用列表初始化(使用花括号):

1
2
3
4
int *pi = new int(1024);           //pi指向的对象的值为1024
string *ps = new string(10'9'); //*派生为“9999999999”
//vector有10个元素,值依次从0-9
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};

值初始化

也可以对动态分配的对象进行值初始化,只需在类型名之后跟一对空括号即可:

1
2
3
4
string *ps1 = new string;  //默认初始化为空string
string *ps = new string(); //值初始化为空string
int *pi1 = new int; //默认初始化;*pi1的值未定义
int *pi2 = new int(); //值初始化为0;*pi2的值为0

对于定义了自己的构造函数的类类型来说,要求值初始化是没有意义的;不管采用什么方式,对象都会通过默认构造函数来初始化。但对于内置类型,两种形式的差别就大了;值初始化的内置类型对象有着良好定义的值,而默认初始化的对象的值则是未定义的。类似的,对于类中那些依赖于编译器合成的默认构造函数的内置类型成员,如果它们未在类内初始化,那么它们的值也是未定义的。