1 C++基础
1.1 程序结构
1.1.1 语句
计算机程序是一系列指令,用于指示计算机执行哪些操作。**语句(Statements)**是C++程序中最常见的指令类型,是C++语言中最小的独立计算单位。大多数语句以分号结尾,语句有很多类型:
- 声明语句 Declaration statements
- 跳转语句 Jump statements
- 表达式语句 Expression statements
- 复合语句 Compound statements
- 选择(条件)语句 Selection statements (conditionals)
- 迭代(循环)语句 Iteration statements (loops)
- 尝试块 Try blocks
1.1.2 函数和main函数
语句通常被分组为称为函数的单元。函数是按顺序执行的语句的集合。每个C++程序都必须有一个名为main的特殊函数。
程序运行时,main函数里面的语句被按顺序执行,直至main函数内的最后一条语句执行完毕。
命名法
在编程中,函数(或对象、类型、模板等)的名称被称为标识符。
标识符通用命名
标识符的命名可以使用完整的单词或公认的缩写形式,尽量采用全英文或全中文拼音,若出现中英混合需用"_"将英文与中文分割开。
较短的单词可以去掉元音形成缩写,较长的单词可取单词的头几个字母形成缩写。例如:temp->tmp、flag->flg、statistic->stat、increment->inc、message->msg。
若命名中有特殊约定,则应在源文件开始处注释说明。
不可使用全数字或奇怪的字符进行命名。除了局部变量,不建议使用单个字符进行命名(i、j、k),。
除了编译开关、头文件等特殊应用,应避免使用以下划线“_”开始或结尾的命名,如_EXAMPLE_TEST_。
三种命名规则:
- 驼峰命名法和下划线命名法
混合使用大小写字母来定义标识符。如:
printEmployeePaychecks(); #驼峰命名法
print_employee_paychecks(); #下划线命名法
第一个函数名使用了骆驼式命名法,函数名中的每一个逻辑断点都有一个大写字母来标记;第二个函数名使用了下划线法,函数名中的每一个逻辑断点都有一个下划线来标记。驼峰式命名法分为大驼峰式命名规则:FirstName, CamelCase
。小驼峰式命名规则(帕斯卡命名法):firstName, camelCase
- 匈牙利命名法
匈牙利命名法关键是:属性+类型+描述。标识符的名字以一个或者多个小写字母开头作为前缀;前缀之后的是首字母大写的一个单词或多个单词组合,该单词要指明变量的用途。
对象 | 通用命名 |
---|---|
全局变量 | g_ |
类成员变量 | m_ |
静态变量 | s_ |
常量 | c_ |
布尔类型 | b |
字符串类型(以\0结尾) | sz |
指针类型 | p |
整型 | n |
双字型 | dw |
长整型 | l(小写L) |
无符号类型 | u |
函数类型 | fn |
1.1.3 字符和文本
在计算机中,字符串(string)是一种由字符或者字节组成的有限序列,常用于表示文本文档、计算机程序、音频、图像等数据类型。
而文本(text)则是指人类语言通过字母标记或符号编码所成的书写物。计算机中的文本数据类型可以采用ASCII、Unicode等编码方式,采用特定的字符编码表将字符映射为计算机可以识别的二进制数字,使得计算机能够读取和处理文本数据。
字符串和文本在存储形式和内存占用上也有所不同。在计算机内部,字符串通常被转化为二进制编码的形式进行存储,通过指针和数组索引进行访问。因此,字符串的内存占用量通常是固定的,而且在一定范围内不断变化,不利于内存管理和优化。而文本则可以采用可变长的内存结构,只在需要的时候才进行内存空间的分配和释放,从而节省内存空间,提高程序性能。
1.1.4 剖析Hello World!
以下是一个简单的"Hello World!"程序,它的功能是在控制台窗口“打印”出一行文本“Hello World!”。
#include<iostream>
int main()
{
std::cout <<"Hello World!";
return 0;
}
第1行称为预处理器指令。#include
表示该行为预处理器指令,<iostream>
为C++标准库。该行命令表示我们将要调用’iostream’标准库,这将允许我们从控制台读取文本或向控制台写入文本。只有写入该行指令,下面第5行命令’std::cout’才能正常使用,否则会导致编译错误。
第2行为空白行,编译时被编译器忽略,此行的存在仅为增强代码的可读性。
第3行定义函数,其标识符为main,并且该函数将返回一个类型为int的值。
第4行和第7行的花括号告诉编译器括号内的语句是main函数的一部分,这段内容称为函数体。
第5行是函数的第一个语句。std::cout
代表字符输出,它和运算符<<
将允许我们在控制台上显示信息Hello World
。
第6行是返回语句。当可执行程序运行完毕时,程序会向操作系统返回一个值来指示程序是否运行成功。这个特殊的返回语句将一个整数值0
返回给操作系统,表示一切正常。返回语句时程序执行的最后一条语句。
1.2 注释
注释是直接插入程序源代码中**,**供程序员阅读的说明。注释会被编译器忽略,仅供程序员使用。
1.2.1 单行注释
C++ 单行注释以符号//
开头,指示编译器忽略从//
到行尾的所有内容。通常单行注释用于对单行代码进行快速注释,注意将注释简单地对齐,例如:
std::cout << "Hello world!\n"; // std::cout in the iostream library
std::cout << "It is very nice to meet you!\n"; // this is much easier to read
std::cout << "Yeah!\n"; // don't you think so?
但是,如果行很长,将注释放在右侧会使行变得很长。在这种情况下,单行注释通常放在被注释行的上方:
// std::cout lives in the iostream library
std::cout << "Hello world!\n";
// this is much easier to read
std::cout << "It is very nice to meet you!\n";
// don't you think so?
std::cout << "Yeah!\n";
1.2.2 多行注释
/*
和符号*/
对表示 C 风格的多行注释。符号之间的所有内容都将被忽略。
/* This is a multi-line comment.
* the matching asterisks to the left
* can make this easier to read
*/
特别注意的是多行注释不能嵌套:
/* This is a multi-line /* comment */ this is not inside the comment */
// The above comment ends at the first */, not the second */
1.2.3 正确使用注释
首先,对于给定的库、程序或函数,注释最好用于描述该库、程序或函数的功能。注释通常位于文件或库的顶部,或紧接函数之前。例如:
// This program calculates the student's final grade based on their test and homework scores.
// This function uses Newton's method to approximate the root of a given equation.
// The following lines generate a random item based on rarity, level, and a weight factor.
其次,在上面描述的库、程序或函数中,可以使用注释来描述代码如何实现其目标,这些注释让用户了解代码如何实现其目标,而无需了解每一行代码的作用。:
/* To calculate the final grade, we sum all the weighted midterm and homework scores
and then divide by the number of scores to assign a percentage, which is
used to calculate a letter grade. */
// To generate a random item, we're going to do the following:
// 1) Put all of the items of the desired rarity on a list
// 2) Calculate a probability for each item based on level and weight factor
// 3) Choose a random number
// 4) Figure out which item that random number corresponds to
// 5) Return the appropriate item
第三,在语句层面,注释应该用来描述代码执行某些操作的原因。糟糕的语句注释只能解释代码的功能。如果你编写的代码过于复杂,需要注释来解释语句的功能,那么你可能需要重写语句,而不是添加注释。
以下是一些行注释的错误/正确示例:
//这是错误的示例,原因是我们直接通过语句即可了解到sight被设置为0
// Set sight range to 0
sight = 0;
//这是正确示例,因为我们通过注释知道了为什么sight要被设置为0
// The player just drank a potion of blindness and can not see anything
sight = 0;
//这是错误的示例,原因是我们直接通过标识符得知这是在计算消费,但为什么数量要乘上2呢
// Calculate the cost of the items
cost = quantity * 2 * storePrice;
//这是正确示例,因为我们通过注释知道了这个2的意义
// We need to multiply quantity by 2 here because they are bought in pairs
cost = quantity * 2 * storePrice;
最后,注释可以用来向编译器隐藏代码。使用 //,或者使用 /* */ 样式注释将代码块暂时变成注释:
std::cout << 1;
std::cout << 2;
std::cout << 3;
// 1、使用‘//’注释
// std::cout << 1;
// std::cout << 2;
// std::cout << 3;
// 2、使用'/**/'注释
/*
std::cout << 1;
std::cout << 2;
std::cout << 3;
*/
1.3 对象和变量
1.3.1 随机存取存储器
计算机中的主存储器(memory)称为随机存取存储器(通常简称为RAM)。当我们运行程序时,操作系统会将程序加载到 RAM 中。任何硬编码到程序本身的数据(例如“Hello, world!”之类的文本)都会在此时加载。
操作系统还会保留一些额外的 RAM 供程序运行时使用。这些内存的常见用途包括存储用户输入的值、从文件或网络读取的数据,或者存储程序运行时计算的值(例如,两个值的和),以便以后再次使用。
1.3.2 对象和变量
在 C++ 中,不鼓励直接访问内存。我们通过对象间接访问内存。**对象(Object)**表示一块可以保存值的存储区域(通常是 RAM 或 CPU 寄存器)。
虽然 C++ 中的对象可以不命名,但我们更常用标识符来命名对象。有名称的对象称为变量(Variable)。
1.3.3 定义变量
为了在程序中使用变量,我们需要告诉编译器我们需要一个变量。最常见的方法是使用一种特殊的声明语句,称为定义(definition)
下面是定义名为的变量的示例x
:
int x; // define a variable named x (of type int)
在编译时(程序正在编译时),当遇到此语句时,编译器会记录下我们需要一个名称为x
的变量,并且该变量的数据类型是int
。从那时起,每当我们在代码中使用标识符x
时,编译器就会知道我们引用的是这个变量。
通常我们定义变量是在某一段函数的开头部分。
int main()
{
int x; // definition of variable x
return 0;
}
1.3.4 创建变量
在运行时(程序加载到内存并运行)时,每个对象都会被赋予一个实际的存储位置(例如 RAM 或 CPU 寄存器),用于存储值。为对象预留存储空间的过程称为分配。分配完成后,对象即被创建并可供使用。假设变量x
在内存位置140处实例化,每当程序使用变量时x
,它都会访问内存位置 140 中的值。
运行上述程序时,将从main()
的顶部开始执行, 为x
分配内存直到程序结束时释放x
。
1.3.5 定义多个变量
可以在单个语句中定义多个相同类型的变量,只需用逗号分隔名称即可。例如以下代码片段:
int a;
int b;
实际上与这个相同:
int a, b;
注意在同一个语句里必须是定义同一个类型的变量:
int a, double b; // wrong (compiler error)
int a; double b; // correct (but not recommended)
// correct and recommended (easier to read)
int a;
double b;
虽然语法允许你这样做,但请避免在单个语句中定义多个相同类型的变量。相反,应该在每个变量的单独一行中定义一个单独的语句(然后使用单行注释来说明其用途)。
1.4 变量赋值和初始化
1.4.1 变量赋值
定义变量后,可以使用运算符=
(在单独的语句中)为其赋值,此过程称为赋值。运算符=
称为赋值运算符。
int width; // define an integer variable named width
width = 5; // assignment of value 5 into variable width
// variable width now has value 5
默认情况下,赋值操作会将运算符=
右侧的值复制到运算符左侧的变量中。这称为复制赋值。
一旦为变量赋予了值,就可以通过std::cout
和运算符<<
打印该变量的值。
1.4.2 变量初始化
赋值的一个缺点是,为刚定义的对象赋值需要两个语句:一个用于定义变量,另一个用于赋值。
这两个步骤可以合并。定义对象时,你可以选择为该对象提供一个初始值。为对象指定初始值的过程称为初始化,用于初始化对象的语法称为初始化器。
例如,以下语句定义一个名为width
(类型为int
)的变量,并使用值对其进行初始化5
:
#include <iostream>
int main()
{
int width { 5 }; // define variable width and initialize with initial value 5
std::cout << width; // prints 5
return 0;
}
在上述变量的初始化中width
,{ 5 }
是初始化式,5
是初始值。
1.4.3 不同形式的初始化
C++中有5种常见的初始化形式:
int a; // default-initialization (no initializer)
// Traditional initialization forms:
int b = 5; // copy-initialization (initial value after equals sign)
int c ( 6 ); // direct-initialization (initial value in parenthesis)
// Modern initialization forms (preferred):
int d { 7 }; // direct-list-initialization (initial value in braces)
int e {}; // value-initialization (empty braces)
您可能会看到上述表格以不同的间距书写,例如int d{7};int e{};int b=5;int c(6);
。是否使用额外的空格以提高可读性取决于个人喜好。
-
默认初始化,当没有提供初始化器(例如
a
上文的变量)时,这被称为默认初始化。在很多情况下,默认初始化不执行任何初始化,而是给变量留下一个不确定的值(一个不可预测的值,有时被称为“垃圾值”)。 -
复制初始化,在等号后提供初始值,这称为复制初始化。这种初始化形式继承自 C 语言。复制初始化在现代 C++ 中已不再受欢迎,因为它对于某些复杂类型而言效率低于其他初始化形式。复制初始化也用于隐式复制值的情况,例如通过值将参数传递给函数、通过值从函数返回或通过值捕获异常。
-
直接初始化,当在括号内提供初始值时,这称为直接初始化。直接初始化最初是为了更高效地初始化复杂对象,与复制初始化一样,直接初始化在现代 C++ 中已经不再受欢迎,主要是因为它被直接列表初始化所取代。然而,直接列表初始化本身也有一些怪癖,因此在某些情况下,直接初始化再次得到了应用。
-
列表初始化,在 C++ 中,初始化对象的现代方法是使用花括号进行初始化。这称为列表初始化(或统一初始化或括号初始化)。列表初始化有两种形式:
int width { 5 }; // direct-list-initialization of initial value 5 into variable width (preferred)
int height = { 6 }; // copy-list-initialization of initial value 6 into variable height (rarely used)
引入列表初始化是为了提供一种在几乎所有情况下都有效、行为一致且具有明确的语法的初始化语法,使我们很容易知道在哪里初始化对象。当我们看到花括号时,我们就知道该语句正在列表初始化一个对象。
1.4.4 列表初始化不允许收缩转换
对于 C++ 新手来说,列表初始化的主要好处之一是它不允许“收缩转换”。这意味着,如果你尝试使用变量无法安全保存的值对变量进行列表初始化,编译器必须生成诊断信息(编译错误或警告)来通知你。例如:
int main()
{
// An integer can only hold non-fractional values.
// Initializing an int with fractional value 4.5 requires the compiler to convert 4.5 to a value an int can hold.
// Such a conversion is a narrowing conversion, since the fractional part of the value will be lost.
int w1 { 4.5 }; // compile error: list-init does not allow narrowing conversion
int w2 = 4.5; // compiles: w2 copy-initialized to value 4
int w3 (4.5); // compiles: w3 direct-initialized to value 4
return 0;
}
以上我们发现,当你对一个整型变量赋值浮点数时,列表初始化会直接返回编译错误,而其他方法则默认向下取整进行赋值。
1.4.5 值初始化和零初始化
当使用空括号初始化变量时,会发生一种特殊形式的列表初始化,称为值初始化。在大多数情况下,值初始化会隐式地将变量初始化为零(或给定类型的最接近零的值)。如果发生归零,则称为零初始化。
int width {}; // value-initialization / zero-initialization to value 0
对于类变量,值初始化(和默认初始化)可以将对象初始化为预定义的默认值,该默认值可以是非零的。
1.4.6 实例化
“实例化”是一个比较复杂的词,指的是一个变量已被创建(分配)并初始化(包括默认初始化)。实例化的对象有时也称为“实例”。该术语通常用于指类类型的对象,但有时也用于指其他类型的对象。
1.4.7 初始化多个变量
一般不建议在同一行定义及初始化多个变量,但是为了理解使用这种风格的某些代码,我们需要了解这种初始化用法。
可以在同一行上对多个变量初始化:
int a = 5, b = 6; // copy-initialization
int c ( 7 ), d ( 8 ); // direct-initialization
int e { 9 }, f { 10 }; // direct-list-initialization
int i {}, j {}; // value-initialization
1.5 iostream简介
1.5.1 输入/输出库
输入/输出库(io库)是 C++ 标准库的一部分,用于处理基本的输入和输出。我们将使用此库中的功能从键盘获取输入并将数据输出到控制台。iostream中的io部分代表输入/输出。
要使用iostream库中定义的功能,我们需要在使用iostream中定义的内容的任何代码文件的顶部包含iostream标头,如下所示:
#include <iostream>
// rest of code that uses iostream functionality here
1.5.2 std::cout
iostream库包含一些预定义变量供我们使用。其中最有用的一个是std::cout
,它允许我们将数据发送到控制台并以文本形式打印。cout
代表“字符输出”。
#include <iostream> // for std::cout
int main()
{
std::cout << "Hello world!"; // print Hello world! to console
return 0;
}
在这个程序中,我们引入了iostream,以便可以访问std::cout
。在main
函数中,我们使用std::cout
以及插入运算符 <<
将文本“Hello world!”发送到控制台进行打印。
std::cout
不仅可以打印文本,还可以打印数字和变量的值:
#include <iostream> // for std::cout
int main()
{
std::cout << 4; // print 4 to console
return 0;
}
#include <iostream> // for std::cout
int main()
{
int x{ 5 }; // define integer variable x, initialized with value 5
std::cout << x; // print value of x (5) to console
return 0;
}
要在同一行打印多个内容,可以在单个语句中多次使用插入运算符<<
来连接多个输出。例如:
#include <iostream> // for std::cout
int main()
{
std::cout << "Hello" << " world!";
return 0;
}
因此,该程序输出:
Hello world!
1.5.3 std::endl
你认为下面这段程序将打印什么内容?
#include <iostream> // for std::cout
int main()
{
std::cout << "Hi!";
std::cout << "My name is Alex.";
return 0;
}
结果:
Hi!My name is Alex.
以上,我们发现在代码段里两行单独的输出语句不会大致控制台上出现换行。如果想在输出的内容中换行,我们可以通过输出换行符来实现,其中一种方法是输出std::endl
(代表结束行)。
#include <iostream> // for std::cout and std::endl
int main()
{
std::cout << "Hi!" << std::endl; // std::endl will cause the cursor to move to the next line
std::cout << "My name is Alex." << std::endl;
return 0;
}
在上面的程序中,第二个语句std::endl
从技术上来说并非必需,因为程序执行完后会立即结束。然而,它确实有一些实用的作用。
首先,它有助于表明输出行是一个“完整的想法”(而不是在代码后面的某个地方完成的部分输出)。从这个意义上讲,它的作用类似于标准英语中的句号。
其次,它将光标定位在下一行,这样如果我们稍后添加额外的输出行(例如让程序说“再见!”),这些行将出现在我们期望的位置(而不是附加到前一行输出)。
第三,从命令行运行可执行文件后,某些操作系统不会在再次显示命令提示符之前输出新行。如果我们的程序没有在光标移动到新行时结束,命令提示符可能会出现在上一行输出的末尾,而不是像用户期望的那样出现在新行的开头。
1.5.4 输出流缓冲
我们程序中的语句请求将输出发送到控制台。但是,该输出通常不会立即发送到控制台,而是将请求的输出存储在专门用于收集此类请求的内存区域(称为缓冲区)中。缓冲区会定期刷新,这意味着缓冲区中收集的所有数据都会传输到其目的地(在本例中是控制台)。
这也意味着,如果您的程序在缓冲区刷新之前崩溃、中止或暂停(例如出于调试目的),则仍在缓冲区中等待的输出都不会显示。与缓冲输出相反的是无缓冲输出。在无缓冲输出中,每个单独的输出请求都会直接发送到输出设备。
将数据写入缓冲区通常很快,而将一批数据传输到输出设备则相对较慢。缓冲可以通过将多个输出请求批量处理,最大限度地减少将输出发送到输出设备的次数,从而显著提高性能。
1.5.5 std::endl
对比\n
使用std::endl
进行换行效率很低,因为它实际上做了两件事:输出换行符(将光标移动到控制台的下一行),然后刷新缓冲区(很慢)。如果我们输出多行以换行符std::endl
结尾的文本,就会进行多次刷新,这会极大降低程序的运行速度,而且没有必要。
为了更有效的进行换行而不刷新输出缓冲区,我们可以使用\n
(在单引号或双引号内),编译器将其解释为换行符。\n
将光标移动到控制台的下一行而不引起刷新,因此效率较std::endl
高。\n
输入起来也更简洁,可以嵌入到现有的双引号文本中。
#include <iostream> // for std::cout
int main()
{
int x{ 5 };
std::cout << "x is equal to: " << x << '\n'; // single quoted (by itself) (conventional)
std::cout << "Yep." << "\n"; // double quoted (by itself) (unconventional but okay)
std::cout << "And that's all, folks!\n"; // between double quotes in existing text (conventional)
return 0;
}
当\n
没有嵌入到现有的双引号文本行中时(例如"hello\n")
,它通常是单引号('\n'
)。在 C++ 中,我们使用单引号来表示单个字符(例如'a'
或'$'
),使用双引号来表示文本(零个或多个字符)。
1.5.6 std::cin
std::cin
是iostream
库中的另一个预定义变量。std::cout
将数据打印到控制台(使用插入运算符<<
提供数据),std::cin
(代表“字符输入”)从键盘读取输入。我们通常使用提取运算符>>
将输入数据放入变量中(然后可以在后续语句中使用)。
#include <iostream> // for std::cout and std::cin
int main()
{
std::cout << "Enter a number: "; // ask user for a number
int x{}; // define variable x to hold user input (and value-initialize it)
std::cin >> x; // get number from keyboard and store it in variable x
std::cout << "You entered " << x << '\n';
return 0;
}
尝试编译并运行此程序。运行程序时,第 5 行将打印“Enter a number:”。当代码进行到第 8 行时,程序将等待您输入。输入数字后(并按下回车键),您输入的数字将被赋值给变量x
。最后,在第 10 行,程序将打印“You entered”以及您刚刚输入的数字。
就像可以在一行中输出多个文本一样,也可以在一行中输入多个值:
#include <iostream> // for std::cout and std::cin
int main()
{
std::cout << "Enter two numbers separated by a space: ";
int x{}; // define variable x to hold user input (and value-initialize it)
int y{}; // define variable y to hold user input (and value-initialize it)
std::cin >> x >> y; // get two numbers and store in variable x and y respectively
std::cout << "You entered " << x << " and " << y << '\n';
return 0;
}
注意,此时输入数字时将以空格(空格、制表符或换行符)作为分隔。
1.5.7 输入流缓冲
类似输出,输入数据也是一个两阶段的过程:
- 您输入的各个字符将被添加到输入缓冲区的末尾。回车键(按下以提交数据)也会被存储为一个
'\n'
字符。 - 提取运算符“>>”会从输入缓冲区的开头移除字符,并将其转换为一个值,该值将通过复制赋值的方式赋给关联的变量之后,该变量即可在后续语句中使用。
示例:
#include <iostream> // for std::cout and std::cin
int main()
{
std::cout << "Enter two numbers: ";
int x{};
std::cin >> x;
int y{};
std::cin >> y;
std::cout << "You entered " << x << " and " << y << '\n';
return 0;
}
我们首先运行一次该程序,一次仅输入一个值:当遇到std::cin >> x;
时,程序将等待输入。输入值4
。输入的4\n
进入输入缓冲区,并将值4
赋值给变量x
。当遇到std::cin >> y;
时,程序将再次等待输入。输入值5
。输入的5\n
进入输入缓冲区,并将值5
赋值给变量y
。最后,程序将打印You entered 4 and 5
。
而当我们尝试第二次运行该程序时,一次输入了两个值,则程序的处理过程有所不同:当遇到std::cin >> x
时,程序将等待输入。输入4 5
。输入的4 5\n
进入输入缓冲区,但只有4
被提取到变量中x
(提取在空格处停止)。当std::cin >> y
遇到 时,程序不会等待输入。相反,仍在输入缓冲区中的5
会被提取到变量 中y
。然后程序会打印You entered 4 and 5
。请注意,在运行 2 中,程序在提取到变量时没有等待用户输入其他输入,y
因为输入缓冲区中已经有可用的先前输入。
std::cin
之所以使用缓冲,是因为它允许我们将输入与提取分离。我们可以输入一次,然后对其执行多次提取请求。
1.5.8 基本提取过程
运算符>>
的工作过程:
- 如果
std::cin
状态不佳(例如,之前的输入缓冲流提取失败并且尚未清除),则不会尝试提取,并且提取过程会立即中止。 - 前导空白字符(缓冲区最前面的空格、制表符和换行符)将从输入缓冲区中丢弃。这将丢弃前一行输入中剩余的未提取的换行符。
- 如果输入缓冲区现在为空,运算符
>>
将等待用户输入更多数据。输入数据前导的空格将被丢弃。 - 最后,运算符
>>
会提取尽可能多的连续字符,直到遇到换行符(表示输入行的结尾)或对于提取到的变量无效的字符。
而提取过程的结果如下:
- 如果提取在步骤 1 时中止,则不会发生提取尝试。不会发生其他任何事情。
- 如果在上述步骤 4 中提取了任何字符,则提取成功。提取的字符将转换为一个值,然后将其复制赋值给变量。
- 如果上述步骤 4 中未提取任何字符,则提取失败。提取对象会被复制赋值
0
(自 C++11 起),之后任何提取操作都将立即失败(直到std::cin
被清除)。
例如:
int x{};
std::cin >> x;
- 如果用户键入
5a
并输入,5a\n
将被添加到缓冲区。5
将被提取,转换为整数,并分配给变量x
。a\n
将留在输入缓冲区中以供下次提取。 - 如果用户输入“b”并回车,
b\n
将被添加到缓冲区。由于b
不是有效整数,因此无法提取任何字符,因此提取失败。变量x
将被设置为0
,并且后续提取将失败,直到输入流被清除。 - 如果
std::cin
由于先前提取失败而导致状态不佳,则不会发生任何事情。变量的值x
不会改变。
1.6 未初始化的变量和未定义的行为
1.6.1 未初始化的变量
与某些编程语言不同,C/C++ 不会自动将大多数变量初始化为给定值(例如零)。当一个未初始化的变量被赋予一个内存地址用于存储数据时,该变量的默认值将是该内存地址中已经存在的任何值(垃圾值)!尚未被赋予已知值(通过初始化或赋值)的变量称为未初始化变量。
大多数现代编译器都会尝试检测变量是否被使用而没有赋值。如果检测到,通常会发出编译时警告或错误。例如,在 Visual Studio 上编译上述程序会产生以下警告:
c:\VCprojects\test\test.cpp(11) : warning C4700: uninitialized local variable 'x' used
1.6.2 未定义的行为
未定义行为(通常缩写为UB)是指执行 C++ 语言未明确定义其行为的代码的结果。在这种情况下,C++ 语言没有任何规则来规定如果使用尚未赋值给已知值的变量的值会发生什么。
实现未定义行为的代码可能会表现出以下任何症状:
- 每次运行时都会产生不同的结果。
- 始终产生相同的错误结果。
- 行为不一致(有时产生正确的结果,有时则不然)。
- 似乎正在运行,但在程序后期产生了不正确的结果。
- 立即或稍后崩溃。
- 在某些编译器上运行,但在其他编译器上运行不运行。
- 可以正常运行,直到您更改一些其他看似不相关的代码为止。
1.6.3 implementation-defined behavior 和 unspecified behavior
特定的编译器及其附带的相关标准库被称为实现(implementation),由实现(implementation)定义的行为称为实现定义的行为(implementation-defined behavior)。
A specific compiler and the associated standard library it comes with are called an implementation (as these are what actually implements the C++ language). In some cases, the C++ language standard allows the implementation to determine how some aspect of the language will behave, so that the compiler can choose a behavior that is efficient for a given platform. Behavior that is defined by the implementation is called implementation-defined behavior. Implementation-defined behavior must be documented and consistent for a given implementation.
未指定的行为几乎与实现定义的行为相同,因为行为由实现来定义,但实现不需要记录该行为。
我们通常希望避免实现定义和未指定的行为,因为这意味着如果在不同的编译器上编译,我们的程序可能无法按预期工作。
1.7 关键字和标识符
C++规定了92个具有特殊含义的关键字:
C++ 还定义了一些特殊标识符:override、final、import和module。这些标识符在特定语境下具有特定含义,但在其他情况下并非保留标识符。
变量(或函数、类型或其他类型的项)的名称称为标识符。命名标识符时必须遵循一些规则:
- 标识符不能是关键字。关键字是保留的。
- 标识符只能由字母(小写或大写)、数字和下划线组成。这意味着名称不能包含符号(下划线除外)或空格(空格或制表符)。
- 标识符必须以字母(小写或大写)或下划线开头。不能以数字开头。
- C++ 区分大小写,因此区分小写字母和大写字母。
nvalue
不同于nValue
不同于NVALUE
。
另外,C++还约定了:
-
函数名和变量名通常以小写字母开头,以大写字母开头的标识符通常用于用户定义的类型(例如结构、类和枚举)。
-
避免使用以下划线开头的标识符。虽然语法上合法,但这些名称通常保留给操作系统、库和/或编译器使用。
-
标识符的名称应该清楚地表明其所持有的值的含义(尤其是在单位不明确的情况下)。标识符的命名方式应该能够帮助那些完全不了解代码功能的人尽快理解。
-
避免使用缩写,除非它们是常见且明确的。
-
对于变量声明,使用注释来描述变量的用途或解释其他可能不太明显的内容会很有用。与其给变量命名(这
numCharsIncludingWhitespaceAndPunctuation
相当冗长),不如在声明行上方或上方放置一个合适的注释,这样可以帮助用户理解:
// a count of the number of chars in a piece of text, including whitespace and punctuation
int numChars {};
代码的阅读次数比编写次数多,因此编写代码时节省的任何时间,都可能被所有读者(包括未来的自己)浪费在阅读代码上。如果你想更快地编写代码,可以使用编辑器的自动完成功能。
1.8 空格和基本格式
1.8.1 空格
空格是指用于格式化的字符。在 C++ 中,空格主要指空格、制表符和换行符。C++ 中的空格通常用于三种用途:分隔某些语言元素、文本内部以及格式化代码。
语法要求某些元素用空格分隔。这主要发生在两个关键字或标识符必须连续放置,以便编译器区分它们。
例如:
int x; // int and x must be whitespace separated
int main(); // int and main must be whitespace separated
预处理器指令(例如#include <iostream>
)必须放在单独的行上:
#include <iostream>
#include <string>
引用的文本按字面意义占用空格量:
std::cout << "Hello world!";
std::cout << "Hello world!";
引用的文本中不允许使用换行符:
std::cout << "Hello
world!"; // Not allowed!
仅由空格(空格、制表符或换行符)分隔的引用文本将被连接:
std::cout << "Hello "
"world!"; // prints "Hello world!"
使用空格来格式化代码,使代码更易于阅读:
#include <iostream>
int main(){std::cout<<"Hello world";return 0;}
#include <iostream>
int main()
{
std::cout << "Hello world";
return 0;
}
1.8.2 基本格式
与其他一些语言不同,C++ 不会对程序员强制任何格式限制。因此,我们称 C++ 为一种与空格无关的语言。
- 使用制表符或空格进行缩进都可以(大多数 IDE 都提供设置,可以将按制表符键转换为相应数量的空格)。建议将制表符设置为 4 个空格的缩进。有些 IDE 默认缩进 3 个空格,这也是可以接受的。
- 许多开发人员喜欢将左花括号放在与语句相同的行上:
int main() {
// statements here
}
这样做的理由是,它减少了垂直空格的数量(因为你不需要用整行来写一个左花括号),这样你就可以在屏幕上容纳更多代码。这增强了代码的理解力,因为你不需要滚动太多页面就能理解代码的作用。
但是建议使用常见的替代方法,即左括号出现在其自己的行上:
int main()
{
// statements here
}
这增强了代码的可读性,并且由于括号对应该始终保持相同的缩进级别,因此不容易出错。如果由于括号不匹配而导致编译器错误,很容易就能找到错误的位置。
- 花括号内的每个语句都应以与其所属函数的左括号相邻的一个制表符开头。例如:
int main()
{
std::cout << "Hello world!\n"; // tabbed in one tab (4 spaces)
std::cout << "Nice to meet you.\n"; // tabbed in one tab (4 spaces)
}
- 每行不应过长。通常,80 个字符已成为每行最大长度的事实上的标准。如果一行要更长,则应(在合理的位置)将其拆分为多行。可以通过在每行后添加一个制表符来缩进,或者如果行数相似,则将其与上一行对齐(以更易于阅读的方式为准)。
int main()
{
std::cout << "This is a really, really, really, really, really, really, really, "
"really long line\n"; // one extra indentation for continuation line
std::cout << "This is another really, really, really, really, really, really, really, "
"really long line\n"; // text aligned with the previous line for continuation line
std::cout << "This one is short\n";
}
- 如果用运算符(例如 << 或 +)拆分长行,则运算符应放在下一行的开头,而不是当前行的结尾:
std::cout << 3 + 4
+ 5 + 6
* 7 * 8;
- 使用空格对齐值或注释或在代码块之间添加间距,使代码更易于阅读。示例如下:
cost = 57;
pricePerItem = 24;
value = 5;
numberOfItems = 17;
std::cout << "Hello world!\n"; // cout lives in the iostream library
std::cout << "It is very nice to meet you!\n"; // these comments are easier to read
std::cout << "Yeah!\n"; // especially when all lined up
// cout lives in the iostream library
std::cout << "Hello world!\n";
// these comments are easier to read
std::cout << "It is very nice to meet you!\n";
// when separated by whitespace
std::cout << "Yeah!\n";
1.8.3 自动格式化
大多数现代 IDE 都会在您输入代码时帮助您格式化代码(例如,当您创建函数时,IDE 会自动缩进函数体内的语句)。然而,当您添加或删除代码、更改 IDE 的默认格式,或粘贴格式不同的代码块时,格式可能会变得混乱。修复文件的部分或全部格式可能会很麻烦。幸运的是,现代 IDE 通常包含自动格式化功能,可以重新格式化选定内容(用鼠标高亮显示)或整个文件。
对于 Visual Studio 用户,在 Visual Studio 中,可以在编辑 > 高级 > 格式化文档和编辑 > 高级 > 格式化选择下找到自动格式化选项。
1.9 字面量和运算符
1.9.1 字面量
以下面的两个语句为例:
std::cout << "Hello world!";
int x { 5 };
“Hello world!”和“5”都是直接插入到源代码中的固定值,称之为字面量(literals)。
字面量和变量都具有值,但与可以初始化和赋值的变量不同,字面量的值是固定的,因此字面量也称为字面常量。
1.9.2 运算符
在数学中,运算是指涉及零个或多个输入值(称为操作数)并产生新值(称为输出值)的过程。要执行的具体操作用称为运算符的符号表示。
运算符输入的操作数个数称为运算符的元数(arity)。C++ 中的运算符有四种不同的元数:
-
一元运算符作用于一个操作数。例如,
-
就是一元运算符。给定-5
,-
接受一个字面量操作数5
,并对其符号进行翻转,以产生新的输出值-5
。 -
二元运算符作用于两个操作数(通常称为左操作数和右操作数)。二元运算符的一个例子是
+
运算符。例如,给定的3 + 4
,+
取左操作数3
和右操作数4
,并应用数学加法来产生新的输出值7
。插入(<<
)和提取(>>
)运算符是二元运算符,在左侧取std::cout
或std::cin
,在右侧取要输出的值或要输入的变量。 -
三元运算符作用于三个操作数。C++ 中只有一个三元运算符(条件运算符)。
-
零值运算符作用于零操作数。C++ 中也只有一个零值运算符(throw运算符)。
运算符可以链接在一起,以便一个运算符的输出可以用作另一个运算符的输入。例如,给定以下内容:2 * 3 + 4
,乘法运算符首先执行,并将左操作数2
和右操作数转换3
为返回值(该返回值将成为加法运算符的左操作数)。接下来,加法运算符执行,并将左操作数6
和右操作数4
转换为新值10
。
1.9.3 返回值和副作用
C++ 中的大多数运算符仅使用其操作数来计算返回值。例如,-5
产生返回值-5
和2 + 3
产生返回值5
。还有一些运算符不产生返回值(例如delete
和throw
)。
一些运算符具有附加行为。除了产生返回值之外还具有其他可观察效果的运算符(或函数)被认为具有副作用。
在 C++ 中,“副作用”一词具有不同的含义:它是运算符或函数除了产生返回值之外的可观察效果。
由于赋值操作具有改变对象值的可观察效果,因此这被视为副作用。我们使用某些运算符(例如赋值运算符)主要是为了它们的副作用(而不是它们产生的返回值)。在这种情况下,副作用既有益又可预测(而返回值通常是偶然的)。
1.10 表达式
在一般编程中,表达式是由字面量、变量、运算符和函数调用组成的非空序列,用于计算某个值。执行表达式的过程称为求值,而产生的结果称为表达式的结果(有时也称为返回值)。
在 C++ 中,表达式的结果是下列之一:
- 一个值
- 对象或函数
- 无。以关键字
Void
定义的函数都没有返回值,调用这些函数只是为了产生副作用。
表达式不以分号结尾,无法单独编译。例如,如果尝试编译表达式x = 5
,编译器会报错(可能是因为缺少分号)。相反,表达式始终作为语句的一部分进行求值。例如:
return { x + 2, y * 3 + 1};
表达式语句是由一个表达式后跟一个分号组成的语句。当表达式语句执行时,表达式会被求值。
当表达式语句中使用表达式时,该表达式生成的任何结果都会被丢弃(因为没有用到)。例如,当表达式x = 5
求值时,的返回值=
会被丢弃。这没问题,因为我们本来就想赋值5
给x
。
我们也可以编写一些虽然能编译但没有任何效果的表达式语句。例如,表达式语句 2 * 3;
就是一个表达式语句,其表达式求值为6,然后将其丢弃。虽然语法上有效,但这样的表达式语句是无用的。某些编译器(例如 gcc 和 Clang)如果检测到表达式语句无用,就会发出警告。