目录
由于C++11引入异常之后,执行流乱跳,所以导致之前 malloc/new 的空间很容易没有被释放,导致内存泄露问题。
所以这时候,我们就需要一个可以自动释放的对象来管理这些空间,来让这些空间都能被释放,不会发生内存泄露。
下面我们先看下异常如何会引起执行流乱跳的问题。
我们看下面一段代码,下面我们在 main 函数中中调用一个 test 函数, test 函数中调用 new 开空间等,还调用了 div 函数,div 函数里面如果发现有 0 ,那么就抛出除0错误。
- class A
- {
- public:
- A(int a = 0)
- :_a(a)
- {
- cout << "构造函数:A(int a = 0)" << endl;
- }
-
- ~A()
- {
- cout << "析构函数:~A()" << endl;
- }
-
- int get_a()
- {
- return _a;
- }
-
- private:
- int _a;
- };
-
- int Div(int a, int b)
- {
- if (b == 0)
- {
- throw invalid_argument("除0错误");
- }
- return a / b;
- }
-
- void test1()
- {
- A* APtr = new A;
- int a = 0, b = 0;
- cont << "输入:";
- cin >> a >> b;
- Div(a, b);
-
- delete APtr;
- }
-
- int main()
- {
- try
- {
- test1();
- }
- catch (exception& e)
- {
- cout << e.what() << endl;
- }
- return 0;
- }
这个test函数里面,我们在 delete APtr 之前调用 div 函数。
如果有除0错误,那么就会直接跳到 main 函数里面,不会进行释放 A 类型的指针,所以就发生了内存泄露问题。
结果:
- 构造函数:A(int a = 0)
- 输入:1 0
- 除0错误
这里并没有调用析构函数!!!
如果没有除0的话,那么还是会正常释放的:
- 构造函数:A(int a = 0)
- 输入:1 1
- 析构函数:~A()
所以如果是我们自己 mallo/new 的指针的话,那么还是容易导致内存泄露。
那么怎么解决呢?我们可以在 test 里面捕获一下异常,然后再里面进行释放内存,然后再把错误抛出去!
- void test1()
- {
- A* APtr = new A;
- cout << "输入:";
- int a = 0, b = 0;
- cin >> a >> b;
- try
- {
- Div(a, b);
- }
- catch (...)
- {
- delete APtr;// 释放
- throw;
- }
- delete APtr;
- }
如果是这样子的话,那么就可以解决了。
结果:
- 构造函数:A(int a = 0)
- 输入:1 0
- 析构函数:~A()
- 除0错误
但是上面这种情况还是比较简单的情况,那么如果复杂一点呢?
- void test2()
- {
- A* APtr1 = new A(10); // 这里可能会抛异常
- A* APtr2 = new A(20); // 这里可能会抛异常
-
- div(1, 0); // 这里可能会抛异常
-
- delete APtr1;
- delete APtr2;
- }
这里三处可能会抛异常,那么怎么办呢?
如果是第一处抛异常,那么还好,没有内存泄露。
如果是第二处抛异常,那么我们还需要释放第一处开的空间。
如果是第三处抛异常,我们又需要释放第一处和第二处开的空间,而且如果又更多的动态内存的申请呢?
所以这样就很容易导致程序出现问题!
那么我们可以怎么解决?
首先我们先说一下什么是智能指针。
当我们需要对 malloc/new 的空间进行管理时,我们可不可以让一个对象去管理这段空间。
当这个对象构造时,我们把空间交给这个对象。
当该对象析构时,然后让该对象释放掉这段空间。
所以指针指针也是一个对象。
auto_ptr 是一个智能指针,我们下面先看一下如何实现,可以让该对象释放时可以释放动态开辟的空间。
首先我们可以再该对象的构造函数里面用一个指针去构造,而该对象也就是管理的这段空间:
- template<class T>
- class auto_ptr
- {
- public:
- // 构造函数 用一个指针初始化
- auto_ptr(T* ptr = nullptr)
- :_ptr(ptr)
- {}
-
- // 析构函数 释放掉管理的空间
- ~auto_ptr()
- {
- if (_ptr)
- {
- delete _ptr;
- }
- }
- private:
- T* _ptr;
- };
那么我们使用 auto_ptr 看一下是否会又内存泄露:
- void test3()
- {
- lxy::auto_ptr Aptr1 = new A(10);
- lxy::auto_ptr Aptr2 = new A(20);
-
- Div(1, 0);// 除0错误,会抛异常
- }
结果:
- 构造函数:A(int a = 0)
- 构造函数:A(int a = 0)
- 析构函数:~A()
- 析构函数:~A()
- 除0错误
这里看到并没有内存泄露。
而且其实这里我们也并没有手动的释放开辟的空间,我们只是让 auto_ptr 进行管理了。
但是如果我们想要访问A对象里面的内容呢?
所以我们的智能指针还需要做到像指针一样。
也就是重载 * 和 ->
- T& operator*()
- {
- return *_ptr;
- }
-
- T* operator->()
- {
- return _ptr;
- }
测试代码:
- void test3()
- {
- lxy::auto_ptr Aptr1 = new A(10);
- lxy::auto_ptr Aptr2 = new A(20);
-
- cout << Aptr1->get_a() << endl;
- cout << (*Aptr2).get_a() << endl;
- }
结果:
- 构造函数:A(int a = 0)
- 构造函数:A(int a = 0)
- 10
- 20
- 析构函数:~A()
- 析构函数:~A()
这里就是没问题的,那么如果我们想拷贝一下呢?
我们试一下:
- void test4()
- {
- lxy::auto_ptr Aptr1 = new A(10);
- lxy::auto_ptr Aptr2(Aptr1);
- }
上面这段代码,用 Aptr1 拷贝 Aptr2 这里是浅拷贝,所以auto_ptr 里面的成员变量是指针类型,发生值拷贝
所以此时的Aptr1 和 Aptr2 里面的指针指向同一块空间。
然后等出作用域的时候,两个对象都会释放,然后就会导致一块空间被释放多次。
结果:
- 构造函数:A(int a = 0)
- 析构函数:~A()
- 析构函数:~A()
上面就奔溃掉了~
那么怎么解决呢?我们可以采用管理资源转移的方法来实现!
也就是当我们发生拷贝时,我们就可以让管理的内容从a对象转移到b对象!
- ~auto_ptr()
- {
- if (_ptr)
- {
- delete _ptr;
- }
- }
这里我们再看上面的那一段代码就没问题了:
- 构造函数:A(int a = 0)
- 析构函数:~A()
正常释放。
但是当我们引入资源转移之后,其他的问题又来了,就是悬空!
当我们现在再使用 Aptr1 对象访问里面的内容时,就会报错!
所以有人使用这个的话,那么又是一个严重的问题。
- // 这里为了方便测试,我们将 A 类的成员变量公有了...
- void test4()
- {
- lxy::auto_ptr Aptr1 = new A(10);
- lxy::auto_ptr Aptr2(Aptr1);
-
- (*Aptr1)._a++;
- (*Aptr2)._a++;
- }
结果:
构造函数:A(int a = 0)
上面对空指针解引用了,会直接导致进程奔溃掉。
那么这个问题如何解决呢?
我们看下面的其他类型的智能指针!
该智能指针解决上面指针悬空的方法就是直接不允许拷贝。
- template<class T>
- class unique_ptr
- {
- public:
- unique_ptr(T* ptr = nullptr)
- :_ptr(ptr)
- {}
-
- ~unique_ptr()
- {
- if (_ptr)
- {
- delete _ptr;
- }
- }
-
- T& operator*()
- {
- return *_ptr;
- }
-
- T* operator->()
- {
- return _ptr;
- }
-
- private:
- unique_ptr(unique_ptr& ptr) {};
-
- unique_ptr& operator=(unique_ptr& ptr) {};
-
- private:
- T* _ptr;
- };
上面实现不允许拷贝的方法就是将拷贝构造和赋值重载都只声明不实现!
但是只声明不实现,可能会有人再类外面实现,所以为了避免这样做,可以声明为私有。
其实还有一种解决方法,那么就是关键字 delete
delete 表示删除掉该函数。
- unique_ptr(unique_ptr& ptr) = delete;
-
- unique_ptr& operator=(unique_ptr& ptr) = delete;
这就是将一个函数删除!
删除掉的函数,就不能被调用。
下面我们测试一下:
- void test5()
- {
- lxy::unique_ptr Aptr1 = new A(10);
- lxy::unique_ptr Aptr2(Aptr1);
- }
这里是不能调用的,我们可以看一下报错信息:
“lxy::unique_ptr::unique_ptr(lxy::unique_ptr &)”: 尝试引用已删除的函数
但是这里还是有问题,那么如果我们需要拷贝呢?
所以仅仅是这和 unique_ptr 还是不够的。
我们还是需要可以拷贝的智能指针,我们来看下面!
shared_ptr 是可以拷贝的,那么如何实现可以拷贝呢?
如果只是单纯的拷贝过去,那么就是值拷贝,可能会导致多次释放。
那么我们可以使用引用计数的方法,来表示该空间一共被多少个对象所管理。
那么这时候我们又出现问题了,我们怎么实现引用计数呢?
- template<class T>
- class shared_ptr
- {
- public:
-
- private:
- T* _ptr;
- int _count;
- };
上面这样实现可以吗?
不可以!
因为如果是成员变量的话,那么 count 是每一个对象都又一个的,那么就是无法起到引用计数的作用。
我们需要的是可以多个对象来管理一块空间的引用计数,而不是每一个对象都有一个引用计数!
那么我们怎么办呢?
我们再看下面的这种方法!
- template<class T>
- class shared_ptr
- {
- public:
-
- private:
- T* _ptr;
- static int _count;
- };
我们使用 static 的变量可以吗?
如果我们使用 static 的变量,那么该变量是属于整个类的。
同时也是属于该类的所有成员的,那么如果现在有两个管理不同空间的对象,那么此时的引用计数应该是多少呢?
所以此时也是不可以的!
那么我们应该怎么办?
我们可以让引用计数是一个动态的,也就是引用计数可以也是一个指针,让管理相同空间的对象的引用计数也是相同的!
- template<class T>
- class shared_ptr
- {
- public:
-
- private:
- T* _ptr;
- int* _pcount;
- };
所以此时,我们只需要再构造函数的时候对这两个变量初始化就可以了。
构造函数
- shared_ptr(T* ptr = nullptr)
- :_ptr(ptr)
- , _pcount(new int(1))
- {}
析构函数
- ~shared_ptr()
- {
- if (--(*_pcount) == 0)
- {
- delete _ptr;
- delete _pcount;
- }
- }
运算符重载
- T& operator*()
- {
- return *_ptr;
- }
-
- T* operator->()
- {
- return _ptr;
- }
拷贝构造
拷贝构造就是将成员变量的两个指针拷贝给其他的类。
拷贝结束后,对引用计数进行加加。
- shared_ptr(shared_ptr& ptr)
- :_ptr(ptr._ptr)
- ,_pcount(ptr._pcount)
- {
- ++(*_pcount);
- }
赋值重载
赋值重载这里需要考虑一些问题:
自己给自己赋值怎么办?
被拷贝对象应该怎么办
拷贝对象应该怎么办?
- shared_ptr& operator=(shared_ptr& ptr)
- {
- // 判断是否是自己给自己赋值
- if (_ptr == ptr._ptr)
- return *this;
-
- // 判断是否需要销毁空间
- if (--(*_pcount) == 0)
- {
- delete _ptr;
- delete _pcount;
- }
- _ptr = ptr._ptr;
- _pcount = ptr._pcount;
-
- ++(*_pcount);
- }
测试代码:
- void test6()
- {
- lxy::shared_ptr Aptr1(new A(20));
- lxy::shared_ptr Aptr2(Aptr1);
-
- lxy::shared_ptr Aptr3;
- Aptr3 = Aptr2;
-
- lxy::shared_ptr Aptr4(new A(100));
- lxy::shared_ptr Aptr5(Aptr4);
- }
结果:
- 构造函数:A(int a = 0)
- 构造函数:A(int a = 0)
- 析构函数:~A()
- 析构函数:~A()
上面这里无论是拷贝还是赋值都没有问题。
实际上 shared_ptr 再一般情况下都是没有问题的,但是再特殊情况下还是会有问题!
我们来看下面的代码:
- struct Node
- {
- A _a;
- lxy::shared_ptr
next; - lxy::shared_ptr
prev; - };
- void test7()
- {
- lxy::shared_ptr
n1(new Node) ; - lxy::shared_ptr
n2(new Node) ; - }
首先我们看 test7 函数,这样是没有问题的:
- 构造函数:A(int a = 0)
- 构造函数:A(int a = 0)
- 析构函数:~A()
- 析构函数:~A()
这里我们是可以正确的释放的。
我们下面让 n1 的 next 指向 n2, n2 的 prev 指向 n1。
- void test7()
- {
- lxy::shared_ptr
n1(new Node) ; - lxy::shared_ptr
n2(new Node) ; -
- n1->next = n2;
- n2->prev = n1;
- }
结果:
- 构造函数:A(int a = 0)
- 构造函数:A(int a = 0)
这里我们只调用到了构造函数,并没有析构为什么?
下面我们只让 n1 的 next 指向 n2试一下,看有没有问题!
- void test7()
- {
- lxy::shared_ptr
n1(new Node) ; - lxy::shared_ptr
n2(new Node) ; -
- n1->next = n2;
- }
结果:
- 构造函数:A(int a = 0)
- 构造函数:A(int a = 0)
- 析构函数:~A()
- 析构函数:~A()
这里看到只让 n1 的 next 指向 n2 是没有问题的,那么下面让 n2 的 prev 指向 n1 看有没有问题:
- void test7()
- {
- lxy::shared_ptr
n1(new Node) ; - lxy::shared_ptr
n2(new Node) ; -
- n2->prev = n1;
- }
结果:
- 构造函数:A(int a = 0)
- 构造函数:A(int a = 0)
- 析构函数:~A()
- 析构函数:~A()
这里看到,我们让 n2 的 prev 指向 n1 也是没有问题的。
说明如果我们只是单个指向的话,是不会出现刚才的内存泄露的问题的。
如果有互相指向,那么就是会出现问题!
下面分析一下为什么?
我们来分析一下互相指向的话释放是怎么释放的:
- struct Node
- {
- A _a;
- lxy::shared_ptr
next; - lxy::shared_ptr
prev; - };
-
- lxy::shared_ptr
n1(new Node) ; - lxy::shared_ptr
n2(new Node) ;
n1 什么时候释放?n1 这个节点由 n1 和 n2 的prev 共同管理,所以如果 n1 释放的话,那么就需要 n2 的 prev 释放了才释放。
那么n2 的 prev 什么时候释放?当 n2 释放了才释放。
n2 什么时候释放?n2 和 n1 的next 再共同管理着一块空间,那么 n2 释放就需要 n1 的next 释放了才释放。
那么 n1 的 next 什么时候释放?当 n1 释放了才释放。
这发现是一个循环一样的东西,首先我们是 n1 什么时候释放,而最后也牵扯到了当 n1 释放的时候才释放。
所以此时就是一个循环,那么怎么解决呢?
那么我们可以让 next 和 prev 不参与管理这块空间不久好了吗?
所以我们可以写一个 weak_ptr,该对象不参与管理,只是为了解决这个问题。
那么该对象如何实现呢?
首先我们需要让 weak_ptr 的类型可以指向 shared_ptr 的类型,所以我们需要一个 shared_ptr 的构造函数。
那么既然 weak_ptr 不参与管理,所以当 shared_ptr 构造 weak_ptr 后,引用计数也不能加加。
- template<class T>
- class weak_ptr
- {
- public:
- weak_ptr(T* ptr = nullptr)
- :_ptr(ptr)
- {}
-
- weak_ptr(shared_ptr
& ptr) - :_ptr(ptr._ptr)
- {}
-
- T& operator*()
- {
- return *_ptr;
- }
-
- T* operator->()
- {
- return _ptr;
- }
- private:
- T* _ptr;
- };
但是这里是不正确的,因为再 shared_ptr 类型的 构造函数,但是那里访问了 shared_ptr 的私有,所以这里会报错!
这里可以有两个解决方法:
将 weak_ptr 设置为 shared_ptr 的友元。
为 shared_ptr 提供一个 get 函数。
下面我们采用第二种。
- // shared_ptr::getPtr()
- T* getPtr()
- {
- return _ptr;
- }
-
- // weak_ptr::weak_ptr(shared_ptr
& ptr) - weak_ptr(shared_ptr
& ptr) - :_ptr(ptr.getPtr())
- {}
这样就可以解决问题了,下面我们再看一下那个测试的结果。
- struct Node
- {
- A _a;
-
- lxy::weak_ptr
next; - lxy::weak_ptr
prev; - };
-
- void test7()
- {
- lxy::shared_ptr
n1(new Node) ; - lxy::shared_ptr
n2(new Node) ; -
- n1->next = n2;
- n2->prev = n1;
- }
结果:
- 构造函数:A(int a = 0)
- 构造函数:A(int a = 0)
- 析构函数:~A()
- 析构函数:~A()
这时候就解决问题了。