前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >【C++】智能指针 && 守卫锁

【C++】智能指针 && 守卫锁

作者头像
利刃大大
发布2025-03-03 08:09:47
发布2025-03-03 08:09:47
3100
代码可运行
举报
文章被收录于专栏:csdn文章搬运
运行总次数:0
代码可运行

Ⅰ. 问题的引入

​ 我们先来看看下面这段代码:

代码语言:javascript
代码运行次数:0
复制
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;
}

​ 上述代码中,main 函数中捕获 Func 的异常,而非常关键,因为可能会导致一些内存泄漏的问题,new 会去调用 operator[],而这是有可能会申请失败的,一旦失败就会抛异常被 main 函数捕捉到,那么 Func 下面的 delete 就没有被执行到,这不是妥妥的内存泄漏吗❓❓❓

​ 这也是异常的引入导致的问题,所以我们必须要解决它,我们可以通过在 Func 中直接 catch 这些异常,但是我们会发现一个问题,我们需要捕获的异常可能是非常多个的,这样子让我们的开发效率和可靠性就降低了许多,所以我们得通过智能指针来解决这些问题!

Ⅱ. 内存泄漏

一、什么是内存泄漏,内存泄漏的危害

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

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

代码语言:javascript
代码运行次数:0
复制
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;
}

二、内存泄漏的分类

C/C++ 程序中一般我们关心两种方面的内存泄漏:

  • 堆内存泄漏 (Heap leak):堆内存指的是程序执行中依据须要分配通过 malloc / calloc / realloc / new 等从堆中分配的一块内存,用完后必须通过调用相应的 free 或者 delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生 Heap Leak
  • 系统资源泄漏:指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

三、如何检测内存泄漏(了解)

四、如何避免内存泄漏

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题,需要智能指针来管理才有保证。
  2. 采用 RAII 思想或者智能指针来管理资源
  3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
  4. 出问题了使用内存泄漏工具检测。不过很多工具都不够靠谱,或者收费昂贵。

总结一下,内存泄漏非常常见,解决方案分为两种:

  1. 事前预防型。如智能指针等。
  2. 事后查错型。如泄漏检测工具。

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

一、RAII

RAIIResource Acquisition Is Initialization 资源获取即初始化)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

在对象构造时获取资源并且初始化,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  1. 不需要显式地释放资源
  2. 采用这种方式,对象所需的资源在其生命期内始终保持有效
代码语言:javascript
代码运行次数:0
复制
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr 
{
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
		cout << "delete ptr success!" << endl;
	}
private:
	T* _ptr;
};

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}

void Func()
{
    // 使用SmartPtr类来包装我们动态开辟的空间
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(new int);
	cout << div() << endl;
}

int main()
{
	try {
		Func();
	}
	catch (const exception& e) {
		cout << e.what() << endl;
	}
	return 0;
}

// 运行结果:
10 0
delete ptr success!
delete ptr success!
除0错误

​ 上述代码中我们使用 RAII 思想设计了一个 SmartPtr 类,其实就是为我们动态开辟的空间,用这个 SmartPtr 类进行包装起来管理,我们都知道,对象在其作用域周期结束之后是会自动调用析构函数的,所以达到了 “智能” 的效果!

二 、智能指针的原理

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

代码语言:javascript
代码运行次数:0
复制
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr 
{
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
		cout << "delete ptr success!" << endl;
	}

	// 重载*和->和[]操作符
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	T& operator[](size_t pos)
	{
		return _ptr[pos]; 
	}
private:
	T* _ptr;
};
int main()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(new int);

	*sp1 = 10;
	cout << --sp1[0] << endl;
	return 0;
}

// 运行结果:
9
delete ptr success!
delete ptr success!

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

  1. RAII 特性
  2. 重载 operator*opertaor-> 等操作符,使其具有像指针一样的行为。

三、auto_ptr(摒弃使用)

std::auto_ptr文档

C++98 版本的库中就提供了 auto_ptr 的智能指针,其包含在头文件 <memory> 中。auto_ptr 的实现原理:管理权转移的思想

​ 虽说很早之前就有了这个智能指针,但是它也一直被 “骂” 到了现在,因为它带来的不只是方便,还带来危害!

代码语言:javascript
代码运行次数:0
复制
#include <memory>
int main()
{
	auto_ptr<int> aptr1(new int);
	auto_ptr<int> aptr2(new int);

	*aptr1 = 20;
	cout << *aptr1 << ",its ptr is " << aptr1.get() << endl;

	auto_ptr<int> aptr3(aptr1);
	return 0;
}

// 运行结果:
20,its ptr is 000002A624DA4810

​ 不仔细看的话,确实是看不出来有什么危害,我们通过调试看一下:

​ 明显发现,aptr1 的资源转移到了 aptr3 中去了,而这其实给用户带来了极大的隐患,因为用户怎么知道 aptr1 已经噶了呢❓❓❓

​ 所以我们在 日常使用的时候,尽量避开使用 auto_ptr,甚至很多公司都明确规定不能使用 auto_ptr,所以我们了解即可!

​ 下面我们给出 auto_ptr 的模拟实现,了解了解即可!

代码语言:javascript
代码运行次数:0
复制
// C++98 管理权转移 auto_ptr
namespace liren
{
    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)
            {
                cout << "delete:" << _ptr << endl;
                delete _ptr;
            }
        }
        
        // 像指针一样使用
        T& operator*()
        {
            return *_ptr;
        }
        T* operator->()
        {
            return _ptr;
        }
    private:
        T* _ptr;
    };
}

// 结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr
int main()
{
    liren::auto_ptr<int> sp1(new int);
    liren::auto_ptr<int> sp2(sp1); // 管理权转移

    // sp1悬空
    *sp2 = 10;
    cout << *sp2 << endl;
    cout << *sp1 << endl; // ❌
    return 0;
}

// 运行结果
10
D:\giteecode\ProgramLanguage\liren\C++\智能指针\智能指针\x64\Debug\智能指针.exe (进程 23904)已退出,代码为 3。

上述实现中主要注意的是 operator= 的实现,因为 operator= 是发生在赋值阶段,而不是初始化阶段,所以必须先将原来存在的空间释放,否则会造成内存泄漏问题

四、unique_ptr💥💥💥

std::unique_ptr文档

unique_ptrC++11 引入的一种智能指针,用于管理动态分配的对象。

​ 与传统的指针不同,unique_ptr 具有独占所有权的特点,即同一时间只能有一个 unique_ptr 指向该对象,当 unique_ptr 被销毁时,它所指向的对象也会被销毁

unique_ptr 还支持移动语义进行 std::move,可以将所有权转移给另一个 unique_ptr,从而实现资源的转移和管理。

​ 其实无论是 unique_ptr 还是我们后面要讲的 shared_ptr,其实它们都是来自于 boost 库中的智能指针,分别是 scoped_ptrshared_ptr,可以这么说,c++11boost 库中的这些智能指针给引入了进来,而不是自主实现的!

​ 💥注意:C++11 中提供的智能指针都只能管理单个对象的资源,没有提供管理一段空间资源的智能指针

代码语言:javascript
代码运行次数:0
复制
#include <memory>
int main()
{
	unique_ptr<int> uptr1(new int);
	unique_ptr<int> uptr2(new int);

	*uptr1 = 20;
	cout << *uptr1 << ",its ptr is " << uptr1.get() << endl;

	unique_ptr<int> uptr3(uptr1); 	    // ❌尝试引用已删除的函数,也就是拷贝构造函数!
    unique_ptr<int> uptr4(move(uptr1)); // √支持移动语义,也就是将uptr1的资源移动到uptr4
	return 0;
}

​ 💥 unique_ptr 的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一个我们自己的智能指针来了解它的原理:

代码语言:javascript
代码运行次数:0
复制
// unique_ptr/scoped_ptr
// 原理:简单粗暴 -- 防拷贝
namespace liren
{
    // unique_ptr/scoped_ptr
    // 原理:简单粗暴 -- 防拷贝
    template <class T>
    class unique_ptr
    {
    private:
        T* _ptr = nullptr;
    public:
        unique_ptr(T* ptr)
            : _ptr(ptr)
        {}

        ~unique_ptr()
        {
            if (_ptr != nullptr)
            {
                cout << "delete:" << _ptr << endl;
                delete _ptr;
                _ptr = nullptr;
            }
        }

        // 支持std::move右值引用传参
        unique_ptr(unique_ptr<T>&& ptr)
        {
            _ptr = nullptr;
            ptr.swap(*this);
        }

        unique_ptr(const unique_ptr<T>& sp) = delete; 			    // 封掉拷贝构造
        unique_ptr<T>& operator=(const unique_ptr<T>& ap) = delete;  // 封掉=赋值

        T& operator*()
        {
            return *_ptr;
        }

        T* operator&()
        {
            return _ptr;
        }

        T& operator[](size_t pos)
        {
            return _ptr[pos];
        }

        void swap(unique_ptr<T>& tmp)
        {
            ::swap(tmp._ptr, _ptr);
        }
    };
}

int main()
{
    liren::unique_ptr<int> uptr1(new int);
    liren::unique_ptr<int> uptr2(new int);

    *uptr1 = 10;
    cout << *uptr1 << " or " << uptr1[0] << endl;

    liren::unique_ptr<int> uptr3(uptr1); // ❌
    uptr2 = uptr1; // ❌
    return 0;
}

五、shared_ptr💥💥💥

std::shared_ptr官方文档

shared_ptr 的原理:通过引用计数实现多个 shared_ptr 对象之间共享资源

注意事项:

  1. shared_ptr 类的内部,每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  2. 当某一个 shared_ptr 对象被生命周期结束时,就说明不使用该资源了,则对象的引用计数减一
  3. 如果引用计数减为 0,就说明已经没有对象使用该资源了,则释放该资源
  4. 如果还不是 0,就说明除了当前对象以外还有其他对象在使用该份资源,此时不能释放该资源,否则其他对象就成野指针了。

shared_ptrunique_ptr 最大的区别就是 shared_ptr 支持拷贝,这是因为其引入了引用计数的方法才得以实现的!

​ 💥但是要注意,这个引用计数不是随随便便用一个 int 变量就可以搞定的,因为每个对象中的变量都是独立的,那就会人想用 static 来修饰一下,但是这样子的话也不行,因为这样子我们开辟出来的对象,这个计数器永远都只有一份,那么就不能达到指向不同空间有不同的计数器的目的。

​ 👍 解决方法就是开辟一个动态内存,其中用来存放这个计数,并用指针维护起来

代码语言:javascript
代码运行次数:0
复制
namespace liren
{
    template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			, _count(new int(1)) // 计数器初始化为1
		{}

		~shared_ptr()
		{
			Release();
		}

		shared_ptr(const shared_ptr<T>& ptr)
			:_ptr(ptr._ptr)
			, _count(ptr._count)
		{
			(*_count)++;
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& ptr)
		{
			if (ptr._ptr != _ptr) // 只有当不是指向同一个空间的指针才进行赋值操作
			{
				Release();

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

		void Release()
		{
			if (--(*_count) == 0) // 只有计数为0才进行释放内存
			{
				delete _ptr;
				delete _count;
			}
		}
        
        T* get() const
		{
			return _ptr;
		}

		int use_count() const
		{
			return *_count;
		}
	private:
		T* _ptr;
		int* _count;
	};
}

int main()
{
	liren::shared_ptr<int> sp1(new int);
	liren::shared_ptr<int> sp2(sp1);
	liren::shared_ptr<int> sp3(sp2);

	*sp1 = 20;

	cout << "*sp1: " << *sp1 << " &sp1: " << sp1.get() << " count: " << sp1.used_count() << endl;
	cout << "*sp2: " << *sp2 << " &sp2: " << sp2.get() << " count: " << sp2.used_count() << endl;
	cout << "*sp3: " << *sp3 << " &sp3: " << sp3.get() << " count: " << sp3.used_count() << endl;
	cout << "------------------------------------------------------------------------------------" << endl;

	liren::shared_ptr<int> sp4(new int);
	liren::shared_ptr<int> sp5(sp4);
	cout << "*sp4: " << *sp4 << " &sp4: " << sp4.get() << " count: " << sp4.used_count() << endl;
	cout << "*sp5: " << *sp5 << " &sp5: " << sp5.get() << " count: " << sp5.used_count() << endl;
	cout << "------------------------------------------------------------------------------------" << endl;

	sp5 = sp1;
	cout << "*sp1: " << *sp1 << " &sp1: " << sp1.get() << " count: " << sp1.used_count() << endl;
	cout << "*sp4: " << *sp4 << " &sp4: " << sp4.get() << " count: " << sp4.used_count() << endl;
	cout << "*sp5: " << *sp5 << " &sp5: " << sp5.get() << " count: " << sp5.used_count() << endl;
	return 0;
}

// 运行结果:
*sp1: 20 &sp1: 000001BDBD676C60 count: 3
*sp2: 20 &sp2: 000001BDBD676C60 count: 3
*sp3: 20 &sp3: 000001BDBD676C60 count: 3
------------------------------------------------------------------------------------
*sp4: -842150451 &sp4: 000001BDBD692C80 count: 2
*sp5: -842150451 &sp5: 000001BDBD692C80 count: 2
------------------------------------------------------------------------------------
*sp1: 20 &sp1: 000001BDBD676C60 count: 4
*sp4: -842150451 &sp4: 000001BDBD692C80 count: 1
*sp5: 20 &sp5: 000001BDBD676C60 count: 4
shared_ptr 的线程安全问题之引用计数的安全

​ 上面这样子的 shared_ptr 大概能跑就完了❓❓❓

​ 不不不,我们来看以下场景,涉及到多线程的情况:

代码语言:javascript
代码运行次数:0
复制
void test_shared_ptr()
{
    int n = 1000;
    liren::shared_ptr<int> sp1(new int);

    // 创建两个线程进行shared_ptr的创建与销毁
    thread t1([&]()
          {
              for (int i = 0; i < n; ++i)
              {
                  liren::shared_ptr<int> sp2(sp1);
              }
          });

    thread t2([&]()
          {
              for (int i = 0; i < n; ++i)
              {
                  liren::shared_ptr<int> sp3(sp1);
              }
          });

    t1.join();
    t2.join();

    cout << sp1.get() << endl;
    cout << sp1.use_count() << endl;
}

// 调用结果1:
000001676507EA90
73
    
// 调用结果2:
000001FB40F9EE50
16

​ 嘶~这不就出问题了吗,在多线程的情况下,就有可能因为时间片轮转与线程切换等问题造成以上情况,具体的可以参考 linux 中的多线程笔记,这里只是稍微解释一下:

​ 我们都知道一个如果存在多线程,并且如果它们都对一个共享资源进行操作的话,那么极有可能导致一些奇怪的结果,比如上述代码中两个线程 t1t2,它们都快速的在调用创建一个 shared_ptr 对象,那么有可能在一个时间段内,t1 线程在 sp2 还没创建好的时候,它的运行时间片就到时间了,那么就要切换到 t2 线程,并且会带走其自己的上下文,其中就包括其中计数 count1

​ 假设 count1 = 10,而这个时候 t2 开始执行 for 循环创建新的 sp3,假设此时 t2 创建了 count2 = 50 个引用计数,那么此时就会导致引用计数不正确,切回 t1 线程的时候,t1 上下文还是原来的样子,它还会继续执行其 sp2 中的 ++ 语句 (++语句不是只有一条汇编语句),所以 t1 操作的是其自己的上下文 count1,对其 ++ 就变成了 count1 = 11,最后再将其写入到内存中,此时对于 t2 来说,这个时候如果它执行 ++ 语句,去取内存中的 count,会发现其变成了 11,这反反复复就导致了最后的 count 可能会奇奇怪怪!

​ 对于销毁来说也是一样的道理!

​ 那这个问题怎么解决❓❓❓

​ 其实我们只要对 shared_ptr 的构造函数进行加锁即可!具体关于锁的知识可以参考 linux 多线程的笔记~

​ 所以我们 需要对 shared_ptr 的析构函数、赋值重载等涉及到线程安全的地方都要加锁! 但其实我们还可以使用库里提供的 <atomic> 原子操作库,它能保证我们写的 ++-- 操作都是原子性的(可以理解为一条汇编语句就操作完),不过我们这里还是使用 <mutex> 来进行加锁进行示范!

​ 下面是才是比较安全的 shared_ptr 的写法:

代码语言:javascript
代码运行次数:0
复制
template<class T>
class shared_ptr
{
public:
    shared_ptr(T* ptr)
        : _ptr(ptr)
        , _count(new int(1))
        , _mtx(new mutex) // 记得锁指针开辟空间
    {}

    ~shared_ptr()
    {
        Release();
    }

    shared_ptr(const shared_ptr<T>& ptr)
        :_ptr(ptr._ptr)
        , _count(ptr._count)
        , _mtx(ptr._mtx) // 记得把锁指针也赋值过去
    {
        _mtx->lock(); // 加锁
        (*_count)++;
        _mtx->unlock();
    }

    shared_ptr<T>& operator=(const shared_ptr<T>& ptr)
    {
        if (ptr._ptr != _ptr)
        {
            Release();

            _ptr = ptr._ptr;
            _count = ptr._count;
            _mtx = ptr._mtx; // 记得把锁指针也赋值过去

            _mtx->lock(); // 加锁
            (*_count)++;
            _mtx->unlock();
        }
        return *this;
    }

    void Release()
    {
        bool flag = false; // 对于每个线程来说flag是局部私有的,互不影响

        _mtx->lock();
        if (--(*_count) == 0)
        {
            delete _ptr;
            delete _count;
            flag = true;
        }
        _mtx->unlock();

        // 只有当真的减到0才会进入去释放锁,但是由于只能在unlock只会才能释放,所以我们在外面用flag进行判断释放
        if (flag == true)
            delete _mtx;
    }

    T& operator*()
    {
        return *_ptr;
    }

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

    T* get() const
    {
        return _ptr;
    }

    int use_count() const
    {
        return *_count;
    }
private:
    T* _ptr;
    int* _count;
    mutex* _mtx; // 锁的指针,注意如果是对象的话,那么会有一些问题
};

​ 我们重新调用一下之前写的测试代码:

代码语言:javascript
代码运行次数:0
复制
000002316FE0EE90
1

​ 可以发现在多次调用之下都是正常的结果!

​ 其实 std::shared_ptr 的实现中已经为我们解决了引用计数安全的问题,所以我们可以放心的使用!

shared_ptr的线程安全问题之管理对象的安全

​ 智能指针管理的对象存放在 上,两个线程中同时去访问,会导致线程安全问题!我们来看看下面的代码:

代码语言:javascript
代码运行次数:0
复制
struct Date
{
	int _year = 0;
	int _month = 0;
	int _day = 0;
};

void test_shared_ptr2()
{
	int n = 100000;
	liren::shared_ptr<Date> sp1(new Date);

	// 创建两个线程进行shared_ptr的增删
	thread t1([&]()
		{
			for (int i = 0; i < n; ++i)
			{
				liren::shared_ptr<Date> sp2(sp1);
				sp2->_year++;
				sp2->_month++;
				sp2->_day++;
			}
		});

	thread t2([&]()
		{
			for (int i = 0; i < n; ++i)
			{
				liren::shared_ptr<Date> sp3(sp1);
				sp3->_year++;
				sp3->_month++;
				sp3->_day++;
			}
		});
	t1.join();
	t2.join();

	cout << sp1->_year << endl;
	cout << sp1->_month << endl;
	cout << sp1->_day << endl;
}

// 调用结果:(结果每次都是会有所不同的,这里只是结果之一)
200000
199999
199999

​ 看到上述代码中,我们虽然已经解决了引用计数的线程安全问题,但是对于这种本身管理对象的线程安全呢❓❓❓

​ 其实这种问题在 std::shared_ptr 中并没有解决,因为这种问题是比较随机的,所以需要我们使用者自己去解决,那么如何解决呢❓❓❓

​ 其实并不难,我们只需要单独为我们这段 ++/-- 代码进行加锁即可~

代码语言:javascript
代码运行次数:0
复制
void test_shared_ptr2()
{
	int n = 1000000;
	liren::shared_ptr<Date> sp1(new Date);

	mutex mtx; // 创建一个锁对象
    
	// 创建两个线程进行shared_ptr的增删
	thread t1([&]()
		{
			for (int i = 0; i < n; ++i)
			{
				liren::shared_ptr<Date> sp2(sp1);
				mtx.lock(); // 加锁
				sp2->_year++;
				sp2->_month++;
				sp2->_day++;
				mtx.unlock(); // 解锁
			}
		});

	thread t2([&]()
		{
			for (int i = 0; i < n; ++i)
			{
				liren::shared_ptr<Date> sp3(sp1);
				mtx.lock(); // 加锁
				sp3->_year++;
				sp3->_month++;
				sp3->_day++;
				mtx.unlock(); // 解锁
			}
		});
	t1.join();
	t2.join();

	cout << sp1->_year << endl;
	cout << sp1->_month << endl;
	cout << sp1->_day << endl;
}

// 调用结果:
2000000
2000000
2000000

六、shared_ptr线程安全问题总结

  1. 对于析构、拷贝构造、赋值重载等 ++/-- 操作的时候,std::shared_ptr 本身是线程安全的
  2. 但是对于 shared_ptr 所管理的资源不是线程安全的,所以需要我们手动去加锁或者用原子性手段进行保护。

七、shared_ptr的循环引用问题

​ 我们先来看看下面这段代码:

代码语言:javascript
代码运行次数:0
复制
struct ListNode
{
	int _val;
	ListNode* _prev = nullptr;
	ListNode* _next = nullptr;

	~ListNode() { cout << "~ListNode()" << endl; }
};

void test_circle_ref()
{
	std::shared_ptr<ListNode> node1(new ListNode);
    std::shared_ptr<ListNode> node2(new ListNode);

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

// 调用结果:
1>------ 已启动生成: 项目: 智能指针, 配置: Debug x64 ------
1>test.cpp
1>D:\giteecode\ProgramLanguage\liren\C++\智能指针\SmartPtr.h(221,35): error C2440: “初始化”: 无法从“liren::ListNode *”转换为“std::shared_ptr<liren::ListNode>”
1>D:\giteecode\ProgramLanguage\liren\C++\智能指针\SmartPtr.h(221,35): message : class“std::shared_ptr<liren::ListNode>”的构造函数声明为“explicit”
1>D:\giteecode\ProgramLanguage\liren\C++\智能指针\SmartPtr.h(222,35): error C2440: “初始化”: 无法从“liren::ListNode *”转换为“std::shared_ptr<liren::ListNode>”
1>D:\giteecode\ProgramLanguage\liren\C++\智能指针\SmartPtr.h(222,35): message : class“std::shared_ptr<liren::ListNode>”的构造函数声明为“explicit”
1>D:\giteecode\ProgramLanguage\liren\C++\智能指针\SmartPtr.h(224,18): error C2440: “=”: 无法从“std::shared_ptr<liren::ListNode>”转换为“liren::ListNode *”
1>D:\giteecode\ProgramLanguage\liren\C++\智能指针\SmartPtr.h(224,18): message : 没有可用于执行该转换的用户定义的转换运算符,或者无法调用该运算符
1>D:\giteecode\ProgramLanguage\liren\C++\智能指针\SmartPtr.h(225,18): error C2440: “=”: 无法从“std::shared_ptr<liren::ListNode>”转换为“liren::ListNode *”
1>D:\giteecode\ProgramLanguage\liren\C++\智能指针\SmartPtr.h(225,18): message : 没有可用于执行该转换的用户定义的转换运算符,或者无法调用该运算符
1>已完成生成项目“智能指针.vcxproj”的操作 - 失败。

​ 这里居然告诉我们无法从 shared_ptr<ListNode> 转化为 ListNode *,因为我们的 node1node2 现在是两个智能指针对象,所以我们要改动一下,将 ListNode 中的 _prev_next 也交给智能指针来管理就行了:

代码语言:javascript
代码运行次数:0
复制
struct ListNode
{
	int _val;
	std::shared_ptr<ListNode> _prev = nullptr; // 两个成员交给智能指针管理
	std::shared_ptr<ListNode> _next = nullptr;

	~ListNode() { cout << "~ListNode()" << endl; }
};

void test_circle_ref()
{
	std::shared_ptr<ListNode> node1(new ListNode);
    std::shared_ptr<ListNode> node2(new ListNode);

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

// 调用结果:

​ 但是问题就来了,上面为什么没有调用析构函数 ~ListNode() 呢,我们不是用智能指针管理两个节点对象吗,那么出了作用域为啥没销毁❓❓❓

​ 其实这是和上面链接前后节点有关系!这导致了我们下面要讲的循环引用:

​ 如果 _next_prev 其中一个想释放管理自己的智能指针,它们就必须先释放这块空间,等到引用计数为 0,但是恰巧的是,它们的成员所指向的就是对方,这就导致了 循环引用,谁也没办法干掉谁,谁也没办法干掉自己的情况!最后就 导致了内存泄漏

​ 也就是说只要上面的 _next 或者 _prev 其中有一个没有指向对方,那么它们最后就都能够得到释放!

所以说平时在设计的时候我们要尽量避开让成员管理其它对象的情况!

​ 那么说到底,如何解决这个问题呢❓❓❓

​ 这个时候就要引入我们的另一个智能指针:weak_ptr

八、weak_ptr

std::weak_ptr官方文档

weak_ptr 是为了配合 shared_ptr 而引入的一种智能指针!

​ 它指向一个由 shared_ptr 管理的对象而不影响所指对象的生命周期,也就是 将一个 weak_ptr 绑定到一个 shared_ptr 不会影响 shared_ptr 的引用计数。不论是否有 weak_ptr 管理着其它对象,只要最后一个指向对象的 shared_ptr 被销毁,该对象就会被释放。

​ 从这个角度看,weak_ptr 更像是 shared_ptr 的一个助手而不是智能指针。

​ 简单地说,weak_ptr 就是可以指向资源/访问资源,但是不参与资源的管理,即不增加引用计数

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <memory>
int main () 
{
	shared_ptr<int> sp (new int);

	weak_ptr<int> wp1;
	weak_ptr<int> wp2 (wp1);
	weak_ptr<int> wp3 (sp);

	cout << "use_count:\n";
	cout << "wp1: " << wp1.use_count() << endl;
	cout << "wp2: " << wp2.use_count() << endl;
	cout << "wp3: " << wp3.use_count() << endl;

	cout << "\nis expired?:" << endl; // 0表示没过期,1表示过期  
	cout << wp1.expired() << endl;
	cout << wp2.expired() << endl;
	cout << wp3.expired() << endl;
	return 0;
}

// 运行结果:
use_count:
wp1: 0
wp2: 0
wp3: 1

is expired?:
1
1
0

​ 接下来我们使用 weak_ptr 来解决我们遇到的循环引用的问题:

代码语言:javascript
代码运行次数:0
复制
struct ListNode
{
	int _val;
	weak_ptr<ListNode> _prev; // 使用weak_ptr,不增加引用计数
	weak_ptr<ListNode> _next;

	~ListNode() { cout << "~ListNode()" << endl; }
};

void test_circle_ref()
{
	std::shared_ptr<ListNode> node1(new ListNode);
	std::shared_ptr<ListNode> node2(new ListNode);

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

// 调用结果:
~ListNode()
~ListNode()

​ 成功解决问题!

​ 既然这样子,我们顺手来简单实现一下 weak_ptr,其实大概思路并不难,不过也就是不使用引用计数嘛!但是要注意,std 库中的实现其实很复杂,因为其还涉及到解决内存碎片、性能的一些问题,我们这里都是简单的模拟实现一下,帮助我们理解大概的底层而已!

代码语言:javascript
代码运行次数:0
复制
template<class T>
class weak_ptr
{
public:
	weak_ptr()
		:_ptr(nullptr)
	{}

	weak_ptr(const shared_ptr<T>& sptr)
		:_ptr(sptr.get()) // 不能直接取到_ptr,所以要用get()
	{}

	weak_ptr<T>& operator=(const shared_ptr<T>& sptr)
	{
		_ptr = sptr.get(); // 不能直接取到_ptr,所以要用get()
		return *this;
	}

    T& operator*()
    {
        return *_ptr;
    }

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

    T* get() const
    {
        return _ptr;
    }
private:
	T* _ptr;
};

struct ListNode
{
	int _val;
	liren::weak_ptr<ListNode> _prev; // 使用weak_ptr,不增加引用计数
	liren::weak_ptr<ListNode> _next;

	~ListNode() { cout << "~ListNode()" << endl; }
};

void test_circle_ref()
{
	liren::shared_ptr<ListNode> node1(new ListNode);
	liren::shared_ptr<ListNode> node2(new ListNode);

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

// 调用结果:
~ListNode()
~ListNode()

Ⅳ. 定制删除器

​ 我们上面针对的管理资源,貌似开辟的都是 “一个” 空间,那如果开辟的是多块空间呢,我们怎么让析构函数去知道要释放 “一个” 还是 “多个” 内存块❓❓❓

代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <memory>
#include <string
int main()
{
	shared_ptr<int> sp1(new int[10]); // 不一定会报错,因为是内置类型
	shared_ptr<string> sp2(new string[10]); // 肯定会报错,因为是自定义类型
	return 0;
}

​ 上述代码运行之后,对于 sp1 来说,不同的编译器可能会有不同的结果,但是这种 delete sp1 的选择肯定不是正确的,但是这里并不会报错!对于 sp2 ,那就不一样了,因为它是 string 类型,并不是普通的内置类型,如果我们 delete sp2 的话,那么肯定就直接报错啦。

​ 那我们应该怎么办❓❓❓

​ 此时就要使用定制删除器来解决问题了!

定制删除器无非就是一个仿函数,当我们传一个删除器给智能指针的时候,它可以按照我们删除器的定制方式(也就是函数体内容)进行对资源的析构!对于 std::shared_ptr 来说,它的构造函数重载版本的第二个参数其实就是一个删除器,如下图所示:

并且在头文件 <memory> 中也存在一个默认的删除器,如下图所示:

​ 二话不说,下面我们直接写一个删除器帮助我们解决上述问题:

代码语言:javascript
代码运行次数:0
复制
// 定制删除器
template<class T>
struct DeleteArray
{
	void operator()(const T* ptr)
	{
		delete[] ptr;
        cout << "delete[] " << ptr << endl;
	}
};

int main()
{
	//shared_ptr<int> sp1(new int[10]); 	  // 不一定会报错,因为是内置类型
	//shared_ptr<string> sp2(new string[10]); // 肯定会报错,因为是自定义类型

	// 注意下述传递的第二个参数是对象而不是类型,所以需要加()进行对象的构造
	shared_ptr<int> sp1(new int[10], DeleteArray<int>());
	shared_ptr<string> sp2(new string[10], DeleteArray<string>());
    
    // 还可用使用lambda表达式
    shared_ptr<string> sp3(new string[10], [](string* ptr) 
		{
			delete[] ptr;
			cout << "lambda delete[] " << ptr << endl;
		});
    
    // 还可以是文件类型
	shared_ptr<FILE> sp4(fopen("test.cpp", "r"), [](FILE* ptr)
		{
			fclose(ptr);
			cout << "file delete[] " << ptr << endl;
		}); 
	return 0;
}

// 运行结果:
file delete[] 00000218B799D410
lambda delete[] 00000218B799E948
delete[] 00000218B799D6F8
delete[] 0000000000008123

​ 是不是很高大上!

​ 但是我们自己来实现这个功能并不简单,因为这个删除器是要给析构函数使用的,而如果像 std::shared_ptr 一样,通过第二个参数传过去,那么我们是要在构造函数接收,但是要在析构函数使用,那么我们就得有一个删除器的类型对象,那么我们就得在重载的构造函数写上一个新的模板参数,假设模板参数是 class D,但是有一个问题,这个 D 类型的删除器模板是这个构造函数的啊,这个时候就不好直接在 std::shared_ptr 中声明一个 D del 对象。

​ 库里面的做法是将引用计数和这个删除器单独作为一个类进行封装,这样子实现起来是比较复杂的!

​ 我们这里就不花时间去进行封装了,直接使用一个办法,就是将原来 shared_ptr 的模板参数添加一个 D 类型的即可,我们了解即可!

代码语言:javascript
代码运行次数:0
复制
template<class T, class D = default_delete<T>> // 默认删除器模板参数
class shared_ptr
{
public:
	// 带有默认删除器的构造函数
	shared_ptr(T* ptr, D del = default_delete<T>())
		:_ptr(ptr)
		, _count(new int(1))
		, _mtx(new mutex)
		, _del(del) // 删除器
	{}

	shared_ptr(const shared_ptr<T>& ptr)
		:_ptr(ptr._ptr)
		, _count(ptr._count)
		, _mtx(ptr._mtx)
		, _del(ptr._del) // 删除器拷贝
	{
		_mtx->lock();
		(*_count)++;
		_mtx->unlock();
	}

	shared_ptr<T>& operator=(const shared_ptr<T>& ptr)
	{
		if (ptr._ptr != _ptr)
		{
			Release();

			_ptr = ptr._ptr;
			_count = ptr._count;
			_mtx = ptr._mtx;
			_del = ptr._del; // 删除器赋值

			_mtx->lock();
			(*_count)++;
			_mtx->unlock();
		}
		return *this;
	}
    
    void release()
    {
        if (--(*_count) == 0)
        {
            cout << "对象没人使用,进行释放:" << _ptr << endl;
            _del(_ptr);
        }
    }
    
    // ......
private:
	T* _ptr;
	int* _count;
	mutex* _mtx;

	D _del; // 删除器
};

template<class T>
struct DeleteArray
{
	void operator()(const T* ptr)
	{
		delete[] ptr;
		cout << "delete[] " << ptr << endl;
	}
};

int main()
{
	liren::shared_ptr<int> sp1(new int[10]); // 使用默认删除器,但不是我们想要的delete[]
	liren::shared_ptr<int, DeleteArray<int>> sp2(new int[10], DeleteArray<int>()); // 使用我们传递的删除器,调用delete[]
	return 0;
}

// 运行结果:
delete[] 0000000000008123

Ⅴ. c++11boost中智能指针的关系

  1. C++98 中产生了第一个智能指针 auto_ptr
  2. C++boost 给出了更实用的 scoped_ptrshared_ptrweak_ptr
  3. C++TR1,引入了 shared_ptr 等。不过注意的是 TR1 并不是标准版。
  4. C++11,引入了 unique_ptrshared_ptrweak_ptr。需要注意的是 unique_ptr 对应 boostscoped_ptr。并且这些智能指针的实现原理是参考 boost 中的实现的。

Ⅵ. 守卫锁

RAII 思想除了可以用来设计智能指针,还可以用来设计 守卫锁防止异常安全导致的死锁问题

​ 其实就是我们在 C++11 锁库中的 lock_guard,这是一个很好用的类模板,它用来管理一个锁对象,保持它一直都是开锁的状态,并且会在出了作用域的时候进行自动销毁!

​ 有没有一种可能,就是在我们上锁和解锁的过程中,发生了异常,我滴乖乖,这个时候发生异常那就很麻烦了,为什么呢?因为我们的锁还没释放就已经抛出异常跳出该函数了,那就导致了死锁的结果。

​ 而解决这个问题的方法就是使用捕捉异常,但是这种方法写起来比较挫,所以我们这里不介绍。

​ 还有一种方法就是用上面的守卫锁,它在创建的时候就会上锁,而出了作用域,即使是抛出异常,也算是出了作用域,那么这个守卫锁就会自动析构,防止了上述的死锁的情况!

代码语言:javascript
代码运行次数:0
复制
// C++11的库中也有一个lock_guard,下面的LockGuard造轮子其实就是为了学习他的原理
template<class Mutex>
class LockGuard
{
public:
	LockGuard(Mutex& mtx)
		:_mutex(mtx)
	{
		_mutex.lock();
	}

	~LockGuard()
	{
		_mutex.unlock();
	}

	LockGuard(const LockGuard<Mutex>&) = delete; // 防止锁的拷贝构造
private:
	// 注意这里必须使用引用或者指针,防止拷贝
	Mutex& _mutex;
};

void test_security()
{
	mutex mtx;
	int n = 10000;
	liren::shared_ptr<int> sp1(new int(0));

	thread t1([&]()
		{
			for (int i = 0; i < n; ++i)
			{
				LockGuard<mutex> lock(mtx); // 创建一个守卫锁
				++(*sp1);
			}
		});

	thread t2([&]()
		{
			for (int i = 0; i < n; ++i)
			{
				LockGuard<mutex> lock(mtx); // 创建一个守卫锁
				++(*sp1);
			}
		});
	t1.join();
	t2.join();

	cout << *sp1 << endl;
}

// 调用结果:
20000
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-03-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Ⅰ. 问题的引入
  • Ⅱ. 内存泄漏
    • 一、什么是内存泄漏,内存泄漏的危害
    • 二、内存泄漏的分类
    • 三、如何检测内存泄漏(了解)
    • 四、如何避免内存泄漏
  • Ⅲ. 智能指针的使用及原理
    • 一、RAII
    • 二 、智能指针的原理
    • 三、auto_ptr(摒弃使用)
    • 四、unique_ptr💥💥💥
    • 五、shared_ptr💥💥💥
      • ① shared_ptr 的线程安全问题之引用计数的安全
      • ② shared_ptr的线程安全问题之管理对象的安全
    • 六、shared_ptr线程安全问题总结
    • 七、shared_ptr的循环引用问题
    • 八、weak_ptr
  • Ⅳ. 定制删除器
  • Ⅴ. c++11和boost中智能指针的关系
  • Ⅵ. 守卫锁
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档