读书笔记_Effective_C++_条款十八:让接口容易被正确使用,不易被误用

从本条款开始,就进入到全书的第四部分:设计与声明。

程序员设计接口时应本的对用户负责的态度,“如果客户企图使用某个接口而却没有获得他所预期的行为,这个代码不应该通过编译;如果代码通过了编译,那它的作为就该是客户所想要的”。

举一个书上的例子:

1 class Date
2 {
3 public:
4     Date(int month, int day, int year);
5 6 };

这个类看上去好像没有问题,提供的构造函数接口简单直观,然而它却做了一个假设——用户都能够按月、日、年的顺序来传参。

但事实上,一定会有不少用户记错这个顺序,比如我们常用的顺序是年、月、日,所以有同学会下意识地这样调用:

Date d(2013, 5, 28);

也许还有同学随便输入,导致传参不符合实际,比如:

Date d(5, 32, 2013);

其实当你设计的程序需要假定用户都能按你想像来进行操作的话,这个程序就存在隐患。

一种好的解决方法,是假定用户输入的数据都是不可靠的,需要对输入进行严格的检测,这是防御式编程的思想,这对那个心怀不轨的用户来说是很好的处理方法。但对于一般的用户,不合法的输入只是缺乏正确的引导。

如何去引导用户,如何让用户动最少的脑筋却能最佳地使用接口,是我们接下来要讨论的。

一种好的引导方式,是让用户在传参的时候,知道自己传的是什么,之前用是一个int,这显然不能提醒用户。那就创建相应的类,像这样:

 1 class Month
 2 {
 3 private:
 4     int m_month;
 5 public:
 6     explicit Month(int month): m_month(month){}
 7 };
 8 
 9 class Day
10 {
11 private:
12     int m_day;
13 public:
14     explicit Day(int day): m_day(day){}
15 };
16 
17 class Year
18 {
19 private:
20     int m_year;
21 public:
22     explicit Year(int year): m_year(year){}
23 };
24 
25 class Date
26 {
27 private:
28     Year m_year;
29     Month m_month;
30     Day m_day;
31     
32 public:
33     Date(Year year, Month month, Day day): m_year(year), m_month(month), m_day(day){}
34 
35 };
36 
37 int main()
38 {
39     Date date(Year(2013), Month(5), Day(28));
40 }

注意Year、Month和Day类中构造函数前有explicit关键字,也就是不允许隐式构造,诸如Date date(2013, 5, 28)等会报以下的错误:

允许的写法就是像main()函数中所示,为年、月、日提供了类封装的好处还不仅仅有这些,当用户输入一个非法数值后,可以在类中进行判断

比如我们可以进一步对Month类的构造函数进行扩充,像这样:

explicit Month(int month): m_month(month){assert(m_month >= 1 && m_month <= 12);}

这样就限制了输入Month数据的合法性。

当用户试图传入一个Month(15)的时候,断言失败就会报下面的错:

 

同时控制台会打印失败的代码至控制台:

还有一种好的设计,就是在Month类中给出月份的枚举类型,这样用户可以更直观地使用:

 1 class Month
 2 {
 3 private:
 4     int m_month;
 5 public:
 6     explicit Month(int month): m_month(month){assert(m_month >= 1 && m_month <= 12);}
 7     enum 
 8     {
 9         Jan = 1,
10         Feb,
11         Mar,
12         Apr,
13         May,
14         Jun,
15         July,
16         Aus,
17         Sep,
18         Oct,
19         Nov,
20         Dec
21     };
22     int GetMonth() const
23     {
24         return m_month;
25     };
26 };

然后在main函数中可以这样写:

1 int main()
2 {
3     Date date(Year(2013), Month(Month::May), Day(28));
4 }

总之就是时刻提醒用户知道自己传的参数是什么。

 

预防用户不正确使用的另一个办法是,让编译器对不正确的行为予以阻止,常见的方法是加上const,比如初学者常常这样写:

if(a = b * c){…}

很明显,初学者想表达的意思是比较两者是否相等,但代码却变成了赋值。这时如果在运算符重载时用const作为返回值,像这样:

const Object operator* (const Object& a, const Object& b);

注意这里的返回值是const,这时编译器就会识别出赋值运算符的不恰当了。

 

书上还提到很重要的一点,就是“尽量令你的自定义类型的行为与内置类型行为一致”,举个夸张的例子就是,不要重载乘号运算符,但里面做的却是加法。自定义类型同时也要形成统一的风格,比如长度,不要有的类型用size表示,有的类型用length表示,这会使得哪怕是一个程序老手也会犯糊涂。虽然有些IDE插件能够自动去寻找相应的方法名,但“不一致性对开发人员造成的心理和精神上的摩擦与争执,没有任何一个IDE可以完全抹除”。

 

最后再谈谈前几节说到的智能指针,“任何接口如果要求客户必须记得做某些事情,就有着不正使使用的倾向,因为客户可能会忘记做那件事”。所以像:

Investment* createInvestment();

就是不好的接口,容易造成资源泄露(对于像服务器一样长时运作的机器而言,资源泄露无疑是致命的)。

学习了智能指针之后,就可以这样做接口,让专门的资源管理类来掌握资源的回收

auto_ptr<Investment> createInvestment();

shared_ptr<Investment> createInvestment();

使用shared_ptr会更好一些,因为它允许存在多个副本,不会在传递过程中改变原有资源管理者所持的资源,且支持自定义的删除器。书上说,自定义删除器可以有效解决”cross-DLL problem”,这个问题发生于在不同的DLL中出生(new)和删除(delete)的情况(对象生命周期横跨两个DLL,但在第二个DLL中结束生命的时候却希望调用的是第一个DLL的析构函数),自定义删除器则会在删除时仍然调用诞生时所在的那个DLL的析构函数。

当然,使用智能指针也是需要代价的,它比原始指针大(Boost库中实现的shared_ptr体积是原始指针的2倍)且慢,而且使用辅助动态内存。任何事物都具有两面性,权衡一下就会发现,智能指针能避免的资源泄露问题(好的接口),相较于它的空间和时间代价而言,都是值得的。

最后总结一下:

  1. 好的接口容易被正确使用,不容易被误用;
  2. 促进正确使用的办法包括接口的一致性,以及与内置类型的行为兼容;
  3. 阻止误用的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任;
  4. 多多使用shared_ptr来代替原始指针
posted @ 2013-06-01 10:00  Jerry19880126  阅读(1116)  评论(2编辑  收藏  举报