本文共 5471 字,大约阅读时间需要 18 分钟。
C++ 中到处充满了接口。函数接口、类接口、模板接口,等等。每个接口都是实现客户端程序员与我们的代码相交互的一种手段。假设客户通情达理,他们的项目也十分优秀,他们便会十分看重我们的接口是否易于正确使用。这是千真万确的,如果他们误用了这些的接口中的任一个,那么我们也难推其咎。在理想状态下,如果客户端程序员尝试使用一个接口,但是没有达到预期的效果,那么代码则不应通过编译。反之,如果代码通过了编译,那么则必须要符合客户端程序员的需求。 开发中我们应做到让接口更易于正确使用而不易被误用,这需要我们考虑到客户端程序员会犯的各种错误。请参见下面的示例,假设编写一个表示日期时间的类的构造函数: Date(int month, int day, int year); 乍一看,这一接口设计得很合理,但是当客户端程序员面对这样的接口时,很容易犯下两种错误。第一,他们可能会使用错误的传参顺序: Date d(30, 3, 1995); // 啊哦,应该是“ 3, 30 ”而不是“ 30, 3 ” Date d(3, 40, 1995); // 啊哦,应该是“ 3, 30 ”而不是 “3, 40” 客户端程序员犯下的许多错误是可以通过引入新类型来避免的。实际上,对于防止不合要求的代码通过编译,类型系统是我们最得力的助手。在上述情况下,我们可以引入几个简单的“包装类型”来区别日期、月份、和年,然后再在 Date 的 构造函数中使用这些类型: explicit Day(int d) : val(d) {} explicit Month(int m) : val(m) {} explicit Year(int y) : val(y) {} Date(const Month& m, const Day& d, const Year& y); Date d(30, 3, 1995); // 报错!类型错误 Date d(Day(30), Month(3), Year(1995)); // 报错!类型错误 Date d(Month(3), Day(30), Year(1995)); // OK ,类型正确 我们可以改善上边简单的应用结构体的思路,让 Day 、 Month 、 Year 变得“羽翼丰满”,从而可以提供完善的数据封装。但是即使是结构体也足以说服我们:引入新的类型可以十分有效地防止接口误用的发生。 只要我们在恰当的地方使用了恰当的类型,便可以合理地限制这些类型的值。比如说,一年有 12 个月,所以 Month 类型应该能够反映出这一点。一个途径是使用枚举类型来表示月份,但是枚举类型并不总能达到我们对于安全的需求。比如说,枚举类型可以像 int 一样使用。一个更安全的解决方法是:预先定义好所有有效 Month 值 的集合: static Month Jan() { return Month(1); } // 用来返回所有有效月份值 static Month Feb() { return Month(2); } // 的函数; ... // 下面将看出为什么使用函数,而不是对象 static Month Dec() { return Month(12); } explicit Month(int m); // 防止创建新的月份值 Date d(Month::Mar(), Day(30), Year(1995)); 如果为上面代码中使用函数来代替具体月份的思路感到奇怪,那么可能是忘记声明非局部静态对象可能会带来可靠性问题。 为防止客户端程序员犯下类似的错误,我们还可以采用另一个途径,那就是严格限制一个类型可以做的事情。加强限制的一个常用的手段就是添加 const 。比如说,对于用户自定义类型 operator* 的返回值来说,具备怎样程度的 const 特性就可以防止客户端程序员犯下下面的错误: if (a * b = c) ... //本来是想进行一次比较! 实际,这仅是“让接口易于正确使用,而不易被误用”的另一种一般的做法:除非有更好的理由阻止我们这样做,否则应保证自己创建的类型的行为与内建数据类型保持一致。客户端程序员已经清楚 int 的行为,所以只要合情合理,就应该力求使我们的类拥有与 int 一致的行为。比如说,如 果 a 和 b 是 int 类型,那么为 a*b 赋 值就是不合法的。 设计接口时应防止与内建数据类型发生不必要的不兼容性,这样做的真正目的是让各类接口拥有一致的行为。除了一致性,很少有特征可以让接口更加易于正确使用,同时,除了不一致性,也很少有特征可以让接口变得更加糟糕。 STL 容器的接口大体上(但并不完美)是一致的,这就使得它们更易于使用。比如说每个 STL 容器都有一个名为 size 成员函数,它可以告诉我们这一容器中容纳了多少对象。这一点与 Java 和 .NET 是不同的, Java 中使用 length 属性 来表示数组的长度, length 方法 来表示字符串的长度,以及 size 方法来表示 List 的大小。而 .NET 中的 Array 拥有一个叫做 Length 的属性,而 ArrayList 中功能相类似的属性则叫做 Count 。一些开发人员认为,集成开发环境( IDE )的存在让这类不一致性问题变得不那么重要,但是实际上他们大错特错了。不一致性问题会会给开发人员带来无穷尽的烦恼,而 IDE 是绝不能解决这些问题的。 任何接口都需要客户端程序员记忆一些易发生错误的内容,这是因为客户端程序员可能会把这些东西搞砸。比如说,引入一个工厂函数来返回一个指向 Investment 类中动态分配对象的指针: Investment* createInvestment(); //为简化代码省略参数表 为防止资源泄漏,由 createInvestment 返回的指针在最后必须被删除,但是这将会给客户端程序员留下至少两个犯错误的机会:忘记删除指针、多于一次删除统一指针。 客户端程序员可以将 createInvestment 的返回值保存在诸如 auto_ptr 或 tr1::shared_ptr 这样的智能指针中,然后让智能指针担负起调用 delete 的责任。但是如果客户端程序员忘记了使用智能指针,这该怎么办呢?通常情况下,更好的接口的设计方案是:让工厂函数返回一个智能指针,在一开始就不给问题任何发生的机会。 std::tr1::shared_ptr<Investment> createInvestment(); 这样便可以从根本上强制客户端程序员将返回值存储在一个 tr1::shared_ptr 中,在一个原始 Invesement 对象再有用之后,这样做可以防止忘记删除这一无用的对象。 事实上,返回 tr1::shared_ptr 让接口设计人员能够防止由资源释放所造成的客户端错误,这是因为 tr1::shared_ptr 允许在智能指针创建时,将资源释放函数(一个“删除器”)绑定在这一智能指针中,而 auto_ptr 没有这一功能。 假设一个客户端程序员从一个 createInvestment 中得到了一个 Investment* 指针,他可能会将这个指针传给一个名为 getRidOfInvestment 的函数,而不是将其删除。这样的接口将会为客户端程序员带来新的错误,客户端程序员将会使用错误的资源析构机制(也就是使用了 delete 而不是 getRidOfInvestment )。实现 createInvestment 的程序员可以通过返回一个绑定了 getRidOfInvestment “删除器”的 tr1::shared_ptr 来预防此类错误。 tr1::shared_ptr 提供了一个拥有两个参数的构造函数:需要管理的指针,以及当引用计数值为零时需要调用的删除器。关于创建绑定 getRidOfInvestment “删除器”的 tr1::shared_ptr ,请看下面的方法: std::tr1::shared_ptr<Investment> pInv(0, getRidOfInvestment); // 尝试创建一个 null 的 shared_ptr,并且让其包含一个自定义的删除器; 这并不是合法的 C++ 语法。 tr1::shared_ptr 的构造函数的第一个参数必须是一个指针,而 0 则不是,它是一个 int 值。的确,它可以转换成一个指针,但是这种情况下此类转换并不值得推荐, tr1::shared_ptr 的第一个参数必须是一个实际的指针。通过一次转型可以解决这一问题: std::tr1::shared_ptr<Investment> pInv(static_cast<Investment*>(0), getRidOfInvestment); //创建一个 null 的 shared_ptr ,并且让其包含一个自定义的删除器; 上面的代码意味着,在实现 createInvestment 时,可让其返回一个“绑定了 getRidOfInvestment 删除器的 tr1::shared_ptr ”: std::tr1::shared_ptr<Investment> createInvestment() std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0), getRidOfInvestment); retVal = ... ; // 让 retVal 指向恰当的对象 当然,如果在创建 pInv 之前就确定了其所管理的裸指针,那么将裸指针传递给 pInv 的构造函数更理想些,而不应该将 pInv 初始化为空值然后对其赋值。 使用 tr1::shared_ptr 的另一个较为显著的好处在于:它自动为每个指针预留一个删除器,用它们来排除另一类客户端错误,也就是所谓的“跨 DLL 问题”。这一问题在下面的情况中会发生:一个动态链接库( DLL )中使用 new 创建了一个对象,而这个对象在另一个 DLL 中由 delete 语句删除了。在许多平台上,此类跨 DLL 的“ new/delete 对”将导致运行时错误。 tr1::shared_ptr 可以防止此类问题发生,因为如果创建了一个 tr1::shared_ptr ,它的默认删除器在同一个 DLL 中使用 delete 。举例说,这将意味着如果 Stock 继承自 Investment ,同时 createInvestment 是这样实现的: std::tr1::shared_ptr<Investment> createInvestment() return std::tr1::shared_ptr<Investment>(new Stock); 那么返回的 tr1::shared_ptr 将在各 DLL 文件中自由穿梭,而不用考虑跨 DLL 问题。这一指向 Stock 的 tr1::shared_ptr 会始终追踪这一事件:当 Stock 的引用计数值为零时,需要使用哪一个 DLL 的 delete 语句。 这里主要讲如何让接口更加易于正确使用,而不易被误用,而不是 tr1::shared_ptr ,但是 tr1::shared_ptr 对于避免此类客户端错误却是一个不可多得的好工具,使用它是值得的。 tr1::shared_ptr 最为通用的实现来自 Boost 。 Boost 中的 shared_ptr 有两个裸指针那么大,它为记录和删除专用数据使用动态分配内存,当调用函数的删除器时使用虚函数,如果它认为一个应用程序是多线程的,那么当修改该程序的一个引用计数值时,将引入线程同步的开销。(也可以通过定义一个预处理记号来禁用多线程。) Boost 中的 shared_ptr 也是有缺点的:它比裸指针更大,更慢,而且使用辅助的动态内存。在许多应用程序中,这些额外的运行时开销并不那么显著,但是它可以降低客户端程序员出错的可能,这一点对每个人来说都是十分显著的。 # 优秀的接口应该易于正确使用,而不易误用。对所有的接口都应该力争做到这一点。 # 保持与内置数据类型有一致的行为,是使接口易于正确使用的一种可行的方法 # 防止错误发生的方法有:创建新的数据类型,严格限定类型的操作,约束对象的值,不要将管理资源的任务留给客户端程序员。 #tr1::shared_ptr 支持自定义的删除功能。这可以防止 “ 跨 DLL 问题 ” ,可以应用在自动解开互斥锁等情况下。 转载地址:http://vuznb.baihongyu.com/