基本规则
概述
任何精美的艺术不仅来源于创造,而且来自于规范。编程也是如此。C++是易总大型语言,有充足的空间进行创造。但是,由于设计空间太大,以至于没有约束–也就是说没有设计结构上的一些适当的约束–大型项目很容易变的难以管理和维护。
- 设计规则:经验告诉我们,某些编程习惯虽然在C++中完全合法,但是决不能简单地用于大型项目中。检验是否遵守了这些规则的过程不能是一种主观过程。设计规则必须足够准确、详尽和良好的定义,以便可以客观的检验是否遵守了这些规则。为了效果更好,设计规则必须适合于进行非人为的、借助自动工具的机械验证。
- 指导方针:经验告诉我们,有一些习惯应尽可能的避免,这种具有更抽象特征性的建议规程称为指导方针,这样的规程有时候允许一些合法的例外。指导方针就像必须遵守的经验法则,除非是别的更紧迫的工程原因要求遵守其他方针
- 原则:有一些观察和事实在设计过程中经常被证明是有用的,但必须在特定设计的上下文中评估,这些观察和事实称为原则
成员数据访问
封装是一个术语,用于描述在过程接口后面隐藏实现细节的概念。类似的术语有信息隐藏和数据隐藏。直接访问一个类的数据成员违反封装原则。
主要设计规则:保持数据成员的私有性
例如:我们有个未封装的接口类,可以直接访问内部成员
class Rectangle
{
public:
Point lower_left_;
Point upper_right_:
...
}
如果我们修改了类内的成员,会导致之前直接访问成员的代码都需要重新编码。组件重用使得这个问题变的更加严重。
如果不能通过某个类的逻辑接口编程访问或检测到其包含的实现细节,则称这些实现细节被该类封装了
封装是面向对象设计的一个重要的工具。封装意味着我们将低层次的信息集合在了一起,使他们以一种紧密耦合的密切方式潜在地交互。而信息隐藏则用于限制外部世界与某些细节交互,这些细节与类要帮助实现的抽象无密切关系
使所有数据成员保持私有,并且提供适当的访问函数和操纵函数,可以着呢宫颈癌可维护性,保证对象的完整性。
全局命名空间
对于中等规模的项目来说,如果参与开发的不止一个人,当各自独立开发的部件集成到一起的时候,就会有命名冲突的危险。问题的严重性会随着系统规模的扩大而成指数级增长。若冲突是由于第三方提供的集成软件引起的,则情况会更加恶化。
全局数据
避免在文件作用域内包含带外部连接的数据
文件作用域中带有外部连接的数据存在于其他编译单元中的全局变量冲突的危险(这些编译单元的作者是以自我为中心,他们认为自己拥有整个全局域)。但是名称污染只是全局变量破坏程序的许多方式之一。全局变量将对象和代码绑定在一起,这种方式使得在别的程序中实际上不可能有选择的重用编译单元。在大型项目中,调试、测试甚至理解大量的全局变量的系统的开销也会变得非常的惊人。
加入我们被迫使用一个已经要求在其接口上使用全局变量的系统,则有两种简单的变换方式能将这些变量非全局化:
- 将全局变量引入一个结构中
- 然后将他们私有化并添加静态访问函数
假如我们有以下变量:
int size;
double scale;
const char *syatem;
我们需要改成下面代码,就可以将其从全局名称空间中删除:
struct Global
{
static int size_;
static double scale_;
static const char *system_;
}
需要再源文件中定义这些静态数据成员,这样就可以将名称冲突减小到会与一个类名称冲突,后续我们讨论解决办法。
我们需要使Global成一个类,并且提供方法访问对应的静态变量,这样可以消除直接访问静态成员所带来的难以维护的问题。
自由函数
自由函数也会对全局名称空间形成危险,尤其是参数基调中不包含任何用户定义类型时。如果一个自由函数在一个头文件中定义为有内部连接或者在源文件中定义为有外部连接时,那么在程序集成过程中,他们可能会和有相同名称的另一个函数定义相冲突。运算符函数例外。
在头文件作用域内避免使用自由函数(运算符函数除外),在源文件中避免使用带有外部连接的自由函数(包括运算符函数)
幸好,自由函数总能分组到一个只包含静态函数的工具类中。这样产生的内聚性不一定是最佳的,但它减少了全局名冲突的可能性。例如:
int getA();
void setB(double b);
int isPasswordCorrect(const char *usr, const char *pass)
该自由函数可以使用下面的静态方法替换:
struct SysUtil{
static int getA();
static void setB(double b);
static int isPasswordCorrect(const char *usr, const char *pass)
}
唯一有冲突的危险符号是类名SysUtil.
但是自由运算符函数不能嵌入类中。这不是一个严重的问题,因为自由运算符要求至少有一个参数是用户自定义类型,因此自由运算符冲突的可能性很小,而且这种冲突在实践中一般不成问题。
枚举类型、typedef和常量数据
枚举类型、typedef和文件作用域常量数据都有内部连接。人们经常在头文件的文件作用域内声明常量、枚举类型或用户自定义类型,这是一个错误。
在头文件作用域内应该避免使用枚举类型、typedef和常量数据。
因为C++支持嵌套类型,因此可以在一个类的作用域中定义枚举类型,或者是声明typedef而不会在全局名称空间中与别的名称冲突。选择一个更受限的作用域,在该作用域中定义一个枚举类型,我们可以确保所有的该枚举类型中的枚举元素的作用域相同,并且不会与该作用域之外定义的别名冲突。
例如:
enum Color { RED, ORANGE };
enum Fruit { APPLE, ORANGE };
这两个枚举类型可能会在某一天产生冲突,如果将这两个枚举类型改为在类中定义,可以解决二义性问题。当然C++11还提供了以下方式声明,也可以解决域冲突的问题。一般情况下,优先使用enum class去定义枚举
enum class Color {}
预处理宏
在C++中几乎不需要哄。他们对包含卫哨(guard)是有用的,并且只有在少数情况下,在一个源文件中他们的好处才会超过他们的问题(最特别的是,当用于为移植或者调试获得条件编译时)。但是,一般来说,预处理宏对软件产品是不合适的。
除非是作为包含卫哨,否则在头文件中应该避免使用预处理宏
预处理器不是C++语言的一部分;他的基本原则是完全不改变原文,这使宏非常难以调试。尽管宏可以使代码易于编写,但是他们的自由形式经常使得代码更难阅读和理解。例如下面代码:
#define glue(x, y) x/**/y
glue(pri, ntf)("hello world");
在源代码层次上,我们如何告诉调试这、浏览器或者其他自动工具去处理上述代码呢?
和在源文件中包含宏一样,甚至有更充足的软件工程理由要求头文件不能包含宏。这样情况是允许的:在一个头文件中使用#define来定义预处理常量。因为宏不是C++语言的一部分,他们不会被放在类的作用域中。任何包含#define的头文件将具有该预处理常量的定义。
在丢失或者没有充分实现C++语言特性的情况下,预处理宏也可以用于实现模板。如果宏用于此目的,那么宏函数将出现在头文件中。有一些解决这个问题的方法(不使用宏),可能更适合大型项目。无论如何,与模板相关的问题应在开发过程的早期解决。
头文件中的名称
在头文件的文件作用域中声明的名称,可能潜在地与整个系统中任何一个文件对的文件作用域名称冲突。即使在一个源文件的文件作用域中声明带有内部连接的名称也不能保证一定不与头文件的文件作用域名称冲突
只有类、结构、联合和自由运算符应该出现在头文件的文件作用域内声明;只有类、结构、联合和内联函数应该在头文件的文件作用域内定义
我们希望只能在一个头文件的文件作用域中找到类的声明、类定义、自由运算符声明和内联函数定义。在类作用域中嵌入所有其他的结构,可以消除大多数与名称冲突相关的麻烦。
包含卫哨
如果我们遵守了2.3,我们仍然会遇到问题
当编译组件c的源文件时候,预处理其首先会包含和c相关的头文件,依次地头文件所包含的其他头文件,则编译器会报多重定义
在每个头文件周围放置一个唯一和可预知的包含卫哨
解决这个问题的传统方法是,在每个头文件的内容周围加上一个内部的保护包装器。不管包含图是什么样的,包装器都能确保类和内联函数在一个给定的编译单元中只出现一次。注意,我们不是在企图组织循环包含;我们要阻止的是重复包含,这种重复包含源自一个非循环包含图中的再收敛
现代的处理方式是:
#ifndef PROJECTNAME_FILENAME_H
#define PROJECTNAME_FILENAME_H
#endif // PROJECTNAME_FILENAME_H
// 或者
#program once
包含冗余卫哨
使用冗余哨位,可以有效的减小编译时间复杂度
#ifndef PROJECTNAME_FILENAME_H
#define PROJECTNAME_FILENAME_H
#ifndef INCLUDE_H1
#include "h1.h"
#endif
#endif // PROJECTNAME_FILENAME_H
文档
为接口建立文档以便其他人可以使用,至少请另一个开发者检查每个接口
明确地声明条件(在该条件下行为没有定义)
使用assert语句有助于为程序员实现编码时的假设建立文档
文档和assert语句的有效使用可以使我们得到更简练但是仍然十分有用的代码。如果有人误用了某个函数,这是他们自己的错–而且他们很快就能发现这个错误!
标识符命名规则
使用一种一致的方法(例如在变量后加_表示成员变量,首字母大写来表示类,使用全部大写来标识不变值、const和预处理数据等, 标识符名称要一致)当函数实现方式相同时候,名称必须一致
在一个大型系统的整个接口获得一致性,可以增强可用性,但是要达到此目的也可能会遇到出人预料的困难。在大型项目中,授权一个顶级开发小组担当接口工程师已经被证明是有效的,可以跨越开发小组获得一致性。容器类以及他们的迭代器也有助于模板的实现,实现模板可有效地加强跨越其他无关对象的一致性。