========================
Effective C++ 设计与声明
书作者:Scott Meyers
原笔记作者:Justin
========================
Item 18 : 接口应该容易被正确使用,不易被误用
--------------------------------------------------
tag:消除客户的资源管理责任 tr1::shared_ptr cross-DLL problem
用户使用接口时却没有获得预期的行为,这个代码不应该通过编译。
用错的可能有:
·调用接口时输入了错误的参数。
如(一个接受年、月、日为参数的接口函数,用户可以轻易给出各种错误的输入),
解决办法:用对象来约束参数输入的范围(不接受简单的整数作为输入,而是Date、Mon、Year对象)
struct Day{ explicit Day(int d):val(d){} int val;}
class Month{
public:
static Month May(){ return Month(5); }
private:
explicit Month(int m);
}
Date d(Month::Mar(),Day(20),Year(1995));
·用常规的用法调用“特别”设计的接口。所以需要尽可能的把自己的设计往常规上靠:数据对象的行为要尽可能符合内建对象(比如int)的行为;接口的名字和意义要尽可能一致(比如STL中的容器基本都有一个叫做size的返回容器大小的接口)……这样做鼓励用户去正确的看待和使用你的接口。
·忘了处理调用接口后的遗留问题。因此不要让用户去“记得”做一些事情。
如设计一个接口返回一个指向某新建对象的指针,该接口的用户需要“记得”去释放这个指针所指的对象:如果用户忘了释放或释放了好几次,后果就是@#¥%
解决的办法之一是让该接口返回一个智能指针(嗯……印象模糊了?去看Item14),这样用户用完了就可以“忘记”这个指针:它自己会处理后事。
·所谓的“跨DLL问题”(cross DLL problem):在一个DLL中new一个对象,然后对象被传到另外一个DLL里被delete。大师推荐用shared_ptr因为它解决了这个问题。
代价:额外对象的创建和销毁需要时间空间。比如boost的shared_ptr就是普通指针的两倍大小,还有额外的对象操作时间+过程动态内存分配等。
实际上有些底层代码根本没这个资本提供这样的“豪华装备”,不过有这样的思想还是很重要D……
Item 19 :设计class犹如设计type
----------------------------------------
tag:class design
·小心设计类的创建和销毁方式。比如说Item8和Item16
·认真考虑如何区分类的构造函数和赋值(assignment)操作符。即初始化与赋值的差别。
·注意实现类的传值(passed by value)。这个实际上是在说要注意拷贝构造函数的实现。
·切勿忽略类对非法输入的处理。其实是要注意各种出错情况,是否需要抛出异常以及如何实现异常处理。
·需要审视类所在的继承体系。
如果该类有父类,那么必定要受到父类的一些限制,特别是函数是否为虚构;如果该类有子类,那么就要考虑是不是一些函数需要定义为虚函数,比如说析构函数。
·谨慎实现类对象与其他类型对象的转换。这一点稍有些复杂:如果有将T1转换为T2的需求,就有隐式转换和显式转换两种方式。
对于前者,可以编写一个(隐式的)转换函数(参考Item15里面的隐式转换咯~),或者是通过额外编写一个T2的构造函数来实现T1向T2的转换。
对于后者,Scott说写一个(显式的)转换函数就可以了。(同样,在Item15里也有显式转换函数的例子)
·需要考虑该类需要参与哪些运算。很明显,如果需要参与A运算就要相应定义类的A运算符函数。大师在这里提的另外一点是,这些运算符号函数有些应该是成员函数,有些不应该。原因在Item23、24、26【555我还没看到,留空】
·不要提供不应该暴露的标准函数。这里的标准函数指的是构造/析构/拷贝等等可能由编译器“自愿”为你生成的函数,如果不希望它们中的一些被外界调用,就声明为私有(private)。没印象了?降级到Item6重新学习~
·注意设计类成员的访问权限。公有(public)、保护(protected)、私有(private)应该用哪一种?有没有需要定义友元?或者是干脆来一个类中类?都需要考虑。
·认真审查类的隐性限制。性能上的要求、使用资源的限制或是出错时的处理都有可能影响到类的具体设计和实现。
·谨慎考虑类的适用范围。也就是说如果某个设计会可能用在很多方面,适用于许多不同的实际对象。也许这个时候你需要设计的不是一个类,而是一个类模板。
最后一点其实应该放在第一位:你真的需要定义一个类吗?如果仅仅是在继承某类的基础上加一两个成员,是不是非成员函数或模板就已经够了捏?
第19招其实更像是个check list,在准备动手设计之前,一一比对打勾划叉,应该可以提前避免很多人间惨剧……
Item 20 : 用 传const引用 替换 传值
-------------------------------------------
tag: const引用 值传递
C++传递对象的时候默认是传值的(pass-by-value),而这样的传递自然是昂贵的:这当中包含了临时对象的构造/析构,以及临时对象中的对象的构造/析构,运气背点还可能有对象中的对象中的对象的构造/析构……(有好的不学,去学C@#¥%)
·相对于传“值”,一个更好的替代方法是传“const引用”(pass-by-reference-to-const)。
·传值与传指针的一个区别是,通过传值传递的对象并不是原来的对象,而是一个复制品,所以随便你打它骂它,真身都不会受到影响。
·而通过传指针的对象和原来的对象就是同一家伙,改动一个另外一个也受到相同的影响。而这有时候并不是我们想要的结果。
·考虑到传值代价太高,传“const引用”就成了一个很好的替代品。
·传“const引用”的另外一个好处在于避免了“剥皮问题”(slicing problem,侯捷大师的版本是“对象切割问题”,我用这个中文名字是为了更容易记住:))
用传值方式传参的函数,如果某参数的类型是一个父类对象,而实际传递的参数是一个子类对象,只有该对象的父类部分会被构造并传递到函数中,子类部分的成员,作为父类对象的“皮”,就被血淋淋的剥掉了……
而如果用传“const引用”方式,就没有这种惨无人道的状况:本来父类的指针就可以用来指向一个子类对象,天经地义。
例外:对于内置类型(bulit-in type)对象以及STL中的迭代器、函数对象,Scott还是建议使用传值方式传递,原因是他们本来就是被设计成适合传值传递的。
(个人观点:大师说:“……it's not unreasonable to choose pass-by-value。”,注意这里有句潜台词:其实对以上类型用传“const引用”方式传递也是可以的。)
如果你认为上面两种情况可以用传值传递是因为它们,比如说内置类型对象,的大小本来就小,进而得出小数据类型就可以用传值传递,就打错特错了。小对象的构造/析构过程完全可能很恐怖。
再退一步,哪怕某个类型很小,它的构造/析构函数也简单到可以忽略不计,我们还是不能以此断定可以用传值传递这种类型的对象:因为编译器往往会做出一些蠢事。书中的一个例子是,对于一些编译器可以接受把一个double类型对象存入寄存器,但是如果你给它一个只有一个double成员的对象交给它,它却拒绝将该对象存入寄存器。(什么事让编译器插一手,不是问题也有了问题……)
最后还有个理由,虽然某对象现在很小,可是随着社会的发展人类的进步,有可能两年后它就会变成一个庞然大物,到时候用传值也会变得不合适。
·除了内置类型和STL的迭代器、函数对象外,其他的对象传递时,用传“const引用”代替传值吧。
Item 21: 该换回对象时别返回它的reference
---------------------------------------------------
tag:
如果一个函数可能返回一个对原来不存在的对象的引用,那么函数就要自己去创建这个对象:要么在栈上(stack)要么在堆上(heap)。
第一种情况中,函数中定义了局部对象,然后返回对该对象的引用。对象在函数结束后自动销毁,引用指向无效的地址。
第二种情况,函数使用new动态创建了一个对象,然后返回对该对象的引用。粗看没有问题,因为这个返回的引用还是有效的。
但是细想就会发现:我们能确保这个对象被正确的收回(delete)吗?
书中举了一个很好的例子:一个*运算符函数,接受两个乘法运算数,返回一个积。
如果在函数中动态创建一个对象来存储乘积,并返回对这个新对象的引用,那么下面的计算就会带来内存泄漏:
Y=A*B*C
因为在这个“连续”乘法中,有两个“乘积对象”被创建,但是我们丢失了第一次乘法创建的对象的指针。
所以这样的做法是不妥的。
也许大师有被问过:那么对于第一种情况我们可不可以返回一个静态(static)对象的引用?书中用了同样的例子来回答:NO。
if (A*B == C*D) {//..}
如果返回静态对象的引用,上面的判断语句永远得到true值,因为“==”号两边的运算结果是指向同一块数据的引用。
不知道是不是后面又有刨根问题的学生追问:那我能不能用一个静态对象的数组来存放不同此运算的结果?大师懒得举例子了,我猜想也没必要:这样的方案带来的副作用及其开销本身就已经大于原来要解决的问题了吧?
不要尝试在函数中返回对局部对象(存储于栈)的引用,也不要对动态创建的对象(存储于堆)做同样的蠢事,而如果真有打算、非常渴望、十分想要返回对静态对象的引用,在这么做之前也要考虑清楚会不会有上面例子中的情况出现。
至少,返回对象不会有上面列出的种种错误危险,仅仅是有可能带来创建额外对象的开销而已,而这个开销的可能还有可能被排除,如果你用的编译器足够聪明的话。
Item 22: 将成员变量声明为private
-----------------------------------------------------
如果数据成员都是私有的,那么访问这些成员就只能通过函数进行。于是用户就不需要费心考虑到底要用什么方式去访问数据成员:因为只有定义了的函数可以用。
通过定义数据成员为私有,可以实现函数来设计、约束或禁止对这些成员的各种访问(读/写等)。而如果将其设为公有(public),你将无法得知你的成员会被谁改动,也不知道会是怎样的改动。
而更重要的好处是封装(encapsulation):可以方便的通过修改函数来改变成员的访问方式;在成员被访问时通知其他对象;实现多线程中的同步等等。
封装的好处究其本质,是通过对用户隐藏数据成员来保证类行为的一致性(class invariant)。因为接口被成员访问函数限制了,类的作者也为自己日后修改类的实现留了后路:如果所有的成员都是公有的,对任何代码的修改都有可能影响到外界的使用。(因此Scott说“Public means unencapsulated, and practically speaking, unencapsulated means unchangeable, especially for classes that are widely used.”)
那么可不可以声明为保护(protected)呢?其实道理和前面的公有是一样的。公有的成员对类的外部完全开放,而保护的成员对类的继承者完全开放。这个就像两个区间:(-infinity, +infinity) 和 (0, +infinity),两者的大小是一样的。
从分装的角度,只有两种访问级别:私有,及其他。
Item 23: 用 non-member、non-friend 替换 member 函数
------------------------------------------------------
从面向对象的角度来看,非成员函数更有利于数据的封装。
一个数据成员被越少的代码访问到,该成员的封装程度就越高。越少函数可以直接访问一个数据成员,该成员的封装程度就越高。
类的数据成员应该定义为私有。如果这个前提成立,那么能够访问一个数据成员的函数便只能是该类的成员函数,或是友元函数。
于是为了更好的封装数据,在可以完成相同功能的前提下,应该优先考虑使用非成员并且非友元函数。
这里的“非成员并且非友元函数”是针对数据成员所在的类而言的,也就是说这个函数完全可以是其他类的成员,只要是不能直接访问那个数据成员就可以。
从灵活性上来说,非成员函数更少编译依赖(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() { // ..};
// ..
}
// ..
这样一来,虽然这些非成员和类不“住在”一个头文件里,它们的“心”还是在一起的(在同一个名字空间, namespace, 中)。
如果有需要添加新的非成员函数,我们要做的只是在相同的名字空间中定义这些函数就可以,那个类丝毫不会被影响,也即所谓的易扩展性吧。
对于类的用户来说,这样的实现方式(指用非成员函数)就更加合理:
因为作为类的用户,需要扩展类的时候又不能去修改别人的类(版权?安全性?或者根本就没有源码?),就算是通过继承该类的方式也不能访问父类的私有数据。
Item 24: 若所有参数皆需类型转换,为此函数采用 non-member 函数
------------------------------------------------------
tag: operator
若需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,这个函数应该设为 non-member.
---------------------
class Rational{
public:
Rational(int num = 0, int denominator = 1); //构造函数刻意不为 explicit; 允许 int-to-Rational 隐式转换
...
const Rational operator* (const Rational& rhs) const;
}
Rational oneHalf(1, 2);
result = oneHalf * 2; //right → result = oneHalf.operator*(2);
result = 2 * oneHalf; //wrong! → result = 2.operator*(oneHalf); → result = operator*(2, oneHalf);
---------------------
const Rational operator*(const Rational& lhs, const Rational& rhs){..} //non-member function.
若函数不该成为member,不一定要成为friend.
Item 25: 考虑写出一个不抛异常的 swap 函数
------------------------------------------------------
tag: 异常处理
·当 std::swap 对你的类型效率不高时,提供一个 swap 成员函数,并确定函数不抛出异常。
·如果你提供一个 member swap,也该提供一个 non-member swap 用来调用前者,
对于 classes(非 templates),请特化 std::swap;
·调用 swap 时应针对 std::swap 使用using 声明式,然后调用 swap 并不带任何“命名空间资格修饰”。
·为“用户定义类型”进行std templates 全特化是好的,但千万不要尝试在 std 内加入某些对 std 而言全新的东西。
std::swap的缺省实现:用了一个中间临时对象,然后两两交换。
缺省的方法很简单,而在一些情况下却也很耗资源:比如需要交换的是一个很复杂庞大的对象时,创建/拷贝大量数据会使得这种swap的效率显得非常低下。
--------------------------
更加适应的实现思路:
在类/模板类(class/class template)中定义一个公有的swap成员函数,这个函数负责实现真正的交换操作。同时,这个函数不能抛出异常。
用成员是因为交换操作中可能会需要访问/交换类的私有成员;用公有(public)来限制是为了给下面的辅助函数(wrapper function)提供接口。
至于不能抛出异常,有两个原因:
一是Item29中所提到的异常安全性(exception-safety)需要不抛出异常的swap来提供保障(更多细节就到拜读29条的时候再记录吧。)
二是一般而言高效的swap函数几乎都是对内置类型的交换,而对内置类型的操作是不会抛出异常的。
1. 如果需要使用swap的是一个类(而不是模板类),就为这个类全特化std::swap,然后在这个特化版本中调用第一步中实现的swap函数。
class AClass{
public :
void swap(AClass & theOtherA){
using std::swap; // 这一句会在稍后的第3点提到
// 通过调用swap来完成该类的特有交换动作
}
// ..
}
namespace std{
// 在std名字域内定义一个全特化的swap
template <> // 这样的定义说明是全特化
void swap < AClass > ( AClass & a, AClass & b){
a.swap(b);
}
}
如此一来,用户可以直接应用同样的swap函数进行交换操作,而当交换对象是需要特殊对待的AClass对象时,也可以无差别的使用并得到预期的交换结果。
2. 如果我们需要交换的是模板类,那么就不能用全特化std::swap的方法了,偏特化的std::swap也行不通,因为:
C++中不允许对函数进行偏特化(只能对类偏特化),也 就是说不能写出下面的程序:
namespace std{
// illegal code as C++ doesn't allow partial specialization for function templates
template<typename T>
void swap< AClassTemplate<T> >(AClassTemplate<T>& a, AClassTemplate<T>& b)
{
a.swap(b);
}
}
std名字空间中的内容都是C++标准委员会的老大们定义的,为了保证std内部代码的正常运作,不允许往里头添加任何新的模板、类、方程,重载也不可以。
虽然可以像1.那样写出全特化的模板函数,但是企图在std的名字空间添加以下重载的swap(这种重载变相实现了函数的偏特化)(虽然你可以通过编译,但是会埋下隐患):
namespace std{
template <typename T>
void swap (AClass<T>& a, AClass<T>& b)
{ a.swap(b);}
}
给自己的一个小提醒:因为函数名swap后没有<>,所以不是偏特化,而是对
namespace std{
template<class _Ty> inline
void swap(_Ty& _X, _Ty& _Y)
{/*..*/}
}
的重载而已。
基于上面两个原因,一个变通的方法是在该模板类所在的名字空间中编写一个 非成员的函数模板来调用这个公有的接口:
namespace AClassSpace{
template <typename T>
void swap (AClass<T>& a, AClass<T>& b)
{ a.swap(b);}
}
在限定的名字空间中实现函数,是为了避免“污染”全局的名字空间。而且,不同的名字空间都可以使用一样的函数名字而不会有冲突。
基于前面第23课所学,使用非成员函数也是应该的了。
至于为什么要函数模板,那就匪常D简单:因为要交换的是模板@#¥%
pimpl也即pointer to implementation,当需要交换两个复杂且臃肿的对象时,可以先用两个指针分别指向着两个对象
之后对这些对象的操作,包括交换,就只需要通过这两个指针来进行(交换两个指针的值便实现了对象的交换)。
posted on 2010-03-15 22:50
Euan 阅读(454)
评论(0) 编辑 收藏 引用 所属分类:
C/C++