Effective C++
来源:互联网 发布:杭州龙翔桥到淘宝城 编辑:程序博客网 时间:2024/06/16 09:49
前言:如何有效运用C++,包括一般性的设计策略,以及带有具体细节的特定语言特性。知道细节很重要,否则如果疏忽几乎总是导致不可预期的程序行为(undefined behavior)。本文总结对于如何使用C++的一些建议,从而让你成为一个有战斗力的C++程序员。
- 以对象管理资源
- 解决方案使用auto_ptr
- 替代方案
- 在资源管理中小心coping行为
- 在资源管理类中提供对原始资源的访问
- 问题
- 解决方法
- 最佳实践
- 成对使用new和delete时要采取相同形式
- 问题
- 正确做法
- 以独立语句将newed对象置入智能指针
- 问题
- 以对象管理资源
资源,就是一旦用了它,将来必须还给系统。C++中最常使用的资源就是动态分配内存,但内存只是你必须管理的众多资源之一。其他常见的资源还包括文件描述符,互斥锁,数据库连接,网络sockets等。
不论哪一种资源,重要的是,当你不再使用它时,必须将它还给系统。但是,难点是,当你考虑到异常、函数内多重回传路径、程序维护不正确的改动等情况,就很难做到上面的保证。
1 以对象管理资源
Use objects to manage resources.
void f(){ Investment* pInv = createInvestment(); // 调用factory函数,返回一个动态对象 // do something // ... delete pInv; // 释放动态对象}
问题:在若干情况下(例如,中途过早返回,或抛出异常等),f可能无法删除动态对象。当然,谨慎地编写程序可以防止上面的错误,但是随着代码的修改和维护,这种保障总是显得吃力。
好的做法:为确保返回的资源总是被释放,我们需要将资源放进对象内,当控制流离开f,该对象的析构函数会自动释放那些资源。许多资源被动态分配于heap内,而后被用于单一区块或函数内。它们应该在控制流离开那个区域或函数时被释放。
解决方案,使用auto_ptr
标准程序库提供的auto_ptr
正是针对这种形势而设计的,auto_ptr
是个类指针对象(智能指针),其析构函数自动对其所指对象调用delete。
void f(){ std::auto_ptr<Investment> pInv(createInvestment()); // do something // ... // 最后由auto_ptr的析构函数自动删除pInv}
想法:
- 获得资源后立刻放进管理对象内。
即,资源取得时机便是初始化时机(Resource Acquisition Is Initialization; RAII
)
- 管理对象运用析构函数确保资源被释放。
不论控制流如何离开区块,一旦对象被销毁,其析构函数会被自动调用,于是资源被释放。
注意:由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意别让多个auto_ptr同时指向同一对象,否则对象会被删除一次以上。为了预防这个问题,auto_ptr有一个特性是,若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权。
// pInv1指向createInvestment返回物std::auto_ptr<Investment> pInv1(createInvestment());// 现在pInv2指向对象,pInv1被设为nullstd::auto_ptr<Investment> pInv2(pInv1);// 现在pInv1指向对象,pInv2被设为nullpInv1 = pInv2;
带来的问题:由于STL容器要求其元素发挥“正常的”复制行为,而auto_ptr的这种诡异的复制行为,导致其不符合STL的容器要求。
替代方案
auto_ptr的替代方案是“引用计数型智能指针(reference-counting smart pointer; RCSP)“。
所谓RCSP,也是个智能指针,持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。RCSP提供的行为类似垃圾回收(garbage collection),不同的是,RCSP无法打破环状引用(cycles of references)。例如,两个其实已经没被使用的对象彼此互指,因而好像还处在“被使用”的状态。
TR1的tr1::shared_ptr
就是个RCSP,所以你可以这么写f:
void f(){ // 使用shared_ptr std::tr1::shared_ptr<Investment> pInv(createInvestment()); // do something // ... // 经由shared_ptr析构函数自动删除pInv}// 看下复制行为void f(){ // pInv1指向createInvestment返回物 std::tr1::shared_ptr<Investment> pInv1(createInvestment()); // pInv1和pInv2指向同一个对象 std::tr1::shared_ptr<investment> pInv2(pInv1); // 同上,无任何改变 pInv1 = pInv2; // pInv1和pInv2被销毁,它们所指的对象也就被自动销毁}
解决auto_ptr的问题:由于tr1::shared_ptr的复制行为”一如预期”,因此,它们可以被用于STL容器。
注意: auto_ptr
和tr1::shared_ptr
两者都在其析构函数内做delete,而不是delete[]动作,那么意味着,在动态分配而得的array身上使用auto_ptr或tr1::shared_ptr是个馊主意。
问题
#include <iostream>#include <string>#include <memory>using namespace std;int main(){ // 馊主意,会用上错误的delete形式,而且编译器不是提示编译错误 std::auto_ptr<std::string> aps(new std::string[10]); std::shared_ptr<int> spi(new int[1024]);}
你会发现:并没有特别针对“C++动态分配数组”而设计的类似auto_ptr或tr1::shared_ptr那样的东西。那是因为,vecotr
和string
几乎总是可以取代动态分配而得的数组。
请记住:
* 为防止资源泄露,请使用RAII对象
,它们在构造函数中获得资源,并在析构函数中释放资源。
* 两个常被使用的RAII classes分别是tr1::shared_ptr
和auto_ptr
。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null。
TODO scope_ptr
2 在资源管理中小心coping行为
问题: auto_ptr
和shared_ptr
可以实现对heap-based
资源的RAII
,然后对于非heap-based
的资源并不合适。因此,有时需要建立自己的资源管理类
。
例如:
void lock(Mutex* pm); // 加锁void unlock(Mutex* pm); // 解锁
为了确保不会将一个被锁住的Mutex解锁,需要建立一个class用来管理锁。这样的class的基本结构由RAII守则支配,也就是“资源在构造期间获得,在析构期间释放”。
class Lock{ public: // 获得资源 explicit Lock(Mutex* pm) : mutexPtr(pm) { lock(mutexPtr); } // 释放资源 ~Lock() { unlock(mutexPtr); } private: Mutex *mutexPtr;};
客户在使用时:
Mutex m; // 定义互斥器// 建立一个区块用来定义critical section{ Lock m1(&m); // 锁定互斥器 // 执行critical section内的操作 // 在区块最末尾,自动解除互斥器锁定}
问题:如果Lock对象被复制,会发生什么事情?
Lock m1(&m); // 锁定mLock m2(m1); // 将m1复制到m2身上,会发生什么?
一般有两种选择:
- 禁止复制。
许多时候允许RAII对象被复制并不合理。如果复制动作对RAII class并不合理,你便应该禁止之。
- 对底层资源进行”引用计数法(reference-count)”
有时候,我们希望保有资源直到它的最后一个使用者被销毁。这种情况下,复制RAII对象时,应该将资源的“被引用数”递增。shared_ptr
便是如此。
请记住:
* 复制RAII对象,必须一并复制它所管理的资源。
* 普通常见的RAII class copying行为是,抑制copying,使用引用计数。
3 在资源管理类中提供对原始资源的访问
Provide access to raw resources in resource-managing classes.
问题
我们期望通过使用resource-managing classes对抗资源泄露,但是许多APIs直接指涉资源,导致下面问题。
std::tr1::shared_ptr<Investment> pInv(createInvestment());// 假设需要下面的函数处理Investment对象int daysHeld(const Investment* pi);// 返回投资天数// 正常需要这样调用int days = daysHeld(pInv); // 错误,无法通过编译,因为daysHeld需要的是Investment* 指针,而我们传递的却是个类型为tr1::shared_ptr<Investment>的对象
解决方法
这个时候需要一个函数可将RAII class对象(tr1::shared_ptr)转换为其所内含之原始资源(Investment*)。有两个方法可以达到:
- 显示转换
shared_ptr
和auto_ptr
都提供了一个get成员函数
(它会返回智能指针内部的原始指针),用来执行显式转换。
int days = daysHeld(pInv.get()); // ok
- 隐式转换
shared_ptr
和auto_ptr
也重载了指针取值操作符(operator->
和operator*
),它们允许隐式转换至底部原始指针。
bool taxable1 = !(pInv->isTaxFree()); // 经由operator->访问资源bool taxable2 = !((*pInv).isTaxFree()); // 经由operator*访问资源
最佳实践
是否应该提供一个显式转换函数
将RAII class
转换为其底部资源,或是应该提供隐式转换
,答案主要取决于RAII class
被设计执行的特定工作,以及它被使用的情况。最佳的设计原则是:让接口容易被正确使用,不易被误用。
请记住
* APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个”取得其所管理之资源”的办法。
* 对原始资源的访问可能经由显式转换或隐式转换。一般而言,显式转换比较安全,但隐式转换对客户比较方便。
4 成对使用new和delete时要采取相同形式
问题
std::string* array = new std::string[100];// ...delete array;
上面array所含的100个string对象中的99个不太可能被适当删除,因为它们的析构函数很可能没有被调用。
当你使用new
,有两件事情发生:
1. 内存被分配出来(通过operator new
的函数)
2. 针对此内存会有一个(或更多)构造函数被调用
当你使用delete
,也有两件事情发生:
1. 针对此内存会有一个(或更多)构造函数被调用
2. 内存被释放(通过operator delete
的函数)
delete的最大问题在于:即将被删除的内存究竟存有多少对象,这个问题的答案决定了有多少个析构函数必须被调用。
当你对着一个指针使用delete,唯一能够让delete知道内存中是否存在一个数组大小记录的办法就是:使用delete时加上中括号(方括号),delete便认定指针指向一个数组,否则,它便认定指针指向单一对象。
正确做法
std::string* ptr1 = new std::string;std::string* ptr2 = new std::string[100];// ...delete ptr1; // 删除一个对象delete [] ptr2; // 删除一个由对象组成的数组
请记住
如果调用new
时使用[]
,那么必须在对应调用delete
时也使用[]
。如果调用new
时没有使用[]
,那么也不应该在对应调用delete
时使用[]
。
5 以独立语句将newed对象置入智能指针
问题
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
new Widget
一定执行于tr1::shared_ptr
构造函数被调用之前,因为这个表达式的结果还要被传递作为tr1::shared_ptr
构造函数的一个实参,但对priority
的调用则可以排在第一或第二或第三执行。如果编译器选择以第二执行它,则操作序列为:
- 执行
new Widget
- 调用
priority
- 调用
tr1:shared_ptr
构造函数
如果对
priority
的调用导致异常,会发生什么?
在此情况下,new Widget
返回的指针将会遗失,因为它尚未被置入tr1::shared_ptr
内。因此,避免此类问题的办法是,使用分离语句。
std::tr1::shared_ptr<Widget> pw(new Widget);// 这个调用动作不会造成泄漏processWidget(pw, priority());
请记住
以独立语句将newd
对象存储于智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。
- 《Effective C++》和《More Effective C++》汇总
- 《Effective C++》读书笔记
- 《Effective C#》 翻译札记
- 《Effective C++》读感
- 《Effective C++》读后感
- 读《Effective C++》所想
- 《Effective C++》读书笔记
- 《Effective C#》 翻译札记
- 细读《Effective C++》之一
- 《Effective C#》 翻译札记
- effective c sharp 印象
- 细读《Effective C++》之一
- Begin reading 'Effective c++'
- 《Effective C#》 翻译札记
- 《Effective c++》读书笔记
- 《Effective C++》 笔记
- Effective C++(1-4)
- Effective C++(5-12)
- 二叉树的实现-Huffman树-摘自数据结构实现java版本(个人笔记整理)
- javascript 用户代理字符串检测技术-
- BeanFactory和ApplicationContext的介绍
- Windows 7+Sublime Text 3配置C/C++开发环境
- HDOJ--1865 1string +HDOJ--2504 A==B? +Problem B
- Effective C++
- Windows7系统如果安装&升级IE11浏览器
- Java——获取图片尺寸和大小
- 排序算法07:三向快速排序
- 3.分布式集群
- Spring AOP详解
- 多线程同步锁实现简单数据的同步输入与输出
- Python高级技巧之一
- 由中根序列和后根序列重建二叉树