C++11--智能指针

引入

为什么需要智能指针?

在介绍异常时,遇到以下场景,处理异常就会比较棘手:

cpp 复制代码
void Func()
{
	int* arr1 = new int[10];
	int* arr2 = new int[20];
	int* arr3 = new int[30];
	
	// ...
	delete[] arr1;
	delete[] arr2;
	delete[] arr3;
}

这里 arr1 arr2 arr3 在使用 new 申请空间时可能会抛出异常(当空间不足时),这样就有四种情况:

  • arr1 抛出异常,不需要释放空间。
  • arr2 抛出异常,需要释放 arr1 申请的空间。
  • arr3 抛出异常,需要释放 arr1arr2 申请的空间。
  • 没有异常抛出,需要释放 arr1arr2arr3 申请的空间。

那我们在处理异常时,就需要像下面类似写法,才能保证异常安全:

cpp 复制代码
void Func()
{
	int* arr1 = new int[10];
	int* arr2;
	int* arr3;
	try
	{
		arr2 = new int[20];
		try
		{
			arr3 = new int[30];
		}
		catch(...)
		{
			delete[] arr1;
			delete[] arr2;
			throw;
		}
	}
	catch(...)
	{
		delete[] arr1;
		throw;
	}

	// 没有异常
	delete[] arr1;
	delete[] arr2;
	delete[] arr3;
}

我们发现在这种场景下,要保证不产生内存泄漏,程序员在编写代码时要格外注意。

内存泄露

内存泄漏及其危害

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

内存泄露的危害: 对于短时间运行的程序来说,内存泄漏并不会造成较大的危害,因为程序结束运行后,会自动回收申请的资源。但是,对于长期运行的程序出现内存泄漏,影响非常大(比如操作系统,后台服务等等),因为出现内存泄漏会导致响应越来越慢,最终卡死。

内存泄露的分类

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

  • 堆内存泄漏(Heap leak): 堆内存是指程序执行中通过malloc/calloc/realloc/new等从堆中分配的一块内存,该内存使用完后必须通过调用对应的free/delete释放该内存。假设程序因设计错误导致这部分内存没有被释放,那么这部分空间后续无法再被使用(程序运行时),就会产生Heap leak
  • 系统资源泄漏: 是指程序使用完系统分配的资源(如套接字、文件描述符、管道等等),没有使用对应的函数释放、归还给操作系统,导致系统资源的浪费,严重可导致系统能效减少,系统执行不稳定等后果。

经典场景

  1. 对象创建后却没有释放。
  2. 智能指针的循环引用,两者相互持有,导致引用计数永不为0,内存无法释放。
  3. 集合类容器中,删除元素后未释放内存。
  4. 在外面手动申请的内存,但进入了异常处理,手动分配的内存未释放。
  5. 静态成员或全局变量持有动态分配的对象。

如何避免内存泄漏

  1. 工程前期要有良好的设计规范,养成良好的编码规范。对于申请的内存空间,要调用匹配的函数去释放。
  2. 采用RAII思想或者智能指针来管理资源。
  3. 有些公司规定使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
  4. 使用内存检测工具。当程序出现问题后,可以使用一些工具检查。(ps:不过很多工具都不太靠谱,或者收费昂贵。)

总结: 内存泄漏的解决方案通常有两种:一是"事先预防"型(如编程规范和智能指针等);二是"事后查错"型(如使用泄漏检测工具等)

补充: 内存泄漏检测工具

智能指针的使用及原理

RAII

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

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

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命周期内始终保持有效。
  • 异常安全: 即使程序在执行过程中抛出异常或多路径返回时,也能确保资源最终得到正确释放,特别是可以避免内存泄漏。
  • 简化资源管理: 将资源的获取和释放逻辑封装在类内,使代码更加简洁且方便维护。

示例

cpp 复制代码
// SmartPtr.h
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		delete _ptr;
		std::cout << "delete: " << _ptr << std::endl;
	}
private:
	T* _ptr;
};
cpp 复制代码
// SmartPtr.cpp
#include <iostream>
#include <exception>
#include "SmartPtr.h"

double Division(int a, int b)
{
	if (b == 0)
	{
		throw std::invalid_argument("除0错误");
	}

	return (double)a / (double)b;
}

void func()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(new int);
	std::cout << Division(1, 0) << std::endl;
}

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

运行结果:

cpp 复制代码
delete: 0000022DDC2C7160
delete: 0000022DDC2C7520
除0错误

这里就算出现异常,并且我们没有手动释放资源,该资源也可以被正确地释放。原因是: 在触发异常后,该异常会被捕获,就会跳转到catch块中,对象sp1sp2的生命周期就结束了,就会调用该对象的析构函数,在析构函数中该对象管理的资源就会被释放。

注意: 这里RAII是一种思想,可以将其应用到其他的资源管理上,并不只是内存。下面给出利用RAII思想管理文件资源:

cpp 复制代码
#include <iostream>
#include <fstream>

class FileHandler 
{
public:
    FileHandler(const std::string& filename) : file(filename) 
    { // 资源获取
        if (!file.is_open()) 
        {
            throw std::runtime_error("Unable to open file");
        }
    }

    ~FileHandler() 
    {
        file.close(); // 资源释放
    }

    void write(const std::string& data) 
    {
        if (file.is_open()) 
        {
            file << data << std::endl;
        }
    }

private:
    std::ofstream file;
};

int main() 
{
    try 
    {
        FileHandler fh("example.txt");
        fh.write("Hello, RAII!");
    }
    catch (const std::exception& e) 
    {
        std::cerr << e.what() << std::endl;
    }
    return 0;
}

应用场景:

  1. 内存管理: 标准库中的std::unique_ptrstd::shared_ptrRAII 的经典实现,用于智能管理动态内存。
  2. 文件管理: std::fstream类在打开文件时获取资源,在析构函数中关闭文件。
  3. 互斥锁: std::lock_guardstd::unique_lock 用于多线程编程中自动管理互斥锁的锁定和释放。

智能指针

智能指针通过使用RAII原则来实现资源的自动管理,即在对象的生命周期开始时获取资源,在对象生命周期结束时释放资源。之所以"指针",是因为它具有指针的行为:比如可以解引用 (*) 或者通过箭头操作符 (->) 访问所指向的对象成员。

所以以下介绍的智能指针,其底层一定重载了这两个操作符(先以上述例子中的SmartPtr类为例,展示一下大致如何进行重载)。

为什么要重载操作符*->

智能指针的目的是提供一种自动管理资源(通常是动态分配的内存)的机制,同时尽可能保持与原生指针相似的接口。这样做的好处是,开发者可以在不改变现有代码风格的情况下,轻松地将原生指针替换为智能指针,从而获得更好的资源管理和异常安全性。

  1. 重载*操作符
    • 当你有一个指向内置类型 对象的指针时,你通常会使用解引用操作符*获取该对象本身的值
    • 智能指针需要模拟这种行为,以便当你通过智能指针访问所管理的对象时,能够得到该对象的值。
    • 因此,智能指针需要重载*操作符,以便在解引用智能指针时返回所管理对象的值。
cpp 复制代码
T& operator*() const
{
	return *_ptr;
}

int main()
{
	SmartPtr<int> sp1(new int(1));
	SmartPtr<int> sp2(new int(2));
	*sp1 += 10;
	
	return 0;
}

SmartPtr<int> 意味着 SmartPtr 类模板的一个实例,它管理的是 int 类型的对象。一个指针如果指向一个内置类型数据,可以通过解引用*访问到这个内置类型数据,所以智能指针类内部要重载操作符*,并且返回该内置类型数据(通过解引用类成员变量_ptr)。

  1. 重载->操作符
    • 当你有一个指向用户定义类型 对象的指针时,你还可以使用箭头操作符->访问该对象的成员
    • 智能指针同样需要模拟这种行为,以便当你通过智能指针访问所管理对象的成员时,能够得到正确的结果。
    • 因此,智能指针需要重载->操作符,以便在通过智能指针访问成员时返回所管理对象的成员。

这里以std::pair<std::string, int> 类型为例:

cpp 复制代码
T* operator->() const
{
	return _ptr;
}

int main()
{
	SmartPtr<std::pair<std::string, int>> sp3(new std::pair<std::string, int>("10", 10));
	std::cout << sp3->first << std::endl;
	return 0;
}

SmartPtr<std::pair<std::string, int>> 意味着 SmartPtr 类模板的一个实例,它管理的是 std::pair<std::string, int> 类型的对象。一个指针如果指向一个用户定义类型数据,可以通过操作符->访问到这个用户定义类型对象内部的数据,所以智能指针类内部要重载操作符->,这样我们就可以通过智能指针对象sp3直接访问到std::pair内部的std::stringint数据了。

如何重载操作符*->

在C++中,重载操作符是通过定义特殊形式的成员函数来实现的。对于智能指针来说,这些成员函数通常被定义为const成员函数,因为它们不会修改智能指针的状态。

  1. 重载*操作符
    • 返回值类型应该是所管理对象的引用(如果智能指针是非const的)或所管理对象的const引用(如果智能指针是const的)。
    • 函数体内部应该返回对所管理对象的解引用结果。
cpp 复制代码
T& operator*() const
{
	return *_ptr;
}
  1. 重载->操作符
    • 返回值类型应该是所管理对象的指针类型
    • 函数体内部应该返回存储的内部指针(即指向所管理对象的指针)。

在C++中,->操作符的重载返回值通常是一个指向被管理对象的指针。这里被管理对象是std::pair<std::string, int>,所以要返回该对象的指针,也就是_ptr

关于->操作符重载的进一步说明

注意: 要访问到用户定义类型对象的内部数据,直接使用操作符->是不行的,还需要编译器做进一步优化,才能达到预期的使用效果:对象sp3通过->操作符返回其成员对象(std::pair* ),但是我们要访问的是std::stringint数据,因此还需要使用一个->操作符,才能访问到 std::pair*firstsecond,但是写两个->不方便(sp3.operator->()->first)。因此,编译器做了优化,只用写一个->即可访问到firstsecond

也就是: 当您重载->操作符时,返回的是一个指向被管理对象的指针。这意味着您可以连续使用->来访问对象的成员,如sp3->first。这里有一个重要的点:由于->操作符的返回值是一个指针,因此它可以继续被->操作符所使用,这允许我们链式访问成员。

例如,如果_ptr是一个指向std::pair<std::string, int>的指针,那么_ptr->first就是访问std::pair对象的first成员。当您重载->并返回_ptr时,sp3->first实际上等价于(_ptr)->first,编译器会自动处理这种嵌套访问。


补充: 迭代器中关于操作符->的重载

上面我们说,->操作符的重载返回值通常是一个指向被管理对象的指针。

  • 对于SmartPtr来说,其管理的对象为data,因此要返回其指针&data,而_ptr就是指向data的指针,所以返回_ptr
  • 对于迭代器来说,其管理的对象为_node->data,因此要返回其指针&_node->data

智能指针的拷贝问题

智能指针模拟的是原生指针的行为,指针在进行赋值时,两个指针会指向同一个数据。所以对于智能指针来说,当发生拷贝构造或赋值操作时,不进行深拷贝,拷贝与被拷贝的智能指针对象管理同一份资源(资源不是自己的,而是代为持有)。

cpp 复制代码
int main()
{
	SmartPtr<int> sp1(new int(1));
	SmartPtr<int> sp2(sp1); // 拷贝构造
	SmartPtr<int> sp3(new int(3));
	sp3 = sp1; // 赋值
	return 0;
}

因此关于,这三个智能指针对象应该管理同一份资源,也就是1所在的堆空间。

关于这个拷贝问题,标准库中的智能指针实现的策略是不同的,下面我们来详细看看( •̀ ω •́ )✧(标准库实现的智能指针在memory头文件中,参考文档

智能指针的介绍

先来介绍一下独占所有权的模型。

使用该模型的智能指针,意味着在一个时刻只有一个实例拥有对该对象的所有权。当实例被销毁或者被移动时,它会自动释放所管理的对象。

auto_ptr(C++98)

在 C++98 中,std::auto_ptr 是一种智能指针,它旨在自动管理动态分配的对象的生命周期。std::auto_ptr 提供了一种独占所有权的模型。

一、auto_ptr的基本特性

  • 独占所有权模型auto_ptr采用独占所有权模型,即任何时候只能有一个auto_ptr实例管理某个资源(动态分配的对象)。
  • 自动释放资源 :当auto_ptr实例被销毁时,它会自动释放所管理的资源。

二、管理权转移

  • 拷贝构造函数与赋值运算符 :在C++98中,auto_ptr的拷贝构造函数和赋值运算符会将管理权从被拷贝的auto_ptr实例转移到拷贝后的实例。这意味着,被拷贝的auto_ptr将不再管理该资源,而是将其所有权转移给新的auto_ptr实例。
  • 管理权转移的后果 :由于管理权的转移,被拷贝的auto_ptr实例将变为空(即其内部指针将被设置为nullptr),而新的auto_ptr实例将接管该资源的管理权。这可能导致一些意外的行为,特别是当程序员不了解这种管理权转移的特性时。

三、auto_ptr的缺陷

  • 潜在的悬空指针问题 :由于管理权的转移,被拷贝的auto_ptr实例在拷贝后可能变为悬空指针(即指向已被释放的内存)。这可能导致未定义的行为,因为悬空指针无法安全地解引用。
  • 不安全的拷贝语义auto_ptr的拷贝语义可能导致资源被意外地释放。

四、使用及原理

cpp 复制代码
#include <memory>

int main()
{
	std::auto_ptr<int> sp1(new int(1));
	std::auto_ptr<int> sp2(sp1);
	return 0;
}

管理权转移:

此时 sp1 的管理权就转移给 sp2 对象,此时就不能再通过 sp1 访问其原先管理的资源。


底层类似于:

cpp 复制代码
template<class T>
class auto_ptr
{
public:
	auto_ptr(T* ptr)
		:_ptr(ptr)
	{}

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

	~auto_ptr()
	{
		delete _ptr;
		std::cout << "delete: " << _ptr << std::endl;
	}
private:
	T* _ptr;
};

五、auto_ptr的替代方案

  • unique_ptr :在C++11及更高版本中,引入了unique_ptr作为auto_ptr的替代方案。unique_ptr同样采用独占所有权模型,但它不允许拷贝构造函数和赋值运算符,从而避免了管理权转移的问题。相反,unique_ptr支持移动语义,允许通过移动构造函数和移动赋值运算符来转移资源的所有权。
  • shared_ptr :另一个替代方案是shared_ptr,它采用共享所有权模型。shared_ptr使用引用计数来跟踪有多少个shared_ptr实例共享同一个资源。当最后一个shared_ptr实例被销毁时,资源才会被释放。这避免了auto_ptr中的管理权转移问题和潜在的悬空指针问题。

综上所述,C++98中的auto_ptr虽然提供了一种自动管理动态分配内存的机制,但其设计存在缺陷,特别是在管理权转移方面。因此,在C++11及更高版本中,建议使用unique_ptrshared_ptr作为替代方案来更安全地管理动态分配的内存。

unique_ptr(C++11)

unique_ptr同样采用独占所有权模型,但它不允许拷贝构造函数和赋值运算符 ,从而避免了管理权转移的问题。相反,unique_ptr支持移动语义,允许通过移动构造函数和移动赋值运算符来转移资源的所有权。(适用于不需要拷贝的场景。)

cpp 复制代码
template<class T>
class unique_ptr
{
public:
	unique_ptr(T* ptr)
		:_ptr(ptr)
	{}

	unique_ptr(const unique_ptr<T>& ap) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>& ap) = delete;

	~unique_ptr()
	{
		delete _ptr;
		std::cout << "delete: " << _ptr << std::endl;
	}
private:
	T* _ptr;
};

为什么std::unique_ptr可以做到不可复制,只可移动?

不可复制 ,因为把拷贝构造函数和赋值运算符标记为了delete,见源码:

cpp 复制代码
template <typename _Tp, typename _Tp_Deleter = default_delete<_Tp> > 
class unique_ptr {
	// Disable copy from lvalue.
	unique_ptr(const unique_ptr&) = delete;
	
	template<typename _Up, typename _Up_Deleter> 
	unique_ptr(const unique_ptr<_Up, _Up_Deleter>&) = delete;
 
	unique_ptr& operator=(const unique_ptr&) = delete;
    
    template<typename _Up, typename _Up_Deleter> 
    unique_ptr& operator=(const unique_ptr<_Up, _Up_Deleter>&) = delete;
};

只可移动:

在C++中,"只可移动"(movable-only)意味着一个对象不能被复制(即不能使用拷贝构造函数或拷贝赋值运算符进行复制),但是它可以被移动(即可以使用移动构造函数或移动赋值运算符进行移动)。移动操作通常涉及将资源(如动态分配的内存、文件句柄等)的所有权从一个对象转移到另一个对象,而不是像复制操作那样创建资源的副本。

对于std::unique_ptr来说,它代表对某个对象的唯一所有权。这意味着在任意时刻,只有一个std::unique_ptr实例可以拥有某个资源(例如一个动态分配的对象)。因此,如果允许std::unique_ptr被复制,那么就会违反它的唯一所有权原则,因为复制后的两个std::unique_ptr实例都会声称拥有同一个资源,这会导致资源管理上的混乱和潜在的错误(如双重释放)。

为了避免这种情况,std::unique_ptr的拷贝构造函数和拷贝赋值运算符被删除(= delete),从而禁止了复制操作。然而,在某些情况下,我们仍然需要将std::unique_ptr的所有权从一个实例转移到另一个实例(例如,将资源从一个函数返回给调用者,或者将资源从一个容器移动到另一个容器)。这就是移动操作的目的。

移动操作不会创建资源的副本,而是将资源的所有权从一个std::unique_ptr实例转移到另一个实例。这通常是通过将源std::unique_ptr的内部指针(指向资源的指针)设置为nullptr(表示放弃所有权),并将该指针的值赋给目标std::unique_ptr来实现的。这样,目标std::unique_ptr就获得了资源的唯一所有权,而源std::unique_ptr则不再拥有任何资源。

如何移动?

std::unique_ptr做到移动的关键在于其移动构造函数和移动赋值运算符的实现。这些移动操作不会创建资源(如动态分配的内存)的副本,而是将资源的所有权从一个std::unique_ptr实例转移到另一个实例。

  1. 移动构造函数

    当使用移动构造函数创建一个新的std::unique_ptr实例时,源std::unique_ptr(即要移动的实例)会将其内部指针(指向所管理资源的指针)的值赋给新创建的std::unique_ptr实例。然后,源std::unique_ptr会将其内部指针设置为nullptr,表示它不再拥有该资源。

  2. 移动赋值运算符

    类似地,当使用移动赋值运算符将一个std::unique_ptr实例的值赋给另一个std::unique_ptr实例时,源std::unique_ptr会将其内部指针的值赋给目标std::unique_ptr,并将自己的内部指针设置为nullptr。这样,目标std::unique_ptr就获得了资源的所有权,而源std::unique_ptr则放弃了所有权。

由于移动操作不会创建资源的副本,因此它们通常比复制操作更高效。在移动之后,源std::unique_ptr不再拥有任何资源,因此可以安全地被销毁或用于其他目的(但需要注意的是,此时它不再指向任何有效的资源)。

std::unique_ptr的移动构造函数和移动赋值运算符是由标准库自动提供的,你不需要手动实现它们。当你使用C++11或更高版本的编译器时,这些移动操作会自动启用,允许你以高效的方式转移std::unique_ptr的所有权。

例如,以下是一个简单的使用std::unique_ptr移动操作的示例:

cpp 复制代码
#include <memory>
#include <iostream>

int main() {
    std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
    std::cout << "ptr1: " << *ptr1 << std::endl; // 输出: ptr1: 42

    // 使用移动构造函数创建ptr2
    std::unique_ptr<int> ptr2 = std::move(ptr1);
    // 此时ptr1不再拥有资源,ptr2拥有资源
    std::cout << "ptr1 (after move): " << (ptr1 ? *ptr1 : "nullptr") << std::endl; // 输出: ptr1 (after move): nullptr
    std::cout << "ptr2: " << *ptr2 << std::endl; // 输出: ptr2: 42

    // 使用移动赋值运算符将ptr2的值赋给ptr3
    std::unique_ptr<int> ptr3;
    ptr3 = std::move(ptr2);
    // 此时ptr2不再拥有资源,ptr3拥有资源
    std::cout << "ptr2 (after move assignment): " << (ptr2 ? *ptr2 : "nullptr") << std::endl; // 输出: ptr2 (after move assignment): nullptr
    std::cout << "ptr3: " << *ptr3 << std::endl; // 输出: ptr3: 42

    return 0;
}

在这个示例中,std::move函数用于将std::unique_ptr实例转换为右值引用,从而启用移动构造函数或移动赋值运算符。然后,资源的所有权被从源std::unique_ptr转移到目标std::unique_ptr

shared_ptr(C++11)

std::shared_ptr 是一种共享所有权 的智能指针,多个shared_ptr可以指向同一个对象。内部使用引用计数来确保只有当最后一个指向对象的shared_ptr被销毁时,对象才会被销毁。

使用场景:

  • 当你需要多个所有者之间共享对象时。
  • 当你需要通过复制构造函数或赋值操作符来复制智能指针时。

shared_ptr 的使用

shared_ptr 定义在头文件 <memory> 中,可以通过 std::shared_ptr 来使用。以下是一些基本的用法示例:

cpp 复制代码
#include <iostream>
#include <memory>

int main() {
    // 创建一个 shared_ptr 管理一个动态分配的 int
    std::shared_ptr<int> ptr1 = std::shared_ptr<int>(new int(10));

    // 输出 ptr1 管理的值
    std::cout << "ptr1: " << *ptr1 << std::endl;

    // 复制 ptr1,生成一个新的 shared_ptr
    std::shared_ptr<int> ptr2 = ptr1;

    // 输出 ptr2 管理的值
    std::cout << "ptr2: " << *ptr2 << std::endl;

    // 此时 ptr1 和 ptr2 指向同一个对象,它们的引用计数为 2
    std::cout << "ptr1 use_count: " << ptr1.use_count() << std::endl;
    std::cout << "ptr2 use_count: " << ptr2.use_count() << std::endl;

    // 当 ptr1 和 ptr2 超出作用域时,它们所管理的对象会被自动释放
    return 0;
}

注意: 标准库中的shared_ptr中的构造函数,不允许隐式类型转换(使用explicit),只能直接构造。

cpp 复制代码
template <class U> explicit shared_ptr (U* p);

也就是说:

cpp 复制代码
std::shared_ptr<int> ptr1 = std::shared_ptr<int>(new int(10)); // 只能向这样使用
std::shared_ptr<int> ptr1 = std::shared_ptr<int> = new int(10); // 不能这样使用

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

引用计数

shared_ptr内部,给每个资源都维护了一个计数,用来记录该份资源被几个对象共享。

  1. 创建 :当一个新的 shared_ptr 实例被创建时(例如通过 std::make_shared 或复制另一个 shared_ptr),它会指向一个已存在的控制块(如果适用),并增加控制块中的引用计数。
  2. 复制 :当一个 shared_ptr 被复制时(例如 std::shared_ptr<int> ptr2 = ptr1;),新的 shared_ptr 实例会指向相同的控制块,并增加引用计数。
  3. 赋值 :当一个 shared_ptr 被赋值给另一个 shared_ptr 时(例如 ptr1 = ptr2;),如果它们原本指向不同的对象,则赋值操作会导致:
    • 目标 shared_ptr 释放其当前管理的对象(如果引用计数为 0,则删除对象)。
    • 目标 shared_ptr 指向源 shared_ptr 的控制块,并增加引用计数。
  4. 销毁 :当一个 shared_ptr 实例被销毁(例如超出作用域)时,它会减少控制块中的引用计数。如果引用计数变为 0,则控制块中的自定义删除器(默认为 delete)会被调用,以释放实际的对象。

这种机制确保了动态分配的对象在不再被任何 shared_ptr 管理时会被自动释放,从而避免了内存泄漏。


设计

引用计数的设计:一份资源有一个引用计数

  1. 不能使用普通成员变量,否则一个对象拥有一个count。
  2. 不能使用静态成员变量,否则所有对象共有一个count。

补充: 静态成员变量不能在类内初始化。带模板类型的静态成员在类外初始化时,需要声明模板参数 template

cpp 复制代码
template<class T>
int shared_ptr<T>::_count = 0;

解决方案: 一个智能指针对象调用构造函数时,也是在申请资源,所以我们可以在类内定义一个指向count的指针(_pcount)。

  • 当调用构造函数 构造一个对象时,就在堆空间中分配count大小的空间,用来存储count并用_pcount 指向该空间;
  • 当调用拷贝构造函数 构造一个对象时,不分配count的空间,而将拷贝对象中的_pcount指向被拷贝对象的_pcount,然后通过这个指针操作count(进行++操作)。

这样共享同一份资源的对象使用同一个count,其原理类似于:

cpp 复制代码
template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		:_ptr(ptr), _pcount(new int(1))
	{}

	shared_ptr(const shared_ptr<T>& sp)
	{
		_ptr = sp._ptr;
		_pcount = sp._pcount;
		++(*_pcount);
	}
	~shared_ptr()
	{
		if (--(*_pcount) == 0)
		{
			delete _ptr;
			std::cout << "delete: " << _ptr << std::endl;
		}
	}

	int use_count()
	{
		return *_pcount;
	}

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

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

private:
	int* _pcount;
	T* _ptr;
};

上述介绍的是,调用拷贝构造函数构造出来对象和被拷贝对象共享资源的情况。赋值操作时,也要更改资源的引用计数,因此还需进行赋值操作符的重载,下面将详细介绍╰(‵□′)╯

赋值重载

1. 赋值重载的工作原理

shared_ptr 的赋值重载中,主要任务是转移资源的所有权。这涉及到对引用计数的正确管理,以确保资源在不再被任何 shared_ptr 实例引用时能够被自动释放。

  • 减少被赋值对象的引用计数 :首先,如果当前 shared_ptr 实例(即被赋值对象)已经管理了一个资源,那么需要减少该资源的引用计数。如果减少后引用计数变为零,则释放该资源及其控制块(包括引用计数)。

  • 增加赋值对象的引用计数 :然后,将新资源(即赋值对象所管理的资源)的控制块中的引用计数增加。这通常意味着将赋值对象所指向的控制块的引用计数指针复制到被赋值对象中,并增加该指针所指向的引用计数。

  • 处理特殊情况:在增加引用计数之前,需要处理一些特殊情况,以避免资源泄漏或双重释放。

2. 需要注意的特殊情况
  • 自我赋值 :如果赋值对象和被赋值对象是同一个 shared_ptr 实例(即它们管理相同的资源),则赋值操作不应该改变资源的引用计数或释放资源。为了避免这种情况,可以在赋值之前检查两个 shared_ptr 实例是否相等(即它们是否管理相同的资源)。如果相等,则不进行任何操作。

  • 管理同一份资源的对象相互赋值 :如果两个不同的 shared_ptr 实例管理相同的资源(即它们指向相同的控制块),则赋值操作应该只更新被赋值对象的内部指针,而不改变资源的引用计数。这通常意味着将赋值对象的控制块指针复制到被赋值对象中,但不需要增加或减少引用计数。然而,由于 shared_ptr 通常通过引用计数指针来间接访问控制块,因此在实际实现中,这种赋值可能只涉及指针的复制,而不需要额外的逻辑来处理引用计数。

  • 区分管理不同资源的对象 :为了确保赋值操作的正确性,shared_ptr 的赋值重载应该能够区分管理不同资源的对象和管理相同资源的对象。这通常通过比较控制块指针(或更具体地,比较控制块中的资源指针 )来实现。如果两个 shared_ptr 实例的控制块指针不同,则它们管理不同的资源,需要进行正常的赋值操作(即减少被赋值对象的引用计数,增加赋值对象的引用计数)。

类似实现细节:

cpp 复制代码
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
	if (_ptr != sp._ptr)
	{
		release(); // --之前的引用计数

		_ptr = sp._ptr;
		_pcount = sp._pcount;
		++(*_pcount);
	}
			
	return *this;
}

void release()
{
	if (--(*_pcount) == 0)
	{
		delete _ptr;
		delete _pcount;
		std::cout << "delete: " << _ptr << std::endl;
	}
}

缺点:循环引用

引入:

如果智能指针的模板参数为ListNode,也就是管理的资源为ListNode,在进行链接时,可能会有些问题:

cpp 复制代码
struct ListNode
{
	int _val;
	struct ListNode* _next;
	struct ListNode* _prev;

	ListNode(int val = 0)
		:_val(val), _next(nullptr), _prev(nullptr)
	{}

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

int main()
{
	std::shared_ptr<ListNode> n1(new ListNode(1));
	std::shared_ptr<ListNode> n2(new ListNode(2));

	n1->_next = n2;
	return 0;
}

n1->_next = n2; 此时,这里不能直接像链表那样进行链接,因为_next的类型为struct ListNode* 只能指向类型为struct ListNode的对象,而n2的类型为std::shared_ptr<ListNode>,类型不兼容,所以不能这样使用。

解决方案: 只能将_next_prev的类型改变为std::shared_ptr<ListNode>

cpp 复制代码
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;

如下场景中,n1、n2代表智能指针编号;s1、s2代表智能指针对象管理的资源(source);c1、c2代表资源的引用计数(count)。

  • 场景一: n1->_next = n2;
cpp 复制代码
struct ListNode
{
	int _val;
	std::shared_ptr<ListNode> _next;
	std::shared_ptr<ListNode> _prev;

	ListNode(int val = 0)
		:_val(val)
	{}

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

int main()
{
	std::shared_ptr<ListNode> n1(new ListNode(1));
	std::shared_ptr<ListNode> n2(new ListNode(2));

	n1->_next = n2;
	return 0;
}

运行结果:

cpp 复制代码
~ListNode()
~ListNode()

分析:

  1. n2对象先进行析构,c2--c2 = 1,引用计数不为0,不能释放s2资源;
  2. n1对象再进行析构,c1--c1 = 0,引用计数为0,释放s1资源,_next被释放;
  3. _next被释放,c2--c2 = 0,引用计数为0,释放s2资源。
  4. s1、s2资源均被正常释放(从打印信息可以看出)。
  • 场景二: n2->_prev = n1;
cpp 复制代码
struct ListNode
{
	int _val;
	std::shared_ptr<ListNode> _next;
	std::shared_ptr<ListNode> _prev;

	ListNode(int val = 0)
		:_val(val)
	{}

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

int main()
{
	std::shared_ptr<ListNode> n1(new ListNode(1));
	std::shared_ptr<ListNode> n2(new ListNode(2));

	n2->_prev = n1;
	return 0;
}

运行结果:

cpp 复制代码
~ListNode()
~ListNode()

分析:

  1. n2对象先进行析构,c2--c2 = 0,引用计数为0,释放s2资源,_prev被释放;
  2. _prev被释放,c1--c1 = 1,引用计数不为0,不释放s1资源;
  3. n1对象再进行析构,c1--c1 = 0,引用计数为0,释放s1资源;
  4. s1、s2资源均被正常释放(从打印信息可以看出)。
  • 场景三: n1->_next = n2; n2->_prev = n1;
cpp 复制代码
struct ListNode
{
	int _val;
	std::shared_ptr<ListNode> _next;
	std::shared_ptr<ListNode> _prev;

	ListNode(int val = 0)
		:_val(val)
	{}

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

int main()
{
	std::shared_ptr<ListNode> n1(new ListNode(1));
	std::shared_ptr<ListNode> n2(new ListNode(2));

	n1->_next = n2;
	n2->_prev = n1;
	return 0;
}

运行结果:

cpp 复制代码
// 空结果

分析:

  1. n2对象先进行析构,c2--c2 = 1,引用计数不为0,不能释放s2资源;
  2. n1对象再进行析构,c1--c1 = 1,引用计数不为0,不能释放s1资源;
  3. s1、s2资源均没有被正常释放 (从打印信息可以看出)。

两块资源不能释放的原因:

  • 想要释放s1资源,需要先将其引用计数c1变为0,而最后一个持有该引用的对象为n2->_prev,就需要释放析构n2->_prev
  • n2->_prev属于s2资源,想要析构n2->_prev,就需要释放s2资源,而想要释放s2资源,需要先将其引用计数c2变为0,而最后一个持有该引用的对象为n1->_next,就需要释放析构n1->_next
  • n1->_next属于s1资源,就需要释放s1资源,想要释放s1资源,需要先将......

这样逻辑就循环了,不能释放两个资源。


解决方案就是使用另一个智能指针weak_ptr

weak_ptr

std::weak_ptr是一种不拥有对象所有权的智能指针,它指向一个由std::shared_ptr管理的对象。weak_ptr用于解决shared_ptr之间的循环引用问题。

使用场景:

  • 当你需要访问但不拥有由shared_ptr管理的对象时。
  • 当你需要解决shared_ptr之间的循环引用问题时。
  • 注意weak_ptr肯定要和shared_ptr搭配使用。
cpp 复制代码
struct ListNode
{
	int _val;
	std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;

	ListNode(int val = 0)
		:_val(val)
	{}

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

int main()
{
	std::shared_ptr<ListNode> n1(new ListNode(1));
	std::shared_ptr<ListNode> n2(new ListNode(2));

	n1->_next = n2;
	n2->_prev = n1;
	return 0;
}

运行结果:

cpp 复制代码
~ListNode()
~ListNode()

因为weak_ptr不持有引用计数,不管理资源,所以这里不会出现循环引用问题,引用计数会减为0,两者对象都会正常析构。

原理

weak_ptr没有资源获取即初始化(RAII)特性,并且不直接参与资源管理,而是与shared_ptr共享内部控制块(control block)。这是它与shared_ptr的一个重要区别。但正是这些特性使得weak_ptr能够解决shared_ptr循环引用的问题。以下是对此问题的详细解释:

这个控制块包含了实际资源指针、引用计数(shared_countweak_count)。当所有的shared_ptr对象销毁时(shared_count = 0),资源被释放,但是控制块是直到所有weak_ptr销毁时才会被释放(weak_count = 0)。

一、循环引用问题

循环引用发生在两个或多个对象互相持有对方的shared_ptr时。由于每个shared_ptr都会增加它所指向对象的引用计数,因此这些对象之间的引用计数永远不会降到零。即使程序已经不再需要使用这些对象,它们也无法被销毁,因为它们的引用计数始终不为零。这就导致了内存泄漏。

二、weak_ptr的特性
  1. 不增加引用计数weak_ptr是一种弱引用,它指向由shared_ptr管理的对象,但不会增加该对象的引用计数。这意味着weak_ptr的存在不会阻止其所指向的对象被销毁。
  2. 观察而不拥有weak_ptr可以从shared_ptr创建,用于观察shared_ptr所指向的对象,但并不拥有该对象。因此,它不会参与对象的生命周期管理。
  3. 可转换为shared_ptrweak_ptr提供了lock方法,该方法尝试将weak_ptr提升为shared_ptr。如果所指向的对象还存活(即强引用计数大于0),lock会成功返回一个有效的shared_ptr实例;如果对象已经被销毁(强引用计数为0),则返回一个空的shared_ptr实例。
三、weak_ptr解决循环引用的原理

在循环引用的场景中,如果将其中一个或多个shared_ptr引用改为weak_ptr,则这些weak_ptr不会增加对象的强引用计数。这意味着当其他所有的shared_ptr都被销毁时,即便还有weak_ptr存在,对象的强引用计数也会降到零,从而触发对象的销毁。

具体来说,当两个对象A和B互相持有对方的shared_ptr时,它们的引用计数都会增加。如果改为A持有B的shared_ptr,而B持有A的weak_ptr,则B的weak_ptr不会增加A的引用计数。因此,当B的shared_ptr被销毁时(例如,B对象本身被销毁),A的引用计数不会因为B的weak_ptr而保持非零。这样,A就可以在没有其他shared_ptr指向它时被销毁。

示例

cpp 复制代码
struct ListNode
{
	int _val;
	std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;
	ListNode(int val = 0)
		:_val(val)
	{}

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

int main()
{
	std::shared_ptr<ListNode> n1(new ListNode(1));
	std::shared_ptr<ListNode> n2(new ListNode(2));
	std::cout << "n1的引用计数:" << n1.use_count() << std::endl;
	std::cout << "n2的引用计数:" << n2.use_count() << std::endl;
	n1->_next = n2;
	n2->_prev = n1;
	std::cout << "n1的引用计数:" << n1.use_count() << std::endl;
	std::cout << "n2的引用计数:" << n2.use_count() << std::endl;
	return 0;
}

运行结果:

cpp 复制代码
n1的引用计数:1
n2的引用计数:1
n1的引用计数:1
n2的引用计数:1
~ListNode()
~ListNode()
底层

weak_ptr 在进行赋值和拷贝构造操作时,其参数应为shared_ptr。并且在函数体内部, weak_ptr 中的指针指向shared_ptr对象管理的资源,但是不增加其引用计数。

注意: weak_ptr拷贝构造和赋值重载中需要访问到shared_ptr的指针,而这个指针是私有的,解决办法有:

  1. weak_ptr设置为shared_ptr的友元类;
  2. shared_ptr设计一个共有接口返回该指针。
cpp 复制代码
// wh::shared_ptr
T* get() const
{
	return _ptr;
}
cpp 复制代码
// wh::weak_ptr
template<class T>
class weak_ptr
{
public:
	weak_ptr()
		:_ptr(nullptr)
	{}

	weak_ptr(const shared_ptr<T>& sp)
	{
		_ptr = sp.get();
	}

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

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

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

private:
	T* _ptr;
};

struct ListNode
{
	int _val;
	wh::weak_ptr<ListNode> _next; // 使用弱引用weak_ptr
	wh::weak_ptr<ListNode> _prev; // 使用弱引用weak_ptr

	ListNode(int val = 0)
		:_val(val)
	{}

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

int main()
{
	wh::shared_ptr<ListNode> n1(new ListNode(1));
	wh::shared_ptr<ListNode> n2(new ListNode(2));
	std::cout << "n1的引用计数:" << n1.use_count() << std::endl;
	std::cout << "n2的引用计数:" << n2.use_count() << std::endl;
	n1->_next = n2;
	n2->_prev = n1;
	std::cout << "n1的引用计数:" << n1.use_count() << std::endl;
	std::cout << "n2的引用计数:" << n2.use_count() << std::endl;
	return 0;
}

运行结果:

cpp 复制代码
n1的引用计数:1
n2的引用计数:1
n1的引用计数:1
n2的引用计数:1
~ListNode()
delete: 0000018B24754570
~ListNode()
delete: 0000018B247541B0

我们发现n1、n2管理的资源,在经过n1->_next = n2; n2->_prev = n1;操作后,其引用计数并没有增加。并且最终资源也被正确释放了。

删除器

在 C++ 中,智能指针默认使用 deletedelete[] 来释放资源,但这并不适用于所有场景。例如,当需要释放多个对象、关闭文件或执行其他自定义清理操作时,就需要使用自定义的删除器(Deleter)。

  1. 管理多个对象 :当需要释放一组对象时,可以使用 delete[]
  2. 管理文件资源 :当需要关闭文件而不是删除对象时,可以使用 fclose()
  3. 其他自定义清理操作:当需要执行其他类型的清理操作时,可以定义相应的删除器。

补充: 关于数组的资源,标准库中提供了这种方式进行管理:

cpp 复制代码
std::shared_ptr<ListNode[]> p2(new ListNode[10]);

删除器的作用

在 C++ 标准库中,std::unique_ptrstd::shared_ptr 都允许你指定一个自定义的删除器。这个删除器是一个可调用对象(如函数、函数对象、lambda 表达式或函数指针),它将在智能指针销毁时被调用以释放资源。

删除器的定义

  1. 使用仿函数定义删除器
cpp 复制代码
struct CustomDeleter 
{
    void operator()(void* ptr) const 
    {
        // 自定义清理操作
        // 例如:释放特定类型的资源
        // delete ptr;
    }
};
  1. 使用函数作为删除器
cpp 复制代码
void customDelete(void* ptr) 
{
    // 自定义清理操作
    // 例如:释放特定类型的资源
    // delete ptr;
}
  1. 使用lambda表达式
    关于lambda表达式,具体可参考篇博客:lambda表达式

删除器的使用

std::unique_ptr

对于 std::unique_ptr,你可以在创建指针时通过第二个模板参数指定删除器的类型,或者在构造时通过 std::default_delete 的一个特化版本或其他可调用对象来指定删除器。

示例一:lambda表达式

例如,如果你有一个通过 malloc 分配的内存块,你可以使用 std::unique_ptr 和一个自定义的删除器来管理它:

cpp 复制代码
#include <memory>
#include <cstdlib> // for malloc and free
#include <iostream>

int main() {
    // 自定义删除器,使用 free 而不是 delete
    auto customDeleter = [](void* ptr) {
        std::free(ptr);
    };

    // 使用 std::unique_ptr 管理通过 malloc 分配的内存
    std::unique_ptr<void, decltype(customDeleter)> ptr(malloc(100), customDeleter);

    // ... 使用 ptr ...

    // 当 ptr 超出作用域时,customDeleter 会被调用以释放内存
    return 0;
}

在这个例子中,ptr 是一个 std::unique_ptr,它管理一个通过 malloc 分配的内存块。当 ptr 超出作用域或被显式销毁时,customDeleter 会被调用以使用 free 释放内存。


cpp 复制代码
// 显式指定删除器类型(避免编译器推断错误)  
std::unique_ptr<double, void(*)(double*)> sp2(new double(2.71), [](double* ptr) {  
    std::cout << "Lambda deleter called for double pointer\n";  
    delete ptr;  
});  

示例二:函数指针

cpp 复制代码
#include <iostream>  
#include <memory>  
  
void customDeleter(double* ptr) {  
    std::cout << "Custom deleter called for double pointer\n";  
    delete ptr;  
}  
  
int main() {  
    // 使用lambda表达式作为删除器  
    std::unique_ptr<double, decltype(&customDeleter)> sp1(new double(3.14), customDeleter);  
    // sp1会在作用域结束时自动调用删除器  
    return 0;  
}

示例三:仿函数

cpp 复制代码
#include <iostream>
#include <memory>
#include <cstdio> // 包含 fclose

// 自定义删除器
struct FileDeleter {
    void operator()(FILE* fp) const {
        fclose(fp);
    }
};

int main() {
    // 使用 FILE* 管理文件资源
    FILE* file = fopen("example.txt", "w");
    if (!file) {
        std::cerr << "Failed to open file." << std::endl;
        return 1;
    }

    // 使用 std::unique_ptr 和自定义删除器管理文件
    std::unique_ptr<FILE, FileDeleter> filePtr(file, FileDeleter());

    // 使用文件
    fprintf(filePtr.get(), "Hello, world!\n");

    // 文件将在 filePtr 被销毁时自动关闭
    return 0;
}
std::shared_ptr

对于 std::shared_ptr,你需要提供一个自定义的删除器类型,并且这个类型需要满足特定的要求(通常是一个函数对象或可调用对象)。

示例一:lambda表达式

cpp 复制代码
#include <iostream>  
#include <memory>  

int main() {  
    // 使用lambda表达式作为删除器  
    std::shared_ptr<int> sp1(new int(42), [](int* ptr) {  
        std::cout << "Lambda deleter called for int pointer\n";  
        delete ptr;  
    });  
  
    // sp1会在作用域结束时自动调用删除器  
    return 0;  
}

示例二:函数指针

cpp 复制代码
#include <iostream>  
#include <memory>  
  
void customDeleter(int* ptr) {  
    std::cout << "Custom deleter called for int pointer\n";  
    delete ptr;  
}  
  
int main() {  
    // 使用函数指针作为删除器  
    std::shared_ptr<int> sp1(new int(43), customDeleter);  
  
    // sp1会在作用域结束时自动调用删除器  
    return 0;  
}

示例三:仿函数

一个更简单的方法是使用 std::shared_ptr 的别名模板和自定义删除器:

cpp 复制代码
#include <memory>
#include <cstdio> // for FILE and fclose
#include <iostream>

// 自定义删除器类型
struct FileDeleter {
    void operator()(FILE* file) const {
        if (file) {
            std::fclose(file);
        }
    }
};

int main() {
    // 打开一个文件
    FILE* file = std::fopen("example.txt", "r");
    if (!file) {
        std::cerr << "Failed to open file." << std::endl;
        return 1;
    }

    // 使用 std::shared_ptr 管理文件指针和自定义删除器
    std::shared_ptr<FILE> filePtr(file, FileDeleter());

    // ... 使用 filePtr ...

    // 当 filePtr 超出作用域或被显式销毁时,FileDeleter 会被调用以关闭文件
    return 0;
}

在这个例子中,FileDeleter 是一个结构体,它重载了 operator() 以关闭 FILE* 指向的文件。然后,我们创建了一个 std::shared_ptr<FILE>,并将文件指针和 FileDeleter 的实例传递给它。当 filePtr 超出作用域或被显式销毁时,FileDeleteroperator() 会被调用以关闭文件。

总结
  1. 删除器的类型

    • 对于std::unique_ptr,删除器类型必须显式指定,否则编译器可能无法正确推断。
    • 对于std::shared_ptr,删除器类型通常可以自动推断。
  2. 资源管理

    • 自定义删除器负责释放资源,因此必须确保删除器正确实现,以避免资源泄漏。
    • 删除器被调用时,智能指针不再拥有资源。
  3. 线程安全

    • 如果智能指针在多个线程之间共享,确保删除器的调用是线程安全的。

通过自定义删除器,可以更灵活、安全地管理动态内存和其他资源,使代码更加健壮和易于维护。

智能指针与删除器的实现

  1. 构造函数中传入删除器

    • 构造函数接收一个删除器作为构造参数。
  2. 析构函数中使用删除器

    • 析构函数在对象销毁时调用存储的删除器来释放资源。

那么,如何将传入构造函数的函数对象给析构函数使用?(下面以shared_ptr的实现为例)

将这个函数对象作为一个类成员变量,析构函数可以使用这个类成员变量。但是定义一个函数对象作为成员变量,需要声明其类型,以下有两种方案可以得到其类型:

方案一: 该类型通过类的类型模板进行传递。

这样类模板为template<class T, class D>,这与标准库中的实现不一致(标准库中的类模板为:template<class T>

cpp 复制代码
template<typename T, typename D>
class shared_ptr
{
public:
	shared_ptr(T* ptr, D del)
		:_ptr(ptr), _pcount(new int(1)), _del(del)
	{}

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

	void release()
	{
		if (--(*_pcount) == 0)
		{
			_del(_ptr);
			//delete _ptr;
			delete _pcount;
			std::cout << "delete: " << _ptr << std::endl;
		}
	}

	~shared_ptr()
	{
		release();
	}
private:
	int* _pcount;
	T* _ptr;
	D _del;
};

方案二: 使用function包装器

所有删除器的参数为T*,返回值为void,是固定的,可以使用包装器对这个类型进行封装(当知道一个函数的参数和返回值类型时,就可以对该函数的类型进行封装)。这样,无论删除器的具体类型是什么,都可以统一处理,不需要在类模板中进行传递了。

示例:

cpp 复制代码
template<class T>
class shared_ptr
{
public:
	template<class D>
	shared_ptr(T* ptr, D del)
		:_ptr(ptr), _pcount(new int(1)), _del(del)
	{}

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

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

			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);
		}
			
		return *this;
	}

	void release()
	{
		if (--(*_pcount) == 0)
		{
			_del(_ptr);
			//delete _ptr;
			delete _pcount;
			std::cout << "delete: " << _ptr << std::endl;
		}
	}

	~shared_ptr()
	{
		release();
	}

	int use_count()
	{
		return *_pcount;
	}

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

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

	T* get() const
	{
		return _ptr;
	}

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

智能指针的建议

  • 尽量使用智能指针,而非裸指针来管理内存,很多时候利用RAII机制管理内存肯定更靠谱、更安全的多。
  • 如果没有多个所有者共享对象的需求,建议优先使用unique_ptr管理内存,它相对shared_ptr会更轻量一些。
  • 在使用shared_ptr时,一定要注意是否有循环引用的问题,因为这会导致内存泄漏。
  • shared_ptr的引用计数是安全的,但是里面的对象不是线程安全的,这点要区别开。

make_shared

std::make_shared 是一个模板函数,它接受一个或多个参数,这些参数用于构造要创建的对象,并返回一个指向该对象的std::shared_ptr智能指针。这个函数 在单个内存块中同时分配对象和控制块(control block),控制块是std::make_shared内部用于管理引用计数、删除器和指向对象指针的数据结构。

优点

  1. 内存管理简化: std::make_shared自动管理动态分配的内存,通过引用计数机制确保当最后一个std::shared_ptr被销毁或重置时,所指向的对象也会被自动删除,从而防止内存泄漏。
  2. 性能优化: 与直接使用new操作符创建对象并通过构造函数传递给std::shared_ptr相比,std::make_shared可以在一次内存分配中同时创建对象和控制块,减少内存碎片,提高内存使用效率,并降低内存分配的开销。
  3. 提高性能: 由于减少内存分配的次数和内存碎片,std::make_shared通常比直接使用newstd::shared_ptr构造函数创建对象更快(单次内存分配意味着分配器只调用一次)。此外,这种方式在多线程环境中也有一定优势,减少了分配内存的竞争。
  4. 降低内存泄露风险: 使用std::make_shared可以避免由于异常导致的 部分已分配内存未释放 的问题。
  5. 代码简洁性: 使用std::make_shared可以使代码更加简洁,避免显示调用newdelete操作符,减少内存管理的复杂性。
cpp 复制代码
class A {
public:
    A() {}
};

std::shared_ptr<A> sp1 = std::shared_ptr<A>(new A());
std::shared_ptr<A> sp2 = std::make_shared<A>();

解释:

  • 在没有std::make_shared之前,我们通常这样创建一个shared_ptr智能指针对象:std::shared_ptr<int> sp(new int(5));。这个过程其实做了两个动作:创建一个临时对象,又创建一个shared_ptr对象。如果第一步的内存分配成功,但 第二步抛出异常,那么就会发生内存泄漏。
  • std::make_shared内部原理: std::make_shared将对象的动态内存和控制块内存(存储引用计数的那块内存)一次性分配,减少了内存分配的次数。例如:auto sp = std::make_shared<int>(5);,这种方式比前一种方式高效,并且更加安全。
  • 异常安全: 使用std::make_shared,如果在创建过程中抛出异常,因为它是"全有或全无"的过程,所以不需要担心部分资源分配成功导致内存泄漏。例如,std::make_shared可以保证在对象和控制块都构建成功之后才开始使用它们。

今天的分享就到这里了,如果,你感觉这篇博客对你有帮助的话,就点个赞吧!感谢感谢......

相关推荐
一丝晨光1 小时前
不同语言的注释和数组
java·开发语言·javascript·c++·c·注释·数组
编程版小新6 小时前
C++初阶:STL详解(七)——list的模拟实现
开发语言·c++·学习·迭代器·list·list的模拟实现
丢掉幻想准备斗争8 小时前
C++(string类的实现)
开发语言·c++
Watermelon Y9 小时前
【C++】多肽
开发语言·c++
碧海蓝天20229 小时前
C++ 线性表、内存操作、 迭代器,数据与算法分离。
开发语言·数据结构·c++·算法
single5949 小时前
【优选算法】(第十七篇)
java·数据结构·c++·vscode·算法·leetcode
Silent starry sky10 小时前
C++初学者指南-5.标准库(第二部分)–随机数生成
c++·算法
C++忠实粉丝10 小时前
C++IO流
开发语言·c++
朔北之忘 Clancy12 小时前
2021 年 12 月青少年软编等考 C 语言二级真题解析
c语言·开发语言·c++·学习·算法·青少年编程·题解
乱打一通12 小时前
C++里的随机数
开发语言·c++