返回介绍

3.4 C++算术运算符

发布于 2024-10-08 23:14:01 字数 12732 浏览 0 评论 0 收藏 0

读者可能还对学校里作的算术练习记忆犹新,在计算机上也能够获得同样的乐趣。C++使用运算符来运算。它提供了几种运算符来完成 5 种基本的算术计算:加法、减法、乘法、除法以及求模。每种运算符都使用两个值(操作数)来计算结果。运算符及其操作数构成了表达式。例如,在下面的语句中:

4 和 2 都是操作数,+是加法运算符,4+2 则是一个表达式,其值为 6。

下面是 5 种基本的 C++算术运算符。

  • +运算符对操作数执行加法运算。例如,4+20 等于 24。
  • −运算符从第一个数中减去第二个数。例如,12−3 等于 9。
  • 运算符将操作数相乘。例如,284 等于 112。
  • /运算符用第一个数除以第二个数。例如,1000/5 等于 200。如果两个操作数都是整数,则结果为商的整数部分。例如,17/3 等于 5,小数部分被丢弃。
  • %运算符求模。也就是说,它生成第一个数除以第二个数后的余数。例如,19%6 为 1,因为 19 是 6 的 3 倍余 1。两个操作数必须都是整型,将该运算符用于浮点数将导致编译错误。如果其中一个是负数,则结果的符号满足如下规则:(a/b)*b + a%b = a。

当然,变量和常量都可以用作操作数,程序清单 3.10 说明了这一点。由于%的操作数只能是整数,因此将在后面的例子中讨论它。

程序清单 3.10 arith.cpp

下面是该程序的输出,从中可知 C++能够完成简单的算术运算:

也许读者对得到的结果心存怀疑。11.17 加上 50.25 应等于 61.42,但是输出中却是 61.419998。这不是运算问题;而是由于 float 类型表示有效位数的能力有限。记住,对于 float,C++只保证 6 位有效位。如果将 61.419998 四舍五入成 6 位,将得到 61.4200,这是保证精度下的正确值。如果需要更高的精度,请使用 double 或 long double。

3.4.1 运算符优先级和结合性

读者是否委托 C++来完成复杂的算术运算?是的,但必须知道 C++使用的规则。例如,很多表达式都包含多个运算符。这样将产生一个问题:究竟哪个运算符最先被使用呢?例如,请看下面的语句:

操作数 4 旁边有两个运算符:+和。当多个运算符可用于同一个操作数时,C++使用优先级规则来决定首先使用哪个运算符。算术运算符遵循通常的代数优先级,先乘除,后加减。因此 3+45 指的是 3+(45),而不是(3+4)5,结果为 23,而不是 35。当然,可以使用括号来执行自己定义的优先级。附录 D 介绍了所有 C++运算符的优先级。其中,*、/和%位于同一行,这说明它们的优先级相同。同样,加和减的优先级也相同,但比乘除低。

有时,优先级列表并不够用。请看下面的语句:

操作数 4 也位于两个运算符中间,但运算符/和*的优先级相同,因此优先级本身并不能指出程序究竟是先计算 120 除以 4,还是先计算 4 乘以 5。因为第一种选择得到的结果是 150,而第二种选择的结果是 6,因此选择十分重要。当两个运算符的优先级相同时,C++将看操作数的结合性(associativity)是从左到右,还是从右到左。从左到右的结合性意味着如果两个优先级相同的运算符被同时用于同一个操作数,则首先应用左侧的运算符。从右到左的结合性则首先应用右侧的运算符。附录 D 也列出了结合性方面的信息。从中可以看出,乘除都是从左到右结合的。这说明应当先对 4 使用左侧的运算符。也就是说,用 120 除以 4,得到的结果为 30,然后再乘以 5,结果为 150。

注意,仅当两个运算符被用于同一个操作数时,优先级和结合性规则才有效。请看下面的表达式:

运算符优先级表明了两点:程序必须在做加法之前计算 205,必须在做加法之前计算 246。但优先级和结合性都没有指出应先计算哪个乘法。读者可能认为,结合性表明应先做左侧的乘法,但是在这种情况下,两个*运算符并没有用于同一个操作数,所以该规则不适用。事实上,C++把这个问题留给了实现,让它来决定在系统中的最佳顺序。对于这个例子来说,两种顺序的结果是一样的,但是也有两种顺序结果不同的情况。在第 5 章讨论递增运算符时,将介绍一个这样的例子。

3.4.2 除法分支

除法运算符(/)的行为取决于操作数的类型。如果两个操作数都是整数,则 C++将执行整数除法。这意味着结果的小数部分将被丢弃,使得最后的结果是一个整数。如果其中有一个(或两个)操作数是浮点值,则小数部分将保留,结果为浮点数。程序清单 3.11 演示了 C++除法如何处理不同类型的值。和程序清单 3.10 一样,该程序也调用 setf( ) 成员函数来修改结果的显示方式。

程序清单 3.11 divide.cpp

注意:

如果编译器不接受 setf( ) 中的 ios_base,请使用 ios。

有些基于 ANSI C 之前的编译器的 C++实现不支持浮点常量的 f 后缀。如果面临这样的问题,可以用(float) 1.e7 / (float) 9.0 代替 1.e7f / 9.0f。

有些实现会删除结尾的零。

下面使用某种实现时,程序清单 3.11 中程序的输出:

从第一行输出可知,整数 9 除以 5 的结果为整数 1。4/5 的小数部分(或 0.8)被丢弃。在本章后面学习求模运算符时,将会看到这种除法的实际应用。接下来的两行表明,当至少有一个操作数是浮点数时,结果为 1.8。实际上,对不同类型进行运算时,C++将把它们全部转换为同一类型。本章稍后将介绍这种自动转换。最后两行的相对精度表明,如果两个操作数都是 double 类型,则结果为 double 类型;如果两个操作数都是 float 类型,则结果为 float 类型。记住,浮点常量在默认情况下为 double 类型。

运算符重载简介

在程序清单 3.11 中,除法运算符表示了 3 种不同的运算:int 除法、float 除法和 double 除法。C++根据上下文(这里是操作数的类型)来确定运算符的含义。使用相同的符号进行多种操作叫做运算符重载(operator overloading)。C++有一些内置的重载示例。C++还允许扩展运算符重载,以便能够用于用户定义的类,因此在这里看到的是一个重要的 OOP 属性(参见图 3.4)。

图 3.4 各种除法

3.4.3 求模运算符

比起求模运算符来说,多数人更熟悉加、减、乘、除,因此这里花些时间介绍这种运算符。求模运算符返回整数除法的余数。它与整数除法相结合,尤其适用于解决要求将一个量分成不同的整数单元的问题,例如将英寸转换为英尺和英寸,或者将美元转换为元、角、分、厘。第 2 章的程序清单 2.6 将重量单位英石转换为磅。程序清单 3.12 则将磅转换为英石。记住,一英石等于 14 磅,多数英国浴室都使用这种单位。该程序使用整数除法来计算合多少英石,再用求模运算符来计算余下多少磅。

程序清单 3.12 modulus.cpp

下面是该程序的运行情况:

在表达式 lbs/Lbs_per_stn 中,两个操作数的类型都是 int,所以计算机执行整数除法。lbs 的值为 181,所以表达式的值为 12。12 和 14 的乘积是 168,所以 181 与 14 相除的余数是 9,这就是 lbs % Lbs_per_stn 的值。现在即使在感情上还没有适应英国的质量单位,但在技术上也做好了去英国旅游时解决质量单位转换问题的准备。

3.4.4 类型转换

C++丰富的类型允许根据需求选择不同的类型,这也使计算机的操作更复杂。例如,将两个 short 值相加涉及到的硬件编译指令可能会与将两个 long 值相加不同。由于有 11 种整型和 3 种浮点类型,因此计算机需要处理大量不同的情况,尤其是对不同的类型进行运算时。为处理这种潜在的混乱,C++自动执行很多类型转换:

  • 将一种算术类型的值赋给另一种算术类型的变量时,C++将对值进行转换;
  • 表达式中包含不同的类型时,C++将对值进行转换;
  • 将参数传递给函数时,C++将对值进行转换。

如果不知道进行这些自动转换时将发生的情况,将无法理解一些程序的结果,因此下面详细地介绍这些规则。

1.初始化和赋值进行的转换

C++允许将一种类型的值赋给另一种类型的变量。这样做时,值将被转换为接收变量的类型。例如,假设 so_long 的类型为 long,thirty 的类型为 short,而程序中包含这样的语句:

则进行赋值时,程序将 thirty 的值(通常是 16 位)扩展为 long 值(通常为 32 位)。扩展后将得到一个新值,这个值被存储在 so_long 中,而 thirty 的内容不变。

将一个值赋给值取值范围更大的类型通常不会导致什么问题。例如,将 short 值赋给 long 变量并不会改变这个值,只是占用的字节更多而已。然而,将一个很大的 long 值(如 2111222333)赋给 float 变量将降低精度。因为 float 只有 6 位有效数字,因此这个值将被四舍五入为 2.11122E9。因此,有些转换是安全的,有些则会带来麻烦。表 3.3 列出了一些可能出现的转换问题。

表 3.3 潜在的数值转换问题

转 换

潜在的问题

将较大的浮点类型转换为较小的浮点类型,如将 double 转换为 float

精度(有效数位)降低,值可能超出目标类型的取值范围,在这种情况下,结果将是不确定的

将浮点类型转换为整型

小数部分丢失,原来的值可能超出目标类型的取值范围,在这种情况下,结果将是不确定的

将较大的整型转换为较小的整型,如将 long 转换为 short

原来的值可能超出目标类型的取值范围,通常只复制右边的字节

将 0 赋给 bool 变量时,将被转换为 false;而非零值将被转换为 true。

将浮点值赋给整型将导致两个问题。首先,将浮点值转换为整型会将数字截短(除掉小数部分)。其次,float 值对于 int 变量来说可能太大了。在这种情况下,C++并没有定义结果应该是什么;这意味着不同的实现的反应可能不同。

传统初始化的行为与赋值相同,程序清单 3.13 演示了一些初始化进行的转换。

程序清单 3.13 assign.cpp

下面是该程序在某个系统中的输出:

在这个程序中,将浮点值 3.0 赋给了 tree。将 3.9832 赋给 int 变量 guess 导致这个值被截取为 3。将浮点型转换为整型时,C++采取截取(丢弃小数部分)而不是四舍五入(查找最接近的整数)。最后,int 变量 debt 无法存储 3.0E12,这导致 C++没有对结果进行定义的情况发生。在这种系统中,debt 的结果为 1634811904,或大约 1.6E09。

当您将整数变量初始化为浮点值时,有些编译器将提出警告,指出这可能丢掉数据。另外,对于 debt 变量,不同编译器显示的值也可能不同。例如,在另一个系统上运行该程序时,得到的值为 2147483647。

2.以{ }方式初始化时进行的转换(C++11)

C++11 将使用大括号的初始化称为列表初始化(list-initialization),因为这种初始化常用于给复杂的数据类型提供值列表。与程序清单 13.3 所示的初始化方式相比,它对类型转换的要求更严格。具体地说,列表初始化不允许缩窄(narrowing),即变量的类型可能无法表示赋给它的值。例如,不允许将浮点型转换为整型。在不同的整型之间转换或将整型转换为浮点型可能被允许,条件是编译器知道目标变量能够正确地存储赋给它的值。例如,可将 long 变量初始化为 int 值,因为 long 总是至少与 int 一样长;相反方向的转换也可能被允许,只要 int 变量能够存储赋给它的 long 常量:

在上述代码中,初始化 c4 时,您知道 x 的值为 66,但在编译器看来,x 是一个变量,其值可能很大。编译器不会跟踪下述阶段可能发生的情况:从 x 被初始化到它被用来初始化 c4。

3.表达式中的转换

当同一个表达式中包含两种不同的算术类型时,将出现什么情况呢?在这种情况下,C++将执行两种自动转换:首先,一些类型在出现时便会自动转换;其次,有些类型在与其他类型同时出现在表达式中时将被转换。

先来看看自动转换。在计算表达式时,C++将 bool、char、unsigned char、signed char 和 short 值转换为 int。具体地说,true 被转换为 1,false 被转换为 0。这些转换被称为整型提升(integral promotion)。例如,请看下面的语句:

为执行第 3 行语句,C++程序取得 chickens 和 ducks 的值,并将它们转换为 int。然后,程序将结果转换为 short 类型,因为结果将被赋给一个 short 变量。这种说法可能有点拗口,但是情况确实如此。通常将 int 类型选择为计算机最自然的类型,这意味着计算机使用这种类型时,运算速度可能最快。

还有其他一些整型提升:如果 short 比 int 短,则 unsigned short 类型将被转换为 int;如果两种类型的长度相同,则 unsigned short 类型将被转换为 unsigned int。这种规则确保了在对 unsigned short 进行提升时不会损失数据。

同样,wchar_t 被提升成为下列类型中第一个宽度足够存储 wchar_t 取值范围的类型:int、unsigned int、long 或 unsigned long。

将不同类型进行算术运算时,也会进行一些转换,例如将 int 和 float 相加时。当运算涉及两种类型时,较小的类型将被转换为较大的类型。例如,程序清单 3.11 中的程序用 9.0 除以 5。由于 9.0 的类型为 double,因此程序在用 5 除之前,将 5 转换为 double 类型。总之,编译器通过校验表来确定在算术表达式中执行的转换。C++11 对这个校验表稍做了修改,下面是 C++11 版本的校验表,编译器将依次查阅该列表。

(1)如果有一个操作数的类型是 long double,则将另一个操作数转换为 long double。

(2)否则,如果有一个操作数的类型是 double,则将另一个操作数转换为 double。

(3)否则,如果有一个操作数的类型是 float,则将另一个操作数转换为 float。

(4)否则,说明操作数都是整型,因此执行整型提升。

(5)在这种情况下,如果两个操作数都是有符号或无符号的,且其中一个操作数的级别比另一个低,则转换为级别高的类型。

(6)如果一个操作数为有符号的,另一个操作数为无符号的,且无符号操作数的级别比有符号操作数高,则将有符号操作数转换为无符号操作数所属的类型。

(7)否则,如果有符号类型可表示无符号类型的所有可能取值,则将无符号操作数转换为有符号操作数所属的类型。

(8)否则,将两个操作数都转换为有符号类型的无符号版本。

ANSI C 遵循的规则与 ISO 2003 C++相同,这与前述规则稍有不同;而传统 K&R C 的规则又与 ANSI C 稍有不同。例如,传统 C 语言总是将 float 提升为 double,即使两个操作数都是 float。

前面的列表谈到了整型级别的概念。简单地说,有符号整型按级别从高到低依次为 long long、long、int、short 和 signed char。无符号整型的排列顺序与有符号整型相同。类型 char、signed char 和 unsigned char 的级别相同。类型 bool 的级别最低。wchar_t、char16_t 和 char32_t 的级别与其底层类型相同。

4.传递参数时的转换

正如第 7 章将介绍的,传递参数时的类型转换通常由 C++函数原型控制。然而,也可以取消原型对参数传递的控制,尽管这样做并不明智。在这种情况下,C++将对 char 和 short 类型(signed 和 unsigned)应用整型提升。另外,为保持与传统 C 语言中大量代码的兼容性,在将参数传递给取消原型对参数传递控制的函数时,C++将 float 参数提升为 double。

5.强制类型转换

C++还允许通过强制类型转换机制显式地进行类型转换。(C++认识到,必须有类型规则,而有时又需要推翻这些规则。)强制类型转换的格式有两种。例如,为将存储在变量 thorn 中的 int 值转换为 long 类型,可以使用下述表达式中的一种:

强制类型转换不会修改 thorn 变量本身,而是创建一个新的、指定类型的值,可以在表达式中使用这个值。

强制转换的通用格式如下:

第一种格式来自 C 语言,第二种格式是纯粹的 C++。新格式的想法是,要让强制类型转换就像是函数调用。这样对内置类型的强制类型转换就像是为用户定义的类设计的类型转换。

C++还引入了 4 个强制类型转换运算符,对它们的使用要求更为严格,这将在第 15 章介绍。在这四个运算符中,static_cast<>可用于将值从一种数值类型转换为另一种数值类型。例如,可以像下面这样将 thorn 转换为 long 类型:

推而广之,可以这样做:

Stroustrup 认为,C 语言式的强制类型转换由于有过多的可能性而极其危险,这将在第 15 章更深入地讨论。运算符 static_cast<>比传统强制类型转换更严格。

程序清单 3.14 演示了这两种基本的强制类型转换和 static_cast<>。可以将该程序第一部分想象为一个功能强大的生态模拟程序的一部分,该程序执行浮点计算,结果被转换为鸟和动物的数目。得到的结果取决于何时进行转换。计算 auks 时,首先将浮点值相加,然后在赋值时,将总数转换为 int。但计算 bats 和 coots 时,首先通过强制类型转换将浮点值转换为 int,然后计算总和。程序的最后一部分演示了如何通过强制类型转换来显示 char 值的 ASCII 码。

程序清单 3.14 typecast.cpp

下面是该程序的运行结果:

首先,将 19.99 和 11.99 相加,结果为 31.98。将这个值赋给 int 变量 auks 时,它被截短为 31。但在进行加法运算之前使用强制类型转换时,这两个值将被截短为 19 和 11,因此 bats 和 coots 的值都为 30。接下来,两条 cout 语句使用强制类型转换将 char 类型的值转换为 int,再显示它。这些转换导致 cout 将值打印为整数,而不是字符。

该程序指出了使用强制类型转换的两个原因。首先,可能有一些值被存储为 double 类型,但要使用它们来计算得到一个 int 类型的值。例如,可能要用浮点数来对齐网格或者模拟整数值(如人口)。程序员可能希望在计算时将值视为 int,强制类型转换允许直接这样做。注意,将值转换为 int,然后相加得到的结果,与先将值相加,然后转换为 int 是不同的,至少对于这些值来说是不同的。

程序的第二部分指出了最常见的使用强制类型转换的原因—使一种格式的数据能够满足不同的期望。例如,在程序清单 3.14 中,char 变量 ch 存储的是字母 Z 的编码。将 cout 用于 ch 将显示字符 Z,因为 ch 的类型为 char。但通过将 ch 强制转换为 int 类型,cout 将采用 int 模式,从而打印存储在 ch 中的 ASCII 码。

3.4.5 C++11 中的 auto 声明

C++11 新增了一个工具,让编译器能够根据初始值的类型推断变量的类型。为此,它重新定义了 auto 的含义。auto 是一个 C 语言关键字,但很少使用,有关其以前的含义,请参阅第 9 章。在初始化声明中,如果使用关键字 auto,而不指定变量的类型,编译器将把变量的类型设置成与初始值相同:

然而,自动推断类型并非为这种简单情况而设计的;事实上,如果将其用于这种简单情形,甚至可能让您误入歧途。例如,假设您要将 x、y 和 z 都指定为 double 类型,并编写了如下代码:

显式地声明类型时,将变量初始化 0(而不是 0.0)不会导致任何问题,但采用自动类型推断时,这却会导致问题。

处理复杂类型,如标准模块库(STL)中的类型时,自动类型推断的有时才能显现出来。例如,对于下述 C++98 代码:

C++11 允许您将其重写为下面这样:

本书后面讨论相关的主题时,将再次提到 auto 的这种新含义。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文