分享

C++编程准则

 web127 2013-04-19

1、  文件的属性以及开始和结束注释标记

a)        在每一个文件的头部用注释填写:章节号:文件名,以及该文件所完成的功能。

b)       如何一个文件是头文件,则什么也不用做

c)       如果这个文件中的程序中不含main()(即,不含主程序的.cpp的文件),这必须与其他部分连接,所以,在注释行的文件名后添加:{O}

d)       如果,文件是主程序(含有main()函数),但是需要和其他部分连接,则有独立的//{L}开头的行,并在{L}后填写所要连接的文件名(不含主程序的.cpp文件的名字)

2、  圆括号,大括号缩排

a)        单行定义常用于内联函数

b)       函数以及类的缩排如下:

Fun(){

/*Bodyhere */:

}

3、  标示符命名

a)        如果标示符是一个类:则第一个字母必须大写

b)       如果标示符是一个函数名或者变量名:则第一个字母必须小写

c)       剩余部分由一个或多个单词构成每个单词第一个字母必须大写

d)       编译时常量(const,#define),标示符的所有字母都必须大写

4、  头文件的包含顺序

C++编程思想里面倡导用“最特殊到最一般的”的顺序,即,首先为本地文件,接着为

自己的工具文件,最后,库文件。由于函数调用的顺序是按照头文件的包含顺序的来的。从而,使得用户的函数不被系统的以及第三方的库文件的函数所冲掉。可以,让用户程序员清晰的指导自己定义的接口是否和第三方或者系统的库函数所冲突。

       另外,Google C++编程规范倡导的刚好相反:“从最一般到最特殊”,所以,能大量减少隐藏的头文件依赖。同时,在其中倡导应该首先包含首选头文件,即a.cpp文件中包含头文件时应该是首先包含a.h文件。

       综上,C++编程思想的编程风格虽有缺点,但是好处还是很多。

5、  在头文件中包含警卫

总是在头文件中包含警卫,从而,避免单个.cpp文件期间一个头文件被多次包含。如下:

//IncludeGuard.h

#ifndefINCLUDEGUARD_H

#defineINCLUDEGARD_H

//Bodyof header file here…

#endif//INCLUDEGARD_H

从中可以看出,警卫以头文件名为基础,将文件名的字母大写,然后用下划线代替’.’。

6、  使用名字空间

在头文件中,必须保证其中没有包含任何有“污染”的名字空间,即如果改变函数或者

类外面的名字空间,将导致包含刺头文件的所有文件的改变。所以,在C++中不允许在函数的声明外面有任何using声明,同时不允许在头文件中有全局的using声明。

7、  使用“前置条件”和后置条件

Require()和assure()都在require.h中,在“核心函数”开始之前,用检查前置条件来保证

“核心函数”执行所需要的条件都是否满足,而,核心函数执行完毕之后使用后置条件来检查核心函数是否已经正确执行,且返回正确的结果。

第十八章——编程准则

1.        首相让程序运行,然后在追求速度。即使这段程序非常重要,而且,是我们的瓶颈。不要优化,首先用尽可能简单的设计让程序可以运行,如果速度不满足要求再对其进行优化。我们总能够发现“我们的”瓶颈并不是问题的所在。节省时间做真正有意义的事。

2.        编写简洁优美的程序有很多潜在的好处。简洁优美的程序不仅易读,易调试,而且,易于理解和维护。这正是能带来经济利益的地方。最初看啦简洁优美的程序会使我们的效率变低,但是,等到我们的程序能够无缝地嵌入到系统时,我们发现一切都是值得的,特别当修改我们的程序时更将淋漓的体现。

3.        分而治之。如果,感到问题复杂,是猜测程序的最基本的操作,为最难的部分创造一个对象——书写代码并且应用这个对象,然后将这个最难的部分嵌入其他的对象,等等。

4.        不用C++重写C的代码,除非我们需要做功能上大的调整(能用即不重做)。但用C++编译器编译C代码却好处多,可以发现代码中潜在的问题。

5.        如果有许多C代码需要改变,首先隔离不需要修改的代码,最好将那些函数打包成“API”的静态成员函数。然后集中精力到要修改的代码,将它们精化成类以使以后的维护修改更容易。

6.        要区别类的创建者和类的使用者(客户程序员)。类的使用者不需要也不想知道类的内部是如何实现的。被创建的类可以被没有经验的程序员使用,而且要工作良好,库只是在透明的情况下才会容易使用。

7.        创建类时,要尽可能用有意义的类名。目标是创建一个尽可能简单接口,这可以通过函数重载和默认参数的方法实现。

8.        数据隐藏允许我们不破坏用户代码下随心所欲的修改代码。所以,我们把对象的成员尽可能的定义为private,而只让接口的部分为public,而且总是使用函数而不是数据。如果类的使用者不需要调用这个函数,那么让这个函数也成为private,如果类的一部分让派生类可见,就定义为protected,并且提供一个接口不是直接暴露数据,这样,实现部分的改变将对派生类产生最小的影响。

9.        不要陷入分析瘫痪中。有些东西只有在编程时才能学到并使各系统正常。C++有内建的防火墙,让它为我们服务,在类或一组类中的错误不会破坏整个系统的完整性。

10.     我们的分析和设计至少要在系统中创建类、它们的公共接口、它们与其他类的关系、特殊的基类。如果我们的方法产生的东西比这些更多,就应当问问自己,是不是所有的成分在程序设计的整个生命周期内都是有价值的,如果不是,将会增加我们的维护开销。许多设计方法并不大奏效,这是事实。

11.     首先写测试代码(在写类之前),并和代码一起提交,运用makefile或其他工具使运行测试自动化。这样在运行测试代码之前就可以自动校验改变,迅速发现错误。因为我们拥有检测错误的体系,所以,当发现需要修改代码时,会更大胆地尽心尝试。

12.     首先写测试代码(在写类之前)可以保证类设计的完整性。如果不懈测试代码,就不知道我们的类能做什么。另外,写测试代码的过程会使我们想到类中所需要的其他特性或约束条件——这些特性或约束条件通常在设计时是不易觉察的。

13.     软件工程的基本原则:所有的问题都可以通过引进一个额外的间接层来简化。这是抽象方法额基础,而抽象是面向对象编程的首要特征。

14.     尽可能地原子化类。也就是每一个类都有一个单一、清楚地目的。如果,我们设计的类或我们设计的系统过于复杂,就应该将所有复杂的类分解成多个简单的系统。

15.     注意较长的成员函数定义,长的复杂的函数难于维护,而且很可能这个函数自己做了太多的事。如果看到这样的一个函数,至少预示着应该分解为几个函数,甚至预示着应该创造一个新类。

16.        注意长的参数表,这样的函数调用会难写、难读、难于维护。应该把这个函数改成一个合适的类,用对象作为参数传递。

17.        不要自我重复。如果一段代码在派生类的许多函数中重复出现,就把这段代码放在基类的一个单一的函数中然后在派生类中调用它。这样既节省了代码空间,又使将来的修改容易传播。我们可以用内联函数来提高效率。有时我们会发现这种通用代码会为我们的接口添加有用的功能。

18.        注意switch和if-else语句。他们是典型的类型检查编码的指示符。意味着程序运行的情况和我们的类型信息有关。(实际的类型也许不是我们最初看起来的类型)我们通常可以将这些代码转换成继承或多态,多态会为我们进行类型检查,使程序可靠和已与扩展。

19. 从设计的角度,寻找并区分那些变化和不变的成分。也就是在系统中寻找那些修改时不需要重新设计的成分,把他们封装到一个类中。

20. 注意不同点。两个语义上不同的对象可能有同样的操作或反应,自然就会试着把一个作为另一个的子类以便利用继承性的好处。这就叫差异,但并没有充分的理由来强制这种并不存在的父子关系。一个好的解决办法是产生一个共同的父类:它包含两个子类——这可能要多占一点空间,但我们可以从继承中获益,并且可能对这种设计有重要发现。

21. 注意在继承过程中的限制。最清晰地设计是向被继承者加入新的功能,而如果在继承过程删除了原有功能,而不是加入新功能,那这个设计就值得怀疑了。但这也不是绝对的,如果我们正在与一个老的类库打交道,对已有的类在子类中进行限制可能更有效,而不必重建一套类层次来使我们的新类适应新的应用。

22. 不要用子类去扩展基类的功能。如果一个类接口部分很关键的话,应当把它放在基类中,而不是在继承中加入。如果我们正在用继承来添加成员函数,我们可能应该重新考虑我们的设计。

23. 一个类一开始时接口部分应尽可能小而精。在类使用过程中,我们会发现需要扩展类的接口。然而一个类一旦投入使用,我们要想减少接口部分,就会影响那些使用了该类的代码,但如果我们我们需要增加函数则不会有影响,一切正常,只需要重新编译一下即可。但即使用新的成员函数取代了原来的功能,也不要去改正原有接口(如果我们愿意的话,可以再低层将两个函数合并。)如果我们需要对一个已有的函数增加参数,我们可以让原来的参数保持不变,把所有新参数作为默认参数,这样不会妨碍对该函数已有的调用。

24. 大声朗读我们的类,确保他们是合理的。读基类时用“is-a”,读成员对象时用“has-a”。

25. 在决定是用继承还是用组合时,问问自己是不是需要向上类型转换到基类。如果不需要,就用组合(成员对象)而不用继承。这样可以减少多重继承的可能。如果我们选择继承,用户会认为他们被假设向上类型转换。

26. 有时我们为了访问基类中的protected成员而采用继承。这可能导致一个可察觉的对多重继承的需求。如果我们不需要向上类型转换,首先导出一个新类来完成保护成员的访问,然后把这个新类作为一个成员对象,放在需要用到它的所有对象中去。

27. 一个典型的基类仅仅是它的派生类的一个接口。当我们创建一个基类时,默认情况下让成员函数都成为纯虚函数。析构函数也可以是纯虚函数(强制派生类对它重新定义),但记住要给析构函数一个函数体,因为继承关系中所有的析构函数总是被调用。

28. 当我们在类中放一个虚函数时,让这个类的所有函数都成为虚函数,并在类中定义一个虚析构函数。只有当我们要求高效时,而且分析工具指出应该这样做,再把virtual关键字去掉。

29. 用数据成员表示值的变化,用虚函数表示行为的变化。如果我们发现一个类中有几个状态变量和几个成员函数,而成员函数在这些变量的作用下改变行为,我们可能要重新设计它,用子类和虚函数来区分这种不同的作用。

30. 如果我们必须做一些不可移植的事,对这种服务做一个抽象并将它定位在一个类的内部,这个额外的间接层可防止这种不可移植性影响我们的整个程序。

31. 尽量不用多重继承。这可帮助我们摆脱困境,尤其是修复我们无法控制的类的借口时。除非我们是一个经验相当丰富的程序员,否则不要在系统中设计多重继承。

32. 不要用私有继承。虽然C++中可以有私有继承,而且似乎在某些场合下很有用,但它和运行时类型识别一起使用时,常常引起语义的模棱两可。我们可以用一个私有成员对象来代替私有继承。

33. 如果两个类因为一些函数的关系(如容器和迭代器)而联系在一起,使一个类设为公有并将另一个类包含成友元。这不仅强调二者之间的联系,而且允许一个类的名字嵌入到另一个类中复用。标准C++通过在每个容器类中定义嵌入的迭代器类,为容器提供了通用接口。嵌入的另一个原因是可以作为私有运行的一部分。这里,嵌入比类之间的联系提供了更大的运行隐藏,而且防止出现上面提到的名字空间污染。

34. 运算符重载仅仅是“语法糖”:另一种函数调用方法。如果重载一个运算符不会使类的接口更清楚、更易于使用,就不要重载它。一个类只创建一个自动类型转换运算符。

35. 首先保证程序能运行,然后再考虑优化。特别是,不要急于写内联函数、使一些函数为非虚函数或者紧缩代码以提高效率。这些在我们开始构建系统时都不用考虑。我们开始的目标应该是证明设计的正确性,除非设计要求一定的效率。

36. 不要让编译器来为我们产生构造函数、析构函数或operator=。类的设计者应该明确地说出类应该做什么,并完全控制这个类。如果我们不想要拷贝构造函数或operator=,就把他们声明为私有的。记住,只要我们产生了任何构造函数,就防止了默认构造函数被生成。

37. 如果我们的类中包含指针,我们必须产生拷贝构造函数、operator=和析构函数,以使类运行正常。

38. 当为派生类写拷贝构造函数时,记住要显示调用基类的拷贝构造函数。如果不这样做,基类会调用默认构造函数,可能这不是我们所想要的情形。要调用基类拷贝构造函数,用以下方式将它传给派生类:Derived(const Derived& d):Base(d){//...

39. 当为派生类写赋值操作符时,记住显示调用基类版本。如果不如此,就不会起作用。应用基类的名字和作用域操作符,调用基类的赋值运算符:

      Derived&operator=(const Derived& d){ Base::operator=(d);

40. 为了减少大项目开发过程中的重复编译,应使用句柄类/Cheshire cat技术,只有需要提高运行效率时才把它去掉。

41. 避免用预处理器。可以用常量来代替值,用内联函数代替宏。

42. 保持范围尽可能地小,这样我们的对象的可见性和生命周期也就尽可能地小。这就减少了错用对象和隐藏难以发现的错误的可能性。

43. 避免使用全局变量。尽可能把数据放在类中。全局函数的存在可能性要比全局变量大。一个全局函数作为一个类的静态成员更合适。

44. 如果我们需要声明一个来自库中的类或函数,应该包含一个头文件的方法。而不要用一个不完全类型指定的方法,如:创建一个函数来写道ostream中:

Class ostream。

应该是:#include <iostream>

当我们创建我们自己的类的时候,在只需用到指针的情况下,如果一个库很大,应该给用户提供一个头文件的简写形式,文件中包含又不完全的类型说明(就是类型名声明),它可以提高编译器的速度。

45. 当选择重载运算符的返回值类型时,要考虑表达式连成一串时可能出现的情况:当定义operator=时,应记住x=x。要对左值返回一个拷贝或一个引用,这样才能用在串连表达式中。

46. 当写一个函数时,我们的第一选择是用const引用来传递参数。只要我们不需要修改正在被传递进入的对象,这种方式是最好的。因为它有着传值方式的简单,但不需要费时的构造和析构来产生局部对象,而这在传值方式时是不可避免的。通常我们在设计和构建我们的系统时不用注意效率问题,但养成这种习惯仍是件好事。

47. 当心临时变量。当调整效率时,要注意临时创建的对象,尤其是用运算符重载时。如果我们的构造函数和析构函数很复杂,创建和销毁临时对象就很费时。当从一个函数返回一个值时,总是应在return语句中调用析构函数来“就地”产生一个对象。return MyType(i,j);这优于 MyTypex(i,j); return x;

48. 当产生构造函数时,要考虑到异常情况,在最好的情况下,构造函数只是抛出异常,其次是:类只从健壮的类被组合和继承,所以当抛出异常时它们会自动清除它们所作的一切。如果我们必须使用裸指针,我们应该负责捕获自己的异常,然后在我们的构造函数抛出异常以前释放所有指针指向的资源。如果一个构造函数无法避免失败,最好的方法是抛出异常。

49. 在我们的构造函数中只做一些最必要的事情,这不仅使构造函数的调用有较低的时间花费,而且我们的构造函数更少地抛出异常和引用问题。

50. 析构函数的作用是释放在对象的整个生命期内分配的所有资源,而不仅仅是在创建期间。

51. 使用异常层次,最好从标准C++异常层次中继承,并作为公共类嵌入能抛出异常的类中。捕获异常的人然后可以确定异常的类型。如果我们加上新的派生异常,已存在的客户代码还是通过基类来捕获这个异常。

52. 用值来抛出异常,用引用来捕获异常。让异常处理机制处理内存管理。如果我们抛出一个指向在堆上产生的异常的指针,则捕获者必须破坏这个异常,这事一种不利的耦合。如果我们用值来捕获异常,我们需要额外的构造和析构,更糟的是,我们的异常对象的派生部分可能在以值向上类型转换时被切片。

53. 除非确有必要,否则不要写自己的类模板。先查看一个标准模板库,然后查问创建特殊工具的开发商。当我们熟悉了这些产品后,我们就可大大提高我们的生产效率。

54. 当创建模板时,留心那些带类型的代码并把它们放在非模板的基类中,以防不必要的代码膨胀。用继承或组合,我们可以产生自己的模板,模板中包含的大量代码都应是必要的,类型相关的。

55. 不要用<cstdio>函数,例如printf()。学会用输入输出流来代替,他们是安全和可扩展类型,而且功能也更强。我们在这上面花费的时间肯定不会白费。一般情况下都要尽可能用C++中的库而不要用C库。

56. 不要用C的内部数据类型,虽然C++为了向后兼容仍然支持他们,但他们不像C++的类那样强壮,所以这会增加我们查找错误的时间。

57. 无论何时,如果我们用一个内部数据类型作为一个全局或自动变量,在我们可以初始化他们之前不要定义他们。每一行定义一个变量,并同时对它初始化。当定义指针时,把"*"紧靠在类型的名字一边。如果我们每个变量占一行,我们就可以很安全地定义他们。

58. 保证在所有代码前面初始化。在构造函数初始化表中完成所有成员的初始化,甚至包括内部数据类型(也是使用伪构造函数)。在初始化子对象时用构造函数初始化表常常更有效;否则调用了默认构造函数,而不再调用使初始化正确的其他成员函数(如:operator=)。

59. 不要用“MyType a = b”的形式来定义一个对象。这是常常引起混乱的原因。因为它调用构造函数来代替operator=。为了清除起见,可以用“MyType a(b)”来代替。这个语句结果是一样的,但不会引起混乱。(虽然,这两种都是调用拷贝构造函数来完成新建对象的)

60. 使用C++显示类型转换。类型转换重载了正常的类型系统,它往往是潜在的错误点。通过把C中一个类型转换负责一切的情况改为多种表达清楚的转换,任何人来调试和维护这些代码时,都可以很容易发现这些最容易发生逻辑错误的地方。

61. 为了使一个程序更健壮,每个组件都必须是很强壮的。在我们创建的类中运用C++中提供的所有工具:隐藏实现、异常、常量更正(避免用预处理器,用常量代替值)、类型检查等等。用这些方法我们可以再构造系统时安全地转移到下一个抽象层次。

62. 建立常量更正。这允许编译器指出一些非常细微且难以发现的错误。这需要一定训练,而且要在类中协调使用,但这是完全值得的。

63. 充分利用编译器的错误检查功能,用完全警告方式编译我们的全部代码,修改我们代码,直到消除所有的警告为止。在我们的代码中宁可犯编译错误也不要犯运行错误(比如不要用变参数列表,这会使所有类型检查无效)。用assert来调试,对运行时错误要进行异常处理。

64. 宁可犯编译错误也不要犯运行错误。处理错误的代码离出错点越近越好。尽量就地处理错误而不要抛出异常。用最近的异常处理器处理所有的异常,这里它有足够的信息处理它们。在当前层次上处理我们能解决的异常,如果解决不了,重新抛出这个异常。

65. 如果一个析构函数调用了任何函数,这些函数都可能抛出异常。一个析构函数不能抛出异常(这会导致terminate()调用,它指出一个程序设计错误)。所以,任何调用了其他函数的析构函数都应该捕获和管理它自己的异常。

66. 不要自己创建私有数据成员名字“修饰”,除非我们有许多已在的全局值,否则让类和名字空间来为我们做这些事。

67. 注意重载,一个函数不应该用某一个参数的值来决定执行哪段代码,如果遇到这种情况,应该产生两个或多个重载函数来代替。

68. 把指针隐藏在容器中。只有当我们要对它们执行一个立即可以完成的操作时才把他们带出来。指针已成为出错的一大来源,当用new运算符时,应该试着把结果指针放在一个容器中。让容器拥有它的指针,这样它就会负责清楚它们。更好的办法是把指针打包进来类中。如果我们仍想让它看起来想一个指针,就重载operator->和operator*,如果我们必须有一个游离状态的指针,记住初始化它,最好是指向一个对象的地址,必要时让它等于0。当我们删除它时把它置为0,以防意外的多次删除。

69. 不要重载全局new和delete,可以在类的基础上去重载它们。重载全局new和delete会影响整个客户程序员的项目,有些事只能由项目的创建者来控制。档位类重载new和delete时,不要假定我们知道对象的大小,有些人可能是从我们的类中继承的。用提供的参数的方法。如果我们做任何特殊的事,要考虑到他可能对继承者产生的影响。

70. 防止对象切片。实际上以值向上类型转换到一个对象毫无意义。为了防止这样,在我们的基类中放入一些纯虚函数。

 71、如果我们用异常说明,用set_unexpected()函数安装我们自己的unexpected()函数。我们的unexpected()应该记录这个错误并重新抛出当前这个异常。这样的话,如果一个已存在的函数被重写并且开始引起异常时,我们可以获得记录,从而修改调用代码处理异常。

72、建立一个用户定义的terminate()函数(指出一个程序员的错误)来记录引起异常的错误,然后释放系统资源,并退出程序。

73、有时简单的集中会很管用。一个航空公司的“旅客舒适系统”有一系列相互五官的因素组成:座位、空调、电视等,而我们需要在一家飞机上创建许多这样的东西。我们要创建私有成员并建立一个全部的接口吗?不,在这种情况下组建本身也是公开接口的一部分,所以我们应该创建公共成员对象。这些对象有它们自己的私有实现,所以,也是很安全的。注意简单的集合不是常用的方法,但也会用到。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多