6、运算符与表达式:算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符、逗号运算符
运算符这东西,说白了就是C语言里的「动作指令」。你写个变量,它自己不会动,得靠运算符去操作它。我刚开始学C的时候,觉得运算符不就是加减乘除嘛,有啥好学的?结果一写代码就翻车——优先级搞错、类型转换踩坑、位运算看得一脸懵。嗯,今天咱们就把这六类运算符彻底捋一遍。
核心观点:运算符是C语言的「动词」,表达式是「句子」。你写的每一行代码,本质上都是在用运算符组装表达式。
6.1 算术运算符:不只是加减乘除
算术运算符有五个:+、-、*、/、%。前四个大家都熟,重点说两个坑。
第一个坑:整数除法。 你写 5 / 2,结果是多少?2.5?错,是2。因为两个整数相除,结果还是整数,直接截断小数部分。我当年写一个计算平均分的程序,死活算不对,查了半天发现是 sum / count 两个都是int,小数全丢了。
避坑指南: 如果你想要浮点数结果,至少让其中一个操作数是浮点型:5 / 2.0 或者 (float)5 / 2。
第二个坑:取模运算符 %。 它只能用于整数,不能用于浮点数。而且结果的符号跟被除数一致。举个例子:
int a = 7 % 3; // a = 1
int b = -7 % 3; // b = -1
int c = 7 % -3; // c = 1
为什么会这样?因为C语言规定:a % b 的结果等于 a - (a / b) * b。你想想看,-7 / 3 在C里是 -2(向零取整),所以 -7 - (-2)*3 = -1。嗯,这个规则在面试里经常被问到。
6.2 关系运算符:比较大小,结果只有0或1
关系运算符有六个:>、<、>=、<=、==、!=。它们的结果只有两种:真(1)或假(0)。
我个人习惯在写条件判断时,把常量写在左边:if (5 == x) 而不是 if (x == 5)。为什么?因为如果你手滑写成 if (x = 5),编译器不会报错,但逻辑就全错了。而 if (5 = x) 编译器会直接报错,因为不能给常量赋值。这个习惯救过我很多次。
小技巧: 浮点数比较不要直接用 ==,因为精度问题。用 fabs(a - b) < 1e-6 这种形式更靠谱。
6.3 逻辑运算符:与或非,短路求值
逻辑运算符有三个:&&(与)、||(或)、!(非)。它们操作的是「真/假」概念,但操作数可以是任意标量类型——0为假,非0为真。
重点来了:短路求值。 对于 &&,如果左边为假,右边就不执行了。对于 ||,如果左边为真,右边也不执行了。这个特性在实际项目中非常有用。
我记得有一次写嵌入式代码,要判断一个指针是否有效,再访问它的成员:
if (ptr != NULL && ptr->value > 10) {
// 安全访问
}
如果 ptr 是 NULL,左边为假,右边根本不会执行,也就不会发生空指针访问。这就是短路求值的妙用。
避坑指南: 不要依赖短路求值中的副作用。比如 if (a++ > 0 && b++ > 0),如果 a 不满足条件,b 就不会自增。这种代码可读性差,还容易出bug。
6.4 位运算符:直接操作二进制位
位运算符有六个:&(按位与)、|(按位或)、^(按位异或)、~(按位取反)、<<(左移)、>>(右移)。
位运算是C语言的特色之一,在嵌入式、网络协议、加密算法中经常用到。我做过一个通信协议解析的项目,一个字节里塞了8个标志位,不用位运算根本没法处理。
常用场景举例:
| 操作 | 表达式 | 说明 |
|---|---|---|
| 置位(设置第n位为1) | x |= (1 << n) |
将第n位设为1,其他位不变 |
| 清零(设置第n位为0) | x &= ~(1 << n) |
将第n位设为0,其他位不变 |
| 取反(翻转第n位) | x ^= (1 << n) |
第n位取反,其他位不变 |
| 判断第n位是否为1 | if (x & (1 << n)) |
结果为非0表示第n位为1 |
注意: 左移 << 右边补0。右移 >> 对于无符号数左边补0,对于有符号数则取决于编译器(通常是算术右移,补符号位)。写可移植代码时,右移有符号数要小心。
6.5 赋值运算符:不只是等号
赋值运算符 = 是C语言里最常用的运算符。但很多人不知道,赋值表达式本身也有值——就是赋完后的值。所以你可以写:
int a, b, c;
a = b = c = 10; // 从右向左结合,c=10, b=10, a=10
复合赋值运算符 +=、-=、*=、/=、%=、&=、|=、^=、<<=、>>= 是简写形式。比如 x += 5 等价于 x = x + 5。我个人习惯用复合赋值,因为代码更简洁,而且编译器有时能生成更高效的代码。
小技巧: 复合赋值运算符的优先级很低,只比逗号运算符高。所以 x *= y + 1 等价于 x = x * (y + 1),而不是 x = x * y + 1。
6.6 逗号运算符:把多个表达式串起来
逗号运算符 , 可能是最被低估的运算符。它的规则很简单:从左到右依次执行每个表达式,整个表达式的值等于最后一个表达式的值。
int a = (1, 2, 3); // a = 3
int b = (a++, a + 5); // 先执行 a++,然后 a+5 作为结果
逗号运算符最常用的场景是 for 循环里初始化多个变量:
for (int i = 0, j = 10; i < j; i++, j--) {
// 同时从两端向中间遍历
}
嗯,这里要注意:函数参数列表里的逗号不是逗号运算符,那是参数分隔符。只有作为表达式出现的逗号才是运算符。
6.7 优先级和结合性:面试必考
运算符多了,优先级就容易乱。我整理了一张常用优先级表,从上到下优先级递减:
| 优先级 | 运算符 | 结合性 |
|---|---|---|
| 1(最高) | () [] . -> |
左到右 |
| 2 | ! ~ ++ -- -(负号) *(指针) &(取地址) |
右到左 |
| 3 | * / % |
左到右 |
| 4 | + - |
左到右 |
| 5 | << >> |
左到右 |
| 6 | < > <= >= |
左到右 |
| 7 | == != |
左到右 |
| 8 | &(按位与) |
左到右 |
| 9 | ^ |
左到右 |
| 10 | | |
左到右 |
| 11 | && |
左到右 |
| 12 | || |
左到右 |
| 13 | ?:(三目运算符) |
右到左 |
| 14 | = += -= 等赋值 |
右到左 |
| 15(最低) | , |
左到右 |
避坑指南: 不要完全依赖优先级表。遇到复杂的表达式,多用括号明确意图。代码是写给人看的,不是写给编译器看的。我见过太多因为优先级搞错而出的bug了。
6.8 类型转换:隐式转换与强制转换
不同类型的数据在一起运算时,C语言会自动进行类型转换。规则是:向精度更高的类型转换。比如 int + double,int会先转成double再运算。
隐式转换的规则可以概括为:
- 整型提升:小于int的类型(char、short)先转成int
- 有符号与无符号:有符号转无符号(这个容易出问题)
- 浮点与整型:整型转浮点
我曾经在项目中遇到过一个问题:
unsigned int a = 10;
int b = -20;
if (a + b > 0) {
printf("结果是正数\n");
} else {
printf("结果是负数\n");
}
你猜输出什么?结果是「结果是正数」。因为 a + b 中,b被转成了unsigned int,变成了一个很大的正数。这就是隐式转换的坑。
核心建议: 涉及有符号和无符号混用的运算,一定要显式强制转换,或者干脆避免混用。强制转换用 (类型名)表达式 的形式。
6.9 自增自减运算符:前置和后置的区别
++ 和 -- 是C语言里最容易被误解的运算符。前置版本(++i)先自增再取值,后置版本(i++)先取值再自增。
int i = 5;
int a = ++i; // a = 6, i = 6
int b = i++; // b = 6, i = 7
我个人习惯在不需要原值的情况下,用前置自增。因为后置自增需要保存原值,理论上多一次拷贝操作。虽然现代编译器会优化,但写代码时养成好习惯总没错。
小技巧: 不要在同一个表达式里多次修改同一个变量。比如 i = i++ + ++i 这种代码,行为是未定义的,不同编译器结果可能不同。面试官看到这种代码,基本就给你打叉了。
6.10 sizeof运算符:不是函数,是运算符
sizeof 是运算符,不是函数。它返回变量或类型所占的字节数,类型是 size_t(通常是unsigned long)。
int a;
size_t size = sizeof a; // 变量可以不加括号
size_t size2 = sizeof(int); // 类型必须加括号
sizeof 在计算数组长度时特别有用:
int arr[10];
size_t len = sizeof(arr) / sizeof(arr[0]); // len = 10
嗯,这里要注意:sizeof 是在编译时计算的,不会对表达式求值。所以 sizeof(a++) 不会让 a 自增。
运算符是C语言的基石,每个C程序员每天都在用。但越是基础的东西,越容易在细节上翻车。我建议你把这张优先级表存下来,写代码时多留个心眼。遇到复杂的表达式,拆成几步写,可读性比炫技重要得多。
好了,运算符这块就聊到这儿。记住一句话:代码是写给人看的,顺便让机器执行。多用括号,少写歧义代码,这是资深工程师的基本素养。