【C++11】理解智能指针实现原理(+内存泄漏)

智能指针

  • 为什么用智能指针?
    • 1.1 什么是内存泄漏?
    • 1.2 内存泄漏的危害
    • 1.3 内存泄漏的分类
    • 1.4 如何避免内存泄漏
    • 1.4 内存泄漏与内存溢出
  • 2. 智能指针的原理
    • 2.1 RAII
    • 2.2 具有指针的行为
  • 3. 智能指针的使用
    • 3.1 shared_ptr
    • 3.2 循环引用
    • 3.3 智能指针的实现
    • 3.2 其他智能指针

为什么用智能指针?

智能指针是为了解决内存泄漏的问题。

在C语言中,我们用malloc申请内存,free释放内存;在C++中,既可以使用上述,对于自定义类型常常会用new申请,delete释放。一旦申请,忘了释放,就会导致内存泄漏。

即便有的时候,delete了,但是由于中间某些操作会异常,导致无法delete,也会造成内存泄漏。

所以引入了智能指针,能最大程度避免内存泄漏又能兼顾效率。

1.1 什么是内存泄漏?

是由于疏忽或错误导致程序未能释放已经不再使用的内存的情况。并不是指内存在物理上的消失,而是在应用程序分配某段内存后,因为设计错误,而失去了对该段内存的控制,因而造成了内存泄漏。

1.2 内存泄漏的危害

长期运行的程序出现内存泄漏,如操作系统、后台服务等,会导致响应越来越慢,最终卡死。

1.3 内存泄漏的分类

  1. 堆内存泄漏(heap leak)

堆内存泄漏指的是通过malloc / calloc / realloc / new 等从堆中分配的一块内存,在使用完成以后,没有使用free / delete 删除。假设因为程序的设计错误导致这块内存没有被删掉,就会导致以后无法使用,就会造成堆内存泄漏。

  1. 系统资源泄漏

指程序使用系统分配的资源,如套接字、文件描述符、管道没有使用对应的函数释放,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

1.4 如何避免内存泄漏

  1. 良好的编码规范,动态申请的内存记着要去释放。
  2. 采用RALL思想或者使用智能指针来管理资源。
  3. 出问题了使用内存泄漏检测工具。

1.4 内存泄漏与内存溢出

  1. 内存泄漏(momory leak):

是指程序在申请新的内存空间后,没有释放已经申请的内存空间,后果也许会造成内存溢出。

  1. 内存溢出(out of memory):

指程序申请内存时,没有足够的内存提供给申请者。内存不够用。

  1. 二者的关系
  • 内存泄漏最终会导致内存溢出。
  • 内存溢出就是你要的内存空间超过了系统能给你分配的空间,系统无法满足你的要求,就会报内存溢出的错误。
  • 内存泄漏是指向系统动态申请的内存,用完以后不归还,结果你申请的那块空间也不能再次使用了。
  • 内存溢出比如说栈,当栈满的时候再进栈必定会内存溢出,叫上溢。当栈空的时候还要出栈叫下溢。
  1. 内存溢出的原因及解决

    1. 原因
  • 内存加载的数据量过于庞大,如一次从数据库取出过多数据。

  • 代码中存在死循环或者循环过多重复的实体对象

  • 使用第三方软件中的BUG

  • 启动参数内存值设定的过小

    1. 解决
  • 检查代码中是否有死循环或递归调用

  • 检查是否有大循环重复产生新对象实体

  • 检查对数据库查询中,是否有一次性获取全部数据。

2. 智能指针的原理

智能指针的作用:可以帮助我们避免在申请内存空间后忘记释放造成内存泄漏的问题。因为智能指针本身是一个类,当出了类的作用域,类会调用析构函数进行释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间。

2.1 RAII

(Resource Acquisition Is Initialization)资源获取即初始化。

在类的构造函数中申请资源并使用,最后在析构函数中释放资源。

// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr()
	{
		if(_ptr)
		delete _ptr;
	}
private:
	T* _ptr;
};
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int)*n);
// 讲tmp指针委托给了sp对象
	SmartPtr<int> sp(tmp);
// _MergeSort(a, 0, n - 1, tmp);
// 这里假设处理了一些其他逻辑
	vector<int> v(1000000000, 10);
// ...
}
int main()
{
	try {
		int a[5] = { 4, 5, 2, 3, 1 };
		MergeSort(a, 5);
	}
	catch(const exception& e)
	{
		cout<<e.what()<<endl;
	}
return 0;
}

2.2 具有指针的行为

上述的smartptr还不能将其成为智能指针,因为还不具备指针的行为。指针可以解引用,也可以通过 -> 去访问所知空间的内容。所以需要对* 、-> 进行重载。

template<class T>
class SmartPtr {
public:
   SmartPtr(T* ptr = nullptr)
   	: _ptr(ptr)
   {}
   ~SmartPtr()
   {
   	if(_ptr)
   	delete _ptr;
   }
   
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}

private:
   T* _ptr;
};
struct Date
{
   int _year;
   int _month;
   int _day;
};
int main()
{
   SmartPtr<int> sp1(new int);
   *sp1 = 10
   cout<<*sp1<<endl;
   SmartPtr<int> sparray(new Date);
// 需要注意的是这里应该是sparray.operator->()->_year = 2018;
// 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
   sparray->_year = 2018;
   sparray->_month = 1;
   sparray->_day = 1;
}

总结原理

RAII特性 + 重载operator* 和 operator -> ,具有像指针一样的行为。

3. 智能指针的使用

包含头文件:

#include < memory>

3.1 shared_ptr

这里的指针主要指的是shared_ptr,是一种引用计数型智能指针。可以记录内存中有多少个智能指针被引用,新增一个引用计数+ 1,过期一个引用计数 - 1,当引用计数为 0 的时候 , 智能指针才会释放资源。

通过引用计数的方式实现多个shared_ptr对象之间的共享资源。

#include <iostream>
#include <memory>

using namespace std;

class A
{
public:
    A(int count)
    {
        _nCount = count;
    }
    ~A(){}
    void Print()
    {
        cout<<"count:"<<_nCount<<endl;
    }
private:
    int _nCount;
};

int main()
{   
    shared_ptr<A>p(new A(10));//初始化 
    p->Print();
    return 0;
}

3.2 循环引用

循环引用计数:两个智能指针互相指向对方,造成内存泄漏。需要weak_ptr,将其中的一个指针设置为weak_ptr。

因为weak_ptr没有共享资源,它的构造函数不会引起智能指针引用计数的变化。

对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数

3.3 智能指针的实现

#include<iostream>
#include<momory>

template<typename T>
class SmartPointer
{
private:
	T* _ptr;
	size_t* _count;
public:
	SmartPointer(T* ptr = nullptr)
		_ptr(ptr)
	{
		if (_ptr)
		{
			_count = new size_t(1);
		}
		else
		{
			_count = new size_t(0);
		}
	}

	SmartPointer(const SmartPointer &ptr)
	{
		if (this != &ptr)
		{
			//共享管理新对象的资源,并增加引用计数
			this->_ptr = ptr._ptr;
			this - _count = ptr._count;
			++(*this->_count);
		}
	}

	SmartPointer& opreater = (const SmartPointer &ptr)
	{
		if (this->_ptr = ptr._ptr)
		{
			return *this;
		}
		if (this->_ptr)
		{
			--(*this->_count);
			if (this->_count == 0)
			{
				delete this->_ptr;
				delete this->_count;
			}
		}

		this->_ptr = ptr._ptr;
		this->_count = ptr._count;
		++(*this->_count);
		return *this;
	}

	T& operator*()
	{
		assert(this->_ptr == nullptr);
		return *(this->_ptr);
	}
	T& operator->()
	{
		assert(this->_ptr == nullptr);
		return this->_ptr;
	}

	~SmartPointer()
	{
		--(*this->_count);
		if (0 == *this->_count)
		{
			delete this->ptr;
			delete this->count;
		}
	}

	size_t use_count()
	{
		return *this->_count;
	}
};

int main()
{
	SmartPointer<int> sp(new int(10));
	SmartPointer<int> sp2(sp);
	SmartPointer<int> sp3(new int(20));
	sp2 = sp3;
	std::cout << sp.use_count() << std::endl;
	std::cout << sp3.use_cout() << std::endl;
}

3.2 其他智能指针

  1. unique_ptr

独占式智能指针,不允许拷贝复制,不允许拷贝构造

是线程安全的。由于只是在当前代码块范围内生效, 因此不涉及线程安全问题

  1. weak_ptr

弱引用指针,用于观察shared_ptr或weak_ptr。用于解决循环引用 计数。

因为weak_ptr没有共享资源,它的构造函数不会引起智能指针引用计数的变化。

你可能感兴趣的:(C++学习之路,c++,面试,内存管理,内存泄漏)