在上一课中,我们介绍了如何定义可用于存储值的变量。在本课程中,我们将探讨如何实际将值放入变量并使用这些值。
这里有一个简短的代码片段,它首先分配一个名为x的整数变量,然后再分配两个名为y和z的整数变量:
int x; // define an integer variable named x
int y, z; // define two integer variables, named y and z
变量赋值
定义变量后,您可以使用= 运算符为其赋值(在单独的语句中) 。这个过程称为赋值,= 运算符称为赋值运算符。
int width; // 定义一个名为width的整数变量
width = 5; // 将值5赋给变量width
// 变量width现在包含值5
默认情况下,赋值会将= 运算符右侧的值复制到运算符左侧的变量。这称为复制分配。
这是我们使用赋值两次的示例:
#include <iostream>
int main()
{
int width; // 声明一个整数变量width
width = 5; // 将值5赋给变量width
std::cout << width; // 输出5,显示变量width的值
width = 7; // 将存储在变量width中的值更改为7
std::cout << width; // 输出7,显示变量width的新值
return 0;
}
输出:
57
当我们将值 7 分配给变量width时,之前的值 5 会被覆盖。普通变量一次只能保存一个值。
新程序员最常犯的错误之一就是混淆赋值运算符 ( =) 和等号运算符 ( ==)。赋值( =) 用于给变量赋值。Equality ( ==) 用于测试两个操作数的值是否相等。
初始化
赋值的一个缺点是它至少需要两条语句:一条用于定义变量,一条用于赋值。
这两个步骤可以结合起来。定义变量时,还可以同时为变量提供初始值。这称为初始化。用于初始化变量的值称为初始值设定项。
C++ 中的初始化非常复杂,因此我们将在这里提供一个简化的视图。
C++ 中有 6 种初始化变量的基本方法:
int a; // 无初始化器(默认初始化)
int b = 5; // 等号后面的初始化器(复制初始化)
int c(6); // 圆括号中的初始化器(直接初始化)
// C++11中的列表初始化方法(推荐使用)
int d{7}; // 大括号中的初始化器(直接列表初始化)
int e = {8}; // 等号后面的大括号中的初始化器(复制列表初始化)
int f{}; // 初始化器为空的大括号(值初始化)
默认初始化
当没有提供初始化值时(例如上面的变量a),这称为默认初始化。在大多数情况下,默认初始化会使变量具有不确定的值。
复制初始化
当在等号之后提供初始化程序时,这称为复制初始化。这种初始化形式是从 C 继承的。
int width = 5; // copy initialization of value 5 into variable width
与复制赋值非常相似,这会将等号右侧的值复制到左侧创建的变量中。在上面的代码片段中,变量width
将使用 value 进行初始化5
。
复制初始化在现代 C++ 中已不再受欢迎,因为对于某些复杂类型来说,复制初始化的效率低于其他形式的初始化。然而,C++17 解决了大部分问题,并且复制初始化现在找到了新的拥护者。您还会发现它在较旧的代码(尤其是从 C 移植的代码)中使用,或者被那些只是认为它看起来更自然且更易于阅读的开发人员使用。
每当隐式复制或转换值时,也会使用复制初始化,例如按值将参数传递给函数、按值从函数返回或按值捕获异常时。
直接初始化
当括号内提供初始化程序时,这称为直接初始化。
int width( 5 ); // direct initialization of value 5 into variable width
最初引入直接初始化是为了更有效地初始化复杂对象(具有类类型的对象,我们将在以后的章节中介绍)。就像复制初始化一样,直接初始化在现代 C++ 中已经不再受欢迎,很大程度上是因为被列表初始化所取代。然而,我们现在知道列表初始化有一些自己的怪癖,因此直接初始化在某些情况下再次得到使用。
直接初始化不受欢迎的原因之一是它使得很难区分变量和函数。例如:
int x(); // 函数x的前向声明
int x(0); // 具有初始化器0的变量x的定义
列表初始化
在 C++ 中初始化对象的现代方法是使用一种利用大括号的初始化形式:列表初始化(也称为统一初始化或大括号初始化)。
列表初始化有三种形式:
int width { 5 }; // 直接列表初始化,将值5初始化到变量width中
int height = { 6 }; // 复制列表初始化,将值6初始化到变量height中
int depth {}; // 值初始化(参见下一节)
在引入列表初始化之前,某些类型的初始化需要使用复制初始化,而其他类型的初始化需要使用直接初始化。引入列表初始化是为了提供在大多数情况下都有效的更一致的初始化语法(这就是为什么它有时被称为“统一初始化”)。
列表初始化还有一个额外的好处:它不允许“缩小转换”。这意味着,如果您尝试使用变量无法安全保存的值来初始化变量,编译器将产生错误。例如:
int width { 4.5 }; // 错误:带有小数点的数字无法放入 int 中
复制和直接初始化只会删除小数部分,从而将值 4 初始化为可变宽度(您的编译器可能会对此发出警告,因为很少需要丢失数据)。但是,通过列表初始化,编译器将生成错误,迫使您在继续之前解决此问题。
总而言之,列表初始化通常优于其他初始化形式,因为它在大多数情况下都有效,它不允许缩小转换,并且它支持使用值列表进行初始化(我们将在以后的课程中介绍)。
优先使用大括号进行初始化
值初始化和零初始化
当使用空大括号对变量进行列表初始化时,会进行值初始化。在大多数情况下,值初始化会将变量初始化为零(或空,如果这更适合给定类型)。在发生归零的情况下,这称为归零初始化。
int width {}; // 值初始化 / 零初始化为值0
问:什么时候应该使用 { 0 } 和 {} 进行初始化?
如果您实际使用该值,请使用显式初始化值。
int x { 0 }; // 显式初始化为值0
std::cout << x; // 我们正在使用这个零值
如果值是临时的并且将被替换,请使用值初始化。
int x {}; // 值初始化
std::cin >> x; // 我们立即替换了这个值
初始化你的变量
初始化多个变量
在上一节中,我们注意到可以通过用逗号分隔名称来在单个语句中定义相同类型的多个变量:
int a, b;
我们建议是完全避免这种语法。但是,由于您可能会遇到使用这种样式的其他代码,因此多讨论一下它仍然很有用。
您可以初始化同一行上定义的多个变量:
int a = 5, b = 6; // 复制初始化
int c(7), d(8); // 直接初始化
int e {9}, f {10}; // 直接大括号初始化(推荐)
int g = {9}, h = {10}; // 复制大括号初始化
int i {}, j {}; // 值初始化
不幸的是,当程序员错误地尝试使用一个初始化语句来初始化这两个变量时,可能会出现一个常见的陷阱:
int a, b = 5; // 错误 (a未初始化!)
int a = 5, b = 5; // 正确
记住避免错误的最好方法是考虑直接初始化或大括号初始化的情况:
int a, b( 5 );
int c, d{ 5 };
因为圆括号或大括号通常放在变量名称旁边,所以这使得值 5 仅用于初始化变量b和d,而不是a或c ,这看起来更清楚一些。
未使用的初始化变量和 [[maybe_unused]]
如果变量已初始化但未使用,现代编译器通常会生成警告(因为这很少是可取的)。而如果启用“将警告视为错误”,这些警告将提升为错误并导致编译失败。
int main()
{
int x { 5 }; // 初始化变量
// 未使用该变量
return 0;
}
使用 g++ 编译器编译此文件时,会生成以下错误:
prog.cc:在函数“int main()”中:
prog.cc:3:9:错误:未使用的变量“x”[-Werror=unused-variable]
并且程序无法编译。
有一些简单的方法可以解决这个问题。
第一个选项是暂时关闭“将警告视为错误”(只是不要忘记重新打开它)。
第二种选择是简单地在某处使用该变量:
#include <iostream>
int main()
{
int x { 5 };
std::cout << x; // variable now used somewhere
return 0;
}
在C++17中,最好的解决方案是使用属性[[maybe_unused]]
。该属性告诉编译器预计该变量可能不会被使用,因此它不会生成未使用的变量警告。
即使 x 未使用,以下程序也不应生成任何警告/错误:
int main()
{
[[maybe_unused]] int x { 5 };
// 因为 x 是 [[maybe_unused]], 所以不会有警告
return 0;
}
原创文章,作者:jkhxw,如若转载,请注明出处:https://www.jkhxw.com/cpp-variable-assignment-and-initialization/