【effective c++读书笔记】【第3章】资源管理

条款13:以对象管理资源

1、例子:

#include<iostream>
using namespace std;

class Test{
public:
	Test(){ cout << "Test构造" << endl; }
	~Test(){ cout << "Test析构" << endl; }
};
Test* createTest(){
	return new Test();
}
void f(){
	Test* pTest = createTest();
	//...若这里出现return语句,或者抛出异常等情况,就会导致资源泄露
	delete pTest;
}

int main(){
	f();

	system("pause");
	return 0;
}

从上述例子及注释中可以看到,如果函数f中在delete操作之前,出现了return语句,或者“…”区域内的语句抛出异常,都会导致内存的泄露,类似情况也发生在对createTest的使用及delete动作位于某循环内,而该循环由于某个continuegoto语句过早退出。

2、 把资源放进对象内,我们便可依赖C++的“析构函数自动调用机制”确保资源被释放。

例子:

#include<iostream>
#include<memory>
using namespace std;

class Test{
public:
	Test(){ cout << "Test构造" << endl; }
	~Test(){ cout << "Test析构" << endl; }
};
Test* createTest(){
	return new Test();
}
void f(){
	auto_ptr<Test> pTest(createTest());
}

int main(){
	f();

	system("pause");
	return 0;
}

上述例子示范“以对象管理资源”的两个关键想法:

a、 获得资源后立刻放进管理对象内(如auto_ptr)。“以对象管理资源”的观念常被称为“资源取得时机便是初始化时机”(Resource Acquisition IsInitialization;RAII)。

b、 管理对象运用析构函数确保资源被释放。一旦对象被销毁,其析构函数被自动调用来释放资源。

3、由于auto_ptr被销毁时会自动删除它所指之物,所以不能让多个auto_ptr同时指向同一对象。所以auto_ptr若通过拷贝构造函数或拷贝赋值操作符复制它们,它们会变成NULL,而复制所得的指针将取得资源的唯一拥有权!

例子:

#include<iostream>
#include<memory>
using namespace std;

class Test{
public:
	Test(){ cout << "Test构造" << endl; }
	~Test(){ cout << "Test析构" << endl; }
};
Test* createTest(){
	return new Test();
}


int main(){
	auto_ptr<Test> pTest1(createTest());
	auto_ptr<Test> pTest2(pTest1);
	if (pTest1.get() == NULL)
		cout << "Construct:pTest1 NULL" << endl;
	if (pTest2.get() == NULL)
		cout << "Construct:pTest2 NULL" << endl;

	pTest1 = pTest2;
	if (pTest2.get() == NULL)
		cout << "assignment:pTest2 NULL" << endl;
	if (pTest1.get() == NULL)
		cout << "assignment:pTest1 NULL" << endl;

	system("pause");
	return 0;
}
运行结果:


4、auto_ptr的替代方案是“引用计数型智能指针”(reference-counting smart pointer;RCSP),它可以持续跟踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。TR1的tr1::shared_ptr就是一个"引用计数型智能指针"。

例子:

#include<iostream>
#include<memory>
using namespace std;

class Test{
public:
	Test(){ cout << "Test构造" << endl; }
	~Test(){ cout << "Test析构" << endl; }
};
Test* createTest(){
	return new Test();
}
void f(){
	shared_ptr<Test> pTest1(createTest());
	shared_ptr<Test> pTest2(pTest1);//pTest1和pTest2指向同一个对象
}

int main(){
	f();

	system("pause");
	return 0;
}

5、auto_ptr和tr1::shared_ptr都在其析构函数内做delete而不是delete[],也就意味着在动态分配而得的数组身上使用auto_ptr或tr1::shared_ptr会使资源得不到释放。

请记住:

  • 为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
  • 两个常被使用的RAII类分别是auto_ptr和tr1::shared_ptr。后者通常是较佳选择,因为其拷贝行为比较直观。若选择auto_ptr,复制动作会使他(被复制物)指向NULL。

条款14:在资源管理类中小心拷贝行为

1、“当一个RAII对象被复制,会发生什么事?”有以下几种选择:

a、禁止复制。

b、引用计数(reference-count)。shared_ptr 允许指定所谓的“删除器”(函数或者函数对象)

c、深度复制(Deep copying)。

d、转移底部资源的拥有权。如auto_ptr奉行的复制意义。

请记住:

  • 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的行为。
  • 普遍而常见的RAII类的copying行为是:禁止copying,施行引用计数法。不过其他行为也都可能被实现。

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

1、许多APIs直接指涉资源,需要直接访问原始资源。这时候需要一个函数可将RAII对象(如tr1::shared_ptr)转换为其所内含之原始资源。有两种做法可以达成目标:显示转换和隐式转换。

2、tr1::shared_ptr和auto_ptr都提供一个get成员函数,用来执行显示转换,也就是返回智能指针内部的原始指针(的复件)。就像所有智能指针一样, tr1::shared_ptr和auto_ptr也重载了指针取值操作符(operator->和operator*),它们允许隐式转换至底部原始指针。

3、显式转换例子:

#include<iostream>
#include<memory>
using namespace std;

class Test{
public:
	Test(){ cout << "Test构造" << endl; }
	~Test(){ cout << "Test析构" << endl; }
	void print(){ cout << "print Test" << endl; }
};
Test* createTest(){
	return new Test();
}
void f(Test* pt){
	cout << "this is test" << endl;
}

int main(){
	shared_ptr<Test> pTest1(createTest());
	//f(pTest1);错误,需要一个Test类的指针
	f(pTest1.get()); //显式转换

	pTest1->print();//重载operator->
	(*pTest1).print();//重载operator*

	system("pause");
	return 0;
}
4、隐式转换例子:
class Font{
public:
	explicit Font(FontHandle fn) :f(fn){}
	....
	FontHandle get() const{
		return f;
	}
	operator FontHandle() const{         //隐式转换
		return f;
	}
	...
	~Font(){ realseFont(f); }
private:
	FontHandle f;
};

请记住:

  • APIs往往需要取得RAII的原始资源,所以每一个RAIIclass应该提供一个“取得其所管理之资源”的方法。
  • 对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便。

条款16:成对使用new和delete时要采取相同形式

1、当你使用new动态生成一个对象,有两件事发生:第一,内存被分配。第二,针对此内存会有一个(或更多)构造函数被调用。当你使用delete,也有两件事发生:针对此内存会有一个(或更多)析构函数被调用,然后内存被释放。

2、单一对象的内存布局一般而言不同于数组的内存布局。数组所用的内存通常还包括“数组大小“的记录,以便delete知道需要调用多少次析构函数。

3、尽量不要对数组形式做typedef动作。

请记住:

  • 如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。    

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

1、例子

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

采用如下形式调用processWidget:

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

在上面函数调用中,可能会出现内存泄露。因为在函数中,参数的调用顺序会因为编译器的不同而不同,例如上面的顺序可能是:

a、执行”new widget”

b、调用priority

c、调用tr1::shared_ptr构造函数

这样可能出现的问题就是当new widget成功后,如果priority()函数调用导致异常,new widget返回的指针将会遗失,因为new widget未能放入到智能指针中,导致内存泄漏。

避免这类问题的办法是使用分离语句:

std::tr1::shared_ptr<Widget> pw(new Widget);    //在单独语句内以智能指存储newd所得对象
processWidget(pw, priority()); //这个调用动作绝不至于造成泄漏

请记住:

  • 以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常抛出,有可能导致难以察觉的资源泄漏。   


版权声明:本文为博主原创文章,未经博主允许不得转载。

原文地址:https://www.cnblogs.com/ruan875417/p/4785445.html