|
最近在温习这本经典的Effective C++,好记性不如烂笔头,记录要点,如有不正确之处,欢迎批评指正。
第一章笔记链接:《Effective C++》第一章笔记 让自己习惯C++
第二章笔记链接:《Effective C++》第二章笔记 构造/析构/赋值运算
第三章笔记链接:《Effective C++》第三章笔记 资源管理
第四章笔记链接:《Effective C++》第四章笔记 设计与声明 [待更新]
第五章笔记链接:《Effective C++》第五章笔记 实现 [待更新]
第六章笔记链接:《Effective C++》第六章笔记 继承与面向对象 [待更新]
第七章笔记链接:《Effective C++》第七章笔记 模板与泛型编程 [待更新]
第八章笔记链接:《Effective C++》第八章笔记 定制new和delete [待更新]
<hr/>引言
所谓资源,一旦向系统申请,将来就得还给系统。资源有内存、网络socket、互斥锁mutex等,如果管理不好就可能导致内存泄漏、coredump等糟糕的结果。
本章介绍了RAII、智能指针、资源复制、new和delete等关于资源使用的知识,遵循本章的建议,可以几乎消除资源管理的问题,是每个C++程序员都得掌握的基础。
条款13:以对象管理资源
首先说说内存泄漏这个词,之前我不理解为什么要叫“泄漏”,后来明白了:当有一块分配的内存不能被系统回收,那么对于系统来说这块内存就是“泄漏”了,好比气球漏气,对气球而言有一些“气资源”泄漏了。
本条例建议用对象来管理资源,举个例子:
Column* createColumn(); // 创建一列,返回一个指向创建的列的指针,调用者需要手动删除
调用处:
void useColumn(){
Column* ptr = createColumn();
...
delete ptr; // 释放对象
}其实这样风险很大:如果在省略处(...)提前return,就发生了资源泄漏。虽然可以小心的避免,但心智成本变高了,软件的维护代价也变大了
上述代码可以修改为用智能指针管理资源(本质也是用对象管理资源),不用操心资源释放的事,出作用域有析构函数自动帮你完成:
void useColumn(){
std::auto_ptr<Column> ptr(createColumn());
}以对象管理资源有两个关键想法:
- 获得资源应该立刻放进管理对象(manager object)内
- 管理对象(manager object)应用析构函数确保资源被释放
这也称为RAII(Resource Acquisition Is Initialization):资源获取时机便是初始化时机
本条款也提到了auto_ptr一个坑爹的地方:
auto_ptr采用copy语义来转移指针资源,转移指针资源的所有权的同时将原指针置为NULL
比如:
std::auto_ptr<Column> p1(createColumn());
std::auto_ptr<Column> p2 = p1; // p2指向对象,p1 = null
p1 = p2; // p1指向对象,p2 = null新指针如果是通过copy构造函数或者copy assignment操作符复制老指针得到的,老指针会变成nullptr
上面代码使用auto_ptr会让被指向物为null,使用shared_ptr则不会
听组里的老师傅也说过不建议使用auto_ptr,详细内容查看: auto_ptr的缺陷在哪里?为什么不应该用?
手动给互斥量加锁解锁添麻烦了,心智成本高,你可能希望建立一个class来管理加解锁,下述是用互斥量mutex+RAII实现的自动析构解锁类Lock,更详尽的版本可以参考lock_guard源码(很简短)
class Lock{
public:
Lock(){ mx.lock(); }
~Lock(){ mx.unlock(); }
}
private:
std::mutex mx; // mutex不允许拷贝构造和拷贝操作,最初产生的mx是未加锁状态
};条款14:在资源管理类中小心coping行为
继续考虑上述Lock示例,当某个使用RAII对象被复制会怎么样?对于mutex来说是禁止拷贝的,只能传参引用,因为如果允许拷贝,可能存在多个线程访问互斥区。
如果从特殊到一般情况呢,当某个使用RAII对象被复制会怎么样?有哪些可能?
- 禁止复制。如上述Lock
- 使用引用计数法。这种情况系复制RAII对象时,应该将它的引用计数增加1,当引用计数为0时才删除,可以采用shared_ptr
class Lock{
public:
explict Lock(mutex& mx_):mx(mx_,unlock){}
private:
std::shared_ptr<mutex> mx;
};
- 复制底部资源。deep copy
- 转移底部资源的拥有权。某些场景你希望只有一个RAII对象指向一个raw resource,即使RAII对象复制也是这样,类似unique_ptr或者auto_ptr
本条款主体内容到此就结束了,主要说了RAII对象的复制,小结:
- 复制RAII对象需要一并复制它所管理的资源,资源的coping行为决定RAII对象的coping行为
- 普遍的RAII class coping行为有:禁止复制、施行引用计数法
条款15:在资源管理类中提供对原始资源的访问
创建一个shared_ptr指针
std::shared_ptr<Column> pCol(createColumn());将指针指针作为某函数的参数
int getRows(const Column* ptr); //返回列的行数
int rows = getRows(pCol); // 错误错误原因是getRows需要的是Column*指针,而传入的却是智能指针
因此,需要取得RAII对象内的原始资源,可以用智能指针的get方法
int rows = getRows(pCol.get()); // 正确
获取原始资源有两种方式,显示转化和隐式转化
- 显示转化:如提供get()接口
- 隐式转化:增加一个名为类名的重载函数,返回原始资源
代码示例:
class A {
A get() const { return a; } // 显示转化
// 隐式转化
operator A() const {
return a;
}
// 原始资源
... a ...
};条款16:成对使用new和delete时要采取相同形式
- new的作用:1.分配一块内存,2.调用一个或多个构造函数
- delete的作用:1.调用一个或多个析构函数 2.释放内存
new和delete的都需要确认构造和析构元素的个数,而这个参数由[]提示编译器来计算得到。
举个例子:
std::string* strP1 = new std::string; //申请1份string的内存并构造
std::string* strP2 = new std::string[100]; // 申请100份string的内存并构造
delete strP1; // 删除一个string对象
delete[] strP2; // 删除多个(100个)string对象如果出现new的delete不匹配的情况,程序将发生程序结果未定义。
这一条款的内容可以用一句话概况:
- 如果你在new表达式中用了[],必须在相应delete表达式中也用[]
- 如果你的new表达式不包含[],那么相应的delete表达式也一定不要包含[]
条款17:以独立语句将newed对象置入智能指针
这一条款起提示的作用,实际情况会踩坑的概率较小。
举个例子:
写一个函数,一个参数传入列的智能指针,另一个参数传入列的优先级,当该函数被多个线程调用时,按照优先级执行。
processColumn(std::shared_ptr<Column> pCol, int priority() );现调用它
processColumn(std::shared_ptr<Column>(new Column), getPriority() );而上述调用可能导致资源泄漏
第一个参数std::shared_ptr<Column>(new Column)会做两件事情:
- 调用new Column
- 调用std::shared_ptr构造函数
对第二个参数的调用getPriority()可能排在第一或第二或第三执行(并不确定),如果编译器选择将它作为第二顺序,则会得到这样的操作序列
- 调用new Column
- 调用getPriority()
- 调用std::shared_ptr构造函数
那么,如果getPriority()函数中出现异常,会导致new Column得到的指针不会被正确用给std::shared_ptr初始化,会引发资源泄漏。
避免这类问题很简单,将new抽离成独立语句即可解决。
std::shared_ptr<Column> pCol(new Column)
processColumn(pCol, getPriority() );
- 以独立语句将newed的对象存储于智能指针内,不过不这样,一旦异常被抛出,有可能导致难以察觉的资源泄漏。
(第三章完) |
|