一、让接口容易被正确使用,不易被误用
使接口容易被正确使用
比如我们开发一个表现日期的类:
1
2
3
4
5
6
7
8
9
10class Date{
public:
Date(int month, int day, int year);
}
//顾客可能会这样使用:
Date d(3, 30, 1995); //正确
Date d(30, 3, 1995); //error
//或是,超出范围地写出不正确的日期
Date d(4, 55, 1995);完全不合理,故而可以这样设计:
1
2
3
4
5
6
7
8struct Day{
explicit Day(int d):val(d){}
int val;
}
class Date{
public:
Date(const Month& m, const Day & d, const Year& y){...}
}或者,给代码足够的注释:
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Date{
public:
/**
*@brief:简介,简单介绍函数作用
*@param:介绍函数参数
*@return:函数返回类型说明
*@exception NSException:可能抛出的异常.
*@author zhangsan:作者
*@date 2011-07-27 22:30:00:时间
*@version 1.0:版本
*@property:属性介绍
*/
Date(int m, int d, int y);
}使接口与普遍认可的接口保持一致,比如,STL中,获取容器当前装载个数的接口都是size(),一个好的设计,应当使接口保持一致性。
阻止接口误用,方法包括:建立新类型,限制类型上的操作,束缚对象值,以及甩锅给客户(你把注释写得足够好,本身代码又没问题,只能怪客户咯)
曾经不少的DLL会出现一种在某个DLL内部新建一块资源,在另一个DLL内部过早或者不正确地delete掉(比如我们想要的删除行为是释放,或者对计数值减一,而它真的把东西删掉了),产生bug的现象,这种情况下,建议在写类的时候就将资源保管在智能指针内部,并指定它的删除器)
二、设计class如同设计type
当设计一个class时,我们应当尽可能周全地考虑清楚可能发生的事,好的type应当又自然的语法,直观的语义,多个高效的实现,当进行设计前,不妨思考以下问题:
- 新的type该如何被创建和销毁
- 对象的初始化和对象赋值应当有何区别
- 新的type若被传值,会发生什么(copy构造函数行为)
- 什么是新type的合法值?(比如Month类不该超出12或1)
- 新的type需要配合某种继承吗,需不需要virtual?
- 新的type是否需要类型转换
- 新的type需要用到哪些操作符
- 什么样的标准函数应当驳回(比如不符合现实逻辑的复制房产证对象)
- 新的type有多么一般化,是不是该考虑从类升级到类模板
- 你是否真的需要一个新的type
三、多使用pass by renference to const 替换 pass by value
在一些情况下,pass by renference to const 比 pass by value 更为节约时空成本,举例:
1 | class Person{ |
试想,现针对Student有一个接口void dosomething(Student s){}
,当我们用值传递时,这涉及到一次Student copy调用,两次string copy调用(school与class),一次Person copy调用以及两次string copy调用(name与adress),共六次copy调用加这些对象的销毁……但若是使用void dosomething(const Student &s)
,则只涉及到一个引用传递,极为便捷。
再考虑一个例子,对于Person和Student,我们写一个函数:
1 | void printinfo(Person p){ |
对于这个函数,我们这样使用:
1 | Student s; |
当以值传入一个Student时,由于这里进行值传递,printinfo中的p会被构造成一个Person对象,因此打印出的信息只有一部分,这是不合适的,而若使用const Person& 作为参数,这种情况下由于多态的存在,是完全OK的。
四、必须返回临时对象时,使用值传递
若是某个函数需要返回一个函数内进行处理的local对象,一般采用值传递,因为函数的local对象会在函数调用完毕时自动被回收(操作系统管理栈,程序员管理堆),而若local对象是在堆上的,则你必须考虑它的delete以防止资源泄漏,比如:
1 | class complex{ |
所以,一般情况下,从函数返回时使用pass by value
五、将成员变量声明为private
将成员变量声明为private为类提供了细微的访问控制划分,且对于用户而言,日后的改动并不影响之前的代码,private为类提供了其封装性。
对象的封装性与其代码改动时可能造成的代码破坏量成正比,故而对于一个public成员,在改动它时,所有使用它的代码都会受到影响,这是一个不可知量,而对于一个protected成员,在改动它时,所有derived class都会受到影响,这也是一个不可知量,所以在封装的观点中,只有两种访问权限:private,others。
六、若non-member, no friend函数和member函数可以做到同样的事,使用non-member函数
对于一个类,如果越多的成员被隐藏,越少的代码可以改变它,那么我们认为这个类的封装性就越好;作为一个粗略的量度,我们认为,越多的函数可以访问它,数据的封装性就越差。因此,在一个non-member, no friend函数和一个member函数中去选择,为了增强数据的封装性,我们往往更倾向于选择前者,因为它不增加能够访问类中private成员的函数数量,也就是说,它提供了更好的封装性。而且,这个non-member, no friend函数可以是另一个类的成员函数。
通常,我们会将这个non-member, no friend函数和需要操作的类写在同一个命名空间中:
1 | namespace WebBroswerStuff{ |
使用命名空间不止增加了程序的可读性,由于命名空间可跨越多个文件,它也为组织代码提供了便利。
比如一个WebBroswer
类需要书签,cookie,设置等多个小功能,一个只使用cookie的客户没道理也要编译书签相关的东西。而使用namespace,你可以将它们分离在不同的头文件中:
1 | //头文件"webbroswer.h" WebBroswer核心 |
这样,用户就可以根据自己的需要,选择编译某一部分,降低了代码的编译依存性,也使得用户可以根据自己的需求去扩展功能,他们只需要添加需要的non-member, no friend函数到此命名空间中。
使用non-member, no friend函数叠加namespace可以增加封装性,包裹弹性和技能扩充性
七、若所有参数都需要类型转换,使用non-member函数
比如新定义了一个类Complex
:
1 | class Complex{ |
当我们想为这个Complex类型定义一个乘法运算符重载函数时,有两种选择,一种是使用member函数,一种是使用non-member函数:
1 | //member: |
若我们直接使用一个整数去乘以一个Complex对象,这个需求是很合理的,但让我们想想,将整数放在 * 号左边和右边带来的影响:
1 | //member |
所以当函数所有参数都需要隐式转换,将他写为一个non-member函数
八、考虑写一个不抛出异常的swap函数
会写一个方便的swap函数对一个优秀的CPP程序员是必不可少的。
在Cpp11前,swap函数定义如下:
1 | namespace std{ |
这样的swap
实现十分常见,但对于一些类型,这是完全不必要的,若你使用pimpl手法对设计一个类型:
1 | class WidgetImpl{ |
此时,若是使用std
中的swap
函数对两个Widget对象进行交换,根据Widget复制构造和复制运算符实现的不同,有可能一个swap会复制三次WidgetImpl对象,这显然是不太对的(Cpp1x中已经有了移动语义,但作者写这本书的时候移动还尚未出现,前人栽树,后人乘凉吧算是)。针对这一情况,我们可以针对Widget,在std中为其写一个特化版本的swap
函数,让编译器自动调用:
1 | class Widget{ |
采用这种方法,不仅解决了问题,而且与STL容器的实现具有一致性,所有STL容器都提供一个public swap和一个std中的swap的特化版本
而对于一个类模板,上述的写法将不再适用。如:
1 | template<typename T> |
这种情况是不被允许的,因为std中允许添加全特化版本的templates,但对于新的templates是明确禁止的,故而,我们为Widget模板相关的东西增加一个专有的命名空间:
1 | namespace WidgetStuff{ |
当你编写一个func template,且该模板内需要swap操作时,应当这样写:
1 | template<typename T> |
这样编写有助于编译器采取更好的swap,如果传入的T的专有命名空间内有一个swap,编译器会优先调用它,若是没有,编译器会使用std空间内部的默认swap。using std::swap
的作用是使std中的swap在函数内曝光,若是std::swap(a, b)
则只会使得编译器只使用std内部的版本,失去了高效性和灵活性。
- 当std::swap效率不高时,提供一个public member的swap函数,并保证它不抛出异常
- 提供一个non-member调用member的swap,对于classes,使这个non-member成为一个全特化版本并放入std空间,对于templates,把它放入其专有空间,尽量使得代码保持一致性
- 可能用到std::swap时,采用声明:
using std::swap
,然后正常调用它,编译器会先在class所属命名空间内找,找不到了再去std空间下,而直接使用std::swap(Args …ards)时,会使编译器不使用命名空间的特化版本。