Effective C++学习笔记(二)
条款13:以对象管理资源
(1)在程序中一旦new一个对象就要delete,如果程序员忘记了delete或者在代码以后的维护中,维护人员不太明了程序的语义,在delete之前直接return,这将会导致new之后没有delete的情况的出现。那么怎么让new的对象自动释放呢?可以使用一个对象,将这个对象作为某一个类的成员变量,在离开函数作用于的时候会自动析构这个对象,然后再这个类的析构函数里将这个指针delete掉,就不会出现上述情况。这就是用对象管理资源的原理。
(2)RAII是“资源获取就是初始化”的缩语(Resource Acquisition Is Initialization),是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连 接、互斥量等等)的简单技术。
RAII 的一般做法是这样的:在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。 借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
1,我们不需要显式地释放资源。
2,采用这种方式,对象所需的资源在其生命期内始终保持有效 —— 我们可以说,此时这个类维护了一个 invariant。这样,通过该类对象使用资源 时,就不必检查资源有效性的问题,可以简化逻辑、提高效率。(3)使用C++提供的auto_ptr类来管理指针。
class A
{
A(){cout << "进入A的构造函数" <<endl;}
~A(){cout<<"进入A的析构函数"<<endl;}
};
void main()
{
A *m_pA = new A();
auto_ptr<A> pA1(m_pA);
auto_ptr<A> pA2(m_pA);
pA2 =pA1;// 将导致pA1被清空,把pA1的值赋给了pA2
}
因为auto_ptr被销毁时会自动删除指向的对象,所以不能让auto_ptr同时指向同一个对象。为了不让auto_ptr指向同一个对象,auto_ptr有一个不寻常的性质:通过copy或者赋值运算符复制他们时,他们都会变成null,而复制所得的指针将或者资源的拥有权。由于这个限制auto_ptr在管理分配的资源的时候作用将会有限。因为它不能发挥正常的复制行为。可以使用智能指针替代。
(4)RCSP(reference-counting smart pointer)引用计数型智能指针,它持续追踪共有多少对象指向某一个资源,并在无人指向它时自动删除该资源。RCSP提供的行为类似垃圾回收,不同的是RCSP无法打破环状引用,例如两个其实已经没有被使用的对象彼此互指,因而好像还处于被使用状态。
class A
{
public:
A(){cout << "进入A的构造函数" <<endl;}
~A(){cout<<"进入A的析构函数"<<endl;}
};
void main()
{
A *m_pA = new A();
std::tr1:: shared_ptr <A> pA1(m_pA);
std::tr1:: shared_ptr <A> pA2(m_pA);
pA1=pA2;//可以正常的赋值
}
(5)注意:auto_ptr和shared_ptr是delete动作而不是delete[]动作。你也可以自己写自己的资源管理类,要注意一些细节。应遵循的原则
*为防止资源泄露,请使用PAII对象,他们在构造函数中获得资源并在析构函数中释放资源
*两个常用的RAII类分别是shared_ptr和auto_ptr,前者是最最佳选择,因为其在copy行为比较直观,若选择auto_ptr赋值动作会使它们指向空。
2. 条款14 在资源类中小心copying行为
(1)RAII同样也可以用在锁上如:
class Lock
{
public:
Lock(Metex *pm):mutexPtr(pm){ lock(mutexPtr);}
~Lock(){unlock(mutexPtr);}
private:
Mutex *mutexPtr;
}
Mutex m;
在使用锁的地方只需要Lock m1(&m);就能锁住,并在退出时自动解锁,防止忘记解锁。
(2)对于所管理资源的复制这个问题要是处理的情况而定。一般处理情况分为两类:1:禁止复制;2:对底层资源祭出“引用计数法”。像锁这个资源是不能随随便便的就被复制给另一个对象的,所以要使用第一种办法,可以使用把赋值运算符重载为private,对于像指针这种资源可以被引用,要使用引用计数法。
3. 条款15:在资源管理类中提供对原始资源的访问
(1)RAII能够做到资源能够自动的释放,不会造成资源泄露,但是如何取得RAII管理的资源呢,这就需要RAII类能够提供一个接口来获得管理的资源。可以通过写一个接口函数也可以重载->或者*等操作符,这要视情况而定。
class Font
{
public:
Font(FontHandle fh):f(fh){}
~Font(){releaseFont(f);}
FontHandle get() const{ return f;}// 通过普通的函数接口获得资源
private:
FontHandle f;
};
4. 条款16:成对使用new和delete是要采用相同的形式
(1)看下面的程序有什么问题?
void main()
{
int *p1=new int(10);
delete []p1;
}
在这个程序中p1指向的是一个int类型,但是却delete一个数组,这样的情况下编译器是不会报错的,但是这样的new和delete明显是不匹配的。
当你使用new,有两件事发生。第一:内存被分配出来;第二:针对内存中会有一个或者多个的构造函数被调用。当你使用delete也会发生两个事情:针对此内存会有一个或者更多的析构函数被调用,然后内存会被释放。delete最大的问题是被删除内存之内究竟含有多少个对象?这个问题的答案决定了有多少个析构函数必须被调用。所以如果你在new表达式中使用了[],必须在相应的delete表达式中也是用[],如果你在new表达式中不使用[],一定不要再相应的delete表达式中使用[]。
(2)
typedef string AddressLines[4];
string * p1 = new AddressLines;//注意new AddressLines返回一个string*,就像new string[4]一样
delete []p1; //在析构的时候必须要加[]
5. 条款17:以独立语句将newed对象置入智能指针
(1)看下面函数
void processWidget(tr1::shared_ptr<Widget>pw,int propity);// 这样一个类型的函数
processWidget(tr1::shared_ptr<Widget>(new Widget),propity());// 这样的函数调用
在调用processWidget之前编译器必须做三件事:1 调用propity 2.执行new Widget 3.调用shared_ptr构造函数
但是不同的编译器做着三件事的是不一样的。所以如果出现了先执行new Widget,然后调用propity,最后调用shared_ptr构造函数,麻烦就来了,如果在调用propity的时候抛出了函数异常,那么就会导致资源创建和资源被转化为资源管理对象不再一个时间点,就有可能发生内存泄露。
解决办法是:tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw,propity());、
这就是以独立语句将newed对象存储于智能指针里,如果不这样做,一旦抛出异常,有可能导致难以觉察的内存泄露。