C++智能指针

1. 概念

RAII是Resource Acquisition Is Initialization的缩写,他是⼀种管理资源的类的设计思想,本质是⼀种利⽤对象⽣命周期来管理获取到的动态资源,避免资源泄漏,这⾥的资源可以是内存、⽂件指针、⽹络连接、互斥锁等等。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问,资源在对象的⽣命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。

  • 资源获取在构造函数中完成。
  • 资源释放在析构函数中完成。
  • 智能指针类除了满⾜RAII的设计思路,还要⽅便资源的访问,所以智能指针类还会想迭代器类⼀样,重载 operator*/operator->/operator[] 等运算符,⽅便访问资源。

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

C++标准库中的智能指针都在<memory>头文件。

2.1 auto_ptr(已废弃)

2.1.1 概念和示例

auto_ptr是C++98时设计出来的智能指针,他的特点是拷⻉时把被拷⻉对象的资源的管理权转移给拷⻉对象,这是⼀个⾮常糟糕的设计,因为他会到被拷⻉对象悬空,访问报错的问题。

#include <iostream>
#include <memory> // 在 C++98/11 中

// 示例:auto_ptr 的问题
void autoPtrDemo() {
    std::auto_ptr<int> p1(new int(10));
    std::auto_ptr<int> p2 = p1; // 所有权转移,p1 变为空

    // std::cout << *p1 << std::endl; // 运行时错误!
    std::cout << *p2 << std::endl; // 正常输出 10
}

// 常见问题:意外所有权转移
void problematicFunction(std::auto_ptr<int> param) {
    // 函数调用时所有权已经转移
}

void testAutoPtr() {
    std::auto_ptr<int> ptr(new int(20));
    problematicFunction(ptr); // ptr 现在为空
    // *ptr = 30; // 运行时错误!
}
2.1.2 模拟实现
#pragma once

namespace simulate_auto_ptr {
    template <typename T>
    class auto_ptr {
        public:
            auto_ptr(T* _ptr) :ptr(_ptr) {}
            auto_ptr(auto_ptr<T>& self) {
                if (ptr) delete ptr;
                ptr = self.ptr;
                self.ptr = nullptr;
            }
            auto_ptr<T>& operator=(auto_ptr<T>& self) {
                if (this != &self) {
                    if (ptr) delete ptr;
                    ptr = self.ptr;
                    self.ptr = nullptr;
                }
                return *this;
            }
            ~auto_ptr() { 
                if (ptr)
                    delete ptr;
            }

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


2.2 unique_ptr

2.2.1 概念和示例

unique_ptr是C++11设计出来的智能指针,特点是不支持拷贝,只支持移动,独占所有权的智能指针。

#include <iostream>
#include <memory>

void uniquePtrDemo() {
    // 创建 unique_ptr
    std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
    std::unique_ptr<int[]> arr = std::make_unique<int[]>(5);

    // 访问数据
    std::cout << *ptr1 << std::endl; // 42
    arr[0] = 10;
    std::cout << arr[0] << std::endl; // 10

    // 移动语义 - 显式所有权转移
    std::unique_ptr<int> ptr2 = std::move(ptr1);
    // std::cout << *ptr1 << std::endl; // 错误!ptr1 为空
    std::cout << *ptr2 << std::endl; // 42

    // 释放所有权
    int* rawPtr = ptr2.release();
    delete rawPtr; // 需要手动释放

    // 重置指针
    auto ptr3 = std::make_unique<int>(100);
    ptr3.reset(new int(200)); // 自动释放旧内存,管理新内存
    ptr3.reset(); // 释放内存,ptr3 为空

    // 自定义删除器
    auto fileDeleter = [](FILE* f) {
        if (f) {
            std::cout << "关闭文件" << std::endl;
            fclose(f);
        }
    };

    std::unique_ptr<FILE, decltype(fileDeleter)> filePtr(fopen("test.txt", "w"), fileDeleter);
}
2.2.2 模拟实现
#pragma once

namespace simulate_unique_ptr {
    // unique_ptr 不允许拷贝
    template <typename T>
    class unique_ptr {
        public:
            explicit unique_ptr(T* _ptr) :ptr(_ptr) {}
            unique_ptr(const unique_ptr<T>& self) = delete;
            unique_ptr<T>& operator=(const unique_ptr<T>& self) = delete;
            unique_ptr(unique_ptr<T>&& self) {
                ptr = self.ptr;
                self.ptr = nullptr;
            }
            ~unique_ptr() { 
                if (ptr)
                    delete ptr;
            }

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

2.3 shared_ptr

2.3.1 概念和示例

shared_ptr共享所有权,使用引用计数,支持拷贝和移动,当最后一个管理资源的shared_ptr被销毁时释放内存。

#include <iostream>
#include <memory>

// 创建 shared_ptr
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::shared_ptr<int> ptr2 = ptr1; // 引用计数增加

// 获取引用计数
std::cout << "引用计数: " << ptr1.use_count() << std::endl;
2.3.2 模拟实现
#pragma once
#include <iostream>
#include <functional>

namespace simulate_shared_ptr {
    template<typename T>
    struct default_delete {
        void operator()(T* ptr) const { delete ptr; }
    };

    template<typename T>
    struct default_delete<T[]> {
        void operator()(T* ptr) const { delete[] ptr; }
    };

    // unique_ptr的实现方式 
    template<typename T , typename Deleter = default_delete<T>> class unique_ptr {};

    template<typename T>
    class shared_ptr {
        public:
            template<typename D>
            shared_ptr<T>(T* _ptr , D del) :ptr(_ptr) ,pcount(new int(1)) , deleter(del) {}

            // 构造的时候初始化引用计数
            shared_ptr<T>(T* _ptr) :ptr(_ptr) ,  pcount(new int(1)) {}
            shared_ptr<T>(const shared_ptr<T>& sp) :ptr(sp.ptr) , pcount(sp.pcount) { ++(*pcount); }
            shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
                // 1.相同对象之间的赋值 p1 = p1;
                // 2.不同对象,但管理的资源相同之间的赋值 p1 = p2;
                // 3.不同对象,管理的资源也不同之间的赋值 p3 = p4;

                // 判断两个对象管理的不是同一个资源再相互赋值
                if (ptr != sp.ptr) {
                    if (--(*pcount) == 0) {
                        deleter(ptr);
                        delete pcount;
                    }
                    ptr = sp.ptr;
                    pcount = sp.pcount;
                    ++(*pcount);
                }
                return *this;
            }

            ~shared_ptr<T>() {
                // 引用计数减到0释放空间
                if (--(*pcount) == 0) {
                    delete ptr;
                    delete pcount;
                }
            }
            T& operator*() { return *ptr; }
            T* operator->() { return ptr; }

        private:
            T* ptr;
            int* pcount;
            std::function<void(T*)> deleter = [](T* ptr){ delete ptr; };    //  默认使用delete
    };

    template<typename T>
    class shared_ptr<T[]> {
        public:
            T& operator[](size_t pos) { return ptr[pos]; }
        private:
            T* ptr;
            int* pcount;
            std::function<void(T*)> deleter;
    };
}

2.4 weak_ptr

std::weak_ptr 是 C++11 引入的一种智能指针,主要用于解决 std::shared_ptr 可能导致的循环引用问题,它本身不拥有对象的所有权。

循环引用问题:

#include <memory>
#include <iostream>
#include <string>

class Node {
public:
    std::shared_ptr<Node> next;
    std::shared_ptr<Node> prev;

    Node(const std::string& name) : name(name) {
        std::cout << name << " 创建\n";
    }

    ~Node() {
        std::cout << name << " 销毁\n";
    }

    std::string name;
};

void circularReferenceProblem() {

    auto n1 = std::make_shared<Node>("Node1");
    auto n2 = std::make_shared<Node>("Node2");

    // 创建循环引用
    n1->next = n2;
    n2->prev = n1;

    std::cout << "n1 引用计数: " << n1.use_count() << std::endl; // 2
    std::cout << "n2 引用计数: " << n2.use_count() << std::endl; // 2

    // 退出作用域时,由于循环引用,对象不会被销毁!
    // 内存泄漏!
}

在这里插入图片描述

在这里插入图片描述


weak_ptr的主要特性:

  1. 不控制对象生命周期
    weak_ptr 指向一个由 shared_ptr 管理的对象,但不会增加其引用计数。当最后一个关联的 shared_ptr 被销毁时,无论是否有 weak_ptr 指向该对象,该资源都会被释放。

  2. 解决循环引用
    当两个对象相互持有 shared_ptr 时会形成循环引用,导致引用计数无法减为 0,内存泄漏。使用 weak_ptr 替代 shared_ptr 可打破循环。

  3. 需要转换为 shared_ptr 使用
    weak_ptr 不能直接访问对象,必须通过 lock() 方法返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回 shared_ptr 是⼀个空对象,如果资源没有释放,则通过返回的 shared_ptr 访问资源是安全的。

  4. weak_ptr⽀持expired检查指向的资源是否过期

  5. 如果两个shared_ptr管理一个资源,再加一个 weak_ptr,当这两个shared_ptr都析构了,同时资源也会被释放,但是为了方便use_count的访问,资源释放了,但是引用计数不会释放,可能通过另一个引用计数来保证。

#include <memory>
#include <string>
using namespace std;

void test() {
    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
    cout << wp.use_count() << endl; // 2
    // sp1和sp2都指向了其他资源,则weak_ptr就过期了
    sp1 = std::make_shared<string>("222222");   
    cout << wp.expired() << endl;   // 0
    cout << wp.use_count() << endl; // 1
    sp2 = std::make_shared<string>("333333");
    cout << wp.expired() << endl;   // 1
    cout << wp.use_count() << endl; // 0
    wp = sp1;
    //std::shared_ptr<string> sp3 = wp.lock();
    auto sp3 = wp.lock();   
    cout << wp.expired() << endl;   // 0
    cout << wp.use_count() << endl; // 2
}

解决循环引用问题:

// 将 std::shared_ptr<Node> next;
//    std::shared_ptr<Node> prev; 
//  改为 weak_ptr<Node> next; weak_ptr<Node> prev;

2.5 shared_ptr和unique_ptr删除器的不同使用

  • unique_ptr 的删除器是模板参数的一部分,需要在声明时指定
  • shared_ptr 的删除器不是模板参数,而是在构造时指定
// unique_ptr 使用删除器
auto deleter = [](int* p) { 
    delete p; 
};

// 删除器是模板参数的一部分
std::unique_ptr<int, decltype(deleter)> uptr(new int, deleter);

// shared_ptr 使用删除器
// 删除器在构造时指定,不影响类型
std::shared_ptr<int> sptr(new int, [](int* p) { 
    delete p; 
});

2.6 底层怎么区分delete还是delete[]

智能指针通过模板特化删除器 来区分 deletedelete[]。标准库为数组类型提供了特化版本。

2.6.1 示例
#include <memory>
#include <iostream>

// unique_ptr 的两种特化形式
void uniquePtrSpecialization() {
    // 1. 对于非数组类型 - 使用 delete
    std::unique_ptr<int> singlePtr = std::make_unique<int>(42);

    // 2. 对于数组类型 - 使用 delete[]
    std::unique_ptr<int[]> arrayPtr = std::make_unique<int[]>(5);
}
2.6.2 部分实现
// 简化版的 unique_ptr 实现
namespace detail {
    // 默认删除器(非数组)
    template<typename T>
    struct default_delete {
        void operator()(T* ptr) const {
            delete ptr;
        }
    };

    // 数组特化的删除器
    template<typename T>
    struct default_delete<T[]> {
        void operator()(T* ptr) const {
            delete[] ptr;
        }
    };
}

// 主模板
template<typename T, typename Deleter = detail::default_delete<T>>
class unique_ptr {
private:
    T* ptr;
    Deleter deleter;

public:
    // 构造函数、析构函数等...
    ~unique_ptr() {
        deleter(ptr);
    }
};

// 数组特化
template<typename T, typename Deleter>
class unique_ptr<T[], Deleter> {
private:
    T* ptr;
    Deleter deleter;

public:
    // 针对数组的特殊接口
    T& operator[](size_t index) {
        return ptr[index];
    }

    ~unique_ptr() {
        deleter(ptr);
    }
};
  • 类模板部分特化的接口继承行为:在类的部分特化中,如果只提供特殊的接口,其他接口会自动使用主模板的版本。部分特化会继承主模板的所有接口。

  • 在部分特化中,不能重复主模板的默认参数。这是 C++ 模板语法的一个规定。

2.7 make_unique、make_shared与直接构造的区别

  • make_unique、make_shared异常安全。

  • make_shared性能更好。

    • 若直接构造,需要两次分配(容易产生内存碎片)

    • make_shared:管理的内存和引用计数分配在一起(减少内存碎片)

  • 若使用自定义删除器,必须使用直接构造。

Logo

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

更多推荐