运算符与表达式 (Operators & Expressions)
章节概述
运算符是程序的”动词”——它们连接数据(操作数)产生新的数据。C 语言以丰富的运算符著称,从基础的加减乘除到位运算、从逻辑判断到类型查询,几乎所有操作都可以通过运算符简洁表达。本章全面覆盖算术运算符、关系运算符、逻辑运算符、位运算符、三元条件运算符 ?:、sizeof 运算符、逗号运算符、复合赋值运算符,以及完整的运算符优先级表。掌握运算符不仅是写出正确代码的前提,更是阅读底层性能优化代码、理解嵌入式 C 编程中位操作的核心能力。
核心主题:运算符本质上是一类特殊的”函数”——每个运算符对应一条或多条 CPU 指令。理解运算符在汇编层面的映射是理解 C 语言高效性的关键。本章多处链接到 ASM 教程帮助你建立”C 运算符 → 汇编指令”的心智模型。
第一节:算术运算符与表达式求值
1.1 基本算术运算符
| 运算符 | 名称 | 示例 | 结果 |
|---|---|---|---|
+ | 加法 | 5 + 3 | 8 |
- | 减法 | 5 - 3 | 2 |
* | 乘法 | 5 * 3 | 15 |
/ | 除法 | 5 / 2 | 2(整数除法) |
% | 取模(余数) | 5 % 2 | 1 |
+(一元) | 正号 | +5 | 5 |
-(一元) | 负号 | -5 | -5 |
#include <stdio.h>
int main() {
int a = 17, b = 5;
printf("加法: %d + %d = %d\n", a, b, a + b);
printf("减法: %d - %d = %d\n", a, b, a - b);
printf("乘法: %d * %d = %d\n", a, b, a * b);
printf("除法: %d / %d = %d\n", a, b, a / b); // 整数除法 → 3
printf("取模: %d %% %d = %d\n", a, b, a % b); // 余数 → 2
// 浮点除法
double x = 17.0, y = 5.0;
printf("浮点除法: %.1f / %.1f = %.6f\n", x, y, x / y);
// 一元运算符
printf("取负: -( %d ) = %d\n", b, -b);
return 0;
}1.2 整数除法的陷阱
#include <stdio.h>
int main() {
// 陷阱1:整数除法截断(向零取整)
printf(" 5 / 2 = %d\n", 5 / 2); // 2(不是 2.5)
printf("-5 / 2 = %d\n", -5 / 2); // -2(C99 以前是实现定义的!)
// C99 起明确规定:向零取整
// 陷阱2:取模结果的符号
printf(" 5 %% 2 = %d\n", 5 % 2); // 1
printf("-5 %% 2 = %d\n", -5 % 2); // -1(结果符号与被除数相同,C99 起)
printf(" 5 %% -2 = %d\n", 5 % -2); // 1
// 陷阱3:除法优先级问题
int result = 10 / 2 * 5; // (10/2)*5 = 25,不是 10/(2*5)=1
printf("10 / 2 * 5 = %d\n", result);
// 浮点陷阱
double d1 = 5 / 2; // 整数除法 2,然后转为 2.0
double d2 = 5.0 / 2; // 浮点除法
double d3 = (double)5 / 2; // 强制浮点除法
printf("5/2 = %f, 5.0/2 = %f, (double)5/2 = %f\n", d1, d2, d3);
return 0;
}1.3 取模运算 % 的应用
#include <stdio.h>
#include <stdbool.h>
int main() {
// 判断奇偶
int n = 7;
printf("%d 是%s数\n", n, (n % 2 == 0) ? "偶" : "奇");
// 限制范围(取个位数)
printf("12345 的个位: %d\n", 12345 % 10); // 5
printf("12345 的最后两位: %d\n", 12345 % 100); // 45
// 循环索引(0~4 循环)
for (int i = 0; i < 10; i++) {
printf("%d ", i % 5);
}
printf("\n"); // 0 1 2 3 4 0 1 2 3 4
// 防止数组越界
int pos = 12;
int arr_size = 8;
int safe_pos = pos % arr_size;
printf("安全位置: %d\n", safe_pos); // 4
return 0;
}1.4 汇编视角:加法/乘法如何被翻译
// C 代码
int compute(int a, int b) {
int sum = a + b;
int diff = a - b;
int prod = a * b;
return sum + diff + prod;
}对应的 x86-64 汇编(简化):
compute:
lea eax, [rdi+rsi] ; eax = a + b (lea 用于高效加法)
mov edx, edi
sub edx, esi ; edx = a - b
add eax, edx ; eax = sum + diff
imul edx, esi ; edx = a * b (有符号乘法)
add eax, edx ; eax += prod
ret
lea(Load Effective Address)指令常被编译器用来做”不要结果的加法”——因为 CPU 的地址生成器比 ALU 更快。详细的汇编指令请参考 。
小节练习
选择题 1
C99 标准下,
-11 % 3的结果是?
A. 2
B. -2
C. 1
D. 未定义
点击查看答案 解析: C99 起规定取模运算的结果符号与被除数(dividend)相同。
-11 % 3: -11 / 3 = -3(向零取整),-11 - (-3 × 3) = -11 + 9 = -2。正确答案: B
判断题 1
5 / 2在 C 语言中的结果是 2.5。 ( )
正确
错误
点击查看答案 解析: 两个整数相除执行整数除法,结果为 2(小数部分被截断)。要得到 2.5,至少需要一个操作数类型为浮点类型(如
5.0 / 2或(double)5 / 2)。答案: 错误
第二节:关系运算符与条件判断
2.1 六种关系运算符
| 运算符 | 名称 | 示例 |
|---|---|---|
== | 等于 | a == b |
!= | 不等于 | a != b |
> | 大于 | a > b |
< | 小于 | a < b |
>= | 大于等于 | a >= b |
<= | 小于等于 | a <= b |
关系运算符返回 int 类型:1(真)或 0(假)。
#include <stdio.h>
int main() {
int a = 10, b = 20;
printf("a == b: %d\n", a == b); // 0 (false)
printf("a != b: %d\n", a != b); // 1 (true)
printf("a > b: %d\n", a > b); // 0
printf("a < b: %d\n", a < b); // 1
printf("a >= b: %d\n", a >= b); // 0
printf("a <= b: %d\n", a <= b); // 1
// 关系运算符可以在表达式中直接使用
int result = (a < b) * 100; // 1 * 100 = 100
printf("result = %d\n", result);
return 0;
}2.2 == vs = —— C 语言最著名的陷阱
#include <stdio.h>
int main() {
int x = 5;
// 经典 bug:if (x = 10) 始终为真!
// 因为 = 是赋值运算符,返回赋的值(10 ≠ 0 → true)
if (x = 10) { // 本意是 x == 10
printf("x is 10? Actually x = %d\n", x);
}
// 防御措施1:常量放左边
if (10 == x) { // 如果误写成 10 = x,编译错误!
printf("Good practice\n");
}
// 防御措施2:启用 -Wall 编译选项
// gcc -Wall 会对 if (x = 10) 产生警告
// 故意在条件中做赋值(合法但需标记意图)
int ch;
while ((ch = getchar()) != EOF) { // 外层括号抑制警告
// 读取字符直到 EOF
}
return 0;
}2.3 汇编视角:条件跳转
C 代码: if (a > b) { ... }
汇编:
cmp eax, ebx ; 比较 a 和 b(实际是 sub,但不保存结果)
jle .Lskip ; 如果 a <= b 则跳转(不大于即跳过)
... ; a > b 时执行的代码
.Lskip:
cmp指令本质上执行减法a - b,但并不保存结果——它只设置标志寄存器(flags)。随后的条件跳转指令(jg、jge、je、jne等)根据标志寄存器状态决定是否跳转。详见 。
小节练习
选择题 1
以下代码中,哪个比较符的结果为 1(真)?
char ch = 'a'; int result = (ch >= 'a' && ch <= 'z');
A.
ch >= 'a'为真,ch <= 'z'为假B.
result的值为 1C.
result的值为 ‘a’D. 编译错误:不能用
&&组合 char点击查看答案 解析:
'a'的 ASCII 值为 97,'z'为 122。ch为 97,同时满足>= 97和<= 122,所以结果为 1(true)。正确答案: B
第三节:逻辑运算符与短路求值
3.1 三种逻辑运算符
| 运算符 | 名称 | 示例 | 说明 |
|---|---|---|---|
&& | 逻辑与(AND) | a && b | 两侧都为真才为真 |
|| | 逻辑或(OR) | a || b | 至少一侧为真即为真 |
! | 逻辑非(NOT) | !a | 真变假,假变真 |
#include <stdio.h>
int main() {
int a = 1, b = 0;
printf("a && b = %d\n", a && b); // 1 && 0 = 0
printf("a || b = %d\n", a || b); // 1 || 0 = 1
printf("!a = %d\n", !a); // !1 = 0
printf("!b = %d\n", !b); // !0 = 1
// C 语言中,0 是 false,非 0 是 true
printf("42 && 1 = %d\n", 42 && 1); // 1
printf("-1 || 0 = %d\n", -1 || 0); // 1
return 0;
}3.2 短路求值(Short-circuit Evaluation)
&& 和 || 具有短路特性:一旦结果确定,剩余的表达式不再求值。
#include <stdio.h>
int test1() {
printf("test1() called\n");
return 1;
}
int test2() {
printf("test2() called\n");
return 0;
}
int test3() {
printf("test3() called\n");
return 1;
}
int main() {
// && 短路:左侧为 0 → 直接返回 0,不计算右侧
printf("=== 0 && test1() ===\n");
0 && test1(); // test1() 不会被调用!
// && 正常:左侧为 1 → 继续计算右侧
printf("=== 1 && test2() ===\n");
1 && test2();
// || 短路:左侧为 1 → 直接返回 1,不计算右侧
printf("=== 1 || test3() ===\n");
1 || test3(); // test3() 不会被调用!
// 利用短路做安全检查
int *p = NULL;
// p != NULL 为假 → 不计算 *p,避免空指针解引用!
if (p != NULL && *p > 0) {
printf("positive\n");
}
return 0;
}3.3 逻辑运算符与位运算符的区别
| 特性 | && / ||(逻辑) | & / |(位) |
|------|-------------------|-----------------|
| 操作数 | 任何标量类型 | 整数类型 |
| 返回 | 0 或 1 | 按位运算结果 |
| 短路 | 是 | 否(两侧都计算) |
| 常见错误 | — | 把 & 当 && 用 |
int a = 2, b = 1;
printf("a && b = %d\n", a && b); // 1 (true)
printf("a & b = %d\n", a & b); // 0 (位与:10 & 01 = 00)
// 2 & 1 虽然非 0,但恰好是 0,这与 a && b 可能不同!小节练习
选择题 1
以下代码中哪个函数调用不会被短路特性跳过?
int x = 0, y = 1; int r = (x != 0) && (y++ > 0); printf("y = %d\n", y);
A. y++ 会被计算
B. y++ 不会被计算
C. 编译错误
D. 取决于编译器
点击查看答案 解析:
(x != 0)为假(0),短路求值使&&右侧表达式(y++ > 0)被跳过,y仍为 1。这是短路求值的经典行为。正确答案: B
第四节:位运算符 —— C 语言底层操作的利器
4.1 六种位运算符
| 运算符 | 名称 | 示例 | 运算规则 |
|---|---|---|---|
& | 按位与(AND) | a & b | 两位都为 1 才得 1 |
| | 按位或(OR) | a | b | 任一位为 1 就得 1 |
^ | 按位异或(XOR) | a ^ b | 两位不同得 1,相同得 0 |
~ | 按位取反(NOT) | ~a | 每位 0→1,1→0 |
<< | 左移 | a << n | 左移 n 位,右侧补 0 |
>> | 右移 | a >> n | 右移 n 位,左侧填充因类型而异 |
#include <stdio.h>
// 打印二进制表示
void print_binary(unsigned int n) {
int bits = sizeof(n) * 8;
for (int i = bits - 1; i >= 0; i--) {
printf("%d", (n >> i) & 1);
if (i % 8 == 0) printf(" ");
}
printf("\n");
}
int main() {
unsigned int a = 0b11001100; // 204
unsigned int b = 0b10101010; // 170
printf("a = "); print_binary(a);
printf("b = "); print_binary(b);
printf("a & b = "); print_binary(a & b); // 10001000 (136)
printf("a | b = "); print_binary(a | b); // 11101110 (238)
printf("a ^ b = "); print_binary(a ^ b); // 01100110 (102)
printf("~a = "); print_binary(~a); // 00110011
printf("a << 2 = "); print_binary(a << 2); // 00110000 (左移,×4)
printf("a >> 2 = "); print_binary(a >> 2); // 00110011 (右移,÷4)
return 0;
}4.2 位运算的经典应用
#include <stdio.h>
int main() {
unsigned int x = 0b10101100;
// 1. 设置某一位(set bit):OR 1
unsigned int sets = x | (1 << 3); // 设置第 3 位
printf("设置第3位后: 0x%02X\n", sets);
// 2. 清除某一位(clear bit):AND 0
unsigned int clears = x & ~(1 << 5); // 清除第 5 位
printf("清除第5位后: 0x%02X\n", clears);
// 3. 翻转某一位(toggle bit):XOR 1
unsigned int toggle = x ^ (1 << 2); // 翻转第 2 位
printf("翻转第2位后: 0x%02X\n", toggle);
// 4. 检查某一位(test bit)
if (x & (1 << 7)) {
printf("第 7 位是 1\n");
} else {
printf("第 7 位是 0\n");
}
// 5. 提取最低位的 1
unsigned int lowest = x & -x;
printf("最低位的1: 0x%02X\n", lowest);
// 6. 判断是否为 2 的幂
unsigned int n = 64;
if (n > 0 && (n & (n - 1)) == 0) {
printf("%u 是 2 的幂\n", n);
}
// 7. 交换两个数(不借助临时变量)
int a = 5, b = 9;
a ^= b; b ^= a; a ^= b;
printf("交换后: a=%d, b=%d\n", a, b);
// 8. 快速乘除 2 的幂
printf("10 * 8 = %d, 10 << 3 = %d\n", 10 * 8, 10 << 3);
printf("100 / 4 = %d, 100 >> 2 = %d\n", 100 / 4, 100 >> 2);
return 0;
}4.3 有符号整数的右移陷阱
#include <stdio.h>
int main() {
int positive = 16;
int negative = -16;
// 无符号右移 → 逻辑右移(左侧补 0)
unsigned int u = (unsigned int)negative;
printf("unsigned -16 >> 2: %u\n", u >> 2); // 很大的正数
// 有符号右移 → 算术右移(左侧补符号位)—— 平台定义!
printf("signed -16 >> 2: %d\n", negative >> 2); // 通常是 -4
// C 标准规定:signed 右移是"实现定义"行为
// 大多数现代编译器使用算术右移(保持符号)
return 0;
}4.4 汇编视角:位运算就是单条指令
; C: a = b & c;
and eax, ebx ; eax = eax & ebx (1 条指令!)
; C: a = b | c;
or eax, ebx
; C: a = b ^ c;
xor eax, ebx
; C: a = ~b;
not eax
; C: a = b << 3;
shl eax, 3 ; 左移 3 位
; C: a = b >> 2;
sar eax, 2 ; 算术右移 2 位(保持符号)
; shr eax, 2 ; 逻辑右移(用于 unsigned)位运算是 C 语言中最接近硬件的操作——每个位运算符都对应一条 CPU 指令,执行速度极快。这也是为什么嵌入式 C 编程和 Linux 内核中大量使用位运算。
小节练习
选择题 1
unsigned int类型的变量执行右移时,左侧填充什么?
A. 0
B. 1
C. 符号位
D. 不确定
点击查看答案 解析: 无符号整数的右移是逻辑右移,左侧始终填充 0。有符号整数的右移是算术右移,左侧填充符号位(大多数平台实现),但这是"实现定义"行为。
正确答案: A
第五节:特殊运算符
5.1 三元条件运算符 ?:
#include <stdio.h>
int main() {
int score = 85;
// 语法:condition ? value_if_true : value_if_false
const char *grade = score >= 60 ? "及格" : "不及格";
printf("成绩: %s\n", grade);
// 嵌套使用(注意可读性)
const char *level = score >= 90 ? "优秀" :
score >= 75 ? "良好" :
score >= 60 ? "及格" : "不及格";
printf("等级: %s\n", level);
// 直接用在 printf 中
printf("结果: %s\n", score % 2 == 0 ? "偶数" : "奇数");
// 取两数中较大值
int a = 10, b = 20;
int max = (a > b) ? a : b;
printf("max = %d\n", max);
// 汇编层面:条件运算符等价于 if/else 分支
// 但有时编译器会生成条件传送指令(cmov)避免分支
return 0;
}5.2 sizeof 运算符
#include <stdio.h>
int main() {
// sizeof 是编译时运算符(对于非 VLA 类型)
printf("sizeof(int) = %zu\n", sizeof(int));
printf("sizeof(int*) = %zu\n", sizeof(int*));
int x = 42;
printf("sizeof(x) = %zu\n", sizeof(x)); // 不需要括号(变量)
printf("sizeof x = %zu\n", sizeof x); // 可以省略括号
// 数组
int arr[10];
printf("sizeof(arr) = %zu\n", sizeof(arr)); // 40 (10 × 4)
printf("元素数 = %zu\n", sizeof(arr) / sizeof(arr[0])); // 10
// 指针陷阱
void print_array_size(int *p) {
printf("sizeof(p) = %zu\n", sizeof(p)); // 8(指针大小,不是数组!)
}
print_array_size(arr);
// struct
struct Point { int x; int y; };
printf("sizeof(Point) = %zu\n", sizeof(struct Point)); // 8
return 0;
}
sizeof不是函数,是编译时运算符。对于 VLA(变长数组),sizeof在运行时计算数组大小——这是sizeof唯一的运行时场景。
5.3 逗号运算符 ,
#include <stdio.h>
int main() {
// 逗号运算符:依次计算左右表达式,返回最右侧的值
int x, y;
// 在 for 循环中的应用
for (int i = 0, j = 10; i < 5; i++, j--) {
printf("i=%d, j=%d\n", i, j);
}
// 作为普通表达式
int result = (x = 1, y = 2, x + y); // result = 3
printf("result = %d\n", result);
// 注意:函数参数中的逗号不是逗号运算符!
printf("不是逗号运算符: ");
printf("%d %d\n", 1, 2); // 这里的逗号是参数分隔符
// 要强制使用逗号运算符需要加括号
printf("逗号运算符: %d\n", (1, 2)); // 输出 2
return 0;
}5.4 复合赋值运算符
| 运算符 | 等价于 | 说明 |
|---|---|---|
+= | x = x + a | 先加再赋值 |
-= | x = x - a | 先减再赋值 |
*= | x = x * a | 先乘再赋值 |
/= | x = x / a | 先除再赋值 |
%= | x = x % a | 先取模再赋值 |
&= | x = x & a | 先按位与再赋值 |
|= | x = x | a | 先按位或再赋值 |
^= | x = x ^ a | 先按位异或再赋值 |
<<= | x = x << a | 先左移再赋值 |
>>= | x = x >> a | 先右移再赋值 |
#include <stdio.h>
int main() {
int x = 10;
x += 5; printf("x += 5 → %d\n", x); // 15
x -= 3; printf("x -= 3 → %d\n", x); // 12
x *= 2; printf("x *= 2 → %d\n", x); // 24
x /= 5; printf("x /= 5 → %d\n", x); // 4
x %= 3; printf("x %%= 3 → %d\n", x); // 1
x <<= 3; printf("x <<= 3 → %d\n", x); // 8
x >>= 1; printf("x >>= 1 → %d\n", x); // 4
x |= 3; printf("x |= 3 → %d\n", x); // 7
x &= 5; printf("x &= 5 → %d\n", x); // 5
x ^= 3; printf("x ^= 3 → %d\n", x); // 6
return 0;
}小节练习
选择题 1
int a = 5; a += a *= 2;执行后a的值是?
A. 15
B. 20
C. 10
D. 不确定
点击查看答案 解析: 复合赋值运算符是右结合性。先计算
a *= 2:a = 10,返回 10;然后a += 10:a = 20。等效于a = a + (a = a * 2)。正确答案: B
第六节:运算符优先级完整表
6.1 C 语言运算符优先级表(从高到低)
| 优先级 | 运算符 | 结合性 | 说明 |
|---|---|---|---|
| 1 | () [] . -> ++(后缀) --(后缀) | 左→右 | 函数调用/数组下标/成员访问 |
| 2 | ! ~ +(一元) -(一元) ++(前缀) --(前缀) *(解引用) &(取地址) sizeof (type) | 右→左 | 一元运算符 |
| 3 | * / % | 左→右 | 乘除取模 |
| 4 | + - | 左→右 | 加减 |
| 5 | << >> | 左→右 | 位移 |
| 6 | < <= > >= | 左→右 | 关系比较 |
| 7 | == != | 左→右 | 相等比较 |
| 8 | & | 左→右 | 按位与 |
| 9 | ^ | 左→右 | 按位异或 |
| 10 | | | 左→右 | 按位或 |
| 11 | && | 左→右 | 逻辑与 |
| 12 | || | 左→右 | 逻辑或 |
| 13 | ?: | 右→左 | 条件运算符 |
| 14 | = += -= *= /= %= &= ^= |= <<= >>= | 右→左 | 赋值 |
| 15 | , | 左→右 | 逗号运算符 |
6.2 优先级常见陷阱
#include <stdio.h>
int main() {
// 陷阱1:* 和 & 的优先级
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
// *p++ 等价于 *(p++),不是 (*p)++
// 后缀 ++ 优先级高于解引用 *
// 陷阱2:位运算优先级低于比较运算
int x = 1;
// x & 0xFF == 0 被解析为 x & (0xFF == 0)
// 正确写法:(x & 0xFF) == 0
// 陷阱3:<< 优先级低于 +
// 1 << 2 + 1 = 1 << (2 + 1) = 1 << 3 = 8
// 正确写法:(1 << 2) + 1 = 4 + 1 = 5
printf("1 << 2 + 1 = %d\n", 1 << 2 + 1); // 8 (不是 5!)
printf("(1 << 2) + 1 = %d\n", (1 << 2) + 1); // 5
// 陷阱4:&& 优先级高于 ||
// a || b && c 等价于 a || (b && c)
printf("0 || 0 && 1 = %d\n", 0 || 0 && 1); // 0
printf("(0 || 0) && 1 = %d\n", (0 || 0) && 1); // 0
// 黄金法则:不确定优先级时,加括号!
// 括号不仅确保正确性,还提高可读性
return 0;
}小节练习
选择题 1
1 + 2 * 3的结果是?
A. 7
B. 9
C. 11
D. 不确定
点击查看答案 解析: 乘法优先级高于加法,所以
2 * 3 = 6先计算,然后1 + 6 = 7。正确答案: A
选择题 2
*p++等价于什么?
A.
(*p)++(取出值后自增)B.
*(p++)(先取 *p,再 p 后移)C.
*(++p)(p 先后移,再取值)D.
&p++点击查看答案 解析: 后缀
++优先级高于*,所以*p++解析为*(p++):先取出*p的值,然后将指针p指向下一个元素。这是 C 语言中遍历数组的经典写法。正确答案: B
章节测试
一、判断题(正确选,错误选)
判断题 1
C 语言中,
/运算符对整数和浮点数操作的行为完全相同。 ( )
正确
错误
点击查看答案 解析: 整数除法截断小数部分(向零取整),浮点除法保留小数部分。
5 / 2 = 2而5.0 / 2.0 = 2.5。答案: 错误
判断题 2
sizeof是 C 语言标准库中的一个函数。 ( )
正确
错误
点击查看答案 解析:
sizeof是运算符而非函数。它在编译时计算(除了 VLA 场景),且对于变量可以省略括号(sizeof x)。答案: 错误
判断题 3
位运算符
&和|也可以进行短路求值。 ( )
正确
错误
点击查看答案 解析: 只有逻辑运算符
&&和||具有短路特性。位运算符&和|始终计算两侧的表达式。答案: 错误
判断题 4
a += b和a = a + b在语义上永远等价。 ( )
正确
错误
点击查看答案 解析:
a += b的定义就是a = a + b,只是a只被求值一次(对复杂表达式arr[i++] += 1时这个区别才重要)。两者语义等价。答案: 正确
判断题 5
异或运算
a ^ a的结果总是 0。 ( )
正确
错误
点击查看答案 解析: 任何数与自己异或都得 0——这是异或运算的基本性质,常用于清零和加密。
答案: 正确
判断题 6
~0的值取决于机器的字长。 ( )
正确
错误
点击查看答案 解析:
~0是"所有位为 1 的整数",其值取决于该整数类型的位数。在 32 位int上~0 = -1 = 0xFFFFFFFF,在 64 位long上~0L = -1L = 0xFFFFFFFFFFFFFFFF。答案: 正确
判断题 7
1 << 31在 32 位int上执行的结果是未定义行为。 ( )
正确
错误
点击查看答案 解析:
1 << 31在 32 位int上结果等于INT_MIN,这是定义良好的(左移进入符号位)。但1 << 32或1 << 31在部分溢出场景可能是未定义行为。更安全的做法是使用(1U << 31)。答案: 错误
判断题 8
5 / 2 * 2的结果是 4。 ( )
正确
错误
点击查看答案 解析: 乘除同优先级,左结合:
5 / 2 = 2(整数除法),2 * 2 = 4。如果先算乘法,会得到5 / 4 = 1。优先级和结合性决定了结果是 4。答案: 正确
判断题 9
!(x > 0)等价于x <= 0。 ( )
正确
错误
点击查看答案 解析:
x > 0为真当且仅当 x 大于 0。取反后等价于 x 不大于 0,即x <= 0。这是德摩根律的简单应用。答案: 正确
判断题 10
三元运算符
?:的结果必须赋给一个变量,不能直接作为表达式使用。 ( )
正确
错误
点击查看答案 解析:
?:是一个表达式,可以用在任何需要表达式的地方,如printf("%s\n", x > 0 ? "正" : "非正");、return a > b ? a : b;,不一定需要赋值给变量。答案: 错误
二、选择题(单项选择题)
选择题 1
int a = 10; int b = a++ + ++a;执行后a和b的值是?
A. a=12, b=22
B. a=12, b=23
C. a=11, b=21
D. 未定义行为
点击查看答案 解析: 这是经典的未定义行为。C 标准规定如果一个表达式中有多个对同一个变量的修改,且这些修改之间没有序列点(sequence point),则结果未定义。
a++ + ++a中修改了两次a,中间没有序列点。正确答案: D
选择题 2
printf("%d\n", 5 && 0);输出什么?
A. 5
B. 0
C. 1
D. 未定义
点击查看答案 解析:
&&运算符总是返回 0 或 1。5 && 0→ 0。正确答案: B
选择题 3
以下哪个表达式的结果为 1?
A.
5 == 5 == 1B.
(5 == 5) == 1C.
5 == (5 == 1)D. 以上都不是
点击查看答案 解析: A 解析为
(5 == 5) == 1→1 == 1→ 1(虽然最终结果也是 1)。但严格来说==是左结合。C 解析为5 == 0→ 0。正确答案: B
选择题 4
以下哪个程序段可以正确交换 a 和 b 的值?
A.
a = b; b = a;B.
a ^= b; b ^= a; a ^= b;C.
a = a + b; b = a;D.
a ^= b; a ^= b; b ^= a;点击查看答案 解析: XOR 交换算法:
a ^= b; b ^= a; a ^= b;不需要临时变量。A 会导致 a 和 b 都变成原来的 b。D 的顺序不对。正确答案: B
选择题 5
sizeof('a')在 C 语言中的值是?
A. 1
B. 4(在 32 位 int 平台上)
C. 2
D. 不确定
点击查看答案 解析: C 语言中字符常量
'a'的类型是int,所以sizeof('a')等于sizeof(int)。32 位平台上为 4。注意:C++ 中'a'是char,sizeof('a') = 1。正确答案: B
选择题 6
unsigned int x = 1; printf("%u\n", ~x);输出什么?(32 位 unsigned int)
A. 0
B. 1
C. 4294967294
D. -2
点击查看答案 解析:
~1 = ~0x00000001 = 0xFFFFFFFE = 4294967294(按 %u 格式输出)。如果用 %d 输出则是 -2。正确答案: C
选择题 7
以下哪个表达式用于判断整数 x 是否为 2 的幂?
A.
x % 2 == 0B.
x & 1 == 0C.
x > 0 && (x & (x - 1)) == 0D.
(x | (x - 1)) == 0点击查看答案 解析:
(x & (x - 1)) == 0清除了最低位的 1,如果结果为 0 说明只有一个 1(即 2 的幂)。需加上x > 0排除 x = 0 的情况。A 只判断偶数,B 判断奇数。正确答案: C
选择题 8
(1, 2, 3)这个表达式的值是?
A. 1
B. 2
C. 3
D. 语法错误
点击查看答案 解析: 逗号运算符从左向右依次计算每个表达式,返回最右侧表达式的值。
(1, 2, 3)→ 返回 3。正确答案: C
选择题 9
以下关于
x << 1的说法正确的是?
A. 总是等价于
x * 2B. 等价于
x * 2,但当 x 为负数时结果不确定C. 等价于
x / 2D. 等价于
x ^ 2点击查看答案 解析:
x << 1等价于x * 2,无论 x 是正数还是负数(在补码机器上)。这是因为左移一位等价于乘以 2 的幂,补码的数学结构支持这种行为。正确答案: A
选择题 10
int a = 5; int b = a = 10;执行后 a 和 b 的值是?
A. a=5, b=5
B. a=10, b=5
C. a=10, b=10
D. 编译错误
点击查看答案 解析: 赋值运算符返回赋值后的值。
a = 10先执行,返回 10,然后b = 10。所以 a=10, b=10。正确答案: C
️ 动手练习题
练习题 1:表达式求值练习
难度:
编写程序,在不编译运行的情况下手算以下表达式的值,然后用程序验证:
(1) 10 + 3 * 5 - 2 (2) 10 / 3 * 3 (3) 10 % 3 + 4 / 2 (4) 5 + 3 << 1 (5) 0 && 1 || 2 (6) 5 > 3 ? 10 : 20 (7) sizeof(char) + 1 (8) ~0 & 0xFF对每个表达式,写出计算步骤和最终结果。
练习题 2:位运算工具集
难度:
编写一个程序,提供以下位操作功能:
- 输入一个整数,打印其二进制表示
- 设置/清除/翻转指定位置的位
- 统计整数中 1 的个数(Brian Kernighan 算法:
while(n) { n &= n-1; count++; })- 反转一个字节的位序(如 0b11000001 → 0b10000011)
- 不使用临时变量交换两个整数(XOR 交换)
力扣练习:力扣位运算题
练习题 3:运算符优先级纠错
难度:
编写一个程序,故意使用容易引起歧义的表达式(不加括号),观察编译器行为:
1 << 2 + 3vs(1 << 2) + 3*p++vs(*p)++vs*(p++)a & 0xF == 0vs(a & 0xF) == 0!x == 0vs!(x == 0)n > 0 ? 1 : n < 0 ? -1 : 0(三元运算符嵌套的陷阱)用
-Wall编译,注意哪些表达式产生了警告。
练习题 4:整型溢出与取模
难度:
力扣练习:力扣大数运算题
用 C 语言完成该题,注意大数问题。额外练习:
- 实现一个函数
add_mod(a, b, mod),在不溢出的前提下计算(a + b) % mod- 实现快速幂算法(使用位运算优化):
power_mod(base, exp, mod)- 理解为何
(a * b) % mod在直接计算时会溢出
练习题 5:表达式树求值
难度:
实现一个简单的表达式求值程序,支持
+ - * /和括号:
- 使用栈处理运算符优先级
- 支持多位数和负数
- 处理除零错误
力扣练习:力扣表达式求值题