最近开始接手项目组的开发管理工作,项目组开发的产品一期功能基本开发完成,进入内部测试及小渠道发布阶段,然而产品的稳定性还存在很大问题。
先做一下背景介绍,项目组开发的是一款面向C端的互联刚产品,运行操作系统为windows。整个项目使用c++开发,总体代码量大概在数十万行。
c++是一门很复杂的语言,有很多强大的特性,然而当用其开发一款商业产品时,这些特性可能会带来麻烦。所以当设计c++的使用规范时,更多的是对其做减法。
本文的规范针对VC++开发环境,开发工具为Visual Studio。
文件系统目录规范
一款完整的商业产品开发通常会涉及到很多模块,这其中包括可执行程序(.exe),项目组开发的库(静态库或动态库),第三方的库(静态库或动态库),测试程序,这么多的模块和代码,需要一个良好组织的目录结够。
这里假设项目名称为XXProject
- XXProject
- XXProject.sln
- Bin
- Debug
- Release
- TestBin
- Debug
- Release
- Src
- Document
其中Bin存放需要发布的可执行程序,TestBin存放测试程序的可执行文件,Src存放项目的工程文件和源代码,Document存放项目相关的开发文档(如项目说明,代码规范等)。
接下来假设项目包括如下工程:XXMain(发布的主程序),XXUpdate(升级程序),XXSdk(自己开发的基础库),XXThird(第三方库), XXTest(测试程序)
- Src
- XXMain
- XXMain.vcxproy
- code文件
- XXUpdate
- XXUpdate.vcxproy
- code文件
- XXTest
- XXTest.vcxproy
- code文件
- XXSdk
- XXSdk.vcxproy
- code文件
- ThirdLib
- XXThird
- Lib
- Debug
- Release
- Include
Lib库用来存放静态库,动态库的.a文件,Include用来存放公共头文件,ThirdLib用来存放第三方库。
解决方案目录规范
解决方案目录是VS开发工具提供的逻辑上组织项目的方式,与物理文件系统并不存在对应关系。
仍然假设项目包含上述项目和模块。
- Solution (解决方案)
- Application (解决方案文件夹)
- XXMain (工程)
- XXUpdate (工程)
- Test (解决方案文件夹)
- XXTest (工程)
- Library (解决方案文件夹)
- XXSdk
- ThirdLibrary(解决方案文件夹)
- XXThird
- Public (解决方案文件夹)
- 公共头文件
代码编写规范
1:禁用全局变量
全局变量会带来晦涩的依赖问题
2:禁用goto指令
goto指令的代码难以阅读和维护
3:禁用异常机制
c++的异常机制有很多缺陷且复杂
4:用struct封装数据,使用class定义对象
C++中class和struct几乎没有区别,在规范中进行语义的区分
5:struct和class必须显示包含构造函数
6:除非特殊情况,总是将析构函数定义为虚函数
方便继承时的资源释放
7:不要在构造函数中执行复杂操作,推荐加入init函数用于初始化操作
构造函数没有返回值,难以反馈错误
8:不要在构造函数中调用虚函数
构造函数中的虚函数不会重定向到子类。
9:慎用继承
相比对象组合,继承带来更强的依赖,推荐使用接口继承而不是对象继承
10:禁用多重继承(接口继承除外)
多重继承通常代表不好的设计
11:慎用运算符重载
运算符重载会混淆代码的语义,应只在不会造成混淆时使用
12:将成员设置为私有并提供访问函数
封装是降低代码耦合的有力武器
13:将同一访问权限的成员定义在一起
可以按照public,protect,private顺序进行组织
14:避免出现大而全的类
当一个类的代码超过1000行,应有所警惕,超过2000行,则应考虑拆分(行数不包括注释)
15:头文件应包含它所需要的头文件
这样可以保证cpp文件引入该头文件后不需要包含其它头文件
16:合理的组织引入的头文件,不要重复引入,不要引入不必要的头文件
可以以系统头文件,第三方库头文件,项目组库头文件,本程序头文件来组合,不同类型头文件之间用空格隔开
17:头文件使用#define宏来避免多重包含
pragma once 指令只有VC编译器能识别
18:允许合理的使用友元特性
19:使用引用传递对象类型参数, 对于不需要改变的参数加入const修饰符
引用传递可以避免对象拷贝
20:函数应该进参在前,出参在后
21:使用明确的返回值指示函数的运行结果,而不是用返回的内容来指示结果
推荐: int GetDeviceName(string& deviceName);
不推荐: string GetDeviceName()
22:声明基本类型变量后立即赋值
正确 int nCount = 0; bool bSuc = false; 错误 int nCount; bool bsuc;
23:使用内联,枚举,常量来代替宏
宏的使用有很多弊端,应尽量避免
24:使用singleton模式代替静态类
相比静态类,singleton模式可以更好地控制初始化时机。
25:使用share_ptr来管理指针
指针和管理在复杂项目中十分困难,使用智能指针是不二选择
26:使用weak_ptr来处理循环引用
27:明确对象或资源的生存周期
明确对象的生存周期通常代表着良好的设计
28:合理的使用缩进,空格
最重要的是保持风格的统一,自动生成的代码可能会打破这种统一,应该灵活设计规则
29:合理使用typedef缩减类型的长度,合理使用auto
使用stl时经常会导致过长的类型,合理使用auto可以有效减少代码长度
命名规则
由于是在VC环境下开发,沿用微软的命名驼峰命名法
选择哪种命名方式实际上不是很重要,最重要的是保持统一
- 代码文件: DeviceMgr.h, DeviceMgr.cpp
- 解决方案目录: NetLibrary
- 工程筛选器: DataModel
- 类:CDeviceMgr
- 结构体: DeviceInfo
- 变量: listDevice
- 类成员: m_deviceName
- 函数: GetName; GetDeviceName;
- 代表bool含义的变量: 都以b开头,如:bOk, bSafe
- 代表整数含义的变量: i表示符号整数,n表示无符号整数
- 避免无意义的变量名和缩写: 如 x,dn(deviceName)等
注释规则
- 文件注释:注明文件作者,联系方式,文件代码作用,重大修改记录等
- 类注释:说明类的作用,使用限制等等
- 函数注释: 尽量依靠意义明确的函数命名而不是依靠注释,说明函数的使用限制,对于意义不明确的参数加以说明
- 变量注释:尽量依靠意义明确的变量命名而不是依靠注释,特殊情况。
- 实现注释: 对于使用了非常规技巧,或复杂算法,或很复杂的业务逻辑部分要加入注释说明
原则: 注释应风格统一,简短而意义明确,最终目的是有效帮助其它人阅读和理解代码的目的
日志
日志的打印十分重要,是产品发生问题时重要的参考依据
- 日志的打印要尽量详尽,合理划分日志等级,一般为Fatal, ERROR, WARNING, DEBUG, INFO
- 统一使用unicode(utf-16)编码来输出日志
- 提供异步打印日志的接口
- 提供定期清理日志的机制
- 在发布版中将日志等级设置为ERROR或更高,提供配置文件供调整日志等级
一些其它规则
- 避免大而全的类(代码控制在1000行以内)
- 避免过长的函数(代码控制在200行以内)
- 避免深层的嵌套(不要超过3层)
- 使用do-while-break技巧来避免重复写释放资源的代码
- 尽量使用RALL技巧来释放资源
- 当函数或代码废弃时,应与标注,最好将其注释掉并定期清理
线程和锁
复杂的项目肯定会涉及到多线程开发,而开发多线程程序是十分困难的。据我们统计,项目组产品有70%左右的崩溃和bug和线程有关。
将线程模块独立出来,交给项目最有经验的开发人员管理和维护,对外暴露抽象接口,屏蔽线程的概念。
强制使用RALL技术来使用锁
未尽
本文并没有涉及到C++规范的所有方面,欢迎讨论和补充