[C++]资源管理

资源管理

所谓资源就是,一旦使用了它,将来必须归还给系统!C++最常见的资源就是动态分配内存,如果不归还就会内存泄露。

1. 以对象管理资源

我们通常希望有一个对象来帮助我们解决资源管理的问题(自动调用析构函数),于是此章我们讨论auto_ptr和shared_ptr。

问题产生

假设我们希望使用一个工厂方法如:

class investment {...};  // 代表一个root class
investment* creatinvestment() {  //  返回一个指针指向继承体系内动态分配的对象。
    ...
}
void f() {
    investment* pInv = createinvestment();
    ...
    delete pInv;
}

乍看起来这个函数并没有什么问题,正常的动态分配了对象,同时也删除了该对象。但问题在于…中可能出现异常情况,例如,提前的return,异常抛出,某个循环的continue等等,使得控制流无法读到delete语句,如此便出现了内存泄露。所以“单纯地以来f总是会执行其delete语句”是行不通的。

解决问题

为了解决这个问题,我们希望能把资源放入对象中,使得对象在离开其作用域时自动调用析构函数。于是,我们使用了auto_ptr。

void f() {
    std::auto_ptr<investment> pInv(createinvestment());
    ...
}
//  经由auto_ptr的析构函数自动删除pInv。

这个简单的例子示范了“以对象管理资源”的两个关键想法:
* 获得资源后立刻放进管理对象内。此观念常被称为“资源取得时机便是初始化时机”(RAII准则),因为我们几乎总是在取得一笔资源后同一语句内以它初始化某个对象。
* 管理对象运用析构函数确保资源被释放。不管控制流如何离开作用域,一旦对象被销毁,其析构函数自然会被自动调用,于是资源被释放。

然而,使用auto_ptr也需要注意,一定不能让多个auto_ptr指向同一个对象!因为每一个auto_ptr都会使用析构函数,他并不像智能指针一样会管理有多少指针指向同一对象。并且与copy相关的操作都会使原来的指针指向null,如此使对象只有一个auto_ptr指向。

基于auto_ptr的特性,我们便不能子啊STL容器中使用auto_ptr。auto_ptr

void f() {
    std::shared_ptr<investment> pInv(createinvestment());
    ...
}
//  经由shared_ptr的析构函数自动删除pInv。

几乎和auto_ptr一样,但是相应的复制行为会正常了许多。

补充auto_ptr相关知识

C++的auto_ptr所做的事情,就是动态分配对象以及当对象不再需要时自动执行清理。
使用std::auto_ptr,要#include 。
以下是源码:

template<class T>
class auto_ptr
{
private:
    T*ap;
public:
    //constructor & destructor-----------------------------------(1)
    explicit auto_ptr(T*ptr=0)throw():ap(ptr)
    {
    }

    ~auto_ptr()throw()
    {
        delete ap;
    }
    //Copy & assignment--------------------------------------------(2)
    auto_ptr(auto_ptr& rhs)throw():ap(rhs.release())
    {
    }
    template<class Y>
    auto_ptr(auto_ptr<Y>&rhs)throw():ap(rhs.release())
    {
    }
    auto_ptr& operator=(auto_ptr&rhs)throw()
    {
        reset(rhs.release());
        return *this;
    }
    template<class Y>
    auto_ptr& operator=(auto_ptr<Y>&rhs)throw()
    {
        reset(rhs.release());
        return *this;
    }
    //Dereference----------------------------------------------------(3)
    T& operator*()const throw()
    {
        return *ap;
    }
    T* operator->()const throw()
    {
        return ap;
    }
    //Helper functions------------------------------------------------(4)
    //value access
    T* get()const throw()
    {
        return ap;
    }
    //release owner ship
    T* release()throw()
    {
        T* tmp(ap);
        ap = 0;
        return tmp;
    }
    //reset value
    void reset(T* ptr = 0)throw()
    {
        if(ap != ptr)
        {
            delete ap;
            ap = ptr;
        }
    }
    //Special conversions-----------------------------------------------(5)
    template<class Y>
    struct auto_ptr_ref
    {
        Y*yp;
        auto_ptr_ref(Y*rhs):yp(rhs){}
    };
    auto_ptr(auto_ptr_ref<T>rhs)throw():ap(rhs.yp)
    {
    }

    auto_ptr& operator=(auto_ptr_ref<T>rhs)throw()
    {
        reset(rhs.yp);
        return*this;
    }

    template<class Y>
    operator auto_ptr_ref<Y>()throw()
    {
        return auto_ptr_ref<Y>(release());
    }

    template<class Y>
    operator auto_ptr<Y>()throw()
    {
        return auto_ptr<Y>(release());
    }
};

shared_ptr

详情见:shared_ptr

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

前面我们提到了RAII观念,并以此作为“资源管理类”的记住,也描述了auto_ptr和shared_ptr如何将这个观念表现在heap-based资源上。然而并非所有资源都是heap-based,对那种资源而言,我们就需要建立自己的资源管理类。(栈,由编译器自动管理,无需程序员手工控制;堆:产生和释放由程序员控制。)

问题产生

我们处理类型为Mutex的互斥器对象,同时使用一个class来管理它。

void lock(Mutex* pm);
void unlock(Mutex* pm);

class Lock {
public:
    explicit Lock(Mutex* pm) : mutexPtr(pm) {
        lock(mutexPtr);
    }
    ~Lock() { unlock(mutexPtr); }
private:
    Mutex *mutexPtr;
};

main () {
    Mutex m;
    ...
    {
    Lock m1(&m);
    ...
    Lock m11(&m);
    Lock m12(m11);  // 将m11复制到m12身上,会发生事?
    }
}

当一个RAII对被复制,会发生什么事?以下是你的两种选择:
* 静止复制。许多时候允许RAII对象被复制并不合理。对一个像Lock这样的class却是可能的,因为很少能够合理拥有“同步化期初器物”的复件。如果复制对RAII class并不合理,就应该让该class继承uncopyable(见前文:)。
* 对底层资源使用“引用计数法”。有时候,我们希望保有资源,直到它的最后一个使用者被销毁,这种情况下复制RAII对象,该资源的引用计数递增。

问题解决

通常只要内含shared_ptr成员变量,就可以实现reference-counting copying行为。并且shared_ptr允许指定所谓的“删除器”(deleter)(一个函数或函数对象),当引用次数为0时便被调用,而不是执行析构函数。删除器对shared_ptr构造函数来说是可有可无的第二参数。

class Lock {
public:
    explicit Lock(Mutex* pm) : mutexPtr(pm, unlock) {
        lock(mutexPtr.get()); // 随后会讨论get
    }
private:
    shared_ptr<Mutex> mutexPtr;
}

此处不再需要声明析构函数,因为没有必要。因为class析构函数会自动调用其non-static成员变量的析构函数,也就是此处的删除器函数。

资源复制的原则

  • 复制底部资源。有时候,只要你喜欢,可以针对一份资源拥有其任意数量的复制(副本)。而你需要“资源管理类”的唯一理由是,当你不再需要某个复件时确保它被释放。在此情况下复制资源管理对象,应该同时也复制其所包括的资源,也就是进行“深拷贝”。
  • 转移底部资源的拥有权。某些罕见场合下,你可以希望确保永远只有一个RAII对象指向一个未加工资源,即使是RAII对象被复制依然如此,此时,资源的拥有权会从被复制物转移到目标物。就像auto_ptr的复制意义。

3. 在资源管理类中提供对原始资源的访问

API往往要求访问原始资源,所以每一个RAII class应该提供一个“取得其所管理之资源”的方法。

问题产生

在#1中我们提到了以下factory函数

std::shared_ptr<investment> pInv = createinvestment();

假设我们希望以某个函数处理investment对象:

int daysHeld(const investment* pi); // 返回投资天数

int days = daysHeld(pInv);  
// error!!! 类型不匹配!函数需要investment指针,而你传给它类型为shared_ptr的对象。

问题解决

这时候我们需要一个函数可将RAII class对象转换为其所内含之原始资源。以下提供两种方法:

显示转换

shared_ptr和auto_ptr都提供了一个get函数用来执行显示转换。

int days = daysHeld(pInv.get());  

同样的,就像几乎所有智能指针一样,shared_ptr和auto_ptr都重载了指针取值的操作符operator -> and operator*。

隐式转换

提供一种重载操作符,完成隐式转换。类似于以下代码:

class type {
public:
    type(int i) : a(i) {
    }
    operator int() {
        return a;
    }
private:
    int a;
};
int main(int argc, const char * argv[]) {
    type a(10);
    std::cout << a << std::endl;
    return 0;
}

总结

是否应该提供一个显式转换函数将RAII class转换为其底部资源,或者应该提供隐式转换,取决于RAII class被设计执行的特定工作,以及它被使用的情况。最佳的设计很可能是坚持“让接口容易被正确使用,不易被误用”。通常显式转换函数如get是比较好的,因为它将“非故意之类型转换”的可能性最小化了。

4. 成对使用new和delete时要采取相同形式

当你使用new时,有两件事情发生:1)内存被分配出来。2)针对此内存会有一个或多个构造函数被调用。同样的,当你使用delete时,也有两件事情发生:1)针对此内存会有一个或多个析构函数被调用。2)内存被释放。但问题出来了,即将被删除的内存之内究竟有多少个对象?这决定了有多少个析构函数被调用。

因为单一对象的内存分配和数组的内存分配是不一样的,所以每当我们使用delete的时候要告诉编译器,我们使用的是单一对象还是数组。方法就是加上[]!

此规则对于喜欢使用typedef的人十分重要,这意味着作者必须说清楚typedef是什么,要用什么类型的delete。

typedef std::string AddressLines[4];

std::string* pal = new AddressLines;

delete [] pal; // that is right! But user may misunderstand.

为了避免诸如此类的错误,最好不要对数组形式采用typedef动作,而是使用vector,string等template来替换数组。

5. 以独立语句将new对象直接放入智能指针

我们无法知道编译器会按照什么样的顺序来执行一条代码语句,而这其中可能隐含着某些异常。

问题产生

假设我们有个函数来揭示处理程序的优先权,另一个函数用来在某动态分配所得的Widget上进行某些带有优先权的处理。

// function definition:
int priority();
void processWidget(std::shared_prt<Widget> pw, int priority);

// function reference:
processWidget(std::shared_ptr<Widget> (new Widget), priority());
// be cautious!
// shared_ptr构造函数需要一个原始指针,但该构造函数是个explicit构造函数
// 无法进行隐式转换,所以需要显示的写出转换。

问题发生得很隐蔽!

我们本以为把一个对象放入”对象资源管理器“就可以有效的解决资源泄露的问题,但其实在这里隐含危机。问题出在编译器运行顺序上,如果编译器按照以下顺序:
1. 执行new Widget
2. 调用priority
3. 调用shared_ptr构造函数
而此时,priority的调用导致异常,那么new Widget返回的指针就会遗失,导致内存泄露。(幸福来得太突然)

问题解决

此问题的解决非常的简单。既然编译器有机会因为代码运行顺序不同而出错,那么我们就强行让他按照我们预想的顺序执行就可以了~

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

因为编译器对于“跨越语句的各项操作”没有重新排列的自由!所以此代码不会出现问题!

所以,尽可能单独地把new对象直接放入智能指针中总是有道理的。

6. 区别堆和栈

  1. 管理方式不同
    栈,由编译器自动管理,无需程序员手工控制;堆:产生和释放由程序员控制。
  2. 空间大小不同
    栈的空间有限;堆内存可以达到4G。
  3. 能否产生碎片不同
    栈不会产生碎片,因为栈是种先进后出的队列。堆则容易产生碎片,多次的new/delete
    会造成内存的不连续,从而造成大量的碎片。
  4. 生长方向不同
    堆的生长方式是向上的,栈是向下的。
  5. 分配方式不同
    堆是动态分配的。栈可以是静态分配和动态分配两种,但是栈的动态分配由编译器释放。
  6. 分配效率不同
    栈是机器系统提供的数据结构,计算机底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令。堆则是由C/C++函数库提供,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家 尽量用栈,而不是用堆。

栈和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。

无论是堆还是栈,都要防止越界现象的发生。

总结

对于C++这种技巧较高的语言来说,资源管理从来都不会是一个简单的事情,所以掌握一定的资源管理方法总是有用的!此博客根据《effective c++》总结了资源管理的几条准则,附加少量自己的补充。

你可能感兴趣的:([C++]资源管理)