Visual C++从入门到精通(第5版)
上QQ阅读APP看书,第一时间看更新

2.4 运算符

C++语言中常用的运算符有赋值运算符、算术运算符、关系运算符、逻辑运算符、自增自减运算符、位运算符、sizeof运算符、new和delete运算符等。本节将详细介绍C++语言中的运算符。

2.4.1 赋值运算符

赋值运算符是程序开发中最基本的运算符。C++语言中最常用的赋值运算符为“=”。在前面几节的示例中,多处使用了赋值运算符。本节将详细讨论赋值运算符。

首先介绍一下赋值与初始化的区别。赋值和初始化均使用“=”运算符,但是初始化只有在定义变量时进行,并且只有一次,而赋值则可以进行多次。

在使用赋值运算符时,应该保证赋值运算符左右两边的表达式数据类型兼容。例如,下面的赋值语句是错误的。

下面的代码是合法的,因为实型数据将被隐式转换为整型。

在上面的代码中,虽然10.55为实数,但是它能够自动地转换为整数,因此不会出现错误。

此外,在使用赋值运算符时,应保证赋值运算符的左边是一个“可读写地址”的表达式。例如,下面的赋值语句是非法的。

在使用赋值运算符时,可以在一条语句中为多个变量赋值。例如:

在上述代码中,变量jvar和ivar的值均为10。

在C++语言中,赋值运算符除了“=”外,还有“+=”“-=”“*=”“/=”和“%=”等。下面以“+=”赋值运算符为例介绍这些运算符的使用。

“+=”运算符表示将运算符左边的表达式与右边的表达式相加,再赋值给左边的表达式。例如:

上述代码执行后,变量ivar的值将为15。

说明

代码“ivar +=5;”等价于“ivar = ivar + 5;”。

“-=”“*=”“/=”和“%=”等运算符与“+=”运算符用法基本相同,详细描述如表2.4所示。

表2.4 C++赋值运算符

2.4.2 算术运算符

算术运算通常为加、减、乘、除和取模5种,在C++语言中对应的运算符如表2.5所示。

表2.5 C++算术运算符

算术运算符在程序中经常被使用,使用方法也比较简单,下面主要介绍除法运算。当两个整数相除时,其结果仍为整数,小数部分被舍弃。例如:

在上面的代码中,变量ivar与jvar的值将相同,均为2。有些读者可能会认为11/4的结果为实数,只是将其截取了小数赋值给整型变量ivar,这种理解是不正确的。观察如下代码:

【例2.13】 两个整数相除的结果。(实例位置:资源包\TM\sl\2\10)

执行上述代码,结果如图2.11所示。

从图2.11中可以发现,变量fvar的值为2.000000,而不是2.75000。因为两个整数相除,结果仍为整数。这是许多初学者编写代码时经常犯的错误。如果对上述代码稍加修改,将整数4修改为实数4.0,结果将发生改变。因为整数与实数相除或两个实数相除,结果将为实数。

【例2.14】 整数与实数相除的结果。(实例位置:资源包\TM\sl\2\11)

执行上述代码,结果如图2.12所示。

图2.11 整数相除

图2.12 整数与实数相除

从图2.12中可以发现,变量fvar的值为2.750000。

2.4.3 关系运算符

关系运算属于一种简单的逻辑运算,程序设计语言通常包含6种关系运算,即大于、小于、等于、大于等于、小于等于和不等于。C++中对应的关系运算符如表2.6所示。

表2.6 C++关系运算符

对于关系运算来说,表达式的值只有真和假。若为真,则表达式的值为1;若为假,则表达式的值为0。例如:

执行上述代码,iret的值将为1,因为关系表达式“5 > 4”的值为真,即1。关系表达式可以由多个关系运算符构成。

观察最后一行代码,关系表达式由两个关系运算符“>”构成。对于关系运算符来说,其结合性(有关运算符的结合性可参考2.4.9节)由左到右,因此关系表达式“ivar > jvar > nvar”首先比较“ivar >jvar”,其结果为1,然后进行“1 > nvar”比较,其结果为0,最后将0赋值给变量iret,因此代码“int iret = ivar > jvar > nvar;”执行后,其值为0。

注意

将变量值代入最后一行代码“int iret = 10 > 9 > 8;”,初学者一看“10 > 9 > 8”很容易将表达式的值当成真值,这是初学者经常犯的错误。

2.4.4 逻辑运算符

逻辑运算符用于连接关系表达式,以构成复杂的逻辑表达式。C++中共有3种逻辑运算符,分别为“&&”“||”和“!”。其中“&&”表示逻辑与运算,即当两个表达式同时为真,其结果为真,否则为假。“||”表示逻辑或运算,即当两个表达式中有一个表达式为真,其结果为真,否则为假。“!”表示逻辑非运算,即当表达式的值为真,其结果为假;当表达式的值为假,其结果为真。表2.7描述了逻辑运算符的运算方式。

表2.7 逻辑运算符真值表

技巧

表2.7看起来有些复杂,尤其是“&&”和“||”运算符,记起来非常容易乱,其实可以用一句话来概括这两个运算符,那就是“都真才真,都假才假”,前半句用来形容“&&”运算符,后半句用来形容“||”运算符。

在逻辑表达式中通常可以包含多个逻辑运算符。例如:

分析最后一行代码,首先进行“ivar > jvar”比较,结果为1;然后进行“nvar > mvar”比较,结果为0,接着进行“1”和“0”的与(&&)运算,结果为0;进行“mvar > ivar”比较,结果为1,最后进行“0”和“1”的或(||)运算,结果为1;将1赋值给变量iret。因此,执行最后一行代码后,iret的值为1。

需要说明的是,最后一行代码并不是规范的代码,因为用户要事先熟知各种运算符的结合性和优先级,否则很难确定代码的执行顺序。如果使用“()”对最后一行代码进行分解,则会使代码更加清晰。例如:

通过使用“()”,用户可以非常清楚地了解代码的执行顺序。

2.4.5 自增自减运算符

为了方便进行加1和减1操作,C++提供了自增和自减运算符,即“++”和“--”。自增自减运算符分为前置运算和后置运算。下面通过代码来演示自增和自减运算符的应用。

【例2.15】 后置自增运算符。(实例位置:资源包\TM\sl\2\12)

执行上述代码,结果如图2.13所示。

在上述第2行代码中使用了后置自增运算符,使ivar的值加1。对于后置运算符来说,先进行赋值操作,然后才使变量ivar的值自动加1。因此,上述代码运行后,变量ivar的值为11,而变量jvar的值为10。如果将第2行中的后置自增运算符改为前置自增运算符,运行结果就不同了。

【例2.16】 前置自增运算符。(实例位置:资源包\TM\sl\2\13)

执行上述代码,结果如图2.14所示。

图2.13 后置自增运算

图2.14 前置自增运算

在上述第2行代码中使用了前置运算符,首先使变量ivar的值加1,然后将其赋值给变量jvar,因此jvar的值为11。

说明

对于自增运算符“++”来说,“变量++”和“++变量”都等价于“变量=变量+1”,区别只在于是先执行加1的运算,还是后进行加1的运算。

自增自减运算符不仅可以应用于数值型变量,还可以应用于指针变量。在开发程序时,通常使用自增运算符来实现指针遍历数组。观察如下代码:

【例2.17】 使用指针遍历数组。(实例位置:资源包\TM\sl\2\14)

执行上述代码,结果如图2.15所示。

图2.15 指针遍历数组

在上述代码中,“*pvar++”表达式先输出指针pvar的数据,然后进行“++”运算,使指针指向下一个元素。

2.4.6 位运算符

在计算机系统中,所有数据均以二进制的形式表示。数据存储是以字节为单位,一个字节包含8位,每一位可以表示为0或1。本节介绍的位运算符是指能够对二进制数据中的位进行运算的符号。C++中主要包含6种位运算符,如表2.8所示。

表2.8 C++位运算符

下面分别进行介绍。

1. 按位与运算—&

按位与运算是指两个相应的二进位均为1,则结果为1,否则结果为0。即0&0结果为0,0&1结果为0,1&0结果为0,1&1结果为1。例如,将10和12进行运算,即“10&12”。数字10对应的二进制数为“00001010”,数字12对应的二进制数为“00001100”。10&12运算过程如图2.16所示。

从图2.16中可以发现,10&12运算结果为8。

在开发程序中,按位与运算通常有其特殊的用途。例如,将某个字节数据清零,可以将该字节的数据与0进行按位与运算,其结果将为0。因为任何数与0进行与运算,结果必然为0。此外,与运算还可以将一个字节中的某些位保留下来。例如,对于二进制数00001110,想要保留其1、3、5、7位,则可以将该二进制数与“01010101”二进制数(该二进制数1、3、5、7位为1,其他位为0)进行按位与运算,获得的结果为“00000100”,也就是说当前数的第3位数字为1,其他1、5、7位数字都为0。

2. 按位或运算—|

按位或运算是指两个相应的二进位只要有一个结果为1,则结果为1,否则结果为0。即0|0结果为0,0|1结果为1,1|0结果为1,1|1结果为1。例如,将10和12进行或运算,即“10|12”。其运算过程如图2.17所示。

从图2.17中可以发现,“10 | 12”的运算结果为14。

根据按位或运算的性质可知,如果要将某一个字节数据中的某一位或几位设置为1,通过按位或运算可以非常方便地实现。例如,要将“00001010”后4位设置为1,将其与“00001111”二进制数(后4位全为1)进行按位或运算即可。

3. 按位异或运算—^

按位异或运算是指两个相应的二进位均相同,则结果为0,否则结果为1。即0^0结果为0,0^1结果为1,1^0结果为1,1^1结果为0。例如,将10和12进行异或运算,即“10^12”。其运算过程如图2.18所示。

图2.16 按位与运算

图2.17 按位或运算

图2.18 按位异或运算

从图2.18中可以发现,“10^12”的运算结果为6。在实际应用中,通常利用按位异或运算来实现二进位的反转。例如,将二进制数“00001010”的后4位反转,可以将其与“00001111”二进制数(后4位均为1)进行按位异或运算,所得结果为“00000101”。此外,还可以利用按位异或运算实现两个变量值的互换(不使用中间变量)。首先分析一下异或运算的性质:

 任何数据与0进行按位异或运算,结果仍为数据本身。

 变量与自身进行按位异或运算,结果为0。

 按位异或运算具有交换性,即a^b^c等于a^c^b,还等于b^a^c等。

下面通过一个实例演示如何使用按位异或运算实现两个变量值的互换。

【例2.18】 使用按位异或运算实现两个变量值的互换。(实例位置:资源包\TM\sl\2\15)

执行上述代码,结果如图2.19所示。

分析上述黑体部分代码。首先观察黑体部分第2行代码“jvar =jvar ^ ivar;”,根据上一行代码将“ivar”替换为“ivar ^ jvar”,那么第2行代码“jvar = jvar ^ ivar;”转换为“jvar = jvar ^ ivar ^ jvar;”。由于异或运算具有交换性,因此也可以写为“jvar=jvar ^ jvar^ ivar;”。由于变量与自身进行按位异或运算结果为0,因此第2行代码又可以转换为“jvar = 0 ^ ivar;”。根据任何数据与0进行按位异或运算,结果仍为数据本身,最终第2行代码的结果为“jvar = ivar”。接着分析黑体部分第3行代码,根据第1行代码和第2行代码,可以将第3行代码转换为“ivar = ivar ^ jvar ^ jvar^ ivar ^ jvar;”,即“ivar = ivar ^ ivar ^jvar ^ jvar ^ jvar;”,等价于“ivar =0^ jvar;”,最终“ivar = jvar”。

上面的分析对于初学者来说或许有些复杂,不过不要急,还有一种简单的方法可以解释清楚,那就是直接代入数据。虽然不能通过代入数据的结果来证明两个变量交换值的原理,但是,想必读者看过具体的计算以后,可以更好地理解上面的分析。下面就将实例中的数据“00000100”(4)和“00000101”(5)代入计算:

图2.19 交换变量

第一步:ivar = 00000101^ 00000100,结果ivar = 00000001(1)。

第二步:jvar = 00000100^ 00000001,结果jvar = 00000101(5)。

第三步:ivar = 00000001 ^ 00000101,结果ivar = 00000100(4)。

通过以上3步,确实实现了两个变量的数值交换。

4. 按位取反运算—~

取反运算符“~”用于对一个二进制数按位取反,即将0转换为1,将1转换为0。例如,对二进制数“00001010”取反,结果为“11110101”。

5. 左移运算符—<<

左移运算符用于将一个数的二进制位左移若干位,右边补0。例如,将10左移两位,表示为:

其结果iret等于40。因为10的二进制数为“00001010”,左移2位后,表示为“00101000”,即40。实际上,左移一位,表示乘以2;左移两位,表示乘以22,即乘以4,以此类推。

在使用左移运算符时,有时会出现移出界的情况。例如:

上述代码执行后,cvar对应的数值将为4。因为129对应的二进制数为“01000001”,将其左移两位为“00000100”,7、8位“01”溢出,被舍弃,因此cvar的值为4。

6. 右移运算符—>>

右移运算符与左移运算符相反,是将一个数的二进制位右移若干位。例如,将10右移两位,表示为:

其结果iret等于2。因为10对应的二进制数为“00001010”,右移2位后为“00000010”,即十进制数2。

位运算符还可以与赋值运算符“=”组合,形成位运算赋值运算符,如“&=”“|=”“^=”等。以“&=”为例,“ivar &= jvar”实际上等价于“ivar = ivar & jvar”。具体运算过程前文已经介绍,这里不再赘述。

2.4.7 sizeof运算符

sizeof运算符用于返回变量、对象或者数据类型的字节长度。例如:

sizeof运算符还可以用于数组。例如:

在上述代码中,arraysize的值为20,因为数组中共有5个元素,每个元素占4个字节。

使用sizeof运算符在确定数组大小时,需要注意的是对字符串常量的测试。例如,测试字符串“Hello”的长度,其长度为6,因为对于字符串常量来说,系统会自动添加“\0”字符作为结束标记。例如,下面的赋值操作将出现错误。

因为字符串常量“Hello”需要占用6个字节空间,因此在编译时会出现数组溢出的错误。如果改为如下代码,就不会出现问题了。

通常,用户会采用如下方式将字符串常量赋值为数组:

此时,数组carray的长度为6。

如果用户使用的是32位的操作系统,当sizeof运算符测试指针的长度时,无论指针是何种类型及指针指向什么数据,指针的长度均为4,因为指针是按32位寻址的,长度为4个字节。这一点对于C++的初学者很重要。许多公司的面试题中经常会出现类似如下的代码:

要求确定size的值,这里size的值应为4。如果以数组作为函数参数,在函数中测试数组参数的长度时,其值为4,因为调用函数时需要传递数组名,而在传递数组名时实际传递的是数组的首地址,也可以认为是一个指针。

【例2.19】 判断参数中数组名的大小。

在上面的代码中,变量size的值为4,因为语句“int size = sizeof(cdata);”中的cdata被认为是一个指针。

注意

作为参数传递的数组其实是以指针的形式传递的,所以在使用sizeof获得数组参数的长度时是4,而不是数字长度。

2.4.8 new和delete运算符

在介绍new和delete运算符之前,先来介绍一下C++应用程序数据的存储方式。对于C++应用程序来说,数据主要有两种存储方式,即栈存储和堆存储。栈存储通常用于存储占用空间小、生命周期短的数据,如局部变量和函数参数等。本节之前的实例中,除了静态变量和全局变量外,其他的所有变量均属于栈存储方式。堆存储通常用于存储占用空间大、生命周期长的数据,如静态变量和全局变量等。除静态变量和全局变量外,用户可以使用new运算符在堆中开辟一个空间,使变量存储在堆中。例如:

在上述代码中,调用new运算符在堆中开辟了4字节的空间,将地址指向指针pvar,接着设置指针pvar的数据,然后输出数据,最后调用delete运算符释放pvar指向的堆空间。

注意

对于手动分配的堆空间,在使用后一定要释放堆空间,否则会出现内存泄漏。

在使用new运算符分配堆空间时,还可以进行初始化。例如:

此时,pvar指向的数据为10。

使用new运算符,还可以为数组动态分配空间。例如:

使用new运算符为数组分配空间时,不能够对数组进行初始化,除非变量是一个对象,并且对象的类型(类)提供了默认的构造函数。此外,delete []用来释放使用new运算符为数组分配的空间。例如:

说明

在上面的代码中,如果使用“delete pvar;”语句来释放pvar指针指向的数组空间,也是可以的,不会出现内存泄漏。不过在开发程序时并不提倡这么做,因为对于简单的基础数据类型(上述代码为int类型),没有提供析构函数,使用“delete pvar;”语句释放数组时不会出现内存泄漏;但是对于类对象数组来说,这样做是不行的,必须使用“delete []”形式来释放new运算符分配的数组空间。

2.4.9 结合性与优先级

运算符具有结合性和优先级两个属性,这两个属性描述的是语句的执行顺序。所谓结合性是指表达式的整体计算方向,即从左向右或从右向左。以“int iret=x+y+z;”语句为例,由于算术运算符的结合性从左向右,即表达式的整体计算方向从左向右,因此语句中“x+y+z”的计算方式首先计算“x+y”,然后结果再与“z”相加。而赋值运算符的计算方向是从右向左,即将右边的结果赋值给左边,因此将“x+y+z”的结果赋值为iret,而不是将iret赋值给“x+y+z”。优先级表示的是运算符的优先执行顺序。在数学中,表达式“x+y*z”的计算方式是先计算“y*z”,然后将结果与“x”相加。在计算机中,为了符合人们的计算习惯,同样规定了“*”运算符的优先级高于“+”运算符,因此,表达式“x+y*z”在程序中的执行顺序与在数学中的计算方式是相同的。表2.9描述了C++运算符的优先级和结合性。

表2.9 C++运算符的优先级和结合性

说明

同一优先级的运算符,运算次序由结合性决定。