C++和C#,python等语言不同,其本身是没有gc机制的,因此我们需要手动来管理内存,一般来说,我们只要将每一个new出来的对象delete之后,就可以保证资源不被泄漏,但万事都不是那么理想的,我们可能因为忘记delete而导致内存泄漏,并且当开发大型项目时,我们可能不了解其他人的代码意图,导致不能正确释放内存,因此,我们有几种方法来解决该问题。
RAII即当资源取得时机就是初始化时机,也就是以内存管理对象。我们首先要明白我们的程序中的变量存放在什么位置,一般来说,我们创建的对象会放在堆中、栈中、静态区中。栈中的对象是由系统来管理的,比如局部变量会在作用域离开局部时被销毁。但堆中的内存是由程序员手动控制的,因此我们要管理好的主要是堆中的内存,我们来看以下代码
void test(){
Point *p = createPoint();
...
delete p;
}
createPoint()函数返回一个Point类型的指针,我们在堆中创建的指针p指向createPoint()返回的指针,我们在…程序块中有可能提前return,导致createPoint()返回的指针资源没被删除,占用了内存导致内存泄漏。我们知道实例化一个类有两种方法,一是在栈中创建,也就是Point p,二是在堆中创建,也就是Point *p = new Point();接下来我们看一个例子。
Point* test(){
Point p;
return &p;
}
该函数返回一个Point类型的指针,但这种写法是错误的,因为我们是在栈中创建的对象p,p由系统管理,当该函数执行完毕后,p被销毁,那么test()函数就返回了一个已经不存在的指针,便会导致错误。
Point* test(){
Point *p = new Point();
return p;
}
对于上述函数,我们要了解,p是一个局部变量,是在栈中分配的4个字节的地址,它里面保存的是在堆中创建的new Point()数据的地址,此时我们将p返回出去,也就是将在堆中创建的Point数据的地址给传出去,我们仍然可以访问之前我们在堆中创建的Point数据。我们画一个图帮助我们形象理解
那么 return p即返回的0x0002a,也就是new Point()这个数据存放的地址,这样我们就可以对我们在堆中申请的内存进行操作。
当我们使用RAII时我们要明确当我们管理的资源需要拷贝时我们需要作出什么行为。一是禁止拷贝,此时我们可以使用class Point:public Uncopyable{};来禁止拷贝行为,具体可参见《Effecitive C++》条款6。二是我们可以使用引用计数,每当有指针指向该资源,引用计数+1,当没有指针指向该资源时,就将其删除。C++引入了shared_ptr来实现该功能,但shared_ptr无法解决环状引用的问题,shared_ptr还可以指定删除器,当释放资源时执行删除器,比如我们可以在删除器中指定文件关闭,解除互斥锁等操作。
shared_ptr在大多数情况下可以解决我们的问题,但有时我们需要自定义一些行为时可以创建自己的资源管理类。如下所示
#include <iostream>
using namespace std;
class File{
};
class Uncopyable
{
protected: // allow construction
Uncopyable() {} // and destruction of
~Uncopyable() {} // derived objects...
private:
Uncopyable(const Uncopyable&); // ...but prevent copying
Uncopyable& operator=(const Uncopyable&);
};
class UncopyableExample: private Uncopyable
{
};
class FileHandle:public Uncopyable{
public:
FileHandle(File *file):m_pFile(file){
cout << "打开文件" << endl;
}
~FileHandle(){
if(m_pFile != nullptr)
{
delete m_pFile;
m_pFile = nullptr;
cout << "关闭文件" << endl;
}
}
private:
File *m_pFile;
};
int main(int argc, char *argv[])
{
FileHandle f(new File());
return 0;
}
File类是文件类,Uncopyable类可用于阻止FileHandle类的拷贝行为,FIleHandle类用于管理File,在实例化时打开文件,在析构时关闭文件,保证资源得到释放。利用shared_ptr指定删除器的版本如下:
class File{
};
void closeFile(File *file){
cout << "关闭文件" << endl;
}
int main(int argc, char *argv[])
{
shared_ptr<File> p(new File(), closeFile);
return 0;
}
总结以上情况,只要我们能理解好RAII就能正确做好内存管理,避免出现内存泄漏等问题。