《编码checklist规范》学习笔记

《编码checklist规范》posts
  • 《编码checklist规范》学习笔记

    • 0 前言
    • 1 排版
      • 1.0 总则
      • 1.1 缩进
      • 1.2 语句行
      • 1.3 大括号
        • 范例
      • 1.4 代码行长度
    • 2 注释
      • 2.0 总则
      • 2.1 声明注释
      • 2.2 语句注释
        • 范例
      • 2.3 废弃代码
    • 3 标识符
      • 3.0 总则
      • 3.1 命名风格
      • 3.2 命名要求
      • 3.3 文件名
      • 3.4 魔数
        • 示例
      • 3.5 变量名称和用途匹配
      • 3.6 减小标识符的作用域和可见性:
        • 示例
      • 3.7 函数声明
    • 4 函数
      • 4.0 总则
      • 4.1 函数规模
      • 4.2 函数参数
      • 4.3 返回值
      • 4.4 const使用
      • 4.5 重复代码提炼成函数:
      • 4.6 格式化字符串的变参函数定义
    • 5 宏定义
      • 5.1 命名
      • 5.2 括号使用
      • 5.3 宏参数
      • 5.4 减少宏使用
      • 5.5 防止命名冲突
    • 6 结构体
      • 6.0 总则
      • 6.1 结构体对齐
      • 6.2 保证数据结构在跨平台时的二进制兼容性:
      • 6.3 变长结构体
    • 7语句
      • 7.0 总则
      • 7.1 括号使用
      • 7.2 goto使用限制:
      • 7.3 循环性能优化:
      • 7.4 不使用复杂表达式:
      • 7.5 switch/case语句:
      • 7.6 控制结构(if/for/while/switch等)的嵌套:
    • 8 错误处理
      • 8.0 总则
      • 8.1 参数合法性检测
      • 8.2 数据合法性检查:
      • 8.3 断言要求:
      • 8.4 “return - 返回值检查:
    • 9 资源管理
      • 9.0 总则
      • 9.1 防止泄露:
      • 9.2 使用配套的资源释放函数释放资源(重点)
      • 9.3 避免重复释放:
      • 9.4 勿混用内存管理方法:
      • 9.5 标准输入、输出、错误的关闭:
    • 10 内存
      • 10.0 总则
      • 10.1 变量初始化
      • 10.2 指针算术:
      • 10.3 结构体比较:
      • 10.4 字符串比较:
      • 10.5 防常量字符串修改:
      • 10.6 字符串格式化:
      • 10.7 防止字符串缺结束符
      • 10.8 字符串长度计算:
      • 10.9 变量大小、偏移计算:
      • 10.10 C99的变长数组和alloca:
      • 10.11 字符串转整数/浮点数:
    • 11 并发
      • 11.0 总则
      • 11.1 信号处理
      • 11.2 信号处理函数
      • 11.3 不暴力终止线程
      • 11.4 wait - 子进程/子线程的后事处理
      • 11.5 互斥锁的使用
      • 11.6 锁定区域内睡眠
      • 11.7 非递归锁的使用:
      • 11.8 死锁
      • 11.9 线程创建
      • 11.10 需要同步的访问:
    • 12 危险的库特性
      • 12.0 总则:
      • 12.1 错误号获取
      • 12.2 不混用文件机制
      • 12.3 不使用不安全函数:
    • 13 危险的语言特性
      • 13.0 总则
      • 13.1 自增/自减运算:
      • 13.2 参数顺序依赖性:
      • 13.3 char类型使用:
      • 13.4 除0错误预防(包括求余运算):
      • 13.5 指针转换:
      • 13.6 移位运算:
    • 14 工具检查
      • 14.0 总则:
      • 14.1 cppcheck:
      • 14.2 c++test(需编译):

0 前言

在学习完《C陷阱与缺陷》后,发现了很多以前没有注意到的错误,比如:字符串与字符的区别,指针函数与函数指针的区别等等。《C陷阱与缺陷》是本值得收藏、反复观看的书籍。本书《Checklist编码规范》与《C陷阱与缺陷》相辅相成,好的编码规范在编程中能减少很多你难以预料到的BUG,显著提高代码质量。

文中加粗的部分是我自认为比较重要且以前没有注意的部分。

1 排版

1.0 总则

总则: 统一清晰的排版可以帮助代码阅读者迅速聚焦代码的关键逻辑,迅速定位区块的开始结束位置,大大提高代码阅读的效率。主要注意以下几点:
  1. 排版风格在同一文件中,必须保持统一;
  2. 尽量把关系密切的逻辑集中在一起,保证视线无需漂移即可浏览到整个逻辑单元;
  3. 在尚未养成习惯之前,可采用astyle之类的工具软件格式化代码;

1.1 缩进

代码的缩进可以说是编程的灵魂了,良好的代码缩进阅读起来十分方便,但是如果没有缩进,代码压根不能看!

程序块要采用统一的缩进和对齐风格编写。整个项目中或者是4个空格,或者是一个TAB。不允许混用这两种。
如果是使用TAB,需保证TAB键的宽度是4个空格()。
如果是在原有代码上修改(如Linux内核代码),需和原有代码的缩进、对齐方式保持一致。
建议在编辑器设置中将TAB键宽度设置为4(大多数编辑器都有这个设置),建议统一使用空格进行对齐。
  • 缩进要求:

    • if else case for while语句需要缩进;
    • case语句与所属的switch语句对齐;
    • 所有{}需要缩进,extern “”C”“, namespace 块除外,case语句除外。
  • 空格使用:

    • 关键字 if else switch case for while 之后要加空格;
    • 如果,;后面没有立即换行, 即后面有变量或语句时, 要在后面加空格,类似for循环for(int a = 1; a < 100; a++)每个条件后都加了空格;
    • 小括号内侧不能有空格, 函数调用(或宏)的名字与括号之间不能有空格
    • 一元操作符 & * + - ~ ! ++ -- 要紧贴对应的变量不能有空格
    • 二元操作符 = + - * / % & | ^ == != >= <= > < ? : 两侧要加空格
    • 结构体成员操作符 . -> 前后不加空格
规范示例:
缩进的范例代码:
struct string_t {int len;char data[0];
};
//注意大括号的写法,但个人习惯这样写。其实大多数IDE都为你设置好了编码风格,但是如果用linux等就要自己注意了。
struct string_t
{int len;char data[0];
};
//-----------------------
#ifdef __cplusplus
extern "C" {
#endifstruct string_t *create_string(int max_len);
struct string_t *copy_string(const char *str);
void release_string(struct string_t *str);#ifdef __cplusplus
}
#endifstruct string_t *copy_string(const char *str)
{int len = strlen(str) + sizeof(struct string_t) + 1;struct string_t *pstr = (struct string_t *)malloc(len);if (!pstr)return NULL;pstr->len = len - sizeof(struct string_t) - 1;strcpy(pstr->data, str);return pstr;
}switch (state) {
case STATE_CONNECT:...break;
case STATE_LOGIN:...break;
case STATE_NORMAL:...break;
default:break;
}for (i = 0; i < cnt; ++i) {if (arr[i] > value) {list_add(list, arr[i]);}
}

1.2 语句行

一行只写一条语句,不允许把多个短语句写在一行中。大多数以分号算作一条语句。

1.3 大括号

大括号是编码中的灵魂,没有大括号就没有作用域。具体规范如下:
  • } 必须独占一行,有两种例外:

    • 如果是在if(…){}else if(…){}else{}中,可与else放同一行;
    • 如果是在do{}while(…)中,可与while放同一行;
  • {

    可以独占一行,且与上一语句的起始位置对齐。 也可以跟在相应的if、for、do、while、switch、class声明、函数声明后面;

  • {,} 的相对位置在整个模块中必须保持一致。 if、for、do、while、switch这几种语句块的{必须保持相对位置一致。 其他语句块的{只要求在同类型之间保持相对位置一致即可。

范例

以下这段代码符合checklist要求:
int find_split(const char* str)
{assert(str);int len = strlen(str);for (int i = 0 ; i < len ; ++i) {if (str[i] == ',' || str[i] == '.') {return i;}}return -1;
}
以下这几段代码不符合checklist要求:
  • if语句和for语句的{相对位置不一致(要统一,不要两种风格并存)
int find_split(const char* str)
{assert(str);int len = strlen(str);for (int i = 0 ; i < len ; ++i) {if (str[i] == ',' || str[i] == '.') {return i;}}return -1;
}
  • {没有和上一语句的起始位置对齐
int find_split(const char* str)
{assert(str);int len = strlen(str);for (int i = 0 ; i < len ; ++i) {if (str[i] == ',' || str[i] == '.') {return i;}}return -1;
}

1.4 代码行长度

每行代码不应超过80列。
如果某些行需要超出80列(比如调用win32 API时,由于API参数过多,往往会超出80列),应该折成多行显示。
例外条款:
  • 注释可以例外;
  • 如果字符串单独占一行仍超出80列,可以例外;

2 注释

注释对代码来说十分重要,可以使阅读代码的人能很好的理解代码。好的注释显得尤其重要,因为若干年后,代码作者可能自己都看不懂自己当年所写的代码是什么意思。

2.0 总则

注释的目的是提升代码可读性,帮助代码读者更快速的了解代码作者的实际意图。
  • 注释应重点阐述目的,而非过程;
  • 注释应重点阐述隐性知识,即代码无法直接反映的意图、原则,如扩展方法,锁策略,内存分配限制等等
  • 注释应重点阐述模块/函数之间的关联知识。 即对比分析多个函数才能得到的知识,比如:
    • 参数及返回值的含义、约束;
    • 外部数据的含义、取值范围;
    • 函数/模块之间的协作关系;
    • 多个变量/函数之间的相互关系;
    • ……
  • 注释应避免描述显而易见的知识,比如:“这是一个构造函数”,“定义一个整型变量”;
  • 注释内容需要和代码实际行为保持一致,不应涉及无关内容,如“今天天气很好”,“checklist规定这里要注释”;
  • 注释需要及时更新,反映代码当前的状态,否则反而误导代码读者; 本条款为阐述、建议性条款;

2.1 声明注释

变量的声明很重要,是对变量含义的进一步解释,而不是只是知道其是什么类型。平时自己写注释最多写下函数的作用,一般很少写的那么详细。但是长久来说这是值得的。

主要注意以下几点:
  • 文件头:在头文件(*.h,*.hpp,*.inc等)和源文件头部应注释说明该其功能;
  • 函数头:函数头部应注释说明其功能及各参数、返回值的含义(无参构造函数、析构函数、重载的运算符函数可无需注释);
  • 全局变量:全局变量应注释说明其功能;
  • 常量: 所有常量定义都应注释说明其功能;
  • 类型:所有类型定义(包括struct,class,enum,union),都应注释说明其功能;
  • 宏定义:所有宏定义应注释说明其功能,如果宏有参数,必须说明参数的用法;
注释采用doxygen的注释标准。方便根据注释直接生成说明文档。范例请参考: 
  • 文件头注释范例:
/*功能:本文件定义文件列表的接口,文件列表是个存储文件路径名和文件信息的列表,本文件提供了文件列表的存储、读取、定位、访问等接口。日期:2011-3-4作者:zbc
*/
#ifndef FILELIST_H_
#define FILELIST_H_
…
#endif //FILELIST_H_
  • 函数头注释范例:
/*** 发送数据给服务器* @param [in]buf  数据缓冲区指针* @param len   数据长度* @return <0表示失败,否则表示实际发送成功的字节数* @sample*  int len = send_buf(buf, buflen);*  if (len < 0) {*   DUMP("send failed, errno: %x\n", len);*   return -1;*  } else {*   DUMP("send ok, already send %d bytes.\n", len);*  }*/
int send_buf(const char* buf, int len);
  • 类型定义注释:
/***  文件列表类* @remark*  可用于记录系统所缓存的所有小文件,该列表可存在磁盘上* @note*/
class filelist {
...
};
  • 全局变量注释:
1)
char g_log_fname[MAX_PATH]; //日志文件的路径名称,从配置文件中读取得到
2)
//日志文件的路径名称,从配置文件中读取得到
char g_log_fname[MAX_PATH];
  • 宏注释:
/***    释放内存,并把指针清零,防止重复释放*    @param ptr 内存块指针,只允许传入malloc/remalloc/strdup等*                C库函数分配的内存块指针*/
#define FREE_ZERO(ptr)         \
do{                            \if (!(ptr)) {              \free(ptr);             \ptr = NULL;            \}                          \
}while(0)//  最大的缓冲区长度
#define MAX_BUF_SIZE 200

2.2 语句注释

主要是对控制结构进行注释,因为具体的跳转如果只通过代码就有点难理解

在下述控制结构处应按要求进行注释。语句块少于5行允许例外。
  • if语句的各个分支,注释说明条件和具体功能;
  • for/while/do的头部,注释说明循环条件和具体功能;
  • switch头部,注释说明判断条件和具体功能;

范例

  • if 语句注释:
// 将buf中保存的消息发送给CGI进程
ret = send(sk, buf, datalen, 0);
if (ret < 0) {                // 没有发送成功LOGDBG("send failed, errno: %d\n", errno);
} else if (ret == 0) {        // 对端关闭LOGDBG("send failed, peer shutdown\n");
} else {                      // 发送成功LOGDBG("send ok, length: %d\n", ret);
}
  • for 语句注释:
// 遍历文件列表,直到找到名字为filename的文件对象,或列表遍历完毕
for (int i = 0 ; i < cnt ; ++i) {if (strcmp(filename, filelists[i]->name) == 0)return i;
}
  • while/do…while 语句注释:
// 将所有用户配置文件打包
pdir = opendir("/var/sangfor");
if (!pdir) {...return -1;
}
// 遍历/var/sangfor目录下所有配置文件(*.conf),通过tar命令将其打到压缩包
while ((pcur = readdir(pdir)) != NULL) {...
}

2.3 废弃代码

确定不适用的功能代码要删除,或者通过注释和#if 0关闭。
#if 0   code
#endif
#if适用于较长的代码,且如果想让code生效,只需要把#if 0改为#if 1。但是千万不要把#if 0 来当作块注释使用, #if 1可以让其间的变量成为局部变量

3 标识符

变量命名相当重要,对代码的可读性至关重要。如果你写的代码所定义的变量都是a、b、c,那么谁知道其代表什么意思呢?一般的变量命名都是用英文单词,不熟悉英文的会用拼音或者拼音缩写代替。

3.0 总则

标识符声明的终极目的是达到“代码即文档”的效果。
所谓“代码即文档”,即由代码本身清晰的反映出作者的意图。
其中,最关键的就是利用标识符给一段代码/一段数据打个“标签”,说明这段代码是用来干嘛的,或者这段数据是用来干嘛的。
  • 标识符的命名应反映目的,而非过程;
  • 作用域越大,影响逻辑越多的标识符,其取名越完整。作用域越小、影响面越小的标识符,其取名越简练。
    • 如循环变量可使用无特殊含义的单字符i进行命名;
  • 标识符的命名规则应当统一;
  • 命名中不应包含对理解代码意图无帮助的部分,如:个人姓名,无意义字符等;

3.1 命名风格

命名风格在同一模块中统一。有三种风格可供选择:
  • UNIX的全小写加下划线的风格,如:create_file
  • 匈牙利命名法(大小写混排),如:CreateFile
  • Java风格,如 createFile
就本人而言,一般会匈牙利命名法和java风格混用。

3.2 命名要求

标识符使用1个或多个英文单词或其缩写进行命名。要求:
  • 不使用拼音;
  • 不使用无意义的字母组合;
  • 除循环变量可使用i、j、k,指针变量可使用p之外,不使用单字符的名字;
  • 不使用下划线开头;
  • 非静态全局变量使用g_开头;
  • 静态全局变量使用s_开头;
  • 类成员变量使用m_开头,等同于C结构体的(没成员函数的)可以例外,union可以例外;
  • 局部变量不加前缀;”
全局变量的命名学习了,还有类成员的,怪不得经常看到m开头的命名。

3.3 文件名

使用include包含的文件名全部使用小写。Windows界面相关的代码文件允许例外。

也就是说头文件定义一般用小写,而不是大写。这个我之前一直用错了。

3.4 魔数

用常量代替数字,比如定义数组长度,用max代表,而不是在需要定义数组长度的时候写数字。

不允许使用0,1,-1之外的魔数,有需要用到数字的地方,请用命名常量代替。
如果作为标识(比如状态标识)时,0,1,-1也不允许直接使用。
所谓魔数,指以字面值形式出现的数值常量(不包括字符串常量),比如3,-4,256,3.14,0.628。作为特例,以下情形是允许的:
  • 初始化一个变量时允许使用魔数,如int num = 20;timeval tv = {10, 10};
  • 定义替代魔数的命名常量时允许使用魔数,如#define PI 3.14;
  • 该魔数代表参数个数,且该参数个数无法通过sizeof等方法测量得到;(请参考注释)
  • 0、-1如果作为返回值,分别代表正常、出错,是允许的。1、0作为布尔值表示真、假,也是允许的。
  • 作为位运算或其他算法的固有参数时,允许使用魔数,但需注释说明;
  • 该魔数只跟当前语句有关(即无需与模块其它代码保持一致),且替换为标识符常量后对代码可读性没有明显提升,则允许直接使用魔数;
定义标识符常量替代魔数的原则是该标识符必须包含更丰富的信息,以提升代码可读性。
典型的可以指明上下文,指明常量的含义(如:USRCFG_USERNAME_MAXSIZE,指在用户配置中用到的用户名的最大长度)以下命名方式是常见的误区:
  • 直接在标识符常量中出现魔数本身,如:#define BUFSIZE_128 128
  • 使用含糊的语义,如:#define LEN 128
  • 把字符串常量也当成魔数,如:#define MKDIR_ERROR_STR ""mkdir error, errno(%d):%s\n"""

示例

所谓魔数: 指以字面值形式出现的数值常量。
  • 不可以这样:
return 2;
ouble area = 3.14*radius*radius;
mkdir(usrcfg_fname, 0660);  //创建文件,并指定权限,有时候别人不知道0660是什么意思,具体可看linux文件权限码
  • 可以这样:
1) 在头文件中有常量的统一定义:const int EOUTMEM = 2;  或#define EOUTMEM 2使用时引用已经定义好的常量:return EOUTMEM;
2) #define PI 3.14double area = PI*radius*radius;
3) #define USRCFG_UGO_RW_RW_NONE 0660mkdir(usrcfg_name, USRCFG_UGO_RW_RW_NONE);
  • 作为特例,允许使用魔数的情形:

    • 魔数代表参数个数:
int ret = sscanf(sz, "%u.%u.%u.%u", &ip1, &ip2, &ip3, &ip4);
if (ret != 4) {printf("read ip failed\n");
}
//该代码中,4代表读取到的参数个数。int sum(int num, int para1, ...);
int val = sum(3, num1, num2, num3);//该代码中,3代表传给sum的待累加的参数的个数。
  • 魔数代表位运算的固有参数:
ret = (val >> 4) & 0xff; //取val的4到8位
size = (size + 3) / 4 * 4; //4字节对齐

3.5 变量名称和用途匹配

主要是不要取无意义或无厘头的变量名
  • 变量的名字和实际用途相符,不使用和实际用途完全无关或相反的命名;
  • 在变量的作用域内,一个变量不用作多个用途;(请参考注释)
  • 如果变量有多个取值范围,且各取值范围代表不同意思,须保证各个取值范围之间不得有重叠;(请参考注释)
1) 完全无关的命名:
比如:count用于表示颜色;2) 名字和用途相反:
比如:free_cnt用于表示当前存活的对象数目;应该是当前释放的对象数目3) 一个变量多个用途:
一个实际引起了BUG的例子:value既作为入参表示哈希值,又作为出参,表示是否成功。
#define HASH_INSERT(hash_table, node, value, type)  \do {                                            \node->pre = NULL;                           \node->pnext = NULL;                         \unsigned int h_i_i = value % HASH_SIZE;     \if (hash_table[h_i_i] != NULL &&            \hash_table[h_i_i]->key != node->key) {  \...                                     \}else if (hash_table[h_i_i] == NULL) {      \hash_table[h_i_i] = node;               \} else {                                    \value = 0;                              \}                                           \} while (0);
4) 同一个用途,多次使用是允许的,如:
int ret = 0;
ret = init_pools();
ret = init_threads();
ret = init_plugins();
//都表示初始化是否成功
5) 不同取值范围代表不同意思,比如:
int find_string(const vector<string>& lst, const char* name);
find_string的功能是从字符串列表中查找是否存在字符串name,当返回值大于0时,表示name在字符串列表lst中的索引,==0表示未找到,<0时查找过程出错,并指示出错类型。如果0也是列表的合法索引值,则find_string违反了本条款的第3项,即:取值范围有重叠,0既可能是索引值,也可能代表没找到。

3.6 减小标识符的作用域和可见性:

1) 不允许在头文件中定义非静态全局变量,仅仅声明除外,例子见批注;
2) 不被别的编译单元访问的全局变量,必须声明为静态全局变量(即必须在定义和声明前加static修饰)
3) 不被别的编译单元访问的函数,必须加上static声明;
4) 不被别的类访问的成员函数,必须声明为private函数;
5) 只被派生类访问的成员函数,必须声明为protected函数;
如确有必要违反以上2、3、4、5项条款,注释说明清楚原因后可以例外。

有时候严格一点效果可能更好,这一段是必须记住的。

示例

  • 全局变量定义:
int g_debug;
int g_debug = 1;
  • 全局变量声明:
extern int g_debug;
  • 静态全局变量:
static int s_debug;

3.7 函数声明

在头文件里写声明,在c/cpp里写定义

1) 函数声明必须和函数定义的保持原型一致;
2) 引用其它模块或.c/.cpp文件提供的有外部链接特性的函数(extern函数), 应使用include头文件的方式引用其函数声明,不允许自行声明,不允许在.c/.cpp中直接声明;
3) 提供给.c文件使用的函数声明,必须放在extern “”C”” {}域内,并通过宏防止问题(见注释);”
  • 提供给c模块使用的函数原型声明:
#ifdef __cplusplus
extern "C" {
#endifvoid foo(int xxx, int yyy);#ifdef __cplusplus
}
#endif

4 函数

函数定义的好坏直接决定代码的质量,良好的软件设计应遵循“高内聚,低耦合”,函数的设计也应遵循此原则。

4.0 总则

函数是控制代码复杂度的最有效工具。
函数可以隔绝两段代码之间的相互影响,约束两段代码之间只能通过参数/返回值/全局变量来传递影响。
函数通过函数名给一段代码“打标签”,能帮助读者快速了解代码片段的意图,提升代码可读性;
  • 使用工具评估代码复杂度,常用的有SourceMonitor、CCCC等,常用的用于评价函数复杂度的指标有代码行数、圈复杂度等;
  • 降低函数参数个数,有助于提高函数的易用性,且降低函数实现的复杂度;
  • 减少函数内部相互作用的变量个数,有助于降低函数的复杂度;
  • 尽可能保证函数功能单一性,不做完全无关的两件事情,不做和函数名没有关系的事情;
  • 使用统一的错误处理模型,有助于提高代码易读性,精简代码的同时避免错误;
  • 时刻关注代码的冗余度,相似代码越多,意味着逻辑抽象程度越差,应想办法将相似逻辑提炼成函数;

4.1 函数规模

一个函数不超过100行,工具自动生成的除外。对既有的第三方代码(比如内核代码)进行修改可例外(新增函数依然不得超过100行)。

想起了以前自己写的函数,基本上功能丰富一点的都要超过好几百行了吧,应该把一些功能切割开来,实现低耦合!

4.2 函数参数

  • 函数调用传递大对象(超过8个字节大小,或者其构造函数会分配资源)时不使用按值传递。返回值允许例外。(也就是引用传递,直接引用原来的对象;返回值例外就是在函数里再定义一个对象,然后将其返回
  • 不允许在函数参数中使用布尔类型(包括使用数值类型仿制的布尔类型)。如有需要用到这类标志性参数,可用枚举代替,或者分拆成多个函数实现,具体见示例。
  • 函数参数中不得定义数组参数,应使用指针代替数组。请注意:如果参数是数组的指针不违反本条款。(也就是C缺陷里的数组即指针),如果数组定义没有指定数组长度,也可例外,如:int main(int argc, char *argv[])
  • 函数参数个数不超过5个(<=5)。
建议:
建议拆分函数功能,分成多个函数实现,这样每个函数功能更简单,参数更少。
如果内部没有复杂逻辑,可以通过结构体指针传参。以下函数声明违反checklist:
1) void func(string name);
//应该直接用引用,不要按值传递
2) struct record {int type;int len;char data[16];
};
void send_record(record rec);
//这个好像没什么问题啊
注2:
函数参数中使用布尔类型,会使代码更难以理解,如:
UpdateData(TRUE)
CreateProcess(chPath, "", NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)
如果不是对这些函数非常熟悉,你知道这些TRUE,FALSE代表什么意思吗?
如果改为UpdateData(SAVE_VALIDATE),CreateProcess(chPath, "", NULL, NULL, NO_INHERIT_HANDLE, 0, NULL, NULL, &si, &pi)会不会好一点?改造方法:UpdateData是MFC函数,原型如下:
BOOL UpdateData(BOOL bSaveAndValidate);下述修改方法都可以提高UpdateData的可读性:1)分拆成两个函数实现
BOOL UpdateData();
BOOL UpdateData_SaveAndValidate();2)将bSaveAndValidate参数改造成枚举
enum SaveAndValidate_e{SAVE_VALIDATE,NO_SAVE_VALIDATE
};
BOOL UpdateData(enum SaveAndValidate_e eSave);
这样,在调用这个函数时就有一个名字了,可以通过枚举的名字更好的理解这个函数调用的意思,如:UpdateData(SAVE_VALIDATE);
多使用枚举代替ture和false!
void test_arr_copy(int arr[ARRAY_SIZE])
{int buf[ARRAY_SIZE];memcpy(buf, arr, sizeof(arr));
}
以上代码违反子条款3,sizeof(arr)的结果为sizeof(int),而非sizeof(int)*ARRAY_SIZE
可改为:
void test_arr_copy(int *arr)
{
...
}
以下函数定义不违反本条款(因为p是指向数组的指针,而非数组):
void test_arr_copy(int (*p)[ARRAY_SIZE]);

4.3 返回值

一般自己使用返回值都是返回函数里定义的局部变量,原来这是不行的。

  • 不返回本函数内定义的非静态局部变量的地址(包括以指针或引用形式返回)。也不允许通过出参或全局变量、类成员变量的方式返回;
  • 在linux平台,如果返回值是int/short/long,且用于标识出错,则统一使用<0表示出错,>=0表示成功。回调函数等有约束的函数可以例外(比如main的返回值);
  • 返回值为BOOL类型的函数,只允许返回TRUE和FALSE两种取值;
  • 在函数返回值中使用的函数指针,必须使用typedef定义的别名。(不容易出现歧义) 比如:

typedef int (*PfnScanner)();
PfnScanner get_scanner(const char *name);
//指向返回值为int类型的函数的指针

4.4 const使用

在使用const的时候要对其十分了解,不然会出现错误。

  • 函数参数:

    • 对于指针或引用参数,如果函数内部不改变该参数的值不调用需要该参数为左值(通俗点说就是可以放在赋值运算符左边的变量)的函数,则必须在该参数定义前加const修饰
    • 钩子函数(回调函数)如果需要和指定的函数原型保持一致可以例外(比如:定时器函数,消息处理函数,qsort的回调函数等)。

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

  • 如果该函数需要返回一个依赖某参数的指针,而这个指针可能被其它函数用来改写数据,则认为这个被依赖的参数是个非const参数,不需要const修饰。例子见注释。

  • 非静态成员函数: 对于非静态成员函数,如果函数内部不修改普通成员变量没有staticmutable修饰的成员变量),则必须声明该函数为const成员函数(在参数列表后加const)。 下列情形可以例外:

    • 构造、析构、重载操作符函数;
    • 作为钩子或回调函数使用;
示例:
void print(const char *buf, size_t len);//成员函数,如:
int ini::find(const char* name) const;//返回一个依赖参数的可写指针:
msg_hdr *get_hdr(char *buf)
{return (msg_hdr *)buf;
}
void get_hdr(char *buf, msg_hdr **hdr)
{*hdr = (msg_hdr *)buf;
}

4.5 重复代码提炼成函数:

超过5行(不算空白行、注释行、只有大括号的行)的重复代码应提炼成函数(特殊情况也可提炼为宏)。
如有效率要求,可将函数内联。重复代码,指满足以下所有条件的代码块:
  • 出现次数超过2次;
  • 去除分隔符(比如空白符、大括号、注释等)以后,只有变量名字或常量内容不同;”

4.6 格式化字符串的变参函数定义

如果需要定义类似于printf的拥有可变参数列表、能格式化输出字符串的函数,需要在函数声明中加上检查参数有效性的属性声明,如下:
void my_print(int fd, const char *fmt, ...) __attribute__((format(printf, 2, 3)));

5 宏定义

5.1 命名

除非有特殊理由,否则宏使用全大写加下划线的方式命名。(有特殊理由的须注释说明原因)

5.2 括号使用

因为宏是直接替换代码,所以一定要用括号规避运算符优先级的问题

  • 用宏定义表达式时,要使用完备的括号。保证该宏的所有可能的使用方法(即包含潜在的某些使用方法)都不会发生运算符优先级问题;
  • 如果宏定义中包含多条语句或者包含有if语句,须将这些语句放在大括号中,建议使用do{…}while(0)的形式;

5.3 宏参数

使用宏时,不允许参数中出现会发生副作用(即会改变某些变量的值或程序的运行环境)的表达式。
比如不允许使用MAX(++a, b+=2)。在《C缺陷》里也有讨论到这个问题。

5.4 减少宏使用

如果能通过下述方式替代宏,且程序功能不会发生变化,则不允许使用宏:
  • C++的常量定义或枚举;(注:C++常量和枚举都可用作编译期常量,用于数组长度定义等需要编译期常量的场合)
  • 函数或内联函数;(注:需要性能保证的多条语句可以封装成内联函数)”

5.5 防止命名冲突

宏内部如果需要定义局部变量,必须防止该变量和上下文中的变量名字冲突,建议加上特殊前缀或后缀。
Gcc编译器开启-Wshadow后对该问题会产生警告,需消除。”例如:
//遍历列表#define foreach_list(list, func)                    \do {                                            \int i;                                      \int cnt = list_size(list);                  \for (i = 0 ; i < cnt ; ++i) {               \func(list, i);                          \}                                           \} while (0)//假如调用者用法如下,就可能发生错误:
for (i = 0 ; i < cnt ; ++i) {foreach_list(&lists[i], do_print_list);
}//经过宏替换后,代码如下:
for (i = 0 ; i < cnt ; ++i) {do {int i;int cnt = list_size(&lists[i]);for (i = 0 ; i < cnt ; ++i) {do_print_list(&lists[i], i);}} while (0);
}
/*由于内层作用域定义的名字自动屏蔽外层定义的名字,编译器不会报错。
这样一来,原本foreach_list(&lists[i],do_print_list)是想访问外层的i,但经宏替换后实际访问到的i却是内层定义的,张冠李戴,岂不糟糕?*/
解决办法是--给宏内部定义的局部变量加特殊前缀或后缀, 如:将int i;int cnt;改为int i_;int cnt_;

6 结构体

6.0 总则

结构体(struct)和联合体(union)提供一种按名字访问数据块中数据内容的能力。使用结构体/联合体,可以让代码更具可维护性,且有助于让代码跨平台。为了更大的发挥各平台的性能,编译器在排布结构体/联合体中的数据成员时,可能在各变量间插入一些“垫片”数据,使得每个变量都做到对齐访问。这些“隐形”的“垫片”,可能导致访问未初始化数据问题(如memcmp比较两个结构体)。

6.1 结构体对齐

结构体对齐的问题在面试中经常会被问道,需要十分注意

  • 保证结构体(C++类也一样)至少是4字节对齐的。 长度大等于2的成员的偏移位置必须能被2整除,长度大等于4的成员的偏移位置必须能被4整除。另:如果不强行指定对齐方式,编译器默认会将所有结构体对齐,即可满足本条款;
  • 需要跨进程传递的结构体,必须保证pack(1)pack(4)编译后是一样的内存结构(即各成员的偏移是一样的)。

    • 如果结构体中含有64位(如double,long long)的数据成员,必须保证pack(1)pack(8)编译后是一样的内存结构;
    • 有一个技巧可以较容易达到这个要求,即结构内成员按成员的对齐系数(alignment modulus)从大到小的顺序排列。
    • 结构体类型的成员的对齐系数是该结构体各成员对齐系数的最大值。
具体对齐原则可见其他笔记。

6.2 保证数据结构在跨平台时的二进制兼容性:

需要在异构平台上传递的结构,不直接使用随平台不一样而导致长度不一的数据类型,如:int,long,unsigned long;

long在32位是占2个字节,而在64位占4个

可使用在不同平台保持长度一致的各种类型别名,如:int32_t,U32,WORD

6.3 变长结构体

变长结构体,指那种含有元素个数可变的数组成员的结构体。
  • 长度可变的数组成员必须是结构体的最后一个成员;
  • 如果该数组成员的元素个数定义为0,则结构体大小并不包含该数组成员的大小;
  • 该结构体必须使用动态分配,并预留足够的内存空间(该结构体大小加上数组成员的大小);
  • 该结构体必须是POD(plain old data)类型,具体原因请参考注释;

POD就是C语言自带的变量,而不是string之类的类,是int,short等

如果结构体不充当变长结构体使用(比如只访问结构体前面固定长度字段),则可以例外。
  • 变长结构体的大小:
struct port_group{unsigned int cnt;unsigned short ports[0];
};
上述结构体中,ports成员的大小为0,所以整个结构体的大小sizeof(struct port_group) == sizeof(unsigned int)
  • 变长结构体必须动态计算长度和分配内存,直接定义变量是有问题的。如下代码会造成运行时错误:
void fun()
{struct port_group pg;pg.cnt = 1;pg.ports[0] = 1;
}
  • 变长结构体分配:
struct port_group *create_port_group(size_t port_cnt)
{size_t size = sizeof(struct port_group) + sizeof(unsigned short) * port_cnt;struct port_group *p = (struct port_group *)calloc(1, size);if (!p){return p;}p->cnt = port_cnt;return p;
}
  • 变长结构体必须是POD(plain old data)类型,以下结构体定义是有问题的:
struct port_group{std::string name;unsigned int cnt;unsigned short ports[0];
};
因为port_group必须使用类似malloc的方法分配内存,name得不到初始化,需要使用placement new 的方法才能正常初始化,很麻烦。

7语句

7.0 总则

对C&C++语句使用的一些约束,主要目的是降低语句复杂度,帮助代码阅读者更容易理解代码的意思。

7.1 括号使用

括号的使用永远是编码风格必须讨论的东西。
以下情况必须使用括号明确表达式优先级:
  1. 同时出现 &、^、| 这三种运算符中的任意两种(或&、^同时出现两次);
  2. 同时出现位运算符(& ^ |)和比较运算符(< <= > >= == !=)
  3. 同时出现&&和||;
  4. 同时出现移位运算符(<< >>)和比较运算符(< <= > >= == !=)
  5. 同时出现比较运算符中(< <= > >= == !=)的任意两种(或一种出现两次);
  6. 同时出现位运算符(& ^ |)和逻辑运算符(&& ||);
  7. 同时出现移位运算符(<< >>)和算术运算符(+ - * / %)
如果一个上述运算符没有同时出现在一个操作数(或高优先级表达式)的两边,可以不做要求。不符合要求:
  • if (op_bits & OP_READ && op_bits & OP_WRITE) &和&&同时出现在OP_READ的两边
  • if (1 > mid != max) >和!=同时出现在mid的两边
  • ret = op_bits | OP_READ & OP_MASK; |和&同时出现在操作数OP_READ的两边;
  • ret = op_bits | (g_def_opbits & 0xffff) & OP_MASK; |和&同时出现在高优先级表达式(g_def_opbits&oxffff)的两边;
  • if (pb == 0 || len == 0 && pe == 0) ||和&&同时出现在高优先级表达式len == 0的两边;
符合要求:
  • if ((op_bits & OP_READ) && (op_bits & OP_WRITE)) 以上表达式已使用括号明确了优先级
  • if (mid >= min && mid < max) 以上表达式中>= 和 <没有同时出现在某个高优先级表达式的两边,中间的&&运算符优先级比较低。
  • if (p && p < pEnd && p > pBegin + 1)

7.2 goto使用限制:

  • 只允许在同一个块作用域内跳转,或者跳转到上层的块作用域。
  • 不得用于跳转到更深的块作用域或者其它平行的块作用域。
  • 不允许使用goto在switch的多个case语句/default语句之间跳转;

迪杰斯特拉说要取消goto,编程这么久了还没用过goto这个语句

例如:下述代码是不符合要求的。
if (ok)goto ready;
func();
while(1){
ready:
//…
}

7.3 循环性能优化:

可以在循环体外进行的耗时计算不放入循环体中。”
反例:
for(int i = 0; i < lst.count(); ++i) {printf("%d", lst[i]);
}
注:lst是虚拟的一个list类,具有链表的含义,有count(利用遍历求链表长度)和operator[](遍历取指定位置成员)两个成员函数。此例会导致count不必要的重复计算,时间复杂度O(n*n).
类似的还有下面这段代码:
for(int i = 0; i < strlen(str); ++i) {if (str[i] == ' ') {break;}
}
学习了,以后针对这种应该在循环外定义一个变量,只计算一次,减少开销。

7.4 不使用复杂表达式:

不使用过于复杂的表达式,如确实有必要这样写须注释说明该表达式的意思。鼓励把复杂表达式分拆成多句书写。虽然能减少代码量,但是生涩难懂对看代码的人来说是种负担
  • 所谓过于复杂的表达式,指一个运算数某一边的运算符个数大等于2个。 单目运算符+,-,*,!,~,&,sizeof及括号[],()不计算在内。 如:*stat_poi ++ += 1;应拆分成*stat_poi += 1;++stat_poi;
  • ?:运算符不允许嵌套使用,如:ret = a < b ? (a < c ? a : c) : (b < c ? b : c);[2012-9-6]”

7.5 switch/case语句:

  • 每个case语句必须以break语句(或continue/goto/return/longjmp/exit等流程转移语句)结束。 如果不需要break,必须在末尾注释说明。《C陷阱》里也有提到。 如果该case标签后没有任何处理语句可以例外。没有处理语句的多个case标签可以写在一行;
  • 每个switch语句都必须要有default标签;
例如:
switch (*pch) {
case ':': case '-':...break;
case '\0':return;
case '%':.../* no break */
default:++pch;break;
}

7.6 控制结构(if/for/while/switch等)的嵌套:

有时候条件查询又不得不用到多层嵌套,也可以用&&等将其提拉到平级控制

  • 不使用过深的嵌套:循环嵌套不超过3层,总共不超过5层;
  • 如果if子句和else子句行数相差超过3行,须保证else子句比if子句长。 如果该条件语句有多个条件,可以例外(即存在else if子句)。 建议使用短路返回的方法减少嵌套层数;

不合格嵌套:
for(int row = 0; row < rowcnt; ++row) {for(int col = 0; col < colcnt; ++col) {char *pname = g_pool[row][col].name;for(int i = 0; i < MAX_NAME_LEN; ++i) {char ch = pname[i];for(int j = 0; j < PREDEF_TABU_SIZE; ++j) {if (ch == g_predef_tabu[j]) {return false;}}...}  }
}
可以改造为:
inline bool is_tabu(char c)
{for(int j = 0; j < PREDEF_TABU_SIZE; ++j) {if (ch == g_predef_tabu[j]) {return true;}}return false;
}
for(int row = 0; row < rowcnt; ++row) {for(int col = 0; col < colcnt; ++col) {char *pname = g_pool[row][col].name;for(int i = 0; i < MAX_NAME_LEN; ++i) {char ch = pname[i];if (is_tabu(ch))return false;...}  }
}
短路返回
if (match_condition()) {... ...if (...)... ...
} else {break;
}
改为以下语句就是短路返回:
if (!match_condition())break;
... ...
if (...)... ...

8 错误处理

8.0 总则

异常,无处不在。异常处理的完善程度,决定了代码的健壮程度。
有些异常需要在设计方案层面善加考虑,有些异常,却仅仅需要局部的关注、处理,就能得到很好的效果。异常是会传播和扩散的,城门失火,殃及池鱼。而且,一些关键信息会在异常传播过程中丢失,导致异常难以定位。所以,异常处理应遵守九字原则:早检查、勤记录、早处理。及早发现异常,记录上下文,处理异常,可以及早定位解决问题,并防止异常造成更大的破坏。
如果严谨的遵守以下checklist条款,在对程序进行逻辑分析时,就可以在局部聚焦正常数据、流程,无需关注其它逻辑引发的异常数据蔓延。也更不容易在代码维护过程中破坏一些假设条件,引入bug。

8.1 参数合法性检测

主要集中在外部接口和内部函数

外部接口:指会被其它模块调用的函数; 内部函数:指不会被其他模块调用的函数;

  • 外部接口(指函数)在使用参数前需检查参数合法性。

    • 不使用该参数或该参数的任何取值都是合法值不会引起程序异常可以例外。
    • 参数有效性检查不要用断言等只在调试版本生效的方法。
    • 应当保证在release版本下检查处理措施依然有效,在发现不合法的参数时执行错误处理(比如返回标识错误的值,抛出异常,执行错误处理流程),保证程序健壮性。
  • 所有内部函数的函数入口处必须通过断言检查所有参数的合法性。

    • 不使用该参数或该参数的所有取值都是合法值,可以不检查该参数。
  • 无论外部接口还是内部函数,发现参数异常都必须输出供错误诊断用的调试信息。

    • 断言本身会输出诊断信息,故使用断言检测异常无需额外打印调试信息。”
结构体或类对象的合法性可通过专门设计的检查函数或宏来进行。结构体的检查方法如:
struct str{char *ptr;size_t size;
};
#define CHECK_STR(x) ASSERT((x).ptr && (x).size < 1024)
void foo(struct str *p){ASSERT(p);CHECK_STR(*p);… …
}
类对象的检查方法:
class str {
public:
#ifndef NDEBUGvoid assert_valid(){ASSERT(m_ptr);     //assert,断言assert的作用是现计算表达式 expression ,如果其值为假(即为0),那么它先向stderr打印一条出错信息。ASSERT(m_size < 1024);//也就是如果m_ptr为空或者m_size > 1024则报错!}
#endif
private:char *m_ptr;size_t m_size;
};
#define ASSERT_VALID(x) (x).assert_valid()  //直接调用类的
void foo(class str& strs){ASSERT_VALID(strs);… …
}

8.2 数据合法性检查:

从外部读取的数据必须检查合法性,不可直接使用,外部数据指通过文件、进程间通讯设施或界面所获取的输入。
不使用该数据或该数据所有取值都是合法值可以例外。
外部数据的检查不应使用断言等仅在调试版本生效的措施。发现数据不满足要求时,必须输出供错误诊断用的调试信息。

比如用户输入的数据,需要检查其合法性。

8.3 断言要求:

断言中禁止对变量赋值或改变变量的值,如assert (size++ > 100), 禁止调用有副作用的函数。
有副作用的函数,指函数内会更改变量值、改变系统环境、进行IO。

和宏定义一样,不要有副作用

8.4 “return - 返回值检查:

主要集中在API和内部函数里
  • API: 对于返回新分配资源(句柄或指针)或者返回出错标识(比如使用FALSE,负数,0指针标识失败)的系统API(包括标准库、WIN32 API、MFC、ATL、POSIX API,第三方库),必须检查并处理失败情况。 不允许使用只在DEBUG版本生效的检查措施(比如:assert,VERIFY)。release也要检查! 包括但不限于下述函数:

    • malloc/realloc/calloc/new;
    • open/socket/pipe/epoll_create/pthread_create/fork/dup/popen;
    • CreateProcess/CreateThread/CreateFile;
    • fopen/fdopen/freopen;
    • GetMessage/PeekMessage;
    • mkdir/findfirst/lstat/recv/send;
    • sendto, recvfrom, bind, accept, sendmsg, recvmsg, setsockopt, select, poll, epoll_ctl,epoll_wait,connect;
    • 例外:
    • 如果忽略该错误不会影响程序正确性,可以在函数调用前加(void)忽略返回值检查。 建议同时注释说明为什么无需处理返回值;

    • 如果API文档有明确说明,在条件满足时一定不会失败的,且调用该API之前已经保证满足不失败条件, 可以直接使用其返回值,或在函数调用前加(void)忽略返回值检查。例如snprintf,time等。
  • 内部函数: 对于内部函数(非系统API),如果有返回值,且返回值会用于标识失败情况(比如使用FALSE,负数,0指针标识失败),必须检查并处理失败情况。 如果确认当前调用在用户环境不可能失败,或者该错误可以忽略,允许使用这两种错误处理手段:
    • 对于不可能失败的情况,可使用只在DEBUG版本生效的检查处理机制(如:assert,VERIFY);[2010-9-8]
    • 在函数调用前面加(void)强调该函数不会出错或者错误可以忽略,建议同时注释说明为什么无需处理返回值;
 (void)close(fd);        //调用close函数的时候强调void,表示其无返回值
只要会返回失败,都应该应对失败情况。
  • 对于recv,send一类函数调用来说,几乎不可能避免失败,而且失败的情况很多。必须针对每一种失败都有妥善的处理。允许把若干种失败情况合并起来处理。
  • 对于一些有严格要求的程序来说,即使是CloseHandle/close一类的调用,也必须处理失败。比如流缓存、数据库一类的就需要检查CloseHandle/close,判断是否会有磁盘回写失败一类的情况。

  • 对于一些要求没那么严格的程序来说,一般的处理方法是,在这些函数失败的时候打印日志信息,帮助开发者了解其中发生的异常情况。

  • 如果明确不可能失败(比如有些函数资料已经明确说明只有在参数无效时才失败),或者不关注这种失败,可以用(void)CloseHandle(hFile)的方法处理返回值。这种方法可以屏蔽来自PC-LINT一类工具的警告,也可以提示代码阅读者知道作者不关注这个错误;

9 资源管理

9.0 总则

资源问题是C/C++/ASM语言中独有的问题,这些语言认为资源管理非常重要,所以应该把资源管理权交给程序员,在语言机制上缺乏垃圾回收机制。
这种理念给了程序员更多的灵活性,能得到更好的性能,但也大大增加了出错的可能性。
最容易出现的资源问题有:
  • 资源使用完毕后没有释放;
  • 资源重复释放;
  • 资源被释放后仍然被引用和访问,包括引用了已经被重新分配出去的资源。 比如:perror之类的函数默认往2号fd输出信息,如果先关闭2号fd,然后open/socket之类的调用会重用2这个fd,导致perror将信息错误输出到其它文件或socket;
  • 使用错误的手段释放资源,比如使用fclose关闭被popen打开的FILE指针;
减少资源使用出错,有几个简单有效的原则:
  • “配对”原则,即分配资源的函数出现在哪个函数里,与之配对的释放资源函数就应出现在哪个函数里。 - “使用前检查,释放后置空”原则,像这段代码一样的做法:
 if (s_fp) { fclose(s_fp); s_fp = NULL;}

9.1 防止泄露:

资源的分配和释放必须配对:
  • 在某函数内分配的资源必须在该函数内释放
  • 如果函数分配了资源但不能马上释放,必须:
    • 必须有机制供调用者获取新分配的资源(通过返回值、出参、全局变量、类变量等);
    • 必须提供与之对应的能保证释放资源的函数(或者能通过delete,free等API直接释放), 并在函数头的注释中说明负责释放的函数名称。 如果需要释放的资源是类的成员且负责释放的函数是析构函数,可不说明。析构函数自动释放资源。

9.2 使用配套的资源释放函数释放资源(重点)

对于各类动态分配得到的资源,必须使用与之配套的释放函数释放。包括但不限于:
  • open/creat/dup/socket/epoll_create分配的描述符由close释放;
  • fopen/fdopen/fopen分配的文件指针由fclose释放;
  • CreateFile分配的句柄由CloseHandle释放;
  • popen分配的文件指针由pclose释放;
  • new分配的内存由delete释放;
  • new[]分配的内存由delete[]释放;
  • malloc/realloc/strdup/calloc分配的内存由free释放;
  • qdbm_open分配的数据结构由qdbm_close释放;
  • kmalloc分配的由kfree释放;
  • vmalloc分配的由vfree释放;

9.3 避免重复释放:

  • 句柄或指针在资源释放结束后应置为无效值:

    • delete/free后需要将指针置为0,不使用delete,free释放非堆内存;
    • close关闭的文件描述符必须置为-1;
    • fclose,pclose关闭的FILE指针必须置为0;
    • CloseHandle关闭的句柄必须置为INVALID_HANDLE_VALUE;
    • 其它函数释放的资源必须置为对应的无效标识;

    例外情况:

    • 析构函数中可以例外;
    • 即将退出程序时可以例外;
    • 如果是局部变量且马上退出函数可以例外;
    • 如果释放后马上指向其它有效资源的可以例外;
以上例外情况需保证已释放资源不再被引用到。
  • 释放资源前需判断句柄/指针的有效性,保证已经置为无效值的指针或句柄不会重复释放。能保证资源处于有效状态的可以例外。
  • 如果释放函数本身能保证0指针或无效句柄释放的安全性,也可以例外,比如free(NULL)是安全的,无需if (!p) free(p)。
资源的分配和释放必须配对。即:
  • 分配和释放应配对出现在同一个函数中,在父函数中分配的资源不应交给子函数释放,具体可参考注释;
  • 一个分配不可对应多个释放操作;
  • 如果封装了资源的分配,就应封装资源的释放,反过来也成立。这两个封装过的资源分配和释放操作应配对出现在同一个函数中。”
错误做法:
void child(void *p)
{//操作p ...free(p);
}void parent(void)
{void *p = malloc(sizeof(int));child(p);//free(p);
}
对于全局资源,建议:
static char *s_buf;int init_buf(void)
{if (s_buf)return 0;s_buf = malloc(BUFSIZ);return s_buf ? 0 : -1;
}void clean_buf(void)
{if (!s_buf)return;free(s_buf);s_buf = NULL;
}

9.4 勿混用内存管理方法:

不能根据标识对同一个指针选用不同的内存管理方法(尤其是内存释放)

delete和free不要混用,怎么声明的就怎么释放

比如:
void process_message(void *ptr, size_t size, bool bInHelp)
{...if(bInHeap)free(ptr);
}
void post_send(void *ptr, size_t size, int flags)
{...if (flags & CRT_HEAP) {free(ptr);} else if (flags & SYS_HEAP) {HeapFree(ptr);} else if (flags & CPP_HEAP) {delete[] ptr;}
}

9.5 标准输入、输出、错误的关闭:

对于标准输入、标准输出、标准错误输出这三个文件,如果有必要关闭的话,必须将其重新打开,
定向到空文件描述符(比如/dev/null)。这三个文件在不同平台下分别为:
  • linux下: 文件描述符为0,1,2;
  • windows下: GetStdHandle(STD_INPUT_HANDLE) GetStdHandle(STD_OUTPUT_HANDLE) GetStdhandle(STD_ERROR_HANDLE)

  • 标准C中: stdin,stdout,stderr

将标准输入、标准输出、标准错误输出进行重定向:
close(0);
close(1);
close(2);
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(fd0);
fd2 = dup(fd0);
if (fd0 != 0 || fd1 != 1 || fd2 != 2) {exit(1);
}

10 内存

10.0 总则

C语言中内存管理太难是许多人放弃c转向JAVA的主要原因

内存问题是C/C++/ASM语言中独有的问题。
这些语言要求程序员自行管理对内存的访问,所以很容易因为程序员的失误引起bug。
  • 尽量避免多个逻辑持有同一个内存块的引用/指针(某个逻辑释放内存块时,其它逻辑继续引用,引起悬挂访问);
  • 尽量避免指针/引用长时间指向一个内存块的内部(当该内存块释放时可能引起悬挂访问);
  • 变量总是应该初始化后再使用,尤其是指针
  • 对数组进行遍历时应当提防临界错误(比如+1或-1的错误);
  • 警惕为NULL的指针和长度为0的数组;

10.1 变量初始化

1)对堆和栈上分配的变量、内存块进行了初始化;
例外情况:
  • 定义处和第一次赋值处相隔不超过5行,这两处之间未访问过该变量,并且没有出现过任何访问了该变量的控制结构(如if/for/while/switch/do等)。
符合的例子:
int foo(const char *name) {int ret;printf("[debug] foo(%s) called\n", name);ret = find_hash(name);if (ret < 0)return ret;return insert_hash(name);
}
不符合例子:
int foo(const char *name) {int ret; if (name && name[0] != '\0') {ret = find_hash(name);if (ret < 0) {return ret;}}return insert_hash(name);
}
后续维护时,如果在insert_hash前加上访问ret的代码(如printf(“%d”, ret))就会有问题了。
  • 效率原因。这种情况须注释说明原因,且保证不出现读未初始化数据的问题。
对于结构体、对象、字符串、数组等内存块,可以通过以下几种方法初始化:
  • 使用初始化列表,如:char buf[BUFSIZE] = {0};
  • 使用构造函数、拷贝构造函数进行初始化;
  • 使用memset,bzero等可以将内存块设置初始值的函数将整个内存块设置为某个初值, 或者使用memcpy等函数拷贝一块合法内存,保证整个内存块数据都为已初始化数据;
使用赋值运算符对结构体和对象类型初始化,如: 
struct person customer;
customer = s_default_person;
  • 使用strcpy等能保证该内存块是合法字符串(有’\0’结尾)的函数初始化;
  • 使用分配时即初始化的函数(如calloc)进行内存分配;
  • 使用其它自定义的函数,只要保证该数据块所有成员都已初始化为有效数据,或成为一个合法字符串(即’\0’之后的部分可以无需赋值);
2)作为出参使用的指针参数,必须在返回之前进行赋值。如明确无需赋值,需注释说明。
例子:
void foo(const char *name) {char buf[BUFSIZE];  //get_full_name会首先初始化bufget_full_name(name, buf);printf("%s\n", buf);
}
3)对于资源句柄(包括指针,下同),应初始化为一个无效标识值,或者通过分配得到的资源句柄,或者确定的可安全引用的资源句柄。
注意: 不要初始化为未获得授权的资源句柄,比如:把文件描述符直接初始化为0,或者将指针指向了不能安全访问的内存或变量(0除外)。

10.2 指针算术:

指针的移动
  • 结构体类型的指针如果要指向下一个结构体头部,只要对指针加1,而不是加结构体长度;
  • 如果结构体是不定长的结构体,应该将指针先转换成char*类型,然后加需要偏移的字节长度;”
例如:
struct iphdr *iph = ip_hdr(skb);
struct tcphdr *tcph = (struct tcphdr *)(iph + (iph->ihl << 2));/上述代码有很严重的BUG,正确应该是:
struct tcphdr *tcph = (struct tcphdr *)((char *)iph + (iph->ihl << 2));

10.3 结构体比较:

不使用memcmp(或者其它按位比较方法)比较两结构是否相等(C++类也一样)。
确认安全的允许例外,但须在类的注释中说明。”

主要是注意结构体内存对齐的问题,memcmp比较需要保证结构体字节按1对其,结构体中必须没有空隙。

比如:
struct strus {char m_char;int m_int;
} a, b;//strus占8个字节
a.m_char = 0;
a.m_int = 0;
b.m_char = 0;
b.m_int = 0;
if (memcmp(&a, &b, sizeof(a)) == 0) //有问题if(a == b)  //比较的是a和b的地址//理想的方法是重载==运算符,一个个比较

10.4 字符串比较:

禁止把字符串转成整数进行比较。如:
if(*(int*)""desc"" == *(int*)str)"

10.5 防常量字符串修改:

不可修改常量字符串,比如
char* p = ""NeiCun"";
p[0] ='R';

10.6 字符串格式化:

  • 保证fprintf/sprintf/snprintf/printf参数的格式化控制符和实参的一致(gcc可以检查出部分此类问题)
  • 通过外部数据得到的字符串不直接作为格式化参数(系统配置文件中的日志信息可以例外);
即用printf("%s", strings)替换掉printf(strings)

10.7 防止字符串缺结束符

这个问题很容易忽略,要相当注意

  • 通过进程间通讯措施(比如:mmap/read/recv/recvfrom/fread/copy_from_user等)读取的内存块,
  • 如块尾是一个字符串,需在末尾补’\0’,避免写入端没有写入’\0’结束符导致错误。
例如:
char *p = (char *)malloc(msgsize + 1);
int ret = recv(sk, p, msgsize, 0);
if (ret <= 0) {…
}
p[msgsize] = '\0';

10.8 字符串长度计算:

用strlen而不要用sizeof,后者是计算字符串定义长度

  • 不可直接假定字符串长度,需使用strlen计算得到,分配容纳字符串的缓冲区,必须给’\0’结束符预留空间;
  • 不可通过sizeof计算常量指针指向的字符串的长度; 作为特例:字面值常量的长度允许使用sizeof计算得到(sizeof计算得到的长度已经包含了’\0’结束符)。 如:

const char* pstr = "hello";
通过sizeof(pstr)计算字符串长度是错误的,但是通过sizeof("hello")-1计算字符串长度是允许的。

10.9 变量大小、偏移计算:

  • 变量大小计算: 计算变量大小,必须使用sizeof,不允许人为假定变量大小。 只要可能,就应该测量变量的大小,而不是测量类型的大小。(也就是不要测量int的大小,而是测量int a ,sizeof(a))
  • 成员偏移计算: 计算结构内成员的偏移使用offsetof(或和该宏等价的措施),不许人为假定成员偏移;
比如:
int val;//推荐:
memcpy(buf, &val, sizeof(val));
//不应该使用这两种:
memcpy(buf, &val, 4);
memcpy(buf, &val, sizeof(int));
对于通过参数传递的数组,无法直接测试数组长度,推荐:
void foo(int arr[ARRAY_SIZE])
{memcpy(g_buf, arr, sizeof(int) * ARRAY_SIZE);
}//错误:
void foo(int arr[ARRAY_SIZE])
{memcpy(g_buf, arr, sizeof(arr));
}

10.10 C99的变长数组和alloca:

  • 禁止使用C的变长数组;
  • 禁止使用alloca/_alloca分配内存。
建议使用std::vector或者malloc替代以上两种用法。
例如:
C99的变长数组:
void foo(int n)
{int a[n];a[0] = 1;...
}
上述代码在n的值超出范围时,会导致栈溢出。

10.11 字符串转整数/浮点数:

将字符串转换为整数或者浮点数,不要使用ctype里的函数,可能会丢失精度

不使用atoi,atol读取数字,除非对输入合法性没有要求的场合。
所使用的读取函数必须保证数字不被截断、不丢失精度,不能double变int。
建议:
  • 有符号整数建议使用strtol读取;
  • 无符号整数建议使用strtoul读取;
  • 浮点数建议使用strtod读取;
  • 有符号的64位整数建议使用strtoll读取;
  • 无符号的64位整数建议使用strtoull读取;”
atoi和strtol函数均是把字符串转换成整数,两者的不同点主要是:
  • atoi的返回值无法区分是正常的返回还是错误的返回,如:
int val;
val = atoi("abc"); 与val = atoi("0");
两者返回的val均为0,因此无法区分哪个是正确parse后的值。
  • strtol函数对异常的返回可以设置errno,从而可以发现异常的返回,如:
errno = 0;    /* To distinguish success/failure after call */
val = strtol(str, &endptr, base);

11 并发

11.0 总则

并发指同一时间运行多个逻辑,包括使用分时手段运行的“伪”并发。
使用并发可以充分利用多CPU、多主机的性能,可以同时服务多个用户。
并发逻辑之间的相互影响非常难以分析,引发的缺陷很难重现和定位,所以应当审慎的选用你的并发方案。
  • 优先使用隔离能力强的并发手段,比如物理隔离的多台设备,内存空间隔离的多个进程,尽量不使用隔离能力差的多线程并发;
  • 尽量限制并发逻辑之间的信息交互,降低并发逻辑之间的相互影响;

11.1 信号处理

长时间运行的linux程序必须处理信号,必须处理或忽略的信号有SIGTERM,SIGINT,SIGPIPE,SIGBUS,SIGSEGV,SIGABRT。
  • 对于SIGPIPE信号,需忽略,或者保证处理之后程序仍能正常运行;
  • 对于SIGBUS、SIGSEGV信号,应打印堆栈,保留现场信息供后续调试;
  • 对于SIGCHLD信号,应保持系统默认行为,即SIG_DFL。
如果能保证后续不调用和waitpid相关的函数(如system,pclose),或者程序逻辑不依赖waitpid的返回值,可以例外。
对于库代码,特别注意不要改变SIGCHLD的行为,以免影响库调用者的一些程序逻辑。

要防止出现僵尸进程和孤儿进程

11.2 信号处理函数

信号处理函数中不调用不可重入函数。不可重入的函数典型特征有:
  • 内部使用了全局变量/静态变量,如:mallocprintf
  • 内部使用了可能导致死锁的机制,如:localtimelocaltime_r
  • 调用了其它不可重入函数;
建议采用的信号处理方法:在信号处理函数中仅仅设置信号标识(或计数),在主循环中判断信号标识执行相应的信号处理。(可以通过这个方法判断子进程)

信号需要绑定函数,然后信号触发后调用相应的函数。具体信号函数可查资料。

11.3 不暴力终止线程

除非程序退出,否则不采用暴力方式终止线程。
推荐通过在线程函数中return的方式结束线程,但不强制要求,确认安全时使用pthread_exit/ExitThread退出线程也可以。

一般用信号终止,绑定一个信号函数专门用来结束线程

11.4 wait - 子进程/子线程的后事处理

处理进程的时候要注意:
  • 子进程终止后必须通过waitpid/wait等待结束,避免子进程成为僵尸进程;
  • 子线程必须使用pthread_join等待结束,或者使用pthread_detach使子线程成为detached状态,避免线程资源泄露;

11.5 互斥锁的使用

互斥是一个很重要的概念,尤其是在多进程/多线程编程中,还有数据库的使用

主要注意以下几点:
  • 使用互斥锁之前,必须对互斥锁的结构体或对象进行初始化,或调用初始化函数;
  • 使用互斥锁之后,必须使用销毁函数对互斥锁的结构体或对象进行销毁。使用PTHREAD_MUTEX_INITIALIZER初始化的可以例外;

11.6 锁定区域内睡眠

在互斥锁锁定区域内不调用阻塞进程或者引发进程睡眠的系统调用,如果确实需要调用,须注释说明;
比如:
//以下代码可能有问题:
lock();
sleep(1);
unlock();
----------------
lock();
recv(sk, buf, bufsize, 0);
unlock();

11.7 非递归锁的使用:

Mutex可以分为递归锁(recursive mutex)非递归锁(non-recursive mutex)。可递归锁也可称为可重入锁(reentrant mutex),非递归锁又叫不可重入锁(non-reentrant mutex)。
二者唯一的区别是:
  • 同一个线程可以多次获取同一个递归锁,不会产生死锁。
  • 而如果一个线程多次获取同一个非递归锁,则会产生死锁。
需要注意以下几点:
  • 非递归锁不用于递归函数;
  • 非递归锁的锁定区域内不调用其它使用相同锁的函数。

(linux下的pthread_mutex_t默认是非递归锁,windows的临界区是递归锁)

11.8 死锁

以下情况会出现死锁:
  • 不解锁返回: 锁定区域内不允许出现不解锁的返回(包括抛出异常)
  • 锁的相互等待: 如果两段代码同时使用两把相同的锁不允许出现相互等待的现象。 例如:
//A线程:
lockA();
lockB();
unlockB();
unlockA()
//B线程:
lockB();
lockA();
unlockA();
unlockB()

11.9 线程创建

线程创建需要注意以下情况:
  • 失败处理创建线程必须判断并处理失败情况; 说明:windows下创建线程可通过_beginthread/_beginthreadex/CreateThread/AfxBeginThread/线程类, Linux下创建线程可通过pthread_create。
  • 启动时序控制: 不可假定线程的执行顺序(除非创建时使用了CREATE_SUSPENDED等控制线程执行顺序的标志), 不可简单使用sleep/usleep等不可靠方法来控制线程的执行顺序。
  • Windows平台线程创建方法
    • 在MFC中,创建界面线程使用AfxBeginThread或线程类,不使用_beginthread/_beginthreadex和CreateThread;
    • 其它情况创建线程使用_beginthreadex(MFC中创建工作线程或非MFC程序),不直接使用CreateThread;

linux的fork是创建进程

11.10 需要同步的访问:

如果多个线程同时访问同一个变量,以下情况需要同步:
  • 一个线程线程先读后写,另一个线程有写;
  • 一个线程写,另一个线程写了再读;(这种情况应避免,可以读临时值) 如果多个线程同时访问多个相联系的变量,只要一个线程有写,整个访问区间都应该同步保护,防止数据结构不一致。
单变量访问,比如:
//A线程:
if (g_cnt < MAXCNT)g_cnt++;
//B线程:
if (g_cnt > 0)g_cnt--;
上述情况需要做同步。多变量同时访问这两个变量:
char* buf;
int buf_cnt;
//A线程:
buf[0] = 0;
buf_cnt = 0;
//B线程:
buf[0] = 10;
buf_cnt = 1;
如果不做同步,可能造成“明明buf里已经没有数据”,但与之相关的buf_cnt计数却被置为1。

12 危险的库特性

12.0 总则:

C&C++标准库中存在一些不安全的特性或者函数,我们应当尽量避免使用。即使需要使用,也应该以保证安全的形式使用。

12.1 错误号获取

在以错误号标识错误类型的API调用和错误号获取代码之间,不允许出现其它可能影响错误号的代码。
这些类型的API,包括Win32 API,socket API,标准C库函数。

errno 是记录系统的最后一次错误代码。代码是一个int型的值,在errno.h中定义。查看错误代码errno是调试程序的一个重要方法。当linux C api函数发生异常时,一般会将errno变量(需include errno.h)赋一个整数值,不同的值表示不同的含义,可以通过查看该值推测出错的原因。在实际编程中用这一招解决了不少原本看来莫名其妙的问题。 比如:

1)
HANDLE hFile = CreateFile(…);
OnCreateFile(hFile, …);
if (hFile == INVALID_HANDLE_VALUE){DWORD err = GetLastError();...
}
2)
HANDLE hFile = CreateFile(…);
if (hFile == INVALID_HANDLE_VALUE){WRITELOG("CreateFile failed\n");if (GetLastError() == XXX)...
}

12.2 不混用文件机制

对同一文件或标准io流,不混用这三种机制:
  • 标准C库的文件IO(如: printf,fprintf,fseek,fgets);
  • POSIX IO(如:open,ftruncate,lseek);
  • C++ iostream(如:ostream, istream, fstream);

主要根据自己写的是什么代码决定

确定没有问题的可以例外,但需注释说明。

12.3 不使用不安全函数:

标准库(或posix)中存在一些历史遗留的不安全函数,这些函数标准库已经提供了对应的安全版本。
对于这类函数,必须使用其安全版本,包括:
  • 提供了后缀为_r的替代函数的不可重入函数:strtok,localtime,asctime,ctime,gmtime;
  • 不判断输入长度的函数:gets;

VS里有使用后缀为_s的替代函数

13 危险的语言特性

13.0 总则

C&C++语言中存在一些特征,这些特性或者很容易导致缺陷,或者是由实现定义,容易引起可移植性问题。
  • 不使用由实现定义的特性,如果确实有必要,应将这部分代码独立出来,通过条件编译技术保证各平台上的一致性;
  • 不使用未定义的语言特征,比如访问未初始化数据;

13.1 自增/自减运算:

表达式计算结果不能依赖于副作用计算发生的时机。
  • 同一语句中不得对同一变量使用多次自增或自减运算符。比如:*p++ = 2 + *p++;
  • 不允许在一个表达式中既对该变量赋值,又对该变量使用自增/自减运算符。 比如:it = lst.erase(it++);

13.2 参数顺序依赖性:

函数调用参数列表中,参数值的计算不得有顺序依赖性。
比如:
Call(a = b, ++a);
Call(foo1(), foo2());
其中foo1和foo2的执行顺序不同会造成不同结果。

13.3 char类型使用:

  • 不直接使用char类型的变量做数组的索引。char类型既可能是有符号的(值的范围:-128~127),也可能没符号(值的范围:0~255)。
  • 需要将char变量当成int等类型使用前,必须先将char类型转化为unsigned char类型。
  • 不使用getch,fgetc,getchar,getc返回的int型变量做数组索引(除非已经确定值大等于0)。”

13.4 除0错误预防(包括求余运算):

  • 用作除数的变量需保证不为0。
  • 求余运算符的右操作数也需保证不为0。
  • 如果该变量来自不可信的输入(外部输入或者其它模块传递的参数),必须先判断是否为0,为0时不作为除数参与运算。”
例如:
size_t unitsize = ini_get("unitsize");
if (unitsize == 0)unitsize = 1;
size_t unitnum = size / unitsize;

13.5 指针转换:

void*类型的指针和其它类型的指针之间必须使用强制转换;

13.6 移位运算:

移位运算的右操作数(即移动位数)必须大等于0并小于左操作数的位数;”< C缺陷里有提到

14 工具检查

14.0 总则:

工欲善其事,必先利其器。
善用代码静态扫描工具,可以找出代码中容易引起问题的不良写法,能找出部分内存访问或逻辑冲突之类的低级错误。

14.1 cppcheck:

所有C/C++代码须通过cppcheck检查,检查时须打开所有的检查选项。
除可以明确是误报的以外,不允许出现任何BUG及风格问题。(第三方代码除外)

Cppcheck是一种C/C++代码缺陷静态检查工具,不同于C/C++编译器及其它分析工具,Cppcheck只检查编译器检查不出来的bug,不检查语法错误。

14.2 c++test(需编译):

所有C/C++代码须通过C++test检查。除可以明确是误报的以外,不允许出现任何警告及错误(第三方代码除外)。
扫描需使用公司预置选项,如果需要额外关闭某些检查选项,需提前取得RDM书面认可。

cppcheck是静态,这个需要编译


查看原文:http://tanwenbo.top/c/%e3%80%8a%e7%bc%96%e7%a0%81checklist%e8%a7%84%e8%8c%83%e3%80%8b%e5%ad%a6%e4%b9%a0%e7%ac%94%e8%ae%b0.html

《编码checklist规范》学习笔记相关推荐

  1. 第二行代码学习笔记——第六章:数据储存全方案——详解持久化技术

    本章要点 任何一个应用程序,总是不停的和数据打交道. 瞬时数据:指储存在内存当中,有可能因为程序关闭或其他原因导致内存被回收而丢失的数据. 数据持久化技术,为了解决关键性数据的丢失. 6.1 持久化技 ...

  2. 第一行代码学习笔记第二章——探究活动

    知识点目录 2.1 活动是什么 2.2 活动的基本用法 2.2.1 手动创建活动 2.2.2 创建和加载布局 2.2.3 在AndroidManifest文件中注册 2.2.4 在活动中使用Toast ...

  3. 第一行代码学习笔记第八章——运用手机多媒体

    知识点目录 8.1 将程序运行到手机上 8.2 使用通知 * 8.2.1 通知的基本使用 * 8.2.2 通知的进阶技巧 * 8.2.3 通知的高级功能 8.3 调用摄像头和相册 * 8.3.1 调用 ...

  4. 第一行代码学习笔记第六章——详解持久化技术

    知识点目录 6.1 持久化技术简介 6.2 文件存储 * 6.2.1 将数据存储到文件中 * 6.2.2 从文件中读取数据 6.3 SharedPreferences存储 * 6.3.1 将数据存储到 ...

  5. 第一行代码学习笔记第三章——UI开发的点点滴滴

    知识点目录 3.1 如何编写程序界面 3.2 常用控件的使用方法 * 3.2.1 TextView * 3.2.2 Button * 3.2.3 EditText * 3.2.4 ImageView ...

  6. 第一行代码学习笔记第十章——探究服务

    知识点目录 10.1 服务是什么 10.2 Android多线程编程 * 10.2.1 线程的基本用法 * 10.2.2 在子线程中更新UI * 10.2.3 解析异步消息处理机制 * 10.2.4 ...

  7. 第一行代码学习笔记第七章——探究内容提供器

    知识点目录 7.1 内容提供器简介 7.2 运行权限 * 7.2.1 Android权限机制详解 * 7.2.2 在程序运行时申请权限 7.3 访问其他程序中的数据 * 7.3.1 ContentRe ...

  8. 第一行代码学习笔记第五章——详解广播机制

    知识点目录 5.1 广播机制 5.2 接收系统广播 * 5.2.1 动态注册监听网络变化 * 5.2.2 静态注册实现开机广播 5.3 发送自定义广播 * 5.3.1 发送标准广播 * 5.3.2 发 ...

  9. 第一行代码学习笔记第九章——使用网络技术

    知识点目录 9.1 WebView的用法 9.2 使用HTTP协议访问网络 * 9.2.1 使用HttpURLConnection * 9.2.2 使用OkHttp 9.3 解析XML格式数据 * 9 ...

  10. 安卓教程----第一行代码学习笔记

    安卓概述 系统架构 Linux内核层,还包括各种底层驱动,如相机驱动.电源驱动等 系统运行库层,包含一些c/c++的库,如浏览器内核webkit.SQLlite.3D绘图openGL.用于java运行 ...

最新文章

  1. linux下的find文件查找命令与grep文件内容查找命令(转)
  2. 一句话后门中eval和assert的区别
  3. linux 5 防火墙,CentOS 5 Linux iptables防火墙的配置
  4. accp8.0转换教材第11章Ajax交互扩展理解与练习
  5. First Scrum 冲刺
  6. java mp3 暂停,Java MP3播放器 - 使用jLayer播放,暂停和搜索不能正常工作
  7. Freemarker循环遍历
  8. docker 镜像开机自启动_Docker常用命令总结
  9. error while loading shared libraries: libstdc++.so.6
  10. 从伪随机数的产生到高大上的蒙特卡洛算法(C语言实现)
  11. android内存测试方法,Android内存测试方法.doc
  12. [ZT]用CSC.exe来编译Visual C#的代码文件,解释CSC参数和开关的具体作用
  13. 我眼中的《APUE》
  14. 88个塑胶模具设计中常用的知识点
  15. 第二章:HLK-7621开发板介绍
  16. Vue生成条形码jsbarcode
  17. 程序员年薪百万,原来是吃到了这样的红利!
  18. MySQL——数据库
  19. 电脑出现不良代码查找
  20. 用什么软件可以给照片加文字描述?

热门文章

  1. Sass-@if,@else if的用法
  2. 少儿 计算机软件 测试,计算机导航--听觉评估系统儿童汉语语音词表测试结果分析...
  3. linux yum 安装桌面,CentOS 中 YUM 安装桌面环境
  4. 通过反射动态修改自定义注解属性值
  5. Windows文件夹管理利器 Clover,值得你拥有
  6. “十步一杀” 消压力于无形
  7. Mysql的sql优化方法
  8. 油液多参数云监测平台实现人工智能化
  9. 西门子携手惠普,将3D打印技术的应用从原型创建扩展至批量生产
  10. jquery实现的圣诞节动画效果