C_表达式

C_表达式

表达式是用来计算某个值的公式,表达式可以用运算符进行连接。

C语言拥有异常丰富的运算符:算术运算符、赋值运算符、关系运算符、判等运算符、逻辑运算符、位运算符等等。

算数运算符

算术运算符包含 +, -, *, /, % ,分别表示加,减,乘,除,取余。其中 /% 需要注意:

  • / 的两个操作数都是整数,结果也是整数 (向零取整)。因此,1/2 的结果为 0 而不是 0.5。
  • +, -, *, /,可以用于浮点数,但 % 要求两个操作数都是整数。
  • %取余运算的结果可能为负数, i%j 的符号总是和 i 的符号相同,比如 -9 % 7 的值为 -2。

运算符的优先级和结合性

当表达式包含多个运算符时,C 语言采用优先级来解决歧义性问题。

表达式优先级

优先级* 的优先级高于 +

1
i + j * k 等价于 i + (j * k) 

当表达式中包含两个或者更多个具有相同优先级的运算符时,对比运算符的结合性进行运算。

左结合:运算符是从左向右结合的(二元运算符大多是左结合的)

1
2
i - j + k 等价于 (i - j) + k
i * j / k 等价于 (i * j) / k

右结合:运算符是从右向左结合的(一元运算符大多是右结合的)

1
-+i 等价于 -(+i)

赋值运算符

简单赋值

表达式 v = e 的作用是:求出表达式 e 的值,并把其赋值给变量 v。如:

1
2
3
i = 5; /* i is now 5 */
j = i; /* j is now 5 */
k = 10 * i + j; /* k is now 55 */

如果 ve 的类型不同,在赋值过程中会把 e 的值转换成 v 的类型:

1
2
3
4
int i;
float f;
i = 72.99f; /* i is now 72 */
f = 136; /* f is now 136.0 */

注意:赋值表达式 v = e 也有值,它的值等于赋值运算后 v 的值。

赋值运算符可以串联在一起,如:

1
i = j = k = 0;

由于赋值运算符是右结合的,上述表达式等价于:

1
i = (j = (k = 0));

复合赋值

利用变量原有的值去计算新的值。例如:

1
i = i + 2;

复合赋值运算符简写表达式:

1
2
3
4
5
i += 2; /* same as i = i + 2 */
i -= 2; /* same as i = i - 2 */
i *= 2; /* same as i = i * 2 */
i /= 2; /* same as i = i / 2 */
i %= 2; /* same as i = i % 2 */

复合赋值运算符也是右结合的:

1
i += j += k;

上面表达式等价于

1
i += (j += k);	

自增运算符和自减运算符

自增运算符++自减运算符--

++--运算符既可以作为前缀运算符 (如 ++i , --i ),也可以作为后缀运算符 (如 i++ , i--),不过表达式的值不同。

前缀表达式: ++i 的值为(i + 1),副作用是i自增

1
2
i = i + 1;
return i;

后缀表达式: i++ 的值为i,副作用是i自增

1
2
3
j = i;
i = i + 1;
return j;

注意

  1. ++i 意味着 “立即自增i”;而 i++ 意味着 “先用 i 的原始值,稍后再自增 i”。
  2. 后缀 ++ 和后缀 -- 比正号、负号的优先级高;前缀 ++ 和前缀 -- 与正号、负号的优先级相同。
  3. 运行速度从快到慢: ++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 是否位于 ik 之间,应该使用: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
2
3
4
unsigned short i, j;
i = 13; // 0000 1101
j = i << 2; // 0011 0100,
j = i >> 2; // 0000 0011

对无符号整数左移 j 位,相当于乘以 2^j (不发生溢出);对无符号整数右移 j 位,相当于除以 2^j

按位运算符

按位位运算符包含:按位取反,按位与,按位异或,按位或。其中按位取反是单目运算符,其余是双目运算符

  • ~i : 会对 i 的每一位进行取反操作,即 0 变成 1,1 变成 0。

  • i & j : 会对 ij 的每一位进行逻辑与运算。

  • i | j : 会对 ij 的每一位进行逻辑或运算。

  • i ^ j : 会对 ij 的每一位进行异或运算,如果对应的位相同则为0,如果对应的位不同则结果为 1。

提示: 千万不要将按位运算符 &|逻辑运算符 &&|| 混淆。

按位异或运算有良好的性质:

1
2
3
4
a ^ 0 = a;
a ^ a = 0;
a ^ b = b ^ a;
(a ^ b) ^ c = a ^ (b ^ c);

除了按位取反运算符外,其余位运算符都有对应的复合赋值运算符:

1
2
3
4
5
i = 21;j = 56;
i <<= 2;i >>= 2;
i &= j;
i |= j;
i ^= j;

常见操作

  1. 请判断一个整数是否为奇数

    1
    2
    3
    bool isOdd(int n) {
      return n & 0x1;
    }
  2. 如何判断一个非 0 整数是否为2的幂(1, 2, 4, 8, 16, …)

    1
    2
    3
    bool isPowerOf2(int n) {
    return (n & n - 1) == 0;
    }
  3. 给定一个值不为0的整数,请找出值为1的最低有效位 (last set bit)。

    1
    2
    3
    int lastSetBit(int n) {
    return n & -n;
    }
  4. 给定两个不同的整数 a 和 b,请交换它们两个的值 (要求不使用中间临时变量)。

    1
    2
    3
    a = a ^ b;
    b = a ^ b;
    a = a ^ b;
  5. 给一个 非空整数数组 nums,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

    1
    2
    3
    4
    5
    6
    7
    int singleNumber(int* nums, int numsSize) {
    int result = 0;
    for (int i = 0; i < numsSize; i++) {
    result ^= nums[i];
    }
    return result;
    }

    异或运算(^)的特性:

    1. 任何数与 0 进行异或运算的结果仍然是原来的数:a ^ 0 = a
    2. 任何数与自身进行异或运算的结果是0:a ^ a = 0
    3. 异或运算满足交换律:a ^ b = b ^ a
    4. 异或运算满足结合律:(a ^ b) ^ c = a ^ (b ^ c)

    推断出如下结果:

    • 如果一个数出现两次,那么与它异或两次的结果是0,即a ^ a = 0
    • 如果一个数出现一次,而其它所有数都出现两次,那么对所有数进行异或运算,相同的数会抵消,最终剩下的结果就是只出现一次的数。

    所以,通过对数组中的所有元素进行异或运算,最终得到的结果就是只出现一次的元素。

  6. 给一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。可以按任意顺序返回答案。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    void 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. 对数组中的所有元素进行异或运算,得到的结果就是两个只出现一次的元素的异或结果。
    2. 找到这个异或结果中为 1 的某一位,这个位说明两个只出现一次的元素在这一位上是不同的。假设这一位是第 k 位。
    3. 根据第 k 位是否为 1,将数组分成两组,一组是第 k 位为 1 的元素,另一组是第 k 位为 0 的元素。
    4. 分别对这两组元素进行异或运算,得到的结果就是只出现一次的两个元素。

条件运算符

条件运算符是一种特殊运算符,通常称为三元运算符。用于根据某个条件来选择两个值中的一个。语法如下:

1
condition ? value1 : value2

如果 condition 为真,则返回 value1,否则返回 value2。下面是一个简单的示例:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main() {
int x = 10;
int y = 20;
int max;
// 使用条件运算符选择最大值
max = (x > y) ? x : y;
printf("最大值是: %d", max);
return 0;
}

条件运算符的优点在于简洁性和表达能力,但是过度使用条件运算符会导致代码可读性降低。

当使用条件运算符时,注意:

  1. 运算符结合性:条件运算符是右结合的,意味着表达式 a ? b : c ? d : e 相当于 a ? b : (c ? d : e)
  2. 类型转换:条件运算符会进行类型转换以保证两个操作数具有相同的类型。两个操作数的类型不同,较低类型的操作数将被提升到较高类型,然后再进行运算。
  3. 求值顺序:条件运算符保证了先评估 condition,然后根据 condition 的结果来评估 value1 或 value2。但是并没有规定 value1 和 value2 的求值顺序,因此在表达式 (condition ? func1() : func2()) 中,func1()func2() 可能会在条件求值之前都被调用,也可能只调用一个。
  4. 嵌套使用:条件运算符可以嵌套使用,但是要注意保持代码的清晰度。过度嵌套会导致代码难以理解。
  5. 返回值:条件运算符本身也是一个表达式,它的返回值可以被赋给变量,也可以作为另一个表达式的一部分使用。