Item18 Make interfaces easy to use correctly and hard to use incorrectly.让接口容易被正确使用,不易被误用
好的接口设计应该是既容易被正确使用,又不易被误用。
就例如书中的Sample,关于时间的,我们一般的做法就是在创建Day对象时,追加校验函数来判断年月日是不是有效。
建议的做法一是:创建新的类型
定义:
class Month {
public:
static Month Jan() {return Month(1); }
static Month feb() {return Month(2); }
…
private Month(int m);
};
Date d(Month::Mar(), Day(30), Year(1995) );
为啥不直接使用静态变量?
参考Item4 P30,简单说就是不能保证在使用non-local static objects时,这个对象就已经初始化了。如果non-local static objects在另一个文件了,又恰巧没有初始化,系统当然就会翘辫子了。
另一种方法:加上const来限制type可以做的事情。
先参考Item3 P19
class Rational { …};
const Rational operator*(const Rational &lhs, const Rational &rhs);
之所以强制设置为const就是为了避免client在使用时出错。因为如果没有clien,那么:
Rational a,b,c;
…
(a*b)=c
这种写法是对的,但是如果a,b是内置类型,这种写法就是错误的。
除非有必要,否则就要保证你的类型type的行为和内置类型一致。
一致性导致接口容易被正确使用。
STL是榜样,java在这里成了反面教材,因为如果想知道容器内对象的数量,用Array,要访问属性length,String要用length函数,ArrayList要用size函数,这就是不一致性。
使用std::tr1::shared_ptr,消除client对资源管理的责任
找出以下写法的两个易错的地方:
Investment* createInvestment();
1 忘记删除createInvestment()返回的指针
2 删除这个指针多次
so,修改定义:
std::tr1::shared_ptr< Investment > createInvestment();
如果出现这种情形:从createInvestment得到Investment*的函数要把这个指针传递个给叫做getRidOfInvestment,由getridOfInvestment取代使用delete。
这里就出现了一个新的client易错的点,用户或许会使用错的资源释放机制。因为delete被getRidOfInvestment取代了。
std::tr1::shared_ptr< Investment >
pInv(static_cast<Investment*>(0), getRidOfInvestment);
那么定义就应该是这样的:
std::tr1::shared_ptr< Investment > createInvestment()
{
std::tr1::shared_ptr< Investment >
retVal(static_cast<Investment*>(0), getRidOfInvestment); //这不能让client来做
retVal = …;
return retVal;
}
tr1::shared_ptr的优点是允许在一个DLL创建对象,在另一个DLL里删除对象。
牢记
- Good interfaces are easy to use correctly and hard to use in correctly.
- 接口一致性,于内置数据类型的行为兼容
- 阻止错误的方式还包括创建新的类型(例如Month),限制类型上的操作,束缚对象值,以及消除客户的资源管理的责任
- tr1::shared_ptr支持定制类型的删除器deleter,允许在一个DLL创建对象,在另一个DLL里删除对象。
Item19:Treat class design as type design.设计class犹如设计type
在设计一个类的时候,要回答一系列的问题哦。
参考大师在P85-P86之间给出的常常的清单吧,其实实际上,我在设计类的时候的确没有想过这么多,问过自己这么的为什么,所以这也是我总是在追求代码重用,却总是发现自己写的代码重用度很低的一个原因把。
Item20:Prefer pass-by-reference-to-const to pass-by-value.宁以pass-by-reference-to-const替换pass-by-value
But先学习一个单词,characteristic,KAO,这竟然是个名词。
再学一个地道的说法:解决问题的方法:The way around the slicing problem is…
函数都是值传递。pass by-value。function parameters are initialized with copies of the actual arguments, and function callers goes back a copy of the value returned by the function.这样当然开销就大了,每次都先copy一份进来,完事以后,再copy一份出去。
假设函数的参数是一个Student对象,bool validateStudent(Student s);调用这个函数,额外的隐性开销包括要先调用copy constructor创建一个Student对象用于函数内部,函数执行结束再调用析构函数释放这个对象。
开销太大了,改进一下:pass by reference-to-const
bool validateStudent(const Student& s);
引用是通过指针来实现实现的,因此传递引用实际上就是在传递指针。references are typically implemented as pointers.
但是这个规则对于内置数据类型不适用,也不是适用STL iterator和函数对象function objects。
即使再小的对象也应该不要使用值传递,而是要使用pass by reference-to-const。
关于slicing problem的另一种描述
slicing problem是在多态规则里面容易产生的。
看一个简单的基类、派生类的定义
class Window
{
public:
int height;
int width;
};
class TextWindow : public Window
{
public:
int cursorLocation;
};
…
Window win;
TextWindow *tWinPtr;
tWinPtr = new TextWindow;
win = *tWinprt;
win是一个Window对象,C++规定:给win分配的内存看见的大小,由其静态类型决定。就是说默认的拷贝函数导致信息会出现丢失。这就是slicing problem。
试想一下这要是通过值传递的方式传递参数,实参一copy就已经丢失信息了。
牢记
- Prefer pass-by-reference-to-const over pass-by-value.这样既有效,又可以避免slicing problem。
- 但是这个规则对于内置数据类型,STL iterator和函数对象function objects不适用。对于它们传递值就好了。
Item 21:Don't try to return a reference when you must return an object.必须返回对象时,别妄想返回其reference
heap and stack
堆和栈这是2个不同的概念,哎哟,我一直以为是一个词。
heap:堆
- 栈是系统提供的功能,特点是快速高效,缺点是有限制,数据不灵活;
- 堆是函数库内部数据结构,不一定唯一。
- 堆空间的分配总是动态的,虽然程序结束时所有的数据空间都会被释放回系统,但是精确的申请内存/释放内存匹配是良好程序的基本要素。
- 使用new创建的对象是在heap上分配内存空间。
stack:栈
- 而堆是函数库提供的功能,特点是灵活方便,数据适应面广泛,但是效率有一定降低。
- 栈是系统数据结构,对于进程/线程是唯一的;不同堆分配的内存无法互相操作。
- 栈空间分静态分配和动态分配两种。静态分配是编译器完成的,比如自动变量(auto)的分配。动态分配由alloca函数完成。栈的动态分配无需释放(是自动的),也就没有释放函数。为可移植的程序起见,栈的动态分配操作是不被鼓励的!
- 定义的局部变量是在stack上分配内存空间的。
牢记
Item22:Declare data members private.将成员变量声明为private
why data members shouldn’t be public?
argument
(against) 争论,意见
实参
形参是parameters
protected data member is mot more encapsulated than public one.
牢记
- data member一定要封装。
- protected不必public有更好的封装。
Item 23:Prefer non-member non-friend functions to member functions.宁以non-member、non-friend替换member函数
这是一个典型的例子:
class WebBrowser {
public:
…
void clearCache();
void clearHistory();
void removeCookies();
…
};
为了提供一个执行所有操作的函数,所以就在WebBrowser里面追加定义:
void clearEverything();
哎,我一直就是这么写的,并自以为有很好的封装,But
void clearBrowser(WebBrowser wb)
{
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
第一:前者并不比后者有很好的封装
这就要解释一下什么叫做“封装”?以及封装的判别标准。
封装的判别标准:可以通过统计能够访问这个data的函数的数目来计算,函数越多,这个data封装也就月不好,因此前一种写法的封装就没有后者好。这也可以用来解释Item22里面,为什么要求数据成员不能定义为public。另外增加clearEverything()作为member function,实际上是降低了封装性。而后面的non-member non-friend functions的定义就没有改变WebBrowser的封装性。
第二:后者还能提供更加灵活的打包package,增加扩展性。
put all convenience functions in multiple header files, but one namespace.
第三:增加函数的可扩展性。
你可以定义自己的convenience functions,写到一个header file里面,放到同一个namespace里面。这是member function做不到的。
牢记
- 优先使用non-member non-friend函数来替换member函数。
Item 24:Declare non-member functions when type conversions should apply to all parameters.若所有参数皆需类型转换,请为此采用non-member函数
原因:
Parameters are eligible for implicit type conversion only if they are listed in the parameter list.
结论:
make operator* a non-member function, thus allowing compilers to perform implicit type conversions on all arguments.
class Rational {
…
};
const Rational operatior*(const Rational& lhs, Rational& rhs)
{
return Rationan(lhs.numerator()*rhs.numerator(),
lhs.denominator()*rhs. denominator () );
}
一个误区:
如果一个函数,和某个类相关,而又不能定义成member,那么这个函数就一定要定义成friend。
上面这个例子就说明这个说法并不正确。真爱生命,慎用friend functions。
Item 25:Consider support for a non-throwing swap.考虑写出一个不抛异常的swap函数
- default swap
就是指std里面定义的swap
- member swap
- nonmember swap
- specializations of std::swap
member swap
Widget::我们希望的是交换指针,但swap实际做的是不仅copy了3个Widget对象,而且还copy了3个WidgetImpl对象。太浪费了!都低碳时代了。
class Widget{
void swap(Widget& other)
{
using std::swap;
swap(pImpl, other.pImpl;);
}
…
};
template<> void swap<Widget>( Widget& a, Widget&b)
{
a.wap(b);
}
nonmember swap
接下来要讨论的是如果Widget和WidgetImpl不是类而是类模板会怎么样?
约束条件:不能在std里面增加新的template,只能特化(specialize)std内的template。
如果非要定义,say sorry。behavior is undefined。KAO,其实这比异常还讨厌。
解决方法是把它定义到一个自己的namespace里面,而不要定义到std里面。
namespace WidgetStuff {
…
template<typename T>
class Widget{…};
…
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
specializations of std::swap
如果仅仅是针对一个class,那就特化std::swap。
If you want to have your class-specializing version of swap called in as many contexts as possible, you need to write both a non-member version in the same namespace as your class and a specialization of std::swap.
这部分十分绕,P111还对于C++的name lookup的规则进行了详细的描述。值得重新温习。