【C++】智能指针介绍

前言:上一篇说到异常会让程序跳过后面的一部分代码,这就带来了一个问题:如果后面的代码刚好是释放内存的,那这块内存该怎么办呢?智能指针就是用来帮我们管理这类资源的。不过它也不是把普通指针换一个名字就完事了,拷贝以后由谁释放资源、多个对象能不能一起管理同一块空间,这些问题都需要考虑。下面就从这些地方来看看几种智能指针的使用和实现。


1.为什么需要智能指针

先来看一个很简单的例子:

cpp 复制代码
void Func()
{
    int* p = new int[10];

    cout << "申请内存成功" << endl;

    // 如果中途发生异常
    throw "发生异常";

    // 异常发生后,这句代码不会执行找出内存泄漏
    delete[] p;
}

int main()
{
    try
    {
        Func();
    }
    catch (const char* errmsg)
    {
        cout << errmsg << endl;
    }

    return 0;
}

这里虽然写了delete[] p,但是执行到throw以后,程序就会跳到外层匹配的catch,不会再回来执行后面的释放语句。

函数退出时,局部变量p的生命周期结束了,但它只是一个保存地址的普通指针,**指针变量的销毁,不会自动释放它指向的堆内存。**所以申请出来的这块空间就没有被释放,出现了内存泄漏。

那在函数里面先捕获异常,把资源释放了,再重新抛出去不就可以了吗?

这个思路可以处理当前的问题,但资源多起来以后就比较麻烦了。比如连续申请两块空间,第一块申请成功,第二次new却抛出了异常,那么第一块仍然需要释放。正常退出、提前返回和不同位置抛异常,都要考虑哪些资源已经申请成功,哪些还没有申请,单靠在每条路径上补释放语句很容易遗漏。

我们之前学过,类对象离开作用域时会自动调用析构函数;异常向外传递、进行栈展开时,已经构造完成的局部对象也会被析构。那么可以把资源交给一个对象管理,等这个管理对象析构时,再由它完成资源的释放。

这就是RAII 的基本思路。RAII是Resource Acquisition Is Initialization的缩写,通常叫作"资源获取即初始化"。理解时可以抓住一点:把资源的管理和对象的生命周期联系起来。

  • 构造管理对象时,接管资源。
  • 对象存活期间,通过它使用资源。
  • 对象析构时,按约定释放资源。

RAII不只适用于内存,文件等需要配对获取和释放的资源也可以这样管理。后面用智能指针管理文件时,就会把释放动作换成fclose

智能指针在这个基础上还需要模拟指针的使用方式,比如重载operator*operator->,让我们仍然能通过解引用和箭头访问对象;数组形式还可以提供operator[]

所以它本质上是一个类对象,只不过内部保存了指针,并且替我们承担了相应的资源管理工作。这里还有一个很重要的问题:如果这个类对象被拷贝了,两个对象都保存同一个地址,析构时岂不是会释放两遍?不同智能指针的区别,很大一部分就在于如何处理这个问题。

2.标准库智能指针的使用

标准库智能指针放在<memory>中。下面这些例子共用的头文件是:

2.1 auto_ptr、unique_ptr与shared_ptr

先定义一个Date类,析构时打印信息,方便观察对象什么时候被释放:

cpp 复制代码
struct Date
{
    int _year;
    int _month;
    int _day;

    Date(int year = 1, int month = 1, int day = 1)
        :_year(year)
        , _month(month)
        , _day(day)
    {}

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

再把几种智能指针放在一起看:

cpp 复制代码
int main()
{
    auto_ptr<Date> ap1(new Date);
    // 构造拷贝时,管理权转移到 ap2 上,导致了 ap1 悬空
    auto_ptr<Date> ap2(ap1);
    //ap1->_year++;

    
    unique_ptr<Date> up1(new Date);
    // 不支持拷贝,但是支持移动但是移动后里面的
    // up1 会悬空所以要谨慎哦
    unique_ptr<Date> up2(move(up1));

    // 它们共同管理一块空间的资源
    shared_ptr<Date> sp1(new Date);
    shared_ptr<Date> sp2(sp1);
    // 这里可以取到这个资源被几个指针管理
    cout << sp1.use_count() << endl; 
    sp1->_year++;
    // 和上面一样同样会有悬空的问题
    shared_ptr<Date> sp3(move(sp1));


    return 0;
}

先看auto_ptr。

auto_ptr是C++98中的智能指针。它处理拷贝的方式比较特殊:不是让两个对象共同管理资源,而是把原对象的管理权转移给新对象。

执行auto_ptr<Date> ap2(ap1)以后,原来的Date对象没有被拷贝,仍然是同一个对象,只是改由ap2管理,ap1内部的指针则被置空。

这时不能再通过ap1访问原对象。如果继续执行ap1->_year++,就会变成对空指针的解引用:

这里要分清楚,原来的资源并没有被提前释放,而是ap1已经不再指向它。上面的访问错误也不是前面通过throw主动抛出的C++异常。

这种"写的是拷贝,实际却把原对象的管理权拿走了"的行为不太符合我们平常对拷贝的理解,使用时容易出问题。auto_ptr在C++11中被弃用,在C++17中被移除,现在主要用来了解早期的设计思路。上面含有它的历史示例可以放在C++11环境中观察,不能要求所有C++17环境继续支持它。版本说明可以看auto_ptr文档

再看unique_ptr。

unique_ptr强调独占所有权,同一份资源在同一时刻由一个unique_ptr负责管理。它直接禁止拷贝构造和拷贝赋值,不让"两个管理对象到底谁来释放"这个问题通过普通拷贝产生。

但是独占不代表管理权永远不能转交。C++11引入移动语义以后,可以通过移动把管理权交给另一个对象。

在上面的unique_ptr<Date> up2(move(up1))中,std::move使这里能够选择移动构造函数,真正转交资源的是移动构造函数。移动完成后,up2管理原来的Dateup1变为空。

所以移动以后仍然不能继续解引用up1。它和auto_ptr的区别在于,转交管理权需要明确使用移动,而不是一次普通拷贝就把原对象变空了。

最后看shared_ptr。

有些场景下,确实需要多个对象共同管理一份资源,这时就可以使用shared_ptr。它支持拷贝,采用引用计数来记录同一组共享所有者的数量。

sp1刚创建时,引用计数为1;用sp1拷贝构造sp2以后,两者共同管理同一个Date,引用计数变成2。因此sp1.use_count()输出2。

这里拷贝的是智能指针对象,不是重新创建一份Date。所以通过sp1->_year++修改对象后,再通过sp2访问,看到的也是修改后的结果。

shared_ptr也支持移动。执行shared_ptr<Date> sp3(move(sp1))时,sp1原来持有的那一份管理权转交给sp3sp1变为空。这次移动没有多增加一位所有者,原对象仍由sp2sp3共同管理。

等某个shared_ptr析构时,先减少一次引用计数;只有最后一位所有者离开、计数降到0时,才真正释放资源。

上面这段代码的输出为:

text 复制代码
2
~Date()
~Date()
~Date()

虽然定义了多个智能指针对象,但总共只申请了三个Date,最终这三个对象各析构一次。

还有一个weak_ptr,它不拥有被观察的对象,不会增加这里的引用计数。它主要用来配合shared_ptr,后面讲循环引用时再具体看。

2.2数组管理与定制删除器

前面的资源都是通过new Date创建的单个对象,使用默认的delete释放就可以了。但资源的释放方式不只有这一种。

new[]申请的数组应该用delete[]malloc申请的空间应该用freefopen打开的文件应该用fclose。智能指针不会根据一个普通地址自动猜出它是如何申请的,释放方式需要匹配。

数组可以使用数组形式的智能指针:

unique_ptr<Date[]>对应数组管理,默认释放时使用delete[]unique_ptr<T[]>从C++11就可以使用,而下面的shared_ptr<Date[]>

除了使用数组形式,还可以提供一个定制删除器。删除器本质上就是一个可调用对象,把资源的释放动作写在里面,智能指针需要释放资源时,再调用它。

先准备仿函数、普通函数和关闭文件的仿函数:

cpp 复制代码
template<class T>
class DeleteArray
{
public:
    void operator()(T* ptr)
    {
        delete[] ptr;
    }
};

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

class Fclose
{
public:
    void operator()(FILE* ptr)
    {
        cout << "fclose:" << ptr << endl;
        fclose(ptr);
    }
};

然后把它们交给智能指针:

cpp 复制代码
int main()
{
    // 对于 new[] 这样会报错
    /*unique_ptr<Date> up1(new Date[10]);
    shared_ptr<Date> up1(new Date[10]);*/

    // 但是库里提供了一个特化版本的,本质上
    // 是在析构时调用delete[]
    unique_ptr<Date[]> up1(new Date[5]);
    shared_ptr<Date[]> sp1(new Date[5]);

    // 也可以使用仿函数对象做删除器
    
    // unique_ptr是在类模板参数⽀持的
    unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
    // shared_ptr是构造函数参数⽀持的
    shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());

    // 一些可调用对象作为删除器

    // 函数指针做删除器
    unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
    shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);

    // lambda表达式做删除器
    auto delArrOBJ = [](Date* ptr) { delete[] ptr; };
    unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
    shared_ptr<Date> sp4(new Date[5], delArrOBJ);

    // 实现其他资源管理的删除器
    shared_ptr<FILE> sp5(fopen("Main.cpp", "r"), Fclose());
    shared_ptr<FILE> sp6(fopen("Main.cpp", "r"), [](FILE* ptr) {
        cout << "fclose:" << ptr << endl;
        fclose(ptr);
        });
    return 0;
}

几种写法虽然不同,但最终做的都是同一件事:让数组通过delete[]释放。

使用删除器时,unique_ptrshared_ptr的接口有一点区别:

  • unique_ptr把删除器类型作为类模板的第二个参数。
  • shared_ptr通过构造函数接收删除器,删除器的类型不需要写进shared_ptr<Date>这个类型名称中。

DeleteArray<Date>是一个仿函数类型,可以默认构造出对象,因此up2只传资源指针也可以使用默认构造出来的删除器。而up3用函数指针作为删除器,就同时传入具体的函数地址,也不知道为什么要设置成两个不同的方式,总感觉有些混乱的感觉

lambda表达式的类型没有一个方便直接写出来的名字,所以先用delArrOBJ保存lambda对象,再用decltype(delArrOBJ)取得类型,放到unique_ptr的第二个模板参数中:

再看最后两个文件对象,就能发现智能指针管理的并不一定是new出来的内存。sp5sp6负责管理FILE*,资源释放时分别调用仿函数和lambda中的fclose

2.3make_shared与判空

shared_ptr除了接收裸指针,还可以通过make_shared创建:

cpp 复制代码
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 << "sp1 is nullptr" << endl;

    // 不能这样,因为本质上是不同的类也没有支持它们
    // 之间的转化
    //shared_ptr<Date> sp5 = new Date(2024, 9, 11);
    //unique_ptr<Date> sp6 = new Date(2024, 9, 11);

    return 0;
}

make_shared<Date>(2024, 9, 11)会创建一个Date,并把后面的参数传给它的构造函数,最后返回管理这个对象的shared_ptr<Date>

所以这里sp1sp2sp3分别管理三个独立创建的对象。构造参数相同不代表它们共享同一份资源,只有建立了共享所有权关系,才会共用同一组引用计数。

shared_ptrunique_ptr都提供了operator bool,因此可以直接放进if判断所保存的指针是否非空。上面sp1保存着有效指针,会进入第一个分支;sp4默认构造为空,会进入if (!sp4)分支。

资源交给智能指针后,也不要再手动释放它。对于shared_ptr,共享所有权应该通过已有的shared_ptr建立,不能把同一个裸指针分别交给两个独立构造的shared_ptr,否则它们各自计数,最后可能重复释放同一个对象。

3.智能指针的模拟实现

3.1auto_ptr

最简单的资源管理方式,就是内部保存一个T*,构造时接管它,析构时释放它,再通过运算符重载模拟指针的行为:

cpp 复制代码
	template<typename T>
	class auto_ptr
	{
	public:
		// explicit 用来防止隐式类型转化
		explicit auto_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{ }

		auto_ptr(auto_ptr<T>& ap)
		{
			_ptr = ap._ptr;
			ap._ptr = nullptr;
		}

		~auto_ptr()
		{
			delete _ptr;
		}


		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			// 防止给自己赋值
			if (this != &ap)
			{
				if (_ptr)
					delete _ptr;

				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}
			return *this;
		}

		// 模拟指针行为
		T& operator*()
		{
			return *_ptr;
		}

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


	private:
		T* _ptr;

	};

这里普通构造和析构比较直接。构造函数把传进来的地址保存到_ptr,析构函数执行delete _ptrdelete空指针不会释放任何对象,因此转移管理权后,原来的智能指针也可以正常析构。

operator*返回所指对象的引用,operator->返回内部指针,这样外面就可以像使用普通指针一样访问对象。

重点是拷贝构造。它先把ap._ptr交给当前对象,再把ap._ptr置空,这就完成了管理权转移。注意参数不是const auto_ptr<T>&,因为这里需要修改被拷贝的对象。

赋值比拷贝构造多一步:赋值对象原来可能已经管理着一份资源,需要先处理旧资源,再接管新资源。

所以先判断是不是自己给自己赋值;不是的话,释放当前的_ptr,接收ap._ptr,最后把ap置空。整个过程中,同一份资源仍然只有一个负责释放它的对象。

3.2unique_ptr

unique_ptr的基础部分相同,变化主要在拷贝和移动上:

cpp 复制代码
	template<typename T>
	class unique_ptr
	{
	public:

		explicit unique_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{ }

		// 禁用左值拷贝构造
		unique_ptr(const unique_ptr<T>& up) = delete;
		unique_ptr(unique_ptr<T>&& up)
		{
			_ptr = up._ptr;
			up._ptr = nullptr;
		}

		// 禁用左值赋值重载
		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
		unique_ptr<T>& operator=(unique_ptr<T>&& up)
		{
			if (this != &up)
			{
				delete _ptr;
				_ptr = up._ptr;
				up._ptr = nullptr;
			}
			
			return *this;
		}

		~unique_ptr()
		{
			delete _ptr;
		}

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

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

	private:
		T* _ptr;
	};

这里通过= delete明确禁用了拷贝构造和拷贝赋值。注意它是告诉编译器"这个函数不能调用",和释放内存的delete _ptr不是一个意思。

移动构造接收unique_ptr<T>&&,先接管原指针,再把原对象置空。移动赋值还要考虑当前对象已有资源,所以先释放自己的旧资源,再接管对方的资源。

3.3shared_ptr

shared_ptr不能在每个对象析构时直接delete _ptr,因为别的共享所有者可能还要继续使用这份资源。它需要知道:当前到底还有几个智能指针在共同管理它。

这里的引用计数要跟着"这一份被共同管理的资源"走,而不是跟着某个独立的智能指针对象走。

如果每个对象都保存一份独立的整数,拷贝后修改的就只是各自的计数;如果使用类的静态成员,同类型的不同资源又会被混到同一个计数里。因此这里单独申请一个计数器,让同一组共享对象都保存指向它的指针。

完整的类定义如下:

cpp 复制代码
	template<typename T>
	class shared_ptr
	{
	public:

		explicit shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			,_pcount(ptr ? new int(1) : nullptr)
		{ }

		template<typename D>
		explicit shared_ptr(T* ptr, D del)
			:_ptr(ptr)
			,_pcount(ptr ? new int(1) : nullptr)
			,_del(del)
		{ }

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

		void Cheke_release()
		{
			// 计数器为0时释放资源
			if (_pcount && --(*_pcount) == 0)
			{
				_del(_ptr);
				delete _pcount;
				_ptr = nullptr;
				_pcount = nullptr;
			}
			
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (this != &sp)
			{
				
				Cheke_release();
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				if (_pcount)
					++(*_pcount);
				_del = sp._del;
			}
			return *this;
		}

		~shared_ptr()
		{
			Cheke_release();
		}

		T* Get() const
		{
			return _ptr;
		}

		int use_count() const
		{
			return _pcount ? *_pcount : 0;
		}

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

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

	private:
		T* _ptr;
		int* _pcount; // 计数器
		function<void(T*)> _del = [](T* ptr) { delete ptr; };

	};

普通构造时,如果传入的指针非空,就创建一个值为1的计数器;如果没有管理资源,_pcount也为空。use_count()根据这一点返回实际计数或0。

拷贝构造时,_ptr_pcount都从原对象中复制过来,所以两个智能指针不只指向相同资源,也指向同一个计数器。再对这个计数器加1,共享关系就建立起来了。

删除器也要一起保存。因为最后释放资源的是哪一个shared_ptr,取决于它们的生命周期,不能假定一定由最开始创建的对象负责释放。这里使用前面学过的function<void(T*)>来保存可调用对象,通过_del(_ptr)执行最终的释放动作。

接着看Cheke_release()。这个函数先减少一次引用计数,表示当前智能指针不再管理这份资源:

  • 减完以后仍然大于0,说明还有其他所有者,资源和计数器都不能释放。
  • 减完以后等于0,说明自己是最后一位所有者,先通过删除器释放资源,再释放计数器。

析构函数直接调用它,而赋值运算符要把"离开原来的共享关系"和"加入新的共享关系"连起来做。

假设当前对象原来和另外两个智能指针共享资源A,现在改成共享资源B,那么赋值时先把A的计数减1,不能直接删除A,因为另外两个对象还在使用它;然后接收B的资源地址、计数器地址和删除器,并把B的计数加1。

this != &sp处理自己给自己赋值的情况。如果是两个不同的智能指针对象,但本来就在同一组共享关系中,这份实现会先减1再加1,最终计数不变。

观察效果:

cpp 复制代码
// 测试
int main()
{
    jy::auto_ptr<Date> ap1(new Date);
    jy::auto_ptr<Date> ap2(ap1);


    jy::unique_ptr<Date> up1(new Date);
    jy::unique_ptr<Date> up3(move(up1));

    jy::shared_ptr<Date> sp1(new Date);
    jy::shared_ptr<Date> sp2(sp1);
    jy::shared_ptr<Date> sp3(sp2);
    cout << sp1.use_count() << endl;
    sp1->_year++;
    cout << sp1->_year << endl;
    cout << sp2->_year << endl;
    cout << sp3->_year << endl;

    return 0;
}

运行结果为:

text 复制代码
3
2
2
2
~Date()
~Date()
~Date()

sp1sp2sp3共同管理一个Date,所以引用计数为3。Date默认年份是1,通过sp1加1以后,再从三个智能指针访问,得到的都是2。

最后仍然只析构了三个Date对象:一个由auto_ptr管理,一个由unique_ptr管理,一个由这三个shared_ptr共同管理。共享的是资源和计数,不是每拷贝一个智能指针就多创建一个资源对象。

4.shared_ptr与weak_ptr

4.1循环引用为什么会造成内存泄漏

引用计数能解决共享资源的释放问题,但如果共享关系形成了环,就可能出现所有对象都已经不再需要,资源却没有释放的情况。

用两个链表节点就能说明这个问题。假设节点A的_next和节点B的_prev都是shared_ptr,A通过_next拥有B,B又通过_prev拥有A。

刚创建节点时,外部的n1n2分别管理一个节点,各自的引用计数都是1。把节点连起来以后,A除了被n1管理,还被B中的_prev管理;B除了被n2管理,还被A中的_next管理,因此两个计数都变成2。

n1n2离开作用域,各自的计数确实会减1,但减完以后仍然是1:

  1. A要被释放,需要B里面的_prev先放弃对它的管理。
  2. _prev是B的成员,B析构时才会跟着析构。
  3. B要被释放,又需要A里面的_next先放弃对它的管理。
  4. _next是A的成员,只有A析构时才会跟着析构。

这样就形成了循环,谁也无法成为第一个被释放的节点。这里的问题是循环强引用让计数无法归零,造成内存泄漏

既然只是想记录另一个节点的位置,能不能不把这条连接也算成一份所有权呢?这就是weak_ptr的作用。

weak_ptr可以观察shared_ptr管理的对象,但不会增加强引用计数,也不会因为自己还存在,就要求对象继续存活。

下面把节点里的连接改成weak_ptr

cpp 复制代码
struct ListNode
{
    int _data;

    //std::shared_ptr<ListNode> _next;
    //std::shared_ptr<ListNode> _prev;

    // 这里改成weak_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;

    return 0;
}

这里实际启用的两个成员都是weak_ptr,所以n1->_next = n2n2->_prev = n1只是建立观察关系,没有增加节点的强引用计数。

对应的输出是:

text 复制代码
1
1
1
1
~ListNode()
~ListNode()

前后四次计数都是1,最后两个节点也正常析构了。对于这个例子,节点生命周期由外部的n1n2负责,节点间的两个成员只负责记录连接关系,不负责延长对方的生命周期。

4.2weak_ptr如何访问资源

weak_ptr不保证被观察的对象一直活着,因此它没有提供像shared_ptr那样直接访问对象的operator*operator->。如果直接保存一个地址就去解引用,那么真正的所有者已经释放资源时,就有可能访问到失效内存。

标准库的weak_ptr主要通过下面三个接口配合使用:

接口 作用
use_count() 返回所观察对象的强引用计数,不是weak_ptr自身的数量
expired() 判断所观察的对象是否已经没有共享所有者
lock() 对象仍存活时,取得一个共享所有权的shared_ptr;已经过期时,返回空shared_ptr

具体行为可以对照weak_ptr接口文档。真正访问对象时,先取得lock()返回的shared_ptr,再判断它是否为空。

cpp 复制代码
int main()
{
    shared_ptr<string> sp = make_shared<string>("hello");

    // weak_ptr观察sp管理的资源,不增加引用计数
    weak_ptr<string> wp = sp;

    cout << "引用计数:" << wp.use_count() << endl;
    cout << "是否过期:" << wp.expired() << endl;

    // 通过lock获取shared_ptr,安全访问资源
    shared_ptr<string> tmp = wp.lock();

    if (tmp)
    {
        cout << *tmp << endl;
    }

    // 释放所有shared_ptr
    sp.reset();
    tmp.reset();

    cout << "是否过期:" << wp.expired() << endl;

    return 0;
}

开始时只有sp拥有这个字符串对象,因此引用计数为1。wp绑定它以后没有增加计数,此时对象仍然存在,expired()返回false,输出为0。

随后wp.lock()成功,返回一个与sp共享所有权的shared_ptr,保存在tmp中。这时才新增了一份共享所有权,强引用计数变成2。通过tmp就可以访问字符串,输出hello

注意lock()不会把wp本身变成一个shared_ptr,它返回的是另一个对象,wp仍然只是观察者。

后面先执行sp.reset()sp放弃所有权,但是tmp还在,因此字符串还不会销毁。再执行tmp.reset(),最后一个共享所有者也放弃了所有权,字符串对象才被销毁。

此时wp虽然还在,但它不能把字符串留住,expired()返回true。完整输出为:

text 复制代码
引用计数:1
是否过期:0
hello
是否过期:1

写一个非常简化的版本:

cpp 复制代码
	template<typename T>
	class weak_ptr
	{
	public:
		weak_ptr()
		{ }

		weak_ptr(shared_ptr<T>& ptr)
			:_ptr(ptr.Get())
		{ }

		weak_ptr<T>& operator=(shared_ptr<T>& ptr)
		{
			_ptr = ptr.Get();
			return *this;
		}

	private:
		T* _ptr;

	};

构造和赋值时,它通过Get()取得地址并保存下来,没有增加计数,也没有负责释放目标资源的析构逻辑。

实际上想要实现一个完整可用的weak_ptr比较复杂前面的两个指针也要大改,我这里就只是像体现一下这个指针的思想

5.智能指针的发展与内存泄漏

5.1C++11与Boost中的智能指针

智能指针并不是到C++11才突然出现的。C++98就有了auto_ptr,而在C++11之前,Boost也提供了多种智能指针,用来处理不同的资源管理需求。

Boost可以理解成一组为C++提供扩展功能的库,其中一些成熟设计也为标准库的发展提供了参考。智能指针这部分的关系大致是:

  • C++98提供auto_ptr,通过拷贝转移管理权。
  • Boost提供了scoped_ptrscoped_arrayshared_ptrshared_arrayweak_ptr等类型。
  • shared_ptrweak_ptr经过TR1这一库技术报告阶段,后来进入C++11标准。TR1不是一个名叫"C++TR1"的语言标准版本。
  • C++11标准库中有了unique_ptrshared_ptrweak_ptr,独占、共享和非拥有观察几种需求有了相应的工具。

unique_ptrboost::scoped_ptr都可以用于独占管理,但两者不能简单理解成完全相同的类。scoped_ptr强调在当前作用域内管理资源,不提供所有权转移语义;unique_ptr则支持通过移动转移所有权。相关背景可以看Boost.SmartPtr的介绍与历史

5.2内存泄漏的危害、检测与避免

内存泄漏不是内存在物理上消失了,而是程序申请的内存已经不再需要,却没有按预期释放,继续占着资源。原因可能是忘记释放,也可能是异常跳过释放语句,或者像前面的循环引用一样,资源管理关系出了问题。

普通的小程序运行一会儿就退出,进程结束后,操作系统通常会回收它占用的进程内存,所以问题有时不容易被注意到。但是长期运行的程序不能依靠"等退出再回收":每次泄漏一点,累积起来就会占用越来越多的资源,影响运行,严重时会导致内存分配失败。

检测内存泄漏需要借助工具。例如Linux环境中可以使用Valgrind的Memcheck,Windows下也有Visual Leak Detector这类工具。工具可以帮助定位未释放内存及相关分配位置,使用时还要确认它与编译器、运行环境的兼容情况。

不过检测是发现问题,设计和编码时仍然要尽量避免产生问题,想要达到这点就多练习吧


相关推荐
Lionel_Coder1 小时前
matmul 总体设计
c++
hanchenxing1 小时前
本地AI绘画网关: 用 30 行 Python 把 NVIDIA FLUX.2 Klein 接入 OpenAI 兼容客户端Python
开发语言·python·ai作画·ai绘画·nvidia
Dream Cosmos1 小时前
C++ 中 static 关键字的作用与使用规则
linux·c++
PHP实战开发录3 小时前
PHP脚本手动能跑定时任务却失败
开发语言·php·开发
蒸蒸yyyyzwd4 小时前
cpp 选手秋招学习笔记 day33
c++·笔记·求职招聘
郝学胜-神的一滴4 小时前
Effective Python 条款 10 :海象运算符_=
开发语言·python·程序人生·开源
wuyk5554 小时前
Python零基础入门第十四章:异常处理(try-except)
开发语言·python
wuyk5554 小时前
【Socket 进阶之路】第 3 章 TCP 三次握手 & 四次挥手深度剖析|连接建立、断开、状态机、TIME_WAIT 核心工程问题
服务器·开发语言·网络·物联网·网络协议·tcp/ip
彧azz10 小时前
图的存储结构详解:邻接矩阵的原理、实现与应用
开发语言·数据结构·学习·php