编写可读可维护代码,如何编写好代码

1 防御性编程

不相信外界的输入/其他函数传递的参数, 通过断言提前拦截非预期情况(c语言也有断言)。

#include <assert.h>
// ...
int main(int argc, char *argv[]) {
  PC = 0; R[0] = 0;
  assert(argc >= 2);  // 要求至少包含一个参数
  FILE *fp = fopen(argv[1], "r");
  assert(fp != NULL); // 要求argv[1]是一个可以成功打开的文件
  int ret = fseek(fp, 0, SEEK_END);
  assert(ret != -1); // 要求fseek()成功
  long fsize = ftell(fp);
  assert(fsize != -1); // 要求ftell()成功
  rewind(fp);
  assert(fsize < 1024); // 要求程序大小不超过1024字节
  ret = fread(M, 1, 1024, fp);
  assert(ret == fsize); // 要求完全读出程序的内容
  fclose(fp);
  while (!halt) { inst_cycle(); }
  return 0;
}

1.1 改进1: 让断言失败时输出更多信息

定义Assert,实现assert被触发时,可以打印一句话信息出来。

#define Assert(cond, format, ...) \
  do { \
    if (!(cond)) { \
      fprintf(stderr, format "\n", ## __VA_ARGS__); \
      assert(cond); \
    } \
  } while (0)

int main(int argc, char *argv[]) {
  PC = 0; R[0] = 0;
  Assert(argc >= 2, "Program is not given");  // 要求至少包含一个参数
  FILE *fp = fopen(argv[1], "r");
  Assert(fp != NULL, "Fail to open %s", argv[1]); // 要求argv[1]是一个可以成功打开的文件
  int ret = fseek(fp, 0, SEEK_END);
  Assert(ret != -1, "Fail to seek the end of the file"); // 要求fseek()成功
  long fsize = ftell(fp);
  Assert(fsize != -1, "Fail to return the file position"); // 要求ftell()成功
  rewind(fp);
  Assert(fsize < 1024, "Program size exceeds 1024 Bytes"); // 要求程序大小不超过1024字节
  ret = fread(M, 1, 1024, fp);
  Assert(ret == fsize, "Fail to load the whole program"); // 要求完全读出程序的内容
  fclose(fp);
  while (!halt) { inst_cycle(); }
  return 0;
}

1.2 改进2: 输出库函数错误原因

#include <string.h>
#include <errno.h>

#define Perror(cond, format, ...) \
  Assert(cond, format ": %s", ## __VA_ARGS__, strerror(errno))

int main(int argc, char *argv[]) {
  PC = 0; R[0] = 0;
  Assert(argc >= 2, "Program is not given");  // 要求至少包含一个参数
  FILE *fp = fopen(argv[1], "r");
  Perror(fp != NULL, "Fail to open %s", argv[1]); // 要求argv[1]是一个可以成功打开的文件
  int ret = fseek(fp, 0, SEEK_END);
  Perror(ret != -1, "Fail to seek the end of the file"); // 要求fseek()成功
  long fsize = ftell(fp);
  Perror(fsize != -1, "Fail to return the file position"); // 要求ftell()成功
  rewind(fp);
  Assert(fsize < 1024, "Program size exceeds 1024 Bytes"); // 要求程序大小不超过1024字节
  ret = fread(M, 1, 1024, fp);
  Assert(ret == fsize, "Fail to load the whole program"); // 要求完全读出程序的内容
  fclose(fp);
  while (!halt) { inst_cycle(); }
  return 0;
}

errno.h:报告库函数执行错误的原因。

2 减少代码中的隐含依赖

破坏隐藏依赖会出现bug(例如这里改了,那里忘记修改)

uint8_t M[512];
Assert(fsize < 1024, "Program size exceeds 1024 Bytes");
ret = fread(M, 1, 1024, fp);  // BUG: 忘了改, 可能发生缓冲区溢出!

第二行的1024忘记修改为512,导致fsize数据大于512,存到内存时候,会出现溢出,出现不确定性问题。
不要让上面的代码有相关性。

#define MSIZE 1024
uint8_t M[MSIZE];
// 另一种方式
uint8_t M[1024];
#define MSIZE (sizeof(M) / sizeof(M[0]))
Assert(fsize < MSIZE, "Program size exceeds %d Bytes", MSIZE);
ret = fread(M, 1, MSIZE, fp);
  • 不言自明 ✅ - 代码中可能还有其他1024
  • 不言自证 ✅ - 不要自信地认为“改的时候我会记得”
    • 面对几十个文件, 几千行代码, 你不会记得的

3 将定义放在头文件

随着项目规模增长, 需要分成多个文件来管理

// main.c
#define MSIZE 512
ret = fread(M, 1, MSIZE, fp);

// inst.c
#define MSIZE 1024 // BUG: 这里忘了改
assert(PC < MSIZE);
uint32_t inst = *(uint32_t *)&M[PC];

更好的代码:

// yemu.h
#define MSIZE 512 // 一改全改

// main.c
#include "yemu.h"
ret = fread(M, 1, MSIZE, fp);

// inst.c
#include "yemu.h"
assert(PC < MSIZE);
uint32_t inst = *(uint32_t *)&M[PC];

4 拒绝复制粘贴

4.1 反面案例

指令模拟器,在addi基础上,新加了xori、ori和andi三条指令。从addi复制过来,然后修改。出现问题很难发现。

if (((inst & 0x7f) == 0x13) && ((inst >> 12) & 0x7) == 0) { // addi
  if (((inst >> 7) & 0x1f) != 0) {
    R[(inst >> 7) & 0x1f] = R[(inst >> 15) & 0x1f] +
      (((inst >> 20) & 0x7ff) - ((inst & 0x80000000) ? 4096 : 0));
  }
} else if (((inst & 0x7f) == 0x13) && ((inst >> 12) & 0x7) == 0x4) { // xori
  if (((inst >> 7) & 0x1f) != 0) {
    R[(inst >> 7) & 0x1f] = R[(inst >> 15) & 0x1f] ^
      (((inst >> 20) & 0x7ff) - ((inst & 0x80000000) ? 4096 : 0));
  }
} else if (((inst & 0x7f) == 0x13) && ((inst >> 12) & 0x7) == 0x6) { // ori
  if (((inst >> 7) & 0x1f) != 0) {
    R[(inst >> 7) & 0x1f] = R[(inst >> 15) & 0x1f] |
      (((inst >> 20) & 0x7ff) - ((inst & 0x80000000) ? 4096 : 0));
  }
} else if (((inst & 0x7f) == 0x13) && ((inst >> 12) & 0x7) == 0x4) { // andi
  if (((inst >> 7) & 0x1f) != 0) {
    R[(inst >> 7) & 0x1f] = R[(inst >> 15) & 0x1f] &
      (((inst >> 20) & 0x7ff) - ((inst & 0x80000000) ? 4096 : 0));
  }
} else if (...) {  ...  }

Copy-Paste = 编写相似代码时, 复制旧代码并稍作修改

  • 开发效率++, 维护难度+++++

上述代码不言自明本身就不怎么样, 不言自证就更难了

  • 需要看很久的代码, 基本上都很难做到不言自证
    • 当你粘贴出上百行这样的代码, 你很可能会改漏几处
    • 哪天你发现了一个共性的问题(例如立即数忘记符号扩展), 所有粘贴的代码都要修改
      • 改漏了 = bug
    • 而且你基本上没有耐心仔细看的 😂

粘贴一时爽, 调试火葬场 😈

4.2 编写可复用的代码

通过变量, 函数, 宏等消除重复/相似的代码

uint32_t inst = *(uint32_t *)&M[PC];
uint32_t opcode = inst & 0x7f;
uint32_t funct3 = (inst >> 12) & 0x7;
uint32_t rd  = (inst >> 7 ) & 0x1f;
uint32_t rs1 = (inst >> 15) & 0x1f;
uint64_t imm = ((inst >> 20) & 0x7ff) - ((inst & 0x80000000) ? 4096 : 0);
if (opcode == 0x13) {
  if      (funct3 == 0x0) { R[rd] = R[rs1] + imm; } // addi
  else if (funct3 == 0x4) { R[rd] = R[rs1] ^ imm; } // xori
  else if (funct3 == 0x6) { R[rd] = R[rs1] | imm; } // ori
  else if (funct3 == 0x7) { R[rd] = R[rs1] & imm; } // andi
  else { panic("Unsupported funct3 = %d", funct3); }
  R[0] = 0; // 若指令写入了R[0], 此处将其重置为0
} else if (...) {  ...  }
PC += 4;
  • 引入中间变量, 不言自明 ✅
  • 对齐的代码更容易阅读并发现错误, 不言自证 ✅

5 使用合适的语言特性

在这里插入图片描述
依据指令结构,采用结构体进行实现,减少发生错误的可能性。

typedef union {
  struct {
    uint32_t opcode  :  7;
    uint32_t rd      :  5;
    uint32_t funct3  :  3;
    uint32_t rs1     :  5;
     int64_t imm11_0 : 12;
  } I;
  struct { /* ... */ } R;
  uint32_t bytes;
} inst_t;

inst_t *inst = (inst_t *)&M[PC];
uint32_t rd  = inst->I.rd;
uint32_t rs1 = inst->I.rs1;
uint64_t imm = (int64_t)inst->I.imm11_0;
if (inst->I.opcode == 0b0010011) {
  switch (inst->I.funct3) {
    case 0b000: R[rd] = R[rs1] + imm; break; // addi
    case 0b100: R[rd] = R[rs1] ^ imm; break; // xori
    case 0b110: R[rd] = R[rs1] | imm; break; // ori
    case 0b111: R[rd] = R[rs1] & imm; break; // andi
    default: panic("Unsupported funct3 = %d", inst->I.funct3);
  }
  R[0] = 0; // 若指令写入了R[0], 此处将其重置为0
} else if (inst->bytes == 0x00100073) {  ...  }
  • struct和位域(bit field)
    把位抽取操作交给编译器
  • union
    可对数据解释成不同类型
  • 指针
    按指针类型对内存地址中的数据进行解释
  • switch-case语句
    替代对同一个变量的连续判断
  • 二进制常数(GNU dialect)
    可以直接抄手册了

6 什么是好代码(总结)

正确的代码 != 好代码

  • 好代码更大概率是正确的
    好代码的两条重要准则
  • 不言自明 - 仅看代码就能明白是做什么的(specification)
  • 不言自证 - 仅看代码就能验证实现是对的(verification)
    使用正确的编程模式写出好代码
  • 防御性编程 - 通过assert检查非预期行为
  • 减少代码中的隐含依赖 - 使得“打破依赖”不会发生
    • 头文件 + 源文件
  • 编写可复用的代码 - 不要Copy-Paste
  • 使用合适的语言特性 - 把细节交给语言规范和编译器
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

狮子座硅农(Leo ICer)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值