目录

1、智能指针的引入

2、智能指针的使用与原理

2.1 RAII 的概念

2.2 智能指针的使用

2.3 智能指针的弊端

3、标准库中的智能指针

3.1 auto_ptr

3.1.1 auto_ptr的使用

3.1.2 auto_ptr的实现

3.2 unique_ptr

3.2.1 unqiue_ptr 的使用

3.2.2 unique_ptr 的实现

3.3 shared_ptr

3.3.1 shared_ptr 的使用

3.3.2 shared_ptr 的实现

3.3.3 shared_ptr的线程安全问题

3.3.4 定制删除器

3.4 weak_ptr

3.4.1 weak_ptr 的使用

3.4.2 weak_ptr 的实现

4、内存泄露

4.1 概念与危害

4.2 如何检测内存泄漏

4.3 如何避免内存泄漏


1、智能指针的引入

# 首先我们在异常章节学过,如果对异常直接抛出,因为抛出异常可能会跳过某些代码不执行,所以就可能造成内存泄漏的问题。比如下面这段代码:

void func1()
{
	//抛出异常
	throw string("这是一个异常");
}
void func2()
{
	int* arr = new int[10];
	func1();
	//...
	cout << "delete[] arr" << endl;
	delete[] arr;//释放内存
}
int main()
{
	try
	{
		func2();
	}
	catch (const string& s)
	{
		cout << s << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

# 我们可以利用对异常重新抛出的方式解决这个问题:

void func1()
{
	//抛出异常
	throw string("这是一个异常");
}
void func2()
{
	int* arr = new int[10];
	try
	{
		func1();
	}
	catch (...)
	{
		cout << "delete[] arr" << endl;
		delete[] arr;//释放内存
		throw;//重新抛出
	}
	//...
	cout << "delete[] arr" << endl;
	delete[] arr;//释放内存
}
int main()
{
	try
	{
		func2();
	}
	catch (const string& s)
	{
		cout << s << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

# 而本章节我们还可以利用一种方式解决这个问题,那就是智能指针,其本质是一种 RAII 的思想。

2、智能指针的使用与原理

2.1 RAII 的概念

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

  1. 不需要显式地释放资源。
  2. 采用这种方式,对象所需的资源在其生命期内始终保持有效。

2.2 智能指针的使用

# 然后我们就能利用 RAII 的思想,实现一个智能指针,既然是指针,我们自然也要重载 *->

#include<vector>
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		:_ptr(ptr)
	{}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	~SmartPtr()
	{
		cout << "delete _ptr" << endl;
		if (_ptr)
			delete _ptr;
	}
private:
	T* _ptr;
};

# 然后我们就可以利用智能指针解决上面的内存泄漏问题:

void func1()
{
	//抛出异常
	throw string("这是一个异常");
}
void func2()
{
	SmartPtr<int> ptr (new int);
	func1();
	//...
}
int main()
{
	try
	{
		func2();
	}
	catch (const string& s)
	{
		cout << s << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

# 在初始化时首先利用原生指针构造一个 SmartPtr 的对象,然后无论是程序正常返回,还是因为抛异常返回,该对象出了作用域之后会自动调用其析构函数,释放内存。

2.3 智能指针的弊端

# 其实智能指针是存在一些弊端的,比如说如果对智能指针进行拷贝构造或者赋值重载,那么就发生内存崩溃与内存泄漏的问题.

# 比如说我们用智能指针定义了四个对象,分别为 ptr1,ptr2,ptr3,ptr4。其中用 ptr2 拷贝 ptr1,然后用 ptr4 赋值 ptr3。

void Test1()
{
    SmartPtr<int> ptr1 = new int;
    SmartPtr<int> ptr2(ptr1);//拷贝构造
    SmartPtr<int> ptr3 = new int;
    SmartPtr<int> ptr4 = new int;
    ptr3 = ptr4;//赋值重载
}

# 这是因为默认生成的拷贝构造是浅拷贝,ptr1 与 ptr2 指向同一块空间,所以析构时对同一块空间析构两次,就会内存崩溃。而且默认生成的赋值重载也是浅拷贝,ptr3 与 ptr4 也是浅拷贝,析构时也会发生内存崩溃,同时因为 ptr4 指向 ptr3 的空间,所以原本 ptr4 的空间就会发生内存泄漏。

# 那我们深拷贝不就行了吗? 但是智能指针的拷贝希望的就是两个智能指针共同管理这块资源,要的就是浅拷贝,所以这里问题的本质就是需要浅拷贝又不能析构两次。

3、标准库中的智能指针

# 因为智能指针的种种弊端,所以 C++ 标准库提供三种不同的智能指针。

3.1 auto_ptr

3.1.1 auto_ptr的使用

# auto_ptr 是 C++98 中引入的智能指针,其通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源在任何时刻都只有一个对象在对其进行管理,这就能避免浅拷贝的多次析构问题。

void Test2()
{
	auto_ptr<int> ap1(new int(3));
	auto_ptr<int> ap2(ap1);
	*ap2 = 1;
	//*ap1 = 2; //error
	auto_ptr<int> ap3(new int(1));
	auto_ptr<int> ap4(new int(2));
	ap3 = ap4;
}

# 但是唯一需要注意的是:管理权转移之后,就不能对原有空间就行访问,否则就会崩溃。这就导致使用 auto_ptr 之前首先得了解其机制,否则就可能出错,这就为智能指针的使用增加了成本。因此有些公司会明令禁止使用 auto_ptr。

3.1.2 auto_ptr的实现

# auto_ptr 本质和我们前面实现的 SmartPtr 没什么区别,只是在拷贝构造与赋值重载时需要对原来指针置空。

namespace tata
{
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			//置空
			ap._ptr = nullptr;
		}
		auto_ptr& operator=(auto_ptr<T>& ap)
		{
			if (this != &ap)
			{
				delete _ptr;
				_ptr = ap._ptr;
				ap._ptr = nullptr;//置空
			}
			return *this;
		}
		~auto_ptr()
		{
			if (_ptr)
				delete _ptr;
		}
	private:
		T* _ptr;
	};
}

3.2 unique_ptr

3.2.1 unqiue_ptr 的使用

# unqiue_ptr 是 C++11 中引入的智能指针,unqiue_ptr 通过直接防止拷贝与赋值的方式解决智能指针的拷贝问题,这样也能保证资源不会被多次释放。比如:

int main()
{
	std::unique_ptr<int> up1(new int(0));
	//std::unique_ptr<int> up2(up1); //error
	return 0;
}

# 但防拷贝其实也不是一个很好的办法,因为总有一些场景需要进行拷贝。

3.2.2 unique_ptr 的实现

# unique_ptr 本质和我们前面实现的 SmartPtr 也没什么区别,只是禁止对其进行拷贝构造与赋值重载。

# shared_ptr 和 unique_ptr 都得构造函数都使用 explicit 修饰,防止普通指针隐式类型转换成智能指针对象。

#include <iostream>
namespace tata
{
    template <class T>
    class unique_ptr
    {
    public:
        unique_ptr(T *ptr = nullptr) explicit
            : _ptr(ptr)
        {
        }
        ~unique_ptr()
        {
            if (_ptr != nullptr)
            {
                delete _ptr;
                _ptr = nullptr;
            }
        }
        T &operator*()
        {
            return *_ptr;
        }
        T *operator->()
        {
            return _ptr;
        }
        unique_ptr(const unique_ptr<T> &up) = delete;
        unique_ptr<T> &operator=(const unique_ptr<T> &up) = delete;

    private:
        T *_ptr;
    };
};

3.3 shared_ptr

3.3.1 shared_ptr 的使用

# 在 C++11 中又引入了一种极为实用的智能指针 —— shared_ptr。它主要通过引用计数的巧妙机制来妥善解决智能指针在拷贝过程中可能出现的问题

# 具体而言,对于每一个被 shared_ptr 所管理的资源,都会存在一个与之对应的引用计数。这个引用计数扮演着关键角色,它精准地记录着当下究竟有多少个对象正在对这块特定的资源实施管理。

# 当出现新增一个对象开始管理这块资源的情况时,相应地,就会对该资源所对应的引用计数执行自增操作(++)。反之,当某个对象不再管理这块资源,或者该对象自身经历析构过程时,那么就会针对该资源对应的引用计数开展自减操作()。而一旦某一资源的引用计数经过一系列的增减操作后最终减为 0,这便意味着此刻已经不存在任何对象在对这块资源进行管理了。

int main()
{
	shared_ptr<int> sp1(new int(1));
	shared_ptr<int> sp2(sp1);
	*sp1 = 10;
	*sp2 = 20;
	cout << sp1.use_count() << endl; //2

	shared_ptr<int> sp3(new int(1));
	shared_ptr<int> sp4(new int(2));
	sp3 = sp4;
	cout << sp3.use_count() << endl; //2
	return 0;
}

3.3.2 shared_ptr 的实现

# 我们实现 shared_ptr 首先得知道指向同一块资源的变量也应该指向同一个引用计数,相当于将各个资源与其对应的引用计数进行了绑定。所以引用计数的资源也是在堆上开辟的。

# 由于引用计数的内存空间也是在堆上开辟的,因此当一个资源对应的引用计数减为 0 时,除了需要将该资源释放,还需要将该资源对应的引用计数的内存空间进行释放。

    template<class T>
	class shared_ptr
	{
	public:
		explicit shared_ptr(T* ptr = nullptr)
			: _ptr(ptr)
			, _pcount(new int(1))
		{}
		template<class D>
		shared_ptr(T* ptr, D del)
			: _ptr(ptr)
			, _pcount(new int(1))
			, _del(del)
		{}
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
			, _del(sp._del)
		{
			++(*_pcount);
		}
		void release()
		{
			if (--(*_pcount) == 0)
			{
				// 最后⼀个管理的对象,释放资源
				_del(_ptr);
				delete _pcount;
				_ptr = nullptr;
				_pcount = nullptr;
			}
		}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				release();
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				++(*_pcount);
				_del = sp._del;
			}
			return *this;
		}
		~shared_ptr()
		{
			release();
		}
		T* get() const
		{
			return _ptr;
		}
		int use_count() const
		{
			return *_pcount;
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pcount;
		//atomic<int>* _pcount;
		function<void(T*)> _del = [](T* ptr) {delete ptr; };
	// 需要注意的是我们这⾥实现的shared_ptr是以最简洁的⽅式实现的,只能满⾜基本的功能

3.3.3 shared_ptr的线程安全问题

# 起始我们模拟实现的 shared_ptr 还存在一个非常严重的问题,那就是线程安全问题,原因在于:

管理同一资源的多个 shared_ptr 对象的引用计数是共享的。多个线程可能同时对同一个引用计数进行自增(如通过拷贝 shared_ptr 对象)或自减(如拷贝出的对象被销毁)操作。而自增和自减操作都不是原子操作,这就可能导致在多线程环境下出现数据不一致的情况。

# 比如我们以下面代码为例:

#include <iostream>
#include <thread>
#include "MySmartPtr.hpp"
// 函数接受一个shared_ptr和循环次数参数
void func(tata::shared_ptr<int>& sp, size_t n)
{
    for (size_t i = 0; i < n; i++)
    {
        // 每次循环拷贝一份shared_ptr
        tata::shared_ptr<int> copy(sp);
    }
}

int main()
{
    // 创建一个管理整型变量的shared_ptr
    tata::shared_ptr<int> p(new int(0));

    const size_t n = 1000;
    // 创建两个线程并传入相同的shared_ptr和循环次数
    std::thread t1(func, p, n);
    std::thread t2(func, p, n);

    // 等待两个线程执行完毕
    t1.join();
    t2.join();

    // 输出最终的引用计数,理论上应为1
    std::cout << p.use_count() << std::endl; 

    return 0;
}

# 在上述代码中:两个线程 t1 和 t2 分别对同一个 shared_ptr 对象 p 进行 1000 次拷贝操作,拷贝出的对象随后会立即被销毁。在此过程中,两个线程会不断对引用计数进行自增和自减操作。理论上,当两个线程执行完毕后,引用计数的值应该是 1,因为拷贝出来的对象都已销毁,只剩最初的 shared_ptr 对象在管理整型变量。但实际每次运行程序得到引用计数的值可能都不一样,这正是由于对引用计数的自增和自减操作不是原子操作所导致的线程安全问题。

# 要解决引用计数的线程安全问题,关键在于使引用计数的自增和自减操作变为原子操作,这里以加锁为例阐述解决办法。首先在 shared_ptr 类中新增堆区创建的互斥锁成员变量,如此可确保管理同一资源的多个线程访问同一互斥锁,管理不同资源的线程访问不同互斥锁。调用拷贝构造函数与拷贝赋值函数时,除传递资源和引用计数外,还需移交对应的互斥锁。

# 当资源引用计数减为 0 时,除释放资源与引用计数,也要释放对应的互斥锁。为简化代码逻辑,可将拷贝构造函数和拷贝赋值函数中的引用计数自增操作封装成 AddRef 函数,将拷贝赋值函数和析构函数中的引用计数自减操作封装成 ReleaseRef 函数,后续只需对 AddRef 和 ReleaseRef 函数进行加锁保护即可。

template <class T>
class shared_ptr
{
private:
    //++引用计数
    void AddRef()
    {
        // 对互斥锁加锁,确保同一时间只有一个线程能操作引用计数
        _pmutex->lock();
        // 引用计数自增,表示多了一个对象共享该资源
        (*_pcount)++;
        // 操作完成后对互斥锁解锁
        _pmutex->unlock();
    }

    //--引用计数
    void ReleaseRef()
    {
        // 对互斥锁加锁
        _pmutex->lock();
        bool flag = false;

        // 引用计数减1,如果减到0,表示没有对象再共享该资源了
        if (--(*_pcount) == 0) 
        {
            if (_ptr!= nullptr)
            {
                // 如果资源指针不为空,释放该资源所占用的内存
                delete _ptr;
                _ptr = nullptr;
            }

            // 释放引用计数所占用的内存
            delete _pcount;
            _pcount = nullptr;

            flag = true;
        }

        // 对互斥锁解锁
        _pmutex->unlock();

        // 如果引用计数为0,释放互斥锁所占用的内存
        if (flag == true)
        {
            delete _pmutex;
        }
    }

public:
    // RAII(Resource Acquisition Is Initialization)机制:
    // 在构造函数中初始化资源、引用计数和互斥锁
    // 构造函数,默认参数为nullptr,可传入一个指向T类型对象的指针
    shared_ptr(T *ptr = nullptr)
        : _ptr(ptr), _pcount(new int(1)), 
        _pmutex(new std::mutex)
    {
    }

    // 析构函数,在对象销毁时调用ReleaseRef函数释放资源等
    ~shared_ptr()
    {
        ReleaseRef();
    }

    // 拷贝构造函数,用于创建一个新的shared_ptr对象,共享原对象管理的资源
    // 并增加引用计数
    shared_ptr(shared_ptr<T> &sp)
        : _ptr(sp._ptr), 
        _pcount(sp._pcount),
        _pmutex(sp._pmutex)
    {
        AddRef();
    }

    // 赋值运算符重载,用于将一个shared_ptr对象赋值给另一个
    // 如果两个对象管理的资源不同,先释放原对象资源,再共享新对象资源并更新引用计数
    shared_ptr &operator=(shared_ptr<T> &sp)
    {
        if (_ptr!= sp._ptr) 
        {
            ReleaseRef();         
            _ptr = sp._ptr;      
            _pcount = sp._pcount; 
            _pmutex = sp._pmutex; 
            AddRef();            
        }
        return *this;
    }

    // 获取引用计数,返回当前管理的资源的引用计数
    int use_count()
    {
        return *_pcount;
    }

    // 重载*运算符,使得可以像使用普通指针一样通过*来访问所管理资源的值
    T &operator*()
    {
        return *_ptr;
    }

    // 重载->运算符,使得可以像使用普通指针一样通过->来访问所管理资源的成员
    T *operator->()
    {
        return _ptr;
    }

private:
    T *_ptr;             // 管理的资源,指向T类型的对象
    int *_pcount;        // 管理的资源对应的引用计数,记录共享该资源的对象个数
    std::mutex *_pmutex; // 管理的资源对应的互斥锁,用于多线程环境下保护引用计数的操作安全
};

3.3.4 定制删除器

# 智能指针对象生命周期结束时,默认皆以 delete 方式释放资源,此做法欠妥。因智能指针所管理的资源并非仅限于通过 new 方式申请的内存空间,它还可能管理以 new [ ] 方式申请到的空间,亦或是一个文件指针等其他类型资源,仅采用 delete 方式来释放显然不能适配所有管理对象的释放需求。为此智能指针还提供一个模版参数 D 来定制删除资源的方式:

  • p:需要让智能指针管理的资源。
  • del:删除器,这个删除器是一个可调用对象,比如函数指针、仿函数、lambda 表达式以及被包装器包装后的可调用对象。

# 比如我们以下面代码为例:

int main()
{
    shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* ptr) {fclose(ptr); });//文件
	shared_ptr<int> sp4((int*)malloc(4), [](int* ptr) {free(ptr); });
    return 0;
}

# 为此,我们也可以通过 C++11 提供的包装器实现一个定制删除器:

    template <class T>
    class shared_ptr
    {
    private:
        //++引用计数
        void AddRef()
        {
            // 对互斥锁加锁,确保同一时间只有一个线程能操作引用计数
            _pmutex->lock();
            // 引用计数自增,表示多了一个对象共享该资源
            (*_pcount)++;
            // 操作完成后对互斥锁解锁
            _pmutex->unlock();
        }

        //--引用计数
        void ReleaseRef()
        {
            // 对互斥锁加锁
            _pmutex->lock();
            bool flag = false;

            // 引用计数减1,如果减到0,表示没有对象再共享该资源了
            if (--(*_pcount) == 0)
            {
                if (_ptr != nullptr)
                {
                    // 如果资源指针不为空,释放该资源所占用的内存
                    _del(_ptr);
                    _ptr = nullptr;
                }

                // 释放引用计数所占用的内存
                delete _pcount;
                _pcount = nullptr;

                flag = true;
            }

            // 对互斥锁解锁
            _pmutex->unlock();

            // 如果引用计数为0,释放互斥锁所占用的内存
            if (flag == true)
            {
                delete _pmutex;
            }
        }

    public:
        // RAII(Resource Acquisition Is Initialization)机制:
        // 在构造函数中初始化资源、引用计数和互斥锁
        // 构造函数,默认参数为nullptr,可传入一个指向T类型对象的指针
        template <class D>
        shared_ptr(T *ptr = nullptr, D del = [](T *ptr)
        { delete ptr; })
            : _ptr(ptr), _pcount(new int(1)),
              _pmutex(new std::mutex),
              _del(del)
        {
        }
        shared_ptr(T *ptr)
            : _ptr(ptr), _pcount(new int(1)),
              _pmutex(new std::mutex)
        {
        }
        // 析构函数,在对象销毁时调用ReleaseRef函数释放资源等
        ~shared_ptr()
        {
            ReleaseRef();
        }

        // 拷贝构造函数,用于创建一个新的shared_ptr对象,共享原对象管理的资源
        // 并增加引用计数
        shared_ptr(shared_ptr<T> &sp)
            : _ptr(sp._ptr),
              _pcount(sp._pcount),
              _pmutex(sp._pmutex)
        {
            AddRef();
        }

        // 赋值运算符重载,用于将一个shared_ptr对象赋值给另一个
        // 如果两个对象管理的资源不同,先释放原对象资源,再共享新对象资源并更新引用计数
        shared_ptr<T> &operator=(shared_ptr<T> &sp)
        {
            if (_ptr != sp._ptr)
            {
                ReleaseRef();
                _ptr = sp._ptr;
                _pcount = sp._pcount;
                _pmutex = sp._pmutex;
                AddRef();
            }
            return *this;
        }

        // 获取引用计数,返回当前管理的资源的引用计数
        int use_count()
        {
            return *_pcount;
        }
        T* get() const
        {
            return _ptr;
        }
        // 重载*运算符,使得可以像使用普通指针一样通过*来访问所管理资源的值
        T &operator*()
        {
            return *_ptr;
        }

        // 重载->运算符,使得可以像使用普通指针一样通过->来访问所管理资源的成员
        T *operator->()
        {
            return _ptr;
        }

    private:
        T *_ptr;   // 管理的资源,指向T类型的对象
        int *_pcount;        // 管理的资源对应的引用计数,记录共享该资源的对象个数
        std::mutex *_pmutex; // 管理的资源对应的互斥锁,用于多线程环境下保护引用计数的操作安全
        std::function<void(T *)> _del;  // 定制删除器
    };

3.4 weak_ptr

3.4.1 weak_ptr 的使用

# share_ptr 虽然看似很完美,但是在某些特殊情况就会引发出一种名为循环引用的问题,比如下面这段代码:

#include<iostream>
using namespace std;
#include<memory>
struct ListNode
{
	shared_ptr<ListNode> _next;
	shared_ptr<ListNode> _prev;
	int _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

int main()
{
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	node1->_next = node2;
	node2->_prev = node1;
	return 0;
}

# 这时程序运行结束后两个结点都没有被释放,但如果去掉连接结点时的两句代码中的任意一句,那么这两个结点就都能够正确释放,根本原因就是因为这两句连接结点的代码导致了循环引用。至于什么是循环引用,我们可以通过下面图示说明:

# 当出了 main 函数的作用域后,node1 和 node2 的生命周期也就结束了,因此这两个资源对应的引用计数最终都减到了 1,但是此时就会出现一个致命的问题:

# 资源的释放条件是其对应的引用计数减为 0。而在此情境下,资源1的释放取决于资源 2 的 _prev 成员,资源 2 的释放则取决于资源 1 的 _next 成员。如此一来,便形成了一个相互制约的死循环,使得资源1和资源2最终都无法按照正常机制完成释放操作。

# 而为了解决这个问题,C++11 就又提供了一个智能指针 weak_ptr。weak_ptr 不是用来管理资源的释放的,它主要是用来解决 shared_ptr 的循环引用问题的。

# 比如将 ListNode 中的 _next 和 _prev 成员的类型换成 weak_ptr 就不会导致循环引用问题:

#include<iostream>
using namespace std;
#include<memory>
struct ListNode
{
	weak_ptr<ListNode> _next;
	weak_ptr<ListNode> _prev;
	int _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

int main()
{
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	node1->_next = node2;
	node2->_prev = node1;
	return 0;
}

3.4.2 weak_ptr 的实现

# 而实现 weak_ptr 其实也很简单,只需要在前面实现智能指针的基础上专门提供 share_ptr 的构造方法即可。

template <class T>
class weak_ptr
{
public:
    // 1. 空构造函数:初始化裸指针为nullptr
    weak_ptr()
        : _ptr(nullptr)
    {
    }

    // 2. 从shared_ptr构造:仅拷贝裸指针
    weak_ptr(const shared_ptr<T> &sp)
        : _ptr(sp.get())  // 只拿shared_ptr管理的裸指针
    {
    }

    // 3. 赋值运算符:从shared_ptr拷贝裸指针
    weak_ptr<T> &operator=(const shared_ptr<T> &sp)
    {
        _ptr = sp.get();
        return *this;
    }

    // 4. 重载*运算符:直接解引用裸指针
    T &operator*()
    {
        return *_ptr;
    }

    // 5. 重载->运算符:直接返回裸指针
    T *operator->()
    {
        return _ptr;
    }

private:
    T *_ptr; // 仅存储裸指针
};

4、内存泄露

4.1 概念与危害

# 内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存,一般是忘记释放或者发生异常释放程序未能执行导致的。内存泄漏并不是指内存在物理上的消失,而是是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

# 危害:普通程序运行⼀会就结束了出现内存泄漏问题也不大,进程正常结束,页表的映射关系解除,物理内存也可以释放。长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务、长时间运行的客户端等等,不断出现内存泄漏会导致可用内存不断变少,各种功能响应越来越慢,最终卡死。

int main()
{
// 申请⼀个1G未释放,这个程序多次运⾏也没啥危害
// 因为程序⻢上就结束,进程结束各种资源也就回收了
char* ptr = new char[1024 * 1024 * 1024];
cout << (void*)ptr << endl;
return 0;
}

4.2 如何检测内存泄漏

# linux下内存泄漏检测:linux下⼏款内存泄漏检测工具

# windows下第三方检测工具:windows下的内存泄露检测工具VLD使用 windows内存泄漏检测工具-CSDN博客

4.3 如何避免内存泄漏

# 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下⼀条智能指针来管理才有保证。

# 尽量使用智能指针来管理资源,如果自己场景比较特殊,采用 RAII 思想自己造个轮子管理。
定期使用内存泄漏工具检测,尤其是每次项目快上线前,不过有些工具不够靠谱,或者是收费。
总结⼀下:内存泄漏非常常见,解决方案分为两种:

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

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐