Effective C++ — 类的设计与声明

来源:互联网 发布:大力水手 知乎 编辑:程序博客网 时间:2024/06/14 16:57

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,3Date 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)); //传参正确

令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对象,此函数将返回其referenceresult = ...; //将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)))

注意,在operator==被调用之前,已经有两个operator*作用式起作用了,每一个都返回了一个指向operator*内部定义

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













原创粉丝点击