所谓资源就是,一旦使用了它,将来必须归还给系统!C++最常见的资源就是动态分配内存,如果不归还就会内存泄露。
我们通常希望有一个对象来帮助我们解决资源管理的问题(自动调用析构函数),于是此章我们讨论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一样,但是相应的复制行为会正常了许多。
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
前面我们提到了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成员变量的析构函数,也就是此处的删除器函数。
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是比较好的,因为它将“非故意之类型转换”的可能性最小化了。
当你使用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来替换数组。
我们无法知道编译器会按照什么样的顺序来执行一条代码语句,而这其中可能隐含着某些异常。
假设我们有个函数来揭示处理程序的优先权,另一个函数用来在某动态分配所得的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对象直接放入智能指针中总是有道理的。
堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家 尽量用栈,而不是用堆。
栈和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。
无论是堆还是栈,都要防止越界现象的发生。
对于C++这种技巧较高的语言来说,资源管理从来都不会是一个简单的事情,所以掌握一定的资源管理方法总是有用的!此博客根据《effective c++》总结了资源管理的几条准则,附加少量自己的补充。