智能指针机制、实现与发展历史

1.引入

为什么需要智能指针?
下面我们先分析一下下面这段程序有没有什么内存方面的问题?提示一下:注意分析MergeSort
函数中的问题
int div()
{
 int a, b;
 cin >> a >> b;
 if (b == 0)
 throw invalid_argument("除0错误");
 return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
 int* p1 = new int;
int* p2 = new int;
 cout << div() << endl;
 delete p1;
delete p2;
}
int main()
{
 try
 {
 Func();
 }
 catch (exception& e)
 {
 cout << e.what() << endl;
 }
 return 0;
}

上面的代码存在内存泄漏的问题

2. 内存泄漏

什么是内存泄漏,内存泄漏的危害
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内
存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对
该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现
内存泄漏会导致响应越来越慢,最终卡死
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;
}
内存泄漏分类
堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一
块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分
内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放
掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定.
如何避免内存泄漏
1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。

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

RAII

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

RAII的思想

RAII(Resource Acquisition Is Initialization)是一种常用的编程技术,主要在C++语言中使用,其核心思想是将资源的管理(如内存、文件句柄、网络连接等)与对象的生命周期绑定。以下是RAII思想的几个关键点:

资源封装:将资源封装在一个对象的构造函数中获取,并在析构函数中释放。这样,资源的生命周期就与对象的生存期同步。

对象生命周期管理:利用C++的对象生命周期机制(创建、使用、销毁)来管理资源。当对象创建时获取资源,当对象被销毁时释放资源。

异常安全:RAII提供了一种异常安全的编程方式。即使发生异常,C++的栈展开机制(stack unwinding)也会保证局部对象被正确地销毁,从而释放它们所管理的资源,防止资源泄漏。

简单性:RAII简化了资源管理,程序员不需要关注在程序的每一个退出点手动释放资源,减少了错误发生的可能性。

具体来说,RAII遵循以下原则:

构造函数获取资源:在对象的构造函数中,完成资源的分配和初始化。

析构函数释放资源:在对象的析构函数中,完成资源的释放和清理工作。

避免资源泄露:通过对象栈上的自动生命周期管理,或者智能指针等 RAII 类型的对象堆上的生命周期管理,确保资源总是被释放。

对象拷贝与资源管理:在实现拷贝构造函数和拷贝赋值运算符时,要特别注意资源的正确管理,通常采用“拷贝构造”或“引用计数”等技术。

RAII是C++中管理资源推荐的方式,与智能指针(如std::unique_ptr和std::shared_ptr)结合使用,可以大大提高程序的安全性和可靠性。

将指针交给智能指针之后,那么资源的释放就十分可靠

// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
    SmartPtr(T* ptr = nullptr)
       : _ptr(ptr)
   {}
    ~SmartPtr()
   {
        if(_ptr)
            delete _ptr;
   }
    
private:
    T* _ptr;
};
int div()
{
 int a, b;
 cin >> a >> b;
 if (b == 0)
 throw invalid_argument("除0错误");
 return a / b;
}
void Func()
{
 ShardPtr<int> sp1(new int);
    ShardPtr<int> sp2(new int);
 cout << div() << endl;
}
int main()
{
    try {
 Func();
   }
    catch(const exception& e)
   {
        cout<<e.what()<<endl;
   }
 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<int> sparray(new Date);
// 需要注意的是这里应该是sparray.operator->()->_year = 2018;
// 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
sparray->_year = 2018;
sparray->_month = 1;
sparray->_day = 1;
}
总结一下智能指针的原理:
1. RAII特性
2. 重载operator*和opertaor->,具有像指针一样的行为。

智能指针的主要类型包括:

  • std::auto_ptr(C++98中引入,C++17中已废弃)
  • std::unique_ptr(C++11中引入,用于独占资源所有权)
  • std::shared_ptr(C++11中引入,用于共享资源所有权)
  • std::weak_ptr(C++11中引入,用于解决shared_ptr的循环引用问题)

下面我们依次介绍这些ptr类

auto_ptr

C++98 版本的库中就提供了 auto_ptr 的智能指针。下面演示的 auto_ptr 的使用及问题。
auto_ptr 的实现原理:管理权转移的思想,下面简化模拟实现了一份 bit::auto_ptr 来了解它的原
// C++98
// 管理权转移,最后一个拷贝对象管理资源,被拷贝对象都被置空
// 很多公司都明确规定了,不要用这个

template<class T>
class auto_ptr
{
public:
	auto_ptr(T* ptr = nullptr)
		: _ptr(ptr)
	{}

	~auto_ptr()
	{
		delete _ptr;
		_ptr = nullptr;
	}

	// 像指针一样
	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}

	// 拷贝构造函数(转移权限)	//p1(p2)
	auto_ptr(const auto_ptr<T>& p2)
		: _ptr(p2._ptr)
	{
		p2._ptr = nullptr;		//p2置空,但是保留p2的资源
	}


private:
	T* _ptr;
};

一句话总结auto_ptr:设计依托答辩,转移指针使得指针悬空,被业内人士喷烂了!

如何在CPP理解悬空

在C++中,"悬空"通常指的是指针悬空(Dangling Pointer)的情况,这是一个内存管理的问题。指针悬空发生在以下几种情况:

指向已删除的对象:当指针所指向的对象被删除后,如果没有重置指针,那么这个指针就变成了悬空指针。

指向超出作用域的对象:局部对象在离开其作用域后,指向它的指针也会悬空。

栈内存被释放:如果指针指向的栈内存被释放,但指针未置空,那么该指针也会悬空。

理解悬空的概念并避免它,是编写安全、可靠的C++代码的重要部分。以下是一些避免悬空指针的建议:

如何理解并避免悬空指针:

初始化指针:在声明指针时就初始化为nullptr,确保指针不会指向不确定的内存地址。

删除后置空:在删除一个对象后,立即将指针赋值为nullptr,这样可以避免悬空指针的问题。

作用域意识:确保在函数或代码块结束时,释放所有动态分配的资源,并将指针置为nullptr。

智能指针:使用C++的智能指针(如std::unique_ptr, std::shared_ptr)来自动管理内存,它们会在适当的时机自动释放所拥有的对象,并置空。

资源管理类:使用资源管理类(如RAII),在对象生命周期结束时自动释放资源。

避免野指针:不要使用未初始化的指针。

下面是一个简单的示例,展示了如何避免悬空指针:


#include <iostream>



int main() {

    int* ptr = nullptr; // 初始化指针



    {

        int var = 5;

        ptr = &var; // 指针指向局部变量

    } // var离开作用域,其内存被释放



    // ptr现在是悬空指针,因为var已经不再存在

    // 应该避免使用ptr



    ptr = nullptr; // 将指针置为nullptr,避免悬空



    if (ptr == nullptr) {

        std::cout << "指针现在是安全的,没有被悬空。" << std::endl;

    }



    return 0;

}

在这个例子中,通过在局部变量var作用域结束前将ptr置为nullptr,我们避免了悬空指针的问题。在实际编程中,应该始终警惕并妥善管理指针,以防止悬空指针引起的潜在错误和程序崩溃。

std::unique_ptr

C++11中开始提供更靠谱的unique_ptr
unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份UniquePtr来了解它的原
// C++11 库才更新智能指针实现
// C++11 出来之前, boost 搞除了更好用的 scoped_ptr/shared_ptr/weak_ptr
// C++11 boost 库中智能指针精华部分吸收了过来
// C++11->unique_ptr/shared_ptr/weak_ptr
// unique_ptr/scoped_ptr
// 原理:简单粗暴 -- 防拷贝

	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~unique_ptr()
		{
			cout << "delete->" << _ptr << endl;

			delete _ptr;
		}

		// 像指针一样
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		// 拷贝构造函数(转移权限)	//p1(p2)
		unique_ptr(const unique_ptr<T>& p2) = delete;		//禁用拷贝构造时,最好将赋值重载也禁用
		unique_ptr<T>& operater=(const unique_ptr<T>& p2) = delete;

	private:
		// C++98
		// 1、只声明不实现
		// 2、限定为私有
		//unique_ptr(const unique_ptr<T>& up);
		//unique_ptr<T>& operator=(const unique_ptr<T>& up);

	private:
		T* _ptr;
	};

std::shared_ptr

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。例如:
老师晚上在下班之前都会通知,让最后走的学生记得把门锁下。
1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共
2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减
一。
3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对
象就成野指针了。

        

不能用类内成员,也不可以用static成员来记录引用计数,否则会出现故障!


	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			,_pcount(new int(1))		//内容初始化为1
		{}

		template<class D>	//定制删除其他类型
        shared_ptr(T* ptr, D deleter)
			:_ptr(ptr)
			,_pcount(new int(1))
			,_deleter(deleter)
		{}

		~shared_ptr()
		{
			release();
		}

		void release()
		{
			if (--(*_pcount) == 0)		//先--
			{
				_deleter(_ptr);		//再调用删除器(不定制删除器时,默认调用_deleter)
				delete _pcount;		//最后释放计数器(_pcount不应该由外界传入释放器,应该内部维护)
			}
		}

		//sp1(sp2)
		share_ptr(const shared_ptr<T>& sp2)
			:_ptr(sp2._ptr)
			,_pcount(sp2._pcount)
			,_deleter(sp2._deleter)
		{
			*_pcount += 1;		//计数器加1
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp2)
		{
			if (sp2._ptr == _ptr)		//避免自我赋值
				return *this;
			
			release();	//先对自身进行一次release
			_ptr = sp2._ptr;
			_pcount = sp2._pcount;
			_deleter = sp2._deleter;	
			*_pcount += 1;		//计数器加1

			return *this;

		}


		// 像指针一样( = 、 * 、 -> )
		T& operator*()
		{
			return *_ptr;
		}
		
		T* operator->()
		{
			return _ptr;
		}

		int use_count() const
		{
			return *_pcount;
		}

		T* get() const
		{
			return _ptr;
		}

	private:
		T* _ptr;
		int* _pcount;		//应该额外在堆区开一块空间,否则新建一个智能指针,计数器就不对了(不能将计数器属于单独的类,而是属于一个资源块)
        function<void(T*)> _deleter = [](T* ptr) { delete ptr; };		//自定义删除器
	};								//捕捉 参数列表 函数体

使用:

n1->next 本质去调用n1这个智能指针的operator->,得到了一个_ptr(n1的成员变量,存储了传入类型new之后的地址)的地址,所以->本质还是对传入类型的指针(原生指针)进行的解引用操作。

即智能指针类对原生指针进行了封装,当智能指针对象进行解引用操作时,本质还是元省指针类型的解引用操作。

所以智能指针可以对任意类型的原生指针进行封装,智能指针进行任意指针操作,功能等都与原生指针一致。

因此,智能指针的行为与原生指针非常相似,但具有额外的内存管理和异常安全性功能。

因此,shared_ptr<//类型>可以理解为对于类型的指针 ,后面的名字可理解为指针的名字

为了解决循环引用,引入了weak_ptr

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就释放了。
5. 也就是说_prev析构了,node1就释放了。
6. 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev
属于node2成员,所以这就叫循环引用,谁也不会释放。

解决方案:在循环引用的智能指针处,将对应的智能指针改成weak_ptr,进行弱引用。

/*

为什么程序能正常结束,但是没有打印~ListNode。(如果一直持续在循环中,按理说程序无法正常结束)

回归本质:从底层看引用循环的真正原因

N1和n2都是封装了ListNode的智能指针

最终结果只能是,引用--,而永远无法执行delete ptr,而ptr才是真正指向结构的指针,所以无法打印~ListNode

而不只是简单的:

N1的next是n2,n2的prev是n1.

想要彻底析构n2,那就得将n2的prev析构,即需要先析构n1

想要彻底析构n1,那就的将n1的next析构,即需要先析构n2

*/

weak_ptr的实现


	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_sp(nullptr)
		{}

		weak_ptr(const shared_ptr<T>& sp)		//拷贝和operater=并没有增加引用计数
			:_sp(sp.get())
		{}

		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			_sp = sp.get();
			return *this;
		}

		// 像指针一样
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

	private:
		T* _sp;		//弱引用,指向shared_ptr的指针,不能改变shared_ptr的引用计数
	};

可以看到weak_ptr在 赋值和拷贝处并没有增加引用,因此不会出现循环引用!

struct ListNode
{
 int _data;
 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;
}

对于ListNode实例化的node1 node2正常用shared_ptr维护。只不过在引用计数发生的关键指针

_prev和_next处,采用weak_ptr进行维护。

shared_ptr是可以赋值给weak_ptr的!

Weak_ptr不支持RAII

weak_ptr 本身并不直接支持 RAII(Resource Acquisition Is Initialization)原则,这是因为它不拥有它所监视的资源。RAII 是一种 C++ 资源管理策略,它通过对象的生命周期来管理资源,确保资源在对象析构时被正确释放。

以下是 weak_ptr 与 RAII 的关系:

资源管理:weak_ptr 不直接管理资源。它只是提供了一个观察 shared_ptr 管理的资源的手段。因此,它不需要在析构时释放资源,因为它不拥有资源。

使用场景:weak_ptr 通常用于解决 shared_ptr 可能引起的循环引用问题。当 weak_ptr 观察的对象不再被任何 shared_ptr 所拥有时,即使 weak_ptr 存在,对象也会被销毁

RAII 实践:尽管 weak_ptr 本身不直接实现 RAII,但它可以与 shared_ptr 一起使用来实现 RAII。shared_ptr 负责资源的获取和释放,而 weak_ptr 可以用来打破循环引用,从而确保 shared_ptr 能够正确释放资源

转换:当需要操作 weak_ptr 观察的资源时,可以通过调用 lock() 方法将 weak_ptr 转换为 shared_ptr。如果资源仍然存在,lock() 将返回一个有效的 shared_ptr,这样就可以安全地访问资源。如果资源已经被销毁,lock() 将返回一个空的 shared_ptr。

总结来说,虽然 weak_ptr 本身不支持 RAII,但它与 shared_ptr 结合使用,可以维护 RAII 原则,并帮助解决智能指针在复杂场景下可能遇到的资源管理问题。

在上述的代码例子中:

 node1->_next = node2;
 node2->_prev = node1;

weak_ptr维护的_next 和_prev并不会设计RAII,但是node1 node2是被shared_ptr所维护的,RAII由对应的shared_ptr实现

如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题

template<class D>	//定制删除其他类型
        shared_ptr(T* ptr, D deleter)
			:_ptr(ptr)
			,_pcount(new int(1))
			,_deleter(deleter)
		{}

我们内部实现了定制删除器,当不传入时,调用默认的定制删除器(用function包装的一个lambda表达式),完成资源的清理。

当外界传入定制删除器时,传入的ptr将交给这个定制删除器。

问题:

如果是new出来的指针,那么shared_ptr可以自动在内部将指针delete,如果不是new出来的指针(比如malloc),那么就需要进行定制删除器去释放空间吗?(也就是说shared_ptr会把维护的指针交给定制删除器去释放吗?)

是的,shared_ptr 默认情况下会使用 delete 操作符来释放它所管理的对象。这是因为在 C++ 中,new 操作符与 delete 操作符是一对,用来分配和释放动态内存。

当你使用 new 来分配内存时,shared_ptr 可以确保当引用计数变为零时,使用 delete 来正确地释放内存。然而,如果你使用 malloc 来分配内存,那么你应该使用 free 来释放它,而不是 delete。在这种情况下,你需要提供一个定制删除器(custom deleter)给 shared_ptr,以确保内存被正确地释放。

#include <memory>
#include <cstdlib> // for free

// 自定义删除器,使用 free 释放内存
void customDeleter(void* ptr) {
    free(ptr);
}

int main() {
    // 使用 malloc 分配内存
    int* rawPtr = static_cast<int*>(malloc(sizeof(int)));
    *rawPtr = 10;

    // 创建 shared_ptr,并传递自定义删除器
    std::shared_ptr<int> sharedPtr(rawPtr, customDeleter);

    // 使用 shared_ptr
    std::cout << *sharedPtr << std::endl;

    // 当 sharedPtr 离开作用域或被 reset 时,customDeleter 会被调用,从而使用 free 释放内存
    return 0;
}

在上面的代码中,我们定义了一个 customDeleter 函数,它接受一个 void* 类型的指针,并使用 free 来释放它。然后,我们创建了一个 shared_ptr,并将 rawPtr 和 customDeleter 作为参数传递给它。当 sharedPtr 的引用计数变为零时,它会调用 customDeleter 来释放内存。

注意,如果你使用 malloc 分配内存,并试图通过 shared_ptr 使用 delete 来释放它,这将导致未定义行为,因为 delete 期望的是通过 new 分配的内存。因此,使用正确的定制删除器是非常重要的。

定制删除器:可以用lambda表达式、可以用仿函数(可见是function<void(T*)>包装器发力了)

4.C++11boost中智能指针的关系

1. C++ 98 中产生了第一个智能指针auto_ptr.
2. C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost
的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。
boost就像是LOL的测试服,C++11会取其精华,去其糟粕。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值