第一部分:进制转换、编码原理与位运算
1. 进制基础与互转方法
1.1 四种常用进制对照
| 进制 | 前缀/标识 | 基数 | 数码范围 | C语言表示 |
|---|
| 二进制 | 0b (C++14/C23) | 2 | 0, 1 | 0b1010 |
| 八进制 | 0 | 8 | 0-7 | 012 |
| 十进制 | 无 | 10 | 0-9 | 10 |
| 十六进制 | 0x / 0X | 16 | 0-9, A-F | 0xA |
1.2 核心互转规则
⭐ 二 ↔ 八:三位一组
- 二进制 → 八进制:从右向左每3位二进制对应1位八进制。
- 八进制 → 二进制:每1位八进制展开为3位二进制。
二进制: 011 101 010
八进制: 3 5 2 → 0352
验证: 0352(八) = 3×64 + 5×8 + 2 = 234(十)
⭐ 二 ↔ 十六:四位一组
- 二进制 → 十六进制:从右向左每4位二进制对应1位十六进制。
- 十六进制 → 二进制:每1位十六进制展开为4位二进制。
二进制: 1111 1010 0011
十六进制: F A 3 → 0xFA3
验证: 0xFA3(十六) = 15×256 + 10×16 + 3 = 4003(十)
⭐ 十 → 二/八/十六:除基取余法
例: 234 → 二进制
234 ÷ 2 = 117 ... 0
117 ÷ 2 = 58 ... 1
58 ÷ 2 = 29 ... 0
29 ÷ 2 = 14 ... 1
14 ÷ 2 = 7 ... 0
7 ÷ 2 = 3 ... 1
3 ÷ 2 = 1 ... 1
1 ÷ 2 = 0 ... 1
逆序读取: 11101010 → 0xEA
⭐ 二/八/十六→十:按位数乘以进制的次幂
⭐ 老师强调
- 计算机内部只存储二进制,八进制和十六进制只是人类可读的“缩写”。
- 十六进制最常用:1位十六进制 = 4位二进制,与字节(8bit)天然对齐,两个十六进制位恰好表示一个字节。
- 调试内存、查看寄存器、网络协议分析时,几乎全部使用十六进制。
2. 原码、反码、补码
2.1 三种编码定义(以8位有符号整数为例)
| 编码 | 正数规则 | 负数规则 | 示例(+5 / -5) |
|---|
| 原码 | 最高位符号位(0),其余为数值 | 最高位符号位(1),其余为数值 | 00000101 / 10000101 |
| 反码 | 与原码相同 | 符号位不变,数值位按位取反 | 00000101 / 11111010 |
| 补码 | 与原码相同 | 反码 + 1 | 00000101 / 11111011 |
2.2 ⭐ 为什么计算机用补码?
- 统一加减法:
A - B 等价于 A + (-B的补码),硬件只需一个加法器。 - 消除±0歧义:原码中
+0=00000000、-0=10000000;补码中0唯一表示为 00000000。 - 多表示一个数:8位补码范围
-128 ~ +127,比原码多一个 -128(10000000)。
2.3 补码运算示例
计算: 5 + (-3) = ?
5 的补码: 00000101
-3 的补码: 11111101 (-3原码10000011 → 反码11111100 → +1 = 11111101)
─────────────────────
相加结果: 00000010 (进位1自然丢弃)
00000010 = +2 ✅ 结果正确
⭐ 老师强调
- C语言中所有有符号整数都以补码形式存储和运算。
- 右移
>> 对有符号数是算术右移(高位补符号位),对无符号数是逻辑右移(高位补0)。 -1 的补码是全1(0xFFFFFFFF),这在位运算中经常作为掩码使用。- 溢出时补码运算仍然正确(模运算性质),但C标准中有符号溢出是未定义行为,实际依赖硬件补码实现。
第二部分:动态内存分配与指针进阶
4. 动态内存分配基础
4.1 静态数组 vs 动态数组
// 栈上静态分配:大小编译期确定
int arr[5];
// 堆上动态分配:大小运行时决定
int *arr2 = (int *)malloc(10 * sizeof(int));
4.2 完整代码示例:一维动态数组
#include <stdio.h>
#include <stdlib.h>
int main() {
int arr[5]; // 三段式普通数组(栈)
int *arr2; // 单纯定义int指针
arr2 = (int *)malloc(10 * sizeof(int)); // 手动开辟堆内存
if (arr2 == NULL) {
printf("内存分配失败!\n");
return 1;
}
// ⭐ 两种等价访问方式
arr2[0] = 11; // 下标法
*(arr2 + 2) = 33; // 完全等价于 arr2[2] = 33;
arr2[8] = 36;
printf("%d\n", arr2[0]); // 11
printf("%d\n", *(arr2 + 2)); // 33
printf("%d\n", arr2[8]); // 36
free(arr2); // ⭐ 用完必释放
arr2 = NULL; // ⭐ 防野指针
return 0;
}
⭐ 老师强调
arr2[i] 与 *(arr2 + i) 完全等价,编译器内部自动转换。malloc 返回 void*,C语言可隐式转换,但显式强转更安全、更清晰。- 必须检查返回值是否为
NULL。 free 后立即置 NULL,防止悬空指针误用。
5. 二级指针与动态二维数组
5.1 内存模型
int **arr
│
▼
┌──────────┐
│ arr[0] ──────▶ [10个int] ← malloc(10*sizeof(int))
│ arr[1] ──────▶ [25个int] ← malloc(25*sizeof(int))
│ arr[2] ──────▶ [7个int] ← malloc(7*sizeof(int))
│ arr[3] │
│ arr[4] │
└──────────┘
↑ malloc(5*sizeof(int*))
⭐ 关键点:行指针数组连续,但各行数据块在堆上独立分配、不一定相邻,支持不规则(锯齿)数组。
5.2 完整代码示例
#include <stdio.h>
#include <stdlib.h>
int main() {
// 第一步:分配行指针数组
int **arr = (int **)malloc(5 * sizeof(int *));
if (!arr) return 1;
// 第二步:逐行独立分配(长度可不同)
arr[0] = (int *)malloc(10 * sizeof(int));
arr[1] = (int *)malloc(25 * sizeof(int));
arr[2] = (int *)malloc(7 * sizeof(int));
// 验证
arr[0][0] = 100;
arr[1][24] = 200;
arr[2][6] = 300;
printf("%d %d %d\n", arr[0][0], arr[1][24], arr[2][6]);
// ⭐ 第三步:从内向外释放
free(arr[0]);
free(arr[1]);
free(arr[2]);
free(arr);
return 0;
}
⭐ 老师强调
- 释放顺序不可逆:先释放每一行,最后释放指针数组。反过来会导致行内存永久泄漏。
- 两次解引用
arr[i][j],中间任一层为 NULL 即崩溃,调试时分层检查。
6. sizeof 对动态数组的陷阱
6.1 错误示范
int *arr2 = (int *)malloc(10 * sizeof(int));
int length = sizeof(arr2) / sizeof(int); // ❌ 结果是指针大小/int大小,不是10
6.2 原因与正确做法
| 场景 | sizeof 返回值 | 能否获取长度 |
|---|
静态数组 int a[10] | 整个数组字节数 | ✅ |
动态指针 int *p | 指针自身字节数(4/8) | ❌ |
函数参数 int a[] | 退化为指针 | ❌ |
✅ 唯一可靠方式:用变量显式记录长度。
int len = 10;
int *arr2 = (int *)malloc(len * sizeof(int));
// 后续所有操作使用 len
附录:本节课核心易错点速查表
| 类别 | 易错点 | 正确做法 |
|---|
| 进制 | 二↔八/十六分组方向搞反 | 始终从右向左分组 |
| 编码 | 混淆原码/反码/补码转换 | 负数补码 = 反码+1,记住即可 |
| 内存 | malloc不检查NULL | 分配后必判空 |
| 内存 | free后置悬空指针 | free后立即赋NULL |
| 内存 | 二维数组释放顺序错误 | 从内向外,先行后指针数组 |
| 内存 | sizeof算动态数组长度 | 用变量手动记录 |