C++代码规范(必须要遵循以及举例)
版权、文件声明规范
Q:为什么要申明版权以及说明?
这个更多意义在于以后的维护,以及代码阶段的版本控制。当项目进入成熟阶段之后,后期维护工作会占据很大的精力
考虑到不同客户类型的需求,往往“类似功能”模块会有好几个,有些模块也不经常改动,因此很容易忘记,良好的习惯,就是增加对应的注释。
1、版权和版本的声明
【规范1-1-1】 C/C++在头文件(.h)需进行版权、版本、作者等声明。
版权和版本的声明位于头文件和源文件的开头(参见示例),主要内容有:
- 版权信息
- 文件名称,摘要
- 当前版本号,作者,日期
/**
*****************************************************************************
* Copyright (C), 2010-2019, GOSUN CL
*
* @file 文件名(包含后缀名)
* @brief 简要描述本文件的内容
*
* @author 作者
* @date 输入日期
* @version 版本号
*
* (若本次是第一个定型版本,可以省略下列Note内容)
*
*----------------------------------------------------------------------------
* @note 历史版本 修改人员 修改日期 修改内容
* @note
*
*****************************************************************************
*/
///以下是例子,供参考///
/**
*****************************************************************************
* Copyright (C), 2010-2019, GOSUN CL
*
* @file CLUtility.h
* @brief 各种通用小功能/通用文件操作
* 1. 小功能:新增 正则匹配/查找下一个字符/文件夹(全部)创建
* 2. 重新封装: CECDateTime/CECString/CECFile/CECLogFile/
* 3. 新增功能模块:CECRunLog(运行日志)/CECReadIni(ini文件读取)
*
* @author gu
* @date 2019-07-18
* @version V1.1.0 20190718
*----------------------------------------------------------------------------
* @note 历史版本 修改人员 修改日期 修改内容
* @note v1.0 gu 2018-2-15 1.创建
*
*****************************************************************************
*/
2、函数声明
【规范1-2-1】 每个函数(除了构造跟析构可以不用),都应该在函数上方进行注释,注释内容要与实际内容含义保持一致。
PS:如果函数功能或输入参数等信息发生变化,需要及时修改注释信息
/**
*****************************************************************************
* @brief 描述该函数主要功能
*
* @param[in] 参数名(与函数里的参数一致) 参数的描述 in:表示输入类型
* @param[out] 参数名(与函数里的参数一致) 参数的描述 out:表示输出类型
* @param[in,out] 参数名(与函数里的参数一致) 参数的描述 in,out:表示既是输入又是输出
*
* @return 返回值的说明
* @author (若与文件申明作者是不一样的,需要填写;否则,可以省略此项)
*
* (若本次是第一个定型版本,可以省略下列Note内容)
*----------------------------------------------------------------------------
* @note 历史版本 修改人员 修改日期 修改内容
* @note v1.0 gu 2019-5-15 1.创建
*
*****************************************************************************
*/
///以下是例子,供参考///
/**
*****************************************************************************
* @brief 对外接口,用于new创建对象(实例CRunLog)
*
* @param[in] strPath 存放路径(完整)
* @param[in] nMaxDay 最多存放多少天
* @param[in] nLenDay 每天多少长度(字节)
*
* @return CRunLog的实例,指针
*----------------------------------------------------------------------------
* @note 历史版本 修改人员 修改日期 修改内容
* @note v1.0 gu 2019-5-15 1.创建
*
*****************************************************************************
*/
注释规范
Q:为什么要注释规范?
首先,统一的注释风格,代码看起来也非常清爽,后期代码走读的时候,也更关注逻辑本身。
其次,使用doxygen等第三方工具导出说明文档,也是需要规范的注释,降低后期的工作量。
1、代码注释
注释采用上方以及右侧两种方式。
注释应当准确、易懂,防止注释有二义性。
如果是右侧方式,建议 采用“///< xxxx”方式(在doxygen “//” 表示下一行注释)
采用“///<”进行右侧注释地方:
1、类成员(包含 成员变量、成员函数)
2、结构体(变量)
3、全局变量
4、静态变量
举例:右侧方式,注释
u_char m_byPipe; ///< 通道号
举例:上方方式,注释
//报文协议处理函数
void DealWithData(const byte *pBuf, int nLen);
【规范2-1-1】 必须要进行注释的地方如下所示:
- 说明性文件(如头文件.h文件、.inc文件、.def文件、编译说明文件.cfg等)头部以及源文件头部应进行注释。
- 函数头部应进行注释。
- 全局变量、成员变量、静态变量、常量要有的注释。
PS:其他非“必须”注释的地方不是说不用注释。注释对于代码维护尤其重要,良好的注释能够体现一个软件的水平。
【规范2-1-2】 修改代码同时修改相应的注释,以保证注释与代码的一致性。不再有用的注释要删除。
命名规范
Q:为什么要命名风格要统一?
命名就好比给你自己小孩取名字。小孩取名字,我们要关注 “姓”,“辈分”,“含义”,“男女偏好”,“简洁”,“好记,好理解”等等。
所以取名字也是非常讲究的。(灵魂拷问:你爱你的孩子吗?你爱你的代码吗?既然爱,怎么不取个好点的名字呢!)
有些人会喜欢很长的信息来表示名字,动不动就几十个字符,这样也不好,你想,如果你小孩的名字也很长,你会喜欢吗?
命名分为 类(结构体)、函数、常量、全局变量、静态变量、成员变量、临时变量(参数)等命名规则,应遵循以下规则:
两个基本原则:
- 含义清晰,不易混淆;
- 不和其它模块、系统API的命名空间相冲突
1、命名
【规范3-1-1】 变量、参数用类型缩写+大写字母开头的单词组合而成。
变量的名字应当使用
“小写缩写类型字符”(参考表) + “名词”
“小写缩写类型字符”(参考表) + “形容词/动词” + “名词”
BOOL bRes;
int nDrawMode;
常见的缩写:
//缩写(前缀)类型
b Boolean /bool/BOOL
n Integer /int
str String,char[],CString
p Pointer //指针
h Handle
fn Function
变量命名容易分两种极端情况,一种特别长,一种特别短,这里需要注意。
例如:
- int nPrivateDTCSaveToFile; ///< 特别长,意思够丰富,但啰嗦
- Cfile file; ///< 缺少必要信息,等于没有任何含义。
建议:变量命名采用2-3个单词。
关于缩写的约束:
【规范3-1-2】 禁止使用拼音首字母进行创造性缩写。除了行业性缩写用语可以用缩写,其他不建议用缩写。
例如:WH(维护),这样的用法是禁止的
行业性缩写,例如:GYK(轨道车运行控制)、CIR、MMI、DMI,这些有些是英文缩写,有些是中文缩写,但已经形成行业用语,可以采用大写缩写方式。
2、静态变量
【规范3-2-1】 静态变量加前缀s_(表示static)。
void Init()
{
static int s_nAllValue; //静态变量
}
//如果静态变量又是全局或成员变量(不推荐这样操作),应该采用如下方式:
ms_nVal
gs_nVal
3、全局变量
【规范3-3-1】 如果需要定义全局变量,则使全局变量加前缀g_(表示global)
int g_nManyPeople; ///< 全局变量
int g_nMuchMoney; ///< 全局变量
4、类成员变量
【规范3-4-1】 类的数据成员加前缀m_(表示member),这样可以避免数据成员与成员函数的参数同名。
void Object::SetValue(int nWidth, int nHeight)
{
m_nWidth = nWidth;
m_nHeight = nHeight;
}
5、常量
【规范3-5-1】 常量全用大写的字母,用下划线分割单词。
//C++ 写法如下:(不允许 #define 方式,不允许混合写法)
const int MAX_DATALEN = 100;
const int MIN_LENGTH = 100;
//C写法如下:
#define MAX_DATALEN 100
#define MIN_LENGTH 100
6、类名、函数名
【规范3-6-1】 类名和函数名用大写字母开头的单词组合而成。
class RunWorker ///< 类名称可能是某些功能的集合也可能是某个类型,建议名词
int SendData() ///< 函数一般来说表示具备什么功能,因此建议采用动名词组合
7、结构体命名
【规范3-7-1】 自定义结构体名称要以“_tag”为开头。
typedef struct _tagWorkerSet
{
byte byPipe;
byte byIsCAN;
byte byClass;
};
8、枚举类型 命名
【规范3-8-1】 枚举类型采用全用大写的字母,用下划线分割单词。
enum OPER_RES
{
OPER_SUCCESS = 0, ///< 成功
OPER_COPYING = 1, ///< 正在复制(上次拷贝没结束)
OPER_FREE = 3, ///< 空闲
OPER_DIR = 100, ///< 操作文件夹(内部)
OPER_FILE = 101, ///< 操作文件(内部)
OPER_ERROR = -1, ///< 普通错误
OPER_OPENDIR_ERROR = -2, ///< 打开文件失败
OPER_NO_DIR = -3, ///< 无效文件夹(不存在,或路径错误)
OPER_NO_COPYFILE = -4 ///< 不需要拷贝,文件都认为相同
};
代码行为规范
Q:代码行为也有规范?
随意性的代码不仅与整体风格,格格不入,也会导致代码混乱,甚至导致隐藏bug。。。。说起来很严重
1、总体原则
【规范4-1-1】 一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样的代码容易阅读,并且方便于写注释。
//下列代码是不允许
int nMode = 0, nCount; ///< 定义了多个变量
DoFun(); RunApp(); ///< 多个执行,多件事情
【规范4-1-2】 if、for、while、do 等语句自占一行,执行语句不得紧跟其后。
if(nLevel == PLEVEL_HIGH)
{
m_lsHigh->Add(pAdd);
}
else if(nLevel == PLEVEL_REPEAT)
{
pAdd->nOldTick = GetTickTime();
m_lsRep->Add(pAdd);
}
else
{
m_lsLow->Add(pAdd);
}
【规范4-1-3】 if语句 必须考虑else的处理。 对于主要逻辑判断,必须写else处理,如果改else是正常不出现的,那么else处理归结为异常处理。
//默认是要打开
if(m_pSerialList[i]->OpenPort() > 0)
{
//DEBUG_LOG02(LOG_IF_SERIAL,"open Serial port successful.",pSet->stP.byPipe,pSet->byPort);
m_pSerialList[i]->StartRun();
//....
}
else
{
//else 不允许不处理
DEBUG_LOG02(LOG_IF_SERIAL,"open Serial port failed.",pSet->stP.byPipe,pSet->byPort); ///< 此处else,理论上可以不处理,但要归结为 异常处理(例如日志记录)
}
2、运算符与表达式
【规范4-2-1】 如果代码行中的运算符比较多(超过2个),用括号确定表达式的操作顺序,避免使用默认的优先级。
【规范4-2-2】 如果是算术运算符(例如:加 减 乘 除)且超过2个,可以不用括号进行确定操作顺序。
【规范4-2-3】 如果全部是相同的运算符且超过2个,可以不用括号进行确定操作顺序。
为了防止产生歧义并提高可读性,应当用括号确定表达式的操作顺序。
nGetData = (nHigh << 8) | nLow; ///< 用括号表示优先级
if ((bResR | bResG) && (bResR & bResB))
PS:
&& 操作符(同理||操作符,也是有存在类似问题),关系是当前面条件为false时,后面的是不会被执行。因此不推荐以下做法:
if(bRes && CallFun()) ///< 此时若bRes为false,那么CallFun是不会被执行
这里也很容易出错,往往也很难理解,甚至不同编译器对优先级理解不一样。
3、if 语句
3.1 布尔变量与零值比较
【规范4-3-1】 不可将布尔变量直接与TRUE、FALSE 或者1、0 进行比较。
根据布尔类型的语义,零值为“假”(记为FALSE),任何非零值都是“真”(记为TRUE)。
TRUE 的值究竟是什么并没有统一的标准。例如Visual C++ 将TRUE 定义为1,而Visual Basic 则将TRUE 定义为-1。
//假设布尔变量名字为bFlag,它与零值比较的标准if 语句如下:
if (bFlag) // 表示flag 为真
if (!bFlag) // 表示flag 为假
//其它的用法都属于不良风格,例如:
if (bFlag == TRUE)
if (bFlag == 1 )
if (bFlag == FALSE)
if (bFlag == 0)
3.2 整型变量与零值比较
【规范4-3-2】 应当将整型变量用“==”或“!=”直接与0 比较。
//假设整型变量的名字为nValue,它与零值比较的标准if 语句如下:
if (0 == nValue)
if (nValue != 0)
//不可模仿布尔变量的风格而写成
if (nValue) ///< 会让人误解nValue 是布尔变量
if (!nValue)
3.3 浮点变量与零值比较
【规范4-3-3】 不可将浮点变量用“==”或“!=”与任何数字比较。
//千万要留意,无论是float 还是double 类型的变量,都有精度限制。所以一定要避免将浮点变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。
//假设浮点变量的名字为fData,应当将
if (0.0 == fData) ///< 隐含错误的比较
//转化为
if ((fData >= -EPSINON) && (fData <= EPSINON))
//其中EPSINON 是允许的误差(即精度)。
3.4 指针变量与零值比较
【规范4-3-4】 应当将指针变量用“==”或“!=”与NULL 比较。
指针变量的零值是“空”(记为NULL)。尽管NULL 的值与0 相同,但是两者意义不同。假设指针变量的名字为pPen,它与零值比较的标准if 语句如下:
if (NULL == pPen) ///< pPen 与NULL 显式比较,强调pPen 是指针变量
if (pPen != NULL)
//或(前提必须符合命名规范)
if (pPen)
if (!pPen)
//不要写成
if (pPen == 0) ///< 容易让人误解p 是整型变量
if (pPen != 0)
4、switch 语句
【规范4-4-1】 每个case 语句的结尾不要忘了加break,否则将导致多个分支重叠(除非有意使多个分支重叠)。
【规范4-4-2】 若某个case不需要break一定要加注释声明,便于明白是故意不加而不是忘记。
【规范4-4-3】 不要忘记最后那个default 分支。即使程序真的不需要default 处理,也应该保留语句default: break;
switch(nParam)
{
case NOPER_UPDATE: ///<升级状态
DoUpdate();
break;
case NOPER_RUN: ///<开始运行
RunApps();
break;
default:
break;
}
5、循环语句(for / while)
【规范4-5-1】 为了防止循环失去控制,尽量不在循环体内修改循环条件变量,首先是规避,然后如果无法规避,必须注释清楚。
for (int i = 0; i < NMAX_RUNSTATE; i++)
{
if(pMain->m_lsAppRun[i] != NULL)
{
//...
}
else
{
i = 0; ///< 虽然此处理论上不存在,但如果出现,必然是死循环
}
}
6、关于空格、空行
【规范4-6-1】 连续超过10行的代码必须要添加空行。
原则:
1、 不同性质的代码(或变量)之间要有空行
2、 相同性质的代码应该放在一起,超过10个的,也要添加空行
3、 性质是指 属性、含义、功能相似等条件,可以根据实际情况来区别
//初始化
m_byKeyUsed = 0x00;
m_byKeyBit7 = 0x80;
m_nCountSend = 0;
m_byLastKey = 0x00;
//读取数据(采用定时器读取数据,不采用事件,方便移植到linux)
m_pRecvTimer = new QTimer(this);
m_pRecvTimer->setInterval(50);
connect(m_pRecvTimer,SIGNAL(timeout()),this,SLOT(ReadKeyCom()));
//发送数据
m_pSendTimer = new QTimer(this);
m_pSendTimer->setInterval(50);
connect(m_pSendTimer,SIGNAL(timeout()),this,SLOT(WriteKeyCom()));
m_pSendTimer->start(50);
m_pRecvTimer->start(50);
//显示刷新
ShowToObj(m_byNowKey);
//打开串口
OpenKeyCom();
【规范4-6-2】 关键字之后要留空格。
像 const、virtual、inline、case 等关键字之后至少要留一个空格,否则无法辨析关键字。
像 if、for、while 等关键字之后应留一个空格再跟左括号‘(’,以突出关键字。
【规范4-6-3】 赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符,的前后应当加空格。 *
如“=”、“+=” “>=”、“<=”、“+”、“”、“%”、“&&”、“||”、“<<”,“^”等二元操作符的前后应当加空格。
void OnReadFile(int nX, int nY, int nZ); ///< 良好的风格
void OnReadFile (int nX,int nY,int nZ); ///< 不良的风格
if (nYear >= 2000) ///< 良好的风格
if(nYear>=2000) ///< 不良的风格
函数规范
Q:函数有哪些地方需要注意的?
函数除了名称之外,还有参数,以及必要输入,输出等约束
何为圈复杂度?
1、参数传递效率
【规范5-1-1】 如果输入参数以值传递的方式传递对象(类型为类、结构体),则用“const & ”方式来传递
这样可以省去临时对象的构造和析构过程,从而提高效率。对于类型为常规类型,如char int等,建议采用直接传值的方式。例如:
//C++ 写法要求:
GetErrorInfo(const _tagRunInfo &stRun)
2、出入口检查,安全
【规范5-2-1】 在函数体的“入口处”,对参数的有效性进行检查。
int CLocalSocket::SendData(const byte byOrder,const byte *pbyBuf,const int nLen)
{
if((nLen > MAXLENGTH_FRAME) || (nLen < 0) || (pbyBuf == NULL))
{
return -1;
}
//....
}
参数必须要判断:
1. 指针类型的参数
对于其他类型 如 int,建议根据实际情况进行判断。
3、入口唯一
【规范5-3-1】 任何函数只能有一个入口;即只能使用函数调用的方式进入函数,不能使用goto等语句进入函数内部。
4、圈复杂度
【规则5-4-1】 函数的圈复杂度应小于20。
圈复杂度用来衡量一个模块判定结构的复杂程度,数量上表现为独立线性路径条数,即合理的预防错误所需测试的最少路径条数。
圈复杂度大说明程序代码可能质量低且难于测试和维护,根据历史经验,程序的可能错误和高的圈复杂度有着很大关系。
圈复杂度计算方法:
- 如果一段源码中不包含控制流语句(条件或决策点),那么这段代码的圈复杂度为1,因为这段代码中只会有一条路径;
- 如果一段代码中仅包含一个if语句,且if语句仅有一个条件,那么这段代码的圈复杂度为2;
- 包含两个嵌套的if语句,或是一个if语句有两个条件的代码块的圈复杂度为3,依次类推。
5、代码行数要求
【规则5-5-1】 一个函数原则上要求代码行数不超过200行(包含注释、空行)。
代码行数比较多,说明开发人员缺乏进一步思考,仅关注功能实现。
代码行数比较多,意味着逻辑复杂、思路不清晰,存在代码隐患就比较高。
代码行要求200行以内已经是非常宽泛的要求了,实际应该在50行以内为最佳。
PS:
另外建议,单个 类(.cpp文件)或.c文件源代码行控制在1000行以内。否则就应该先考虑是否可以进行优化设计。
内存使用规范
Q:内存使用本身就是C++的精华,也是把双刃剑。
除了细心一点,还要注意一些原则。否则出了事情,都是大事情。
1、防止产生“野指针”
【规范6-1-1】 释放了内存之后,立即将指针设置为NULL,防止产生“野指针”,除非该指针变量本身将要消亡。
【建议】申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存;
delete m_pTxOri;
m_pTxOri = NULL; ///< 及时变成NULL
delete[] m_pWriteBuf;
m_pWriteBuf = NULL; ///< 及时变成NULL
野指针指向一个已删除的对象或未申请访问受限内存区域的指针。
与空指针不同,野指针无法通过简单地判断是否为 NULL避免,而只能通过养成良好的编程习惯来尽力减少。
对野指针进行操作很容易造成程序错误。
以下一些手段可以有效控制:
- new delete 在一个函数内,最好是在一个屏幕可视范围内
- new出来的对象,不要被其他对象进行引用
- 特别是结构体的成员或类成员是指针,要注意被引用的可能。
像例子的那样,一般也还好,比如这样,就麻烦:
byte *pBuf = m_pTxOriBuf;
//do somethings
delete[] m_pTxOriBuf;
m_pTxOriBuf = NULL; ///< 及时变成NULL
//.....
//...
//do somethings
if(pBuf != NULL)
{
//此时pBuf 是野指针了,比较容易忽视这一点。
pBuf[0] = 0;
}
再来一个比较隐蔽的例子:
void CallFun1(byte *pBuf)
{
//....
delete[] pBuf;
pBuf = NULL;
}
void CallFun2(byte *pBuf)
{
//....
pBuf[0] = 0x00;
//....
}
void main()
{
byte *pTxBuf = new byte[250];
//do somethings
CallFun1(pTxBuf);
//.....
//...
//do somethings
if(pTxBuf != NULL)
{
//此时pBuf 是野指针了,比较容易忽视这一点。
CallFun2(pTxBuf);
}
}
2、数组越界
【规范6-2-1】 避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。
数组越界会带来不可预期的问题,也有可能运行时不报错。所以经常会犯错,特别是一些莫名其妙的错误,往往是越界导致。
//eg1:
char strName[20];
for(int i = 0;i <= sizeof(strName);i++) ///< 已经越界
{
strName[i] = 0x30 + i;
}
//eg2:
const int MAX_NMAE_LEN = 16; ///< 原先是10,由于协议改变,现在变成16
char strName[10]; ///< 此处未及时改过来
for(int i = 0;i < MAX_NMAE_LEN;i++) ///< 已经越界
{
strName[i] = 0x30 + i;
}
容易导致越界:
数组下标是变量,当特殊情况下,变量可能变成-1或很大一个值
协议内容定义变化,原先协议是没问题,后来协议增加了,导致问题出现
3、内存泄露
【规范6-3-1】 当对象消亡时确保指针成员指向的系统堆内存全部被释放,否则会造成内存泄露。
另外应当注意,如果是用第三方一些API或机制,一定要注意是否有内存泄露的风险。比如在GDI中,资源如果不回收,那么就会内存泄露。
Image* pImage = Image::FromFile(L"E:\\bac.bmp");
Graphics g(pDC->m_hDC);
g.DrawImage(pImage,0,0);
delete pImage; ///< 必须,否则泄露。这个取决API函数的特性
g.ReleaseHDC(pDC->m_hDC);
【规范6-3-2】 动态内存的申请与释放必须配对(特殊应用可特殊处理,但不建议把特殊当成常规),不需要的内存应被及时释放,防止内存泄漏。
并且new、delete和new[]、delete[]要成对使用。特别要注意中间return。
不匹配也会导致内存泄露的。