C++ 智能指针

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;
}

如何避免内存泄漏

1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。

2. 采用RAII思想或者智能指针来管理资源。

3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。

4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。

总结一下: 内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。

2. 智能指针的使用及原理

2.1 RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法的好处:不需要显式地释放资源,对象所需的资源在其生命期内始终保持有效。

template<class T>
class SmartPtr {
public:
    SmartPtr(T* ptr = nullptr)
        : _ptr(ptr)
    {}
    ~SmartPtr()
    {
        if (_ptr)
        delete _ptr;
    }
private:
    T* _ptr;
};
int main()
{
    SmartPtr<int> sp(new int);
    return 0;
}

上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。

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<Date> sp2(new Date);
    sp2->_year = 2018;
    sp2->_month = 1;
    sp2->_day = 1;
}

总结一下智能指针的原理:

1. RAII特性

2. 重载operator*和opertaor->,具有像指针一样的行为。

2.2 std::auto_ptr

auto_ptr 是 C++98 标准中引入的一种智能指针模板类,旨在自动管理动态分配的内存,确保在适当的时机释放内存,以防止内存泄漏。auto_ptr 的设计思想基于资源获取即初始化(Resource Acquisition Is Initialization, RAII)原则,它在构造时接管资源的所有权,并在析构时释放资源。

构造函数

auto_ptr通过指针来进行初始化的构造函数,但是其被explicit修饰,说明不允许进行类型转换

int main()
{
	auto_ptr<int> ptr1(new int(5));//正确
	auto_ptr<int> ptr2 = new int(10);//错误
	return 0;
}

auto_ptr只允许通过第一种方式直接构造,第二种方式本质是类型转换,从int*转为auto_ptr<int>,只要有对应类型的构造函数那么C++就可以支持这个类型转换,但是如果构造函数被explicit修饰,这个类型转换功能就会被禁止,而auto_ptr就被禁止了。

其实,C++的四种智能指针,都不允许通过原生指针的类型转化来构造,也就是四种智能指针都只能通过小括号来初始化。不过拷贝构造是允许的,因为拷贝构造没有被explicit修饰.

int main()
{
	auto_ptr<int> ptr1(new int(5));
	auto_ptr<int> ptr2(ptr1);
	auto_ptr<int> ptr3 = ptr2;
	return 0;
}

get函数:

不过当auto_ptr发生拷贝,原先的auto_ptr会变成空指针,拷贝的auto_ptr会指向原先指针指向的空间,我们可以通过auto_ptr的内置函数get来获取auto_ptr内部存储的地址。

int main()
{
	auto_ptr<int> ptr1(new int(5));
	cout << ptr1.get() << endl;
	auto_ptr<int> ptr2(ptr1);
	cout << ptr1.get() << endl;
	cout << ptr2.get() << endl;
	return 0;
}

auto_ptr 的一个关键特性是所有权的转移。当一个 auto_ptr 对象被赋值给另一个 auto_ptr 对象时,它会将自身所拥有的资源的所有权转让给接收者,而不再拥有该资源。这意味着,一个资源在任何时候只能由一个 auto_ptr 实例所拥有。正如文档中的介绍,auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr,而更多的使用unique_ptr。

模拟实现

namespace lbk
{
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}
		auto_ptr(auto_ptr<T>& sp)
			:_ptr(sp._ptr)
		{
			// 管理权转移
			sp._ptr = nullptr;
		}
		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			// 检测是否为自己给自己赋值
			if (this != &ap)
			{
				// 释放当前对象中资源
				if (_ptr)
					delete _ptr;
				// 转移ap中资源到当前对象中
				_ptr = ap._ptr;
				ap._ptr = NULL;
			}
			return *this;
		}
		~auto_ptr()
		{
			if (_ptr)
				delete _ptr;
		}
		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

2.3 std::unique_ptr

对于auto_ptr的内存转移问题,unique_ptr的解决方案是简单粗暴的防拷贝。unique_ptr 是 C++11 标准中引入的一种智能指针,它代表了对单一对象的所有权。与 auto_ptr 不同,unique_ptr 不允许所有权的共享,即在任何时刻只有一个 unique_ptr 实例可以拥有和管理一个特定的对象。这有助于避免资源管理中的歧义和错误,并且可以防止内存泄漏。

unique_ptr的功能跟auto_ptr功能基本相同,只是不允许拷贝构造,并且还有一个优化,可以管理数组并且重载了[ ],auto_ptr不适用于管理数组,因为它使用 delete 而不是 delete[ ]释放内存。

int main()
{
	unique_ptr<int[]> ptr1(new int[5] {1, 2, 3, 4, 5});
	cout << ptr1[2] << endl;
	return 0;
}

如果指向的动态内存是数组的形式,模板参数要写为type[]的形式,来告诉unique_ptr该指针维护的动态内存,是以数组的形式开辟的。以上示例中,模板参数为int[]

模拟实现

	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}
		~unique_ptr()
		{
			if (_ptr)
				delete _ptr;
		}
		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		unique_ptr(const unique_ptr<T>& sp) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
	private:
		T* _ptr;
	};

2.4 std::shared_ptr

shared_ptr 是 C++11 标准中引入的一种智能指针,它用于管理动态分配的资源,并且支持多个shared_ptr实例共享同一资源。这种共享所有权的机制通过内部的引用计数来实现,确保了资源的正确管理和及时释放。当最后一个shared_ptr实例被销毁或重置时,它所管理的资源将被自动释放,从而避免了内存泄漏。

1. shared_ptr在其内部,给每个资源都维护着一份计数,来记录该份资源被几个对象共享。

2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源,对象引用计数减一。

3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源。

4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

shared_ptr的功能跟unique_ptr功能基本相同,不过允许拷贝构造,并且拷贝构造的指针与原先指针指向同一空间。

int main()
{
	shared_ptr<int[]> ptr1(new int[5] {1, 2, 3, 4, 5});
	cout << ptr1[2] << endl;
	cout << "ptr1:" << ptr1.get() << endl;
	shared_ptr<int[]> ptr2 = ptr1;
	cout << ptr2[2] << endl;
	cout << "ptr2:" << ptr2.get() << endl;
	return 0;
}

模拟实现

template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			, _pRefCount(new int(1))
		{}
		void AddRef()
		{
			++(*_pRefCount);
		}
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pRefCount(sp._pRefCount)
		{
			AddRef();
		}
		void Release()
		{
			if (--(*_pRefCount) == 0 && _ptr)
			{
				delete _ptr;
				delete _pRefCount;
			}
		}
		~shared_ptr()
		{
			Release();
		}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				Release();
				_ptr = sp._ptr;
				_pRefCount = sp._pRefCount;
				AddRef();
			}
			return *this;
		}
		int use_count()
		{
			return *_pRefCount;
		}
		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		T* get() const
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pRefCount;
	};

但是shared_ptr不当使用可能会导致循环引用,从而引起内存泄漏。

struct ListNode
{
	int _data;
	shared_ptr<ListNode> _prev;
	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;
	cout << node2.use_count() << endl;
	node1->_next = node2;
	node2->_prev = node1;
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	return 0;
}

1. node1 node2 两个智能指针对象指向两个节点,引用计数变成 1 ,我们不需要手动 delete。
2. node1 _next 指向 node2 node2 _prev 指向 node1 ,引用计数变成 2
3. node1 node2 析构,引用计数减到 1 ,但是 _next 还指向下一个节点。但是 _prev 还指向上一个节点。
4. 也就是说 _next 析构了, node2 就释放了,_prev析构了, node1 就释放了。
5. 但是 _next 属于 node 的成员, node1 释放了, _next 才会析构,而 node1 _prev 管理, _prev属于node2 成员,所以这就叫循环引用,谁也不会释放。
解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了.

2.5 std::weak_ptr

weak_ptr是 C++11 标准库中的一种智能指针,它与 shared_ptr 一起使用,用于管理动态分配的资源。不同于 shared_ptr,weak_ptr 不控制所指向对象的生命周期,它的主要作用是解决循环引用问题,从而避免内存泄漏,weak_ptr 通过不增加引用计数来实现这一点.它可以从 shared_ptr 或另一个 weak_ptr 对象构造,获得资源的观测权。
struct ListNode
{
	int _data;
    //要注意的是,weak_ptr不支持原生指针初始化,哪怕是nullptr也不可以
    //,因此在ListNode的初始化列表中,删掉了_next和_prev的初始化。
	weak_ptr<ListNode> _prev;
	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;
	cout << node2.use_count() << endl;
	node1->_next = node2;
	node2->_prev = node1;
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	return 0;
}

3. deleter

如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题。在 C++ 中,删除器(Deleter)是一种用于自定义智能指针(如 shared_ptr, unique_ptr)管理的动态分配内存释放行为的机制。删除器是一个可以被调用的对象,通常是一个函数或一个类的实例,其作用是在智能指针所管理的对象不再需要时执行特定的清理或资源释放操作。删除器的使用使得资源的释放过程更加灵活和可控,可以适应不同的资源管理和释放需求。

删除器可以采用多种形式,包括函数指针、函数对象(functor)或任何满足可调用要求的对象。当智能指针管理的对象需要释放时,删除器会被调用,并将对象的指针作为参数传递。以下是一些常见的删除器类型:

  • 函数指针:可以是一个简单的函数,用于释放资源。
  • 函数对象:可以是包含有状态的类,用于执行更复杂的资源释放操作。
  • lambda 表达式:C++11 引入的特性,可以方便地创建匿名函数,用于简单的删除操作或包含局部状态的复杂清理过程。

3.1 shared_ptr

语法:

shared_ptr<T> p(new T, deleter_function);
struct deleteFile
{
	void operator()(FILE* ptr)
	{
		fclose(ptr);
	}
};
int main()
{
	shared_ptr<FILE> fp1(fopen("test1.txt", "w"), deleteFile());
	shared_ptr<FILE> fp2(fopen("test2.txt", "w"), [](FILE* ptr) {fclose(ptr); });
	return 0;
}

对于shared_ptr,只需要把删除器的可调用对象,直接作为第二个参数传入即可。

3.2 unique_ptr

语法:

unique_ptr<T, deleter_function类型> p(new T, deleter_function对象);
struct deleteFile
{
	void operator()(FILE* ptr)
	{
		fclose(ptr);
	}
};
int main()
{
	unique_ptr<FILE, deleteFile> fp1(fopen("test1.txt", "w"), deleteFile());
	auto deletefunction = [](FILE* ptr) {fclose(ptr); };
	unique_ptr<FILE, decltype(deletefunction)> fp2(fopen("test2.txt", "w"), deletefunction);
	return 0;
}

对于unique_ptr,需要把删除器的可调用对象类型传入模板,可调用对象作为第二个参数传入。

  • 8
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我要满血复活

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值