《Effective C++》学习笔记(六)

来源:互联网 发布:江宁广电网络 编辑:程序博客网 时间:2024/06/02 11:52

原创文章,转载请注明出处:http://blog.csdn.net/sfh366958228/article/details/38922567


前言

今天学的条款都是出自于《设计与声明》这一张,那么,什么是软件设计呢?书上如是解释:是“令软件做出你希望它做的事情”的步骤和做法,通常以颇为一般性的构思开始,最终演变成十足得细节,以允许特殊接口的开发。


条款18:让接口容易被正确使用,不易被误用

C++中拥有各种接口,function接口、class接口、template接口,每一种都是客户与你代码互动的手段。理想上,如果客户企图使用某个接口而却没有获得他所预期的行为,这个代码是不该通过编译的,而如果代码通过了编译,它的作为就该是客户所想要的。

我们来看一下一个用来表示日期的class类的构造函数:

class Date{public:    Date(int year, int month, int day);    ...}
粗略看上去,这个构造函数通情达理,但是它的客户很容易会犯下至少两个错:

1、以错误的次序传递参数,比如传的顺序为月、日、年

2、传第一个无效的月份或者天数,比如传一个13月,或者传一个2月30日

不要觉得这样不可能发生,一旦通过编译客户就可能以为代码是正常的。

最简单的解决办法是用简单的外覆类型来区别天数、月份、年份:

struct Day{    explicit Day(int d):val(d){ }    int val;}
struct Month{    explicit Month(int d):val(d){ }    int val;}
struct Year{    explicit Year(int d):val(d){ }    int val;}

class Date{public:    Date(const Year &y, const Month &m, const Dat &d);    ...}Date date(Year(1995), Month(3), Day(30));

这个办法简单的解决了传值次序问题,但如果令Year、Month、Day成为成熟且经充分锻炼的class并封装其内数据,比简单使用上述的struct好。一旦正确地类型就定位,限制其值有时候是通情达理的。例如一年只有一个12个有效月份,所以Month应该反映这一事实。

办法之一是利用enum表现月份,但enum不具备我们希望拥有的类型安全性,例如enum可以拿来当一个int用,比较安全的解法是预先定义所有有效的Month:

class Month {public:    static Month Jan() { return Month(1)};    static Month Feb() { return Month(2)};    ...    static Month Dec() { return Month(12)};private:    explicit Month(int m);    ....}Date date(Year(1995), Month::Mar(), Day(20));
预防客户错误的另一个办法是:限制什么类型内什么事可做,什么事不能做。常见得限制是加上const。

下一个是另一个一般性准则”让types容易被正确使用,不容易被误用“的表现形式:除非有好理由,否则尽量让你的types的行为与内置types的行为一致。比如a和b都是int,那么对a*b赋值并不合法,一旦对一致性有所怀疑,可以拿int做范本参考。

任何接口如果有要求客户必须做的某些事情,就有着”不正确“使用的倾向。比如一个工厂函数,返回一个指针指向Investment继承体系内的动态分配对象:

Investment* createInvestment();
为了避免资源泄漏,create返回的指针最终必须被删除,但这样可能会照成两个错误:没有删除指针,或者删除同一个指针两次。

许多时候,较佳接口的设计原则是先发制人,就是令工厂函数直接返回一个智能指针。

如果对于返回的指针有特殊的删除处理函数,可以利用shared_ptr自定义删除器实现,如:

std::shared_ptr<Investment> createInvestment(){    std::shared_ptr<Investment*> retVal(static_cast<Investment*>(0), myDelete());    retVal = ...; // 令retVal指向正确对象    return retVal;}
之所以在0之前要加static_cast转换是因为0是一个int,虽然它可以用作一个指针,但是此情况下不够好。如果被retVal指向的原始指针可以在建立之前确定下来,将原始指针传给retVal构造函数会比讲retVal初始化为Null后再做一次赋值好。

shared_ptr的另一个好特性就是:它会自动使用它的专属删除器,不会有DLL问题。


总结:

1)好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。

2)”促进正确使用“的办法包括接口的一致性,以及与内置类型的行为兼容。

3)”阻止误用“的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。

4)shared_ptr支持定制型删除器。这可以防范DLL问题,可被用来自动解除互斥锁。


条款19:设计class犹如设计type

C++和其他OOP(面向对象编程)语言一样,当你定义一个新class,也就定义了一个新type。

身为C++程序猿,你的许多时间主要用来扩张你的类型系统。这意味着你并不只是class设计者,还是type设计者。重载、函数、操作符、控制内存的分配和归还、定义对象的初始化和终结...全部在你手中。

因此你应该带着和”语言设计者当初设计语言内置类型时“一样的谨慎来研讨class的设计。

那么,如何设计高效的class呢?首先你必须了解你面对的问题。几乎每一个class都要求你面对以下提问,而你的回答往往导致你的设计规范:

  • 新type的对象应该如何被销毁?这会影响到class的构造函数、析构函数、内存分配函数(new)和释放函数(delete)
  • 对象初始化和对象的赋值该有什么样的差别?初始化是调用构造函数,而赋值是调用赋值操作符
  • 新的type的对象如果被passed by value,意为着什么?copy构造函数用来定义一个type的值传递如何实现
  • 什么是新type的”合法值“?对于class成员变量而言,通常只有某些值是有效地,它将影响函数跑出的异常等
  • 你的新type需要配合某个继承图系吗?如果你继承自其它class,就要受到它们的设计约束,特别是它们的函数是virtual或non-virtual的影响。如果你允许其它class继承你的class,那么回影响你声明的函数,尤其是析构函数是否为virtual。
  • 你的新type需要配合什么样的转换?
  • 什么样的操作符和函数对此type而言是合理的?
  • 什么样的标准函数应该被驳回?有些函数必须声明为private
  • 谁该取用新type的成员?帮你决定哪个成员是public,哪个是protected,哪个是private。它也帮你决定哪个class或者functions应该是friend、
  • 什么是新type的”未声明接口“?
  • 你的新type有多么一般化?或许你需要定义的是一组相似的type家族,如果是这样你应该定义一个新的class template
  • 你真的需要一个新type么?如果只是添加一个派生类以便为现有的class添加机能,或许可以定义一个或多个非成员函数或模板。

总结:
Class的设计就是type的设计。在定义一个新的type之前,请确定你已经考虑过本条款覆盖的所有讨论主题。


条款23:宁以non-member、non-friend替换member函数

如果有个类用来表示浏览器,在这个class 中可能提供的总舵函数中,有一些用来清除高速缓存、清除访问过的 URL 的历史纪录、以及移除系统中的所有 cookies:

class WebBrowser{    ...    void clearCache();    void clearHistory();    void removeCookies();        void clearEverything(); // 调用clearCache、clearHistory、removeCookies    ...}void clearBrowser(WebBrowser &wb){    wb.clearCache();    wb.clearHistory();    wb.removeCookies();}

我们可以写成clearEveryting、也可以写成clearBrowser。那么,哪一个比较好呢?

面向对象守则要求,数据以及操作数据的那些函数应该捆绑在一起,这意外它建议member函数是较好的选择。不幸的是这个建议并不正确。

面向对象守则要求数据应该尽可能被封装,然后与直观相反的,member函数clearEverything带来的封装性比non-member函数clearBrowser低。

另外,提供non-member函数可允许对webBrowser相关技能有较大的包裹弹性(packaging flexibility),而那最终导致较低的编译相依度,增加了WebBrowser的可延伸性。

先聊聊封装。如果某些东西被封装,它就不再可见。越多东西被封装,越少人可以看到它,我们也就越有弹性去变化它。

现在考虑对象内的数据。越少代码可以看到数据,越多的数据可被封装。能够访问private成员变量的函数只有class的member函数加上friend函数。如果要在一个member函数和一个non-member non-friend函数之间作抉择,而且两者提供相同的机能,那么封装性大的是non-member non-friend函数。因为它并不增加“能够访问class内的private成分”的函数数量。

在这一点上有两件事情得注意:

1)这个论述只应用于non-member non-friend函数。

2)只因在意封装性而然函数“成为class的non-member”并不意味着它不可以是其它class的member。

在C++中,比较自然的做法是让clearBrowser成为一个non-member函数并位于WebBrowser所在的同一个namespace(命名空间)内:

namespace WebBrowserStuff{    class WebBrowser { ... }    void clearBrowser(WebBrowser &wb);    ...}
然而不止是为了看起来自然而已,namespace和class不同,前者可以跨越多个源码文件而后者不可以。

一个像WebBrowser这样的class可能会有大量的便利函数,有点与书签有关,有的与打印有关,有的与cookie的管理有关,但是大多数客户可能只对其中某些感兴趣。

分离它们的最直接做法就是将书签相关得便利函数声明在一个头文件中,将cookie相关得头文件声明在另一个头文件中,再将打印的相关遍历函数声明在第三个头文件中,依次类推。

这正是C++标准程序库的组织方式。将所有便利函数放在多个头文件但是隶属同一个命名空间,意味客户可以轻松扩展这一组便利函数,他们需要做的就是添加更多的non-member non-friend函数到此命名空间中。

总结:

宁可拿non-member non-friend函数替换member函数。这样做可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性。


条款24:若所有参数皆需类型转换,请为此采用non-member函数

虽然很多时候是不提倡进行隐式转换的,但是如果有足够的理由,为什么不这么做?例如将整数“隐式转换”成有理数合理。

class Rational{public:    Rational(int numerator = 0, int denominator = 1); // 允许隐式转换    int numerator() const; // 分子分母的访问函数    int denominator() const;    const Rational operator* (const Rational &rhs) const;private:   ...}Rational oneEighth(1, 8);Rational oneHalf(1, 2);Rational result = oneHalf * oneEighth;result = result * oneEighth;
我们的类中实现了乘法的算术运算,在调用的时候也一切正常,可以将两个有理数轻松的相乘。但是你希望支持混合运算,比如支持int相乘。
result = oneHalf * 2; // 正确result = 2 * oneHalf; // 错误
为什么第二种是错的呢?我们可以把第二种形式重写成:
result = 2.operator*(oneHalf);
整数2没有对应的class也就没有operator*成员函数。为什么第一种有2对了呢?因为它发生了隐式转换。

只有当参数被列于参数列内,这个参数才是隐式类型转换得合格参与者,地位相当于“被调用之成员函数所隶属的那个对象”——即this对象——的那个隐喻参数,绝不是隐式转换的合格者。

然后如果你一定要第二种形式行得通的话,让operator*成为一个non-member函数。

class Rational{   ... // 里面没有operator*};const Rational operator*(const Rational &lhs, const Rational &rhs){    ...}

总结:

如果你需要为某个函数的所有参数(包括被this指针所指得那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。


条款25:考虑写出一个不抛异常的swap函数

swap是个有趣的函数。原本它只是STL的一部分,后来成为异常安全性编程的脊柱,以及用来处理自我赋值可能性的一个常见机制。

所谓swap两对象,意思是将两对象的值彼此赋予对方。默认情况下swap动作由std标准程序库中的swap算法完成。

namespace std{    template<typename T>    void swap(T &a, T &b)    {        T temp(a);        a = b;        b = temp;    }}

只要类型T支持copying,默认的swap实现对吗就会帮你置换类型为T的对象,你不需要再做任何工作。

但是对于某些类型而言,这些复制动作无一必要,比如“以指针指向一个对象,内含真正数据”那种类型。这种设计的常见表现形式是所谓的“pimp(pointer to implementation)手法”。如果以这种手法设计Widget,看起来会是这样:

class WidgetImpl{public:    ...private:    int a, b, c;    std::vector<double> v;    ...};class Widget{public:    Widget(const Widget &rhs);    Widget& operator=(const Widget &rhs)    {        ...        *pImpl = *(rhs.pImpl);        ...    }private:    WidgetImpl *pImpl;};
如果我们需要只换两个Widget对象值,事实上我们需要做的只是置换其pImpl指针,但是默认的swap算法不知道,它不只复制三个Widget,还复制三个WidgetImpl对象,非常缺乏效率。

将std::swap针对Widget特化是一个提高效率的办法。我们不能够改变std命名空间内的东西但可以为标准template制造特化版本,使它专属于我们自己的class。

我们先在Widget类中写一个名为swap的public函数实现置换工作。然后将std::swap特化,令它调用该成员函数。

class Widget{public:    ...    void swap(Widget &other)    {        using std::swap;        swap(pImpl. other.pImpl);    }};namespace std{    template<>    void swap<Widget><widget>(Widget &a, Widget &b)    {        a.swap(b);    }}</widget>
这种做法不只能通过编译,还与STL容器有一致性,因为所有STL容器也都提供有public swap成员函数和std::swap特化版本。

然而假设Widget和WidgetImpl都是class template,我们也可以将WidgetImpl内的数据类型加以参数化。

template<typename T>class WidgetImpl { ... };template<typename T>class Widget { ... };

在Widget中写个swap就像以往一样简单,但是我们却在特化std::swap时遇上乱流。

namespace std{    template<typename T>    void swap<Widget<T>> (Widget<T> &a, Widget<T> &b)    {        a.swap(b);    }}namespace std{    template<typename T>    void swap(Widget<T> &a, Widget<T> &b)    {        a.swap(b);    }}
上面两种特化版本都是错的。用户可以全特化std中的template,但是不能添加新的template。那怎么办呢?

答案很简单,我们还是声明一个non-member swap让他调用member swap,但不再将那个non-member swap声明为std::swap的特化版本或重载版本。

为求简化,我们将Widget的所有相关技能都置于命名空间WidgetStuff中。

namespace WidgetStuff{    ...    template<typename T>    class Widget    {        ...    }    ...    template<typename T>    void swap(Widget<T> &a, Widget<T> &b)    {        a.swap(b);    }}

现在这个做法对class和class template都行得通,但是有一个理由让你应该为class特化std::swap,所以如果你想让你的“class专属版”swap在尽可能多的语境下被调用,你需要同时在class所在命名空间内写一个non-member版本以及一个std::swap特化版本。

换位思考一下,如果你是一个客户,在使用swap的时候,究竟是调用哪个swap呢?

template<typename T>void doSomething(T &obj1, T &obj2){    using std::swap; // 令std::swap在此函数内可用    ...    swap(obj1, obj2); // 为T型函数调用最佳swap版本    ...}

在调用swap的时候千万不要加std::,因为它会让你永远调用std内的swap。

如果swap的默认代码对你的class和class template提供可接受的效率,你不需要额外做任何事。如果swap默认版本效率不足(几乎总是意味你的class或template使用了pimpl手法),你可以:

1)提供一个public swap成员函数,这个函数绝不该抛出异常。

2)让那个你的class或template所在的命名空间内提供一个non-member swap,并令它调用上述swap成员函数。

3)如果你正编写一个class,为你的class特化std::swap。并令他它调用你的swap成员函数。

最后,如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数中曝光可用。记得调用的时候不要画蛇添足的加std::。

成员版swap绝不可抛出异常,因为swap的一个最好的应用就是帮助class提供强烈的异常安全性保障。这个技术基于一个假设:成员版的swap绝不抛出异常。这一约束只施行于成员版,不可施行于非成员版,因为swap的默认版本是以copying为依据,而一般条件下都是允许抛出异常的。


总结:

1)当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。

2)如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于class,也请特化std::swap。

3)调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何"命名空间修饰"

4)为”用户定义类型“进行std template全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。

0 0