C++编程规范整理(一)

来源:互联网 发布:汽车维修管理系统源码 编辑:程序博客网 时间:2024/04/30 19:15

1. 从头说起

确定完代码结构,打开编辑器,开始编写C++的类时,首先要考虑采用class还是struct结构。然后要确定代码结构,定义接口(纯虚基类)、基类和子类之间的关系。接着最好完整地声明构造函数、拷贝构造函数、析构函数和重载赋值运算符。最后就是声明成员变量和所需要的行为——成员函数。确定了这些,就可以开始编写类的代码逻辑了。

1.1. 继承

继承是OO中的重要特性,也是被广泛“滥用”的特性,很多使用基类的情况仅仅只是为了可以默认地使用某些方法(这种情况应该使用“接口”而不是继承)。这不仅仅让类的设计产生了不一致的抽象,使用户难以理解和使用。这种设计往往会产生紧耦合,对代码的重用和重构带来很大困难。

另外在使用继承时,需要注意的是不要覆盖基类的非虚函数,这样在动态绑定时,会导致多态性失效,引起难以发现的Bug。

因此在使用继承时,需要关注:

  • 公开继承的类,必须表示“is-a”的关系,禁止仅仅因为可以复用某些方法而使用承
  • 对于不满足“is-a”关系的两个类,若要复用代码,使用组合代替继承
  • 避免使用多重继承,如果要进行多重实现继承,考虑使用组合代替
  • 派生类不允许覆盖基类中的非虚函数
  • 建议使用公有继承,慎用保护继承和私有继承

1.2. class还是struct

C++中对数据的包装主要有两种方式:class和struct。这两种在语法上没有很大的区别,只有在默认成员的访问权限上存在差别(class默认为private,struct默认为public)。但是在C++中,struct是从C语言中借过来的概念,因此最好尽量的保持其语义与C语言中的一致——数据集合。

因此在C++中,对于class还是struct的选择最好遵从下列规则:

  • 用struct封装数据集合,公开定义数据成员,struct中一般不定义成员函数
  • 用class封装用户的行为,不公开定义数据成员,class之间的数据交互通过成员函数进行

1.3. 构造函数做些什么

构造函数,顾名思义就是对象创建时,构造对象基本数据的函数,它实现了对象的初始化过程。在设计构造函数时,可以遵循一个简单的规则:对于已完成构造的对象,应该是即刻可用的。换句话说,构造函数都已经调用了,对象都构造完了,如果此时对象还不可用,那就是违背构造函数的语义的。

有很多实现采用init()函数来代替构造函数完成对象的初始化。但是额外增加的init()函数会大大增加类实现和使用的复杂性。构造函数在创建对象的时候被且仅被调用一次。为了保持该特性,如果使用init()函数来代替构造函数,则必须做到:

  • 所有的成员函数都必须判断init()函数是否被正确调用
  • init()函数没调用时,所有成员函数都必须返回一个错误码来明确指示
  • init()实现的过程中必须处理多次重复调用的情况,所有成员函数的消费者都必须检查其返回值等等

在设计构造函数时,如果需要构造的数据过于复杂,可以考虑用创造性设计模式将每个数据的构造分拆到多个类中。此外还需要注意的是,只有当构造函数成功调用后,新建的对象才是合法对象,在释放对象时,才会调用其析构函数。因此如果在调用构造函数的时候出现异常从而导致构造过程退出时,必须自行释放所有已分配的资源。

综上所述,在设计和实现构造函数时,应该遵从一下原则:

  • 构造函数能够使对象进入一个一致的状态,因此在构造函数调用结束之后,对象应该是即刻可用的
  • 构造函数只负责初始化数据成员,避免在构造函数中实现过于复杂的业务逻辑。也要使用构造函数来完成复杂的工作
  • 必须使用初始化列表来显示初始化直接基类和所有数据成员
  • 构造函数可以抛出异常,但是必须自行清理在之前构造过程中所占用的资源
  • 在设计一个类时,尽量避免使用init()函数

1.4. 默认构造函数

默认构造函数是指没有参数的构造函数。当程序员没有显示定义任何构造函数时,C++编译器将为其自动生成一个默认构造函数。但是该构造函数的语义往往与程序员的期望不一致。因此尽量不要使用编译器生成的默认构造函数。显示定义默认构造函数既能避免一些意想不到的错误,也使代码具有更好的可读性,同时也便于通过注释向类消费者说明默认构造的对象状态。因此在编写类代码时需要注意:

  • 有默认语义的类,必须显示定义其默认构造函数
  • 没有默认语义的类,必须显示定义其他构造函数或者将默认构造函数声明为private

1.5. 显示构造函数

显示构造函数的概念相对于隐式构造函数。所谓的隐式构造函数是指形如MyClass::MyClass(MyArg arg)的单参数构造函数。这种构造函数会定义MyArg类型到MyClass的隐式类型转换,这可能会带来很大的风险(轻则效率低,重则导致不可意料的程序行为,且很难追查)。因此:

  • 对于单参数的构造函数,除非是拷贝构造,或者的确希望提供隐式类型转换功能(如类string,需要char*到string的隐式类型转换),则必须对其进行显示声明(explicit关键字)
  • 在极少数的确需要隐式转换的情况下,可以隐式声明单参数构造函数

1.6. 拷贝构造函数

拷贝构造函数是指形如MyClass::MyClass(const MyClass&)的构造函数。拷贝构造函数主要在对象拷贝时调用(是拷贝,区分于赋值)。当程序员没有声明或定义任何构造函数时,编译器会自动生成一个拷贝构造函数。

但事实上,有一些类是没有拷贝的语义的(如托管资源的类,CFile)。此时则应该隐藏拷贝构造函数,以免发生用户错误调用造成的资源泄漏、重复释放等问题。而对于需要拷贝语义的类,编译器提供的拷贝构造函数对指针类型进行了浅拷贝,这种策略显然不是开发者想要看到的。

因此,对于拷贝构造函数需要有如下规定

  • 没有拷贝意义的类,必须将拷贝构造函数声明为private,并不给实现
  • 有拷贝意义的类,需要明确指定其拷贝行为(浅拷贝还是深拷贝)

1.7. 重载赋值运算符

赋值运算函数和拷贝构造类似,它会在对象作为左值时被调用。在程序员没有重载赋值运算符时,编译器会自动生成一个默认的赋值运算符函数。

因此,与拷贝构造函数类似,对于没有赋值意义的类(如托管资源的类),应当防止用户错误调用而导致资源泄漏、重复释放等后果。由于默认的赋值运算符函数对指针实现浅拷贝,因此对于有赋值意义的类,尤其是包含指针成员变量的类,必须重载赋值运算符函数。因此:

  • 没有赋值意义的类必须private声明复制赋值函数并且不给出实现
  • 有赋值意义的类必须重载赋值运算符,并小心指定其拷贝的行为(浅拷贝、深拷贝等)

1.8. 析构函数

析构函数负责对象销毁时,做资源回收、清理数据等工作。当编写代码时没有显式定义析构函数,则编译器会提供一个默认的析构函数。默认析构函数不会析构指针成员所指向的对象,更不会释放其所占能存,因此这可能会导致内存泄漏或资源句柄长期未释放。另外若基类没有将虚构函数声明为虚函数,那么delete pBaseClass操作时,不会调用子类的析构函数从而会产生不可预期的结果。

因此对于析构函数的定义和使用,必须遵循以下几点:

  • 若类定义了虚函数,必须定义虚析构函数
  • 若类设计为可被继承的,应该定义公开的虚析构函数或protected的非虚析构函数(不允许delete操作)
  • 若类包含有指针成员,则必须显示定义虚构函数,并在其中明确指明指针是否销毁、如何销毁
  • 绝不允许让异常离开析构函数
  • 析构函数应该用于释放资源,销毁对象,避免执行复杂的操作,尤其避免执行可能失败的操作
  • 不继承包含有非虚析构函数的类

1.9. 成员访问控制

访问控制是OO的封装性中重要的概念。若类成员都用public/protected方式定义成员的访问控制,那么类就完全地暴露在消费者或派生类之中,从而失去了通过重构优化代码的能力,也失去了封装性。因此适当的访问控制是必须的。

在类和类之间,数据成员的耦合性比成员函数要大。而且一旦将数据成员暴露在外,就难免会受到数据状态一致性的约束,这些是很难控制的。因此对于成员变量,需要尽量使用private约束。如需和外界进行数据交互,最好定义getter和setter来封装。

对于静态的成员变量,必须考虑到其线程安全性。因此尽量使用const来保护静态成员变量,对于可变的,需要用getter和setter来进行保护。

因此,对于成员的访问控制,需要做到:

  • 在满足需求与接口完整性的前提下,必须为所有方法成员提供尽可能严格的访问控制
  • 类不能定义public非静态数据成员,不应该定义protected数据成员
  • 类可以定义public静态数据成员,但必须是const的。否则,应该通过静态getter/setter访问,并通过文档指定其是否线程安全

1.10. 成员声明顺序

在C++中,成员的声明顺序一般按照访问控制权限来管理,一般按照public、protected和private的先后顺序来声明。

1.11. 友元

友元是C++中的一个重要概念,它使得类与类之间的关系变得非常灵活,但是错误的使用友元也会导致严重的错误。

友元的存在破坏了类的封装性,增加了类与类之间的耦合性,这对于代码结构的设计和实现都会带来一些麻烦,因此应该尽量避免使用友元。

但是也存在着一些类,它们之间的确在语义上就存在着较强的耦合关系,在这种情况下可以使用友元(如容器和它的迭代器、类与涉及该类的运算符等等)。

原创粉丝点击