嵌入式Linux入门12:编程规范

作为一名软件工程师,日常与代码打交道,免不了接触到编程规范。而编程规范,可谓是各式各样,不同系统有不同风格,不同公司有不同风格。就笔者经历而言,在学生年代学习单片机编程时已经开始建立自己的“编程规范”。总的来说,笔者认为《高质量C、C++编程指南》、《Google C++ 编码规范》非常好,有指导意义。工作多年,接触了多种风格,并能自由切换。但还有一些原则性的条例,是自己一直坚持的。

笔者一直认为,所谓的规范,不仅仅要规范命名风格,还要规范编码的原则。因此,本文结合参考的资料以及笔者经验,关注两大方面:编码风格以及设计原则。

一、编码风格

编码如写文章,有段落,有条理。如书法,有整体布局。本节主要就目录组织形式和书写命名方面进行介绍。

1、源码组织(目录格式)

将头文件及实现文件分开存放,但内部使用的头文件可以不按此约定。使用第三方库源码亦要与自实现代码分开存放。示例:
app:应用层
bin:编译生成的二进制可执行文件
build:编译脚本
core:核心业务目录
doc:文档
test:测试目录
drivers:设备驱动
3rdpart:第三方库

2、命名

与当前所使用的代码风格保持一致。

总体原则:入乡随俗。比如:
内核及driverlib层使用linux风格。
应用层程序使用 windows风格
借鉴他人(如TI的mcfw代码、live555)
同一项目,不能模块,可以存在不同的风格。

源码文件名称需要统一使用一种风格。

比如,统一使用小写+下划线,或者统一使用大小写。

形式:动词+名词。

可顺序阅读的。如获取下一帧数据:getNextFrameData()

类:开头、所有词首字母大写

变量:开头小写

宏、枚举:全部大写

成员变量:可加m_作区别,亦可不加。

专用名大写(如:PI、OSD)

常用前缀:vec、lst、ptr(或p)

void createNew()

int rtpPortNum

int maxCNAMElen

doEventLoop()

3、代码布局

整个文件布局整齐、整洁,不能出现多余的空行,行尾也不应有多余的空格。

原则:如写文章一样,有段落。

4、注释

注释分为文件注释及函数接口注释,以清楚明白为原则,主要在头文件进行,在实现代码中需要写上重要的、必要的注释。风格不限,但需统一。建议使用doxygen格式的注释。doxygen注释可以生成文档,对于部门之间沟通,或者对外提供SDK比较方便。

只对代码无法表达的东西写注释(Comments Only What the Code Cannot Say)。

以笔者经历,一些奇异用法的地方会写上注释,操作硬件模块会写上各步骤注释。一些待确认、待进行的,使用TOCHECKTODO字眼。注释因人而异,不能完全信奉教条。适当的注释能起划分段落的作用,利于代码布局。

错误的注释会加扰对代码的理解,可能是改了代码未改注释,可能是前人加的注释,后人不理解,也不敢改。

5、头文件包含

原则:只在必须的文件中进行,比如bar.h头文件不需要foo.h,而bar.c文件需要,则仅在bar.c文件中包含。

不要包含不必要的头文件。

对包含的头文件可以作一些必要的注释。如:

1
#include <strings.h>   /**< bzero */

二、设计原则

上一节的代码,只徒有其表,写得再漂亮的代码,没有内涵,没有条理性,没有逻辑,也是狂然。本节就一些设计原则进行介绍。——至于如架构设计、设计模式、架构模式涉及内容太多,就不在本文讨论范围。

函数设计

1、函数接口返回值一般应该为整型 int,成功返回0,错误返回负数,并在接口注释中标明错误码。
例外:像内存管理类的接口,根实际情况作处理,如申请共享内存,要返回内存地址。

2、一个函数做一件事,把相对独立的事情封装到一个函数中。
举例:从JPEG转为BMP,应分2个函数,解压JPEG得到RGB数据,将RGB数据保存为BMP。

参数设计

若参数为只读字符串,不需要修改,则使用const,如打开文件:

1
void foo_open(const char* file)

若参数过多,可考虑封装为结构体。

对于C++,参数可以使用引用的方式。

宏定义及判断语句

以肯定语气为主,宏判断不宜过多。
示例:

1
2
3
#ifndef FOO_BAR
// something
#endif

不好的示例1:

1
#if (!defined(A_PLATFORM) && !defined(_B_PLATFORM_)) || (defined(_Z_APP_))

不好的示例2(原型来自TI某代码):

1
2
3
4
5
6
7
8
9
10
11
switch (cmd)
 {
     case DISABLE_A: // 如果禁止A
         break;
     case DISABLE_B: // 如果禁止B
         break;
     case DISABLE_NONE: // 如果都不禁止
         break;
     default:
         break;
 }

应改为:

1
2
3
4
5
6
7
8
9
10
11
switch (cmd)
 {
     case ENABLE_A: // 使能A
         break;
     case ENABLE_B: // 使能B
         break;
     case ENABLE_ALL: // 使能所有
         break;
     default:
         break;
 }

信息打印

在调试期间,可以打印许多的信息,但要使用宏或者等级来控制,不要直接出现调用printf的实际函数。

该打印的一直要打印出来,比如错误信息,一些重要的步骤过程(监控代码运行)。

不该打印的不要打印,比如打印传递的参数值、为跟踪代码而打印的(作为debug打印)。

不同的错误原因,不能使用同一打印信息(为省事直接复制代码),打印信息应准确反应错误原因。

不好的示例:

1
2
3
4
if (init_foo() < 0)
     printf("foo error.\n");
if (exit_foo() < 0)
     printf("foo error.\n");

应改为:

1
2
3
4
if (init_foo() < 0)
     printf("init foo error.\n");
if (exit_foo() < 0)
     printf("exit foo error.\n");

C++

C++11特性只选用合适的,模板排查问题时不好排查。
匿名函数,如代码行数少,可用,如多,单独一个函数。
命名空间不要太过如:
boost::asio::ip::tcp
std::chrono::time_pointstd::chrono::high_resolution_clock begintime;

少用try-catch。

简单的atoi用C风格的。

如果复杂的类型,用auto,简单的,不用。

stl:用list queue vector array map等。

其它

1、不要在if()括号中中写大量的函数调用,return也不要写。

1
if (foo() || bar() || a < b)

1
return (foo() ? b : a) || (bar() &&flag)

–>原因:1:函数调用不突出,因为在if里面。2:出错不好排查。因为不知道哪个函数出错。

2、不要一个语句写大量的函数。

ss* ptr = (ss*)foo()->getname()->file()->state()

–>如果中间有非法指针(如getname()返回NULL),可能会挂掉。

3、数组越界,GCC检查不出来的。示例:

1
2
3
4
5
6
int data[2]; // 此处为2个元素的数组
data[0] = param->enable;
data[1] = param->type;
data[2] = param->width; // 越界
data[3] = param->height; // 越界
data[4] = param->count; // 越界

判断语句,布尔型的可以使用if (!enable),也可以使用if (enable == false)

一定要用括号把优先级强调出来。——即使本身优先级已确定。

不要在宏中多次++操作。

C++结构体,直接当类使用、初始化:

struct ata_print_options
 {

bool drive_info;

ata_print_options()
     : drive_info(false),

{}

}

多用uint8_t、int32_t这类标准写法,不用int(在stdint.h头文件中,如没有,可自行定义)

本文涉及内容,仅为一人之言,不当之处,请方家斧正。

PS:如与参考资料有冲突之处,请以个人/公司实际情况为准。

参考资料:

《高质量C、C++编程指南》

《Google C++ 编码规范》

李迟 2017.9.5 夜