Week 02: 数据类型与运算符
深入理解 C 语言的数据类型、类型转换规则、算术运算符与位运算符.
1. 整数类型详解
1.1 整数的存储
计算机使用二进制补码表示有符号整数:
| 类型 | 表示方式 |
|---|---|
| 正数 | 原码 = 补码 |
| 负数 | 补码 = 反码 + 1 |
// 以 8 位 (char) 为例
// 5 的补码: 00000101
// -5 的补码: 11111011
int8_t x = 5; // 二进制: 00000101
int8_t y = -5; // 二进制: 11111011
// 验证: 5 + (-5) = 0
// 00000101 + 11111011 = 100000000 (溢出舍弃最高位) = 000000001.2 整数溢出
#include <stdio.h>
#include <stdint.h>
#include <limits.h>
int main(void) {
int8_t x = 127;
printf("x = %d\n", x); // 127
x = x + 1; // 有符号溢出 (未定义行为)
printf("x + 1 = %d\n", x); // -128 (通常结果)
uint8_t y = 255;
y = y + 1; // 无符号溢出 (定义良好, 回绕)
printf("y + 1 = %u\n", y); // 0
return 0;
}有符号整数溢出是未定义行为 (UB), 编译器可能做出意外优化.
1.3 类型选择指南
| 场景 | 推荐类型 |
|---|---|
| 计数、索引 | size_t (无符号) |
| 一般整数 | int |
| 需要具体位宽 | int32_t, uint64_t 等 |
| 字节操作 | uint8_t |
| 布尔值 | bool (C99) |
| 文件大小 | off_t, size_t |
1.4 布尔类型 (C99)
C99 引入 _Bool 类型及 <stdbool.h> 头文件:
#include <stdbool.h>
int main(void) {
bool flag = true; // stdbool.h 定义 bool, true, false
_Bool raw = 1; // 原始类型名
// _Bool 自动转换: 非零为 1, 零为 0
_Bool b1 = 42; // b1 = 1
_Bool b2 = 0; // b2 = 0
_Bool b3 = -5; // b3 = 1
// sizeof
printf("sizeof(bool) = %zu\n", sizeof(bool)); // 通常为 1
// 在条件中使用
if (flag) {
printf("flag is true\n");
}
return 0;
}C23 变化: bool, true, false 成为关键字, 不再需要 <stdbool.h>.
1.5 C23 类型新特性
1. constexpr 对象
C23 引入编译期常量, 类似 C++ 的 constexpr:
constexpr int SIZE = 100;
int arr[SIZE]; // SIZE 是编译期常量
constexpr double PI = 3.14159265358979;
// 必须用常量表达式初始化
// constexpr int x = rand(); // 错误!2. typeof 和 typeof_unqual
获取表达式的类型:
int x = 10;
typeof(x) y = 20; // y 是 int
const int cx = 10;
typeof(cx) cy = 20; // cy 是 const int
typeof_unqual(cx) z = 30; // z 是 int (去掉 const)
// 实用场景: 泛型宏
#define SWAP(a, b) do { \
typeof(a) _tmp = (a); \
(a) = (b); \
(b) = _tmp; \
} while(0)3. _BitInt(N) 任意位宽整数
// 精确位宽的整数类型
_BitInt(128) big = 12345678901234567890wb;
unsigned _BitInt(256) huge = 0;
// 获取最大位宽
#include <limits.h>
printf("Max BitInt width: %d\n", BITINT_MAXWIDTH);
// 字面量后缀: wb (有符号), uwb (无符号)
_BitInt(64) a = 100wb;
unsigned _BitInt(64) b = 100uwb;4. nullptr 空指针常量
int *p = nullptr; // 替代 NULL
// nullptr 有类型 nullptr_t, 比 NULL 更类型安全
#include <stddef.h>
nullptr_t np = nullptr;2. 浮点类型详解
2.1 IEEE 754 表示
浮点数使用科学计数法存储:
(-1)^符号位 × 1.尾数 × 2^(指数-偏移)| 类型 | 符号 | 指数 | 尾数 | 总位数 |
|---|---|---|---|---|
| float | 1 | 8 | 23 | 32 |
| double | 1 | 11 | 52 | 64 |
2.2 浮点精度问题
#include <stdio.h>
int main(void) {
float f = 0.1f;
double d = 0.1;
// 0.1 无法精确表示为二进制小数
printf("0.1f = %.20f\n", f); // 0.10000000149011611938
printf("0.1 = %.20lf\n", d); // 0.10000000000000000555
// 累积误差
float sum = 0.0f;
for (int i = 0; i < 10; i++) {
sum += 0.1f;
}
printf("0.1 * 10 = %.20f\n", sum); // 不精确等于 1.0
// 比较浮点数
if (sum == 1.0f) {
printf("Equal\n");
} else {
printf("Not equal\n"); // 可能输出这个
}
return 0;
}2.3 浮点比较
#include <math.h>
#include <float.h>
// 使用 epsilon 比较
int float_equal(float a, float b) {
return fabs(a - b) < FLT_EPSILON;
}
// 相对误差比较
int float_near(float a, float b, float rel_epsilon) {
return fabs(a - b) <= rel_epsilon * fmax(fabs(a), fabs(b));
}2.4 特殊值
#include <math.h>
float inf = 1.0f / 0.0f; // 正无穷
float neg_inf = -1.0f / 0.0f; // 负无穷
float nan_val = 0.0f / 0.0f; // NaN (Not a Number)
printf("inf: %f\n", inf); // inf
printf("-inf: %f\n", neg_inf); // -inf
printf("nan: %f\n", nan_val); // nan
// 检测
isinf(inf); // 非零
isnan(nan_val); // 非零
// NaN 的特殊性质
nan_val == nan_val; // 0 (false)! NaN 不等于任何值, 包括自己3. 类型转换
3.1 隐式转换 (自动)
int i = 10;
double d = i; // int → double (安全, 自动)
double x = 3.14;
int y = x; // double → int (截断, 警告)3.2 算术转换规则
当运算符两边类型不同时, 进行常规算术转换:
long double > double > float > unsigned long long > long long >
unsigned long > long > unsigned int > intint i = 5;
double d = 2.5;
double result = i + d; // i 转换为 double3.3 整数提升
小于 int 的类型在运算时提升为 int:
char a = 10;
char b = 20;
char c = a + b; // a 和 b 先提升为 int, 计算后转回 char
// 潜在问题
uint8_t x = 200;
uint8_t y = 200;
uint16_t z = x + y; // 正确: 400 (先提升为 int)
uint8_t w = x + y; // 截断: 400 - 256 = 1443.4 显式转换 (强制)
double d = 3.14;
int i = (int)d; // 强制转换, 截断小数部分
// 指针转换
int *p = (int *)malloc(sizeof(int));
// 危险的转换
float f = 3.14f;
int *bad = (int *)&f; // 类型双关, 可能违反严格别名规则3.5 有符号与无符号转换
int i = -1;
unsigned int u = i; // 二进制不变, 解释方式改变
printf("%d\n", i); // -1
printf("%u\n", u); // 4294967295 (2^32 - 1)
// 危险的比较
if (i < u) {
printf("i < u\n");
} else {
printf("i >= u\n"); // 输出这个! -1 被转换为很大的无符号数
}4. 算术运算符
4.1 基本运算符
| 运算符 | 描述 | 示例 |
|---|---|---|
+ | 加法 | 5 + 3 → 8 |
- | 减法 | 5 - 3 → 2 |
* | 乘法 | 5 * 3 → 15 |
/ | 除法 | 5 / 3 → 1 (整除) |
% | 取模 | 5 % 3 → 2 |
4.2 整数除法
int a = 7, b = 3;
int q = a / b; // 商: 2 (向零截断)
int r = a % b; // 余数: 1
// C99 规定: a == (a/b)*b + a%b
// 7 == 2*3 + 1
// 负数情况
int c = -7, d = 3;
printf("-7 / 3 = %d\n", c / d); // -2 (向零截断)
printf("-7 %% 3 = %d\n", c % d); // -14.3 自增与自减
int x = 5;
// 前缀: 先增后用
int a = ++x; // x = 6, a = 6
// 后缀: 先用后增
int b = x++; // b = 6, x = 7
// 避免在同一表达式中多次修改
int y = 5;
// int z = y++ + ++y; // 未定义行为!4.4 复合赋值
int x = 10;
x += 5; // 等价于 x = x + 5
x -= 3; // 等价于 x = x - 3
x *= 2; // 等价于 x = x * 2
x /= 4; // 等价于 x = x / 4
x %= 3; // 等价于 x = x % 35. 比较与逻辑运算符
5.1 比较运算符
| 运算符 | 描述 |
|---|---|
== | 等于 |
!= | 不等于 |
< | 小于 |
> | 大于 |
<= | 小于等于 |
>= | 大于等于 |
int a = 5, b = 3;
int result = (a > b); // 1 (true)5.2 逻辑运算符
| 运算符 | 描述 |
|---|---|
&& | 逻辑与 |
| ` | |
! | 逻辑非 |
int a = 5, b = 0;
if (a && b) { } // false (0)
if (a || b) { } // true (1)
if (!b) { } // true (1)5.3 短路求值
int a = 0;
int b = 5;
// && 短路: a 为 0, 不计算 b
if (a && (b = 10)) { }
printf("b = %d\n", b); // 5 (b 未被修改)
// || 短路: a 为 0 (false), 需要计算 b
if (a || (b = 10)) { }
printf("b = %d\n", b); // 10
// 常见用法: 避免空指针解引用
if (ptr != NULL && *ptr > 0) { }6. 位运算符
6.1 按位运算符
| 运算符 | 描述 | 示例 |
|---|---|---|
& | 按位与 | 5 & 3 → 1 |
| ` | ` | 按位或 |
^ | 按位异或 | 5 ^ 3 → 6 |
~ | 按位取反 | ~5 → -6 |
<< | 左移 | 5 << 1 → 10 |
>> | 右移 | 5 >> 1 → 2 |
6.2 位运算示例
// 5 = 0101
// 3 = 0011
// --------
// & : 0001 = 1
// | : 0111 = 7
// ^ : 0110 = 6
uint8_t a = 5, b = 3;
printf("5 & 3 = %u\n", a & b); // 1
printf("5 | 3 = %u\n", a | b); // 7
printf("5 ^ 3 = %u\n", a ^ b); // 6
printf("~5 = %d\n", ~a); // -6 (0xFA 解释为有符号)6.3 移位运算
uint8_t x = 5; // 00000101
// 左移: 相当于乘以 2^n
printf("5 << 1 = %u\n", x << 1); // 10 (00001010)
printf("5 << 2 = %u\n", x << 2); // 20 (00010100)
// 右移: 相当于除以 2^n
printf("5 >> 1 = %u\n", x >> 1); // 2 (00000010)
// 有符号右移: 符号扩展
int8_t y = -8; // 11111000
printf("-8 >> 1 = %d\n", y >> 1); // -4 (11111100)6.4 位操作技巧
// 设置第 n 位
x |= (1 << n);
// 清除第 n 位
x &= ~(1 << n);
// 切换第 n 位
x ^= (1 << n);
// 检查第 n 位
if (x & (1 << n)) { }
// 获取最低位的 1
x & (-x);
// 清除最低位的 1
x & (x - 1);
// 判断是否为 2 的幂
(x != 0) && ((x & (x - 1)) == 0);6.5 位域标志
#define FLAG_READ (1 << 0) // 0001
#define FLAG_WRITE (1 << 1) // 0010
#define FLAG_EXEC (1 << 2) // 0100
unsigned int flags = 0;
// 设置标志
flags |= FLAG_READ | FLAG_WRITE;
// 检查标志
if (flags & FLAG_READ) {
printf("可读\n");
}
// 清除标志
flags &= ~FLAG_WRITE;
// 切换标志
flags ^= FLAG_EXEC;7. 运算符优先级
| 优先级 | 运算符 | 结合性 |
|---|---|---|
| 1 (最高) | () [] -> . | 左到右 |
| 2 | ! ~ ++ -- (type) * & sizeof | 右到左 |
| 3 | * / % | 左到右 |
| 4 | + - | 左到右 |
| 5 | << >> | 左到右 |
| 6 | < <= > >= | 左到右 |
| 7 | == != | 左到右 |
| 8 | & | 左到右 |
| 9 | ^ | 左到右 |
| 10 | ` | ` |
| 11 | && | 左到右 |
| 12 | ` | |
| 13 | ?: | 右到左 |
| 14 | = += -= ... | 右到左 |
| 15 (最低) | , | 左到右 |
建议: 使用括号明确优先级, 提高可读性.
// 不推荐
if (flags & FLAG_READ == FLAG_READ) { } // 错误! == 优先级高于 &
// 推荐
if ((flags & FLAG_READ) == FLAG_READ) { }8. 练习
8.1 位计数
编写函数计算一个整数的二进制表示中 1 的个数.
8.2 字节交换
编写函数交换一个 32 位整数的字节顺序 (大小端转换).
8.3 类型转换陷阱
分析以下代码的输出并解释原因:
unsigned int u = 1;
int i = -2;
printf("%d\n", u + i > 0);9. 思考题
- 为什么有符号整数溢出是未定义行为?
- 浮点数为什么不能精确表示 0.1?
- 逻辑运算符和位运算符有什么区别?
- 右移有符号负数的结果是什么?
- 为什么位运算比乘除法快?
10. 本周小结
- 整数存储: 二进制补码.
- 浮点存储: IEEE 754.
- 类型转换: 隐式转换, 显式转换, 整数提升.
- 算术运算符: 加减乘除, 取模, 自增自减.
- 位运算符: 与或非异或, 移位.
- 运算符优先级: 使用括号明确.
理解数据在内存中的表示, 是掌握 C 语言的基础. 位运算是系统编程的必备技能.