当前位置: 首页 > Linux

“C++”借来的资源如何潇洒归还?

时间:2023-04-06 21:34:41 Linux

前言本文内容将专门针对内存管理,培养借还的好习惯,杜绝资源管理问题。文中所谓的资源,就是一旦使用,以后就必须归还给系统。如果不是这样,就会发生不好的事情。C++程序中常见的资源:内存的动态分配、文件描述符、互斥锁、图形页面中的字体和画笔、数据库连接、网络套接字,无论是哪种资源,重要的是当你不再使用它时,你必须把它归入系统是一个好习惯。细节01:用对象管理资源将资源放在析构函数中,交给析构函数释放资源假设一个类包含一个工厂函数,它获取对象的指针:A*createA();//返回指针,指向一个动态分配的对象。//删除它是调用者的责任。上面注释中提到,createA的调用者使用函数返回的对象后,负责删除它。现在考虑一个f函数来完成这个职责:voidf(){A*pa=createA();//调用工厂函数...//其他代码deletepa;//释放资源}这看起来很安全,但是在某些情况下,f函数可能无法执行deletepa语句,这将导致资源泄漏,例如以下情况:可能是因为“...“区域;也许是因为“...”区的循环语句过早地退出了continue或goto语句;可能因为“...”区的语句抛出异常,无法执行删除。当然,这种错误是可以通过仔细编写程序来避免的,但是你要想到时间久了代码可能会被修改。如果新手不注意这类情况,难免会再次发生内存泄漏。可能性。为确保A返回的所有资源都被回收,我们需要将资源放入对象中。当对象离开作用域时,对象的析构函数会自动释放资源。“智能指针”是个好帮手,让它来管理指针对象。对于在堆内存中动态分配(new)的对象,指针对象在离开作用域时不会自动调用析构函数(需要手动delete)。构造函数回收资源,我们需要使用“智能指针”特性。常用的“智能指针”有如下三种:std::auto_ptr(C++98提供,C++11建议丢弃)std::unique_ptr(C++11提供)std::shared_ptr(由C++11提供)std::auto_ptr演示如下Howtousestd::auto_ptrtoavoidpotentialresourceleaksintheffunction:voidf(){std::auto_ptrpa(createA());//调用工厂函数...//一如既往地使用pa}//离开作用域后,通过auto_ptr的析构函数自动删除pa;这个简单的例子演示了“用对象管理资源”的两个关键思想:获取资源后立即放入管理对象中。上面代码中createA返回的resource被当做它的managerauto_ptr的初始值,立即放入management对象中。托管对象使用析构函数来确保资源释放。不管控制流如何离开块,一旦对象被销毁(例如,当对象超出作用域时),它的析构函数自然会被自动调用,资源就会被释放。为什么在C++11中不推荐使用auto_ptr?当然auto_ptr是有缺陷的,以后不建议再用了。auto_ptr有一个不寻常的属性:如果它们通过“复制构造函数或赋值运算符函数”复制,它们将变为null,并且复制的指针获得资源的唯一所有权!请参见以下示例:std::auto_ptrpa1(createA());//pa1指向createA返回对象std::auto_ptrpa2(pa1);//现在pa2指向对象,pa1将被设置为nullpa1=pa2;//现在pa1指向一个对象,pa2将被设置为null,这是一种奇怪的复制行为。如果再次使用指向null的指针,势必会导致程序崩溃。意味着auto_ptr不是管理动态分配资源的法宝。std::unique_ptrunique_ptr也采用所有权模型,但在使用时,直接禁止通过拷贝构造函数或赋值运算符函数拷贝指针对象。下面的例子编译时会报错:std::unique_ptrpa1(createA());//pa1指向createA返回的对象std::unique_ptrpa2(pa1);//编译错误!pa1=pa2;//编译错误!std::shared_ptrshared_ptr使用拷贝构造函数或赋值运算符函数后,引用计数会累加,两个指针对象指向同一块内存,这一点不同于unique_ptr和auto_ptr。voidf(){std::shared_ptrpa1(createA());//pa1指向createA的返回对象std::shared_ptrpa2(pa1);//引用计数+1,pa2和pa1指向同一个A内存pa1=pa2;//引用计数+1,pa2和pa1指向同一块内存}当对象离开作用域时,shared_ptr会将引用计数值设置为-1,直到引用计数值为0时才会删除该对象。由于shared_ptr在释放空间的时候会提前判断引用计数值的大小,所以不会出现多次删除一个对象的错误。总结-请记住防止资源泄漏,请使用RAII(ResourceAcquisitionIsInitaliaztion-资源获取的时机是初始化的时机)对象,它们在构造函数中获取资源,并在析构函数中释放资源。两个推荐使用的RAII类分别是std::unique_ptr和std::shared_ptr。前者不允许复制动作,后者允许复制动作。但是不推荐使用std::auto_ptr。如果选择auto_ptr,复制操作将使其(复制的对象)指向null。细节02:注意资源管理类中的复制行为假设。我们使用C语音的API函数来处理Mutex类型的互斥对象。有两个可用的锁定和解锁功能:voidlocak(Mutex*pm);//锁定pm指的是互斥体voidunlock(Mutex*pm);//解锁互斥量为了确保一个锁定的互斥量永远不会被忘记解锁,我们不妨创建一个类来管理锁资源。这样的类必须遵守RAII代码,即“资源在构造时获取,在解构时释放”:classLock{public:explicitLock(Mutex*pm)//Constructor:pMutex(pm){lock(pMutex);}~Lock()//析构函数{unlock(pMutex);}私人:互斥*pMutex;};这样定义的Lock符合RAII方式:Mutexm;//定义你需要的互斥锁。..{//创建一个本地块作用域Lockm1(&m);//Lockthemutex...}//离开块作用域时自动解锁mutex这很好,但是如果Lock对象被复制了,会发生什么?锁定m1(&m);//锁定mLockm2(&m1);//将m1复制到m2,会发生什么?这就是我们需要思考和面对的:“当一个RAII对象被复制时会发生什么?”大多数时候你会选择以下两种可能:禁止复制。如果RAII不允许被拷贝,那么我们需要将类的拷贝构造函数和赋值运算符声明为private。使用引用计数。有时我们想持有资源直到它的最后一个对象被消耗掉。在这种情况下,复制RAII对象时,资源的“引用计数”应该增加。std::shared_ptr就是这样做的。如果前面提到的Lock打算使用引用计数,它可以使用std::shared_ptr来管理pMutex指针,不幸的是std::shared_ptr的默认行为是“当引用计数为0时删除它的引用”,那不是我们期望的行为,因为释放Mutex的操作是解锁而不是删除。幸运的是std::shared_ptr允许指定自定义删除方法,即函数或函数对象。如下:classLock{public:explicitLock(Mutex*pm):pMutex(pm,unlock)//用某个Mutex初始化shared_ptr,//并使用unlock函数作为deleter。{锁(pMutex.get());//get获取指针地址}private:std::shared_ptrpMutex;//使用shared_ptr};请注意,此示例中的Lock类不再声明析构函数。因为编译器会自动创建一个默认的析构函数来自动调用其非静态成员变量(本例中为pMutex)的析构函数。pMutex的析构函数会在互斥锁的引用计数为0时自动调用std::shared_ptr的删除器(在本例中为unlock)。总结-请记住,复制一个RAII对象必须同时复制其托管资源(深复制),所以资源的复制行为决定了RAII对象的复制行为。常见且常见的RAII类复制行为有:禁止复制,实现引用计数。细节03:在资源类中,提供访问智能指针到原始资源的“显式”转换,即通过get成员函数将其转换为原始指针对象。上面提到的“智能指针”有:std::auto_ptr、std::unique_ptr、std::shared_ptr。它们都可以访问原始资源,并且都提供一个执行显式转换的get成员函数,即它返回智能指针内的原始指针(的副本)。例如使用std::shared_ptr这样的智能指针来保存createA()返回的指针对象:std::shared_ptrpA(createA());假设你想用某个函数处理A对象,像这样:intgetInfo(constA*pA);你想这样称呼它:std::shared_ptrpA(createA());getInfo(pA);//错误!!将编译错误,因为getInfo需要一个A指针对象,而不是std::shared_ptr类型的对象。这时候就需要使用std::shared_ptr智能指针提供的get成员函数来访问原始资源:std::shared_ptrpA(createA());getInfo(pA.get());//很好,将pA中的原始指针“隐式”转换为getInfo智能指针的方法是通过指针值运算符。智能指针用指针值运算符(operator->和operator*)重载,允许隐式转换为底层原始指针:classA{public:boolisExist()const;...};A*createA();//工厂函数,创建指针对象std::shared_ptrpA(createA());//让shared_ptr管理对象资源boolexist=pA->isExist();//通过operator->exist2=(*pA).isExist();访问资源bool;//通过运算符访问资源*和大多数设计良好的类一样,它隐藏了程序员不需要看到的东西,却拥有程序员需要的一切。因此,对于我们自己设计的RAII类,我们也需要提供一种“获取它所管理的资源”的方法。总结-请记住,API通常需要访问原始资源,因此每个RAII类都应提供“获取它管理的资源”方法。可以通过显式或隐式转换访问原始资源。一般来说,显式转换更安全,但隐式转换更方便。细节04:下面的动作new和delete成对使用有什么问题?std::string*strArray=newstd::string[100];...删除strArray;一切似乎都井井有条。用的是new,对应的delete也是匹配的。但是仍然有些地方是完全错误的。strArray中包含的100个字符串对象中的99个被正确删除的可能性不大,因为它们的析构函数可能未被调用。当使用new时,会发生两件事:分配内存(通过一个名为operatornew的函数)并为这块内存调用一个或多个构造函数当使用delete时,会发生两件事:为这块内存调用一个或多个析构函数,然后内存将被释放(通过一个叫做operatordelete的函数)。delete最大的问题是:内存中有多少对象要删除?这个答案决定了需要执行多少析构函数。对象数组使用的内存通常还包括“数组大小”的记录,以便delete知道需要调用多少次析构函数。单对象内存没有这条记录。可以想象两者不同的内存布局如下,其中n是数组的大小:当你对指针使用delete时,delete要知道内存中是否有“数组大小记录”的唯一方法是让你告诉它。如果加上方括号[]删除,delete假定指针指向数组,否则假定指针指向单个对象:std::string*strArray=newstd::string[100];std::string*strPtr=newstd::strin;...delete[]strArray;//删除一个对象deletestrPtr;//删除一个对象数组游戏规则很简单:如果在new表达式中使用了[],则相应的delete表达式中也必须使用[]。如果在new表达式中不使用[],请确保不要在相应的delete表达式中使用[]。总结-请记住,如果在新表达式中使用[],则还必须在相应的删除表达式中使用[]。如果在new表达式中不使用[],请确保不要在相应的delete表达式中使用[]。细节05:用单独的语句将newed(新)对象放入智能指针假设我们有如下演示函数:intgetNum();voidfun(std::shared_ptrpA,intnum);现在考虑调用fun:fun(newA(),getNum());无法编译,因为std::shared_ptr构造函数需要裸指针,而且构造函数是显式构造函数,不能隐式转换。如果这样写,可以编译通过:fun(std::shared_ptr(newA),getNum());没想到,上面的调用可能会泄漏资源。接下来我们一步步分析为什么会存在内存泄漏的可能。在进入fun函数之前,必须先执行每个实参。上面第二个实参只是对getNum函数的简单调用,但是第一个实参std::shared_ptr(newA)由两部分组成:执行newA表达式调用std::shared_ptr构造函数和then在调用fun函数之前,必须完成以下三件事:调用getNum函数执行新的A表达式调用std::shared_ptr构造函数。他们的执行顺序是否一定如上?可以肯定的是,newA必须在std::shared_ptr构造函数之前执行。但是对getNum的调用可以首先执行,也可以第二个或第三个执行。如果编译器选择第二种:donewAexpressioncallgetNumfunctioncallstd::shared_ptrconstructor如果在调用getNum函数时出现异常会怎样?这样的话,newA返回的指针就不会放到std::shared_ptr智能指针中,就会出现内存泄漏。避免此类问题的方法很简单:使用分离语句。单独写出来:createA放在一个智能指针里面,然后把智能指针传给fun函数。std::shared_ptrpA(新A);//首先构造智能指针对象fun(pA,getNum());//此调用操作永远不会导致泄漏。上述方法可以避免序列引起的内存泄漏。总结-记住在单独的语句中将新的(newed)对象存储在智能指针中。如果不这样做,一旦抛出异常,可能会导致难以察觉的资源泄漏。最后,本文的部分内容参考了《Effective C++ (第3版本)》的第三章。前两章内容,可以看旧文《学过 C++ 的你,不得不知的这 10 条细节!》关注公众号,后台回复“我要学”,即可获得精心整理的《ServerLinuxC/C++》成长之旅(书籍资料+思维导图)