Appearance
运算符详解:算术、位运算与表达式求值
预计学习时长:约 3.5 小时(内容较多,建议分两次完成)· 运行环境:VS2026 / gcc / clang 均可
🎯 学习目标
学完本讲,你将能够:
- 在二进制、八进制、十进制、十六进制之间做基本转换
- 说出原码、反码、补码的区别,以及整数为什么用补码存储
- 正确使用移位操作符和 4 个位操作符,并知道哪些写法是 UB
- 用位运算完成交换、统计 1 的个数、修改指定二进制位
- 理解优先级、结合性、整型提升和寻常算术转换
🔗 先修知识
- 第 2 讲:第一个程序:编译运行的基本流程
- 第 3 讲:数据类型和变量:整数类型、sizeof
先修自检(三问)
int、char、short分别至少占几个字节?a % b中,% 要求两个操作数是什么类型?sizeof(int)的结果代表什么?
答不上来先点开第 3 讲复习。
一、运算符分类总览
C 语言运算符按作用分为以下几类(先有个印象,本讲逐个讲):
| 分类 | 运算符 |
|---|---|
| 算术 | + - * / % |
| 移位 | << >> |
| 位 | & | ^ ~ |
| 赋值 | = += -= *= /= %= <<= >>= &= |= ^= |
| 单目 | ! ++ -- + - ~ sizeof (类型),以及 & *(指针部分讲) |
| 关系 | > >= < <= == != |
| 逻辑 | && || |
| 条件 | ? : |
| 逗号 | , |
| 下标 / 函数调用 | [] () |
| 结构成员 | . -> |
算术、赋值、关系、逻辑、条件操作符已在前面接触过;本讲重点是与二进制相关的操作符。
二、二进制与进制转换
2.1 四种进制只是数值的不同写法
同一个数 15:
| 进制 | 写法 | 标志 |
|---|---|---|
| 二进制 | 1111 | C 代码中写 0b1111(C23 标准化,gcc 早已支持) |
| 八进制 | 17 | 以 0 开头:017 |
| 十进制 | 15 | 无特殊标志 |
| 十六进制 | F | 以 0x 开头:0xF |
十六进制每位范围是 0~9、a~f(大小写均可),分别表示 10~15。
2.2 二进制转十进制:按位权重相加
二进制满 2 进 1,每一位只能是 0 或 1。从右向左,每一位的权重是 2⁰、2¹、2²、2³……
text
二进制 1101:
1 1 0 1
2³ 2² 2¹ 2⁰
8 4 0 1 = 132.3 十进制转二进制:除 2 取余
把十进制数反复除以 2,记录每次的余数,最后把余数倒序排列。例如 13:
text
13 ÷ 2 = 6 余 1
6 ÷ 2 = 3 余 0
3 ÷ 2 = 1 余 1
1 ÷ 2 = 0 余 1
余数倒序:11012.4 二进制与八进制、十六进制互转
- 转八进制:八进制一位最大是 7(二进制 111),所以从右向左每 3 位二进制分为一组,每组换算成一个八进制位。
01101011→01 101 011→ 0153 - 转十六进制:十六进制一位最大是 f(二进制 1111),所以每 4 位一组。
01101011→0110 1011→ 0x6b
反过来,八进制/十六进制每一位展开成 3 位/4 位二进制即可。
三、原码、反码、补码
3.1 三种表示
整数在二进制中有三种表示方法。有符号整数的最高位是符号位:0 表示正,1 表示负;其余位是数值位。
- 原码:直接按正负把数值翻译成二进制
- 反码:原码符号位不变,其余位逐位取反
- 补码:反码加 1
正整数三种码完全相同;负整数三种码各不相同。以 -1(8 位)为例:
| 形式 | 值 |
|---|---|
| 原码 | 10000001 |
| 反码 | 11111110 |
| 补码 | 11111111 |
补码转回原码,同样用“取反、加 1”,过程完全对称。
3.2 为什么内存中存的是补码
- 符号位与数值位统一处理:CPU 不用专门判断符号;
- 加减法统一:CPU 只有加法器,减法
a - b可以变成a + (-b)的补码加法; - 转换过程对称:原码到补码、补码到原码都是“取反加 1”,硬件电路简单。
无符号整数没有符号位,所有位都是数值位。
📜 标准卡(L2 选读):C11 强制补码吗?
标准:C11 §6.2.6.2 允许有符号整数用原码、反码、补码三种表示之一;C23 起强制使用二进制补码。 实际情况:gcc、MSVC、clang 在所有主流平台上都使用补码。本讲后续涉及负数位模式的结论,在这些平台上都成立。
四、移位操作符
移位操作符的两个操作数都必须是整数。
4.1 左移 << :左边抛弃,右边补 0
📄 left_shift.c · ✅ 完整程序
c
#include <stdio.h>
int main()
{
int num = 10; // 二进制 ...00001010
int n = num << 1; // 结果 ...00010100 = 20
printf("n = %d\n", n); // 20
printf("num = %d\n", num); // 10,原变量不变
return 0;
}左移一位在结果不溢出时相当于乘 2。
⚠️ 但对有符号整数,左移后的值若超出该类型能表示的范围,是 🔴 未定义行为。典型如 1 << 31(32 位 int),结果 2147483648 超出 int 最大值。想安全移位请用无符号数:1u << 31 🟢。
4.2 右移 >> :两种规则
右移分两种:
- 逻辑右移:左边补 0
- 算术右移:左边补原来的符号位(负数补 1)
📄 right_shift.c · ✅ 完整程序
c
#include <stdio.h>
int main()
{
int num = -10;
int n = num >> 1;
printf("n = %d\n", n);
printf("num = %d\n", num);
return 0;
}对正数,右移一位相当于除以 2(10 >> 1 = 5)。
📜 标准卡(L2 选读):负数右移的结果
标准:C11 §6.5.7p5:E1 >> E2,当 E1 为有符号负数时,结果是 🟡 实现定义行为(算术或逻辑右移由实现选择)。 gcc:算术右移。MSVC:算术右移。clang:算术右移。注意算术右移向负无穷取整:-9 >> 1 得 -5(floor(-4.5)),而直接写 -9 / 2 按 C 规则向 0 取整得 -4。
4.3 移位的两条禁令
- 不要移动负数位:
num >> -1是 🔴 未定义行为 🟢(C11 §6.5.7p3)。 - 移动位数不要超过(或等于)类型宽度:
int移 32 位及以上也是 🔴 未定义行为(结果类型无宽度概念时同样禁止)。
五、位操作符
四个位操作符,操作数都必须是整数:
| 操作符 | 含义 | 逐位规则 |
|---|---|---|
& | 按位与 | 两位都为 1,结果才为 1 |
| | 按位或 | 任一位为 1,结果为 1 |
^ | 按位异或 | 两位不同,结果为 1 |
~ | 按位取反 | 0 变 1,1 变 0 |
📄 bit_demo.c · ✅ 完整程序
c
#include <stdio.h>
int main()
{
int num1 = -3;
int num2 = 5;
printf("%d\n", num1 & num2);
printf("%d\n", num1 | num2);
printf("%d\n", num1 ^ num2);
printf("%d\n", ~0);
return 0;
}运行结果(32 位 int,补码):
text
5
-3
-8
-1分析:-3 的补码是 ...11111101,5 是 ...00000101;按位与得 5,按位或得 -3,异或得 ...11111000 即 -8;~0 所有位变 1,即 -1。
5.1 异或的两个特性
a ^ a == 0(相同抵消)a ^ 0 == a
利用它们,可以不借助临时变量交换两个整数:
📄 xor_swap.c · ✅ 完整程序
c
#include <stdio.h>
int main()
{
int a = 0, b = 0;
scanf("%d %d", &a, &b);
a = a ^ b;
b = a ^ b; // b = (原a ^ 原b) ^ 原b = 原a
a = a ^ b; // a = (原a ^ 原b) ^ 原a = 原b
printf("a = %d b = %d\n", a, b);
return 0;
}⚠️ 技巧归技巧,工程上请用临时变量
异或交换若 a、b 恰好是同一地址的变量(比如数组元素交换时下标相等),a ^ a 会把变量变成 0。临时变量 int t = a; a = b; b = t; 更安全、可读性更好,编译器生成的机器码往往一样快。
5.2 例子:统计二进制中 1 的个数
方法 1:除 2 取余(仅适用于非负数)
📄 popcount1.c · ✅ 完整程序
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
unsigned int num = 0;
scanf("%u", &num);
int count = 0;
while (num != 0)
{
if (num % 2 == 1)
count++;
num = num / 2;
}
printf("1 的个数 = %d\n", count);
return 0;
}注意我们用了 unsigned int:若用有符号 int 且输入 -1,-1 / 2 向 0 取整得 0,循环直接结束,会错误地输出 0。
方法 2:逐位测试(所有数都适用,固定检查 32 位)
📄 popcount2.c · ✅ 完整程序
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
unsigned int num = 0;
scanf("%u", &num);
int count = 0;
for (int i = 0; i < 32; i++)
{
if ((num & (1u << i)) != 0)
count++;
}
printf("1 的个数 = %d\n", count);
return 0;
}这里用 1u << i 而不是 1 << i:当 i 等于 31 时,1 << 31 超出 int 范围是 UB;1u 是无符号数,移位结果始终有良好定义。
方法 3:num & (num - 1) 消去最低位的 1
📄 popcount3.c · ✅ 完整程序
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
unsigned int num = 0;
scanf("%u", &num);
int count = 0;
while (num != 0)
{
count++;
num = num & (num - 1);
}
printf("1 的个数 = %d\n", count);
return 0;
}num & (num - 1) 每次恰好把最低位的一个 1 变成 0,所以循环次数正好等于 1 的个数,不必固定跑 32 次。
5.3 例子:把指定二进制位置 1 或清 0
📄 set_clear_bit.c · ✅ 完整程序
c
#include <stdio.h>
int main()
{
int a = 13; // 二进制 ...00001101
a = a | (1 << 4); // 第 5 位置 1:...00011101 = 29
printf("置 1 后 a = %d\n", a);
a = a & ~(1 << 4); // 第 5 位清 0,恢复为 13
printf("清 0 后 a = %d\n", a);
return 0;
}记住两个固定套路:
- 第 k 位置 1:
a | (1 << k) - 第 k 位清 0:
a & ~(1 << k)
5.4 实用例子:英文字母大小写转换
ASCII 中大小写字母只差第 5 位(值 32):'A' 是 65(01000001),'a' 是 97(01100001)。
📄 case_convert.c · ✅ 完整程序
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
char ch = 0;
scanf("%c", &ch);
if (ch >= 'A' && ch <= 'Z')
ch = ch | (1 << 5); // 第 5 位置 1 → 小写
else if (ch >= 'a' && ch <= 'z')
ch = ch & ~(1 << 5); // 第 5 位清 0 → 大写
printf("%c\n", ch);
return 0;
}六、单目操作符速览
只有一个操作数的运算符:!、++、--、+、-、~、sizeof、(类型);其中 &(取地址)和 *(解引用)留到指针部分学习。
sizeof计算类型或变量所占字节数,返回类型是size_t(无符号整型),打印用%zu:
📄 sizeof_demo.c · ✅ 完整程序
c
#include <stdio.h>
int main()
{
printf("char 占 %zu 字节\n", sizeof(char));
printf("int 占 %zu 字节\n", sizeof(int));
int a = 10;
printf("变量 a 占 %zu 字节\n", sizeof a);
return 0;
}(类型)是强制转换,比如(double)5 / 2先把 5 转成 double,结果是 2.5。
七、逗号表达式
形式:exp1, exp2, ..., expN。从左到右依次执行,整个表达式的值是最后一个表达式的值。
📄 comma.c · ✅ 完整程序
c
#include <stdio.h>
int main()
{
int a = 1, b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);
printf("a = %d, b = %d, c = %d\n", a, b, c);
return 0;
}运行结果:a = 12, b = 13, c = 13。分析:a = b + 10 让 a 变 12;b = a + 1 让 b 变 13;最后一个表达式是 b 的赋值,值 13,所以 c 为 13。
⚠️ 注意区分:int a = 1, b = 2; 里的逗号是声明分隔符,不是逗号表达式。
八、下标 [] 与函数调用 ()
8.1 下标引用操作符 []
它有两个操作数:数组名和下标。
c
int arr[10]; // 创建 10 个元素的数组
arr[9] = 10; // [] 的操作数是 arr 和 9数组将在第 7 讲系统学习。
8.2 函数调用操作符 ()
第一个操作数是函数名,其余操作数是参数。
📄 call.c · ✅ 完整程序
c
#include <stdio.h>
void greet(const char *name)
{
printf("你好,%s\n", name);
}
int main()
{
greet("小明"); // () 是函数调用操作符,"小明" 是参数
return 0;
}九、结构体初识与 . 、->
char、int、double 这些内置类型描述不了“一个学生”(同时需要姓名、年龄、学号)。C 语言允许用结构体把不同类型的值打包成一个整体。这些值叫成员变量。
9.1 声明结构体
🧩 示意片段
c
struct Stu
{
char name[20]; // 姓名
int age; // 年龄
char id[20]; // 学号
}; // 末尾分号不能丢9.2 定义与初始化
🧩 示意片段
c
struct Point
{
int x;
int y;
} p1; // 声明类型的同时定义变量 p1
struct Point p2; // 单独定义
struct Point p3 = {10, 20}; // 按顺序初始化
struct Point p4 = {.y = 20, .x = 10}; // 按成员名初始化(C99 起)9.3 点操作符 .:结构体变量直接访问成员
📄 point_dot.c · ✅ 完整程序
c
#include <stdio.h>
struct Point
{
int x;
int y;
};
int main()
{
struct Point p = {3, 4};
printf("x = %d, y = %d\n", p.x, p.y);
return 0;
}9.4 箭头操作符 ->:通过结构体指针访问成员
📄 point_arrow.c · ✅ 完整程序
c
#include <stdio.h>
struct Point
{
int x;
int y;
};
int main()
{
struct Point p = {3, 4};
struct Point *ptr = &p;
ptr->x = 10;
ptr->y = 20;
printf("x = %d, y = %d\n", ptr->x, ptr->y);
return 0;
}记忆:手里是变量用 .,手里是指针用 ->。结构体完整内容见第 14 讲:结构体、联合与枚举。
十、优先级与结合性
10.1 优先级
优先级高的运算符先执行:
c
3 + 4 * 5; // * 高于 +,先算 4 * 5,结果 2310.2 结合性
优先级相同的运算符,由结合性决定顺序:
- 大部分运算符左结合:
5 * 6 / 2先算5 * 6再除以 2; - 赋值类右结合:
c = b = a先算b = a再算c = b。
常用优先级从高到低(记个大概即可):
()括号++--和其他单目运算符*/%+-- 关系运算符
- 赋值运算符
💡 与其死背优先级表,不如多写括号,既不会错又好读。完整优先级表:cppreference 运算符优先级。
十一、整型提升与寻常算术转换
11.1 整型提升
CPU 的整型运算器按 int 的宽度工作,所以比 int 小的类型(char、short)在参与运算前会先转成 int(或 unsigned int),这叫整型提升。🟢
📄 promote.c · ✅ 完整程序
c
#include <stdio.h>
int main()
{
char c1 = -1; // 8 位:11111111
char c2 = 1; // 8 位:00000001
printf("c1 + c2 = %d\n", c1 + c2);
return 0;
}结果是 0:有符号 char 的 -1 高位补符号位 1,提升为 32 位的 -1;1 高位补 0;相加为 0。
提升规则:
- 有符号类型:高位补符号位;
- 无符号类型:高位补 0。
📜 标准卡(L2 选读):char 到底有没有符号
标准:C11 §6.2.5p15:char 的符号性是 🟡 实现定义。 gcc / MSVC(x86、x64):有符号 char。ARM 平台 gcc / 安卓 NDK:默认无符号 char。需要明确时用 signed char 或 unsigned char。
11.2 寻常算术转换
两个不同类型的数做运算,编译器自动把较低类型转成较高类型(排名靠后的向靠前的转换):
text
long double ← 最高
double
float
unsigned long
long
unsigned int
int ← 最低例如 3.14 + 2:int 的 2 先转成 double,结果是 double 类型的 5.14。
十二、问题表达式:为什么不要写“炫技”表达式
📘 提高(L2):原 PDF 的 5 个问题表达式逐个说清
规则依据是 C11 §6.5p2:两个序列点之间,同一个变量最多被修改一次;若还要读取它,读取必须是为了确定要写入的值,否则为 UB。
a * b + c * d + e * f:🟠 未指定。只保证*比+先算,三个乘法、两个加法的内部先后顺序不指定;但只要 a~f 互不相同,结果数值唯一。c + --c:🔴 UB。c 被修改,同时左边又读取 c 的值。i = i-- - --i * (i = -3) * i++ + ++i:🔴 UB。i 在一个表达式里被修改多次。fun() - fun() * fun()(fun 带自增副作用):🟠 未指定行为。三次调用的先后顺序不指定,结果依赖调用顺序,但不是 UB(函数调用各自有序列点)。(++i) + (++i) + (++i):🔴 UB。i 被修改多次。gcc 与 MSVC 结果不同不是“编译器差异”,而是代码本身没有定义结果——编译器怎么处理都不算错。
💡 给初学者的实用准则:一个表达式里,同一个变量只允许修改一次;拿不准就拆成多行。
⚠️ 常见坑与报错(本讲汇总)
| 你看到的现象 | 原因 | 修法 |
|---|---|---|
| 负数统计 1 的个数得到 0 | 有符号数除 2 向 0 取整 | 用 unsigned int,或用方法 2/3 |
gcc 对 1 << 31 报警或结果异常 | 有符号左移溢出是 UB | 写成 1u << 31 |
| 负数右移结果和“除 2”对不上 | 算术右移向负无穷取整,且规则实现定义 | 需要数学除法时显式判断处理 |
num >> -1、移位 ≥32 | 移位位数非法,UB | 保证 0 ≤ 位数 < 类型宽度 |
| char 变量在不同机器行为不同 | char 的符号性实现定义 | 明确写 signed char / unsigned char |
| 同一表达式里 i++ 出现多次 | 🔴 UB,编译器结果各异 | 拆成多条语句 |
| printf 打印 sizeof 结果乱码 | size_t 用了 %d | 用 %zu |
017 被当成十进制 17 | 前导 0 表示八进制,017 = 15 | 注意字面量前缀 |
🛠️ 动手练习
📊 本讲网页练习进度0 / 5(0%)
进度自动保存在本浏览器;编程题不计数,请在编辑器中完成。
练习 1 · ⭐ 基础 · 读输出
知识点:位操作符
题干:写出输出:
📄 ex1.c · ✅ 完整程序
c
#include <stdio.h>
int main()
{
int a = 0;
printf("%d\n", ~a);
printf("%d\n", a | 1);
printf("%d\n", a ^ 1);
return 0;
}请按打印顺序写出三个数(用空格分隔即可):
空①
练习 2 · ⭐ 基础 · 读输出
知识点:移位操作符
题干:写出输出:
📄 ex2.c · ✅ 完整程序
c
#include <stdio.h>
int main()
{
int a = 8;
int b = -8;
printf("%d %d\n", a << 2, a >> 2);
printf("%d\n", b >> 2);
return 0;
}请按打印顺序写出全部三个数:
空①
练习 3 · ⭐ 基础 · 读输出
知识点:整型提升
题干:下面代码输出什么?
📄 ex3.c · ✅ 完整程序
c
#include <stdio.h>
int main()
{
char a = 100, b = 100;
printf("%d\n", a + b);
return 0;
}请写出输出:
空①
练习 4 · ⭐ 基础 · 找错改错
知识点:移位 UB
题干:下面代码想检查整数第 31 位(从 0 数)是否为 1,请填出正确的掩码表达式:
🧩 待改错片段
c
int x = 0;
if (x & (1 << 31))
printf("第 31 位是 1\n");请填出 x & (...) 括号中正确的表达式:
空①
练习 5 · ⭐ 基础 · 程序填空
知识点:置位与清零
题干:补全代码,把变量第 k 位先置 1 再清 0:
🧩 待补全片段
c
int a = 0;
int k = 3;
a = a ______ (1 << k); // 置 1
a = a ______ (1 << k); // 再清 0请分别填出两个空中的运算符(第二空需要两个符号):
空①空②
练习 6 · ⭐⭐ 提高 · 写小程序
知识点:统计 1 的个数
🛠 动手编程题 · 请在 VS2026(或你的编辑器)中完成
输入一个非负整数,输出它二进制表示中 1 的个数。
✅ 过关标准:输入 13(1101)输出 3;输入 0 输出 0。
网页内无法练习写代码,亲手敲、亲手编译才能真正学会。下方按顺序展开 思路 → 步骤 → 答案。
💡 提示 1(思路)
可以逐位检查 32 位;更优的办法是用 num & (num - 1) 每次消掉一个 1。
🧭 提示 2(步骤)
- count 初始 0
- while (num != 0):count++;num = num & (num - 1)
- 打印 count
🔑 参考答案与解释
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
unsigned int num = 0;
scanf("%u", &num);
int count = 0;
while (num != 0)
{
count++;
num &= num - 1;
}
printf("1 的个数 = %d\n", count);
return 0;
}为什么用 unsigned:保证输入很大的数(如 4294967295)时也能正确统计为 32。
练习 7 · ⭐⭐ 提高 · 写小程序
知识点:位操作的实际应用
🛠 动手编程题 · 请在 VS2026(或你的编辑器)中完成
输入一个英文字母,输出它对应的另一种大小写形式(大写变小写、小写变大写);输入非字母则原样输出。
✅ 过关标准:输入 A 输出 a,输入 z 输出 Z。
网页内无法练习写代码,亲手敲、亲手编译才能真正学会。下方按顺序展开 思路 → 步骤 → 答案。
💡 提示 1(思路)
大小写字母 ASCII 恰好相差第 5 位,先判断是不是字母。
🧭 提示 2(步骤)
scanf("%c", &ch)- 判断范围 A~Z 或 a~z
- 对第 5 位做翻转(异或 1<<5)
🔑 参考答案与解释
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
char ch = 0;
scanf("%c", &ch);
if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'))
ch = ch ^ (1 << 5); // 翻转第 5 位即互换大小写
printf("%c\n", ch);
return 0;
}对比本讲正文:正文用“置 1 / 清 0”分两种情况,这里用 ^ 翻转一位,写法更统一——异或适合“翻转”,或适合“置 1”,与适合“清 0”。
练习 8 · ⭐⭐⭐ 挑战 · 写小程序
知识点:num & (num - 1) 的变形
🛠 动手编程题 · 请在 VS2026(或你的编辑器)中完成
输入一个正整数,判断它是不是 2 的整数次幂(1、2、4、8……),是输出“yes”,否则输出“no”。
✅ 过关标准:输入 1024 输出 yes,输入 100 输出 no,输入 1 输出 yes。
网页内无法练习写代码,亲手敲、亲手编译才能真正学会。下方按顺序展开 思路 → 步骤 → 答案。
💡 提示 1(思路)
2 的整数次幂写成二进制有什么特征?只有一个 1。
🧭 提示 2(步骤)
- 2 的幂:二进制形如 100...0,恰好一个 1
- 若 n 是 2 的幂,则 n & (n - 1) 必然为 0
- 先排除 n 为 0
🔑 参考答案与解释
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
unsigned int n = 0;
scanf("%u", &n);
if (n != 0 && (n & (n - 1)) == 0)
printf("yes\n");
else
printf("no\n");
return 0;
}为什么要先判断 n != 0:0 满足 0 & (0 - 1) == 0,但 0 不是 2 的幂;且无符号下 0 - 1 是极大值,与 0 相与仍为 0。这个判断是嵌入式位运算面试的经典题。
📝 小结与自测
核心结论
- 二进制权重按 2ⁿ 展开;八进制 3 位一组、十六进制 4 位一组。
- 负数用补码存储:反码加 1;补码让加减法统一。
- 左移低位补 0;负数右移为实现定义(主流编译器算术右移);移位不能为负、不能超过宽度。
&清 0/提取位,|置 1,^翻转/抵消,~全部取反;掩码常量用 1u。- char、short 运算前整型提升;不同类型运算按寻常算术转换;同一表达式中一个变量只修改一次。
自测清单
| 我能独立做到 | ✅ |
|---|---|
| 手算 13 与 1101 的互转 | |
| 写出 -1 的 8 位补码 | |
| 用位运算交换两个变量并说出风险 | |
| 用任意一种方法统计二进制 1 的个数 | |
| 用 . 和 -> 分别访问结构体成员 | |
| 判断一个数是不是 2 的幂 |
🔬 选学拓展(L3)
以下内容为选学
跳过不影响下一讲学习。
L3-1:现代编译器怎么帮你抓 UB
gcc/clang 加 -fsanitize=undefined 可以在运行时检测未定义行为(移位越界、有符号溢出等)并打印出错位置;VS2026 可在项目属性中启用 AddressSanitizer。写练习时打开它,比单纯靠输出更能发现问题。
L3-2:不用数 32:让 popcount 适应不同平台
本讲固定按 32 位统计。可移植的写法是循环 sizeof(unsigned int) * CHAR_BIT 次(CHAR_BIT 定义在 limits.h,标准保证 ≥8);C23 还提供了内置式的 <stdbit.h>。
L3-3:整数除法与右移不是一回事
编译器会把 x / 2(x 为非负)优化成右移,但不会把有符号负数的除法直接替换:因为算术右移向负无穷取整,而 C 语言除法向 0 取整,-9 >> 1 = -5 ≠ -9/2 = -4。理解这一点,就明白“移位代替乘除”只在确定非负时安全。
预告:下一讲学习分支:让程序学会选择的 if 与 switch。