C_表达式

C_表达式
BarbecueC_表达式
表达式是用来计算某个值的公式,表达式可以用运算符进行连接。
C语言拥有异常丰富的运算符:算术运算符、赋值运算符、关系运算符、判等运算符、逻辑运算符、位运算符等等。
算数运算符
算术运算符包含 +
, -
, *
, /
, %
,分别表示加,减,乘,除,取余。其中 /
和 %
需要注意:
/
的两个操作数都是整数,结果也是整数 (向零取整)。因此,1/2
的结果为 0 而不是 0.5。+
,-
,*
,/
,可以用于浮点数,但%
要求两个操作数都是整数。%
取余运算的结果可能为负数,i%j
的符号总是和i
的符号相同,比如 -9 % 7 的值为 -2。
运算符的优先级和结合性
当表达式包含多个运算符时,C 语言采用优先级来解决歧义性问题。
优先级:*
的优先级高于 +
1 | i + j * k 等价于 i + (j * k) |
当表达式中包含两个或者更多个具有相同优先级的运算符时,对比运算符的结合性进行运算。
左结合:运算符是从左向右结合的(二元运算符大多是左结合的)
1 | i - j + k 等价于 (i - j) + k |
右结合:运算符是从右向左结合的(一元运算符大多是右结合的)
1 | -+i 等价于 -(+i) |
赋值运算符
简单赋值
表达式 v = e
的作用是:求出表达式 e
的值,并把其赋值给变量 v
。如:
1 | i = 5; /* i is now 5 */ |
如果 v
和 e
的类型不同,在赋值过程中会把 e
的值转换成 v
的类型:
1 | int i; |
注意:赋值表达式
v = e
也有值,它的值等于赋值运算后v
的值。
赋值运算符可以串联在一起,如:
1 | i = j = k = 0; |
由于赋值运算符是右结合的,上述表达式等价于:
1 | i = (j = (k = 0)); |
复合赋值
利用变量原有的值去计算新的值。例如:
1 | i = i + 2; |
复合赋值运算符简写表达式:
1 | i += 2; /* same as i = i + 2 */ |
复合赋值运算符也是右结合的:
1 | i += j += k; |
上面表达式等价于
1 | i += (j += k); |
自增运算符和自减运算符
自增运算符:++
,自减运算符:--
++
和--
运算符既可以作为前缀运算符 (如 ++i
, --i
),也可以作为后缀运算符 (如 i++
, i--
),不过表达式的值不同。
前缀表达式: ++i
的值为(i + 1)
,副作用是i
自增
1 | i = i + 1; |
后缀表达式: i++
的值为i
,副作用是i
自增
1 | j = i; |
注意
++i
意味着 “立即自增i”;而i++
意味着 “先用 i 的原始值,稍后再自增 i”。- 后缀
++
和后缀--
比正号、负号的优先级高;前缀++
和前缀--
与正号、负号的优先级相同。- 运行速度从快到慢:
++i
>i++
>i+=1
>i=i+1
关系运算符
逻辑表达式:用关系运算符,判等运算符和逻辑运算符来构建。逻辑表达式的值为 0
或者 1
(0表示false, 1表示true)。
关系运算符:包含 <
, >
, <=
, >=
。关系运算符的优先级低于算术运算符,并且是左结合的
1 | i + j < k - 1 等价于 (i + j) < (k - 1) |
注意:表达式
i < j < k
是合法的,但i < j < k
等价于(i < j) < k
,该表达式首先检测i
是否小于j
,然后用比较后产生的结果 (0 或者 1) 和k
进行比较。若要测试j
是否位于i
和k
之间,应该使用:i < j && j < k
。
判等运算符
判等运算符:包含 ==
, !=
。判等运算符的优先级低于关系运算符,是左结合的。
1 | i < j == j < k 等价于 (i < j) == (j < k) |
逻辑运算符
逻辑运算符:包含 &&
, ||
, !
。逻辑运算符把任何零值当作 false
,任何非零值当作 true
。
注意: &&
和 ||
会对操作数进行 “短路” 计算,操作符会首先计算左操作数的值,然后计算右操作数。
如果整个表达式的值可以由左操作数的值推导出来,将不会计算右操作数的值。如:
1 | (i != 0) && (j / i > 0) |
短路计算的好处是显而易见的,没有短路计算,上面的表达式会出现除零错误。
运算符 !
的优先级和正负号的优先级是相同的,而且是右结合的; &&
和 ||
的优先级低于关系运算符和判等运算符。
1 | i < j && k == m 等价于 (i < j) && (k == m) |
位运算符
位操作在编写系统程序 (包括编译器和操作系统)、加密程序、图形程序以及性能要求非常高的程序时,非常有用。
移位运算符
移位运算符可以通过将位向左或向右移动来变换整数的二进制表示。
左移运算符 <<
:如 i << j
,将 i
的位左移 j
位,在 i
的右边补 0
。
右移运算符 >>
:如 i >> j
,将 i
的位右移 j
位。 i
是无符号数或者非负值,则在左边补 0
, i
是有符号负值,会在左边补 1
。
Tips: 为了可移植性,最好仅对无符号数进行移位运算。
1 | unsigned short i, j; |
对无符号整数左移 j
位,相当于乘以 2^j
(不发生溢出);对无符号整数右移 j
位,相当于除以 2^j
。
按位运算符
按位位运算符包含:按位取反,按位与,按位异或,按位或。其中按位取反是单目运算符,其余是双目运算符。
~i
: 会对i
的每一位进行取反操作,即 0 变成 1,1 变成 0。i & j
: 会对i
和j
的每一位进行逻辑与运算。i | j
: 会对i
和j
的每一位进行逻辑或运算。i ^ j
: 会对i
和j
的每一位进行异或运算,如果对应的位相同则为0,如果对应的位不同则结果为 1。
提示: 千万不要将按位运算符
&
和|
与逻辑运算符&&
和||
混淆。
按位异或运算有良好的性质:
1 | a ^ 0 = a; |
除了按位取反运算符外,其余位运算符都有对应的复合赋值运算符:
1 | i = 21;j = 56; |
常见操作
请判断一个整数是否为奇数
1
2
3bool isOdd(int n) {
return n & 0x1;
}如何判断一个非 0 整数是否为2的幂(1, 2, 4, 8, 16, …)
1
2
3bool isPowerOf2(int n) {
return (n & n - 1) == 0;
}给定一个值不为0的整数,请找出值为1的最低有效位 (last set bit)。
1
2
3int lastSetBit(int n) {
return n & -n;
}给定两个不同的整数 a 和 b,请交换它们两个的值 (要求不使用中间临时变量)。
1
2
3a = a ^ b;
b = a ^ b;
a = a ^ b;给一个 非空整数数组 nums,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
1
2
3
4
5
6
7int singleNumber(int* nums, int numsSize) {
int result = 0;
for (int i = 0; i < numsSize; i++) {
result ^= nums[i];
}
return result;
}异或运算(^)的特性:
- 任何数与 0 进行异或运算的结果仍然是原来的数:
a ^ 0 = a
- 任何数与自身进行异或运算的结果是0:
a ^ a = 0
- 异或运算满足交换律:
a ^ b = b ^ a
- 异或运算满足结合律:
(a ^ b) ^ c = a ^ (b ^ c)
推断出如下结果:
- 如果一个数出现两次,那么与它异或两次的结果是0,即
a ^ a = 0
。 - 如果一个数出现一次,而其它所有数都出现两次,那么对所有数进行异或运算,相同的数会抵消,最终剩下的结果就是只出现一次的数。
所以,通过对数组中的所有元素进行异或运算,最终得到的结果就是只出现一次的元素。
- 任何数与 0 进行异或运算的结果仍然是原来的数:
给一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。可以按任意顺序返回答案。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21void findSingleNumbers(int* nums, int numsSize, int* single1, int* single2) {
// Step 1: 对数组中的所有元素进行异或运算,得到两个只出现一次的元素的异或结果
int xorResult = 0;
for (int i = 0; i < numsSize; i++) {
xorResult ^= nums[i];
}
// Step 2: 找到异或结果中为 1 的某一位
int diffBit = xorResult & (-xorResult); // 使用补码操作得到最低位的 1
// Step 3: 根据第 k 位是否为 1,将数组分成两组,分别对这两组元素进行异或运算
*single1 = 0;
*single2 = 0;
for (int i = 0; i < numsSize; i++) {
if (nums[i] & diffBit) {
*single1 ^= nums[i];
} else {
*single2 ^= nums[i];
}
}
}具体的步骤如下:
- 对数组中的所有元素进行异或运算,得到的结果就是两个只出现一次的元素的异或结果。
- 找到这个异或结果中为 1 的某一位,这个位说明两个只出现一次的元素在这一位上是不同的。假设这一位是第 k 位。
- 根据第 k 位是否为 1,将数组分成两组,一组是第 k 位为 1 的元素,另一组是第 k 位为 0 的元素。
- 分别对这两组元素进行异或运算,得到的结果就是只出现一次的两个元素。
条件运算符
条件运算符是一种特殊运算符,通常称为三元运算符。用于根据某个条件来选择两个值中的一个。语法如下:
1 | condition ? value1 : value2 |
如果 condition 为真,则返回 value1,否则返回 value2。下面是一个简单的示例:
1 |
|
条件运算符的优点在于简洁性和表达能力,但是过度使用条件运算符会导致代码可读性降低。
当使用条件运算符时,注意:
- 运算符结合性:条件运算符是右结合的,意味着表达式
a ? b : c ? d : e
相当于a ? b : (c ? d : e)
。 - 类型转换:条件运算符会进行类型转换以保证两个操作数具有相同的类型。两个操作数的类型不同,较低类型的操作数将被提升到较高类型,然后再进行运算。
- 求值顺序:条件运算符保证了先评估 condition,然后根据 condition 的结果来评估 value1 或 value2。但是并没有规定 value1 和 value2 的求值顺序,因此在表达式
(condition ? func1() : func2())
中,func1()
和func2()
可能会在条件求值之前都被调用,也可能只调用一个。 - 嵌套使用:条件运算符可以嵌套使用,但是要注意保持代码的清晰度。过度嵌套会导致代码难以理解。
- 返回值:条件运算符本身也是一个表达式,它的返回值可以被赋给变量,也可以作为另一个表达式的一部分使用。