您的位置:首页 > 编程语言 > C语言/C++

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

2013-06-01 10:00 330 查看
从本条款开始,就进入到全书的第四部分:设计与声明。

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

举一个书上的例子:

class Date
{
public:
Date(int month, int day, int year);
…
};


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

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

Date d(2013, 5, 28);


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

Date d(5, 32, 2013);


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

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

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

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

class Month
{
private:
int m_month;
public:
explicit Month(int month): m_month(month){}
};

class Day
{
private:
int m_day;
public:
explicit Day(int day): m_day(day){}
};

class Year
{
private:
int m_year;
public:
explicit Year(int year): m_year(year){}
};

class Date
{
private:
Year m_year;
Month m_month;
Day m_day;

public:
Date(Year year, Month month, Day day): m_year(year), m_month(month), m_day(day){}

};

int main()
{
Date date(Year(2013), Month(5), Day(28));
}


注意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类中给出月份的枚举类型,这样用户可以更直观地使用:

class Month
{
private:
int m_month;
public:
explicit Month(int month): m_month(month){assert(m_month >= 1 && m_month <= 12);}
enum
{
Jan = 1,
Feb,
Mar,
Apr,
May,
Jun,
July,
Aus,
Sep,
Oct,
Nov,
Dec
};
int GetMonth() const
{
return m_month;
};
};


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

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


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

预防用户不正确使用的另一个办法是,让编译器对不正确的行为予以阻止,常见的方法是加上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倍)且慢,而且使用辅助动态内存。任何事物都具有两面性,权衡一下就会发现,智能指针能避免的资源泄露问题(好的接口),相较于它的空间和时间代价而言,都是值得的。

最后总结一下:

好的接口容易被正确使用,不容易被误用;

促进正确使用的办法包括接口的一致性,以及与内置类型的行为兼容;

阻止误用的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任;

多多使用shared_ptr来代替原始指针
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐