0105、安全开发篇(一):类型转换与运算符
返回:安全开发篇(一)汇总 · 上一节:数据与运算 · 安全开发总目录
这一篇接着数据类型往下走,重点是“数据如何参与表达式计算”。
C 语言里的运算符看起来简单,但很多安全问题都藏在这些小地方:类型被自动转换、整数被截断、条件判断写错、&& 和 || 的短路顺序被误用、= 和 == 混淆、优先级靠猜。复盘这一章时,不需要追求花哨写法,先把表达式写清楚、算明白、调试看得到。
4.0 数据类型转换
类型转换可以分成两类:隐式转换和显式转换。
| 类型 | 示例 | 说明 |
|---|---|---|
| 隐式转换 | double x = 10; |
编译器自动把 int 转成 double |
| 显式转换 | (double)a / b |
程序员明确要求转换 |
| 赋值转换 | char c = 300; |
赋值时目标类型可能装不下 |
| 表达式转换 | 1 + 2.5 |
运算前操作数会被转换到合适类型 |
整数和浮点数转换
1 |
|
a / b 两边都是整数,所以结果还是整数,得到 2。把其中一个操作数转换成 double 后,表达式按浮点数计算,得到 2.50。
窄化转换和截断
1 |
|
unsigned char 通常只能保存 0 ~ 255。把 300 转进去时,会丢掉高位,只留下目标类型能保存的部分。
安全开发里,类型转换最容易影响长度、下标、偏移和缓冲区大小。看到强制转换时要多问一句:转换后范围有没有变?符号有没有变?高位有没有被截掉?
符号扩展和零扩展
1 |
|
同样是 0xFF,如果原来按有符号数解释,提升成 int 时会保留负数含义;如果原来按无符号数解释,提升后就是 255。
4.1 算术运算符
C 语言常见算术运算符包括 +、-、*、/、%。
| 运算符 | 含义 | 示例 |
|---|---|---|
+ |
加法 | a + b |
- |
减法 | a - b |
* |
乘法 | a * b |
/ |
除法 | a / b |
% |
取余 | a % b |
1 |
|
除法和取余
整数除法会保留整数部分,取余得到余数。
1 |
|
这种写法常用于分页、分块、对齐和循环处理。后面处理文件块、网络包和缓冲区时也会用到。
除零问题
1 |
|
任何来自外部输入、文件、网络或配置的数据,在作为除数前都要检查是否为 0。
4.2 前置后置递增操作
++ 和 -- 用来让变量自增或自减。
| 写法 | 含义 |
|---|---|
++i |
先自增,再使用表达式结果 |
i++ |
先使用原值,再自增 |
--i |
先自减,再使用表达式结果 |
i-- |
先使用原值,再自减 |
1 |
|
不要写复杂自增表达式
1 | // 不推荐:可读性差,还容易触发未定义行为 |
初学和复盘阶段,尽量把自增单独写成一行。
1 | int i = 0; |
这样不仅更容易读,也更方便断点调试。
4.3 反汇编角度探究语法底层原理
这一节不用急着背汇编指令。重点是建立一个直觉:C 语言表达式最终会被编译成 CPU 能执行的指令。
例如下面的代码:
1 | int a = 10; |
从反汇编角度看,大致会经历:
- 把数据放进寄存器或内存。
- 执行加法指令。
- 把结果保存到目标位置。
调试时可以观察什么
| 窗口 | 可以观察 |
|---|---|
| 反汇编窗口 | 一行 C 代码对应了哪些底层指令 |
| 寄存器窗口 | 运算过程中寄存器值怎么变化 |
| 内存窗口 | 变量附近的字节内容 |
| 调用堆栈 | 当前执行路径从哪里来 |
现在不需要把每条汇编都背下来。先知道“语法不是魔法”,它最后都会落到指令、寄存器和内存上。后面学逆向时,这个直觉会慢慢接上。
注意优化影响
同一段 C 代码,在不同优化级别下可能生成完全不同的汇编。调试学习时,可以先用 Debug 模式观察;真正分析 Release 程序时,再考虑编译优化带来的变化。
4.4 赋值运算符
赋值运算符把右边表达式的结果保存到左边变量里。
| 写法 | 等价理解 |
|---|---|
a = b |
把 b 的值赋给 a |
a += b |
a = a + b |
a -= b |
a = a - b |
a *= b |
a = a * b |
a /= b |
a = a / b |
a %= b |
a = a % b |
1 |
|
= 和 == 不要混淆
1 |
|
= 是赋值,== 是比较。条件判断里把 == 写成 =,是非常经典的 bug。
4.5 比较运算符
比较运算符用于得到真假结果。C 语言中,比较结果通常是 1 或 0。
| 运算符 | 含义 |
|---|---|
== |
等于 |
!= |
不等于 |
> |
大于 |
< |
小于 |
>= |
大于等于 |
<= |
小于等于 |
1 |
|
字符串不能直接用 == 比较内容
1 |
|
数组名参与表达式时经常会退化成地址。== 比较两个字符串数组时,比较的不是内容是否相同,而更可能是地址是否相同。
浮点数比较要留误差
1 |
|
4.6 逻辑运算符
逻辑运算符用于组合多个条件。
| 运算符 | 含义 | 示例 | ||||
|---|---|---|---|---|---|---|
&& |
逻辑与,两个条件都真才真 | a > 0 && b > 0 |
||||
| ` | ` | 逻辑或,至少一个条件为真 | `a > 0 | b > 0` | ||
! |
逻辑非,取反 | !is_valid |
1 |
|
短路求值
&& 和 || 会短路。
1 |
|
因为 p != 0 为假,&& 后面的 value / p 不会被执行,从而避免除零。
短路求值可以用来保护危险表达式,但不要把带副作用的操作写进复杂逻辑条件里。条件越关键,越应该写得直白。
4.7 运算优先级
运算符有优先级,但不要把代码写成“考读者背表”。
| 优先级方向 | 常见运算符 | ||
|---|---|---|---|
| 高 | ()、函数调用、数组下标 |
||
| 较高 | !、++、--、强制类型转换 |
||
| 中 | *、/、% 高于 +、- |
||
| 比较 | <、<=、>、>= 高于 ==、!= |
||
| 逻辑 | && 高于 ` |
` | |
| 低 | =、+=、-= 等赋值运算符 |
1 |
|
该加括号就加括号
1 | if ((age >= 18) && (has_ticket || is_vip)) |
这类括号不是多余,而是在告诉自己和读者:这里的条件关系就是这样设计的。
4.8 三目运算符
三目运算符也叫条件运算符:
1 | condition ? value_if_true : value_if_false |
1 |
|
适合简单选择
三目运算符适合处理很短的二选一逻辑。
1 | int max = (a > b) ? a : b; |
如果逻辑里有多步操作、多个分支、日志输出或错误处理,就用正常的 if,不要硬塞进三目。
4.9 单行 IF 语句
C 语言允许 if 后面不写 {},只控制紧跟着的一条语句。
1 | if (score >= 60) |
但复盘和正式代码里,更推荐写完整代码块:
1 | if (score >= 60) |
为什么推荐加花括号
1 |
|
这段代码看缩进好像两行都属于 if,但实际上只有第一行受 if 控制,第二行无论如何都会执行。
涉及权限、边界判断、登录状态、文件写入和危险操作时,if 一律建议加 {}。少写两行括号省不了多少时间,但能少很多低级事故。
本章最小复盘代码
这段代码把类型转换、算术运算、逻辑判断、三目运算符和输入边界放在一起。
1 |
|
复盘时可以观察:
total / count和(double)total / count的结果为什么不同。count <= 0为什么要在除法之前判断。- 三目运算符适不适合这里。
&&两侧条件的执行顺序。- 用断点看每一步赋值后的变量变化。
常见错误整理
| 错误 | 示例 | 问题 | ||
|---|---|---|---|---|
| 忘记类型转换 | 5 / 2 |
得到整数 2,不是 2.5 |
||
| 窄化转换 | unsigned char c = 300; |
发生截断 | ||
| 除零 | a / b,但 b 可能为 0 |
程序异常或未定义行为 | ||
| 复杂自增 | i++ + ++i |
容易产生未定义行为 | ||
| 赋值比较混淆 | if (flag = 1) |
条件被赋值结果影响 | ||
字符串用 == 比较 |
str1 == str2 |
比较地址,不是内容 | ||
| 浮点数直接比较 | a == b |
精度误差导致结果不稳定 | ||
| 条件不加括号 | `a && b | c` | 读者需要猜优先级 | |
| 单行 if 漏花括号 | 缩进看似受控,实际不受控 | 可能造成权限或逻辑错误 |
学习检查清单
| 检查项 | 状态 | ||
|---|---|---|---|
| 能区分隐式转换和显式转换 | 待复盘 | ||
| 能解释整数除法和浮点除法的区别 | 待复盘 | ||
| 能说明窄化转换为什么可能截断数据 | 待复盘 | ||
能正确使用 +、-、*、/、% |
待复盘 | ||
能说明前置 ++i 和后置 i++ 的区别 |
待复盘 | ||
| 能用调试窗口观察表达式执行过程 | 待复盘 | ||
| 能区分赋值运算符和比较运算符 | 待复盘 | ||
能用 strcmp 比较字符串内容 |
待复盘 | ||
能理解 && 和 ` |
` 的短路求值 | 待复盘 | |
| 能通过加括号让复杂条件更清晰 | 待复盘 | ||
| 能合理使用三目运算符处理简单二选一 | 待复盘 | ||
能坚持给 if 写花括号 |
待复盘 |
这一篇最重要的复盘点是:表达式不是只看表面符号,还要看每个操作数的类型、求值顺序、边界条件和最终保存到哪里。






