第五章:操作符和表达式
5.1操作符
5.1.1 算术操作符
+ - * / %
-
/
:操作数都是整数,执行整除运算,结果是一个整数(丢弃了小数部分,即向下取整)。
其他情况,执行浮点数除法,结果是一个浮点数。//整除运算 int a = 7; int b = 3; int result = a / b; // result 是 2,因为 7 除以 3 的整数结果是 2
-
%
:操作数只能是整数。
需要注意的是如果左边操作数为负数,那么结果也是负数。
为了确保跨平台的可移植性,如果你需要处理负数的取模运算,并且需要特定的行为(比如总是得到非负的余数),参考环形队列中的方法可修改为如下,来确保余数在[0, b-1]
的范围内。int main() { int a = -7; int b = 3; int remainder = (a % b + b) % b; // 确保余数为非负 printf("Non-negative Remainder: %d\n", remainder); return 0; }
5.1.2 移位操作符
<< >>
<<
>>
:操作数只能是非负整数值。<<
:左移补码,移动后用0补齐。
>>
:右移补码,逻辑右移0补齐,算数右移符号位补齐(负数符号位为1,正数符号位为0)- 无符号数执行的所有位移运算都是逻辑位移,有符号数采用逻辑位移or算计位移取决于编译器。
- 如果右边操作数为负数, C语言标准并没有定义这种行为,编译器可能会产生错误或警告,或者产生未定义的结果。因此,位移量是一个合适的非负整数值。
5.1.3 位操作符
& | ^
位操作相关题
- 指定位 置1:
value=value | 1<<bit_number;
- 指定位 置0:
value=value & ~(1<<bit_number);
5.1.4 赋值
=
-
赋值也是一个表达式,表达式就有一个值,这个值就是左操作数的新值, 它可以作为其他赋值操作符的操作数。举个例子:
int main() { int a = 5; int b; b = (a = 10) + 5; //相当于 a = 10; b = a + 5; printf("%d", b);//15 }
-
整型截断和提升问题:
char ch; while ((ch = getchar()) != EOF) { // 处理字符... }
EOF(End Of File)是文件结束的标志,在C语言中,它通常被定义为一个整数值,通常是-1。
getchar()
函数返回的是一个int
类型的值,这样它就可以返回任何字符的ASCII码值(0-255对于无符号字符,或者-128到127对于有符号字符)。当我们将getchar()
的返回值赋值给一个char
类型的变量时,就会发生截断 。 这意味着int
类型的值会被转换为char
类型,通常只保留最低有效字节(least significant byte, LSB)。如果这个int
值超出了char
类型能够表示的范围,就会发生溢出,导致数据丢失。
对于EOF来说,其值为-1。在二进制补码表示中,-1被编码为所有位都是1(对于int
类型来说)。当我们将这个值赋给一个char
变量时,只有最低有效字节被保留。如果这一字节恰好也是全1,那么截断后的值在提升回int
类型进行比较时,可能恰巧与EOF相等,导致逻辑错误。
正确的做法应该是直接将getchar()
的返回值存储为int
类型的变量,如下所示:int ch; while ((ch = getchar()) != EOF) { // 处理字符... }
//复合赋值
+= -= *= /= %= <<= >>= &= ^= |=
a+=exp
相当于a=a+(exp)
,其他同理。
5.1.5 单目操作符
! ~ + - & * sizeof (类型) ++ --
-
!
:逻辑求反 -
~
:位求补,0变1,1变0 -
sizeof
:用于获取数据类型或对象在内存中的大小(以字节为单位)
若sizeof(变量类型)
或sizeof(变量名)
返回字节数。
若sizeof(数组名)
返回数组长度,单位为字节。-
sizeof
在计算大小的时候并不实际计算或求值其操作数。它只查看类型信息,而不会执行任何形式的代码或计算。int main() { int a = 5; char b = 0;//int b = 0 printf("%zu\n", sizeof(b = a + 3)); // 输出变量b类型的大小,不会计算 a + 3 的值 printf("%d", b); }
在这个例子中,
sizeof(b = a + 3)
并不会计算b = a + 3
,而是查看b
的类型,并返回该类型的大小。
-
-
(类型)
:强制类型转换float f = 3.14; int i = (int)f; // 强制将 f 转换为 int 类型,i 的值将是 3
f
的值3.14
被强制转换为int
类型,这导致小数部分被截断,结果3
被存储在变量i
中。 -
++
--
:
原文中这段话可以理解它们如何工作以及它们返回什么结果。
前缀形式的自增和自减操作符会首先增加或减少变量的值,然后返回改变后的值的拷贝。 这意味着,在表达式中使用前缀形式的操作符时,你会得到变量改变后的值。int i = 5; int j = ++i; // i 先变为 6,然后 j 被赋值为 6
后缀形式的自增和自减操作符会先返回变量当前值的拷贝,然后再增加或减少变量的值。 这意味着,在表达式中使用后缀形式的操作符时,你会得到变量改变前的值。
int i = 5; int j = i++; // j 被赋值为 5,然后 i 变为 6
因为后缀形式返回的是变量改变前的值的拷贝,而不是变量本身。左值是指一个可以出现在赋值操作左侧的表达式,它代表一个内存位置。由于后缀形式返回的是拷贝,而不是内存位置,所以它们不能用作左值。
例如,下面的代码是错误的:int i = 5; (i++) = 10; // 错误:i++ 不是一个左值
在这段代码中,尝试将
10
赋值给i++
的结果是错误的,因为i++
返回的是i
的一个拷贝,而不是i
本身的内存位置。
5.1.6 关系操作符
> >= < <= != ==
- 注意
=
和==
,这是一个很常见的问题。
5.1.7 逻辑操作符
&& ||
- 短路求值(short-circuited evaluation)问题:
exp1 && exp2
,若exp1
为假,整体必为假,exp2
不再求值;exp1 || exp2
,若exp1
为真,整体必为真,exp2
不再求值;
5.1.8 条件操作符
exp1?exp2:exp3
5.1.9 逗号操作符
exp1,exp2,exp3,...,expN
自左向右逐个求值,整个逗号表达式的值就是最后那个表达式的值。
5.1.10 下标引用、函数调用和结构成员
下标引用会在第6章详细说明
函数调用会在第7章详细说明
结构成员会在第10章详细说明
5.2 布尔值
C并不具备显式的布尔类型,所以使用整数来代替。其规则是:零是假,任何非零值皆为真。
5.3 左值和右值
左值(L-value) 就是赋值符号=
左边的东西,它们通常表示内存中有明确地址的对象 ,因此我们可以找到这块地址的数据,也可以进行赋值操作。
右值(R-value) 就是赋值符号=
右边的东西。它们通常表示临时的值。 右值表示表达式结束后就不再存在的临时对象,因此它们没有内存地址,也不能用于赋值操作。
a = b + 25;//√
b + 25 = a;//×,没有明确的地址
int c[30];
c[b+10]=0;//√
5.4 表达式求值
表达式的求值顺序一部分是由它所包含的操作符的优先级和结合性决定。同样,有些表达式的操作数在求值过程中可能需要转换为其他类型。
5.4.1 隐式类型转换
整型提升(Integer Promotion)是C和C++语言中的一种隐式类型转换规则,用于在表达式求值过程中保证整型操作数的精度和范围。当较小的整型在表达式中与较大的整型一起使用时,较小的整型会被隐式地转换为较大的整型,以确保表达式求值的正确性。
整型提升的规则如下:
- 如果整型操作数的类型等级低于
int
,那么它会被提升为int
类型(如果int
可以容纳该类型的所有值)。 - 如果整型操作数的类型等级等于
int
,则不进行提升。 - 如果整型操作数的类型等级高于
int
,则它保持其原有类型不变。
整型提升的主要目的是确保在混合使用不同大小的整型时,表达式求值能够正确地进行,并且避免数据丢失或溢出。同时,它也简化了编译器在处理整型表达式时的逻辑。
需要注意的是,整型提升只发生在表达式求值过程中,它并不会改变变量的实际类型或存储在内存中的值。 在赋值操作或数据传递时,还需要考虑其他类型转换规则,如算术转换等。
int main() {
char a = '5'; // 字符'5'的ASCII值为53
char b = '3'; // 字符'3'的ASCII值为51
int sum = a + b;
printf("%d\n", sum); // 输出结果:104,而不是8
return 0;
}
尽管a
和b
都是char
类型,但在表达式a + b
中,它们会被整型提升为int
类型。因此,实际的加法运算是两个整数(53和51)之间的加法,而不是字符之间的加法。最终结果是104,而不是8。
5.4.2 算术转换
寻常算术转换用于在执行算术运算时处理不同操作数类型之间的转换。当操作数的类型不同时,这些规则会指导编译器如何自动转换操作数的类型,以便能够执行运算。
寻常算术转换的规则如下:
这段话的意思就是不同类型进行操作时需要把其中一个操作数的转换成另一个操作数的类型,这里的规则是 :
- 如果其中一个操作数是
long double
类型,那么另一个操作数也会被转换为long double
类型。 - 如果其中一个操作数是
double
类型,那么另一个操作数也会被转换为double
类型。 - 如果其中一个操作数是
float
类型,那么另一个操作数也会被转换为float
类型。
如果上述三条规则都不满足,那么会进行整型升级:
int main() {
int a = 4;
float f = 4.5f;
double result = a + f; // 在计算 a + f 之前,a 会被转换为 float 类型,然后再执行加法运算
return 0;
}
为了计算a + f
,整数a
首先被转换为float
类型,以便与float
类型的变量f
进行加法运算。
5.4.3 优先级和求值的顺序
复杂表达式的求值顺序是由3个因素决定的:操作符的优先级、操作符的结合性以及操作符是否控制执行的顺序。
两个相邻的操作符哪个先执行取决于它们的优先级,如果两者的优先级相同,那么它们的执行顺序由它们的结合性决定。优先级表