Effective C++
____________________________________________________________
条款18:让接口容易被正确使用,不易被误用
C++在接口之海漂浮.function接口,class接口,template接口.....每一种接口都是客户与你的代码互动手段.假设你面
对的是一群讲道
理的人,那些客户企图把事情做好.他们想要正确的使用你的接口.这种情况下如果他们对任何其中一个
接口的用法不正确,你至少也得
负一部分责任. 理想上,如果客户企图使用某个接口而却没有获得他所预期的行为,
这个代码不该通过编译,如果代码通过编译,它的
作为就该是客户想要的.
欲开发出一个"容易被正确使用,不容易被误用"的接口,首先必须考虑客户可能做出什么样的错误.假设你为一个用来
表现日期的class
设计的构造函数:
class Date{
public:
Date(int month, int day, int year);
...
};
Date d(30, 3, 1995); //应该是3.30 而不是30,3
Date d(2, 30, 1995); //应该是 3.30 而不是 2.30
我们知道会有人这样不小心把参数传传错,比如客户使用了错误的顺序传递进来参数.这样写的接口就太搓了,许多客户
端错误,可以因为导入新类型而获得预防.真的,在防范"不值得拥有的代码"上,类型系统是你的主要同盟国.既然这样
,就让我们导出简单的外覆类型来区别天数,月份,和年份. 然后再与Date构造函数中使用这些类型,
struct Day
{
explicit Day(int d)
:val(d)
{}
int val;
};
struct Month
{
explicit month(int m)
:val(m)
{}
int val;
};
struct Year
{
explicit Year(int Y)
:val(Y)
{}
int val;
};
class Date{
public:
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)); //传参正确
Date d(Day(30),Month(3),Year(1995)); //错误参数类型不匹配
Date d(Month(3), Day(30), Year(1995)); //传参正确
令Day,Month和Year成为成熟且经充分锻炼的classes并封装其内数据,比简单使用上述的structs好.但即使structs也已
经足够示范明智而审慎地导入新类型对预防"接口被误用"有神奇的疗效. 请记住这类方法.
接下来还有一种情况,条款13当中表明客户如何将一个函数的返回值存储于一个智能指针如auto_ptr或shared_ptr内部,
因而将delete责任推给智能指针.但万一客户忘记使用智能指针了怎么办? 这个时候一个好的接口就非常容易使用了.
令该函数的返回值为智能指针,客户不得不把这个返回值装进智能指针.
shared_ptr<T> fun();
这就是强制客户将返回值存储在一个shared_ptr当中,几乎消弭了忘记删除底部返回的对象.
总结:
好的接口很容易被正确使用,不容易被误用.你应该在你的所有接口中努力达到这些性质.
促进正确使用 的办法包括接口的一致性,以及与内置类型的行为兼容.
"阻止误用"的办法包括建立新的类型,限制类型上的操作,舒服对象值,以及消除客户的资源管理责任.
shared_ptr支持定制型删除器,这可防范DLL问题,可被用来自动解除互斥锁等等.
条款19:设计class犹如设计type
目前为止我写的class都是一些平淡平庸的,所以对设计class可能没有什么见解. 以后会补充.
条款20:宁以pass-by-reference-to-const替换pass-by-value
缺省情况下C++以by value方式传递对象至函数. 除非你另外指定,否则函数参数都是以实际实参的复件为初值,而调
用端所获得的
亦是函数返回值的一个复件.这些复件系由对象的copy构造函数产出,这可能使得pass-by-value成为昂
贵的操作.考虑一下class继
承体系.
class Person
{
public:
Person();
virtual ~Person();
...
private:
string name;
string address;
};
class Student :public Person
{
public:
Student();
~Student();
...
private:
string schoolName;
string schoolAddress;
};
现在考虑下面的代码,其中调用函数validateStudent,后者需要一个student实参并返回它是否有效:
bool validateStudent(Student s); //函数以by value方式接受学生
Student plato;
bool platoIsOK = validateStudent(plato);//调用函数
当上述函数调用之后会发生什么事情呢?
无疑地Student的copy构造函数会被调用,以plato为蓝本将s初始化.同样明显地,当validateStudent返回s会被销毁.
因此对于函数而言,参数的传递成本是"一次student copy构造函数调用,加上一次student析构函数调用"
但是这还不是整个故事喔。student对象内有两个string对象,所以每次构造一个studenet对象也必须沟槽出两个
string对象
。此外student对象继承Person对象,所以每次又得构造出来一个person对象,每个person对象里面又有
两个string对象
所以最终by-value方式传递一个Student对象会导致一次student copy构造函数,一次person copy构
造函数,四次string copy构造函数
当函数的那个stduent复件被销毁时,每个构造函数都要对应相应的析构函数调用
动作. 因此,by-value传递一个student对象,总的成本为
"六次构造函数和六次析构函数"!
这是正确且值得拥有的行为,毕竟你希望你的所有对象都能被确实地构造和析构.但尽管如此,如果有什么办法可以
回
避所有那些构造和析构
调用动作就太好了,还真的有: 就是pass by reference-to-const
bool validateStudent(const Student& s);
总结:
尽量以pass-by-reference-to-const替换pass-by-value. 前者通常比较高效,并可避免切割问题.
以下规则并不适用于内置类型,以及STL的迭代器和函数对象,对他们而言,pass-by-value往往比较适当.
条款21:必须返回对象时,别妄想返回其reference
一旦程序员领悟了,pass-by-value的效率牵连层面,往往变成十字军战士,一心一意根除pass-by-value带来的种种
邪恶.
在坚定追求pass-by-reference的纯度当中,他们一定会犯致命错误:开始传递一些reference指向其实并不存在
的对象.
首先如果任何一个函数返回一个reference指向某个local对象,都将一败涂地, local的危害相信大家都遭受
过
还有当你返回一个reference指向heap,这个时候,谁该对你new出来的对象delete.
最后还有人提出了,让operator*返回的reference指向一个被定义为函数内部的static Rational对象,听起来还不
错.
不过看下面这种情况->
举个例子:
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
static Rational result; //static对象,此函数将返回其reference
result = ...; //将lhs乘以rhs,并将结果置于result中.
return result;
}
感觉返回一个static对象很高效的样子,那么到底是不是这个样子呢?我们来看看它其中的问题.
bool operator ==(const Rational& lhs, const Rational& rhs);
Rational a, b, c, d;
...
if ((a * b) == (c * d))
{
...
}
else
{
}
如果这样调用,表达式if ((a * b) == (c * d))总是被核算为true,不论a,b,c和d的值是什么!
一旦代码重写为等价的函数形式,很容易可以可以了解出了什么意外:
if (operator ==(operator*(a, b), operator*(c,d)))
的
static Rational对象
因此operator== 被要求将"
operator*内部定义的static Rational对象
"拿来和"
operator*内部
定义
的
static Rational对象"比较,如果结果
不相同,那么才是不对的呢.
这应该足够说服你,在一定要返回对象时不要
返回
一个对象的引用.
总结:
绝不要返回pointer或者reference指向一个local stack对象,或返回reference指向一个head-allocated对象,或返回
pointer或reference指向一个
local static对象而有可能同时需要多个这样的对象.
条款22:将成员变量声明为private
切记将成员变量声明为private,这可赋予客户访问数据的一致性,可细微划分访问控制,允诺约束条件获得保证.
protected并不比public具有封装性. 要么private要么public.
条款23:宁以non-member,non-friend替换member函数
请记住:宁可拿non-member、non-friend函数替换member函数。这样做可以增加封装性、包裹弹性和机能扩充性。
推崇封装的原因:它使我们能够改变事物而只影响有限客户。愈少代码可以看到数据(也就是访问它),愈多的数据可
被封装,而我们也就愈能自由地改变对象数据。愈多函数可以访问它,数据的封装性就愈低。
导致较大封装性的是non-member、non-friend函数,因为它并不增加“能够访问class内之private成分”的函数数量
namespace和classes不同,前者可跨越多个源代码文件而后者不能。
分离它们的最直接做法就是将书签相关便利函数声明于一个头文件,将cookie相关便利函数声明于另一个头文件,再将
打印相关便利函数声明于第三个头文件。将所有便利函数放在多个头文件内但隶属同一个命名空间,意味客户可以轻松
扩展这一组便利函数。
条款24:若所有参数皆需类型转换,请为此采用non-member函数.
总结:
如果你需要为某个函数的所有参数进行类型转换,那么这个函数必须是个non - member