[27] 编码规范
(Part of C++ FAQ Lite, Copyright © 1991-2001, Marshall Cline, cline@parashift.com)

简体中文版翻译:Alex


FAQs in section [18]:


[27.1] 有哪些好的C++编码规范?

很高兴你在这里找答案,而不仅仅是试图建立自己的编码规范。

但要注意在comp.lang.c++上有一些人对这个话题非常敏感。几乎所有的软件工程师在某个时候,都曾经被一些人利用过,这些人把编码规范当作是一种“权力游戏”。另外,有些不知道自己在说些什么的人也设定了一些C++编码规范,因此当这些标准的制定者实际写代码时,标准往往就成了只是用来观赏的东西。这些情况促使人们不相信编码规范。

很明显,问这个问题的人们是想要得到训练,因此他们并逃避他们在知识上的欠缺。但虽然如此,在comp.lang.c++上发帖问这个问题常常会导致争吵,而不是解决方案。

Sutter和Alexandrescu对这个问题有本非常好的书叫"C++ Coding Standards"(220页, Addison-Wesley出版,2005,ISBN 0-321-11358-6)。里面提供了101条规则、指导和最佳时间。作者和编辑们提供了一些确切的材料,并且对结对审查团队很有帮助。所有这些都使得此书更有价值。值得购买。

TopBottomPrevious sectionNext section ]


[27.2] 编码规范是必需的吗?有它就够了么?

编码规范不会把一个不懂OO的程序员变得懂OO了,这需要培训和经验。编码规范的好处是,当大型机构协调不同群体的程序员时,有助于降低分化的产生。

但仅有编码规范是不够的。编码规范减少了新人的自由度,使他们不用操心一些事情,这是好的。但仅有编码规范是不够的,还需要更多实用的指南。机构需要一种一致的设计和实现的哲学。例如,是使用强类型还是弱类型?在接口中使用引用还是指针?用stream I/O还是stdio?C++代码能够调用C代码么?反过来可以么?抽象基类应该怎么使用?继承是应该采用实现方法还是特化方法?应采用何种测试策略和审查策略?接口应该统一为每个数据成员提供get()和/或set()么?接口应该是从外向内设计还是从内向外设计?错误是应该用try/catch/throw处理还是用错误码?等等。

需要有一份有关详细设计的“伪标准”。我推荐一种三头并进的方法来达到这种标准化程度:培训、指导和库。培训能够提供“强化的指示”,指导能够让OO在实际中得到应用而不仅仅是教过就没事了。而高质量的C++类库则是一种“长期的指示”。针对这三种“训练”的商业市场正不断扩大。经历过这些困难的机构给出的意见非常统一:购买现成的,不要试图构建自己的。购买库、培训、工具和咨询。如果一个公司试图提供自足的工具,同时又制作应用程序或系统,那么它将发现很难取得成功。

很少人会认为编码规范是“理想的”,甚至都算不上“良好”。但在上述的机构中,编码规范又是必要的。

以下条目给出了一些基本的约定和风格方面的指南。

TopBottomPrevious sectionNext section ]


[27.3] 我们机构应该根据以前用C的经验来制定编码规范么?

不要!

不管你用C的经验多么丰富,不管你对C掌握的多么熟练,一名好的C程序员不一定就会是一名好的C++程序员。从C转换到C++,并不只是学习一下C++中++部分的语法和语义。那些希望获得OO好处的机构,如果不在“OO编程”中真正运用“OO”技术,那么他们就是在自欺欺人;他们的蠢行最终会在财报上表现出来。

应该由C++专家来打造C++的编码规范。开始可以在comp.lang.c++上问问题。寻找能够帮助你避开陷阱的专家。购买程序库,然后看“好”的库能否通过你的编码规范。在获得足够的C++经验之前,不要自己制定编码规范。没有标准要好过一份糟糕的标准,因为不合适的“官方”标准会让错误的做法一直存在。现在C++培训和程序库的市场正不断壮大,可以从中吸取经验。

还有:只要对某件事物有需求,就会增加出现伪专家的机会。要三思而后行。同时还要从过去的公司中寻求反馈,因为有娴熟技术的人未必是一名好的沟通者。最后,选一名会教学生的专业人,注意这可不是有足够语言/编程范式相关知识的全职教师。

TopBottomPrevious sectionNext section ]


[27.4] <xxx>和<xxx.h>这两种头文件有何不同?

ISO C++标准的头文件不包含.h后缀。标准委员会修改了以前的做法。C的头文件和C++头文件在细节上不同。

C++标准库保证包含来自C语言的那18个标准头文件。这些头文件有两种标准风格:<cxxx>和<xxx.h>(这里xxx是头文件的基本文件名,例如stdio,stdlib等等)。这两种风格的头文件是一样的,但有一点不同:<cxxx>风格的头文件将所有声明放在std名字空间中,而<xxx.h>除了将声明放在std名字空间,同时还放在了全局名字空间。委员会这么做是为了已有的C代码能够被C++编译器编译。但<xxx.h>是已经过时了,虽然现在仍被标准接受,但可能在以后的标准中不再支持。(参见ISO C++标准的D.5节)。

C++标准库还增加了32个C里面没有直接对应的标准头文件,例如, 。你可能会在老的代码中看到#include <iostream.h>这种写法,因此一些编译器厂商还提供这些.h版本的头文件。但要注意,.h版本的头文件可能与标准版本不一样。如果一个程序里有些用<iostream>,有些用<iostream.h>,那这个程序可能无法正常运行。

在新项目中,应当用<xxx>,而不该用<xxx.h>。

当修改或扩展使用旧式头文件名的代码时,最好还是遵从那些代码的做法,除非有很重要的理由换用标准头文件(例如标准<iostream>提供了一些功能,而厂商的<iostream.h>里没有)。如果想要使以后代码符合标准,那么要确保在所有被链接起来的代码中包括外部库,里面所用的所有C++头文件都修改了。

以上内容只对标准头文件有影响。你自己的头文件可以随便怎么命名,参见[27.9]

TopBottomPrevious sectionNext section ]


[27.5] 我应该在我的代码中使用using namespace std么?

可能不该。

人们不喜欢一边又一遍地键入std::。他们发现using namespace std能够使编译器看到任何std中的名字,即使名字前没有被std限定也能看到。问题是这会使编译器看到所有在std中的名字,包括那些你没想到的名字。换句话说,这可能导致名字冲突和二义性。

例如,假设你的代码需要计数,然后你定义了一个名为count的变量或函数。但std库也使用count这个名字(这是一个std算法),这就可能导致二义性。

你看,名字空间的用处就是用来防止两部分独立开发的代码产生名字冲突。using指令(描述using namespace XYZ的术语)实际上是把一个名字空间的内容全部引入到另外一个名字空间了,这就违背了名字空间的本意。using指令是为了使遗留的C++代码容易迁移到名字空间上来,但至少在新的C++代码中,不该大范围这么做。

如果真的不想敲std::,可以使用using声明,或使自己适应std::(不是办法的办法)

我个人觉得与其为每个不同的std名字决定是否使用using声明、并且找到最适合放置这个声明的地方,不如直接敲"std::",这样还更快。但这两种方法都不错。记住你是一个团队的一分子,所以要确保使用的方法和其它人保持一致。

TopBottomPrevious sectionNext section ]


[27.6] ?:操作符能够写出难以阅读的代码,它是邪恶的么?

不是。但和往常一样,记住可读性是最重要的事情之一。

有人觉得应避免使用?:运算符,因为和if语句相比,它有时会令人困惑。在很多情况下,?:常会使代码更难读懂(因此应该替换为if语句)。但有时用?:更清晰,因为这会强调到底在干什么事情,而不是强调那里有个if。

让我们先来看一个很简单的例子。假设你需要打印出一个函数调用的结果。这时你应该把真正的目的(打印结果)放在开头,然后把函数调用放在后面,因为函数调用是相对次要的(直觉上大多数开发者认为一行开头的内容是最重要的)。

 // 更好(强调主要目的-打印):
 std::cout << funct();
 
 
// 不那么好(强调次要目的-函数调用):
 functAndPrintOn(std::cout);

现在我们把这个观点扩展到?:上来。假设你的真正目的是要打印一些东西,但需要做一些额外操作来决定打印的内容。因为在概念上打印是更重要的事,所以我们倾向于把它放在开头,而把用于判断的逻辑放在后面。在下面的例子中,变量n代表消息发送者的数量;消息本身被打印到std::cout上:

 int n = /*...*/;   // 发送者的数量
 
 
// 更好(强调主要目的-打印):
 std::cout << "Please get back to " << (n==1 ? "me" : "us") << " soon!\n";
 
 
// 不那么好(强调次要目的-函数调用):
 std::cout << "Please get back to ";
 if (n == 1)
   std::cout << "me";
 else
   std::cout << "us";
 std::cout << " soon!\n";

已经说过了,通过不同组合使用?:、&&和||等运算符,可以写出很过分且难以阅读的代码(“只写代码”)。例如

 // 更好(意思很明显):
 if (f())
   g();
 
 
// 不太好(更难理解):
 f() && g();

我个人觉得这里明确写出if会更清晰,因为这强调主要的事情(至于要做什么是根据f()的结果来决定的),而不是强调次要的事情(调用f())。换句话说,在这里使用if是恰当的,原因和上面用if是不恰当的一样:我们希望把主要事情放在在显眼位置,次要事情放到次要位置。

不管怎样,别忘了可读性是最终目的(至少是目的之一)。你的目标应是为了避免类似?:、||、if或者甚至是goto这样的语法结构。如果你变成一个“唯标准论者”,那么你最终会另自己蒙羞,因为任何基于语法的规则总是存在反例。如果你是强调大的目标和指导原则(例如“主要的事情要放在显眼位置”,或者“把重要事情放在开头”,甚至是“让你的代码含义明显容易阅读”),那就好多了。

写出来的代码是要被其它人读的,不是给编译器看的。

TopBottomPrevious sectionNext section ]


[27.7] 我应该把变量声明放在函数体中间还是开头?

在第一次使用的附近声明。

对象是在声明时被初始化(构造)的。如果在函数中间才有足够的信息来初始化一个对象,那就应该把声明放在中间,以便对象可以正确初始化。不要现在开头给对象初始化为一个“空值”,然后在其它地方给它实际“赋值”。这么做是为了运行时的效率。与其先把对象构造到一个错误状态,然后再修正,不如开始就把对象构造正确,这样速度更快。简单的例子表明对像String这样简单的类,也会有350%的速度差别。具体的数值可能有所不同,而且整个系统的效率损失肯定小于350%,但的确会有效率损失。不必要的效率损失。

对这个问题一个常见的回应是:“我们要为对象中的每个数据提供一个set()成员函数,这样构造对象的代价就被平摊开了。”这就不仅是损失效率了,因为还导致维护困难。为每个数据成员提供set()函数和public数据一样糟糕:你把实现技术暴露给外部了。你唯一隐藏掉的是成员对象的物理名字,而具体的实现细节(比方说用了一个List、一个String和一个float),则还是给外面知道了。

底线是:局部变量应在靠近第一次使用的地方声明。对C语言专家来说可能不太习惯,但新事物不一定就不好。

TopBottomPrevious sectionNext section ]


[27.8] 哪种源代码文件名最好?foo.cpp?foo.C?foo.cc?

如果已经有一种命名约定了,那就继续使用。否则,需要查一下所使用的编译器接受哪种文件名。常用的是:.cpp, .C, .cc或.cxx(当然如果用.C的话,那么文件系统需要能够区分大小写,以便不会混淆.C和.c)。

我们已经使用.cpp做为C++源文件名的后缀了,我们也用.C。如果用.C,那么在把代码移植到大小写不敏感的文件系统时,需要告诉编译器将.c文件做为C++源文件对待(例如IBM CSet++是用-Tdp选项,Zortech C++编译器用-cpp,Borland C++编译器用-P)。

关键在于这些文件扩展名中,并不存在说哪个比其它更好。我们通常根据客户的要求来选择(这些问题应当依据商业上的考量,而不是技术)。

TopBottomPrevious sectionNext section ]


[27.9] 哪种头文件名最好?foo.H?foo.hh?foo.hpp?

如果已经有一种命名约定了,那就继续使用。如果还没有,而且不需要让编辑器区分C和C++文件,那就用.h好了。否则,就按编辑器的要求来,例如.H、lhh或.hpp。

我们倾向于使用.h或.hpp做为C++头文件的后缀名。

TopBottomPrevious sectionNext section ]


[27.10] C++有没有一些像lint一样的规范原则?

有的。有些做法一般被认为是危险的。但没有一个是总是“不好”的,因为最糟糕的做法有时也有用武之地。

[译注1]: 这里“构造性”的指二元运算符的结果是一个新构造的对象。

TopBottomPrevious sectionNext section ]


[27.11] 为何人们对指针转换和/或引用转换如此担忧?

因为它们是邪恶的!(这说明在使用它们时需要很小心谨慎)。

不知为什么,程序员在转换指针时不太注意。他们到处转换指针类型,然后还奇怪为什么会出问题。最糟糕的是,当编译器给出一条错误消息时,他们就添加一个类型转换“让编译器闭嘴”,然后他们再“测试一下”看能否运行。如果你做了很多指针或引用的类型转换,请继续往下读。

当你转换指针类型和/或引用类型时,编译器通常不会产生任何信息。指针类型转换(和引用类型转换)会使编译器保持沉默。我把他们当作是一种错误信息的过滤器:编译器想要抱怨,因为它发现你正在做蠢事,但同时也发现它不该抱怨因为你用了类型转换,所以编译器就把错误消息丢掉了。这就像用密封胶带封住编译器的嘴:它试图告诉你一些重要事情,而你却故意让它闭嘴。

指针类型转换告诉编译器:“别想了,赶紧生成代码;我很聪明,你太笨了;我很伟大,你很渺小;我知道我在做什么,所以就假装这是汇编语言,然后生成代码吧。”当你转换类型时,编译器就盲目地生成代码-由你来控制(和负责)生成的结果。编译器和语言会缩减(甚至是消除)你所能得到的保证。你只能靠自己了。

做个类比,虽然手抛链锯玩完全合法,但这么做却很蠢。如果出了问题,别向链锯制造商抱怨-你做了他们没有保证的事情。你只能靠自己。

为公平起见,语言的确在类型转换时做了一些保证,至少是在一个有限的子集内有保证。例如,语言保证当从对象指针(指向一块数据的指针,不是指向函数,也不是指向成员)转换到void*,并且再转换数据类型时,是没有问题的。但很多时候,你只能靠自己。)

TopBottomPrevious sectionNext section ]


[27.12]这两种标识符的名字:that_look_like_this和thatLookLikeThis,哪种更好?

这个要看以前是怎么做的。如果你有Pascal或Smalltalk背景,那么会喜欢youProbablySquashNamesTogether。如果有Ada背景,那么会喜欢You_Probably_Use_A_Large_Number_Of_Underscores. 如果有微软Windows背景,那么可能倾向于“匈牙利”命名法,即在标识符前面添加表示类型的前缀[译注1]。对于Unix C背景的人来说,会喜欢用缩写[译注2]。

所以没有普遍适用的标准。如果你在的项目团队已经有一份命名规范了,就照上面说的做。如果硬要推翻重来,可能更多会带来争吵而不是解决问题。从商业角度来看,只有两件事是重要的:代码可读性好,团队中的每个成员都使用相同风格。

除此之外,差别很小。

还有,在使用平台相关的代码时,不要用一种完全不同的风格。例如,一种编码风格在使用微软的库时可能看起来很自然,但在和UNIX库一起使用时就会看起来很奇异。别这么做。为不同的平台使用不同的风格。(为避免有人不仔细看,别给我发email询问那些要移植到(或是用在)不同平台上的通用代码,因为这些代码不是平台相关的,所以刚才说的“为不同的平台使用不同的风格”在这里并不适用。)

好吧,还有。真的。别跟自动生成的代码(例如通过工具产生的代码)过不去。一些人对编码规范抱有一种宗教般的狂热,他们试图让工具产生的代码符合他们的风格。别这么做,即使工具产生的代码风格不同,也别管它。记住钱和时间才重要?!?整个编码规范目的就是为了省钱省时间。别把这个变成烧钱的陷阱。

[译注1]:原文是jkuidsPrefix vndskaIdentifiers ncqWith ksldjfTheir nmdsadType

[译注2]: 原文是abbr evthng n use vry srt idntfr nms. (AND THE FORTRN PRGMRS LIMIT EVRYTH TO SIX LETTRS.)

TopBottomPrevious sectionNext section ]


[27.13]从哪里可以找到一些编码规范么?

有好几个地方可以找到。

在我看来,Sutter和Alexandrescu的"C++ Coding Standards"(220页,Addison-Wesley出版,2005, ISBN 0-321-11358-6)是最好的。 我有理由推荐此书,并且本书作者很能激发推荐者的热情。所有人都大力推荐,以前我可没见过这事。

这里有一些编码规范,可以以此为起点来制定机构的编码规范。(列表顺序是随机的)(有些已经过时了,有些可能非常糟糕。我不会推荐任何一种。使用者自己注意。)

注意:

TopBottomPrevious sectionNext section ]


[27.14] 我应该用“不常见”的语法么?

只有当有足够的理由时再去用。换句话说,就是通过“普通”的语法无法获得同样的结果。

决定软件方面决策的是钱。除非你是在象牙塔中,否则,当你的做法会增加费用、增加风险、增加时间,或者是在一个受限环境中增加产品的时空开销,那么你的做法就不好。在意识中,你应该把这些都转换为钞票。

根据这种以实用为目的、以钞票为导向的观点,只要有等价的“正常”语法,程序员就应避免实用非主流的语法。如果一个程序员写下隐晦的代码,其它程序员看了会困惑,这就会耗费金钱。其它程序员可能会引入bug(会花钱),可能会需要更长的时间来维护(钱),修改起来可能会很困难(错过了市场机遇等于损失了钱),可能在优化时更困难(在受限的环境中,有人会需要为更大内存、更快CPU和/或更大电池来买单),另外客户可能还不满意(钱)。这是一种有关风险和回报的权衡。但如果有等价的“正常”语法能够达到同样目的,那么再努力降低使用“非正常”语法所带来的风险,就没有任何“回报”。

例如,在混乱C代码大赛中使用的技术,礼貌来讲是不正常的。没错,其中很多是合法的,但不是所有合法的事情都合理。使用奇怪的技巧会使其它程序员感到困惑。一些程序员喜欢“秀”他们挑战极限的能力,但这是把自我的虚荣心放在了比钱更重要的位置,是不专业的表现。坦白说,任何这么干的人都该被开除。(如果你觉得我太“刻薄”或“残忍”,我建议你调整一下态度。记住:公司雇你来是为了来帮助它而不是来伤害它的。那些把自我放到公司最佳利益上的人应该被开除出去)。

举个非主流语法的例子,?:运算符一般不作为语句来用。(一些人甚至不喜欢把它用在表达式里。但必须承认有很多地方用到了?:,所以不管喜欢不喜欢,(用作表达式)是“正常”的。这里有个把?:用作语句的例子:

 blah();
 blah();
 xyz() ? foo() : bar();  
// 应该用if/else
 blah();
 blah();

还有把||和&&当作"if-not"和"if"语句来用也是一样道理。是的,Perl里面有这些惯用法,但C++不是Perl,用这些来代替if语句(而不是用在表达式中)在C++中是“不正常”的。例如:

 foo() || bar();  // 应该用if (!foo()) bar();
 foo() && bar();  
// 应该用if (foo()) bar();

这里还有个例子,好像是能够运行,甚至是合法的,但绝不是正常的。

 void f(const& MyClass x)  // 应该用const MyClass& x
 {
   
...
 }

TopBottomPrevious sectionNext section ]


E-Mail E-mail the author
C++ FAQ LiteTable of contentsSubject indexAbout the author©Download your own copy ]
Revised Jan 2, 2009