12(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
mutexPtr; }
此处不再需要声明析构函数,因为没有必要。因为class析构函数会自动调用其non-static成员变量的析构函数,也就是此处的删除器函数。
资源复制的原则
复制底部资源。有时候,只要你喜欢,可以针对一份资源拥有其任意数量的复制(副本)。而你需要“资源管理类”的唯一理由是,当你不再需要某个复件时确保它被释放。在此情况下复制资源管理对象,应该同时也复制其所包括的资源,也就是进行“深拷贝”。
转移底部资源的拥有权。某些罕见场合下,你可以希望确保永远只有一个RAII对象指向一个未加工资源,即使是RAII对象被复制依然如此,此时,资源的拥有权会从被复制物转移到目标物。就像auto_ptr的复制意义。
3. 在资源管理类中提供对原始资源的访问
API往往要求访问原始资源,所以每一个RAII class应该提供一个“取得其所管理之资源”的方法。
问题产生
在#1中我们提到了以下factory函数
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是比较好的,因为它将“非故意之类型转换”的可能性最小化了。
4. 成对使用new和delete时要采取相同形式
当你使用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来替换数组。
5. 以独立语句将new对象直接放入智能指针
我们无法知道编译器会按照什么样的顺序来执行一条代码语句,而这其中可能隐含着某些异常。
问题产生
假设我们有个函数来揭示处理程序的优先权,另一个函数用来在某动态分配所得的Widget上进行某些带有优先权的处理。
// function definition:
int priority();
void processWidget(std::shared_prt
pw, int priority); // function reference: processWidget(std::shared_ptr
问题发生得很隐蔽!
我们本以为把一个对象放入”对象资源管理器“就可以有效的解决资源泄露的问题,但其实在这里隐含危机。问题出在编译器运行顺序上,如果编译器按照以下顺序:
1. 执行new Widget
2. 调用priority
3. 调用shared_ptr构造函数
而此时,priority的调用导致异常,那么new Widget返回的指针就会遗失,导致内存泄露。(幸福来得太突然)
问题解决
此问题的解决非常的简单。既然编译器有机会因为代码运行顺序不同而出错,那么我们就强行让他按照我们预想的顺序执行就可以了~
std::shared_ptr
pw(new Widget); processWidget(pw, priority());
因为编译器对于“跨越语句的各项操作”没有重新排列的自由!所以此代码不会出现问题!
所以,尽可能单独地把new对象直接放入智能指针中总是有道理的。
6. 区别堆和栈
管理方式不同。
栈,由编译器自动管理,无需程序员手工控制;堆:产生和释放由程序员控制。 空间大小不同。
栈的空间有限;堆内存可以达到4G。 能否产生碎片不同。
栈不会产生碎片,因为栈是种先进后出的队列。堆则容易产生碎片,多次的new/delete
会造成内存的不连续,从而造成大量的碎片。 生长方向不同。
堆的生长方式是向上的,栈是向