<>读书笔记(二)

条款13 以对象管理资源

把资源放进对象内,便可以依赖C++的析构函数自动调用机制,确保资源被释放.两个步骤:
* 获得资源后立刻放进管理对象
* 管理对象运用析构函数确保资源被释放

这种以对象管理资源的观念被资源获得世纪便是初始化时机也就是所谓的RAII机制.可以为资源专门创建一个类来管理,也可以把资源交由一些智能指针来管理.下面是一个使用RAII机制管理的互斥锁资源:

#ifndef LIB_MUTEX_H_
#define LIB_MUTEX_H_

#include <iostream>
#include <pthread.h>
#include <boost/noncopyable.hpp>

//mutex资源
class Mutex:boost::noncopyable
{

public:
    Mutex():m_mutex(PTHREAD_MUTEX_INITIALIZER)
    {
            //do nothing
    }   

    ~Mutex()
    {
        pthread_mutex_destroy(&m_mutex);
    }

    int Lock()
    {
        return pthread_mutex_lock(&m_mutex);
    }

    pthread_mutex_t *get_mutex()
    {
        return &m_mutex;
    }

    int Unlock()
    {
        return pthread_mutex_unlock(&m_mutex);
    }

private:
    pthread_mutex_t m_mutex;
};

//资源管理类 RAII class
class MutexLockGuard:boost::noncopyable
{
    public: 
        MutexLockGuard(Mutex &mutex):m_mutex(mutex) {
            m_mutex.Lock();
        }

        ~MutexLockGuard() {
            m_mutex.Unlock();
        }
    private:
        Mutex& m_mutex;
};
#endif //end of LIB_MUTEX_H_

条款14 在资源管理类中小心copying行为

  • 复制RAII对象,必须一并复制它所管理的资源,所以资源的copying行为决定了RAII对象的copying行为
  • 普遍而常见的RAII class copying行为是,抑制copying,施行引用技术法.

条款15 在资源管理类中提供对原始资源的访问

  • 一些函数或者是API往往要求访问原始资源,所以每一个RAII class应该提供一个所管理之资源的办法.
  • 对原始资源的访问可能经由显示转换或隐式转换,一般而言显式转换比较安全,但隐式转换对客户比较方便.

条款17 以独立语句将newed对象置入对象智能指针

int priority();
void processWidget(std::shared_ptr<Widget> pw,int priority);

对于上面两个函数接口来说,如果使用下面的方法来调用:

processWidget(new Widget,priority());

上面的调用存在问题,processWidget要求传入的是一个shared_ptr类型的参数,尽管一个Widget可以隐式转换为shared_ptr类型,但是shared_ptr的这个构造函数是explicit,因此上面的调用会失败.把上面的调用改成下面的形式:

processWdget(std::shared_ptr<Widget>(new Widget),priority());

现在上面的调用不会出错了,表面上看起来是很好的.但是实际上这种调用方式会导致泄露资源.上面的调用会先计算好参数,然后将参数传递给函数再调用.因此,上面的调用分为以下几步:

  • 执行new Widget表达式
  • 调用std::shared_ptr构造函数
  • 调用priority

那么C++会按照什么顺序来执行上面几个步骤呢?这是不定的,唯一可以确定的就是new Widget会在std::shared_ptr之前调用,但是priority什么时候调用,这是不一定的.如果priority在new Widget之后在std::shared_ptr之前调用,此时如果priority发生了异常将会导致资源泄露.因此为了避免上述问题应该将上面的语句分离出来.

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

  • shared_ptr支持定制删除器,这可以防范DLL问题,可被用来自动解除互斥锁.
    DLL问题,就是cross-DLL problem,这个问题发生于,对象在动态链接库(DLL)中被new创建,却在另外一个DLL内被delete销毁.在许多平台上这一类夸DLL之new/delete成对运用,会导致运行期错误.shared_ptr不存在这个问题,因为shared_ptr可以定制删除器,那个删除器缺省是来自于shared_ptr所在的DLL中

  • 让接口容易被正确使用,不易被误用

条款19 设计class犹如设计type

  • 新type的对象应该如何被创建和销毁
  • 对象的初始化和对象的赋值该有什么样的差别
  • 新type的对象如果被passed by value意味着什么?
  • 什么是新type的合法值
  • 你的新type需要配合某个继承图系
  • 你的新type需要什么样的转换
  • 什么样的操作符和函数对此新type而言是合理的
  • 什么样的标准函数应该驳回?
  • 谁该取用新type的成员?
  • 什么是新type的未声明接口?
  • 你的新type有多么一般化?
  • 你真的需要一个新type吗?

条款20 宁以pass-by-reference-to-const替换pass-by-value

  • pass-by-value会导致无意义的数据成员拷贝,效率低.
class person {
public:
    person();
    ~person();
private:
    string name;
    string address;
};

//函数按照pass-by-value的形式接受person
bool validatePerson(person s);
person plato;
bool platoIsOK = validatePerson(plato); 
//这段代码无疑会导致调用person的拷贝构造函数,对形参s进行初始化,然后等待函数运行结束后,s被销毁,在s被拷贝构造的同时,其内部
//维护的name和address同时也会被拷贝.可想而知这代价是巨大的,毕竟这个函数并不需要去修改plato.如果这里换成引用或者指针,那么代价就少很多了.
  • pass-by-value会导致对象切割问题
class Window {
public:
    std::string name() const;
    virtual void display() const;
};

class WindowWithScrollBars:public Window {
public:
    virtual void display() const;
};

现在编写一个函数用来打印窗口名称:

void printNameAndDisplay(Window w)
{
    std::cout << w.name();
    w.display();
}
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb)

很显然上面的代码产生了切割问题,形参w,会利用wwsb构造成一个Windows对象.为了避免这个问题使用pass-by-reference-const即可.

  • 尽量以pass-by-reference-to-const替换pass-by-value,前者通常比较高效,并可以避免切割问题.
  • 以上规则并不适用于内置类型,以及STL的迭代器和函数对象,对它们而言pass-by-value往往比较适当.

条款21 必须返回对象时,别妄想返回其reference

  • 返回一个指向local stack对象的reference,当函数执行完毕,local stack对象会被析构,这个reference会失效
  • 返回一个指向函数内部的heap对象的reference,那么会增加构造函数调用的成本,还要承担负责析构这个head对象的责任
  • 返回一个指向local static对象的reference,如果需要再多个线程中调用这个函数,返回reference,那么需要考虑线程安全问题.

综上所述最好不要妄想返回一个reference,可以返回一个value,编译器会帮我们优化的,避免临时对象的构造和析构陈本.

条款22 将成员变量声明为private

  • 切记将成员变量声明为private,这可赋予客户访问数据的一致性,可细微划分访问控制,允诺约束条件获得保证.并提供class作者以充分的实现弹性.
  • protected 并不比public更具封装性

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

面向对象守则要求,数据以及操作数据的那些函数应该绑在一起.一些类相关的便利函数,不适合成为member函数,正确的做法应该是放在同一个namespace中,只要一些核心机能作为成员函数,这样可增加类的机能扩充性,包裹弹性.减少编译的依赖性.如果把一大堆便利函数放在类的内部,作为成员函数,那么修改或者增加都会导致要重新编译整个类,有些便利函数还不怎么经常使用.因此应该只把那些每个用户都会经常使用的函数作为类的成员函数.

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

如果你需要为某个函数的所有参数进行类型转换,那么这个函数必须是个non-member函数.具体原因见如下分析:

class Rational {
public:
    Rational(int numberator = 0,
             int denominator = 1);
    int numerator() const;
    int denominator() const;
private:
    .....
}

如果此时给这个类添加一个乘法运算,operator* 假设先作为类的成员函数,写法如下:

class Rational {
public:
    ....
    const Rational operator* (const Rational& rhs) const;
};

目前看起来貌似没什么大问题,来使用一下吧:

Rational one(1,8);
Rational two(1,2);
Rational result = one * two; //运行正常
Rational result2 = one * 2; //很好, 2会隐式转换为RationalRational result3 = 2 * one; //错误. 整数2并没有对应的class,也没有operator*成员函数.

通过上面的使用可以看出这里的确存在使用问题,不满足交换律,可想而知,operator*作为类的成员函数其实并不适合.现在我们来看下,operator*作为nn-member函数.

const Rational operator* (const Rational& lhs,
                          const Rational& rhs)
{
    return Rational(lhs.nymerator() * rhs.numerator(),
                    lhs.denominator() * rhs.denominator());                          
}

现在上面这个函数可以满足交换律了,代码如下:

Rational result2 = one * 2;
Rational result3 = 2 * one;

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

异常安全性编程的脊柱,缺省情况下swap动作可由标准程序库提供的swap算法,下面是swap算法的典型实现如下:

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

类型T需要支持copy构造函数,和copy assignment操作符.通过上面的代码可以看到缺省的swap实现十分平淡.如果遇到下面的情况缺省的swap效率是很低下的,当要交换的类是以pimpl手法构建的时候,例如下面这个类:

一个impl手法手机的数据成员类.

class WidgetImpl {
  public:
    .....
  private:
    int a,b,c;
    std::vector<double> v;
    ....
}

真正的Widget类

class Widget {
  public:
    Widget(const Widget& rhs);
    Widget operator=(const Widget& rhs) {
        ....
        *pImpl = *(rhs.pImpl);
    }
  private:
    WidgetImpl *pImpl;
};

对于上面这个类来说,缺省的swap算法会赋值三个Widget对象,还会复制三个WidgetImpl对象.效率底下,上面的代码很容易就可以看出其实只要交换两者的WidgetImpl指针即可.但是默认的swap算法不知道.因此为了让swap更加高效,我们必须特化swap.

namespace std {
    template<>  //swap模板特化
    void swap<Widget>(Widget& a,
                       Widget& b)
    {
           swap(a.pImpl,b.pImpl); //调用标准的swap算法 
    }
}

很可惜上面的代码无法通过编译,因为pImpl是私有数据,所以这里需要把这个特化的swap函数变成成员函数,
或者是friend函数.下面是一种解决的办法:

class Widget {
    public:
        void swap(Widget& other){
            using std::swap;
            swap(pImpl,other.pImpl);
        }
};

namespace std {
    template<>
    void swap<Widget>(Widget& a,
                       Widget& b)
    {
        a.swap(b);                   
    }
};

设置swap成员函数,然后使用全局的特化版本的swap函数调用Widget类的swap成员函数即可.这种做法不只能够通过编译,还和STL容器有一致性.
现在如果把Widget模板化如下:

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

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

一如既往,在Widget模板类中添加一个swap成员函数,然后偏特化(这里是偏特化,而不是全特化)全局的swap,在全局的swap中调用Widget模板类中的
swap成员函数.

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

可惜上面的偏特化遇到了问题,C++只允许对class template偏特化, 在function template中偏特化是行不通的.
为此只能使用函数重载解决这个问题.下面是重载版本的swap

namespace std {
    template<typename T>
    //注意这里的swap没有swap<Widget> 进行偏特化
    void swap(Wdget<T>& a,
               Widget<T>& b)
    {
        a.swap(b);
    }
}

一般而言上面这种写法已经可以解决问题了,但是很可惜其命名空间是std,这个命名空间是一个特殊的命名空间,其管理规则比较特殊.客户可以全特化
std内的templates,但是不可以添加新的templates到std里面,std里面的内容完全由C++标准委员会决定. 那该如何是好?为此我们可以使用一个
non-member swap让它调用member swap,但是不让这个non-member的swap声明为std::swap的特化版本和重载版本.

namespace WidgetStuff {
    ....
    class Widget {
        public:
            Widget(const Widget& rhs);
            Widget operator=(const Widget& rhs) {
                ....
                 *pImpl = *(rhs.pImpl);
            }
        private:
            WidgetImpl *pImpl;
    };
    template<typename T>
    class swap<Widget<T>& a,
               Widget<T>& b)
    {
        a.swap(b);           
    }
}

当我们调用swap函数的时候按照C++的名称查找规则总是会先查找到WidgetStuff命名空间下的swap函数.从而使这个问题得到了解决.
到此为止swap已经分析完毕,让我们总结一下吧:

  • 提供一个public swap成员函数,让它高效地置换你的类型的两个对象值.
  • 在你的class或template所在的命名空间内提供一个non-member swap ,并令它调用上述swap成员函数.
  • 如果你正确编写一个class,为你的class特化std::swap,并令它调用你的swap函数.
    最后,如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数内曝光.然后不加任何namespace修饰符,赤裸裸的调用swap.

你可能感兴趣的:(C++,读书笔记,C语言,effective)