Bootstrap

【C++从入门到精通】第1篇:C++基础知识(上)


1.1 C++语句和程序结构

1.1.1 本篇介绍

在本章中,我们将首先了解每个C++程序都必不可少的一些内容。本章的目的是帮助你理解基本的C++程序的构造。

1.1.2 语句

计算机程序是告诉计算机该做什么的一系列指令。语句是一种指令,它使程序执行某种操作。

语句是C++程序中最常见的指令类型。这是因为它们是C++语言中最小的独立计算单元。在这方面,它们的行为很像自然语言中的句子。当我们想要向另一个人传达一个想法时,我们通常用句子写或说,而不是词或字。在C++中,当我们想让程序做某事时,通常会编写语句。

C++中的大多数语句都以分号结尾。如果你看到一行以分号结尾,它可能是一条语句。

在像C++这样的高级语言中,一条语句可以编译成许多机器语言指令。

拓展知识

C++中有许多不同类型的语句:

  1. Declaration statements 声明语句
  2. Jump statements 跳转语句
  3. Expression statements 表达式语句
  4. Compound statements 复合语句
  5. Selection statements (Conditionals statements) 选择语句(条件语句)
  6. Iteration statements (Loops statements) 迭代语句(循环语句)
  7. Try blocks 试块

1.1.3 函数和主函数

在C++中,语句通常被分成称为函数的单元。函数是顺序执行的语句的集合(从上到下)。当你学习编写自己的程序时,你将能够以任何你喜欢的方式创建自己的函数和混合和匹配语句。

规则
每个C++程序都必须有一个名为main的特殊函数。当程序运行时,main中的语句按顺序执行。

程序通常在main函数内的最后一条语句执行后终止(结束运行)。

函数通常是为完成特定的工作而编写的。例如,一个名为“max”的函数可能包含计算两个数字中哪个更大的语句。一个名为“calculateGrade”的函数可以从一组考试成绩中计算学生的成绩。我们很快就会学习更多关于函数的内容了,因为它们是程序中最常用的组织工具。

注意
在讨论函数时,在函数名后面附加一对括号是相当常见的速记方式。例如,如果您看到术语main()doSomething(),这是分别命名为main或doSomething的函数的简写。这有助于将函数与其他有名称的东西(如变量)区分开来,而不必每次都写单词“function”。

1.1.4 解析Hello world

现在您已经对语句和函数有了简要的了解,让我们回到“Hello world”程序,更详细地深入了解每一行的作用。

#include <iostream>

int main()
{
	std::cout << "Hello world!";
	return 0;
}

第1行是一种特殊类型的行,称为预处理器指令。这个预处理器指令表明我们希望使用iostream库的内容,iostream库是C++标准库的一部分,允许我们从控制台读写文本。为了在第5行使用std::cout,我们需要这一行。删除这一行将导致第5行出现编译错误,因为编译器不知道std::cout是什么。

第2行是空的,所以编译器会忽略它。这一行的存在只是为了使程序更容易被人读懂(通过将#include预处理器指令和程序的后续部分分开)。

第3行告诉编译器,我们将定义一个名为main的函数。如上所述,每个C++程序都必须有一个main函数,否则它将无法链接。

第4行和第7行告诉编译器哪些行属于main函数。第4行开始大括号和第7行结束大括号之间的所有内容都被认为属于main函数。这被称为函数体

第5行是main函数中的第一条语句,也是我们运行程序时要执行的第一条语句。std::cout(代表“字符输出”)和<<操作符允许我们在控制台上显示信息。该语句实现了程序的可视输出。

第6行是一个返回语句。当一个可执行程序完成运行时,该程序将一个值发送回操作系统,以指示它是否成功运行。这个特殊的返回语句将值0返回给操作系统,这意味着“一切正常!”。这是执行的程序中的最后一条语句。

我们编写的所有程序都将遵循这个通用模板,或者它的变体。

注意
如果上述解释的部分(或全部)令人困惑,这在这里是意料之中的。这只是提供一个快速的概述。随后的课程将深入研究上述所有内容,并提供大量额外的解释和示例。

你可以自己编译并运行这个程序,你会看到它向控制台输出如下内容:

Hello world!

1.1.5 语法和语法错误

在英语中,句子是根据你可能在学校英语课上学到的特定语法规则构建的。例如,正常的句子以句号结尾。支配语言中句子构造的规则被称为语法。如果你忘记了句号,把两个句子连在一起,这是违反英语语法的。

C++也有语法:关于程序如何构造才能被认为是有效的。当你编译程序时,编译器负责确保你的程序遵循C++语言的基本语法。如果您违反了规则,编译器将在您尝试编译程序时发出抱怨,并向您发出语法错误

让我们看看如果省略“Hello world”程序第5行的分号会发生什么,如下所示:

#include <iostream>

int main()
{
	std::cout << "Hello world!"
	return 0;
}

您可以编译这个格式错误的程序。

VS Code报错提示为(您的编译器可能产生不同的提示):

expected ';' before 'return' gcc [行5,列29]

这告诉您在第5行有一个语法错误:编译器期望在return语句之前有一个分号,但是没有找到。虽然编译器会告诉您遇到语法错误时它正在编译的代码行,但遗漏实际上可能在前一行(大部分编译器在此问题上会显示问题在第6行)。

在编写程序时,语法错误是很常见的。幸运的是,它们通常很容易找到和修复,因为编译器通常会直接指向它们。只有解决了所有语法错误,程序的编译才会完成。

您可以尝试从“Hello world”程序中删除字符甚至整行,以查看生成的不同类型的错误。尝试恢复第5行末尾缺失的分号,然后删除第1、3或4行,看看会发生什么。

1.1.6 练习时间

下面的测验是为了加强你对上述材料的理解。

问题1
什么是声明?

问题2
什么是函数?

问题3
所有程序必须具有的函数的名称是什么?

问题4
当程序运行时,从哪里开始执行?

问题5
C++中的语句通常以什么符号结尾?

问题6
什么是语法错误?

问题7
什么是C++标准库?
提示:复习上一篇第五节——编译器、链接器和库的介绍


1.2 注释

注释是程序员可读的文本,它直接插入到程序的源代码中。注释被编译器忽略,只供程序员使用。

在C++中有两种不同风格的注释,它们都服务于相同的目的:帮助程序员以某种方式记录代码。

1.2.1 单行注释

//符号表示开始一个C++单行注释,告诉编译器忽略从//符号到行尾的所有内容。例如:

std::cout << "Hello world!"; // 从这里到行尾的所有内容都被忽略

通常,单行注释用于对单行代码进行快速注释。

std::cout << "Hello world!\n"; // std::cout存在于iostream库中
std::cout << "It is very nice to meet you!\n"; // 这些注释使代码难以阅读
std::cout << "Yeah!\n"; // 特别是当行长度不同时

在一行的右侧加上注释会使代码和注释都难以阅读,特别是在行很长的情况下。如果行相当短,注释可以简单地对齐(通常是TAB键),如下所示:

std::cout << "Hello world!\n";					// std::cout存在于iostream库中
std::cout << "It is very nice to meet you!\n";	// 这更好阅读
std::cout << "Yeah!\n";							// 你不这样认为吗?

但是,如果行很长,在右边放置注释会使行很长。在这种情况下,单行注释通常放在它正在注释的行之上:

// std::cout存在于iostream库中
std::cout << "Hello world!\n";

// 这更好阅读
std::cout << "It is very nice to meet you!\n";

// 你不这样认为吗?
std::cout << "Yeah!\n";

注意
上面的语句代表了我们第一次遇到的代码片段之一。因为代码片段不是完整的程序,所以它们不能自己编译。相反,它们的存在是为了以简洁的方式演示特定的概念。

如果您想编译一个代码片段,您需要将其转换为完整的程序,以便进行编译。通常,该程序看起来像这样:

#include <iostream>

int main()
{
    // Replace this line with the snippet of code you'd like to compile

    return 0;
}

1.2.2 多行注释

/**/符号表示C++的多行注释。符号之间的所有内容都被忽略。

/* 这是一个多行注释。
   这一行将会被省略。
   这一行也是。 */

由于符号之间的所有内容都会被忽略,所以你有时会看到程序员“美化”他们的多行注释:

/* 这是一个多行注释。
 * 左边匹配的星号,
 * 可以使它更容易阅读。
 */

不能嵌套多行样式的注释。否则,将产生意想不到的结果:

/* 这是一个多行 /* 注释 */ 这不再注释里面 */
// 以上注释结束在第一处 */, 而不是第二处 */

当编译器试图编译它时,它将忽略从第一个/*到第一个*/的所有内容。由于这不在注释中,*/不被认为是注释的一部分,编译器将尝试编译它。这将不可避免地导致编译错误。

这是使用语法高亮笔非常有用的地方,因为注释的不同颜色应该清楚地表明哪些是注释的一部分,哪些不是。

警告
不要在其他多行注释中使用多行注释。在多行注释中包装单行注释是可以的。

1.2.3 正确使用注释

通常,注释应该用于三件事。首先,对于给定的库、程序或函数,注释最好用于描述库、程序或函数所做的工作。它们通常放在文件或库的顶部,或者紧挨着函数前面。例如:

// 这个程序根据学生的考试和家庭作业成绩来计算他们的最终成绩。
// 该函数使用牛顿法来逼近给定方程的根。
// 下面几行根据稀有度、等级和权重系数生成一个随机物品。

所有这些注释使读者不必查看实际代码就能很好地了解库、程序或函数试图完成的任务。用户可以一眼看出代码是否与他要完成的任务相关。当作为团队的一部分工作时,这一点尤其重要,因为不是每个人都熟悉所有的代码。

其次,在上面描述的库、程序或函数中,注释可以用来描述代码将如何完成其目标。

/* 为了计算最终成绩,我们将所有期中和作业的加权分数加起来
    然后除以分数的数量来分配一个百分比,
    这个百分比用于计算字母分数。*/
// 要生成随机物品,我们将执行以下操作:
// 1) 将所有想要的稀有物品放在一个列表中
// 2) 根据级别和权重因子计算每个项目的概率
// 3) 选择一个随机数
// 4) 找出这个随机数对应的物品
// 5) 返回适当的物品

这些注释让用户了解代码将如何完成其目标,而不必理解每一行代码的作用。

第三,在语句级别,注释应该用来描述代码为什么要做某事。一个糟糕的注释解释了代码在做什么。如果您曾经编写过非常复杂的代码,以至于需要注释来解释语句的作用,那么您可能需要重写语句,而不是注释。

下面是一些不好和好的注释的例子。

不好的注释:

// 将sight设为0
sight = 0;

原因:通过查看语句,我们已经可以看到sight被设置为0。

好的注释:

// 玩家刚刚喝了致盲药水,什么也看不见
sight = 0;

原因:现在我们知道为什么玩家的视线被设置为0。

不好的注释:

// 计算物品的成本
cost = quantity * 2 * storePrice;

原因:我们可以看到这是一个成本计算,但是为什么数量要乘以2呢?

好的注释:

// 这里我们需要将数量乘以2,因为它们是成对购买的
cost = quantity * 2 * storePrice;

原因:现在我们知道为什么这个公式是有道理的。

程序员经常不得不在用一种方法解决问题还是用另一种方法解决问题之间做出艰难的决定。评论是提醒自己(或告诉别人)你做这个决定而不是另一个决定的好方法。

好的注释:

// 我们决定使用链表而不是数组,
// 因为数组插入速度太慢。
// 我们要用牛顿法来求一个数的根,
// 因为没有确定性的方法来解这些方程。

最后,注释应该以一种对不知道代码做什么的人有意义的方式编写。通常情况下,程序员会说“这是很明显的!我不可能忘记这件事。”你猜怎么着?这并不明显,你会惊讶于你忘记的速度有多快:你(或者别人)以后会感谢你用人类语言写下代码的内容、方式和原因。阅读单行代码很容易。理解他们想要实现的目标并不重要。

最佳实践
自由地注释你的代码,并像对一个不知道代码是做什么的人说话一样写你的注释。不要以为你会记得为什么你做了特定的选择。

注意
在本专栏的其他文章中,我们将在代码块中使用注释来引起您对特定内容的注意,或者帮助说明事情是如何工作的(同时确保程序仍然可以编译)。机智的读者会注意到,按照上述标准,大多数评论都是可怕的,当你阅读教程的其余部分时,请记住,这些评论是有意为之的教育目的,而不是试图展示什么是好的评论。

1.2.4 注释掉代码

将一行或多行代码转换为注释称为注释掉代码。这提供了一种方便的方法暂时将部分代码排除在已编译的程序之外。

要注释掉一行代码,只需使用//样式注释将一行代码临时转换为注释:

未注释:

std::cout << 1;

注释:

// 	std::cout << 1;

要注释掉一段代码,可以在多行代码上使用//,或者使用/* */样式注释将代码块临时转换为注释。

未注释:

std::cout << 1;
std::cout << 2;
std::cout << 3;

注释:

// 	std::cout << 1;
// 	std::cout << 2;
// 	std::cout << 3;

/*
	std::cout << 1;
	std::cout << 2;
	std::cout << 3;
*/

你可能有很多原因想要这样做:

  1. 您正在编写一段还不能编译的新代码,您需要运行该程序。如果存在编译器错误,编译器将不允许您编译代码。注释掉不能编译的代码将允许程序编译,这样您就可以运行它了。当您准备好了,您可以取消注释代码,并继续处理它。
  2. 您已经编写了可以编译但不能正常工作的新代码,并且直到以后才有时间修复它。注释掉损坏的代码将确保损坏的代码不会执行,也不会导致问题,直到您可以修复它。
  3. 找出错误的根源。如果程序没有产生期望的结果,注释掉部分代码以查看是否可以隔离导致程序无法正常工作的原因,这有时会很有用。如果您注释掉了一行或多行代码,并且您的程序开始按预期运行,那么您最后注释掉的内容很可能是问题的一部分。然后,您可以调查为什么这些代码行会导致问题。
  4. 您希望用另一段代码替换一段代码。不只是删除原始代码,您可以将其注释掉,留下作为参考,直到您确定新代码可以正常工作。一旦确定新代码可以正常工作,就可以删除旧的注释掉的代码。如果你不能让你的新代码工作,你总是可以删除新代码,取消旧代码的注释,以恢复到你以前的代码。

注释掉代码是开发过程中常见的事情,因此许多IDE都提供了注释掉突出显示的代码部分的支持。访问此功能的方式因IDE而异。

对于Visual Studio用户
编辑 -> 高级 -> 注释选择(或取消评论)

对于Code::Blocks用户
编辑 -> 注释(或取消注释、切换注释等)

对于VS Code用户
Control+K+C注释掉选中的内容,Control+K+U取消选中的内容的注释。


1.3 对象和变量

1.3.1 数据和值

在本篇文章的1.1节中,你了解了程序中的大部分指令都是语句,并且函数是按顺序执行的语句组。函数内的语句执行程序被设计时希望生成的任何结果。

但是这些项目究竟是如何产生结果的呢?它们通过对数据的操作(读入、写入、更改等)来实现这一点。在计算机中,数据是任何可以被计算机移动、处理或存储的信息。

关键点
程序是操作数据以产生预期结果的指令集合。

程序可以通过多种方式获取数据:从文件或数据库,通过网络,从用户在键盘上提供输入,或从程序员直接将数据放入程序本身的源代码中。在“Hello world”程序中,从前面的课中,文本“Hello world!”直接插入到程序的源代码中,为程序提供使用的数据。然后,程序通过将这些数据发送到要显示的监视器来操作这些数据。

计算机上的数据通常以一种便于存储或处理的格式存储(因此我们无法读懂)。因此,当编译“Hello World”程序时,文本“Hello World !”被转换成程序使用的更有效的格式(即二进制)。

是可以表示为数据的字母、数字、文本,或者一些其他有用的概念(如分数)。

1.3.2 对象和变量

所有的计算机都有内存,称为RAM(随机存储器的缩写),供您的程序使用。您可以将RAM看作一系列编号的邮箱,在程序运行时,每个邮箱可用于保存一段数据。

在一些较旧的编程语言(如Applesoft BASIC)中,您可以直接访问这些邮箱(语句就像是“获取存储在邮箱号7532中的值”一样)。

在C++中,不鼓励直接访问内存。相反,我们通过对象间接访问内存。对象是一个存储区域(通常是内存),可以存储一个值,并具有其他相关属性。编译器和操作系统如何为对象分配内存?这超出了本课的范围。但是关键在于,我们不是直接去获取存储在邮箱号7532中的值,而是去获取这个对象存储的值。这意味着我们可以专注于使用对象来存储和检索值,而不必担心它们在内存中的实际位置。

对象可以是命名的,也可以是未命名的(匿名)。命名的对象称为变量,对象的名称称为标识符。在我们的程序中,我们创建和使用的大多数对象都是变量。

主页
在一般编程中,术语对象通常指内存中的未命名对象、变量或函数。在C++中,术语对象的定义较窄,不包括函数。

关键点
对象和命名对象(变量)用于存储值。

1.3.3 变量实例化

为了创建变量,我们使用一种称为定义的特殊声明语句(稍后我们将讲解声明和定义之间的区别)。

下面是一个定义变量x的例子:

int x; // 定义一个名为x的整形变量

在编译时,当编译器看到这个语句时,它会知道我们正在定义一个变量,并将其命名为x,并且它的类型是int(稍后会详细介绍类型)。之后,每当编译器看到标识符x时,它就会知道我们引用了这个变量。

当程序正在运行时(称为运行时),变量将被实例化。实例化是一个奇特的词,它意味着对象将被创建并分配一个内存地址。变量在被用来存储值之前必须被实例化。为了方便示例,假设变量x在内存位置140实例化。每当程序使用变量x时,它将访问内存位置140中的值。实例化的对象有时称为实例

1.3.4 数据类型

到目前为止,我们已经介绍了变量是可以存储数据值的命名存储区域。数据类型(简称为类型)告诉编译器变量将存储什么类型的值(例如,数字、字母、文本等)。

在上面的例子中,我们的变量x被赋予int类型,这意味着变量x将表示一个整数值。整形是可以不带小数部分的数字,如4、27、0、-2或-12。所以,我们可以说x是一个整形变量。

在C++中,变量的类型必须在编译时(当程序被编译时)就确定,除非重新编译程序,否则不能更改该类型。这意味着整型变量只能保存整数值。如果希望存储其他类型的值,则需要使用不同的类型。

整型只是C++支持的开箱即用的众多类型之一。为了便于说明,这里有另一个使用数据类型double定义变量的例子:

double width; // 定义一个名为width的变量,类型为double

C++还允许您创建自己的用户定义类型,这是使C++强大的一个功能之一,我们将在以后讨论。

1.3.5 定义多个变量

通过用逗号分隔变量名,可以在单个语句中定义相同类型的多个变量。下面的两段代码实际上是相同的:

int a;
int b;

与:

int a, b;

当以这种方式定义多个变量时,新程序员往往会犯两个常见的错误(不严重,因为编译器会捕获这些错误并要求您修复它们):

第一个错误是在按顺序定义变量时给每个变量一个类型。

int a, int b; // 错误

int a, b; // 正确

第二个错误是试图在同一语句中定义不同类型的变量,这是不允许的。不同类型的变量必须在单独的语句中定义。

int a, double b; // 错误

int a; double b; // 正确(但不推荐)

// 正确并推荐(易于阅读)
int a;
double b;

最佳实践
定义多个变量(然后使用单行注释说明它的用途)。

1.3.6 练习时间

问题8
什么是数据?

问题9
什么是值?

问题10
什么是变量?

问题11
什么是标识符?

问题12
什么是类型?

问题13
什么是整形?


1.4 变量赋值和初始化

在上一节中,我们介绍了如何定义可以用来存储值的变量。在本节中,我们将探索如何实际地将值放入变量并使用这些值。

复习一下,这里有一个简短的代码片段,它首先分配一个名为x的整数变量,然后分配两个名为y和z的整数变量:

int x;    // 定义一个名为x的整形变量
int y, z; // 定义两个整形变量,名为y,x

1.4.1 变量赋值

定义变量后,可以使用=操作符为其赋值。这个过程称为赋值,而=操作符称为赋值操作符

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

// 变量width现在的值为5

默认情况下,赋值操作将=操作符右侧的值复制到操作符左侧的变量。这叫做拷贝赋值

下面是我们两次使用赋值的例子:

#include <iostream>

int main()
{
	int width;
	width = 5; // 将值5拷贝到变量width中

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

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

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

	return 0;
}

当我们将值7赋值给变量width时,先前存在的值5将被覆盖。普通变量一次只能保存一个值。

警告
新程序员最常犯的错误之一是混淆了赋值操作符(=)和相等操作符(==)。赋值(=)用于为变量赋值。相等(==)用于测试两个操作数的值是否相等。

1.4.2 初始化

赋值的一个缺点是它至少需要两条语句:一条用于定义变量,另一条用于赋值。

这两个步骤可以结合起来。在定义变量时,还可以同时为该变量提供初始值。这称为初始化。用于初始化变量的值称为初始值

C++中的初始化异常复杂,因此我们在这里给出一个简化的视图。

C++中有6种初始化变量的基本方法:

int a;         // 无初始化(默认初始化)
int b = 5;     // 初始值为等号后的值(拷贝初始化)
int c( 6 );    // 初识值为括号内的值(直接初始化)

// 数组初始化方法(C++11)
int d { 7 };   // 初始值设定为大括号中的值(数组直接初始化)
int e = { 8 }; // 初始值设为等号后面大括号中的值(数组拷贝初始化)
int f {};      // 初始值为空括号(值初始化)

你可能会看到上面的表单用不同的间距写成(例如int d{7};)。是否使用额外的空格来提高可读性是个人偏好的问题。

1.4.3 默认初始化

当没有提供初始化值时(如上面的变量a),称为默认初始化。在大多数情况下,默认初始化会留下一个值不确定的变量。

我们将在下一篇文章中讨论这种情况。

1.4.4 复制初始化

当等号后面提供初始化式时,称为复制初始化。这种形式的初始化继承自C语言。

与复制赋值非常相似,这将等号右侧的值复制到左侧创建的变量中。在上面的代码片段中,变量width将被初始化为值5。

复制初始化在现代C++中已经不再受欢迎,因为对于某些复杂类型,它比其他形式的初始化效率低。然而,C++ 17修复了这些问题中的大部分,并且拷贝初始化现在找到了一些新的倡导者。您还会发现它用于较旧的代码(特别是从C移植的代码),或者被仅仅认为它看起来更好的开发人员使用。

拓展
在隐式复制或转换值时也使用复制初始化,例如按值向函数传递参数、按值从函数返回或按值捕获异常。

1.4.5 直接初始化

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

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

最初引入直接初始化是为了允许更有效地初始化复杂对象。然而,在现代C++中,直接初始化通常不受欢迎。

拓展
当值显式转换为另一类型时,也可以使用直接初始化。
直接初始化不受欢迎的原因之一是它很难区分变量和函数。例如:

int x();  // 函数x的直接声明
int x(0); // 初始值为0的变量x的定义

1.4.6 列表初始化

在C++中初始化对象的现代方法是使用一种使用大括号的初始化形式:数组初始化(也称为统一初始化大括号初始化)。

列表初始化有三种形式:

int width { 5 };    // 将变量width数组直接初始化为值5
int height = { 6 }; // 将变量height数组拷贝初始化为值6
int depth {};       // 值初始化(见下一节)

注:
在引入数组初始化之前,有些类型的初始化需要使用拷贝初始化,而其他类型的初始化需要使用直接初始化。引入数组初始化是为了提供更一致的初始化语法(这就是为什么它有时被称为“统一初始化”),在大多数情况下都能工作。
此外,数组初始化提供了一种用值数组初始化对象的方法。

列表初始化还有一个额外的好处:它不允许“缩小转换范围”。这意味着,如果您尝试使用变量无法安全保存的值来大括号初始化变量,编译器将产生错误。例如:

int width { 4.5 }; // 错误:具有小数的数字无法分配给整形变量

最佳实践
使用列表初始化来初始化变量。

1.4.7 值初始化和零初始化

当使用空大括号初始化变量列表时,将进行值初始化。在大多数情况下,值初始化会将变量初始化为零(或空,如果更适合给定类型)。在发生归零的情况下,这称为零初始化

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

问:何时应使用 { 0 }{} 进行初始化?
答:如果实际使用该值,请使用显式初始化值。

int x { 0 };    // 显式初始化为值0
std::cout << x;

如果值是临时值并且将被替换,请使用值初始化。

int x {};      // 值初始化
std::cin >> x;

1.4.8 初始化变量

在创建时初始化变量。您最终可能会发现出于特定原因(例如,使用大量变量的代码的性能关键部分)而忽略此建议的情况,这没关系。

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

1.4.9 初始化多个变量

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

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; // 正确

在上面的语句中,变量a将保持未初始化状态,编译器可能会也可能不会报错。

记住这是错误的最好方法是考虑直接初始化或大括号初始化的情况:

int a, b( 5 );
int c, d{ 5 };

1.4.10 未使用的初始化变量和 [[maybe_unused]]

如果变量已初始化但未使用,现代编译器通常会生成警告(因为这很少是可取的)。如果启用了“将警告视为错误”,则这些警告将提升为错误并导致编译失败。

考虑以下看起来无辜的程序:

int main()
{
    int x { 5 }; // 声明变量

    // 但没有使用

    return 0;
}

使用g++编译时,会生成以下错误:

prog.cc: In function 'int main()':
prog.cc:3:9: error: unused variable 'x' [-Werror=unused-variable]

并且程序无法编译。

有几种简单的方法可以解决此问题。

第一个是暂时关闭“将警告视为错误”(只是不要忘记将其重新打开)。

第二种选择是简单地在某处使用该变量:

#include <iostream>

int main()
{
    int x { 5 };

    std::cout << x; // 使用了变量

    return 0;
}

在C++17中,最好的解决方案是使用 [[maybe_unused]] 属性。此属性告诉编译器预期变量可能未被使用,因此它不会生成未使用的变量警告。

以下程序应该不会生成警告或错误,即使x未使用也是如此:

int main()
{
    [[maybe_unused]] int x { 5 };

    // since x is [[maybe_unused]], no warning generated

    return 0;
}

1.4.11 练习时间

问题14
初始化和赋值有什么区别?

问题15
当您想要使用特定值初始化变量时,您应该首选哪种形式的初始化?

问题16
什么是默认初始化和值初始化?它们执行的是什么?你应该更喜欢哪个?


1.5 本篇答案

问题1
语句是计算机程序中的一条指令,它告诉计算机执行一个动作。

问题2
函数是顺序执行的语句的集合。

问题3
main。

问题4
从main函数中的第一条语句开始。

问题5
分号(;)。

问题6
语法错误是指当程序违反C++语言的语法规则时,在编译时出现的编译器错误。

问题7
库文件是预编译代码的集合,这些代码已经被“打包”以便在其他程序中重用。C++标准库是C++附带的库。它包含在您的程序中使用的附加功能。

问题8
数据是可以被计算机移动、处理或存储的任何信息。

问题9
值是字母、数字、文本或者可以表示为数据的一些其他有用概念的实例。

问题10
变量是一个命名的内存区域,可用于存储值。

问题11
标识符是访问变量的名字。

问题12
类型告诉程序如何解释内存中的值。

问题13
整数是一个不含小数部分的数字。

问题14
初始化在创建变量时为其提供初始值。赋值在创建变量后的某个时间点为变量提供一个值。

问题15
直接列表初始化(又名直接大括号初始化)。

问题16
默认初始化是指变量初始化没有初始值设定项(例如int x;)。在大多数情况下,变量会留下不确定的值。

值初始化是指变量初始化具有空大括号(例如int x{};)。在大多数情况下,这将执行零初始化。

与默认初始化相比,您应该更喜欢值初始化。

;