文章目录
1. 为什么需要智能指针?
下面我们分析一下下面这段程序有没有什么内存方面的问题?提示一下:注意分析 MergeSort 函数中的问题。
#include <vector>
void _MergeSort(int* a, int left, int right, int* tmp)
{
if (left >= right) return;
int mid = left + ((right - left) >> 1);
// [left, mid]
// [mid+1, right]
_MergeSort(a, left, mid, tmp);
_MergeSort(a, mid + 1, right, tmp);
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int index = left;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
tmp[index++] = a[begin1++];
else
tmp[index++] = a[begin2++];
}
while (begin1 <= end1)
tmp[index++] = a[begin1++];
while (begin2 <= end2)
tmp[index++] = a[begin2++];
memcpy(a + left, tmp + left, sizeof(int)*(right - left + 1));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
_MergeSort(a, 0, n - 1, tmp);
// 这里假设处理了一些其他逻辑
vector<int> v(1000000000, 10);
// ...
// free(tmp);
}
int main()
{
int a[5] = { 4, 5, 2, 3, 1 };
MergeSort(a, 5);
return 0;
}
【问题分析】:上面的问题分析出来我发现有以下两个问题?
- malloc 出来的空间,没有进行释放,存在内存泄漏的问题;
- 异常安全问题。如果在 malloc 和 free 之间如果存在抛异常,那么还是有内存泄漏。这种问题就叫异常安全。
2. 内存泄漏
2.1 什么是内存泄漏
什么是内存泄漏
:内存泄漏就是指因为疏忽或者某种错误导致未能释放已经不再使用的内存。内存泄漏并不是指内存在物理上的消失,而是指程序在分配内存后,由于设计错误,失去了对该段内存的控制,因而造成内存的浪费。
内存泄漏的危害
:长期运行的程序,如果出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
void MemoryLeaks()
{
// 1.内存申请了忘记释放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 2.异常安全问题
int* p3 = new int[10];
Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
delete[] p3;
}
2.2 内存泄漏分类
C/C++ 程序中我们一般关心两种方面的内存泄漏:
- 堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。 - 系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
2.3 如何避免内存泄漏
- 养成良好的编码规范,申请的内存空间记着匹配的去释放。
- 采用 RAII 思想或者智能指针来管理资源
- 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
- 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总结一下:
内存泄漏非常常见,解决方案一般分两种:1、事前预防型,如智能指针等。2、事后查错型。如泄漏检测工具。
3. 智能指针的使用及原理
3.1 RAII
RAII(Resource Acquisition Is Initialization)
是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络桥接、互斥量等等)的简单技术。在对象构造时获取资源,接着控制对资源的访问使之在对象的声明周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任委托给了一个对象。
使用 RAII 的好处:
- 不需要显式释放资源
- 采用这种方式,对象所需的资源在其生命周期内始终保持有效
#include<iostream>
using namespace std;
// 使用RAII思想设计的SmartPtr类
template<typename T>
class SmartPtr{
private:
T *_ptr;
public:
SmartPtr(T* ptr = nullptr) : _ptr(ptr){}
~SmartPtr()
{
if(_ptr)
{
cout << "调用析构" << endl;
delete _ptr;
}
}
};
int main(int argc ,char* argv[])
{
int *a = (int*)malloc(sizeof(int));
SmartPtr<int> s(a);
}
运行结果如下:确实调用了虚构函数释放掉了内存。
3.2 智能指针的原理
上述的 SmartPtr 还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过 -> 去访问所指空间的内容,因此:
AutoPtr 模板类中还需要将 *、-> 重载下,才可让其像指针一样去使用
。
#include<iostream>
using namespace std;
template<typename T>
class SmartPtr{
private:
T *_ptr;
public:
SmartPtr(T* ptr = nullptr) : _ptr(ptr){}
// 重载 * 运算符
T &operator *()
{
return *_ptr;
}
// 重载 -> 运算符
T *operator->()
{
return _ptr;
}
~SmartPtr()
{
if(_ptr)
{
cout << "调用析构" << endl;
delete _ptr;
}
}
};
class A{
public:
int count;
A(int i) : count(i){}
void setCount(int i)
{
count = i;
}
};
int main(int argc ,char* argv[])
{
// 1. 基本数据类型使用智能指针
int num = 10;
int *a = (int*)malloc(sizeof(int));
a = #
SmartPtr<int> s(a);
*s = 100;
cout << *a << endl; // 100
// 自定义类型使用智能指针
A *a1 = new A(20);
SmartPtr<A> s1(a1);
s1->setCount(200);
cout << s1->count << endl; // 200
}
运行结果:调用了两次析构函数
智能指针的原理
- RAII 特性
- 重载 operator* 和 operator ->,具有像指针一样的行为。
3.3 std::auto_ptr
C++98版本的库中就提供了auto_ptr 的智能指针。下面演示 auto_ptr 的使用及问题。
#include <memory>
class A{
public:
A(){ cout << "A()" << endl; }
~A(){ cout << "~A()" << endl; }
int m;
};
int main()
{
auto_ptr<A> ap(new A);
auto_ptr<A> copy(ap);
// auto_ptr的问题:当对象拷贝或者赋值后,前面的对象就悬空了
// C++98中设计的auto_ptr问题是非常明显的,所以实际中很多公司明确规定了不能使用auto_ptr
ap->m= 2018;
return 0;
}
auto_ptr 的实现原理:管理权转移的思想,下面简化模拟实现了一份 AutoPtr 来了解它的原理。
#include <iostream>
#include <memory>
using namespace std;
template<typename T>
class AutoPtr
{
private:
T *_ptr;
public:
AutoPtr(T *ptr = nullptr) : _ptr(ptr){}
// 拷贝构造
// 一旦发生拷贝,就将ap中资源转移到当前对象中,然后另ap与其所管理资源断开联系,
// 这样就解决了一块空间被多个对象使用而造成程序奔溃问题
AutoPtr(AutoPtr<T>& ap) : _ptr(ap._ptr)
{
ap._ptr = nullptr;
}
// 重载赋值运算符
AutoPtr<T> &operator=(AutoPtr<T> &ap)
{
// 检测是否为自己赋值
if(this != &ap)
{
// 释放当前对象中的资源
delete _ptr;
// 转移 ap的资源到当前对象中
_ptr = ap._ptr;
ap,_ptr = nullptr;
}
return *this;
}
// 重载 * 运算符
T& operator *()
{
return *_ptr;
}
// 重载 -> 运算符
T* operator->()
{
return _ptr;
}
~AutoPtr()
{
if(_ptr)
{
cout << "析构函数" << endl;
delete _ptr;
}
}
};
class A{
public:
A(){ cout << "A()" << endl; }
~A(){ cout << "~A()" << endl; }
int m;
};
int main(int argc ,char* argv[])
{
// 现在再从实现原理层来分析会发现,这里拷贝后把ap对象的指针赋空了,导致ap对象悬空
// 通过ap对象访问资源时就会出现问题。
AutoPtr<A> ap(new A);
AutoPtr<A> cp(ap);
ap->m = 10;
return 0;
}
3.4 std::unique_ptr
C++11中开始提供更靠谱的 unique_ptr
void testUniquePtr()
{
unique_ptr<int> ap(new int);
unique_ptr<int> cp;
// unique_ptr 的设计思路非常粗暴——防拷贝,就是不让拷贝和赋值
cp = ap; // error
*cp = 110;
cout << *cp << endl;
}
unique_ptr 的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份 UniquePtr 来了解它的原理。
template<typename T>
class UniquePtr
{
public:
UniquePtr(T *ptr = nullptr) : _ptr(ptr) {}
~UniquePtr()
{
if(_ptr)
{
std::cout << "析构函数" << endl;
delete _ptr;
}
}
// 重载 * 运算符
T &operator*()
{
return *_ptr;
}
// 重载 -> 运算符
T *operator->()
{
return _ptr;
}
private:
T* _ptr;
// 将拷贝构造和赋值运算符作为私有成员,防止拷贝和赋值
UniquePtr(const UniquePtr<T> &) = delete;
UniquePtr<T> &operator=(const UniquePtr<int> &) = delete;
};
3.5 std::shared_ptr
C++11开始提供更靠谱并且支持拷贝的 share_ptr
void testSharesPtr()
{
// shared_ptr 通过引用计数支持智能指针的拷贝
shared_ptr<int> ap(new int);
shared_ptr<int> cp(ap);
*ap = 100;
cout << *ap << endl; // 100
*cp = 200;
cout << *ap << endl; // 200
}
shared_ptr的原理
:是通过引用计数的方式来实现多个shared_ptr 对象之间共享资源。
- shared_ptr 在其内部,给每个资源都维护着一份计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
- 如果引用计数为0,就说明自己是最后一个使用该资源的对象,必须释放该资源。
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其它对象就成野指针。
#include <pthread.h>
#include <iostream>
using namespace std;
template<typename T>
class SharedPtr
{
public:
SharedPtr(T *ptr = nullptr) : _ptr(ptr), _pRefCount(new int(1))
{
pthread_mutex_init(&mutex, NULL);
}
~SharedPtr()
{
Release();
}
T &operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
SharedPtr(const SharedPtr<T> &cp) : _ptr(cp._ptr), _pRefCount(new int(1))
{
pthread_mutex_init(&mutex, NULL);
AddRefCount();
}
SharedPtr<T> &operator=(const SharedPtr<T> &cp)
{
if(this != &cp)
{
// 释放旧资源
Release();
// 共享管理新对象的资源,并增加引用计数
_ptr = cp._ptr;
_pRefCount = cp._pRefCount;
mutex = cp.mutex;
AddRefCount();
}
return *this;
}
void AddRefCount()
{
pthread_mutex_lock(&mutex);
++(*_pRefCount);
pthread_mutex_unlock(&mutex);
}
int UseCount()
{
return *_pRefCount;
}
private:
T *_ptr; // 指向管理资源的指针
int *_pRefCount; // 引用计数
pthread_mutex_t mutex; // 互斥锁
void Release()
{
bool delflag = false;
// 引用计数减一
pthread_mutex_lock(&mutex);
(*_pRefCount)--;
if(*_pRefCount == 0)
{
cout << "delete _ptr" << endl;
delete _ptr;
delete _pRefCount;
delflag = true;
}
pthread_mutex_unlock(&mutex);
if(delflag)
pthread_mutex_destroy(&mutex);
}
};
int main()
{
SharedPtr<int> sp1(new int(10));
SharedPtr<int> sp2(sp1);
*sp2 = 20;
cout << sp1.UseCount() << endl; // 1
cout << sp2.UseCount() << endl; // 2
SharedPtr<int> sp3(new int(10));
sp2 = sp3;
cout << sp1.UseCount() << endl; // 1
cout << sp2.UseCount() << endl; // 2
cout << sp3.UseCount() << endl; // 2
sp1 = sp3;
cout << sp1.UseCount() << endl; // 3
cout << sp2.UseCount() << endl; // 3
cout << sp3.UseCount() << endl; // 3
}
【运行结果】
std::shared_ptr 的线程安全问题
通过下面的程序我们来测试 shared_ptr 的线程安全问题。需要注意的是 shared_ptr 的线程安全问题分为两个方面:
- 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未被释放或者程序崩溃的问题。所以智能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作是线程安全的。
- 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。
// SharedPtr.h
template<typename T>
class SharedPtr
{
public:
SharedPtr(T *ptr = nullptr) : _ptr(ptr) {}
~SharedPtr()
{
if(_ptr)
{
std::cout << "析构函数" << std::endl;
delete _ptr;
}
}
T &operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
SharedPtr(const SharedPtr<T> &cp) : _ptr(cp._ptr)
{
AddRefCount();
}
SharedPtr<int> &operator=(const SharedPtr<T> &cp)
{
if(this != &cp)
{
// 释放旧资源
Release();
// 共享管理新对象的资源,并增加引用计数
_ptr = cp._ptr;
_pRefCount = cp._pRefCount;
_pMutex = cp._pMutex;
AddRefCount();
}
return *this;
}
void AddRefCount()
{
pthread_mutex_lock(_pMutex);
(*_pRefCount)++;
pthread_mutex_unlock(_pMutex);
}
private:
T *_ptr; // 指向管理资源的指针
int *_pRefCount; // 引用计数
pthread_mutex_t *_pMutex; // 互斥锁
void Release()
{
bool delflag = false;
// 引用计数减一
pthread_mutex_lock(_pMutex);
(*_pRefCount)--;
if(*_pRefCount == 0)
{
delete _ptr;
delete _pRefCount;
delflag = true;
}
pthread_mutex_unlock(_pMutex);
if(delflag)
delete _pMutex;
}
};
验证代码:
- 演示引用计数线程安全问题,就把 SharedPtr.h 中的 锁去掉
- 演示可能不会出现线程安全问题,因为线程安全是偶现性问题,main 函数中n改大一些概率就变大了
下列代码是线程安全的。
#include <iostream>
#include "SharedPtr.h"
using namespace std;
class Date{
public:
Date(){}
~Date(){}
int m;
};
typedef struct{
SharedPtr<Date> d;
int n;
}param_t;
void *SharedPtrFunc(void *arg)
{
param_t *p = (param_t *)arg;
SharedPtr<Date> sp = p->d;
size_t n = p->n;
cout << sp.Get() << endl;
for(size_t i = 0; i < n; i++)
{
// 这里智能指针访问管理的资源,智能指针析构会--计数,这里是线程安全的
SharedPtr<Date> cp(sp);
cp->m++;
}
}
int main()
{
SharedPtr<Date> sp(new Date);
cout << sp.Get() << endl;
size_t n = 100000;
param_t p;
p.d = sp;
p.n = n;
pthread_t tid1,tid2;
pthread_create(&tid1, NULL, SharedPtrFunc, (void *)&p);
pthread_create(&tid2, NULL, SharedPtrFunc, (void *)&p);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
cout << sp->m << endl;
}
运行结果如下:m 的值被加了 200000 次。
std::shared_ptr 的循环引用
#include <iostream>
#include <memory>
using namespace std;
struct ListNode{
int data;
shared_ptr<ListNode> pre;
shared_ptr<ListNode> next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl; // 1
cout << node2.use_count() << endl; // 1
node1->next = node2;
node2->pre = node1;
cout << node1.use_count() << endl; // 2
cout << node2.use_count() << endl; // 2
}
【运行结果】
为什么这里没有调用析构函数呢?
循环引用分析:
- node1 和 node2 两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动 delete。
- node1 的 next 指向 node2,node2 的 pre 指向 node1,引用计数变成2。
- node1 和 node2 析构,引用计数减到1,但是 next 还指向下一个节点。但是 pre 还指向上一个节点。
- 也就是说 next 析构了,node2 就释放了。
- 也就是说 pre 析构了,node1 就释放了。
- 但是 next 是与 node 的成员,只有 node1 释放了,next 才会析构。而 node1 由 pre 管理,pre 属于 node2 的成员,这就叫循环引用,谁也不会释放。
- 所以,最后都没有调用析构函数
那么,有什么解决方案吗?
解决方案:在引用计数场景下,把节点中 pre 和 next 换成 weak_ptr 就可以了
原理:node1->next = node2; node2->pre = node1; 时 weak_ptr 的 next 和 pre 不会增加 node1 和 node2 的引用计数。
代码如下:
#include <iostream>
#include <memory>
using namespace std;
struct ListNode{
int data;
weak_ptr<ListNode> pre;
weak_ptr<ListNode> next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl; // 1
cout << node2.use_count() << endl; // 1
node1->next = node2;
node2->pre = node1;
cout << node1.use_count() << endl; // 2
cout << node2.use_count() << endl; // 2
}
【运行结果】
相信通过以上例子,大家对 shared_ptr 有了一定的了解了,但是我们发现上述的例子中智能指针管理的都是通过 new 出来的对象,那么,如果不是 new 出来的对象,如何通过智能指针进行管理呢?其实 shared_ptr 设计了一个删除器来解决这个问题。
#include <iostream>
#include <memory>
using namespace std;
// 仿函数的删除器
template<typename T>
struct FreeFunc{
void operator()(T *ptr)
{
cout << "free: " << ptr << endl;
free(ptr);
}
};
template<typename T>
struct DeleteArrayFunc{
void operator()(T *ptr)
{
cout << "delete[] " << ptr << endl;
delete[] ptr;
}
};
int main()
{
FreeFunc<int> f;
shared_ptr<int> sp1((int *)malloc(4), f);
DeleteArrayFunc<int> d;
shared_ptr<int> sp2((int *)malloc(4), d);
}
【运行结果】
4. C++11 和 boost 中智能指针的关系
- C++98 中产生了第一个智能指针 auto_ptr。
- C++ boost 给出了更实用的 scoped_ptr 和 shared_ptr 和 weak_ptr。
- C++ TR1,引入了 shared_ptr 等。不过注意的是 TR1 并不是标准版。
- C++11 引入了 unique_str 和 shared_ptr 和 weak_ptr 。需要注意的是 unique_ptr 对应 boost 的 scoped_ptr。并且这些智能指针的实现原理是参考 boost 中的实现的。