【C++ Primer每日一刷之十】 操作符(一)

表达式

C++ 提供了丰富的操作符,并定义操作数为内置类型时,这些操作符的含义。除此之外,C++ 还支持操作符重载,允许程序员自定义用于类类型时操作符的含义。标准库正是使用这种功能定义用于库类型的操作符。

本章重点介绍 C++ 语言定义的操作符,它们使用内置类型的操作数;本章还会介绍一些标准库定义的操作符。第十四章将学习如何定义自己的重载操作符。

表达式由一个或多个操作数通过操作符组合而成。最简单的表达式仅包含一个字面值常量或变量。较复杂的表达式则由操作符以及一个或多个操作数构成。

每个表达式都会产生一个结果。如果表达式中没有操作符,则其结果就是操作数本身(例如,字面值常量或变量)的值。当一个对象用在需要使用其值的地方,则计算该对象的值。例如,假设ival 是一个int 型对象:

if (ival) // evaluate ival as a condition

// ....

上述语句将 ival 作为 if 语句的条件表达式。当 ival 为非零值时, if条件成立;否则条件不成立。

对于含有操作符的表达式,它的值通过对操作数做指定操作获得。除了特殊用法外,表达式的结果是右值,可以读取该结果值,但是不允许对它进行赋值。

操作符的含义——该操作符执行什么操作以及操作结果的类型——取决于操作数的类型。

除非已知道操作数的类型,否则无法确定一个特定表达式的含义。下面的表达式

i + j

既可能是整数的加法操作、字符串的串接或者浮点数的加法操作,也完全可能是其他的操作。如何计算该表达式的值,完全取决于 i 和 j 的数据类型。

C++提供了一元操作符和二元操作符两种操作符。作用在一个操作数上的操作符称为一元操作符,如取地址操作符(&)和解引用操作符(*);而二元操作符则作用于两个操作数上,如加法操作符(+)和减法操作符(-)。除此之外,C++还提供了一个使用三个操作数的三元操作符(ternary operator),我们将在第 5.7节介绍它。

有些符号(symbols)既可表示一元操作也可表示二元操作。例如,符号 * 既可以作为(一元)解引用操作符,也可以作为(二元)乘法操作符,这两种用法相互独立、各不相关,如果将其视为两个不同的符号可能会更容易理解些。对于这类操作符,需要根据该符号所处的上下文来确定它代表一元操作还是二元操作。

操作符对其操作数的类型有要求,如果操作符应用于内置或复合类型的操作数,则由C++语言定义其类型要求。例如,用于内置类型对象的解引用操作符要求其操作数必须是指针类型,对任何其他内置类型或复合类型对象进行解引用将导致错误的产生。

对于操作数为内置或复合类型的二元操作符,通常要求它的两个操作数具有相同的数据类型,或者其类型可以转换为同一种数据类型。关于类型转换,我们将在第 5.12 节学习。尽管规则可能比较复杂,但大部分的类型转换都可按预期的方式进行。例如,整型可转换为浮点类型,反之亦然,但不能将指针类型转换为浮点类型。

要理解由多个操作符组成的表达式,必须先理解操作符的优先级、结合性和

操作数的求值顺序。例如,表达式

5 + 10 * 20/2;

使用了加法、乘法和除法操作。该表达式的值取决于操作数与操作符如何结合。例如,乘法操作符* 的操作数可以是 10 和 20,也可以是 10 和 20 /2,或者 15 和 20 、 15 和 20/2。结合性和优先级规则规定了操作数与操作符的结合方式。在 C++ 语言中,该表达式的值应是 105,10 和 20 先做乘法操作,然后其结果除以 2,再加 5 即为最后结果。

求解表达式时,仅了解操作数和操作符如何结合是不足够的,还必须清楚操作符上每一个操作数的求值顺序。每个操作符都控制了其假定的求值顺序,即,我们是否可以假定左操作数总是先于右操作数求值。大部分的操作符无法保证某种特定的求值次序,我们将于第 5.10 节讨论这个问题。

5.1. 算术操作符

除非特别说明,表5-1 所示操作符可用于任意算术类型(第 2.1 节)或者任何可转换为算术类型的数据类型。

表 5.1 按优先级来对操作符进行分组——一元操作符优先级最高,其次是乘、除操作,接着是二元的加、减法操作。高优先级的操作符要比低优先级的结合得更紧密。这些算术操作符都是左结合,这就意味着当操作符的优先级相同时,这些操作符从左向右依次与操作数结合。

表 5.1. 算术操作符

操作符 功能 用法

+ unary plus(一元正号)+ expr

- unary minus(一元负号)- expr

* multiplication(乘法) expr * expr

/ division(除法) expr/ expr

% remainder(求余)expr % expr

+ addition(加法) expr+ expr

- subtraction(减法)expr – expr

对于前述表达式

5 + 10 * 20/2;

考虑优先级与结合性,可知该表达式先做乘法( *)操作,其操作数为 10 和20,然后以该操作的结果和 2为操作数做除法(/)操作,其结果最后与操作数5 做加法( +)操作。

一元负号操作符具有直观的含义,它对其操作数取负:

int i = 1024;

int k = -i; // negates the value of itsoperand

一元正号操作符则返回操作数本身,对操作数不作任何修改。

警告:溢出和其他算术异常

某些算术表达式的求解结果未定义,其中一部分由数学特性引起,例如除零操作;其他则归咎于计算机特性,如溢出:计算出的数值超出了其类型的表示范围。

考虑某台机器,其 short 类型为16 位,能表示的最大值是 32767。假设 short 类型只有 16 位,下面的复合赋值操作将会溢出:

// max value if shorts are 8 bits

short short_value = 32767;

short ival = 1;

// this calculation overflows

short_value += ival;

cout << "short_value: "<< short_value << endl;

表示 32768 这个有符号数需 17位的存储空间,但是这里仅有 16 位,于是导致溢出现象的发生,此时,许多系统都不会给出编译时或运行时的警告。对于不同的机器,上述例子的 short_value 变量真正获得的值不尽相同。在我们的系统上执行该程序后将得到:

short_value: -32768

其值“截断(wrapped around)”,将符号位的值由 0 设为 1,于是结果变为负数。因为算术类型具有有限的长度,因此计算后溢出的现象常常发生。

二元 +、 - 操作符也可用于指针值,对指针使用这些操作符的用法将在第4.2.4 节介绍。

算术操作符 +、-、* 和 / 具有直观的含义:加法、减法、乘法和除法。对两个整数做除法,结果仍为整数,如果它的商包含小数部分,则小数部分会被截除:

int ival1 = 21/6; // integral resultobtained by truncating the

remainder

int ival2 = 21/7; // no remainder, resultis an integral value

ival1 和 ival2 均被初始化为 3。于计算左操作数除以右操作数的余数。该操作符的操作数只能为整型,包括bool、char、short 、int 和 long 类型,以及对应的 unsigned 类型:

int ival = 42;

double dval = 3.14;

ival % 12; // ok: returns 6

ival % dval; // error: floating pointoperand

如果两个操作数为正,除法(/)和求模(%)操作的结果也是正数(或零);如果两个操作数都是负数,除法操作的结果为正数(或零),而求模操作的结果则为负数(或零);如果只有一个操作数为负数,这两种操作的结果取决于机器;求模结果的符号也取决于机器,而除法操作的值则是负数(或零):

21 % 6; // ok: result is 3

21 % 7; // ok: result is 0

-21 % -8; // ok: result is -5

21 %-5; // machine-dependent: result is 1 or -4

21 / 6; // ok: result is 3

21 / 7; // ok: result is 3

-21 / -8; // ok: result is 2

21 / -5; // machine-dependent: result -4 or-5

当只有一个操作数为负数时,求模操作结果值的符号可依据分子(被除数)或分母(除数)的符号而定。如果求模的结果随分子的符号,则除出来的值向零一侧取整;如果求模与分母的符号匹配,则除出来的值向负无穷一侧取整。

5.2. 关系操作符和逻辑操作符

关系操作符和逻辑操作符(表 5.2)使用算术或指针类型的操作数,并返回bool 类型的值。

表 5.2. 关系操作符和逻辑操作符

下列操作符都产生 bool 值

操作符 功能 用法

! logical NOT(逻辑非)!expr

< less than(小于)expr < expr

<= less than or equal(小于等于) expr <= expr

> greater than(大于) expr > expr

>= greater than or equal(大于等于) expr >= expr

下列操作符都产生 bool 值

操作符 功能 用法

== equality(相等)expr == expr

!= inequality(不等)expr != expr

&& logical AND(逻辑与) expr && expr

|| logical OR(逻辑或)expr || expr

逻辑与、逻辑或操作符

逻辑操作符将其操作数视为条件表达式(第 1.4.1 节):首先对操作数求值;若结果为 0,则条件为假(false),否则为真(true)。仅当逻辑与(&&)操作符的两个操作数都为 true,其结果才得 true 。对于逻辑或(||)操作符,只要两个操作数之一为 true,它的值就为 true。给定以下形式:

expr1 && expr2 // logical AND

expr1 || expr2 // logical OR

仅当由 expr1 不能确定表达式的值时,才会求解 expr2。也就是说,当且仅

当下列情况出现时,必须确保 expr2 是可以计算的:

? 在逻辑与表达式中,expr1 的计算结果为 true。如果 expr1 的值为

false,则无论 expr2 的值是什么,逻辑与表达式的值都为 false 。当

expr1 的值为 true 时,只有 expr2 的值也是 true ,逻辑与表达式的

值才为 true。

? 在逻辑或表达式中,expr1 的计算结果为 false。如果 expr1 的值为false,则逻辑或表达式的值取决于 expr2 的值是否为 true。逻辑与和逻辑或操作符总是先计算其左操作数,然后再计算其右操作数。只有在仅靠左操作数的值无法确定该逻辑表达式的结果时,才会求解其右操作数。我们常常称这种求值策略为“短路求值(short-circuit evaluation)”。

对于逻辑与操作符,一个很有价值的用法是:如果某边界条件使 expr2 的计算变得危险,则应在该条件出现之前,先让 expr1 的计算结果为 false。例如,编写程序使用一个 string 类型的对象存储一个句子,然后将该句子的第一个单词的各字符全部变成大写,可如下实现:

string s("Expressions in C++ arecomposed...");

string::iterator it = s.begin();

// convert first word in s to uppercase

while (it != s.end() &&!isspace(*it)) {

*it = toupper(*it); // toupper covered insection 3.2.4 (p. 88)

++it;

}

在这个例子中,while 循环判断了两个条件。首先检查 it 是否已经到达string 类型对象的结尾,如果不是,则 it 指向 s 中的一个字符。只有当该检验条件成立时,系统才会计算逻辑与操作符的右操作数,即在保证it 确实指向一个真正的字符之后,才检查该字符是否为空格。如果遇到空格,或者 s 中没有空格而已经到达 s 的结尾时,循环结束。

逻辑非操作符

逻辑非操作符(!)将其操作数视为条件表达式,产生与其操作数值相反的条件值。如果其操作数为非零值,则做 ! 操作后的结果为 false。例如,可如下在 vector 类型对象的 empty 成员函数上使用逻辑非操作符,根据函数返回值判断该对象是否为空:

// assign value of first element in vec tox if there is one

int x = 0;

if (!vec.empty())

x = *vec.begin();

如果调用 empty 函数返回false,则子表达式

!vec.empty()的值为true。

不应该串接使用关系操作符

关系操作符(<、<=、>、<=)具有左结合特性。事实上,由于关系操作符返回bool 类型的结果,因此很少使用其左结合特性。如果把多个关系操作符串接起来使用,结果往往出乎预料:

// oops! this condition does not determineif the 3 values are unequal

if (i < j < k) { /* ... */ }

这种写法只要 k 大于 1,上述表达式的值就为 true。这是因为第二个小于操作符的左操作数是第一个小于操作符的结果:true 或 false。也就是,该条件将 k 与整数 0 或 1 做比较。为了实现我们想要的条件检验,应重写上述表达式如下:

if (i < j && j < k) { /* ...*/ }

相等测试与bool 字面值

正如第 5.12.2 节将介绍的,bool类型可转换为任何算术类型——bool 值false 用 0 表示,而 true 则为 1。

由于 true 转换为 1,因此要检测某值是否与 bool 字面值true 相等,其等效判断条件通常很难正确编写:

if (val == true) { /* ... */ }

val 本身是 bool 类型,或者 val 具有可转换为 bool 类型的数据类型。如果 val 是 bool 类型,则该判断条件等效于:

if (val) { /* ... */ }

这样的代码更短而且更直接(尽管对初学者来说,这样的缩写可能会令人费解)。

更重要的是,如果 val 不是bool 值,val 和 true 的比较等效于:

if (val == 1) { /* ... */ }

这与下面的条件判断完全不同:

// condition succeeds if val is any nonzerovalue

if (val) { /* ... */ }

此时,只要 val 为任意非零值,条件判断都得 true。如果显式地书写条件比较,则只有当 val 等于指定的 1 值时,条件才成立。

时间: 2024-10-31 14:18:53

【C++ Primer每日一刷之十】 操作符(一)的相关文章

【C++ Primer每日一刷之十二】 箭头操作符,条件操作符,sizeof操作符,逗号,优先级

5.6. 箭头操作符 C++ 语言为包含点操作符和解引用操作符的表达式提供了一个同义词:箭头操作符(->).点操作符(第 1.5.2 节)用于获取类类型对象的成员: item1.same_isbn(item2); // run thesame_isbn member of item1 如果有一个指向 Sales_item 对象的指针(或迭代器),则在使用点操作符 前,需对该指针(或迭代器)进行解引用: Sales_item *sp = &item1; (*sp).same_isbn(item

【C++ Primer每日一刷之九】创建动态数组

表达式 C++ 提供了丰富的操作符,并定义操作数为内置类型时,这些操作符的含义.除此之外,C++ 还支持操作符重载,允许程序员自定义用于类类型时操作符的含义.标准库正是使用这种功能定义用于库类型的操作符. 本章重点介绍 C++ 语言定义的操作符,它们使用内置类型的操作数:本章还会介绍一些标准库定义的操作符.第十四章将学习如何定义自己的重载操作符. 表达式由一个或多个操作数通过操作符组合而成.最简单的表达式仅包含一个字面值常量或变量.较复杂的表达式则由操作符以及一个或多个操作数构成. 每个表达式都

【C++ Primer每日一刷之八】之八 C 风格字符串

4.3 C 风格字符串 尽管 C++ 支持 C 风格字符串,但不应该在 C++ 程序中使用这个类型.C 风格字符串常常带来许多错误,是导致大量安全问题的根源. 在前面我们第一次使用了字符串字面值,并了解字符串字面值的类型是字符常量的数组,现在可以更明确地认识到:字符串字面值的类型就是const char 类型的数组.C++ 从 C 语言继承下来的一种通用结构是C 风格字符串,而字符串字面值就是该类型的实例.实际上,C 风格字符串既不能确切地归结为 C 语言的类型,也不能归结为 C++ 语言的类型

【C++ Primer每日一刷之七】指针操作

4.2.3 指针操作 指针提供间接操纵其所指对象的功能.与对迭代器进行解引用操作一样,对指针进行解引用可访问它所指的对象,* 操作符(解引用操作符)将获取指针所指的对象: string s("hello world"); string *sp = &s; // sp holds theaddress of s cout <<*sp; // prints hello world 对 sp 进行解引用将获得 s 的值,然后用输出操作符输出该值,于是最后一条语句输出了 s

【C++ Primer每日一刷之五】标准库类型小结

标准库类型小结 C++ 标准库定义了几种更高级的抽象数据类型,包括 string 和 vector 类型.string 类型提供了变长的字符串,而 vector 类型则可用于管理同一类型 的对象集合.迭代器实现了对存储于容器中对象的间接访问.迭代器可以用于访问和遍历 string 类型和vectors 类型的元素.下一节将介绍 C++ 的内置数据类型:数组和指针.这两种类型提供了类似于 vector 和 string 标准库类型的低级抽象类型.总的来说,相对于C++ 内置数据类型的数组和指针而言

【C++ Primer每日一刷之六】数组

引言 C++ 语言提供了两种类似于vector 和迭代器类型的低级复合类型--数组和指针.与vector 类型相似,数组也可以保存某种类型的一组对象:而它们的区别在于,数组的长度是固定的.数组一经创建,就不允许添加新的元素.指针则可以像迭代器一样用于遍历和检查数组中的元素. 现代 C++ 程序应尽量使用vector 和迭代器类型,而避免使用低级的数组和指针.设计良好的程序只有在强调速度时才在类实现的内部使用数组和指针. 数组是 C++ 语言中类似于标准库vector 类型的内置数据结构.与 ve

【C++ Primer每日一刷之八】之九 创建动态数组

4.3.1. 创建动态数组 数组类型的变量有三个重要的限制:[数组长度固定不变],[在编译时必须知道其长度],[数组只在定义它的块语句内存在]. 实际的程序往往不能忍受这样的限制--它们需要在运行时动态地分配数组. 虽然数组长度是固定的,但动态分配的数组不必在编译时知道其长度,可以(通常也是)在运行时才确定数组长度.与数组变量不同,动态分配的数组将一直存在,直到程序显式释放它为止.每一个程序在执行时都占用一块可用的内存空间,用于存放动态分配的对象,此内存空间称为程序的自由存储区或堆. C 语言程

【C++ Primer每日刷】之一 迭代器

迭代器的介绍 概述 迭代器是一种检查容器内元素并遍历元素的数据类型. 迭代器(iterator)是一种对象,它能够用来遍历标准模板库容器中的部分或全部元素,每个迭代器对象代表容器中的确定的地址.迭代器修改了常规指针的接口,所谓迭代器是一种概念上的抽象:那些行为上像迭代器的东西都可以叫做迭代器.然而迭代器有很多不同的能力,它可以把抽象容器和通用算法有机的统一起来. 标准库为每一种标准容器(包括 vector)定义了一种迭代器类型.迭代器类型提供了比下标操作更通用化的方法:所有的标准库容器都定义了相

C++ Primer Plus的若干收获--(十)

明天就要回学校了,本来回家之前是有一片宏图伟志的,无奈只能抱着这可怜的十篇博客回学校了.自己马上就要大三,大学的下一半马上就要开始了,我的未来还有什么在等待着我呢,好期待!!! 10.1 友元 我们知道C++控制对类对象私有部分的访问.通常,公有类方法是唯一的途径,但是除此之外C++还提供了另外一种机制:友元.友元有三种友元函数,友元类与友元成员函数.在介绍如何成为友元前,先介绍一下为何需要友元.在为类重载二元运算符是常常需要友元.将之前的Time对象乘以实数就是这种情况. 我们之前重载了+ T