std::unique_ptr 智能指针


std::unique_ptr 是 C++11 引入的一个智能指针类型,用于管理动态分配的内存。它的主要特点是 “独占所有权”:在给定时间只有一个 std::unique_ptr 可以拥有某个对象,当这个 std::unique_ptr 被销毁(例如,离开其作用域)时,它所管理的对象也会被删除。这可以防止内存泄漏和双重释放等问题。

1. 实现原理

1.1 基本概念

std::unique_ptr 是一个模板类,定义在 <memory> 头文件中,其基本定义如下:

template <typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
public:
    // 类型别名
    using pointer = T*;            // 被管理对象指针类型
    using element_type = T;        // 被管理对象类型
    using deleter_type = Deleter;  // 自定义删除器,会从析构函数调用

    // 构造函数
    constexpr unique_ptr() noexcept;
    explicit unique_ptr(pointer p) noexcept;
    unique_ptr(pointer p, Deleter d) noexcept;    
    // 析构函数
    ~unique_ptr();
    
    // 禁用拷贝构造函数和赋值运算符重载
    unique_ptr(const unique_ptr&) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;
    // 移动构造函数
    unique_ptr(unique_ptr&& u) noexcept;
    // 移动赋值运算符
    unique_ptr& operator=(unique_ptr&& u) noexcept;

	// 获取被管理对象指针
	T* get() const noexcept;
	// 获取自定义删除器
	const Deleter& get_deleter() const noexcept;
	
    // 运算符重载,如同访问被管理对象指针
    T& operator*() const;
    T* operator->() const noexcept;
    T& operator[]( std::size_t i ) const;
    explicit operator bool() const noexcept;

	// 释放所有权,并返回一个指向被管理对象的指针。
    T* release() noexcept;
    // 重置被管理对象指针
    void reset(pointer p = pointer()) noexcept;
    // 交换被管理对象指针
    void swap(unique_ptr& u) noexcept;

private:
    pointer ptr;       // 被管理对象指针
    Deleter deleter;   // 自定义删除器
};

1.2 构造与析构

    // 构造函数
    constexpr unique_ptr() noexcept;
    explicit unique_ptr(pointer p) noexcept;
    unique_ptr(pointer p, Deleter d) noexcept;
    
    // 析构函数
    ~unique_ptr();

std::unique_ptr 的构造函数可以接受裸指针,也可以接受自定义的删除器:

  • 默认构造函数初始化 ptrnullptr
  • 接受裸指针的构造函数将 ptr 初始化为传入的指针。
  • 接受删除器的构造函数将 deleter 初始化为传入的自定义删除器。

析构函数会在 std::unique_ptr 被销毁时调用自定义删除器删除管理的对象,若无删除器则默认调用 delete

1.3 移动语义

 	// 禁用拷贝构造函数和赋值运算符重载
    unique_ptr(const unique_ptr&) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;

    // 移动构造函数
    unique_ptr(unique_ptr&& u) noexcept;

    // 移动赋值运算符
    unique_ptr& operator=(unique_ptr&& u) noexcept;

std::unique_ptr 禁用了拷贝构造和赋值运算符重载操作,以确保一个对象只有一个所有者。但是,std::unique_ptr 支持移动构造和移动赋值操作:

  • 移动构造函数会将被移动对象的 ptrdeleter 移动到新对象,并将被移动对象的 ptr 置为 nullptr
  • 移动赋值运算符会释放当前对象所拥有的指针,然后将被赋值对象的 ptrdeleter 移动过来,并将被赋值对象的 ptr 置为 nullptr

1.4 访问被管理对象指针

	// 获取被管理对象指针
	T* get() const noexcept;
	
    // 运算符重载,如同访问被管理对象指针
    T& operator*() const;
    T* operator->() const noexcept;
    T& operator[]( std::size_t i ) const;
    explicit operator bool() const noexcept;

std::unique_ptr 提供了一些方法来访问和修改被管理对象指针:

  • get() 返回被管理对象指针。
  • operator*operator->,operator[],operator bool允许像使用被管理对象指针一样使用 std::unique_ptr

1.5 交换智能指针

    // 交换被管理对象指针以及关联的删除器
    void swap(unique_ptr& u) noexcept;

swap() 方法用于交换两个 std::unique_ptr 对象管理的指针和关联的删除器。

1.6 重置和释放智能指针

	// 释放所有权,并返回一个指向被管理对象的指针。
    T* release() noexcept;
    // 重置被管理对象指针
    void reset(pointer p = pointer()) noexcept;

1.7 获取删除器

	// 获取自定义删除器
	const Deleter& get_deleter() const noexcept;

可以为 std::unique_ptr 提供一个自定义的删除器,以便在销毁对象时使用自定义的删除逻辑。

2. 应用

被管理对象以CTest为例,讲述如何应用。

class CTest
{
public:
    // 构造函数
    CTest(int num)
        : m_nNum(num)
    {
        std::cout << "CTest::CTest() " << m_nNum << std::endl;
    }
    
    // 析构函数
    ~CTest()
    {
        std::cout << "CTest::~CTest() " << m_nNum << std::endl;
    }
    
	// 自定义删除器
    static void CustomDelete(CTest* p)
    {
        std::cout << "CTest::CustomDelete()" << std::endl;
    }

    int m_nNum{ 0 };
};

2.1 初始化

  • 构造一个空的智能指针,并重置一个被管理对象指针
int main() {

    // 构造一个空的智能指针
    std::unique_ptr<CTest> ptr;
    if (!ptr) {
        std::cout << "被管理对象指针为空!" << std::endl;
    }

    // 重置
    ptr.reset(new CTest(1));
    if (ptr) {
        ptr->PrintNum();
    }
 }

输出结果
在这里插入图片描述

  • 构造并传入被管理对象指针,仔细观察被管理对象被释放的时机
int main() {

    std::cout << " main() begin " << std::endl;

    // 构造并传入被管理对象指针
    std::unique_ptr<CTest> ptr(new CTest(0));

    {
        std::unique_ptr<CTest> ptr1(new CTest(1));
    }// ptr1离开作用域     

    std::cout << " main() end " << std::endl;
	return 0;
}// ptr离开作用域

输出结果
在这里插入图片描述

  • 构造并传入被管理对象指针以及删除器
int main() {

    // 构造并传入被管理对象指针以及删除器
    std::unique_ptr<CTest, std::function<void(CTest*)>> ptr(new CTest(1), CustomDelete);
}

输出结果
在这里插入图片描述

  • 构造并传入被管理对象数组首地址以及lambda表达式删除器
int main() {

// 构造并传入被管理对象数组首地址以及lambda表达式删除器
std::unique_ptr<CTest[], std::function<void(CTest*)>> ptr(new CTest[3]{ 1,2,3 }, [](CTest* p) {delete[] p; });
}

输出结果
在这里插入图片描述

  • 使用 make_unique 初始化
int main() {

    // 使用 make_unique 初始化
    auto ptr = std::make_unique<CTest>(0);
}

输出结果
在这里插入图片描述

2.2 release与reset区别

  • release() 函数仅将被管理对象指针置nullptr
  • reset() 函数会将被管理对象指针置nullptr,且释放其内存。
int main() {
	
	// reset
    std::unique_ptr<CTest> ptr(new CTest(0));
    ptr.reset();
    if (!ptr) {
        std::cout << "ptr is nullptr!" << std::endl;
    }
	
	// release
    std::unique_ptr<CTest> ptr1(new CTest(1));
    ptr1.release();
    if (!ptr1){
        std::cout << "ptr1 is nullptr!" << std::endl;
    }
    
	return 0;
}

输出结果
在这里插入图片描述

2.3 get获取被管理对象指针

int main() {

    auto pMgrObjPoint = new CTest(0);
    std::unique_ptr<CTest> ptr(pMgrObjPoint);
    auto pTemp = ptr.get();

    std::cout << "pMgrObjPointL: " << pMgrObjPoint << std::endl;
    std::cout << "pTemp:         " << pTemp << std::endl;
	return 0;
}

输出结果
在这里插入图片描述

2.4 get_deleter获取删除器

int main() {

    std::unique_ptr<CTest, std::function<void(CTest*)>> ptr(new CTest(0), CustomDelete);
    auto& pTemp = ptr.get_deleter();
  
    pTemp(nullptr);
	return 0;
}

输出结果
在这里插入图片描述

2.5 运算符重载,等同于操作被管理对象指针

int main() {

    std::unique_ptr<CTest> ptr(new CTest(0));
    
    // -> 运算符重载
    ptr->PrintNum();

    // * 运算符重载
    (*ptr).PrintNum();

    // bool 运算符重载
    if (ptr){
        std::cout << "ptr is not nullptr" << std::endl;
    }

    std::unique_ptr<CTest[]> ptr1(new CTest[2]{ 1,2 });
    // [] 运算符重载
    ptr1[1].PrintNum();

	return 0;
}

输出结果
在这里插入图片描述

2.6 独占所有权

std::unique_ptr 满足可移动构造 (MoveConstructible) 和可移动赋值 (MoveAssignable) ,但不满足可复制构造 (CopyConstructible) 或可复制赋值 (CopyAssignable) 。

  1. 可移动构造 (MoveConstructible):可以通过移动构造函数创建一个新的 std::unique_ptr 实例,将所有权从一个 std::unique_ptr 转移到另一个。

    std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
    std::unique_ptr<int> ptr2 = std::move(ptr1); // 移动构造
    
  2. 可移动赋值 (MoveAssignable):可以通过移动赋值运算符将所有权从一个 std::unique_ptr 实例转移到另一个。

    std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
    std::unique_ptr<int> ptr2;
    ptr2 = std::move(ptr1); // 移动赋值
    

但是,std::unique_ptr 不具有以下特性:

  1. 不可复制构造 (CopyConstructible):无法通过复制构造函数创建一个新的 std::unique_ptr 实例,因为 std::unique_ptr 的设计就是为了确保每个对象只能有一个所有者。

    std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
    std::unique_ptr<int> ptr2 = ptr1; // 错误,不能复制构造
    
  2. 不可复制赋值 (CopyAssignable):无法通过复制赋值运算符将一个 std::unique_ptr 赋值给另一个。

    std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
    std::unique_ptr<int> ptr2;
    ptr2 = ptr1; // 错误,不能复制赋值
    

这种设计使得 std::unique_ptr 能够独占管理其指向的对象的生命周期,确保对象不会被多个 std::unique_ptr 实例管理,从而避免了双重删除等问题。

3. 总结

3.1 RAII 机制

RAII 机制,利用对象的生命周期管理资源(如内存、文件句柄、网络连接等)基本思想,在对象构造初始化时,绑定资源,离开作用域后,在对象析构中释放资源。

3.2 独占所有权

禁用拷贝构造以及赋值运算符重载,支持移动构造及移动赋值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值