作为一名软件工程师,日常与代码打交道,免不了接触到编程规范。而编程规范,可谓是各式各样,不同系统有不同风格,不同公司有不同风格。就笔者经历而言,在学生年代学习单片机编程时已经开始建立自己的“编程规范”。总的来说,笔者认为《高质量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)。
以笔者经历,一些奇异用法的地方会写上注释,操作硬件模块会写上各步骤注释。一些待确认、待进行的,使用TOCHECK
、TODO
字眼。注释因人而异,不能完全信奉教条。适当的注释能起划分段落的作用,利于代码布局。
错误的注释会加扰对代码的理解,可能是改了代码未改注释,可能是前人加的注释,后人不理解,也不敢改。
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 | #ifndef FOO_BAR |
不好的示例1:
1 | #if (!defined(A_PLATFORM) && !defined(_B_PLATFORM_)) || (defined(_Z_APP_)) |
不好的示例2(原型来自TI某代码):
1 | switch (cmd) |
应改为:
1 | switch (cmd) |
信息打印
在调试期间,可以打印许多的信息,但要使用宏或者等级来控制,不要直接出现调用printf的实际函数。
该打印的一直要打印出来,比如错误信息,一些重要的步骤过程(监控代码运行)。
不该打印的不要打印,比如打印传递的参数值、为跟踪代码而打印的(作为debug打印)。
不同的错误原因,不能使用同一打印信息(为省事直接复制代码),打印信息应准确反应错误原因。
不好的示例:
1 | if (init_foo() < 0) |
应改为:
1 | if (init_foo() < 0) |
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 | int data[2]; // 此处为2个元素的数组 |
判断语句,布尔型的可以使用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 夜