在本章中,您将深入学习各种语言特性来控制代码中的流程。
在格式化和编写代码方面,C++ 是一种非常灵活的语言。它也是一种强类型语言,这意味着有关于声明变量类型的规则,您可以通过让编译器帮助您编写更好的代码来利用这些规则。在本节中,我们将介绍如何格式化 C++ 代码以及声明和限定变量范围的规则。
除了字符串文字之外,您可以自由使用空格(空格、制表符、换行符),并且可以随意使用。C++ 语句由分号分隔,因此在下面的代码中有三个语句,它们将编译并运行:
int i = 4;
i = i / 2;
std::cout << "The result is" << i << std::endl;
整个代码可以编写如下:
int i=4;i=i/2; std::cout<<"The result is "<<i<<std::endl;
有些情况下需要空格(例如,在声明变量时,类型和变量名之间必须有空格),但是惯例是尽可能明智地使代码可读。虽然从语言的角度来看,将所有语句放在一行是完全正确的(就像 JavaScript 一样),但它会使代码几乎完全不可读。
If you are interested in some of the more creative ways of making code unreadable, have a look at the entries for the annual International Obfuscated C Code Contest (http://www.ioccc.org/). As the progenitor of C++, many of the lessons in C shown at IOCCC apply to C++ code too.
请记住,如果您编写的代码是可行的,它可能会使用几十年,这意味着您可能必须在编写代码几年后再回来,这意味着其他人也会支持您的代码。让您的代码可读不仅是对其他开发人员的一种礼貌,而且不可读的代码总是可能被替换的目标。
不可避免地,无论你为谁编写代码,都将决定你如何格式化代码。有时这是有意义的,例如,如果您使用某种形式的预处理来提取代码和定义,从而为代码创建文档。在许多情况下,强加给你的风格是别人的个人偏好。
Visual C++ allows you to place XML comments in your code. To do this you use a three--slash comment (///
) and then compile the source file with the /doc
switch. This creates an intermediate XML file called an xdc
file with a <doc>
root element and containing all the three--slash comments. The Visual C++ documentation defines standard XML tags (for example, <param>
, <returns>
to document the parameters and return value of a function). The intermediate file is compiled to the final document XML file with the xdcmake
utility.
C++ 中有两大风格: K & R 和奥尔曼。
克尼根和里奇(K&R)写了第一本,也是最有影响力的关于 C 语言的书(丹尼斯·里奇是 C 语言的作者)。K&R 风格被用来描述那本书中使用的格式风格。通常,K&R 将代码块的左大括号放在最后一条语句的同一行。如果您的代码有嵌套语句(通常会),那么这种风格可能会有点混乱:
if (/* some test */) {
// the test is true
if (/* some other test */) {
// second test is true
} else {
// second test is false
}
} else {
// the test is false
}
这种风格通常用在 Unix(以及类似 Unix 的)代码中。
Allman 样式(以开发人员 Eric Allman 的名字命名)将左大括号放在一个新行上,因此嵌套示例如下所示:
if (/* some test */)
{
// the test is true
if (/* some other test */)
{
// second test is true
}
else
{
// second test is false
}
}
else
{
// the test is false
}
奥尔曼风格是微软的典型风格。
请记住,您的代码不太可能出现在纸面上,因此 K&R 更紧凑的事实不会拯救任何树木。如果有选择,就应该选择可读性最强的款式;这位作者的决定,对于这本书来说,是奥尔曼更具可读性。
如果有多个嵌套块,缩进可以让您知道代码驻留在哪个块中。然而,评论可以有所帮助。特别是,如果一个代码块有大量的代码,注释代码块的原因通常是有帮助的。例如,在if
语句中,将测试结果放在代码块中是有帮助的,这样您就知道该块中的变量值是什么。在测试的右括号上加上注释也很有用:
if (x < 0)
{
// x < 0
/* lots of code */
} // if (x < 0)
else
{
// x >= 0
/* lots of code */
} // if (x < 0)
如果您将测试作为注释放在右大括号上,这意味着您有一个搜索词可以用来查找导致代码块的测试。前面几行使这种注释变得多余,但是当您的代码块有几十行代码,并且有许多嵌套级别时,这样的注释会非常有帮助。
语句可以是变量的声明、计算值的表达式,也可以是类型的定义。语句也可能是一种控制结构,影响代码的执行流程。
语句以分号结束。除此之外,很少有关于如何格式化语句的规则。您甚至可以单独使用分号,这称为空语句。null 语句没有任何作用,所以分号太多通常是良性的。
表达式是一系列运算符和操作数(变量或文字),它们会产生一些值。请考虑以下几点:
int i;
i = 6 * 7;
右侧6 * 7
为表达式,赋值(从左侧i
到右侧分号)为语句。
每个表达式要么是左值,要么是右值**。您最有可能在错误描述中看到这些关键词。实际上,左值是指某个内存位置的表达式。赋值左侧的项必须是左值。然而,左值可以出现在赋值的左侧或右侧。所有变量都是左值。右值是一个临时项,它的存在时间不会长于使用它的表达式;它将有一个值,但不能有赋值,所以它只能存在于赋值的右侧。文字是右值。下面显示了左值和右值的一个简单示例:**
int i;
i = 6 * 7;
在第二行中,i
是左值,表达式6 * 7
产生右值(42
)。由于左侧有一个右值,以下内容将无法编译:
6 * 7 = i;
广义地说,当你添加一个分号时,一个表达式就变成了一个语句。例如,以下是两种说法:
42;
std::sqrt(2);
第一行是42
的右值,但是因为是临时的,所以没有效果。C++ 编译器会马上优化它。第二行调用标准库函数计算2
的平方根。同样,结果是一个右值,并且该值没有被使用,所以编译器将对此进行优化。但是,它说明了可以在不使用返回值的情况下调用函数。虽然std::sqrt
的情况并非如此,但许多函数除了它们的返回值之外,还有持久的影响。事实上,一个函数的全部意义通常是做一些事情,返回值通常只是用来指示函数是否成功;通常开发人员会假设一个函数会成功,并忽略返回值。
操作符将在本章后面介绍;然而,在这里引入逗号操作符是有用的。作为一条语句,可以有一系列用逗号分隔的表达式。例如,以下代码在 C++ 中是合法的:
int a = 9;
int b = 4;
int c;
c = a + 8, b + 1;
作者打算输入c = a + 8 / b + 1;
和:
,他们用逗号代替了/
。目的是将c
分配到 9 + 2 + 1 或 12。这段代码将编译并运行,变量c
将被赋值为 17 ( a + 8
)。原因是逗号将赋值的右侧分隔为a + 8
和b + 1
两个表达式,它使用第一个表达式的值来赋值c
。在本章的后面,我们将研究运算符优先级。不过这里值得一说的是,逗号的优先级最低,+
的优先级比=
高,所以语句执行的顺序是加法:赋值,然后是逗号运算符(结果b + 1
扔掉)。
您可以使用括号将表达式分组来更改优先级。例如,输入错误的代码可能如下:
c = (a + 8, b + 1);
这个语句的结果是:变量c
赋给 5(或b + 1
)。原因是使用逗号运算符,表达式是从左到右执行的,因此表达式组的值是最紧密的。有些情况下,例如在for
循环的初始化或循环表达式中,您会发现逗号运算符很有用,但正如您在这里看到的,即使是有意使用的,逗号运算符也会产生难以阅读的代码。
在这里提供基本信息是有用的。C++ 是一种强类型语言,这意味着你必须声明你使用的变量的类型。这样做的原因是编译器需要知道要为变量分配多少内存,它可以通过变量的类型来确定这一点。此外,如果变量没有被显式初始化,编译器需要知道如何初始化变量,并且为了执行这个初始化,编译器需要知道变量的类型。
C++ 11 provides the auto
keyword, which relaxes this concept of strong typing. However, the type checking of the compiler is so important that you should use type checking as much as possible.
C++ 变量可以在代码中的任何地方声明,只要它们是在使用前声明的。在哪里声明一个变量决定了如何使用它(这被称为变量的作用域)。一般来说,最好在尽可能接近您将使用它的地方,并且在最严格的范围内声明变量。这可以防止名称冲突,在这种情况下,您必须添加额外的信息来消除两个或更多变量的歧义。**
你可以并且应该给你的变量起描述性的名字。这使得您的代码可读性更强,更容易理解。C++ 名称必须以字母字符或下划线开头。除空格外,它们可以包含字母数字字符,但也可以包含下划线。因此,以下是有效的名称:
numberOfCustomers
NumberOfCustomers
number_of_customers
C++ 名称区分大小写,前2,048
个字符有意义。变量名可以以下划线开头,但不能使用两个下划线,也不能使用下划线后跟大写字母(这些是 C++ 保留的)。C++ 也保留了关键字(例如while
和if
,很明显你不能使用类型名作为变量名,既不能使用内置的类型名(int
、long
等),也不能使用自己的自定义类型。
在语句中声明一个变量,以分号结束。声明变量的基本语法是,先指定类型,然后指定名称,以及可选的变量初始化。
在使用内置类型之前,必须对其进行初始化:
int i;
i++ ; // C4700 uninitialized local variable 'i' used
std::cout << i;
初始化变量基本上有三种方法。可以赋值,可以调用类型构造函数(类的构造函数将在第 4 章、类中定义)或者可以使用函数语法初始化变量:
int i = 1;
int j = int(2);
int k(3);
这三个都是合法的 C++,但是风格上第一个更好,因为更明显:变量是整数,叫做i
,赋值 1。第三个看起来很混乱;它看起来像是一个函数的声明,实际上它是在声明一个变量。
第四章、类将涵盖类,你自己的自定义类型。自定义类型可能被定义为具有默认值,这意味着您可能决定在使用自定义类型的变量之前不初始化它。然而,这将导致较差的性能,因为编译器将用默认值初始化变量,随后您的代码将赋值,导致赋值被执行两次。
每种类型都有一个文字表示。整数是没有小数点的数字,如果是有符号的整数,文字也可以用加号或减号来表示符号。同样,实数可以有包含小数点的文字值,甚至可以使用包含指数的科学(或工程)格式。在代码中指定文字时,C++ 有各种规则可以使用。这里显示了一些文字的示例:
int pos = +1;
int neg = -1;
double micro = 1e-6;
double unit = 1.;
std::string name = "Richard";
注意对于unit
变量,编译器知道文字是实数,因为数值有小数点。对于整数,您可以在代码中提供十六进制文字,方法是在数字前面加上0x
,因此0x100
是十进制的256
。默认情况下,输出流将以 10 为基数打印数值;但是,您可以在输出流中插入一个操纵器,告诉它使用不同的基数。默认行为为std::dec
,表示数字应以 10 为基数显示,std::oct
表示以八进制(8 为基数)显示,std::hex
表示以十六进制(7 为基数)显示。如果您希望看到前缀被打印,那么您可以使用流操纵器std::showbase
(更多细节将在第 5 章、使用标准库容器中给出)。
C++ 定义了一些文字。对于逻辑类型bool
,有true
和false
常量,其中false
为零,true
为 1。还有nullptr
常量,同样是零,它被用作任何指针类型的无效值。
在某些情况下,您可能希望提供可以在整个代码中使用的常量值。例如,你可以决定为π
声明一个常数。您不应该允许更改该值,因为它会更改代码中的底层逻辑。这意味着您应该将变量标记为常量。当您执行此操作时,编译器将检查变量的使用情况,如果它在更改变量值的代码中使用,编译器将发出错误:
const double pi = 3.1415;
double radius = 5.0;
double circumference = 2 * pi * radius;
在这种情况下,符号pi
被声明为常数,因此它不能改变。如果您随后决定更改常数,编译器将发出一个错误:
// add more precision, generates error C3892
pi += 0.00009265359;
一旦你声明了一个常量,你可以确信编译器会确保它保持不变。您可以使用如下表达式分配常数:
#include <cmath>
const double sqrtOf2 = std::sqrt(2);
在这段代码中,一个名为sqrtOf2
的全局常数被声明,并使用std::sqrt
函数赋值。因为这个常数是在函数外声明的,所以它对文件是全局的,可以在整个文件中使用。
这种方法的问题是预处理器做了一个简单的替换。对于用const
声明的常量,C++ 编译器将执行类型检查,以确保常量被正确使用。
您也可以使用const
来声明一个常量,该常量将被用作常量表达式。例如,您可以使用方括号语法声明数组(更多详细信息将在第 2 章、使用内存、数组和指针中给出):
int values[5];
这在堆栈上声明了一个由五个整数组成的数组,这些项通过values
数组变量来访问。这里的5
是一个常量表达式。当您在堆栈上声明一个数组时,您必须向编译器提供一个常量表达式,这样它就知道要分配多少内存,这意味着在编译时必须知道数组的大小。(您可以分配一个只有在运行时才知道大小的数组,但这需要动态内存分配,在第 2 章、中解释了如何使用内存、数组和指针。)在 C++ 中,可以声明一个常量来做以下事情:
const int size = 5;
int values[size];
在代码的其他地方,当您访问values
数组时,您可以使用size
常量来确保您不会访问超过数组末尾的项目。由于size
变量只在一个地方声明,如果您需要在稍后阶段更改数组的大小,您只有一个地方可以进行更改。const
关键字也可用于指针和引用(参见第 2 章、处理内存、数组和指针)以及对象(参见第 4 章、类);通常,你会看到它用在函数的参数上(见第三章、使用函数)。这用于让编译器帮助确保指针、引用和对象按照您的意图得到适当的使用。
C++ 11 引入了一个名为constexpr
的关键词。这适用于表达式,并指示应在编译类型而不是运行时计算表达式:
constexpr double pi = 3.1415;
constexpr double twopi = 2 * pi;
这类似于初始化用const
关键字声明的常数。但是,constexpr
关键字也可以应用于返回可以在编译时计算的值的函数,因此这允许编译器优化代码:
constexpr int triang(int i)
{
return (i == 0) ? 0 : triang(i - 1) + i;
}
在本例中,函数triang
递归计算三角数。代码使用条件运算符。在括号中,测试函数参数看它是否为零,如果是,函数返回零,实际上结束了递归并将函数返回给原始调用方。如果参数不为零,则返回值为参数之和,用参数调用的triang
的返回值递减。
当在代码中使用文字调用该函数时,可以在编译时进行计算。constexpr
是对编译器的指示,检查函数的使用情况,看它能否在编译时确定参数。如果是这种情况,编译器可以评估返回值,并比在运行时调用函数更有效地生成代码。如果编译器在编译时无法确定参数,该函数将被调用为正常。标有constexpr
关键字的函数必须只有一个表达式(因此在triang
函数中使用了条件运算符?:
)。
提供常量的最后一种方法是使用enum
变量。实际上,enum
是一组命名常数,这意味着您可以使用enum
作为函数的参数。例如:
enum suits {clubs, diamonds, hearts, spades};
这定义了一个名为suits
的枚举,为一副牌中的花色命名值。枚举是一个整数类型,默认情况下编译器会采用int
,但是您可以通过在声明中指定整数类型来更改它。由于卡牌套装只有四种可能的值,所以使用int
(通常是4
字节)是浪费内存,我们可以使用char
(单个字节):
enum suits : char {clubs, diamonds, hearts, spades};
当使用枚举值时,可以只使用名称;但是,通常用枚举的名称来限定它的范围,这使得代码更易读:
suits card1 = diamonds;
suits card2 = suits::diamonds;
这两种形式都是允许的,但后者更明确地表明该值是从枚举中获取的。要强制开发者指定范围,可以应用关键字class
:
enum class suits : char {clubs, diamonds, hearts, spades};
有了这个定义和前面的代码,声明card2
的行将编译,但是声明card1
的行将不编译。使用限定作用域的enum
,编译器将枚举视为新类型,并且没有从新类型到整数变量的内置转换。例如:
suits card = suits::diamonds;
char c = card + 10; // errors C2784 and C2676
enum
类型是基于char
的,但是当您将suits
变量定义为作用域时(使用class
,第二行将不会编译。如果枚举被定义为不在范围内(没有class
),那么在枚举值和char
之间有一个内置的转换。
默认情况下,编译器将为第一个枚举数赋值 0,然后为后续枚举数递增该值。因此suits::diamonds
的值为 1,因为它是suits
中的第二个值。您可以自己赋值:
enum ports {ftp=21, ssh, telnet, smtp=25, http=80};
在这种情况下,ports::ftp
的值为 21,ports::ssh
的值为 22 (21 递增),ports::telnet
为 22,ports::smtp
为 25,ports::http
为 80。
Often the point of enumerations is to provide named symbols within your code and their values are unimportant. Does it matter what value is assigned to suits::hearts
? The intention is usually to ensure that it is different from the other values. In other cases, the values are important because they are a way to provide values to other functions.
枚举在switch
语句中很有用(见后面),因为命名值比只使用整数更清楚。您也可以将枚举用作函数的参数,从而限制通过该参数传递的值:
void stack(suits card)
{
// we know that card is only one of four values
}
由于我们讨论的是变量的使用,所以解释用于定义指针和数组的语法是值得的,因为存在一些潜在的陷阱。第 2 章、使用内存、数组和指针对此进行了更详细的介绍,因此我们将只介绍语法,以便您熟悉它。
在 C++ 中,您将使用类型化指针来访问内存。类型指示内存中指向的数据的类型。因此,如果指针是一个(4
字节)整数指针,它将指向四个可以用作整数的字节。如果整数指针递增,那么它将指向接下来的四个字节,可以用作整数。
Don't worry if you find pointers confusing at this point. Chapter 2, Working with Memory, Arrays, and Pointers, will explain this in more detail. The purpose of introducing pointers at this time is to make you aware of the syntax.
在 C++ 中,指针是用*
符号声明的,您可以用&
运算符访问内存地址:
int *p;
int i = 42;
p = &i;
第一行声明了一个变量p
,它将被用来保存一个整数的内存地址。第二行声明一个整数并给它赋值。第三行给指针p
赋值,作为刚刚声明的整数变量的地址。需要强调的是,p
的值不是42
;这将是一个存储42
值的存储地址。
注意声明如何在变量名上有*
。这是常见的惯例。原因是如果在一条语句中声明几个变量,*
只适用于立即变量。例如:
int* p1, p2;
最初,这看起来像是在声明两个整数指针。然而,这条线并没有做到这一点;它只声明了一个指向整数p1
的指针。第二个变量是一个名为p2
的整数。前一行相当于下面一行:
int *p1;
int p2;
如果您希望在一个语句中声明两个整数,那么您应该这样做:
int *p1, *p2;
名称空间为您提供了一种模块化代码的机制。命名空间允许您用一个唯一的名称来标记您的类型、函数和变量,这样,使用范围解析操作符,您可以给出一个完全限定名。这样做的好处是,你确切地知道哪个项目会被调用。缺点是使用完全限定名实际上是关闭了 C++ 的参数依赖查找机制,该机制用于重载函数,编译器将根据传递给函数的参数选择最适合的函数。
定义命名空间很简单:用namespace
关键字和给它起的名字来修饰类型、函数和全局变量。在以下示例中,utilities
命名空间中定义了两个函数:
namespace utilities
{
bool poll_data()
{
// code that returns a bool
}
int get_data()
{
// code that returns an integer
}
}
Do not use semicolon after the closing bracket.
现在,当您使用这些符号时,您需要用命名空间限定名称:
if (utilities::poll_data())
{
int i = utilities::get_data();
// use i here...
}
命名空间声明可能只声明函数,在这种情况下,实际的函数必须在其他地方定义,并且您需要使用限定名:
namespace utilities
{
// declare the functions
bool poll_data();
int get_data();
}
//define the functions
bool utilities::poll_data()
{
// code that returns a bool
}
int utilities::get_data()
{
// code that returns an integer
}
名称空间的一个用途是对代码进行版本化。您的代码的第一个版本可能有一个不在您的功能规范中的副作用,在技术上是一个错误,但是一些调用方会使用它并依赖它。当您更新代码以修复错误时,您可以决定允许调用方选择使用旧版本,这样他们的代码就不会中断。您可以通过命名空间来实现这一点:
namespace utilities
{
bool poll_data();
int get_data();
namespace V2
{
bool poll_data();
int get_data();
int new_feature();
}
}
现在,想要特定版本的调用者可以调用完全限定名,例如,调用者可以使用utilities::V2::poll_data
来使用较新的版本,使用utilities::poll_data
来使用较旧的版本。当特定命名空间中的项调用同一命名空间中的项时,它不必使用限定名。所以,如果new_feature
函数调用get_data
,被调用的将是utilities::V2::get_data
。需要注意的是,要声明嵌套的名称空间,必须手动进行嵌套(如下所示);您不能简单地声明一个名为utilities::V2
的名称空间。
前面的例子是这样写的,代码的第一个版本将使用名称空间utilities
调用它。C++ 11 提供了一种称为内联命名空间的工具,允许您定义嵌套的命名空间,但允许编译器在执行依赖于参数的查找时将这些项视为在父命名空间中:
namespace utilities
{
inline namespace V1
{
bool poll_data();
int get_data();
}
namespace V2
{
bool poll_data();
int get_data();
int new_feature();
}
}
现在要调用第一版get_data
,可以用utilities::get_data
或者utilities::V1::get_data
。
完全限定的名称会使代码难以阅读,尤其是如果您的代码只使用一个命名空间。为了帮助你,你有几个选择。您可以放置一个using
语句来指示在指定命名空间中声明的符号可以在没有完全限定名的情况下使用:
using namespace utilities;
int i = get_data();
int j = V2::get_data();
您仍然可以使用完全限定的名称,但是这个语句允许您放宽要求。请注意,嵌套的名称空间是名称空间的成员,因此前面的using
语句意味着您可以使用utilities::V2::get_data
或V2::get_data
调用第二个版本的get_data
。如果你用了不合格的名字,那就意味着你会叫utilities::get_data
。
一个命名空间可以包含许多项,您可能会决定只使用其中的几项来放宽对完全限定名的使用。为此,使用using
并给出项目名称:
using std::cout;
using std::endl;
cout << "Hello, World!" << endl;
该代码表示,每当使用cout
时,都是指std::cout
。您可以在函数中使用using
,也可以将其作为文件范围,并使意图对文件是全局的。
您不必在一个地方声明一个名称空间,您可以在几个文件中声明它。以下内容可能位于与先前utilities
声明不同的文件中:
namespace utilities
{
namespace V2
{
void print_data();
}
}
print_data
函数仍然是utilities::V2
命名空间的一部分。
您也可以在一个名称空间中放置一个#include
,在这种情况下,头文件中声明的项目现在将成为名称空间的一部分。前缀为c
(例如cmath
、cstdlib
和ctime
)的标准库头文件通过在std
命名空间中包含适当的 C 头文件来访问 C 运行时函数。
命名空间的最大优点是能够用可能很常见的名称来定义您的项,但是对于不知道命名空间名称的其他代码来说,这些名称是隐藏的。命名空间意味着这些项仍然可以通过完全限定名在代码中使用。但是,只有当您使用唯一的名称空间名称时,这才有效,并且名称空间名称越长,它就越可能是唯一的。Java 开发人员经常使用 URI 命名他们的类,您可以决定做同样的事情:
namespace com_packtpub_richard_grimes
{
int get_data();
}
问题是完全限定名变得相当长:
int i = com_packtpub_richard_grimes::get_data();
您可以使用别名来解决这个问题:
namespace packtRG = com_packtpub_richard_grimes;
int i = packtRG::get_data();
C++ 允许你定义一个没有名字的名字空间,一个匿名的名字空间。如前所述,名称空间允许您防止在几个文件中定义的代码之间的名称冲突。如果您打算只在一个文件中使用这样的名称,您可以定义一个唯一的名称空间名称。但是,如果您必须为几个文件执行此操作,这可能会很乏味。一个没有名字的命名空间有一个特殊的含义,它有内部链接,即项目只能在当前翻译单元、当前文件中使用,不能在其他任何文件中使用。
未在命名空间中声明的代码将成为global
命名空间的成员。您可以在没有名称空间名称的情况下调用代码,但是您可能希望使用没有名称空间名称的范围解析运算符来明确指示该项在global
名称空间中:
int version = 42;
void print_version()
{
std::cout << "Version = " << ::version << std::endl;
}
编译器会将你的源文件编译成单独的项目,称为翻译单元。编译器将确定您声明的对象和变量以及您定义的类型和函数,一旦声明,您就可以在声明范围内的后续代码中使用这些对象和变量。从最广泛的角度来看,您可以通过在项目中所有源文件都将使用的头文件中声明一个项来声明全局范围内的项。如果不使用命名空间,那么使用这样的全局变量将它们命名为全局命名空间的一部分通常是明智的:
// in version.h
extern int version;
// in version.cpp
#include "version.h"
version = 17;
// print.cpp
#include "version.h"
void print_version()
{
std::cout << "Version = " << ::version << std::endl;
}
这段代码有两个源文件(version.cpp
和print.cpp
)的 C++ 和两个源文件都包含的头文件(version.h
)。头文件声明全局变量version
,两个源文件都可以使用;它声明变量,但不定义它。实际变量在version.cpp
中定义和初始化;编译器将在这里为变量分配内存。表头声明上使用的extern
关键字向编译器表明version
有外部链接,即该名称在定义变量以外的文件中可见。version
变量在print.cpp
源文件中使用。在该文件中,使用的范围解析运算符(::
)没有名称空间名称,因此指示变量version
在全局名称空间中。
您也可以声明仅在当前翻译单元中使用的项目,方法是在使用之前在源文件中声明它们(通常在文件的顶部)。这产生了一定程度的模块化,并允许您对其他源文件中的代码隐藏实现细节。例如:
// in print.h
void usage();
// print.cpp
#include "version.h"
std::string app_name = "My Utility";
void print_version()
{
std::cout << "Version = " << ::version << std::endl;
}
void usage()
{
std::cout << app_name << " ";
print_version();
}
print.h
头包含文件print.cpp
中代码的接口。只有那些在头文件中声明的函数可以被其他源文件调用。调用者不需要知道usage
函数的实现,正如您在这里看到的,它是使用对一个名为print_version
的函数的调用来实现的,该函数只对print.cpp
中的代码可用。变量app_name
是在文件范围内声明的,因此它只能由print.cpp
中的代码访问。
如果另一个源文件在文件范围内声明了一个变量,叫做app_name
,也是std::string
,文件会编译,但是链接器在尝试链接目标文件时会抱怨。原因是链接器会看到在两个地方定义了同一个变量,并且不知道使用哪个变量。
函数也定义了一个范围;函数中定义的变量只能通过该名称访问。函数的参数也作为变量包含在函数中,因此在声明其他变量时,必须使用不同的名称。如果某个参数没有标记为const
,则可以在函数中更改该参数的值。
只要在使用变量之前声明变量,就可以在函数中的任何地方声明变量。花括号({}
)用于定义代码块,同时也定义局部范围;如果您在代码块中声明了一个变量,那么您只能在那里使用它。这意味着您可以在代码块之外声明同名的变量,编译器将使用最接近它被访问的范围的变量。
在结束本节之前,重要的是要提到 C++ 存储类的一个方面。在函数中声明的变量意味着编译器将在为该函数创建的堆栈框架上为该变量分配内存。当函数完成时,堆栈框架被拆除,内存被回收。这意味着,在函数返回后,任何局部变量中的值都会丢失;当再次调用该函数时,变量被重新创建并再次初始化。
C++ 提供了static
关键字来改变这种行为。static
关键字意味着程序启动时分配变量,就像在全局范围声明的变量一样。将static
应用于函数中声明的变量意味着该变量具有内部链接,即编译器将对该变量的访问限制在该函数中:
int inc(int i)
{
static int value;
value += i;
return value;
}
int main()
{
std::cout << inc(10) << std::endl;
std::cout << inc(5) << std::endl;
}
默认情况下,编译器会将一个静态变量初始化为0
,但您可以提供一个初始化值,这将在首次分配变量时使用。当该程序启动时,在调用main
函数之前,value
变量将被初始化为0
。第一次调用inc
函数时,value
变量增加到 10,由函数返回并打印到控制台。当inc
功能返回时,value
变量被保留,因此当再次调用inc
功能时,value
变量增加5
至15
值。
运算符用于根据一个或多个操作数计算值。下表以相等的优先级对所有运算符进行分组,并列出它们的关联性。表中的值越高,运算符在表达式中的执行优先级就越高。如果表达式中有几个运算符,编译器将在低优先级运算符之前执行高优先级运算符。如果表达式包含同等优先级的运算符,则编译器将使用结合律来决定操作数是与其左边的运算符还是右边的运算符分组。
There are some ambiguities in this table. A pair of parentheses can mean a function call or a cast and in the table these are listed as function()
and cast()
; in your code you will simply use ()
. The +
and -
symbols are either used to indicate sign (unary plus and unary minus, given in the table as +x
and -x
), or addition and subtraction (given in the table as +
and -
). The &
symbol means either "take the address of" (listed in the table as &x
) or bitwise AND
(listed in the table as &
). Finally, the postfix increment and decrement operators (listed in the table as x++
and x--
) have a higher precedence than the prefix equivalents (listed as ++ x
and --x
).
| 优先和结合 | 操作员 |
| 1 :无关联性 | ::
|
| 2 :从左到右的关联性 | .
或-> [] function() {} x++ x-- typeid const_cast dynamic_cast reinterpret_cast static_cast
|
| 3 :从右向左关联性 | sizeof ++ x --x ~ ! -x +x &x * new delete cast()
|
| 4 :从左到右的关联性 | .*
或->*
|
| 5 :从左到右的关联性 | * / %
|
| 6 :从左到右的关联性 | + -
|
| 7 :从左到右的关联性 | << >>
|
| 8 :从左到右的关联性 | < > <= >=
|
| 9 :从左到右的关联性 | == !=
|
| 10 :从左到右的关联性 | &
|
| 11 :从左到右的关联性 | ^
|
| 12 :从左到右关联性 | |
|
| 13 :从左到右关联性 | &&
|
| 14 :从左到右关联性 | ||
|
| 15 :从右向左关联性 | ? :
|
| 16 :从右向左关联性 | = *= /= %= += -= <<= >>= &= |= ^=
|
| 17 :从右向左关联性 | throw
|
| 18 :从左到右关联性 | ,
|
例如,看看下面的代码:
int a = b + c * d;
这被解释为先执行乘法,然后执行加法。编写相同代码的更清晰的方法是:
int a = b + (c * d);
原因是*
的优先级比+
高,所以先进行乘法,再进行加法:
int a = b + c + d;
在这种情况下,+
运算符具有相同的优先级,高于赋值的优先级。由于+
具有从左到右的关联性,该语句解释如下:
int a = ((b + c) + d);
也就是第一个动作是b
和c
的相加,结果加到d
上,就是这个结果用来赋值a
。这看起来并不重要,但请记住,加法可以在函数调用之间进行(函数调用的优先级高于+
):
int a = b() + c() + d();
这意味着这三个函数按照b
、c
、d
的顺序调用,然后根据从左到右的关联性对它们的返回值求和。这可能很重要,因为d
可能依赖于由其他两个函数改变的全局数据。
如果通过用括号将表达式分组来显式指定优先级,会使代码更易读、更容易理解。写b + (c * d)
可以立即清楚哪个表达式先执行,而b + c * d
意味着你必须知道每个运算符的优先级。
内置运算符是重载的,也就是说,无论操作数使用哪种内置类型,都使用相同的语法。操作数必须是相同的类型;如果使用不同的类型,编译器将执行一些默认转换,但在其他情况下(特别是在对不同大小的类型进行操作时),您将必须执行强制转换,以明确表明您的意思。
C++ 自带广泛的内置运算符;大多数是算术或逻辑运算符,这将在本节中介绍。内存操作符将包含在第 2 章、*使用内存、数组和指针、*以及 第 4 章、类中的对象相关操作符中。
算术运算符+
、-
、/
、*
和%
除了除法和模数运算符之外,几乎不需要其他解释。所有这些运算符都作用于整数和实数类型,除了%
,它只能用于整数类型。如果您混合了这些类型(比如,将一个整数加到一个浮点数上),那么编译器将执行自动转换。除法运算符/
的行为与您对浮点变量的预期一样:它产生两个操作数的除法结果。当你在两个整数之间执行除法a / b
时,结果是被除数(a
)中除数(b
)的整数。除法的余数由模数%
获得。因此,对于任何整数b
(除了零),可以说,整数a
可以表示如下:
(a / b) * b + (a % b)
请注意,模数运算符只能用于整数。如果你想得到浮点除法的余数,使用标准函数std:;remainder
。
使用整数除法时要小心,因为小数部分会被丢弃。如果您需要小数部分,那么您可能需要显式地将数字转换为实数。例如:
int height = 480;
int width = 640;
float aspect_ratio = width / height;
这给出了1
的纵横比,而它应该是1.3333
(或4 : 3
)。为了确保执行浮点除法,而不是整数除法,可以将被除数或除数转换为浮点数。
这些运算符有两个版本,前缀和后缀。顾名思义,前缀表示运算符放在操作数的左边(例如++ i
),后缀运算符放在右边(i++
)。++
运算符将增加操作数,--
运算符将减少操作数。前缀运算符表示“在操作后返回数值*,后缀运算符表示“在操作前返回数值因此,下面的代码将增加一个变量,并使用它来分配另一个变量:*
a = ++ b;
这里,使用前缀运算符,因此变量b
增加,并且变量a
在b
增加后被赋值。另一种表达方式是:
a = (b = b + 1);
下面的代码使用后缀运算符赋值:
a = b++ ;
这意味着变量b
增加,但是变量a
在b
增加之前被赋值。另一种表达方式是:
int t;
a = (t = b, b = b + 1, t);
Note that this statement uses the comma operator, so a
is assigned to the temporary variable t
in the right-most expression.
递增和递减运算符可以应用于整数和浮点数。运算符也可以应用于指针,在指针中它们有特殊的含义。当您增加一个指针变量时,这意味着将指针增加操作符所指向的类型的大小。
整数可以看作是一系列的位,0
或1
。与其他操作数中相同位置的位相比,按位运算符对这些位起作用。有符号整数使用一位来表示符号,但是按位运算符作用于整数中的每一位,因此通常只对无符号整数使用它们才是明智的。在下文中,所有类型都标记为unsigned
,因此它们被视为没有符号位。
&
运算符是按位“与”,这意味着左侧操作数中的每一位都与右侧操作数中相同位置的位进行比较。如果两者都是 1,则相同位置的结果位将是 1;否则,结果位为零:
unsigned int a = 0x0a0a; // this is the binary 0000101000001010
unsigned int b = 0x00ff; // this is the binary 0000000000001111
unsigned int c = a & b; // this is the binary 0000000000001010
std::cout << std::hex << std::showbase << c << std::endl;
在本例中,对0x00ff
使用按位&
与提供屏蔽除最低字节之外的所有字节的屏蔽具有相同的效果。
如果同一位置的任一位或两位都为 1,则按位“或”运算符|
将返回值 1,只有当两位都为 0 时,才返回值 0:
unsigned int a = 0x0a0a; // this is the binary 0000101000001010
unsigned int b = 0x00ff; // this is the binary 0000000000001111
unsigned int c = a & b; // this is the binary 0000101000001111
std::cout << std::hex << std::showbase << c << std::endl;
&
运算符的一个用途是查找是否设置了特定的位(或特定的位集合):
unsigned int flags = 0x0a0a; // 0000101000001010
unsigned int test = 0x00ff; // 0000000000001111
// 0000101000001111 is (flags & test)
if ((flags & test) == flags)
{
// code for when all the flags bits are set in test
}
if ((flags & test) != 0)
{
// code for when some or all the flag bits are set in test
}
flags
变量有我们需要的位,test
变量是我们正在检查的值。值(flags & test)
将只有那些也在flags
中设置的test
变量中的位。因此,如果结果为非零,则表示test
中至少有一位也设置在flags
中;如果结果与flags
变量完全相同,则flags
中的所有位都设置在test
中。
异或运算符^
用于测试位不同时;如果操作数中的位不同,则结果位为1
,如果它们相同,则结果位为0
。异或可用于翻转特定位:
int value = 0xf1;
int flags = 0x02;
int result = value ^ flags; // 0xf3
std::cout << std::hex << result << std::endl;
最后的按位运算符是按位补码~
。此运算符应用于单个整数操作数,并返回一个值,其中每个位都是操作数中相应位的补码;所以如果操作数位是 1,结果中的位是 0,如果操作数中的位是 0,结果中的位是 1。请注意,所有位都会被检查,因此您需要知道整数的大小。
==
操作者测试两个值是否完全相同。如果你测试两个整数,那么测试是显而易见的;比如x
是 2,y
是 3,那么x == y
显然就是false
。然而,即使你这样认为,两个实数也可能不一样:
double x = 1.000001 * 1000000000000;
double y = 1000001000000;
if (x == y) std::cout << "numbers are the same";
double
类型是一个 8 字节的浮点类型,但这对于这里使用的精度来说是不够的;存储在x
变量中的值是1000000999999.9999
(到小数点后四位)。
!=
操作员测试两个值是否不正确。运算符>
和<
测试两个值,查看左侧操作数是否大于或小于右侧操作数,>=
运算符测试左侧操作数是否大于或等于右侧操作数,<=
运算符测试左侧操作数是否小于或等于右侧操作数。这些运算符可用于if
语句,类似于前面示例中==
的用法。使用运算符的表达式返回类型为bool
的值,因此您可以使用它们为布尔变量赋值:
int x = 10;
int y = 11;
bool b = (x > y);
if (b) std::cout << "numbers same";
else std::cout << "numbers not same";
赋值运算符(=
)比大于(>=
)运算符具有更高的优先级,但是我们使用了圆括号来明确表示该值在用于赋值变量之前已经过测试。您可以使用!
运算符来否定逻辑值。因此,使用之前获得的b
的值,您可以编写以下内容:
if (!b) std::cout << "numbers not same";
else std::cout << "numbers same";
您可以使用&&
(与)和||
(或)运算符组合两个逻辑表达式。带有&&
运算符的表达式只有在两个操作数都为true
时才成立,而带有||
运算符的表达式只有在其中一个或两个操作数都为true
时才成立:
int x = 10, y = 10, z = 9;
if ((x == y) || (y < z))
std::cout << "one or both are true";
这段代码涉及三个测试;第一个测试x
和y
变量是否具有相同的值,第二个测试变量y
是否小于z
,然后进行一个测试,看前两个测试中的一个或两个是否为true
。
在像这样的||
表达式中,第一个操作数(x==y
)是true
,则总的逻辑表达式将是true
,而不考虑右操作数的值(这里,y < z
)。所以测试第二个表达式没有意义。相应地,在&&
表达式中,如果第一个操作数是false
,那么整个表达式必须是false
,所以表达式的右边部分不需要测试。
编译器将为您提供执行此短路的代码:
if ((x != 0) && (0.5 > 1/x))
{
// reciprocal is less than 0.5
}
该代码测试x
的倒数是否小于 0.5(或者相反,x
是否大于 2)。如果x
变量的值为 0,那么测试1/x
是错误的,但是在这种情况下,表达式将永远不会被执行,因为&&
的左操作数是false
。
按位移位运算符将左操作数整数中的位按指定方向移位右操作数中给定的指定位数。向左移动一位会将数字乘以 2,向右移动一位会除以 2。在下面,一个 2 字节的整数被移位:
unsigned short s1 = 0x0010;
unsigned short s2 = s1 << 8;
std::cout << std::hex << std::showbase;
std::cout << s2 << std::endl;
// 0x1000
s2 = s2 << 3;
std::cout << s2 << std::endl;
// 0x8000
在本例中,s1
变量设置了第五位(0x0010
或 16)。s2
变量有此值,左移 8 位,因此单个位被移至第 13 位,底部 8 位全部设置为 0 ( 0x10000
或 4,096)。这意味着0x0010
已经乘以 2 8 ,或者 256,得到0x1000
。接下来,该值再左移 3 位,结果为0x8000
;最高位被置位。
运算符丢弃任何溢出的位,因此如果您设置了最高位并将整数左移一位,则最高位将被丢弃:
s2 = s2 << 1;
std::cout << s2 << std::endl;
// 0
最后左移一位会得到值 0。
重要的是要记住,当与流一起使用时,运算符<<
意味着插入到流中,当与整数一起使用时,它意味着按位移位。
赋值运算符=
将左边的左值(变量)和右边的右值(变量或表达式)的结果赋值:
int x = 10;
x = x + 10;
第一行声明一个整数并将其初始化为 10。第二行通过再加 10 来改变变量,所以现在变量x
的值是 20。这是作业。C++ 允许您使用缩写语法根据变量值更改变量值。前面几行可以写成如下:
int x = 10;
x += 10;
像这样的递增运算符(以及递减运算符)可以应用于整数和浮点类型。如果运算符应用于指针,则操作数指示指针改变了多少个整项地址。例如,如果int
是 4 个字节,并且您将10
添加到int
指针,则实际指针值将增加 40 (10 乘以 4 个字节)。
除了增量(+=
)和减量(-=
)赋值,您还可以有乘法(*=
)、除法(/=
)和余数(%=
)赋值。除了最后一个(%=
)之外,所有这些都可以用于浮点类型和整数。余数赋值只能用于整数。
还可以对整数执行按位赋值操作:左移位(<<=
)、右移位(>>=
)、按位“与”(&=
)、按位“或”(|=
)和按位“异或”(^=
)。通常只有将这些应用于无符号整数才有意义。所以,乘以 8 可以通过这两条线来实现:
i *= 8;
i <<= 3;
C++ 提供了许多测试值和循环代码的方法。
最常用的条件语句是if
。最简单的形式是,if
语句在一对括号中取一个逻辑表达式,紧接着是语句,如果条件为true
,则执行该语句:
int i;
std::cin >> i;
if (i > 10) std::cout << "much too high!" << std::endl;
也可以使用else
语句捕捉条件为false
的场合:
int i;
std::cin >> i;
if (i > 10) std::cout << "much too high!" << std::endl;
else std::cout << "within range" << std::endl;
如果要执行几条语句,可以用大括号({}
)定义一个代码块。
条件是一个逻辑表达式,C++ 将从数字类型转换为bool
,其中 0 是false
,任何不为 0 的都是true
。如果你不小心,这可能是一个错误的来源,不仅很难注意到,而且会有意想不到的副作用。考虑下面的代码,它要求从控制台输入,然后测试用户是否输入-1:
int i;
std::cin >> i;
if (i == -1) std::cout << "typed -1" << endl;
std::cout << "i = " << i << endl;
这是人为的,但是您可能在循环中要求值,然后对这些值执行操作,除非用户输入-1,此时循环结束。如果输入错误,可能会出现以下代码:
int i;
std::cin >> i;
if (i = -1) std::cout << "typed -1" << endl;
std::cout << "i = " << i << endl;
在这种情况下,使用赋值运算符(=
)代替等式运算符(==
)。只有一个字符的区别,但是这段代码仍然是正确的 C++ 并且编译器很乐意编译它。
结果是,不管您在控制台上键入什么,变量i
都被赋值为-1,并且由于-1 不是零,if
语句中的条件是true
,因此执行该语句的 true 子句。由于变量被赋给了-1,这可能会进一步改变代码中的逻辑。避免这个错误的方法是利用赋值中左边必须是左值的要求。按照以下步骤进行测试:
if (-1 == i) std::cout << "typed -1" << endl;
这里的逻辑表达式是(-1 == i)
,由于==
运算符是可交换的(操作数的顺序无关紧要;您得到了相同的结果),这与您在前面的测试中预期的完全相同。但是,如果您输入了错误的运算符,您会得到以下结果:
if (-1 = i) std::cout << "typed -1" << endl;
在这种情况下,赋值在左侧有一个右值,这将导致编译器发出错误(在 Visual C++ 中这是C2106 '=' : left operand must be l-value
)。
允许在if
语句中声明一个变量,变量的作用域在语句块中。例如,可以按如下方式调用返回整数的函数:
if (int i = getValue()) {
// i != 0 // can use i here
} else {
// i == 0 // can use i here
}
虽然这是完全合法的 C++,但有几个原因让你想要这么做。
在某些情况下,可以使用条件运算符?:
来代替if
语句。运算符执行?
运算符左侧的表达式,如果条件表达式为true
,则执行?
右侧的表达式。如果条件表达式为false
,则执行:
右侧的表达式。运算符执行的表达式提供条件运算符的返回值。
例如,下面的代码确定了两个变量的最大值,a
和b
:
int max;
if (a > b) max = a;
else max = b;
这可以用以下一句话来表达:
int max = (a > b) ? a : b;
主要选择代码中可读性最好的。显然,如果赋值表达式很大,最好在if
语句中将它们拆分成行。但是,在其他语句中使用条件语句很有用。例如:
int number;
std::cin >> number;
std::cout << "there "
<< ((number == 1) ? "is " : "are ")
<< number << " item"
<< ((number == 1) ? "" : "s")
<< std::endl;
该代码确定变量number
是否为 1,如果是,则在控制台there is 1 item
上打印。这是因为在这两种条件下,如果number
变量的值为 1,则测试为true
,并使用第一个表达式。请注意,整个运算符周围有一对括号。原因是流<<
运算符重载,您希望编译器选择接受字符串的版本,这是运算符返回的类型,而不是表达式(number == 1)
的类型bool
。
如果条件运算符返回的值是左值,则可以在赋值的左侧使用它。这意味着您可以编写以下相当奇怪的代码:
int i = 10, j = 0;
((i < j) ? i : j) = 7;
// i is 10, j is 7
i = 0, j = 10;
((i < j) ? i : j) = 7;
// i is 7, j is 10
条件运算符检查i
是否小于j
,如果是,则为i
赋值;否则,它会为j
分配该值。这段代码很简洁,但缺乏可读性。在这种情况下,使用if
语句要好得多。
如果你想测试一个变量是否是几个值中的一个,使用多个if
语句会变得很麻烦。C++ switch
语句更好地实现了这个目的。基本语法如下所示:
int i;
std::cin >> i;
switch(i)
{
case 1:
std::cout << "one" << std::endl;
break;
case 2:
std::cout << "two" << std::endl;
break;
default:
std::cout << "other" << std::endl;
}
如果所选变量是指定值,每个case
本质上是要运行的特定代码的标签。default
条款适用于不存在case
的价值观。您不必有default
条款,这意味着您只针对特定情况进行测试。default
条款可以用于最常见的情况(在这种情况下,案例过滤掉不太可能的值),也可以用于例外值(在这种情况下,案例处理最可能的值)。
一个switch
语句只能测试整数类型(包括enum
),只能测试常量。char
类型为整数,这意味着您可以在case
项中使用字符,但只能使用单个字符;您不能使用字符串:
char c;
std::cin >> c;
switch(c)
{
case 'a':
std::cout << "character a" << std::endl;
break;
case 'z':
std::cout << "character z" << std::endl;
break;
default:
std::cout << "other character" << std::endl;
}
break
语句表示为case
执行的语句的结束。如果您没有指定,将通过执行*,以下case
语句将被执行,即使它们是为不同的情况指定的:*
switch(i)
{
case 1:
std::cout << "one" << std::endl;
// fall thru
case 2:
std::cout << "less than three" << std::endl;
break;
case 3:
std::cout << "three" << std::endl;
break;
case 4:
break;
default:
std::cout << "other" << std::endl;
}
这段代码显示了break
语句的重要性。值 1 会将one
和less than three
打印到控制台,因为执行会通过到达前面的case
,即使case
是另一个值。
对于不同的情况,通常会有不同的代码,所以您最常使用break
来完成一个case
。很容易误错过一个break
,这会导致不寻常的行为。当故意遗漏break
语句时,记录您的代码是一个很好的做法,这样您就知道如果遗漏了一个break
,那很可能是一个错误。
您可以为每个case
提供零个或多个语句。如果有一个以上的语句,它们都是针对该特定情况执行的。如果您没有提供任何语句(如本例中的case 4
),则意味着不会执行任何语句,甚至不会执行default
条款中的语句。
break
语句意味着脱离这个代码块,在循环语句while
和for
中也是如此。还有其他方法可以突破switch
。A case
可以调用return
来完成switch
声明的功能;它可以调用goto
跳转到一个标签,也可以调用throw
抛出一个异常,该异常将被switch
之外的异常处理程序捕获,甚至在函数之外。
到目前为止,这些案例是按数字顺序排列的。这不是一个要求,但它确实使代码更具可读性,显然,如果您想让通过case
语句(如这里的case 1
所示),您应该注意case
项的顺序。
如果您需要在case
处理程序中声明一个临时变量,那么您必须使用大括号定义一个代码块,这将使变量的范围只局限于该代码块。当然,您可以在任何case
处理程序中使用在switch
语句之外声明的任何变量。
由于枚举常数是整数,您可以在switch
语句中测试enum
:
enum suits { clubs, diamonds, hearts, spades };
void print_name(suits card)
{
switch(card)
{
case suits::clubs:
std::cout << "card is a club";
break;
default:
std::cout << "card is not a club";
}
}
这里的enum
虽然没有限定作用域(既不是enum class
也不是enum struct
,但并不要求在case
中指定值的作用域,反而让代码更清楚常量指的是什么。
大多数程序需要循环一些代码。C++ 提供了几种方法来实现这一点,要么用索引值迭代,要么测试逻辑条件。
for
语句有两个版本,迭代和基于范围。后者是在 C++ 11 中引入的。迭代版本具有以下格式:
for (init_expression; condition; loop_expression)
loop_statement;
您可以提供一个或多个循环语句,对于多个语句,您应该使用大括号提供一个代码块。循环表达式可以满足循环的目的,在这种情况下,您可能不希望执行循环语句;这里,您使用了 null 语句,;
表示什么也不做。
括号内是三个用分号分隔的表达式。第一个表达式允许您声明和初始化循环变量。该变量的作用域是for
语句,因此只能在for
表达式或后面的循环语句中使用。如果需要多个循环变量,可以使用逗号运算符在此表达式中声明它们。
当条件表达式为true
时,for
语句将循环;因此,如果使用循环变量,可以使用这个表达式来检查循环变量的值。第三个表达式在循环结束时调用,在循环语句被调用之后;接下来,调用条件表达式来查看循环是否应该继续。这个最终表达式通常用于更新循环变量值。例如:
for (int i = 0; i < 10; ++ i)
{
std::cout << i;
}
在该代码中,循环变量为i
,并初始化为零。接下来,检查条件,由于i
将小于 10,将执行语句(将值打印到控制台)。下一个动作是循环表达式;调用++ i
,递增循环变量i
,然后检查条件,依此类推。由于条件是i < 10
,这意味着此循环将运行十次,值为 0 到 9 之间的i
(因此您将在控制台上看到 0123456789)。
循环表达式可以是您喜欢的任何表达式,但通常它会增加或减少一个值。您不必将循环变量值更改 1;例如,您可以使用i -= 5
作为循环表达式,在每个循环上将变量减少 5。循环变量可以是您喜欢的任何类型;它不必是整数,甚至不必是数字(例如,它可以是指针,或者是使用标准库容器的 第 5 章、中描述的迭代器对象,并且条件和循环表达式不必使用循环变量。事实上,您根本不需要声明循环变量!
如果不提供循环条件,则循环将是无限的,除非您在循环中提供检查:
for (int i = 0; ; ++ i)
{
std::cout << i << std::endl;
if (i == 10) break;
}
这使用了之前与switch
语句一起引入的break
语句。表示执行退出for
循环,也可以使用return
、goto
或throw
。你很少会看到使用goto
结束的语句;但是,您可能会看到以下内容:
for (;;)
{
// code
}
在这种情况下,没有循环变量,没有循环表达式,也没有条件。这是一个永久的循环,循环中的代码决定了循环何时结束。
for
语句中的第三个表达式,循环表达式,可以是任何你喜欢的;唯一的属性是它在循环结束时执行。您可以选择更改该表达式中的另一个变量,或者您甚至可以提供由逗号运算符分隔的几个表达式。例如,如果您有两个函数,一个名为poll_data
的函数返回true
如果有更多的数据可用,而false
当没有更多的数据可用,以及一个名为get_data
的函数返回下一个可用的数据项,您可以使用for
如下(记住;这是一个做作的例子,说明一点):
for (int i = -1; poll_data(); i = get_data())
{
if (i != -1) std::cout << i << std::endl;
}
当poll_data
返回一个false
值时,循环将结束。需要if
语句,因为第一次调用循环时,get_data
尚未被调用。更好的版本如下:
for (; poll_data() ;)
{
int i = get_data();
std::cout << i << std::endl;
}
下一节请记住这个例子。
在for
循环中还有一个关键词可以使用。在许多情况下,您的for
循环将有许多行代码,并且在某个时候,您可能会决定当前循环已经完成,并且您想要开始下一个循环(或者,更具体地说,执行循环表达式,然后测试条件)。为此,你可以致电continue
:
for (float divisor = 0.f; divisor < 10.f; ++ divisor)
{
std::cout << divisor;
if (divisor == 0)
{
std::cout << std::endl;
continue;
}
std::cout << " " << (1 / divisor) << std::endl;
}
在这段代码中,我们打印了数字 0 到 9 的倒数(0.f
是一个 4 字节的浮点文字)。for
循环的第一行打印循环变量,下一行检查变量是否为零。如果是,则打印新行并继续,即不执行for
循环中的最后一行。原因是最后一行打印倒数,用零除任何数字都是错误的。
C++ 11 引入了另一种使用for
循环的方式,该循环旨在与容器一起使用。C++ 标准库包含用于容器类的模板。这些类包含对象的集合,并以标准方式提供对这些项的访问。标准方法是使用迭代器对象迭代集合。关于如何做到这一点的更多细节将在 第 5 章使用标准库容器中给出;语法要求理解指针和迭代器,所以我们在这里不涉及它们。基于范围的for
循环提供了一种简单的机制来访问容器中的项目,而无需显式使用迭代器。
语法很简单:
for (for_declaration : expression) loop_statement;
首先要指出的是,表达式只有两个,用冒号(:
)隔开。第一个表达式用于声明循环变量,该变量属于正在迭代的集合中的项的类型。第二个表达式提供对集合的访问。
In C++ terms, the collections that can be used are those that define a begin
and end
function that gives access to iterators, and also to stack-based arrays (that the compiler knows the size of).
标准库定义了一个名为vector
的容器对象。vector
模板是一个包含尖括号(<>
)中指定类型项目的类;在下面的代码中,vector
以 C++ 11 中新的特殊方式初始化,称为列表初始化。该语法允许您在花括号之间的列表中指定向量的初始值。下面的代码创建并初始化一个vector
,然后使用一个迭代for
循环打印出所有的值:
using namespace std;
vector<string> beatles = { "John", "Paul", "George", "Ringo" };
for (int i = 0; i < beatles.size(); ++ i)
{
cout << beatles.at(i) << endl;
}
Here a using
statement is used so that the classes vector
and string
do not have to be used with fully qualified names.
vector
类有一个名为size
的成员函数(通过.
操作符调用,意思是“在这个对象上调用这个函数”),它返回vector
中的项数。使用传递项目索引的at
函数访问每个项目。这段代码的一个大问题是它使用随机访问,也就是说,它使用索引访问每个项目。这是vector
的一个属性,但是其他标准库容器类型没有随机访问。以下使用基于范围的for
:
vector<string> beatles = { "John", "Paul", "George", "Ringo" };
for (string musician : beatles)
{
cout << musician << endl;
}
此语法适用于任何标准容器类型和堆栈上分配的数组:
int birth_years[] = { 1940, 1942, 1943, 1940 };
for (int birth_year : birth_years)
{
cout << birth_year << endl;
}
在这种情况下,编译器知道数组的大小(因为编译器已经分配了数组),因此它可以确定范围。基于范围的for
循环将遍历容器中的所有项目,但是与上一版本一样,您可以使用break
、return
、throw
或goto
离开for
循环,并且您可以使用continue
语句指示下一个循环应该被执行。
在前一节中,我们给出了一个人为的例子,其中for
循环中的条件轮询数据:
for (; poll_data() ;)
{
int i = get_data();
std::cout << i << std::endl;
}
在本例中,条件中没有使用循环变量。这是while
条件循环的候选:
while (poll_data())
{
int i = get_data();
std::cout << i << std::endl;
}
该语句将继续循环,直到表达式(本例中为poll_data
)的值为false
。与for
一样,您可以使用break
、return
、throw
或goto
退出while
循环,并且您可以使用continue
语句指示下一个循环应该被执行。
第一次调用while
语句,在执行循环之前测试条件;在某些情况下,您可能希望循环至少执行一次,然后测试条件(很可能取决于循环中的操作)以查看循环是否应该重复。方法是使用do-while
循环:
int i = 5;
do
{
std::cout << i-- << std::endl;
} while (i > 0);
注意while
子句后的分号。这是必需的。
该循环将以相反的顺序打印 5 比 1。原因是循环从i
初始化为 5 开始。循环中的语句通过后缀运算符递减变量,这意味着递减之前的值被传递给流。循环结束时,while
子句测试变量是否大于零。如果这个测试是true
,循环重复。当调用循环时i
被赋值为 1,1 的值被打印到控制台,变量减为零,while
子句将测试表达式false
,循环将结束。
两种循环的区别是在while
循环中执行循环之前先测试条件,所以可能不执行循环。在do-while
循环中,条件在循环之后被调用,这意味着,在do-while
循环中,循环语句总是被调用至少一次。
C++ 支持跳转,在大多数情况下,有更好的分支代码的方法;然而,为了完整起见,我们将在这里介绍该机制。跳转有两个部分:要跳转到的标记语句和goto
语句。标签与变量具有相同的命名规则;它被声明为以冒号为后缀,并且必须在语句之前。使用标签的名称调用goto
语句:
int main()
{
for (int i = 0; i < 10; ++ i)
{
std::cout << i << std::endl;
if (i == 5) goto end;
}
end:
std::cout << "end";
}
标签必须与调用goto
功能相同。
跳转很少使用,因为它们鼓励您编写非结构化代码。但是,如果您有一个带有高度嵌套循环或if
语句的例程,那么使用goto
跳转来清理代码可能会更有意义,可读性也更好。
现在让我们使用您在本章中学到的功能来编写应用。这个例子是一个简单的命令行计算器;您键入一个表达式,如*6 * 7*
,应用解析输入并执行计算。
启动 Visual C++ 并单击“文件”菜单,然后单击“新建”,最后单击“文件”...选项来获取“新建文件”对话框。在左侧窗格中,单击 Visual C++,在中间窗格中,单击 C++ 文件(。cpp),然后单击打开按钮。在你做任何其他事情之前,保存这个文件。使用 Visual C++ 控制台(一个命令行,有 Visual C++ 环境),导航到Beginning_C++
文件夹,创建一个名为Chapter_02
的新文件夹。现在,在 Visual C++ 中,在“文件”菜单上,单击“将源 1.cpp 另存为”...在“文件另存为”对话框中,找到您刚刚创建的Chapter_02
文件夹。在文件名框中,键入 calc.cpp,然后单击保存按钮。
应用将使用std::cout
和std::string
;所以在文件的顶部,添加定义这些的标题,这样就不必使用完全限定的名称,添加一个using
语句:
#include <iostream>
#include <string>
using namespace std;
您将通过命令行传递表达式,因此在文件底部添加一个采用命令行参数的main
函数:
int main(int argc, char *argv[])
{
}
该应用处理形式为arg1 op arg2
的表达式,其中op
是运算符,arg1
和arg2
是参数。这意味着,当调用应用时,它必须有四个参数;第一个是用于启动应用的命令,最后三个是表达式。main
函数中的第一个代码应确保提供正确数量的参数,因此在该函数的顶部添加一个条件,如下所示:
if (argc != 4)
{
usage();
return 1;
}
如果用多于或少于四个参数调用命令,则调用函数usage
,然后main
函数返回,停止应用。
在main
功能前增加usage
功能,如下:
void usage()
{
cout << endl;
cout << "calc arg1 op arg2" << endl;
cout << "arg1 and arg2 are the arguments" << endl;
cout << "op is an operator, one of + - / or *" << endl;
}
这只是解释了如何使用该命令并解释了参数。此时,您可以编译应用。由于您使用的是 C++ 标准库,因此您需要在支持 C++ 异常的情况下进行编译,因此请在命令行中键入以下内容:
C:\Beginning_C++ Chapter_02\cl /EHsc calc.cpp
如果你输入的代码没有任何错误,文件应该编译。如果您从编译器得到任何错误,请检查源文件,查看代码是否与前面代码中给出的完全一样。您可能会得到以下错误:
'cl' is not recognized as an internal or external command,
operable program or batch file.
这意味着控制台不是用 Visual C++ 环境设置的,所以要么关闭它并通过 Windows“开始”菜单启动控制台,要么运行 vcvarsall.bat 批处理文件。
一旦代码编译完毕,你就可以运行它了。首先用正确数量的参数(例如calc 6 * 7
)运行它,然后用不正确数量的参数(例如calc 6 * 7 / 3
)尝试它。请注意,参数之间的间距很重要:
C:\Beginning_C++ Chapter_02>calc 6 * 7
C:\Beginning_C++ Chapter_02>calc 6 * 7 / 3
calc arg1 op arg2
arg1 and arg2 are the arguments
op is an operator, one of + - / or *
在第一种情况下,应用什么也不做,所以您看到的只是一个空行。在第二个示例中,代码已经确定没有足够的参数,因此它将使用信息打印到控制台。
接下来,您需要对参数进行一些简单的解析,以检查用户是否传递了有效值。在main
功能的底部,添加以下内容:
string opArg = argv[2];
if (opArg.length() > 1)
{
cout << endl << "operator should be a single character" << endl;
usage();
return 1;
}
第一行用第三个命令行参数初始化一个 C++ std::string
对象,该参数应该是表达式中的运算符。这个简单的例子只允许操作符只有一个字符,所以后面的几行检查以确保操作符是一个字符。C++ std::string
类有一个名为length
的成员函数,它返回字符串中的字符数。
argv[2]
参数的长度至少为一个字符(没有长度的参数不会被视为命令行参数!),所以我们必须检查用户输入的运算符是否超过一个字符。
接下来,您需要进行测试,以确保该参数是允许的受限设置之一,如果用户键入另一个运算符,则打印一个错误并停止处理。在main
功能的底部,添加以下内容:
char op = opArg.at(0);
if (op == 44 || op == 46 || op < 42 || op > 47)
{
cout << endl << "operator not recognized" << endl;
usage();
return 1;
}
测试将在一个角色上进行,所以你需要从string
对象中提取这个角色。这段代码使用at
函数,传递给你需要的字符的索引。(第 5 章、使用标准库容器,将给出更多关于std::string
类成员的细节。)下一行检查该字符是否不受支持。代码依赖于我们支持的字符的以下值:
| 字符 | 值 |
| +
| 42
|
| *
| 43
|
| -
| 45
|
| /
| 47
|
可以看到,如果字符小于42
或者大于47
就会不正确,但是在42
和47
之间还有两个我们也要拒绝的字符:,
( 44
)和.
( 46
)。这就是为什么我们有前面的条件:“如果字符小于 42 或大于47
,或者是44
或46
,那么拒绝它。”
char
数据类型是整数,这就是测试使用整数文字的原因。您可以使用字符文字,因此以下更改同样有效:
if (op == ',' || op == '.' || op < '+' || op > '/')
{
cout << endl << "operator not recognized" << endl;
usage();
return 1;
}
你应该使用你认为最易读的。由于检查一个字符是否大于另一个字符意义不大,本书将使用前者。
此时,您可以编译代码并测试它。首先尝试使用一个多于一个字符的运算符(例如,**
),并确认您得到的消息是运算符应该是单个字符。其次,用不被识别的操作符进行测试;尝试除+
、*
、-
或/
以外的任何角色,但也值得尝试.
和,
。
请记住,命令提示符对某些符号有特殊的操作,如“&
”和“|
”,甚至在调用代码之前,命令提示符可能会通过解析命令行来给你一个错误。
接下来要做的是将参数转换成代码可以使用的形式。命令行参数以字符串数组的形式传递给程序;但是,我们将其中一些参数解释为浮点数(实际上是双精度浮点数)。C 运行库提供了一个名为atof
的函数,该函数可通过 C++ 标准库获得(在本例中,<iostream>
包括包含<cmath>
的文件,其中atof
被声明)。
It is a bit counter-intuitive to get access to a math function such as atof
through including a file associated with stream input and output. If this makes you uneasy, you can add a line after the include
lines to include the <cmath>
file. The C++ Standard Library headers have been written to ensure that a header file is only included once, so including <cmath>
twice has no ill effect. This was not done in the preceding code, because it was argued that atof
is a string function and the code includes the <string>
header and, indeed, <cmath>
is included via the files the <string>
header includes.
在main
功能的底部增加以下几行。前两行将第二个和第四个参数(记住,C++ 数组是从零开始索引的)转换为double
值。最后一行声明一个变量来保存结果:
double arg1 = atof(argv[1]);
double arg2 = atof(argv[3]);
double result = 0;
现在我们需要确定传递了哪个操作符,并执行请求的操作。我们将用switch
语句来实现这一点。我们知道op
变量是有效的,所以我们不需要提供default
子句来获取我们没有测试过的值。在函数底部添加switch
语句:
double arg1 = atof(argv[1]);
double arg2 = atof(argv[3]);
double result = 0;
switch(op)
{
}
前三种情况+
、-
和*
,都很简单:
switch (op)
{
case '+': result = arg1 + arg2; break; case '-': result = arg1 - arg2; break; case '*': result = arg1 * arg2; break;
}
同样,由于char
是一个整数,您可以在switch
语句中使用它,但是 C++ 允许您检查字符值。在这种情况下,使用字符而不是数字使代码更易读。
在switch
后,添加最终代码打印出结果:
cout << endl;
cout << arg1 << " " << op << " " << arg2;
cout << " = " << result << endl;
现在,您可以编译代码并用涉及+
、-
和*
的计算来测试它。
除法是个问题,因为除以零是无效的。为了测试这一点,在switch
的底部添加以下行:
case '/': result = arg1 / arg2; break;
编译并运行代码,传递零作为最终参数:
C:\Beginning_C++ Chapter_02>calc 1 / 0
1 / 0 = inf
代码运行成功,打印出了表达式,但是上面说结果是一个奇数inf
。这里发生了什么?
除以零将result
分配给NAN
的值,这是在<math.h>
中定义的常数(通过<cmath>
包含),表示“不是数字”cout
对象的插入运算符的double
重载测试该数字是否有有效值,如果该数字有值NAN
,则打印字符串 inf。在我们的应用中,我们可以测试一个零除数,并且我们将传递零的用户操作视为一个错误。因此,更改代码,使其如下所示:
case '/':
if (arg2 == 0) { cout << endl << "divide by zero!" << endl; return 1; } else {
result = arg1 / arg2;
}
break;
现在当用户通过零作为除数时,你会得到一条divide by zero!
信息。
现在,您可以编译完整的示例并进行测试。应用支持使用+
、-
、*
和/
运算符的浮点运算,并将处理被零除的情况。
在本章中,您已经学习了如何格式化代码,以及如何识别表达式和语句。您已经学习了如何识别变量的范围,以及如何将函数和变量的集合分组到名称空间中,以便防止名称冲突。您还学习了 C++ 中循环和分支代码的基本管道,以及内置运算符的工作原理。最后,您将所有这些放在一个简单的应用中,允许您在命令行执行简单的计算。
在下一章中,您将学习如何使用内存、数组和指针。**