因为数组能天然支持块号直接索引、保证内存连续与布局可控,而指针数组易碎片化且无法直接映射逻辑块号;模拟磁盘块应让数组元素即块内容(如char blocks1024),而非存储指针。

为什么不用指针而用数组模拟磁盘块
因为真实磁盘管理需要固定大小、连续编号、可随机访问的“块”单元,而动态分配的指针数组容易产生碎片、地址不连续、难以映射逻辑块号(如块号 block_id = 128),数组天然支持 blocks[block_id] 直接索引,且内存布局可控。
常见错误是直接用 std::vector<char></char> 存指针——这模拟的是“块地址表”,不是“块本身”。真正模拟块,得让数组元素就是块内容,比如 char blocks[1024][512] 表示 1024 个 512 字节的块。
- 块大小通常取 512、1024、4096 字节,需对齐硬件扇区或页边界
- 数组总大小 = 块数 × 块大小,必须在编译期或启动时确定(栈上用
std::array,堆上用new char[N * BLOCK_SIZE]) - 避免用
std::vector<:array>></:array>—— 每次push_back可能触发复制,破坏块地址稳定性
如何用一维数组实现二维块寻址
堆上分配大块内存更灵活,但要自己算偏移。假设定义 char* disk = new char[TOTAL_BLOCKS * BLOCK_SIZE],那么块 i 的起始地址是 &disk[i * BLOCK_SIZE],不是 &disk[i]。
错误写法:memcpy(buf, &disk[i], BLOCK_SIZE) —— 这只拷了 1 字节;正确写法必须乘以块大小。
立即学习“C++免费学习笔记(深入)”;
- 封装成内联函数更安全:
inline char* get_block(int i) { return &disk[i * BLOCK_SIZE]; } - 访问越界极易发生:检查
i >= 0 && i ,否则可能读写相邻块甚至崩溃 - 若块大小非 2 的幂(如 512),除法取模性能尚可;但若为 4096,可用位运算
i 加速(前提是严格对齐)
怎么模拟空闲块链表(Free List)
不能真用指针连内存块(因为块内容属于数据区,不能同时当指针用),而是用“块内前 4 字节存下一个空闲块号”的方式,即用块自身存储元数据。
例如:块 free_head = 3,blocks[3] 的前 4 字节是 7,表示下一个是块 7;blocks[7] 前 4 字节是 -1 表示结尾。这就是典型的“数组模拟链表”。
- 初始化时,把所有块号串成链:从
i = TOTAL_BLOCKS-1往 0 遍历,*(int*)get_block(i) = next - 分配时取头节点:
int b = free_head; free_head = *(int*)get_block(b);,记得清零新分配块(可选) - 释放时插回头部:
*(int*)get_block(b) = free_head; free_head = b; - 注意字节序和对齐:
int在某些平台不是 4 字节或未对齐,建议用int32_t并确保块首地址 % 4 == 0
读写时如何避免覆盖元数据
每个块既要存用户数据,又要存管理信息(如空闲链指针、校验和、引用计数),必须划分区域。典型做法是“块头 + 数据体”,比如前 16 字节为头,后 BLOCK_SIZE - 16 字节为数据。
错误是直接把文件内容 memcpy 到 get_block(i) —— 这会冲掉块头。正确做法是 memcpy 到 get_block(i) + HEADER_SIZE。
- 定义常量:
constexpr size_t HEADER_SIZE = sizeof(int32_t) + sizeof(uint16_t); // next + checksum - 写数据前先更新头:
*(int32_t*)get_block(i) = next_block;,再写数据体 - 读数据后应校验头(比如简单异或校验),防止块被意外篡改
- 如果块要支持多版本或日志,头区还得加时间戳、事务 ID 等字段,预留空间比硬编码更稳妥
实际用起来最易忽略的是块头与数据体的边界对齐——尤其当 HEADER_SIZE 是奇数或未考虑 CPU 缓存行(64 字节)时,跨缓存行写入会显著拖慢性能。模拟本身不难,难在把边界、对齐、一致性这些细节钉死。


















