C++总结之设计和实现篇

C++类的设计是个难题,没有多年的经验是无法胜任的,虽然我们也能写出非常好的类。

这里依然是依照Effecitve C++第三版为主线进行的梳理。

如何设计一个类

如何设计一个类,需要回答如下问题:
1.新type的对象应该如何被创建和销毁?
这会影响到类的构造函数和析构函数以及内存分配函数和释放函数的设计。
2.对象的初始化和对象的赋值该有什么样的差别?
这个答案决定了类的构造函数和赋值操作符的行为,以及他们之间的差异。
3.新type的对象如何被pass by value,意味着什么?
涉及到拷贝构造函数如何设计。
4.什么是新type的“合法值”?
对于类的成员变量而言,通常只有某些数值集是合法的,那些数值集决定了类必须维护的约束条件(invariants),也决定了成员函数必须进行的检查工作。
5.你的新type需要配合某个继承图系吗?
如果你允许其他class继承你的class,那会影响你所声明的函数-尤其是析构函数-是否为virtual。
6.你的新type需要什么样的转换?隐式还是显式?
7.什么样的操作符&函数对此新type而言是合理的?

这个问题答案决定你将为你的class声明哪些函数。其中某些该是member函数,某些则否。
8.什么样的标准函数应该驳回?
那些正是我们必须声明为private的函数,如当一个类不允许存在副本的时候的赋值构造函数(std::auto_ptr)等。
9.什么是新type的“未声明接口”?
这方面的保证将为class实现代码加上相应的约束条件。
10.谁该取用新type的成员?
这个问题的回答决定了哪个成员是public,哪个为protected,哪个为private。也决定哪个 classes或者functions应该是friends。
11.你的新type有多么一般化?
或许我们其实并不需要定义一个新的type, 而是要定义一整个types家族。如果是这样,我们应该定一个新的class template,而非新的class。(我曾经傻乎乎的定义了针对不同类型的iterator,而还以为自己有多么高明,其实一个class template 足以)。
12.你真的需要一个新type吗?
如果只是定义新的dervied class以便为既有的class添加机能,那么说不定单纯定义一个或多个non-member函数或者template,更能够达到目标。
这个是我现在遇到的问题,有些时候真的不知道是否需要增加一个新的type,我一般倾向于增加新的type)。

 

让接口容易被正确使用,不易被误用
1.好的接口很容易被正确使用,不易被误用。

我们应该努力使我们的接口具有这种性质。“促进正确使用”的方法包括接口的一致性,以及与内置类型的的行为兼容。“阻止误用”的方法包括建立新的类型,限制类型上的操作,束缚对象值,以及消除客户的资源管理责任(如返回一个std::auto_ptr而不是指针)。
2.std::tr1::shared_ptr支持定制型删除器(custom delete)。

可以防范DLL问题,可以被用来自动解除互斥锁等等。

 

对象的传入和传出
1.尽量以pass-by-reference-to-const 替换 pass-by-value。
通常前者比较高效,并可能避免切割问题。但是对于内置类型,以及STL的迭代器和函数对象并不适用(它们使用pass-by-value往往比较恰当)。
2.当我们必须在“返回一个reference和返回一个object”之间进行抉择时,我们的工作就是找出正行为正确的那一个。
绝对不要返回pointer或reference 指向一个local stack 对象,或者返回reference指向一个heap-allocated对象,或者返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。(已经有人为在多线程环境中返回local static的reference付出了代价)。

 

数据的封装
1.切记将成员变量声明为private。
这可赋予客户访问数据的一致性,可细微划分访问控制,允诺约束条件获得保证,并提供class作者以充分的实现弹性。
2.protected 并不比public更有封装性。
因为某些东西的封装性与“当其内容改变时可能造成的代码破坏量”成反比。如果protected的成员变量有改变,那么他将影响所有使用它的derived classes的成员变量,这个也像public成员变量被更改一样,是个不可预知的量。
3.宁可拿non-member,non-friend函数替换member函数。
这样做可以增加封装性,包裹弹性和机能扩充性。
4.如果我们需要为某个函数的所有参数进行类型转换,那么这个函数必须是个non-member函数。
有理数类支持乘法交换律的示例可以体现这一点。(这一点的最根本的理论基础是:设计的类要符合现实模型)。

 

写出一个不抛出异常的swap函数
1.当std::swap对我们的类型效率不高时,可以提供一个swap成员函数,并确保这个函数不抛出异常。
2.如果我们提供了一个member swap,也应该提供一个non-member swap来调用它。对于classes(而非templates),需要特化std::swap。
3.调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“名字空间修饰”。
4.为“用户定义类型”进行std templates全特化是好的,但是千万不要尝试在std内加入某些队std而言全新的东西。(我加过,被批了!)。

 

实现过程中需要注意的细节
1.尽可能延后变量定义式的出现的时间。

这样做可以增加程序的清晰度并改善程序效率。对于循环体内的临时变量,如果确定classes的一个赋值成本低于一组构造+析构的成本,把classes声明到循环外比较高效,尤其是循环比较大的时候。否则需要声明到循环内。(鼓励声明到循环内,因为大多参与循环的取值操作,大部分的classes构造和析构成本都比较低,当然,如果使用引用,更是没有任何性能的损失)。
2.如果可能,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。
如果必须使用,试着发展无需转型的替换设计。
3.如果转型是必须的,设置将它隐藏于某个函数的背后。
客户随后可以调用该函数,而不需要将转型放进他们自己的代码内。
4.宁可使用C++-style转型,不要使用旧式转型。
前者很容易辨认,而且也比较有着分门别类的执掌。
5.避免返回handles(references,指针,迭代器)指向对象的内部。
遵守这个条款可以增加封装性,帮助const成员函数的行为像个const(就是概念上的const)。

 

C++的新式转型
C++除了像C语言那样可使用()进行转型外,还提供四种新式转型:
const_cast<T>(expression);
dynamic_cast<T>(expression);
reinterpret_cast<T>(expression);
static_cast<T>(expression);
1.const_cast
通常被用来将对象的常量性转除(cast away the constness)。它也是唯一有此能力的C++-style转型操作符。
2.dynamic_cast
主要用来执行"安全向下转型"(即基类向子类转型),它是唯一无法由旧工语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。
3.reinterpret_cast
意图执行低级转型,实际动作取决于编译器,也就不可移植。例如将一个整型指针转型为一个int,这种转型在低级代码外很少见,我们也很少用到。
4.static_cast
用来强迫隐式转换,可能是用得最多的,例如int转double,也可以将non-const转为const(与const_cast相反),还可以将void*转为其它类型的指针,将基类指针转为派生类指针。

 

关于异常安全
当异常被抛出时,带有异常安全的函数会:
A.不泄漏任何资源;B.不允许数据破坏。
异常安全函数(Exception-safe functions)提供三种保证之一:
1.基本承诺
如果异常被抛出,程序内的任何事情仍然保持在有效状态下。
2.强烈保证
如果异常被抛出,程序状态不改变。(这个比较难)
3.nothrow保证
承诺不抛出异常,以为它们总是能够完成它们原先承诺的功能。作用于内置类型身上的所有操作都提供nothrow保证。这是异常安全码中一个必不可少的关键基础材料。

对于异常安全函数,需要注意:
1.异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构被破坏。
2.“强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或者具备现实意义。
3.函数提供的“异常安全保证”通常最高只等于其所调用的各个函数的“异常安全保证”中最弱者。

 

关于inline
1.将大多说inline限制在小型,被频繁调用的函数身上。
这可使日后的调试过程和二进制升级更加容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
2.不要只因为function templates出现在头文件,就将它们声明为inline。

 

将文件的编译依存关系降至最低
只要将接口从实现总分离,就可以把文件的编译依赖关系降至最低。
编译依存性最小化的本质是:实现中让头文件尽可能自我满足,万一做不到,则让它与其它文件内的声明式(而非定义式)相依。
策略如下:
1.如果使用object references 或 object pointers可以完成任务,就不要使用objects。
2.如果能够,尽量以class的声明式替换class的定义式。
3.为声明式和定义式提供不同的头文件。

因此,我们需要:
1.支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。
基于此构想的两个手段是Handle classes和Interface classes。(大型商业项目构架中,这两种用的比较多)。
2.程序库头文件应该以“完全且仅有声明式”的形式存在。
看看STL吧--iosfwd文件。

 

 

主线:《Effective C++》 Third Edition

-- 转载请注明出处

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值