把资源放进对象内,便可以依赖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_
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
表达式那么C++会按照什么顺序来执行上面几个步骤呢?这是不定的,唯一可以确定的就是new Widget
会在std::shared_ptr
之前调用,但是priority什么时候调用,这是不一定的.如果priority在new Widget
之后在std::shared_ptr
之前调用,此时如果priority发生了异常将会导致资源泄露.因此为了避免上述问题应该将上面的语句分离出来.
shared_ptr支持定制删除器,这可以防范DLL问题,可被用来自动解除互斥锁.
DLL问题,就是cross-DLL problem,这个问题发生于,对象在动态链接库(DLL)中被new创建,却在另外一个DLL内被delete销毁.在许多平台上这一类夸DLL之new/delete成对运用,会导致运行期错误.shared_ptr不存在这个问题,因为shared_ptr可以定制删除器,那个删除器缺省是来自于shared_ptr所在的DLL中
让接口容易被正确使用,不易被误用
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.如果这里换成引用或者指针,那么代价就少很多了.
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即可.
综上所述最好不要妄想返回一个reference,可以返回一个value,编译器会帮我们优化的,避免临时对象的构造和析构陈本.
面向对象守则要求,数据以及操作数据的那些函数应该绑在一起.一些类相关的便利函数,不适合成为member函数,正确的做法应该是放在同一个namespace中,只要一些核心机能作为成员函数,这样可增加类的机能扩充性,包裹弹性.减少编译的依赖性.如果把一大堆便利函数放在类的内部,作为成员函数,那么修改或者增加都会导致要重新编译整个类,有些便利函数还不怎么经常使用.因此应该只把那些每个用户都会经常使用的函数作为类的成员函数.
如果你需要为某个函数的所有参数进行类型转换,那么这个函数必须是个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会隐式转换为Rational类
Rational 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;
异常安全性编程的脊柱,缺省情况下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已经分析完毕,让我们总结一下吧: