1.智能指针的使⽤场景分析

问题:手动 new/delete 在异常下的困境

在 Divide 函数可能抛异常,new 也可能抛异常的情况下:

  • array1 先 newarray2 后 new

  • 如果 array2 的 new 抛异常 → 需要手动释放 array1

  • 如果 Divide 抛异常 → 需要释放 array1 和 array2

  • 代码中必须大量 try/catch 并重复 delete极易遗漏或导致冗余

核心问题

资源获取后,在异常到达 delete 之前就可能跳出作用域

double Divide(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Divide by zero condition!";
	}
	else
	{
		return (double)a / (double)b;
	}
}
void Func()
{
	// 这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array和array2没有得到释放。
	// 所以这⾥捕获异常后并不处理异常,异常还是交给外⾯处理,这⾥捕获了再重新抛出去。
	// 但是如果array2new的时候抛异常呢,就还需要套⼀层捕获释放逻辑,这⾥更好解决⽅案
	// 是智能指针,否则代码存在冗余
	int* array1 = new int[10];
	int* array2 = new int[10]; // new抛异常要释放array1
	try
	{
		int len, time;
		cin >> len >> time;
		cout << Divide(len, time) << endl;
	}
	catch (...)
	{
		cout << "delete []" << array1 << endl;
		cout << "delete []" << array2 << endl;
		delete[] array1;//手动管理:每个 new 都需要匹配 delete,且异常路径下极易泄漏
		delete[] array2;
		throw; // 异常重新抛出,捕获到什么抛出什么
	}
	// ...
	cout << "delete []" << array1 << endl;
	delete[] array1;
	cout << "delete []" << array2 << endl;
	delete[] array2;
}
int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

上⾯程序中我们可以看到,new了以后,我们也delete了,但是因为抛异常导致后⾯的delete没有得到执⾏,所以就内存泄漏了,所以我们需要new以后捕获异常,捕获到异常后delete内存,再把异常抛出,但是因为new本⾝也可能抛异常,连续的两个new和下⾯的Divide都可能会抛异常,让我们处理起来很⿇烦。智能指针放到这样的场景⾥⾯就让问题简单多了。

  • 智能指针:作用域结束自动释放,彻底解决“忘记 delete”和“异常跳过 delete”的问题

建议:现代 C++ 中,除非极特殊情况,否则不要显式调用 new/delete,优先使用智能指针或容器(如 vector)。

2.RAII和智能指针的设计思路

RAII 与智能指针

RAII(Resource Acquisition Is Initialization)是一种管理资源的类设计思想。

RAII 就是:将资源的生命周期,绑定到某个对象的生命周期(即利用对象的生命周期来管理获取到的动态资源,避免资源泄漏)。这里的资源可以是内存、文件指针、网络连接、互斥锁等。RAII 的工作流程分为三步:

  1. 获取资源时:把资源委托给一个对象(通常在构造函数中完成)

  2. 使用资源时:通过该对象控制对资源的访问,资源在对象的生命周期内始终保持有效

  3. 释放资源时:在对象析构时释放资源,保障资源的正常释放,避免资源泄漏


智能指针在 RAII 的基础上更进一步:

  • 满足 RAII 的设计思路(自动管理资源生命周期)

  • 方便访问资源:像迭代器一样,重载 operator*operator->operator[] 等运算符,使资源的使用方式与原始指针类似,既安全又便捷。

template<class T>
class SmartPtr
{
public:
	// RAII
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		cout << "delete[] " << _ptr << endl;
		delete[] _ptr;
	}
	// 重载运算符,模拟指针的⾏为,⽅便访问资源
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	T& operator[](size_t i)
	{
		return _ptr[i];
	}
private:
	T* _ptr;
};
double Divide(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Divide by zero condition!";
	}
	else
	{
		return (double)a / (double)b;
	}
}
void Func()
{
	// 这⾥使⽤RAII的智能指针类管理new出来的数组以后,程序简单多了
	SmartPtr<int> sp1 = new int[10];
	SmartPtr<int> sp2 = new int[10];
	for (size_t i = 0; i < 10; i++)
	{
		sp1[i] = sp2[i] = i;
	}
	int len, time;
	cin >> len >> time;
	cout << Divide(len, time) << endl;
}
int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

3.C++标准库智能指针的使⽤

在上面简单的SmartPtr实现中在拷贝时,由于没有实现拷贝构造于是编译器默认生成浅拷贝的拷贝构造,于是导致两个智能指针对象指向同一块资源,同一块空间就析构了两次。

但这里又不是深拷贝因为智能指针模拟的是原生指针行为,原生指针给给原生指针期望的是两个指针指向同一个资源,共同管理,我们期望的就是浅拷贝但是不想析构两次于是就有了下面的发展历史:

头文件:#include <memory>

智能指针的核心设计思想:RAII + 指针行为模拟。它们的主要区别在于拷贝时的资源管理策略


一、智能指针的发展历程


二、各智能指针详解

参考文档:cplusplus.com/reference/memory/

1. auto_ptr(C++98)—— 已废弃

特点: 拷贝时将被拷贝对象的资源管理权转移给拷贝对象。

问题: 被拷贝对象会变成悬空指针,访问时导致程序崩溃。

std::auto_ptr<int> p1(new int(42));
std::auto_ptr<int> p2 = p1;  // 所有权转移,p1 变为空
*p1 = 10;  // 崩溃!p1 已经悬空

结论: 这是一个非常糟糕的设计,C++11 后强烈禁止使用。


2. unique_ptr(C++11)—— 独占所有权

特点: 不⽀持拷贝,只支持移动语义。

适用场景: 不需要拷贝的场景。

std::unique_ptr<int> p1(new int(42));
// std::unique_ptr<int> p2 = p1;  //  编译错误
std::unique_ptr<int> p2 = std::move(p1);  //  移动,p1 变空

这里的做法就是直接将拷贝构造函数和拷贝赋值重载删除(delete)


3. shared_ptr(C++11)—— 共享所有权

特点: 支持拷贝,底层使用引用计数实现。

适用场景: 需要多个指针共享同一资源的场景。

std::shared_ptr<int> p1(new int(42));
std::shared_ptr<int> p2 = p1;  //  拷贝,引用计数 +1
// p1 和 p2 都离开作用域后,资源才释放

shared_ptr 通过控制块中的引用计数记录有多少个指针共享同一资源,每次拷贝计数 +1,每次析构计数 -1,当计数从 1 变成 0 时,才真正释放资源。


4. weak_ptr(C++11)—— 弱引用

特点:

  • 不支持 RAII,不能直接管理资源

  • 配合 shared_ptr 使用,不影响引用计数

作用: 解决 shared_ptr 的循环引用导致的内存泄漏问题。

struct Node 
{
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // 使用 weak_ptr 打破循环
};

三、删除器(Deleter)

问题

智能指针析构时默认使用 delete 释放资源。如果管理的是非 new 分配的资源(如 malloc、文件句柄、自定义释放逻辑),直接 delete 会崩溃。

// 会崩溃
unique_ptr<Date> up1(new Date[10]);
shared_ptr<Date> sp1(new Date[10]);

原因:

~unique_ptr()
{
    if (_ptr) delete _ptr;  // delete 只释放一个对象
                            // 对于 new[10],应该用 delete[]
}

new[] 分配了 10 个连续对象,必须用 delete[] 释放。delete 只会调用一次析构函数并释放一个对象大小的内存,导致:

  • 其余 9 个对象没有调用析构函数(资源泄漏)

  • 释放的内存大小不对(可能导致堆损坏)

解决方案 1:特化版本 T[]

unique_ptr<Date[]> up1(new Date[5]);   // 正确
shared_ptr<Date[]> sp1(new Date[5]);   // 正确

原理:标准库对 T[] 进行了模板特化

// 普通版本 T
template<class T>
class unique_ptr
{
    T* _ptr;
    ~unique_ptr() { delete _ptr; }  // 用 delete
};
// 特化版本 T[]
template<class T>
class unique_ptr<T[]>
{
    T* _ptr;
    ~unique_ptr() { delete[] _ptr; }  // 用 delete[]
    
    // 提供 operator[] 而不是 operator* 和 operator->
    T& operator[](size_t index) { return _ptr[index]; }
};

使用区别

unique_ptr<Date>    up(new Date);      // 管理单个对象,支持 *up, up->
unique_ptr<Date[]>  up(new Date[5]);   // 管理数组,支持 up[0], up[1]

shared_ptr<Date>    sp(new Date);      // 管理单个对象,支持 *sp, sp->
shared_ptr<Date[]>  sp(new Date[5]);   // 管理数组,支持 sp[0], sp[1](C++17 起)

解决方案 2:删除器(Deleter,更通用)

1. 仿函数(Functor)做删除器

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

// unique_ptr:删除器类型作为模板参数
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
//               ↑ 第二个模板参数指定删除器类型

// shared_ptr:删除器对象作为构造函数参数
shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());
//              ↑ 构造函数第二个参数传递删除器对象

为什么 unique_ptr 可以不在构造函数传递仿函数?

unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
// 等价于
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5], DeleteArray<Date>());

因为 DeleteArray 是空类(没有成员变量),默认构造即可。unique_ptr 内部会存储一个 DeleteArray 对象。

template<class T, class Deleter>
class unique_ptr
{
public:
    // 第二个参数给了默认值 Deleter()
    unique_ptr(T* ptr, Deleter del = Deleter())
        : _ptr(ptr), _del(del)
    {}
    // ...                          ↑ 默认构造一个 Deleter 对象,由于 lambda 没有默认构造,生成不了匿名对象,所以还要显式在参数传递
private:
    T* _ptr;
    Deleter _del;  // 存储删除器对象
};

2. 函数指针做删除器

template<class T>
void DeleteArrayFunc(T* ptr)
{
    delete[] ptr;
}

// unique_ptr:必须传函数指针(因为不能从类型推导出具体函数)
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
//               ↑ 删除器类型                      ↑ 删除器函数

// shared_ptr:直接传函数指针
shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);

注意unique_ptr 用函数指针时必须传具体的函数地址,因为它需要构造删除器对象。


3. Lambda 表达式做删除器

auto delArrOBJ = [](Date* ptr) { delete[] ptr; };

// unique_ptr:需要 decltype 推导 lambda 类型
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
//               ↑ lambda 类型         ↑ lambda 对象

// shared_ptr:直接传 lambda
shared_ptr<Date> sp4(new Date[5], delArrOBJ);

Lambda 本质:编译器生成一个匿名仿函数类,所以用法和仿函数类似。

注意:Lambda 没有默认构造 → Deleter() 无法生成匿名对象 → 必须显式在参数传递。所以这样是不行的:unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5]);显式传递删除器 → 实参拷贝/移动构造形参 → 形参拷贝/移动构造成员变量 _del

总结:unique推荐用仿函数,shared推荐用lamda


4. 管理其他资源(FILE* 示例)

// 仿函数版本
struct Fclose
{
    void operator()(FILE* ptr) const
    {
        cout << "fclose:" << ptr << endl;
        fclose(ptr);
    }
};

shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());

// Lambda 版本
shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
    cout << "fclose:" << ptr << endl;
    fclose(ptr);
});

5. unique_ptr 和 shared_ptr 删除器的核心区别

为什么设计不同?

// unique_ptr:删除器是类型的一部分
unique_ptr<Date, Deleter1> p1;
unique_ptr<Date, Deleter2> p2;
// p1 和 p2 是不同的类型!

// shared_ptr:删除器不是类型的一部分
shared_ptr<Date> p3(new Date, Deleter1());
shared_ptr<Date> p4(new Date, Deleter2());
// p3 和 p4 是相同的类型!可以放入同一个容器

shared_ptr 的设计让不同删除器的对象可以作为同一类型,更灵活。代价是用类型擦除(std::function 或虚函数)存储删除器,有微小性能开销。


四、完整的带删除器实现

unique_ptr 简化版

template<class T, class Deleter = default_delete<T>>
class unique_ptr
{
public:
    explicit unique_ptr(T* ptr, Deleter del = Deleter())
        : _ptr(ptr), _del(del)
    {}

    ~unique_ptr()
    {
        if (_ptr) _del(_ptr);  // 调用删除器
    }

    // 移动构造/赋值需要处理删除器
    unique_ptr(unique_ptr&& up) noexcept
        : _ptr(up._ptr), _del(std::move(up._del))
    {
        up._ptr = nullptr;
    }

private:
    T* _ptr;
    Deleter _del;  // 空仿函数通过空基类优化不占空间
};

// 默认删除器
template<class T>
struct default_delete
{
    void operator()(T* ptr) const { delete ptr; }
};

// T[] 特化的删除器
template<class T>
struct default_delete<T[]>
{
    void operator()(T* ptr) const { delete[] ptr; }
};
  • shared_ptr 简化版(类型擦除)

注意事项:

template<class T>  // 只有 T,没有 Deleter 模板参数!
class shared_ptr
{
    T* _ptr;
    ??? _del;      // 这里该写什么类型?不知道!
    
    template<class Deleter>
    shared_ptr(T* ptr, Deleter del)
        : _ptr(ptr), _del(???) {}  // 怎么存?
};

解决方法:类型擦除(Type Erasure)

shared_ptr 的核心思路是:把不同类型的删除器,包装成统一的类型存储。下面采用之前学过的包装器包装lamda对象,标准库使用虚函数,这里参考cplusplus.com/reference/memory/shared_ptr/

template<class T>
class share_ptr
{
public:
	explicit share_ptr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))
	{}

	template<class D>
	explicit share_ptr(T* ptr, D del)
		: _ptr(ptr)
		, _pcount(new int(1))
		, _del(del)//这里不管del为函数指针,lamda,仿函数只要是可调用对象都可以用_del这个包装器包装
	{}

	share_ptr(const share_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
		, _del(sp._del)
	{
		(*_pcount)++;
	}

	//sp1=sp2
	share_ptr<T>& operator=(const share_ptr<T>& sp)
	{
		//if (_ptr != sp._ptr)
		if(_pcount!=sp._pcount)
		{
			if (--(*_pcount) == 0)
			{
				_del(_ptr);
				delete _pcount;
			}
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			(*_pcount)++;
			_del = sp._del;
		}
		return *this;
	}

	~share_ptr()
	{
		if (_pcount&&--(*_pcount) == 0)
		{
			_del(_ptr);
			delete _pcount;
			_ptr = nullptr;
			_pcount = nullptr;
		}
	}

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

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

	int use_count()
	{
		return *_pcount;
	}
private:
	T* _ptr;
	int* _pcount;
	//D _del;//无法定义
	function<void(T*)> _del = [](T* ptr) {delete ptr; };//传缺省,在没传删除器时默认直接delete
};

核心思想:通过可调用对象定制资源释放方式,让智能指针不仅管理 new 出来的内存,还能管理任何需要 RAII 的资源


五、辅助功能

1. make_shared —— 推荐构造方式

template <class T, class... Args>
shared_ptr<T> make_shared(Args&&... args);

优点:

  • 异常安全

  • 一次内存分配(对象 + 控制块),性能更好,并且减少内存碎片

  • 代码更简洁

// 推荐
auto sp = std::make_shared<Date>(2024, 12, 25);

// 不推荐
std::shared_ptr<Date> sp(new Date(2024, 12, 25));

2. operator bool —— 判空检查

智能指针重载了 operator bool,可以直接用于条件判断:

int main()
{
	shared_ptr<Date> sp1(new Date(2024, 9, 11));
	shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
	auto sp3 = make_shared<Date>(2024, 9, 11);
	shared_ptr<Date> sp4;

	// if (sp1.operator bool())
	if (sp1)
		cout << "sp1 is not nullptr" << endl;
	if (!sp4)
		cout << "sp4 is nullptr" << endl;
	//等价于
	/*if (!sp4.operator bool())
		cout << "sp4 is nullptr" << endl;*/
	return 0;
}

3. explicit 构造函数 —— 防止隐式转换

智能指针的构造函数都使用 explicit 修饰,防止普通指针隐式转换成智能指针:

// 编译错误:不能隐式转换
std::shared_ptr<int> p = new int(42);

// 正确:显式构造
std::shared_ptr<int> p(new int(42));

六、总结对比

总结

unique_ptr 独占资源(禁止拷贝),shared_ptr 共享资源(引用计数),weak_ptr 解决循环引用,auto_ptr 已死勿用。删除器让智能指针可以管理任意类型的资源。

4.智能指针的原理

以下模拟实现只是为了大概加深了解智能指针原理,只满足最简单的功能

4.1 模拟实现auto_ptr

auto_ptr 的核心就一个词:"拷贝即转移"。一个 auto_ptr 被拷贝给另一个时,前者就把指针让出来,自己变成空。这么做确实保证了资源不会被双重释放——同一个资源永远只有一个主人。

template<class T>
class auto_ptr
{
public:
	explicit auto_ptr(T*ptr)//防止普通指针隐式类型转换
		:_ptr(ptr)
	{}

	auto_ptr(auto_ptr<T>& ap)
	{
		_ptr = ap._ptr;
		ap._ptr=nullptr;//资源转移
	}

	~auto_ptr()
	{
		if (_ptr)
		{
			delete _ptr;
			cout << "~auto_ptr" << endl;
		}
	}

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

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

	//p1=p2
	auto_ptr<T>& operator=(auto_ptr<T>& ap)
	{
		if (&ap!=this)
		{
			delete _ptr;
			_ptr = ap._ptr;
			ap._ptr = nullptr;	
		}
		return *this;
	}
private:
	T* _ptr;
};

4.2 模拟实现unique_ptr

unique_ptr 吸取了 auto_ptr 的全部教训,核心改成:"拷贝禁止,移动允许"。就是将拷贝构造赋值delete,保留移动构造(资源转移)

template<class T>
class unique_ptr
{
public:
	explicit unique_ptr(T* ptr)
		:_ptr(ptr)
	{
	}
	//禁止拷贝
	unique_ptr(const unique_ptr<T>& up) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>& up)= delete;

	//p1(move(p2))
	unique_ptr(unique_ptr<T>&& up)
	{
		_ptr = up._ptr;
		up._ptr = nullptr;
	}

	//p1=move(p2)
	unique_ptr<T>& operator=(unique_ptr<T>&& up)
	{
		if (this != &up)               // 防止自移动赋值
		{
			delete _ptr;               // 释放旧资源
			_ptr = up._ptr;            // 接管新资源
			up._ptr = nullptr;         // up 放弃所有权
		}
		return *this;
	}

	~unique_ptr()
	{
		if (_ptr)
		{
			delete _ptr;
			cout << "~unique_ptr" << endl;
		}
	}

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

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

4.3 模拟实现shared_ptr

注意事项一:错误的方案——为每个智能指针对象设置独立的 count

如果int  count 是每个智能指针自己的成员变量:

template<class T>
class WrongSharedPtr
{
    T* _ptr;
    int _count = 0; // 每个智能指针对象都有自己独立的一份 count
public:
    // 构造函数
    WrongSharedPtr(T* ptr) : _ptr(ptr), _count(1) {}

    // 拷贝构造函数
    WrongSharedPtr(const WrongSharedPtr& other) 
        : _ptr(other._ptr), _count(1) // 新对象初始化自己的 count 为 1
    {} 

    ~WrongSharedPtr() {
        // 这里只能操作自己的 count,无法影响 other 对象的 count
        // 所以无法得知是否还有其他对象在共享 _ptr
        if (--_count == 0)
        { 
            delete _ptr;
        }
    }
};

推演一下:

  1. WrongSharedPtr<Resource> sp1(new Resource);

    • sp1._count 变为 1。全局只有一个指向资源的指针。

  2. WrongSharedPtr<Resource> sp2 = sp1;

    • sp2 也指向那块资源,但 sp2._count 被初始化为 1。

    • 关键点: sp1 和 sp2 各自内心(count)都只记着自己(1),没有共享同一个计数器。

  3. sp2 离开作用域,析构。

    • sp2 的 _count 从 1 减为 0,条件成立,delete _ptr

    • 资源被释放。

  4. sp1 离开作用域,析构。

    • sp1 对此毫不知情,它自己的 _count 还是 1,减为 0 后,会再次执行 delete _ptr

    • 程序崩溃:同一块内存被释放了两次。

结论: 这种各自为政的计数方式,就像多人共同管理一份文件,但每个人都在自己的笔记本上单独记着“有几个人在看”,彼此信息不通,最终必然出错。

注意事项二:有人可能会想静态的count行不行,答案是不行的,静态成员变量是属于整个类,所有对象共用一个static int count管理两组不同的资源就出问题了

注意事项三:operator=的实现

  1. 原对象可能还有对象在管理资源,要先进行判断如果为最后一个对象就释放资源
  2. 注意防止自赋值情况,当多个对象管理一块资源时似乎没问题但是如果只有一个对象一块支援就把自己的资源释放了,还要注意

正确的方案——使用一个共享的计数指针int* _pcount 指向堆上的同一个计数器

  • 构造时new int(1),计数器初始化为 1。

  • 拷贝构造时:拷贝指针,让两个对象的 _pcount 指向同一个计数器,然后 ++(*_pcount)

  • 析构时--(*_pcount),如果减到 0,说明自己是最后一个,那就 delete _ptr 释放资源,再 delete _pcount 释放计数器自身。

template<class T>
class share_ptr
{
public:
	explicit share_ptr(T* ptr)
		:_ptr(ptr)
		,_pcount(new int(1))
	{}

	share_ptr(const share_ptr<T>& sp)
		:_ptr(sp._ptr)
		,_pcount(sp._pcount)
	{
		(*_pcount)++;
	}

	//sp1=sp2
	share_ptr<T>& operator=(const share_ptr<T>& sp)
	{
		//if (this != &sp)//可以但不推荐
		if(_ptr!=sp._ptr)
		{
			if (--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
			}
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			(*_pcount)++;
		}
		return *this;
	}

	~share_ptr()
	{
		if (--(*_pcount) == 0)
		{
			delete _ptr;
			delete _pcount;
			_ptr = nullptr;
			_pcount = nullptr;
		}
	}

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

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

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

4.4 shared_ptr 的循环引用问题

4.4.1 什么是循环引用

两个 shared_ptr 互相引用,形成闭环,导致引用计数永远不为 0,资源无法释放。

4.4.2 循环引用场景

struct ListNode
{
	int _data;

	std::shared_ptr<ListNode> _next;
	std::shared_ptr<ListNode> _prev;
	// 这⾥改成weak_ptr,当n1->_next = n2;绑定shared_ptr时
	// 不增加n2的引⽤计数,不参与资源释放的管理,就不会形成循环引⽤了

	/*std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;*/

	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	// 循环引⽤ -- 内存泄露
	std::shared_ptr<ListNode> n1(new ListNode);
	std::shared_ptr<ListNode> n2(new ListNode);
	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
	n1->_next = n2;
	n2->_prev = n1;
	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;

	// weak_ptr不⽀持管理资源,不⽀持RAII
	// weak_ptr是专⻔绑定shared_ptr,不增加他的引⽤计数,作为⼀些场景的辅助管理
	//std::weak_ptr<ListNode> wp(new ListNode);
	return 0;
}

4.4.3 引用计数推演

  • 初始:

            n1: 计数=1, _next=null, _prev=null
            n2: 计数=1, _next=null, _prev=null

  • n1->_next = n2 后:

            n1: 计数=1
            n2: 计数=2  ← n1._next 增加一次

  • n2->_prev = n1 后:

            n1: 计数=2  ← n2._prev 增加一次
            n2: 计数=2

  • 析构时

        n2 析构:计数 2→1
        n1 析构:计数 2→1

最终:两个资源计数都是 1,不会释放!内存泄漏!

  1. 右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。

  2.  _next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了。

  3. 左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。

  4.  _prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。

• ⾄此逻辑上成功形成回旋镖似的循环引⽤,谁都不会释放就形成了循环引⽤,导致内存泄漏


4.5 weak_ptr —— 弱引用

 循环引用的解决方案

weak_ptr 是 shared_ptr 的观察者:有以下特性:

// 不支持从裸指针构造
//weak_ptr<Date> wp(new Date);  // 编译错误

// 只能从 shared_ptr 构造
shared_ptr<Date> sp(new Date);
weak_ptr<Date> wp(sp);        // 不增加 shared_ptr 的引用计数
  • 不增加引用计数,只观察资源是否还存在

  • 不能直接访问资源,需要先提升为 shared_ptr

  • 不重载 operator* 和 operator->因为不参与资源管理,如果绑定的 shared_ptr 已经释放了资源,直接访问就是危险的

  • 提供安全检测机制

  • 通过 lock() 安全访问资源

    weak_ptr<Date> wp(sp);
    
    if (auto sp2 = wp.lock())    // 尝试提升为 shared_ptr
    {
        // 资源未释放,通过 sp2 安全访问
        *sp2;   //ok
        sp2->(); //ok
    }
    else
    {
        // 资源已释放,sp2 为空
    }
    资源未释放lock() 返回一个管理资源的 shared_ptr,安全访问;资源已释放lock() 返回空 shared_ptr,不会误访问

使用示例与验证

int main()
{
	std::shared_ptr<string> sp1(new string("111111"));
	std::shared_ptr<string> sp2(sp1);
	std::weak_ptr<string> wp = sp1;
	cout << wp.expired() << endl;//0(flase没过期)
	cout << wp.use_count() << endl;//2 (sp1 和 sp2 持有 不增加计数)

	// sp1和sp2都指向了其他资源,则weak_ptr就过期了
	sp1 = make_shared<string>("222222");
	cout << wp.expired() << endl;//0
	cout << wp.use_count() << endl;//1(快过期了,只剩 sp2 持有)

	sp2 = make_shared<string>("333333");//sp2 放弃 "111111",管理 "333333"
	cout << wp.expired() << endl;//1(true过期了)
	cout << wp.use_count() << endl;//0

	wp = sp1;//wp 重新观察 "222222"
	//std::shared_ptr<string> sp3 = wp.lock();
	auto sp3 = wp.lock();//锁住资源,在资源释放之前生成一个新的shared_ptr
	cout << wp.expired() << endl;//0(flase没过期)
	cout << wp.use_count() << endl;//2 sp1 和 sp3 持有
	*sp3 += "###";
	cout << *sp1 << endl;
	return 0;
}

5.内存泄漏

什么是内存泄漏

内存泄漏是指程序未能释放已经不再使用的内存,一般是忘记释放或者在异常发生时释放代码未能执行导致的。

内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害

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

对于长期运行的程序,内存泄漏会导致:

  • 可用内存不断减少

  • 各种功能响应越来越慢

  • 最终卡死

避免内存泄漏方法

一、事前预防型

1. 良好的编码规范

工程前期良好的设计规范,申请的内存空间记着匹配释放。

但这是理想状态。如果遇到异常,就算注意释放了,还是可能出问题:

void func()
{
    int* p = new int(10);
    // ... 如果这里抛出异常 ...
    delete p;  // 这行永远不会执行,内存泄漏!
}

此时就需要智能指针来保证安全。

2. 使用智能指针管理资源

void func()
{
    shared_ptr<int> sp(new int(10));
    // ... 即使抛出异常 ...
    // sp 析构时自动释放资源,不会泄漏!
}

如果场景比较特殊,可以采用 RAII 思想自己造个轮子管理。


二、事后查错型

定期使用内存泄漏检测工具,尤其是每次项目上线前。

注意:有些工具不够靠谱,或者是收费的。


三、总结

内存泄漏非常常见,最佳实践是事前预防为主,事后检测为辅

 

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐