变量赋值和初始化

在之前的课程中,我们介绍了如何定义一个用于存储值的变量。在这节课中,我们将探讨如何实际将值放入变量中。 作为提醒,这里有一个简短的程序,首先分配一个名为x的整数变量,然后分配另外两个名为y和z的整数变量:

int main()
{
    int x;    // 定义一个名为x的整数变量(推荐)
    int y, z; // 定义两个整数变量,分别命名为y和z

    return 0;
}

作为提醒,推荐每行定义一个变量。我们稍后将回到定义多个变量的情况。

变量赋值

在变量被定义之后,你可以使用=运算符给它赋一个值(在单独的语句中)。这个过程被称为赋值,=运算符被称为赋值运算符。

int width; // 定义一个名为width的整数变量
width = 5; // 将值5赋给变量width

// 变量width现在有值5

默认情况下,赋值会将=运算符右侧的值复制到左侧的变量中。这被称为复制赋值。

一旦变量被赋予了一个值,该变量的值可以通过std::cout和«运算符打印出来。 赋值可以在我们想要改变变量所持有的值时使用。这里有一个例子,我们使用了两次赋值:

#include <iostream>

int main()
{
    int width; // 定义一个名为width的变量
    width = 5; // 将值5复制赋值给变量width

    std::cout << width; // 打印5

    width = 7; // 将变量width中存储的值更改为7

    std::cout << width; // 打印7

    return 0;
}

这将打印:

57

当这个程序运行时,执行从main函数的顶部开始并顺序进行。首先,为变量width分配内存。然后我们给width赋值为5。当我们输出width的值时,它将5打印到控制台。当我们然后将值7赋给width时,任何先前的值(在这种情况下是5)将被覆盖。因此,当我们再次输出width时,这次它打印7。 普通变量一次只能持有一个值。

警告

新程序员最常犯的一个错误是将赋值运算符(=)与相等运算符(==)混淆。赋值(=)用于给变量赋值。相等(==)用于测试两个操作数的值是否相等。

变量初始化

赋值的一个缺点是,给刚定义的对象赋值需要两个语句:一个用于定义变量,另一个用于赋值。 这两个步骤可以合并。当一个对象被定义时,你可以选择性地为该对象提供一个初始值。为对象指定初始值的过程被称为初始化,用于初始化对象的语法被称为初始化器。非正式地,初始值通常也被称为“初始化器”。 例如,以下语句既定义了一个名为width(类型为int)的变量,又用值5初始化了它:

#include <iostream>

int main()
{
    int width { 5 };    // 定义变量width并用初始值5初始化
    std::cout << width; // 打印5

    return 0;
}

在上面的width变量初始化中,{ 5 }是初始化器,5是初始值。

关键洞见

初始化为变量提供了一个初始值。想到“初始-化”。

不同的初始化形式

与赋值(通常很直接)不同,C++中的初始化出奇地复杂。因此,我们将在这里提供一个简化的视图以开始。

C++中有5种常见的初始化形式:

int a;         // 默认初始化(无初始化器)

// 传统初始化形式:
int b = 5;     // 复制初始化(等号后的初始值)
int c ( 6 );   // 直接初始化(括号内的初始值)

// 现代初始化形式(推荐):
int d { 7 };   // 直接列表初始化(大括号内的初始值)
int e {};      // 值初始化(空大括号)

你可能会看到上述形式的写法有不同的空格(例如 int b=5; int c(6);, int d{7};, int e{};)。是否使用额外的空格以提高可读性是个人偏好的问题。 截至C++17,复制初始化、直接初始化和直接列表初始化在大多数情况下行为相同。我们将在下面介绍它们不同的地方。

复制初始化

当在等号后提供初始值时,这被称为复制初始化。这种初始化形式是从C语言继承而来的。

int width = 5; // 将值5复制初始化到变量width

与复制赋值类似,这种初始化形式将等号右侧的值复制到左侧正在创建的变量中。在上面的代码片段中,变量width将被初始化为值5。 复制初始化在现代C++中已不再受欢迎,因为它对某些复杂类型的初始化效率低于其他形式。然而,C++17解决了这些问题的大部分,复制初始化现在正在找到新的支持者。你也会在旧代码中找到它(特别是从C移植过来的代码),或者由那些认为它看起来更自然、更容易阅读的开发者使用。

复制初始化也用于值被隐式复制的情况,例如按值传递参数给函数、按值从函数返回或按值捕获异常。

直接初始化

当在括号内提供初始值时,这被称为直接初始化。

int width ( 5 ); // 将值5直接初始化到变量width

直接初始化最初被引入是为了允许更有效地初始化复杂对象(那些具有类类型的,我们将在后续章节中介绍)。就像复制初始化一样,直接初始化在现代C++中已不再受欢迎,主要是因为被直接列表初始化取代。然而,直接列表初始化有一些自己的怪癖,因此直接初始化在某些情况下再次被使用。

直接初始化也用于值被显式转换为另一种类型时(例如通过static_cast)。

列表初始化

在C++中初始化对象的现代方式是使用一种利用大括号的初始化形式。这被称为列表初始化(或统一初始化或大括号初始化)。 列表初始化有两种形式:

int width { 5 };    // 直接列表初始化,将初始值5初始化到变量width(推荐)
int height = { 6 }; // 复制列表初始化,将初始值6初始化到变量height(很少使用)

在C++11之前,某些类型的初始化需要使用复制初始化,其他类型的初始化需要使用直接初始化。复制初始化可能难以与复制赋值区分(因为两者都使用=)。直接初始化可能难以与函数相关操作区分(因为两者都使用括号)。

列表初始化被引入是为了提供一种几乎在所有情况下都能工作的初始化语法,行为一致,并且具有明确的语法,使我们能够轻松地知道我们正在初始化一个对象。

关键洞见 当我们看到大括号时,我们知道我们正在列表初始化一个对象。

此外,列表初始化还提供了一种用一系列值而不是单个值初始化对象的方法(这就是为什么它被称为“列表初始化”)。

对于新的C++程序员来说,列表初始化的一个主要好处是“窄化转换”是不允许的。这意味着如果你尝试使用一个变量无法安全持有的值来列表初始化变量,编译器必须产生诊断(编译错误或警告)来通知你。例如:

int main()
{
    // 整数只能持有非分数值。
    // 使用分数值4.5初始化整数变量(只能持有非分数值)需要编译器将4.5转换为整数可以持有的值。
    // 这样的转换是窄化转换,因为值的小数部分将丢失。

    int w1 { 4.5 }; // 编译错误:列表初始化不允许窄化转换

    int w2 = 4.5;   // 编译:w2复制初始化为值4
    int w3 (4.5);   // 编译:w3直接初始化为值4

    return 0;
}

在上面程序的第7行中,我们使用了一个具有小数部分(.5)的值(4.5)来列表初始化一个整数变量(只能持有非分数值)。因为这是一个窄化转换,所以编译器在这种情况下必须生成诊断。 复制初始化(第9行)和直接初始化(第10行)都会默默地丢弃.5,并将变量初始化为值4(这可能不是我们想要的)。你的编译器可能会警告你(因为丢失数据很少是期望的),但也可能不会。

注意,这种对窄化转换的限制只适用于列表初始化,而不适用于对变量的任何后续赋值:

int main()
{
    int w1 { 4.5 }; // 编译错误:列表初始化不允许4.5到4的窄化转换

    w1 = 4.5;       // 可以:复制赋值允许4.5到4的窄化转换

    return 0;
}

值初始化和零初始化

当使用空的大括号初始化变量时,会发生一种特殊的列表初始化,称为值初始化。在大多数情况下,值初始化会隐式地将变量初始化为零(或者对于给定类型最接近零的值)。在发生零初始化的情况下,这被称为零初始化。

int width {}; // 值初始化/零初始化为值0

对于类类型,值初始化(和默认初始化)可能会将对象初始化为预定义的默认值,这些值可能非零。

列表初始化是现代C++中首选的初始化形式 列表初始化(包括值初始化)通常比其他初始化形式更受青睐,因为它在大多数情况下都能工作(因此最一致),它不允许窄化转换(我们通常不想要),并且它支持用一系列值初始化(我们将在后续课程中介绍)。

最佳实践

优先使用直接列表初始化或值初始化来初始化你的变量。

作者注

Bjarne Stroustrup(C++的创建者)和Herb Sutter(C++专家)也推荐使用列表初始化来初始化你的变量。 在现代C++中,有些情况下列表初始化并不像预期的那样工作。我们在课程16.2 — std::vector介绍和列表构造函数中介绍了这样一个案例。由于这些怪癖,一些经验丰富的开发者现在主张根据情况使用复制、直接和列表初始化的混合。一旦你对语言足够熟悉,理解每种初始化类型的细微差别以及这些建议背后的原因,你可以自己评估是否觉得这些论点有说服力。

Q:我应该在什么时候使用{ 0 }与{}进行初始化? 当你实际上使用初始值时,使用直接列表初始化:

int x { 0 };    // 直接列表初始化,初始值为0
std::cout << x; // 我们在这里使用那个0值

当对象的值是临时的,将被替换时,使用值初始化:

int x {};      // 值初始化
std::cin >> x; // 我们立即替换那个值,所以显式的0将毫无意义

在创建时初始化你的变量。你最终可能会发现有特定原因想要忽略这个建议(例如,使用大量变量的性能关键代码段),这是可以的,只要选择是故意的。

相关内容 对于这个话题的更多讨论,Bjarne Stroustrup(C++的创建者)和Herb Sutter(C++专家)在这里亲自提出了这个建议。

我们在课程1.6 — 未初始化变量和未定义行为中探讨了如果你尝试使用一个没有良好定义值的变量会发生什么。

最佳实践 在创建时初始化你的变量。

实例化 实例化是一个花哨的词,意味着变量已经被创建(分配)和初始化(包括默认初始化)。一个实例化的对象有时被称为实例。大多数情况下,这个术语适用于类类型的对象,但偶尔也适用于其他类型的对象。

初始化多个变量 在上一节中,我们注意到可以在同一语句中通过用逗号分隔名称来定义多个相同类型的变量:

int a, b; // 创建变量a和b,但不初始化它们

我们还注意到,最佳实践是完全避免这种语法。然而,由于你可能会碰到使用这种风格的其他代码,因此进一步讨论它仍然有用,如果不是为了避免它的原因,至少是为了加强你应该避免它的原因。 你可以在定义在同一行的多个变量上进行初始化:

int a = 5, b = 6;          // 复制初始化
int c ( 7 ), d ( 8 );      // 直接初始化
int e { 9 }, f { 10 };     // 直接列表初始化
int i {}, j {};            // 值初始化

不幸的是,这里有一个常见的陷阱,当程序员错误地尝试使用一个初始化语句初始化两个变量时会发生:

int a, b = 5;     // 错误:a没有初始化为5!
int a = 5, b = 5; // 正确:a和b都初始化为5

在上面的语句中,变量a将保持未初始化状态,编译器可能会也可能不会抱怨。如果它没有,这是让你的程序间歇性崩溃或产生零星结果的好方法。我们很快会讨论如果你使用未初始化变量会发生什么。 记住这是错误的最好方式是注意到每个变量只能由它自己的初始化器初始化:

int a = 4, b = 5; // 正确:a和b都有初始化器
int a, b = 5;     // 错误:a没有自己的初始化器

未使用初始化变量警告 现代编译器通常会在变量被初始化但未使用时生成警告(因为这很少是可取的)。如果“将警告视为错误”已启用,这些警告将被提升为错误并导致编译失败。 考虑以下看似无害的程序:

int main()
{
    int x { 5 }; // 定义变量x

    // 但在任何地方都没有使用

    return 0;
}

当使用GCC和“将警告视为错误”启用时编译此程序,将生成以下错误:

prog.cc: In function 'int main()':
prog.cc:3:9: error: unused variable 'x' [-Werror=unused-variable]
```程序无法编译。
有几个简单的方法可以解决这个问题。

如果变量确实未使用且不需要,那么最简单的选项是删除x的定义(或将其注释掉)。毕竟,如果它没有被使用,那么删除它不会影响任何事情。

另一个选项是简单地在某个地方使用变量:
```cpp
#include <iostream>

int main()
{
    int x { 5 };

    std::cout << x; // 变量现在在某个地方被使用

    return 0;
}

但这需要一些努力来编写代码来使用它,并且有一个缺点,可能会改变你的程序行为。 C++17的[[maybe_unused]]属性 在某些情况下,上述两个选项都不可取。考虑我们有一组在许多不同程序中使用的数学/物理值:

#include <iostream>

int main()
{
    // 以下是我们从其他地方复制粘贴的一些数学/物理值
    double pi { 3.14159 };
    double gravity { 9.8 };
    double phi { 1.61803 };

    std::cout << pi << '\n';  // pi被使用了
    std::cout << phi << '\n'; // phi被使用了

    // 编译器可能会抱怨gravity被定义但未使用

    return 0;
}

如果我们经常使用这些值,我们可能将它们保存在某个地方,并一起复制/粘贴/导入它们。 然而,在任何我们不使用所有这些值的程序中,编译器可能会抱怨每个实际未使用的变量。在上面的例子中,我们可以很容易地只删除gravity的定义。但如果有20或30个变量而不是3个呢?如果我们在多个地方使用它们呢?逐个检查变量列表以删除/注释未使用的变量需要时间和精力。后来,如果我们需要一个我们之前删除的值,我们将不得不花费更多的时间和精力回去重新添加/取消注释它。 为了解决这种情况,C++17引入了[[maybe_unused]]属性,允许我们告诉编译器我们不介意变量未使用。编译器不会为这些变量生成未使用变量警告。 以下程序应该不生成任何警告/错误:

#include <iostream>

int main()
{
    [[maybe_unused]] double pi { 3.14159 };  // 如果pi未使用,不要抱怨
    [[maybe_unused]] double gravity { 9.8 }; // 如果gravity未使用,不要抱怨
    [[maybe_unused]] double phi { 1.61803 }; // 如果phi未使用,不要抱怨

    std::cout << pi << '\n';
    std::cout << phi << '\n';

    // 编译器不再警告gravity未使用

    return 0;
}

此外,编译器可能会将这些变量优化出程序,因此它们没有性能影响。 [[maybe_unused]]属性应该只选择性地应用于有特定和合法未使用理由的变量(例如,因为你需要一个命名值列表,但在给定程序中实际使用的具体值可能有所不同)。否则,未使用的变量应该从程序中删除。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"