写高质量的C语言工程的规范

添加更多的编译选项(comiler options)来防止bug

对于我常用的GCC, 推荐开启一下的compiler options:

  • -Wall: enable a lot of common warnings

  • -Wno-format-truncation: warns about the snprintf output buffer not being large enough for a corresponding “%s” in the format string.

  • -Werror: turn warnings into errors.

 

动态申请的空间到底要不要释放

When using a barebones embedded OS, you absolutely need to tightly manage your memory.

但是, 如果你是写应用业务的代码, 特别是在内存足够的场景下. 最好不要手动释放内存, 因为当线程/进程退出时, 操作系统会自动帮我们释放. 某些情况下, 释放内存的操作会很大程度上增加逻辑的复杂度.

如果你是一个内核程序员, 则必须手动的释放. 不用怀疑.

 

尽可能在创建变量时赋初值

放置某些变量创建后是 magic value. 而使用这些变量可能不会立马导致错误, 但是这是一个隐患.

但这会产生一个问题, 有时我们定义变量之后的不久之后就会对其赋予正确的值, 这时候初值就是 多余的. 而且维护者可能认为这个值是meaningful, 这就要求我们如果要赋初值, 就要说明这个值 仅仅是无意义的初值.

 

使用#define, enum

对于代码在不同地方使用的同一个值, 应使用#define来声明使得代码maintainable.

如果这些值有多个且能规划为同一类别, 则还可将#define的方式换为enum. 这会使代码更加meaningful

使用enum使还要注意其所占内存空间在不同架构中可能不同的问题, see enum的优势和漏洞

 

使用typedef优化function pointer

 

重定义一套自己的类型

在开发大项目时, 需要考虑可移植性的情况下, 最好利用typedef对类型进行重定义.

#if SYSTEM1
    typedef  int  INT32;
    ...
#else
    typedef  long INT32;
    ...
#endif

如上, 对于某些架构int类型可能不是32bit, 此时就要使用long. 这种定义的方式会保证我们的系统 在任何架构中都不会出现类型的bug. 而且也增加了代码的readability.

 

善用~0

在做嵌入式编程时, 有时在设置掩码(mask)或者其他情况会要用到全1的变量值, 你是否经常这样声明?

int mask = 0xffff;

暂且不谈int类型到底占多少字节的问题. 就像上面一样, 我们程序员经常忘记某个类型的大小, 而少添加了f. 会导致变量mask的值不是全1(32位情况下).

这是要变换一下思维, 使用~0的定义方法就可轻松化解, 无需管变量的类型是什么.

int mask = ~0;

 

合理的使用goto语句

在大学课堂中, 我们老师说过禁止使用goto语句, 但却没有给出明确的原因.

实际上, 合理的使用goto能够极大的减少程序的冗余度.

goto语句常用于程序出现错误要退出时, 可能有多个情况会使用重复的代码处理, 例如释放一些allocated memory. 相较于使用flag, 使用goto显然更加clearly and readability.

所以, 在面对重复的错误处理代码时, 想想能不能用goto进行优化. 当然, 避免过早优化.

注意, goto出现的场景其实很受限. Never use a backward goto or jump into control statements.

 

定义合理, 正确的结构体

结构体是C语言编程应用中常用的数据结构, 关于结构体也有许多要注意的点.

#1 Flexible Array Member

C99开始支持Flexible Array Member. 且看我lstring库的结构体定义:

struct str {
    int length;
    int size;
    char data[];   // Flexible array member - C99 only
};

对于这种不定长的数组元素, 我之前都是定义一个指针, 占用一个sizeof(char *)的空间. 而Flexible Array Member本身不占用空间. 需要在malloc时为他单独声明空间.

int n = 100
struct str *s = malloc(sizeof(struct str) + sizeof(char[n]));

这里也有一个小trick, 使用sizeof(char[n])sizeof(char) * n 更简洁!

# 2 Padding and Packed

有关结构体的大小, 和地址对齐的问题. 假设我有一个结构体如下:

struct mystruct_A
{
   char a; int b; char c;
} x;

Padding是编译器对结构体默认做的事情. 它会在成员之间插入一些 gap 来保证地址对齐:

struct mystruct_A {
    char a;
    char gap_0[3]; /* inserted by compiler: for alignment of b */
    int b;         /* int 在32位上其地址是4字节对齐的 */
    char c;
    char gap_1[3]; /* -"-: for alignment of the whole struct in an array */
} x;

除了保证每个成员的地址是对齐的, 整个结构体的地址也是按照其最大的成员类型来对齐, 即对齐到int(4字节).

如果你不想要这些 gap, 那么可以对结构体声明使用 __attribute__((__packed__))关键字. 整个结构体大小仅为6个字节.

struct __attribute__((__packed__)) mystruct_A {
    char a;
    int b;
    char c;
};

 

永远为你的函数设置error return value

一旦你的函数可能被其他人调用, 那么养成设置return value的习惯. 即便你现在的实现 并不会产生任何错误, 也请返回success.

这样做的原因是, caller可以根据你的定义做错误判断, 即便以后你的实现加上了出错情况, 上层的代码也不需要修改.

 

变量类型的选择

  • 名字, 特定不变的字符串使用const char *, 甚至const char const*
  • 长度使用size_t
  • 表示类型的参数尽可能使用enum
  • 循环变量i使用signed, 避免溢出后出错

 

Reference

How I Improve My (C) Code Quality

Ten Fallacies of Good C Code


创建于: 2022-06-14T17:59:22, Lastmod: 2023-09-24T18:08:59