重温 C 语言的第一天。作为 Java 出身的开发者,我习惯从 Java 的视角切入:先回顾 Java 的数据类型体系,再对照 C 中的有符号 / 无符号、进制表示、浮点数存储与表达式求值规则,把那些容易踩的坑一次性梳理清楚。
一、Java 数据类型
1.1 基本类型(8 种)
| 类型 | 字节 | 范围 | 默认值 |
|---|---|---|---|
byte | 1 | -128 ~ 127 | 0 |
short | 2 | -32768 ~ 32767 | 0 |
int | 4 | -2³¹ ~ 2³¹-1 | 0 |
long | 8 | -2⁶³ ~ 2⁶³-1 | 0L |
float | 4 | ±3.4×10³⁸ | 0.0f |
double | 8 | ±1.8×10³⁰⁸ | 0.0d |
char | 2 | 0 ~ 65535 | '\u0000' |
boolean | 未明确 | true / false | false |
1.2 引用类型
类、接口、数组、枚举、注解。默认值都是 null。
1.3 包装类
| 基本类型 | 包装类 |
|---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
1.4 关键坑
float/double不能精确表示大多数小数Integer有 -128 ~ 127 缓存(所以Integer == Integer比较有时"碰巧"相等)- Java 只有有符号整数,没有 unsigned
二、有符号与无符号
- 有符号(signed):最高位当符号位,能表示负数
- 无符号(unsigned):所有位表示大小,只能非负
- 同样位数,无符号正数范围更大,有符号能表示负数
- Java 只有有符号;C/C++ 两者都有——这是重温 C 时最大的思维切换点
同一串二进制,两种解释完全不同(以 8 位为例):
| 二进制 | 无符号解释 | 有符号解释 |
|---|---|---|
| 0000 0000 | 0 | 0 |
| 0111 1111 | 127 | 127 |
| 1000 0000 | 128 | -128 |
| 1111 1111 | 255 | -1 |
注意 1000 0000:有符号时不是"负 0",而是 -128——补码表示里没有负 0。
三、进制
| 进制 | 前缀 | 合法数字 |
|---|---|---|
| 二进制 | 0b | 0-1 |
| 八进制 | 0 | 0-7 |
| 十进制 | 无 | 0-9 |
| 十六进制 | 0x | 0-9, a-f |
3.1 示例
888、877含 8,不是合法八进制777(八进制)= 511(十进制)7777(八进制)= 4095(十进制)- 八进制负数加负号:
-017 0xab= 171(十进制)= 253(八进制)= 1010 1011(二进制)
3.2 十六进制、八进制、二进制互转
- 十六进制每位 = 4 位二进制
- 八进制每位 = 3 位二进制
- 先转二进制,再分组,最快
例:0xab → a = 1010,b = 1011 → 1010 1011 → 从右往左每 3 位一组:10 101 011 → 253(八进制)。
四、浮点数与 IEEE 754
4.1 float 组成(32 位)
┌─┬─────────┬───────────────────────┐
│S│ E(8) │ M(23) │
└─┴─────────┴───────────────────────┘
S:符号位(0 正 1 负)
E:指数,按"移码"存储,真实指数 = E - 127
M:尾数,隐含最高位 1,即 1.M
规格化数值 = (-1)^S × 1.M × 2^(E-127)
4.2 double 组成(64 位)
- 1 位符号 + 11 位指数(偏移 1023)+ 52 位尾数
- 十进制有效数字:float 约 7 位,double 约 15~16 位
- 这就是为什么 float 存不下"精确的 123456789",而 double 可以
4.3 特殊值
| 指数 E | 尾数 M | 含义 |
|---|---|---|
| 全 0 | 全 0 | ±0 |
| 全 0 | 非 0 | 非规格化数(极小数) |
| 1 ~ 254 | 任意 | 规格化数(正常数值) |
| 全 1 | 全 0 | ±∞ |
| 全 1 | 非 0 | NaN |
4.4 精度坑
0.1 的二进制是 0.0001100110011... 无限循环,永远存不精确:
#include <stdio.h>
#include <math.h>
int main(void) {
double a = 0.1 + 0.2;
printf("%.17f\n", a); /* 0.30000000000000004 */
printf("%d\n", a == 0.3); /* 0 —— 不相等! */
/* 正确姿势:用误差范围比较 */
double eps = 1e-9;
printf("%d\n", fabs(a - 0.3) < eps); /* 1 */
return 0;
}
- C:用
fabs(a - b) < EPSILON比较,别用== - Java:金额等精度敏感场景用
BigDecimal - 终极方案:换成整数运算(以"分"为单位存钱)
五、表达式求值
5.1 整型提升与常用算术转换
- 整型提升:
char/short参与运算时先提升为int - 常用算术转换:两个操作数类型不同时,低精度 → 高精度;同大小时
int→unsigned int - 经典陷阱——有符号数和无符号数比较:
int i = -1;
unsigned int u = 1;
if (i < u)
printf("i < u\n"); /* 不会走到这里 */
else
printf("i >= u\n"); /* 实际输出:i >= u */
i 被转换成 unsigned int,-1 变成 4294967295,自然大于 1。Java 里没有这个问题,因为整数全部有符号。
5.2 运算符优先级与结合性
| 优先级 | 运算符 | 结合性 |
|---|---|---|
| 1(最高) | () [] -> . | — |
| 2 | ! ~ ++ -- + -(单目) * & sizeof (类型) | 右 → 左 |
| 3 | * / % | 左 → 右 |
| 4 | + - | 左 → 右 |
| 5 | << >> | 左 → 右 |
| 6 | < <= > >= | 左 → 右 |
| 7 | == != | 左 → 右 |
| 8 | & | 左 → 右 |
| 9 | ^ | 左 → 右 |
| 10 | | | 左 → 右 |
| 11 | && | 左 → 右 |
| 12 | || | 左 → 右 |
| 13 | ?: | 右 → 左 |
| 14 | = += -= *= /= %= 等 | 右 → 左 |
| 15(最低) | , | 左 → 右 |
口诀:单目 > 算术 > 移位 > 关系 > 相等 > 位运算(& ^ |)> 逻辑 > 条件 > 赋值 > 逗号。位运算符的优先级低于比较运算符,是高频错误点:x & 1 == 0 实际是 x & (1 == 0)。
5.3 求值顺序与未定义行为(UB)
- C 不规定函数参数和子表达式的求值顺序
- 两个序列点之间修改同一个变量两次 → 未定义行为
&&、||有短路求值(这是唯一保证顺序的运算符)
int i = 1;
printf("%d %d\n", i++, i++); /* 求值顺序未指定,结果不可依赖 */
i = i++; /* 未定义行为 */
i++ + ++i; /* 未定义行为 */
5.4 整数溢出
int x = 2147483647; /* INT_MAX */
x = x + 1; /* 有符号溢出:未定义行为(UB) */
unsigned int y = 0u;
y = y - 1; /* 无符号回绕:合法,y 变成 4294967295 */
这也是"无符号数做减法要小心"的原因:结果永远不会是负数,而是回绕成一个巨大的数。
5.5 经典例题
| 表达式 | 结果 | 原因 |
|---|---|---|
-1 < 1U(C) | 假 | -1 被转为无符号数 4294967295 |
-1 < 1(Java) | 真 | Java 整数全是有符号 |
0.1 + 0.2 == 0.3 | 假 | 二进制无法精确表示 0.1 |
sizeof(i++) | i 不变 | sizeof 编译期求大小,不求值表达式 |
char c = 200; 后输出 %d | 通常 -56 | 超出 signed char 范围,补码机器上回绕 |
六、今日小结
- Java 8 种基本类型 + 对应包装类,整数只有有符号;C 有
signed/unsigned之分,同一串二进制有两种解释 - 进制互转先转二进制再分组,最快
- 浮点数 = 符号 + 指数(移码)+ 尾数,多数小数无法精确表示,比较要用误差范围
- 表达式求值三大坑:隐式类型转换(尤其有符号 vs 无符号)、优先级(尤其位运算)、未定义行为(
i++类问题) - 面试高频:
-1 < 1U、0.1 + 0.2、i = i++
下一篇:指针与数组——C 语言的灵魂,也是从 Java 转过来最需要重建心智模型的部分。