一、让接口容易被正确使用,不易被误用
接口设计的原则是,方便日后和其他用户的使用,不要把问题留给接口使用者
(1)用常规的用法调用“特别”设计的接口。所以需要尽可能的把自己的设计往常规上靠:数据对象的行为要尽可能符合内建对象(比如int)的行为;接口的名字和意义要尽可能一致(比如STL中的容器基本都有一个叫做size的返回容器大小的接口)……这样做鼓励用户去正确的看待和使用你的接口。
(2)忘了处理调用接口后的遗留问题。因此不要让用户去“记得”做一些事情。
如设计一个接口返回一个指向某新建对象的指针,该接口的用户需要“记得”去释放这个指针所指的对象:如果用户忘了释放或释放了好几次,后果SB了
解决的办法之一是让该接口返回一个智能指针,这样用户用完了就可以“忘记”这个指针:它自己会处理后事。
(3)所谓的“跨DLL问题”(cross DLL problem):在一个DLL中new一个对象,然后对象被传到另外一个DLL里被delete。大师推荐用shared_ptr因为它解决了这个问题。
代价:额外对象的创建和销毁需要时间空间。比如boost的shared_ptr就是普通指针的两倍大小,还有额外的对象操作时间+过程动态内存分配等。
二、设计class犹如设计type
(1)小心设计类的创建和销毁方式,及构造函数和析构函数。
(2)认真考虑如何区分类的构造函数和copy构造函数、赋值(assignment)操作符。即初始化与赋值的差别。
(3)注意实现类的传值(passed by value)。这个实际上是在说要注意拷贝构造函数的实现。
(4)需要审视类所在的继承体系。
如果该类有父类,那么必定要受到父类的一些限制,特别是函数是否为虚构;如果该类有子类,那么就要考虑是不是一些函数需要定义为虚函数,比如说析构函数。
(5)谨慎实现类对象与其他类型对象的转换。这一点稍有些复杂:如果有将T1转换为T2的需求,就有隐式转换和显式转换两种方式。
对于前者,可以编写一个(隐式的)转换函数,或者是通过额外编写一个T2的构造函数来实现T1向T2的转换。
对于后者,Scott说写一个(显式的)转换函数就可以了。
(6)·需要考虑该类需要参与哪些运算。很明显,如果需要参与A运算就要相应定义类的A运算符函数。大师在这里提的另外一点是,这些运算符号函数有些应该是成员函数,有些不应该。
(7)不要提供不应该暴露的标准函数。这里的标准函数指的是构造/析构/拷贝等等可能由编译器“自愿”为你生成的函数,如果不希望它们中的一些被外界调用,就声明为私有(private)。
(8)注意设计类成员的访问权限。公有(public)、保护(protected)、私有(private)应该用哪一种?有没有需要定义友元?或者是干脆来一个类中类?都需要考虑。
三、pass by referrence to const 替换 pass by value
C++传递对象的时候默认是传值的(pass-by-value),而这样的传递自然是昂贵的:这当中包含了临时对象的构造/析构,以及临时对象中的对象的构造/析构,运气背点还可能有对象中的对象中的对象的构造/析构。
(1)相对于传“值”,一个更好的替代方法是传“const引用”(pass-by-reference-to-const)。
(2)传值与传指针的一个区别是,通过传值传递的对象并不是原来的对象,而是一个复制品,所以随便你打它骂它,真身都不会受到影响。
(3)而通过传指针的对象和原来的对象就是同一家伙,改动一个另外一个也受到相同的影响。而这有时候并不是我们想要的结果。
(4)考虑到传值代价太高,传“const引用”就成了一个很好的替代品。
(5)传“const引用”的另外一个好处在于避免了“剥皮问题”(slicing problem,侯捷大师的版本是“对象切割问题”,我用这个中文名字是为了更容易记住:))
用传值方式传参的函数,如果某参数的类型是一个父类对象,而实际传递的参数是一个子类对象,只有该对象的父类部分会被构造并传递到函数中,子类部分的成员,作为父类对象的“皮”,就被血淋淋的剥掉了……
而如果用传“const引用”方式,就没有这种惨无人道的状况:本来父类的指针就可以用来指向一个子类对象,天经地义。
例外:对于内置类型(bulit-in type)对象以及STL中的迭代器、函数对象,Scott还是建议使用传值方式传递,原因是他们本来就是被设计成适合传值传递的。
四、该换回对象时别返回它的reference
如果一个函数可能返回一个对原来不存在的对象的引用,那么函数就要自己去创建这个对象:要么在栈上(stack)要么在堆上(heap)。
(1)第一种情况中,函数中定义了局部对象,然后返回对该对象的引用。对象在函数结束后自动销毁,引用指向无效的地址。
(3)第二种情况,函数使用new动态创建了一个对象,然后返回对该对象的引用。粗看没有问题,因为这个返回的引用还是有效的。
但是细想就会发现:我们能确保这个对象被正确的收回(delete)吗?
反思:在类中重载运算符,默认以该类的对象调用其重载操作符,返回对改已经存在对象的引用没什么问题,所以与该条款描述的不符。
五、将成员变量声明为private
公有的成员对类的外部完全开放,而保护的成员对类的继承者完全开放。从封装的角度,只有两种访问级别:私有,及其他。其他不多说。
六、 用 non-member、non-friend 替换 member 函数
相比较Java和c#,C++允许类外定义非成员函数,存在即合理,必要时可以祭出
从灵活性上来说,非成员函数更少编译依赖(compilation dependency),也就更利于类的扩展。
例:一个类可能有多个成员函数,可能有一个函数需要A.h,另外一个函数要包含B.h,那么在编译这个类时就需要同时包含A.h和B.h,也就是说该类同时依赖两个头文件。
如果使用非成员函数,这个时候就可以把这些依赖关系不同的函数分别写在不同的头文件中,有可能这个类在编译时就不需要再依赖A.h或是B.h了。
把这些非成员函数分散定义在不同头文件中的同时,需要用namespace关键字把它们和需要访问的类放在一起。
// code in class_a.h namespace AllAboutClassA { class ClassA { // ..}; // .. } // code in utility_1.h // .. namespace AllAboutClassA { void WrapperFunction_1() { // ..}; // .. } // .. // code in utility_2.h // .. namespace AllAboutClassA { void WrapperFunction_2() { // ..}; // .. } // ..
如果有需要添加新的非成员函数,我们要做的只是在相同的名字空间中定义这些函数就可以,那个类丝毫不会被影响,也即所谓的易扩展性吧。
对于类的用户来说,这样的实现方式(指用非成员函数)自由度更大