1.C++中的资源概念
在学习编程时,我们经常能听到"资源"这个词。资源可能是一个很宽泛的概念,但总体来讲,资源是我们可以用来使用,并且使用完之后要返还给系统的东西。在C++中,资源多数是指动态分配的内存。如果你只用new来分配内存却不在使用完后delete掉,将会导致内存泄漏。
其他资源,如文件描述符(file descriptor),Mutex锁,GUI中的字体(font)和画刷(brush),网络接口(socket)。但不论资源是什么,我们一定要保证在使用过后资源后要及时释放,否则就会造成内存泄露。当我们的代码变得越来越复杂,比如增加了异常抛出,函数不同的返回路径,手动管理资源将会变得费时费力,因此我们需要用对象来管理资源。
2.简单工厂模式[预备知识]
概念:定义一个简单工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。在简单工厂模式中,大体上有3个角色:
a.工厂(Factory):根据客户提供的具体产品类的参数,创建具体产品实例;
b.抽象产品(AbstractProduct):具体产品类的基类,包含创建产品的公共方法;
c.具体产品(ConcreteProduct):抽象产品的派生类,包含具体产品特有的实现方法,是简单工厂模式的创建目标。
简单工厂模式实现流程:
a.设计一个抽象产品类,它包含一些公共方法的实现;
b.从抽象产品类中派生出多个具体产品类,如篮球类、足球类、排球类,具体产品类中实现具体产品生产的相关代码;
c.设计一个工厂类,工厂类中提供一个生产各种产品的工厂方法,该方法根据传入参数(产品名称)创建不同的具体产品类对象;
d.客户只需调用工厂类的工厂方法,并传入具体产品参数,即可得到一个具体产品对象。
例如,一个体育用品生产厂(即一个工厂Factory),该工厂可以根据客户需求生产篮球、足球和排球。篮球、足球和排球被成为产品(Product),产品的名称可以被成为参数。客户CurryCoder需要时可以向工厂提供产品参数,工厂根据产品参数生产对应产品,客户CurryCoder并不需要关心产品的生产过程细节。
1#include <iostream> 2 3using namespace std; 4 5// 抽象产品类AbstractProduct 6class AbstractSportProduct 7{ 8public: 9 AbstractSportProduct(){ 10 11 } 12 // 抽象方法 13 void printName(){}; 14 void play(){}; 15}; 16 17// 具体产品类Basketball 18class Basketball :public AbstractSportProduct 19{ 20public: 21 Basketball(){ 22 printName(); 23 play(); 24 } 25 // 具体实现方法 26 void printName(){ 27 printf("CurryCoder get Basketball\n"); 28 } 29 void play(){ 30 printf("CurryCoder play Basketball\n"); 31 } 32}; 33 34// 具体产品类Football 35class Football :public AbstractSportProduct 36{ 37public: 38 Football(){ 39 printName(); 40 play(); 41 } 42 // 具体实现方法 43 void printName(){ 44 printf("CurryCoder get Football\n"); 45 } 46 void play(){ 47 printf("CurryCoder play Football\n"); 48 } 49}; 50 51// 具体产品类Volleyball 52class Volleyball :public AbstractSportProduct 53{ 54public: 55 Volleyball(){ 56 printName(); 57 play(); 58 } 59 // 具体实现方法 60 void printName(){ 61 printf("CurryCoder get Volleyball\n"); 62 } 63 void play(){ 64 printf("CurryCoder play Volleyball\n"); 65 } 66}; 67 68// 工厂类 69class Factory 70{ 71public: 72 AbstractSportProduct *getSportProduct(string productName) // 工厂方法或工厂函数 73 { 74 AbstractSportProduct *pro = NULL; 75 if (productName == "Basketball"){ 76 pro = new Basketball(); 77 } 78 else if (productName == "Football"){ 79 pro = new Football(); 80 } 81 else if (productName == "Volleyball"){ 82 pro = new Volleyball(); 83 } 84 return pro; 85 } 86}; 87 88 89 90int main() 91{ 92 printf("简单工厂模式\n"); 93 94 // 定义工厂类对象 95 Factory *fac = new Factory(); 96 AbstractSportProduct *product = NULL; 97 98 product = fac->getSportProduct("Basketball"); 99 100 product = fac->getSportProduct("Football"); 101 102 product = fac->getSportProduct("Volleyball"); 103 return 0; 104}
3.基于对象管理资源问题的引入
下面假设你正在为不同类型的投资写一个程序库,其中各种不同投资类型都继承自一个基类Investment,如下所示:
1class Investment{ 2 // ... 3};
进一步假设,这个程序库通过工厂函数(工厂方法)createInvestment(),供某种具体类型的Investment对象使用。
1Investment* createInvestment(); // 返回指针,指向Investment继承体系中的动态分配对象
工厂函数createInvestment()的调用者f(),使用了工厂函数返回的对象后,有义务将其删除。
1void f(){ 2 Investment* pInv = createInvestment(); // 调用工厂函数 3 //.... 使用 4 delete pInv; // 释放 5}
上面的一系列操作看起来是可行的,使用完后动态分配的内存后马上释放掉它。但若干种情况下,函数f()可能无法释放它从createInvestment()处得到的动态资源!例如,如果中间"..."部分存在并触发了一个return语句,最后的delete语句便会被跳过;如果用在循环里,中间存在并触发了break或goto语句,delete也不会被执行;如果中间的代码抛出了异常,这个指针pInv也不会被删除掉。如果这个动态分配的对象没有被清理掉,不仅仅是它占用的内存资源泄露,它所占有的所有资源也将泄露。
即使代码写得再小心,避免了以上的种种情况,但当项目被不同的人接手时,别人对某一部分的改动也可能让原来自己苦心制作的手动管理机制失效,导致资源泄露,所以手动管理费时费力,最后效果也不好。
解决方法原理:为了保证createInvestment()函数返回的资源能够总是被释放,我们需要将资源放入对象内。当控制流离开f()作用域时,该对象的析构函数会自动释放那些资源。
4.解决方法
方法1
可以利用C++的对象析构函数自动调用机制,把资源封装在对象里面,这样当对象完成了它的使用周期,资源就会保证被释放。使用标准库的模板智能指针auto_ptr(注:C++11标准中,已将此智能指针抛弃),它的析构函数会自动调用delete来释放它所指向的对象,十分方便。
1#include <iostream> 2#include <memory> 3 4 5 6class Investment{ 7 // ... 8}; 9 10Investment* createInvestment(); 11 12void f(){ 13 std::auto_ptr<Investment> pInv(createInvestment()); // 调用工厂函数 14}
上面的简单示例阐明了基于对象来管理资源的两个关键概念:
(1).获得资源后要立即传递给资源管理对象用来初始化。以上代码中我们将createInvestment()函数获得的资源当作auto_ptr的初始值。这就是C++重要的RAII(Resource Acquisition Is Initialisation)概念,意为"资源获取便是初始化的时机"。
(2).管理对象运用析构函数确保资源被释放。当对象超出了它的作用域(scope)时(对象被销毁时),它的析构函数会自动调用。所以,用析构函数来释放资源是保证安全的。但是,正如文章C++中的析构函数不要抛出异常所说,我们要防止在对象析构过程中抛出异常。
注意:由于auto_ptr被销毁时,会自动删除它的所指之物。所以,一定不要让多个auto_ptr同时指向同一个对象。因为如果那样的话,同一个对象会被删除多次,从而会出现未定义行为。为了防止这种操作,标准库给auto_ptr定义了一个奇怪的特性:如果通过拷贝构造函数或拷贝赋值运算符函数赋值原先auto_ptr的所指之物。它们会变成null,而拷贝所得的指针将取得资源的唯一拥有权。如下所示:
1std::auto_ptr<Investment> pInv1(createInvestment()); 2 3std::auto_ptr<Investment> pInv2(pInv1); // 通过拷贝构造函数来拷贝,pInv1现在是NULL 4 5pInv1 = pInv2; // 通过拷贝赋值运算符函数来拷贝,pInv2现在是NULL
这个特性意味着auto_ptr不能被用在STL容器中,也就说明它不是管理资源最好的方法。STL容器要求它的元素拥有"正常的"拷贝特性,因为当使用STL容器的算法功能时将会使用赋值传递,这就会导致在函数生成本地拷贝的同时,容器原有的元素被设为NULL,所以装有auto_ptr的STL容器是不被允许的,会在编译时报出FBI(划掉)Warning。
方法2
auto_ptr的替代方案是利用引用计数的智能指针(Reference-Counting Smart Pointer, RCSP),它在运行时会统计有多少对象指向当前的资源,然后当没有任何对象指向当前资源时便会自动释放。C++标准库中的智能指针shared_ptr就是其中的代表,如下所示:
1void f() 2{ 3 std::shared_ptr<Investment> pInv1(createInvestment()); // pInv1指向工厂函数createInvestment()的返回之物 4 5 std::shared_ptr<Investment> pInv2(pInv1); // 通过拷贝构造函数来拷贝,pInv1和pInv2指向同一个对象 6 7 pInv1 = pInv2; // 通过拷贝赋值运算符函数来拷贝,pInv1和pInv2指向同一个对象 8} // pInv1和pInv2被销毁,它们的所指的对象也会被自动销毁
上面的代码看起来和方法1中的代码差不多,但shared_ptr可以在STL容器中使用,成为一个更好的选择。由于shared_ptr没有像auto_ptr会自动把原对象设为NULL的拷贝特性。同时,当使用容器的算法功能生成本地拷贝时,此时有两个对象指向了这个资源。即使拷贝的析构函数被调用了,原有的对象依然在指向这个资源,该资源便不会被提前释放。
注意:使用智能指针只是基于对象来管理资源的方法之一,而且也存在着局限性。例如,我们不能使用标准库的智能指针来指向一个动态分配的数组,因为auto_ptr和shared_ptr两者都是在其析构函数中调用delete而不是delete []。虽然,delete[]这样做也可以通过编译。
1std::auto_ptr<std::string> aps(new std::string[10]); 2std::shared_ptr<int> spi(new int[1024]); // 析构函数并不会调用delete []
5.总结
(1).为了防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
(2).std:shared_ptr和auto_ptr是两个常用的RAII类。一般情况下std::shared_ptr是更好的选择,因为它的拷贝不会影响到其它对象,并且支持STL容器。auto_ptr拷贝动作会使它(被拷贝物)指向null。
6.参考资料
[1] https://zhuanlan.zhihu.com/p/70415131
[2] https://blog.csdn.net/sinat_21107433/article/details/102598181