《Effective C++》读书笔记(5)
4. 设计与声明
条款18:让接口容易被使用,不易被误用
- 好的借口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
- “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
- “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
- std::shared_ptr支持定制型删除器(custom deleter)。可防范DLL问题,可被用来自动解除互斥锁等等。
这个条款的思想其实适用于所有的编程语言,只要你写的代码需要被其他地方调用,你都需要考虑接口的易用性。即使不暴露,良好的接口设计也能够提高代码的可读性。作者提出的这几点只是在C++这个语言的基础上需要注意的一些小点,我认为设计一个良好的易用的接口,仅仅做到这几点还远远不够。
接口一致性
作者认为除非有充分的理由,否则最好让类型与内置的类型的行为尽量一致。这个可以降低使用者错误使用的概率。
我想补充的另外一点是,比如你提供一整套的接口,你需要让自己的接口相互保持一致的风格。包括但不仅限于:函数的命名风格、函数的行为尽量保持统一的风格。
防止误用
任何接口如果要求客户必须记得做某件事情,就是有着“不正确使用”的倾向,因为客户可能会忘记做那件事。
条款19:设计class犹如设计type
Class 的设计就是type的设计。在定义一个新type之前,确保阅读了本条目的所有讨论主题。
- 新type的对象应该如何被创建和销毁? 这个涉及到构造函数、构造函数以及operator new, operator delete, operator new[], operator delete[]。
- 对象的初始化和对象的赋值该有什么样的差别?这个主要是要考虑构造函数和赋值操作符的行为。
- 新type的对象如果被passed by value,意味着什么?
- 什么事新type的“合法值”?这点主要是要注意类型的成员变量和合法性检查。
- 你的新type需要配合某个继承图系吗?如果你继承其他的类,需要考虑它们的函数virtual和non-virtual性。如果你的类可能被继承就需要注意你声明的函数——尤其是析构函数。
- 你的新type需要什么样的转换?如果你的类型需要转换成其他的类型,需要考虑定义显式转换或者隐式转换的函数。
- 什么样的操作符和函数对此新type而言是合理的?参考条款23,24,26.
- 谁该取用新type的成员?这个可以使用者的角度来帮助你判断哪一些成员为public,哪个为protected。
- 你的新type有多么一般化(generic)?如果很一般化,可以考虑设计一个class template。
- 你真的需要一个新type吧?如果只是定义新的derived class以便为既有的class添加功能,那么说不定单纯定义一个或多个non-member函数或template更能够达到目标。
这些问题并不是那么容易回答,在设计一个类型的时候尽量的去考虑这些问题能够更好地设计出一个易用合理的类型。
条款20:宁以 pass-by-reference-to-const 替换 pass-by-value
- 尽量以pass-by-reference-to-const 替换 pass-by-value。前者通常比较搞笑,并可以避免切割问题(slicing problem)。
- 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对于它们而言,pass-by-value往往比较适当。
C++默认的传递对象的方法是by-value的方式,当一个对象有很多的成员变量或者有很深的继承结构。那么对于这个对象的构造和销毁会带来很大的开销。这种情况下用pass by reference的方式就能够节省下这种开始。加上const是避免对传入的参数进行修改。
Pass by value的另一个问题就是对象切割的问题。如果一个子类对象以pass-by-value的形式传递到一个函数中来,那么它的对象将会被切割而失去子类特有的成员变量,而且同时也会失去多态性。pass-by-reference可以解决这个问题,因为reference往往以指针实现,所以截断对象也同时会保有多态性。
决定一个对象pass by value还是pass by reference的依据不是简单地类型的成员数量,还要考虑这个类型在拷贝的时候是否会做一些比较昂贵的操作。
有一些例外,即内置类型,以及STL的迭代器和函数对象。除此之外 pass-by-reference-to-const 优于 pass-by-value。
条款21:必须返回对象是,别妄想返回其reference
- 绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。
返回一个pointer或者reference指向一个local stack对象明显是不合理的,因为local stack的生命周期在函数返回的时候就已经结束了,那么返回的那个指针或者引用将会指向一块被销毁的对象的内存。
指向一个heap-allocated的对象也是不合理的,因为你把销毁对象的任务移交给了使用函数的人,很多情况下人们会忘记。或者说所写的代码并没有让他有机会去销毁这个对象。比如说:
1 | const Rational& operator * (const Rational& lhs, |
只有在类似于单例的情况下,返回一个reference指向一个static的对象是合理的。比如说条款4中FileSystem的例子。
条款22:将成员变量声明为private
- 切记将成员变量声明为private,这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
- protected 并不比public更具封装性。
为什么public不好?
public的成员变量破坏了类的封装性,它将成员直接暴露给客户,那么在未来修改字段的时候就会影响到使用者。
为什么protected也不好?
有一个原则,即某些东西的封装性与“其内容改变时可能造成的代码破坏量”成反比。如果从这个原则出发的话,protected虽然比public好,但是也并没有好太多。因为如果说你这个类型是对外暴露的,你不能阻止用户继承你这个类型,然后去访问其中的成员变量(C++暂时没有类似于final class的概念)。所以说protected的成员变量也不好。