Skip to content

Latest commit

 

History

History
589 lines (398 loc) · 23 KB

CodingGuide.md

File metadata and controls

589 lines (398 loc) · 23 KB

序言

C++具有很多强大的语言特性,但是不受约束的编码会使得代码更易于出现bug、难于阅读和维护。

为统一开发部所有成员的代码规格,以方便阅读、理解,特制定本指南。由于开发语言的多样性和复杂性,也由于时间所限,本指南不可能涵盖所有情况。因此留待大家讨论和补充。代码风格的本质是使代码容易理解,因此一切关于本指南的改进不应该脱离于这种本质的要求。

本指南以C++为主要说明对象。

头文件

头文件定义

每个头文件都必须使用#define来防止被多重包含。如果是手动编写的.h文件,命名格式应该为:

<PROJECT>_<PATH>_<FILE>_H_

如:

#ifndef PMCAD_MAIN_COMDATA_H_

#define PMCAD_MAIN_COMDATA_H_

//头文件代码

#endif //PMCAD_MAIN_COMDATA_H_

Include文件以及lib文件路径的设定

Include以及lib文件的路径应该在Project中进行设置,禁止在VC的Option中修改全局的路径参数(包括P盘公共库路径也不得在此设置)。

标准的工程目录结构为:

+./

- lib

- inc

- Project1

...

在Project中的路径设置必须采用相对路径,如 ../inc,而不得用d:\aaa\inc的格式。(P盘的库以绝对路径设置)

头文件的引用

头文件的引用顺序应该依次为:

  • 1 系统头文件(如 stdlib.h)
  • 2 公共代码库头文件 (如 comdata.h)
  • 3工程自有的头文件

对于第1、2类头文件,应该是尖括号引用,如#include <stdlib.h>。

对于第3类头文件应该使用引号包含,如 #include "myclass.h"

对头文件的引用禁止使用绝对路径。

#include <hash_map>
#include <vector>

#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/public/bar.h"

变量

局部变量

局部变量应该放置于尽可能小的作用域内,并且声明的位置离第一次使用越近越好。

但是对于对象变量,需要考虑其构造函数与析构函数的效率,应该避免不必要的重复调用。

全局变量

禁止使用class类型的全局变量。

内建基本类型的全局变量是允许的,但是应该尽量避免。

禁止使用函数返回值来初始化全局变量。

(全局变量的构造函数以及析构函数的调用顺序是不确定的,使用全局对象容易导致难以发现的bug。比如dll不能加载,但debug可以加载等)

如果必须使用全局的对象变量,则使用单件模式(singleton patter)

静态变量与全局变量一视同仁

函数

内联函数

只有小于10行的程序才可以定义为内联函数

为了消除函数调用的时空开销,C++ 提供一种提高效率的方法,即在编译时将函数调用处用函数体替换,类似于C语言中的宏展开。这种在函数调用处直接嵌入函数体的函数称为内联函数(Inline Function),又称内嵌函数或者内置函数。

函数的参数

函数的参数顺序:输入参数在前,输出参数在后。

对于输入参数,一般采用常数引用的方式(const references)。这一点非强制性规定。

构造函数

  • 构造函数内不可调用虚函数,只能进行数据的初始化

  • 如果类有数据成员变量,则应该定义默认的构造函数,对数据初始化。编译器自动产生的默认构造函数不会初始化数据

  • 注意拷贝构造函数(包括等号=重载)

  • 禁止使用带缺省参数的构造函数

结构体与类

只有数据成员而没有成员函数的时候才可以使用struct,否则使用class

struct与class的命名规则不同

  • 关于继承

不可以滥用继承,只有明确意义的继承关系才用继承,否则用组合模式(Has-A而不是Is-A)

尽量避免多重继承,只有在一个基类是接口(Interface)的时候才可以使用多重继承

接口是特殊的类

  • 只有纯虚函数和静态函数

  • 没有非静态成员数据

  • 没有构造函数

操作符重载,尽量避免操作符重载,特别是=的重载,一般使用成员函数实现相关功能。如果确系需要(如使用STL容器)也必须有明确的说明注释

存取控制。成员私有或者保护,提供内联的Get和Set函数

声明次序。

  • 基本规则:public在private之前,成员函数在成员变量之前。

  • public-protected-private三部分

  • 每一部分中按照常量、构造函数、析构函数、成员函数、成员变量的顺序声明

成员函数

  • 函数体要短小,功能单一

  • 每个成员函数不可超过40行,保证不需要翻屏幕才能看全屏幕

命名规则

命名采用匈牙利命名法,以chBoundSameDensity为例,ch表示char变量,然后以一连串首字母大写的单词表述变量的意义。取名的意义十分重大,需要认真对待。因为固然你可以加一个说明解释变量的意义,但都不及一个好名字来得方便。代码的最高境界是不需注释就能明白代码的意义(当然一个精巧的算法应该用注释说明,一个公式也应该注释说明,因为此时只靠命名不能解决阅读问题)。 若命名过长,可适当考虑使用一些缩写,但缩写的原则应以仍能见名知意为准,否则就不应使用缩写。例如Button缩写为Btn是容易理解的。而自创的缩写往往是不容易(被别人)理解的。

  • 1 一般变量、函数参数变量的命名:变量类型前缀+变量名,变量类型的前缀如下,注意前缀为小写:
     char/TCHAR        ch     --字符型
     int                  i      --整型
     short/INT16          o      --短整型数
     float                f       --单精度浮点数
     double              d       --双精度浮点数
     HANDLE            h       --windows句柄 
     DWORD            dw     --字
     bool/BOOL           b       --布尔量,建议除了WINDOWS的API                                    函数常用BOOL外,其它均采用boolenum                e       --枚举
     char[]               sz       --字符串数组 
     其他数组            在变量的结尾加后缀s(小写,表示复数,多个之意)
  • 2 指针变量在1的基础上加前缀p。

  • 3 类对象变量: 一般类对象变量在1的基础上加前缀c 常见MFC对象前缀命名如下

     CString        s
     CEdit          cEdt
     CButton        cBtn
     CStatic         CStatic若显示字符串用m_cLab,若显示图像用m_cImg
     CListBox       cLbx
     CList          cLst
     CComboBox    cCmb
     CDialog        dlg 
     CPen          cPen
     ........
  • 4 临时变量可加前缀tmp。

  • 5 成员变量的命名在1,2,3的基础上加前缀m_。

  • 6 全局变量的命名在1,2,3的基础上加前缀g_。

  • 7 资源命名全部采用大写,其中类型不简写,含义段若开发文档已经给予了对应的变量名,可将变量名大写作为其含义段,例如: IDC_CHECK_NOSHOWCONSAMEBEAMSTEEL,前面的IDC_CHECK是控件放在窗口上时就缺省有的,后跟‘_’+NOSHOWCONSAMEBEAMSTEEL,是对应变量m_bNoShowConSameBeamSteel的‘NoShowConSameBeamSteel’(选中字符串,按Ctrl+Shift+U可转换为大写)。资源名不能随意命名,或者采用缺省的IDC_CHECK1,IDC_CHECK2等等。注意我们的程序是要见名知意,一切无意义的表达都应尽量避免。 常用的资源类型有:

        按钮    IDC_BUTTON_
        复选框  IDC_CHECK_
        单选框  IDC_RADIO_
        下拉框  IDC_ COMBO_
        列表框  IDC_LIST_
        编辑框  IDC_EDIT_
        对话框  IDD_DIALOG_
        菜单    IDR_MENU_
        右键菜单IDR_POP_MENU_
        工具条  IDR_TOOLBAR_  
  • 8 函数的命名: 函数的命名也采用匈牙利命名法,即以一连串首字母大写的单词表述变量的意义。此时不需要在前面加前缀去表明函数的返回值!例如, IsSame()函数,就没有必要写成 bIsSame(),因为IsSame已经明确表达了是和否的概念,因此理应返回一个bool 量。不重复地表达同一个概念,应该也成为命名的一个原则。 函数的形参定义放在后面描述。 事件函数的命名,一般以MFC自动生成的代码为准。

  • 9 类的命名 类以C开头,同样采用匈牙利命名法;如果是子类,可采用前缀的方法去表明:例如CSection是截面类,CRectSect是矩形截面类

编码的行文风格

编码就像写文章,要自然段清晰。每一件事为一段,中间以空行分开。每一段若不清楚可加若干注释。但要注意,代码的最高境界是无需注释(代码应该成为你的第二语言,而不需要加翻译),因此首要是代码清晰、简明,其次才是加注释。

如何使代码清晰?需要遵循一定的原则。

1

代码应该排版,层次清晰,例如有了‘{’后应该换行缩进。如果代码已经乱了,可选择要排版的自然段,按Ctrl+K,Ctrl+F自动排版(VC6中按Alt+F8); 有时会发现排版不起作用,例如一下情况:

        ACRX_DXF_DEFINE_MEMBERS (
			CAColZhongSteel, AcDbObject,
			AcDb::kDHL_CURRENT, AcDb::kMReleaseCurrent, 
			AcDbProxyEntity::kNoOperation, GBACOL,
			 "GSPLOTAPP"
			 "|Product Desc:     XXXX"
			 "|Company:          XXXX"
			 "|WEB Address:      www.xxxx.com.cn"
			 )
上述是一段宏,注意这个宏结尾是不需加;,因为宏展开以后的代码是有;号结尾的。但是排版程序不能识别宏的语句,导致排版后,宏的下一行在宏的结尾处对齐。处理这种情况,可以在这个宏结尾加一个;,这不会引起语法错误,实际上就是加了一句空语句,在RELEASE版本中,编译器会优化掉,也不会有性能的损失。
又例如,goto语句(虽然不提倡用)转到的标签行,应该在标签行后另起一行写代码,就能得到正确的排版。
.......

2

代码的缩进层次,一般来说不应超过3层。当缩进层次多了以后,不移动水平滚动条就无法看到代码,十分不便。实际上这种多层缩进是可以解开的。例如下面代码:

if(OldFile.Open(m_szFileName, CFile::modeRead|CFile::typeText|CFile::shareDenyNone, NULL) )
{
   ...........
}

实际上可以写成

if(!OldFile.Open(m_szFileName, CFile::modeRead|CFile::typeText|CFile::shareDenyNone, NULL) )
{
	AfxMessageBox("ChangTimeTemp open error!");
	return ;
}
.........

这样打开文件成功后的代码(一般肯定比错误处理代码更长)就可以不缩进了。

另外,如果缩进过多,某种程度上说明本函数干得过多,把应该另起一个函数的内容写在了本函数。即最内层的几层缩进可以用一个函数来代替。

3

创建对象要记得删除,可在写new代码的时候,就在相应的位置写delete,避免忘记。删除对象,同时对对象指针赋 NULL,除非能确定删除后指针马上就不用(例如析构函数里面),这是因为一个NULL的指针被非法访问总是能够方便地查找,一个非NULL的失效指针难以查找错误。

4

所有的变量在使用前应该初始化。使用未初始化的代码会带来难以检查的错误。成员变量在构造函数中初始化,局部变量在定义时初始化。注意我说的定义时初始化的意思:

int iData=1;------合理
int iData;
iData=1;  -------不合理

上述表达中,第一种情况,程序会在定义的时候就置初值;第二种情况,是先定义,然后赋值。赋值语句的代价要高于置初值的代价。 若对数组清零初始化,应该用内存拷贝函数memset一次赋值,而不应用循环。

5

局部变量应该就近初始化,即在第一次使用的地方初始化。这样做的好处是强化局部变量的临时意义,有助于代码重构。----当选择一段代码提取函数的时候,这些变量就在此段使用,不会产生多余的函数形参。同时不同意义的局部变量不能使用同一个变量名,一量多义会阻碍代码的理解。不要怕多了几个局部变量会造成性能的损失,好的编译器会优化掉这些多余开销。

6

循环变量一般定义为 int i , j , k ,大家一看就明白。注意有的代码会用 INT16 i,对于循环变量,不建议使用。因为32/64位程序的常用寄存器就是32位的(int 量),编译器出于指令对齐的原因(速度快),采用16位量循环反而会慢。

7

当使用多个if...elseif语句或者使用Switch语句时,不要使用下例中的1,2,3......而应该使用枚举enum 来定义,或者使用#define来定义。

     if (iType==0).....
     else if (iType==1).....

因为1,2,3这种定义不能见名知意,实际上不方便阅读。可以在头文件中先定义枚举类型:

     enum{GS_RECTSECTION,//矩形截面
          GS_ISECTION//I形截面
          };

然后将前述语句改为

     if (iType==GS_RECTSECTION).....
     else if (iType==GS_ISECTION).....

当然,当这种条件语句中的执行内容很多时,就要考虑是否需要使用抽象类和子类,用虚函数来代替Switch语句。

8

合理、积极地在形参定义中使用const。

     void CParaDWin::DrawOnULeft(  CDC *DC,const TCoord3D& pos,const TCHAR* FanText,const float h,float &l)

上面的函数定义中使用了3个const,分别说明: 第一个const 表明这是个引用传递,首先注意,除非是基本变量类型,都应该使用引用或者指针传递(引用和指针本质是一回事),因为引用传递的是一个地址,只需要拷贝一个32位数(地址的值),若不使用引用,调用函数时将会发生值传递,即拷贝一个对象,这个开销有时是相当大的。然而,引用有个缺点就是函数的调用者和函数的编写者都能修改。那么对于调用者,不能确定当使用了本函数后被调用的值会不会修改(有时他并不想修改)。如果使用const修饰,则调用者知道本函数是不会修改被调用的值的。另外,对于函数的编写者,一开始就确定了const,则会约束编写者自己的行为,预防不经意中去修改了形参(编译器会报错)。 第二个const 传递的是一个字符串常量指针,这里有个很大很方便的好处:调用者可直接使用字符串常量来作为形参,而不需要预先定义一个变量(下面调用的标红线处):

DrawOnULeft(DC,pos,_T(”左左”),3.3,l);

第三个const传递的是一个基本变量类型,由于float量也是一个4字节数,传递一个4字节数和传递一个4字节指针效果相同,因此没有必要写引用。const float h和float h的效果相同(但是还是建议写,这样能明确知道不可修改)

9

经常使用标准库中给予的几种容器。例如使用 vector来代替自己定义的动态数组。标准库是C++标准委员会推荐使用的,也是C++创始人极力推行的一套库代码,不应该拒绝使用这么好的工具。也不用担心他的将来的稳定性问题。目前C++的新标准为11版标准,新的c++标准库的性能比旧的更好,也增加了相当多有用的工具,例如,我认为正则表达式是很有用的,特别是当我们提供搜索的灵活性的时候。但是,正由于它博大精深,我们应该逐步使用,而不宜大规模推进(目前只推荐将容器当数组、当链表、当树使用)。

10

拒绝使用goto语句(虽然我们的老代码中有),在每一个语言教材中都不可避免地提出不使用goto语句。其意义我相信不用解释了。要相信一切goto语句都有办法去替代。仔细设计你的代码逻辑,就应该能避免goto。

11

函数体不应过大,过大的函数体很难被看懂。过大的函数体往往可以从中抽象出一些函数来描述其中的某一部分的功能。过大的函数体在初学者中是一个常见的毛病(一个函数几千行)。在linux中,大多数的函数的代码在100~200行以内。因此要相信自己能将函数组织地不过于庞大,如果自己不能控制,则尽量使一个函数的功能单一(要总是注意函数的功能和函数名的描述能不能一致)。一个功能一个函数是推行自动化测试的基础。

12

头文件的包含顺序应该是从“最特殊到最一般”;头文件上应该设置警卫,防止头文件被多次包含。

推荐两本参考书供大家参考:《The Effective C++》和《More Effective C++》,网上均有中文的PDF的电子版下载。

规范化注释

如何保证代码和文档的一致性一直是一件困难的事情。主要原因是当程序员花费大量时间在保证代码和文档的一致性时,不免就会提出疑问:为什么要提供文档。文档是给谁看的。

那么,至少有两点是我们需要有文档的原因:其一,当代码相当多的时候,理解代码的组织就成了一个问题。我们可以看到,当我们了解MFC库的时候,我们并没有去把它的源代码都阅读一遍(虽然阅读源代码是获益良多的),我们通过帮助文件去了解MFC库,了解有哪些类,类的相互关系,类函数有哪些,哪些是可以调用的外部函数。那么,如果我们的注释能够自动化生成这些帮助文件,那就解决了大问题;其二,当我们需要开放一些API的时候,就可以将这些帮助文件发布出去,而不需要另写一个帮助文件。最重要的是,每当我们修改函数形参,同时修改注释的时候,只需要重新生成一次帮助文件就行了。

现在有这样的帮助文件自动生成工具---Doxygen。我们采用JavaDOC书写风格。详细的编写规定可参考本章结尾的参考文档。

先简单说明注释的编写格式:下面第一种是块注释形式,第二种是行注释形式。注意和普通C++注释的不同,块注释每行多了一个’*’,结尾为‘.’--注意是英文的句号。行注释每行多了一个前缀’/’,结尾为‘.’。Doxygen程序正是通过这些标识去提取文档的。

/** 简要说明.
 *  详细说明.
 */

/// 简要说明.
/// 详细说明.

1

文件的开头应该有版权信息,规格如下:

/**  
* @file AColZhongSteel.h.
* @brief 本文件为暗柱钢筋类的头文件.
* @Copyright 软件有限公司.
* @version 1.0 .
* @data 2010/1/1.
*/ 

上式中,以@开头的一些单词是Doxygen的一些保留字,这些保留字是帮助文档的生成的。 无论是cpp还是h文件,都应该有此版权信息头

2 类注释

/** 
 * @class CTest
 * @ingroup testgroup
 * @brief test class
 *
 * @details test details class
 */
class CTest{
};

///@brief 本类用于暗柱纵筋的存储和显示
///
///@details 该类应该在TGBACol中被调用
///
class CAColZhongSteel : public AcDbObject{

3 函数注释

///@brief 设置定位点
///
///@param[in] const TCoord& pos:二维点坐标
///@ see TCoord
	void SetPos(const TCoord& pos);

上式中,参数描述@param[in]中的[in]表示是输入变量,而[out]表示输出变量。

/**
 * a normal member taking two arguments and returning an integer value.
 * @param[in] a an integer argument.
 * @param[out] s a constant character pointer.
 * @see  testMe2()
 * @return The test results
 */

上式中,@return描述返回值,@see表示参见testMe2函数。

4 变量注释

一般只注释成员变量,私有变量不应公开。

有三种形式,示例如下:

  • a)
/**
 * test var 
 */
double m_dTest;
  • b)
double m_dTest; ///< test var
  • c)
double m_dTest; /**< test var */

以上2)3)4)条只注释头文件的声明部分,cpp中实现部分不再注释,一方面是为减少注释量,另一方面如果想查看变量的意思,选择函数体右键很容易就能定位到头文件相应位置。注释应当适当、够用。重复注释增加维护量,而过时的注释反而会对代码的理解造成障碍。

参考资料:doxygen_manual-1.5.9.pdf,在网上也可以搜寻到一些大公司的源代码作为范本(例如在code.google.com中搜寻 )

格式

行长度

一行长度一般不超过80个字符,特殊不超过132字符,特殊计算公式等不受限制

函数定义

返回值必须与函数名一行,变量过多可以分行写

括号必须与函数名在同一行,并且中间没有空格,右括号紧跟最后一个参数

花括号另起一行

ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) 
{
  DoSomething();
  ...
}
ReturnType ClassName::ReallyLongFunctionName(Type par_name1,
                                             Type par_name2,
                                             Type par_name3) 
{
  DoSomething();
  ...
}

函数调用

尽量放在同一行,否则,将实参封装在圆括号中。

bool retval = DoSomething(argument1, argument2, argument3);

如果同一行放不下,可断为多行,后面每一行都和第一个实参对齐,左圆括号后和右圆括号前不要留空格

bool retval = DoSomething(averyveryveryverylongargument1,
                          argument2, argument3);

函数参数比较多,可以出于可读性的考虑每行只放一个参数

bool retval = DoSomething(argument1,
                          argument2,
                          argument3,
                          argument4);

条件语句

不在圆括号中添加空格,关键字else另起一行。

if (condition)   // 没有空格
{
  ...  // 缩进
}
else //else另起一行
{  
  ...
}

有些条件语句写在同一行以增强可读性,只有当语句简单并且没有使用else子句时使用

if (x == kFoo) return new Foo();

分行写的一定用大括号,即使只有一句。

if (condition) 
{
  DoSomething(); 
}

指针和引用表达式

句点(.)或箭头(->)前后不要有空格,指针/地址操作符(*、&)后不要有空格。

x = *p;
p = &x;
x = r.y;
x = r->y;

声明指针变量或参数时,星号与类型或变量名紧挨。

char *c;
const string &str;

编写ObjectARX代码的一些规范:

  • 1 打开数据库对象之后一定记得关闭,不关闭下次就打不开或者报错;

  • 2 创建的实体对象(new)在被添加进数据库后要使用close()关闭,而未添加进数据库的要delete;

  • 3 创建的(new)缓冲区resbuf在使用结束后要用acutRelRb()释放;

  • 4 得到ads_name之后注意使用acedSSFree()释放;

  • 5 使用Arx提供的全局函数时注意判断返回值是否符合要求,例如acdbGetObjectId()注意判断返回值是否等于Acad::eOk;一般在循环中不满足可以continue,这样可以减少层次关系;

  • 6 有的Arx函数返回的字符串需要手动释放资源,有的不用,需要仔细区分。例如获得图层的函数pEnt->layer(),获得的字符串应该delete。