0.规则
<The Elements of Programming Style>
<The Elements of Style>程序员
1.假想的编译程序
(1)使用编译器提供的全部的可选警告设施数据库
加强类型静态检查的能力
eg: void* memchr(const void* str, int ch, int size);
那个调用该函数时,即便互换其字符ch和大小size参数,编译器也不会发出警告安全
可是若是在函数原型中使用更加精确的类型,就能够加强原型提供的错误检查能力
void* memchr(const char* str, unsigned char ch, size_t size);函数
注:引入无符号数能够加强类型检查能力,可是也致使 无符号数带来的隐式转换错误(有符号数必须转为无符号数)工具
(2)使用lint等静态检查工具来检查编译器漏掉的错误
(3)若是有单元测试,就进行单元测试单元测试
2.本身设计并使用断言测试
/* memcpy v1: 拷贝不重叠的内存块 */ void* memcpy(void* to, const void* from, size_t size) { void* pto = (char*)to; const void* pfrom = (const char*)from; if (pto == NULL || pfrom == NULL) { fprintf(stderr, "Bad args in memcpy\n"); abort(); } while (size-- > 0) *pto++ = *pfrom++; return pto; }
/* memcpy v2: 拷贝不重叠的内存块 */ void* memcpy(void* to, const void* from, size_t size) { void* pto = (char*)to; const void* pfrom = (const char*)from; #ifdef DEBUG if (pto == NULL || pfrom == NULL) { fprintf(stderr, "Bad args in memcpy\n"); abort(); } #endif while (size-- > 0) *pto++ = *pfrom++; return pto; }
既要维护程序的 release 版本,又要维护程序的 debug 版本
利用#ifdef DEBUG 调试宏这种方法的关键是保证调试代码不会在最终产品中出现编码
/* memcpy v3: 拷贝不重叠的内存块 */ void* memcpy(void* to, void* from, size_t size) { void* pto = (char*)to; const void* pfrom = (const char*)from; assert(pto != NULL && pfrom != NULL); while (size-- > 0) *pto+= = *pfrom++; return pto; }
尽量多的使用断言,及早发现错误:
必须使用断言对函数的每一个指针参数进行检查
必须当即使用断言对获取的资源(malloc获取的指针, fopen获取的FILE指针, 打开的数据库链接,获取的文件描述符等等)进行安全检查spa
/* memcpy v4: 拷贝不重叠的内存块 */ void* memcpy(void* to, const void* from, size_t size) { void* pto = (char*)to; const void* pfrom = (const char*)from; assert(pto != NULL && pfrom != NULL); assert(pto >= pfrom + size || pfrom >= pto + size); /* 检查是否重叠 */ while (size-- > 0) *pto++ = *pfrom++; return pto; }
在程序中使用断言检查语法中未定义行为特性的非法使用debug
3.为子系统设防
内存管理程序,可能犯的错误:
a.分配一个内存块并使用未经初始化的内容
b.释放一个内存块但继续引用其中的内容
c.调用realloc对一个内存块进行扩展,所以原来的内容发生了存储位置的变化,但程序引用的还是原来存储位置的内容
d.分配一个内存块后当即"失去"了它,由于没有保存指向所分配内存块的指针
e.读写操做越过了所分配内存块的边界
f.没有对错误状况进行检查
/* new_memory v1 : 分配一个内存块 */ int new_memory(void** ptr, size_t size) { unsigned char** p = (unsigned char**)ptr; *p = (unsigned char*)malloc(size); return (*p != NULL); }
而后以下调用:
if (new_memory(&block, 32))
成功,block指向所分配的内存块
else
不成功,block等于NULL
根据ANSI标准,调用malloc存在两处未定义行为,必须加以处理:
a.分配长度为0时,结果未定义
b.malloc分配成功,返回的内存块的内容未定义,能够是0,也能够是随意的信息
/* new_memory v2: 分配一个内存块 */ /* 加上内存块大小的检查和内存块的填充初始化 */ #define INIT_VALUE 0xA3 int new_memory(void** ptr, size_t size) { unsigned char** p = (unsigned char**)ptr; assert(ptr != NULL && size != 0); *p = (unsigned char*)malloc(size); #ifdef DEBUG { if (*p != NULL) memset(*p, INIT_VALUE, size); } #endif return (*p != NULL); }
在程序的调试版本中保存额外的信息,就能够提供更强的错误检查
只要相应的 release 版本可以知足要求,就能够在debug版本加入尽量多的调试代码来检查错误
4.对程序进行逐条跟踪
5.糖果机界面
/* strdup: 为一个字符串创建副本 */ char* strdup(const char* str) { char* newstr = (char*)malloc(strlen(str) + 1); assert(newstr); strcpy(newstr, str); retrun newstr; }
不要把错误标志和有效数据混杂在一块儿返回
int resize_memory(void** ptr, size_t newsize) { unsigned char** p = (unsigned char**)ptr; unsigned char* presize = (unsigned char*)realloc(*p, newsize); if (presize != NULL) *p = presize; return (presize != NULL); }
一个函数只干一件事,编写功能单一的函数,而不是多功能集一身的函数
反面教材: void* realloc(void** ptr, size_t size);
该函数改变先前已分配的内存块大小:
a.若是新请求大小小于原来长度,realloc释放该块尾部多余的内存空间,返回的ptr不变
b.若是新请求大小大于原来长度,扩大后的内存块有可能被分配到新地址处,该块的原有内容被拷贝到新的位置,返回的指针指向扩大后的内存首地址,而且新内存块扩大部分未经初始化
c.若是知足不了扩大内存块的请求,realloc返回NULL,当缩小内存块时,老是成功的
d.若是ptr == NULL 则realloc的做用至关于调用 malloc(size);
e.若是ptr != NULL 且 size == 0 则realloc的做用至关于调用 free(ptr);
f.若是ptr == NULL 且 size == 0 则realloc结果未定义
在容许大小为0的参数时要特别当心,一开始就要为函数的输入选择严格的定义,并最大限度地利用断言
为了程序的易读性和扩充性,不要使用布尔类型做为函数的参数类型
6.风险事业
ANSI并无标准化 char,int,long 这样的基本数据类型
ANSI没有标准化 基本数据类型的缘由:C语言产生于70年代,等到标准化时已经有了20多年写出来的代码基,定义严格的标准将会使大量现存代码无效
char* strcpy(char* pto, const char* pfrom) { char* ptr = pto; while ((*pto ++ = *pfrom++) != '\0') NULL; return ptr; }
上述代码在任何编译系统上均可以正确工做
int strcmp(const char* left, const char* right) { for (NULL; *left = *right; left++, right++) { if (left == '\0') return 0; } return ((*left < *right) ? -1 : 1); }
上述代码因为最后一行的比较操做而失去了可移植性。修改strcmp,只需声明 left 和 right 为 unsigned char 指针,或者直接在比较中先使用强制转型
(*(unsigned char*)left < *(unsigned char*)right)
for (unsigned char ch = 0; ch <= UCHAR_MAX; ch++) array[ch] = ch;
若是 ch = UCHAR_MAX 时,执行最后一次循环,循环以后,ch增长为 UCHAR_MAX + 1, 这将引发ch上溢为0,所以该循环变成了无限循环
if (n < 0) n = -n;
这段代码可能会出现bug. 在二进制补码系统中,数据类型的表达范围不是对称的,例如 char [-128, 127) 若是n正好为最小负数,则 n = -n 则会上溢
7.编码中的假象
不要引用不属于你的未知存储区,"引用"意味着不只读并且要写,这样可能会和别的进程产生难以想象的相互做用
/* unsign_to_str: 将无符号数转换为字符串 */ void unsign_to_str(unsigned u, char* str) { char* start = str; while (u > 0) { *str++ = (u % 10) + '\0'; u /= 10; } *str = '\0'; reverse_string(start); }
上述代码 是反向顺序导出数字,确正向顺序创建字符串,因此须要 reverse_string 来重排数字顺序
void unsign_to_str(unsigned u, char* str) { assert(u < UMAX); /* 将每一位数字从后往前存储, 字符串足够大以便能存储 u 的最大可能值 */ char* ptr = &str[5]; /* 假设 u <= 65536 */ *ptr = '\0'; while (u > 0) { *(--ptr) = (u % 10) + '\0'; u /= 10; } strcpy(str, ptr); }
函数能正确工做是不够的,还必须可以防范程序员产生明显的错误
尽可能慎用静态(或全局)存储区传递数据
紧凑的C代码并不能保证获得高效的机器代码,首先应该考虑的是代码的正确性和可读性
8.剩下来的就是态度问题