傲渊山岳 发表于 2025-3-27 15:41:00

【C++】:智能指针

目次

智能指针的概念
智能指针的使用
unique_ptr
核心作用
根本用法
shared_ptr
核心作用
根本用法 
线程安全
示例:未加锁导致数据竞争
自定义删除器
weak_ptr
总结

智能指针的概念

C++中的智能指针是一种用于自动管理动态内存的工具,遵循RAII原则,确保资源在对象生命周期结束时自动释放,从而避免内存泄漏和悬空指针等问题。
   RAII原则
RAII(Resource Acquisition Is Initialization,资源获取即初始化)是C++中管理资源的核心原则,通过对象的生命周期自动化资源管理,确保资源的获取与释放安全可靠。


[*] 构造时获取资源:在对象的构造函数中完成资源分配(如内存、文件句柄、锁等)。
[*] 析构时释放资源:在析构函数中自动释放资源,确保资源不泄漏。
[*] 对象生命周期绑定:资源管理与对象作用域同等,离开作用域时自动触发析构。
实现步调
// 封装资源到类
class FileHandle
{
private:
    FILE* file;
public:
    FileHandle(const char* filename, const char* mode)
    {
      file = fopen(filename, mode);
      if (!file)
      {
            throw std::runtime_error("Open failed");
      }
    }
    ~FileHandle() { if (file) fclose(file); }

    // 禁用拷贝(避免重复释放)
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;

    // 允许移动(安全转移所有权)
    FileHandle(FileHandle&& other) noexcept
    : file(other.file)
    {
      other.file = nullptr;
    }
}; 智能指针的使用

unique_ptr

unique_ptr 是 C++11 引入的智能指针,用于管理动态分配的内存资源,遵循 独占全部权(exclusive ownership) 原则。它的核心目标是自动释放内存,避免内存泄漏,同时提供零额外开销的高效内存管理。
核心作用



[*] 自动内存管理: unique_ptr 离开作用域时,自动释放其管理的资源(调用 delete 或自定义删除器)。
[*] 独占全部权:同一时间只能有一个 unique_ptr 拥有资源,克制拷贝操作(包管全部权唯一性)。
[*] 高效轻量:与裸指针性能同等,无引用计数等额外开销(对比 std::shared_ptr)。
[*] 支持移动语义:通过 std::move 转移全部权,适合资源转达场景。
根本用法

   一、创建 unique_ptr 
1、使用 std::make_unique(C++14+ 推荐)
安全且高效,避免直接使用 new
#include <memory>
auto ptr1 = make_unique<int>(42);      // 管理一个 int
auto ptr2 = make_unique<MyClass>();      // 管理一个对象
auto arr = make_unique<int[]>(10);       // 管理动态数组(C++14+)
arr = 1;// 直接通过下标访问
2、直接构造(C++11 或特殊场景) 
需要显式 new,适用于自定义删除器
unique_ptr<int> ptr(new int(10)); 3、空指针初始化
默认构造函数生成空指针
unique_ptr<int> empty_ptr;// 初始化为 nullptr   二、全部权转移 
 1、通过 move
转移后,原指针变为 nullptr
auto ptr1 = std::make_unique<int>(100);
auto ptr2 = std::move(ptr1);   // ptr1 不再拥有资源 2、函数返回 unique_ptr
函数可以安全返回 unique_ptr(全部权转移)
unique_ptr<int> create_ptr(int value)
{
    return make_unique<int>(value);
}
auto ptr = create_ptr(50);// 所有权转移给 ptr    三、访问资源
 1、操作符 -> 和 *
类似裸指针访问成员或解引用
struct MyClass
{
    void doSomething()
    {}
};

auto obj = make_unique<MyClass>();
obj->doSomething();// 访问成员函数
int value = *obj;    // 解引用获取值(假设 obj 管理的是 int)   四、释放与重置资源 
1、主动释放资源 release()
放弃全部权,返回裸指针(需手动管理)
auto ptr = std::make_unique<int>(20);
int* raw = ptr.release();// ptr 变为 nullptr,需手动 delete raw 2、重置资源 reset()
释放当前资源并担当新资源(或置空)
ptr.reset(new int(30));// 释放旧值,管理新分配的 int(30)
ptr.reset();             // 等同于 ptr = nullptr   留意事项 

[*] 克制拷贝,只能移动。
[*] 优先使用 make_unique。
[*] 避免袒露裸指针。
// 1、禁止拷贝(编译错误示例)
auto ptr1 = make_unique<int>(10);
auto ptr2 = ptr1;// 错误!unique_ptr 不可拷贝

// 2、避免悬空指针(所有权转移后,原指针不再有效)
auto ptr1 = make_unique<int>(10);
auto ptr2 = move(ptr1);
*ptr1 = 20;// 未定义行为!ptr1 已为空

// 3、优先使用 make_unique
// 比直接 new 更安全(避免异常导致的内存泄漏)

// 4、不要混合使用裸指针
int* raw = new int(5);
unique_ptr<int> p1(raw);
unique_ptr<int> p2(raw);// 重复释放! shared_ptr

shared_ptr 是 C++11 引入的智能指针,用于管理动态分配的资源,支持共享全部权。多个 shared_ptr 可以指向同一对象,通过引用计数自动释放资源。
通过这种引用计数的方式就能支持多个对象一起管理某一个资源,也就是支持了智能指针的拷贝,并且只有当一个资源对应的引用计数减为0时才会释放资源,因此包管了同一个资源不会被释放多次。
核心作用



[*] 共享全部权
多个 shared_ptr 可共享同一对象的全部权,资源在全部全部者烧毁后自动释放。
[*] 引用计数
内部维护一个计数器,记录指向对象的 shared_ptr 数目。计数归零时调用析构函数。
[*] 线程安全
引用计数的增减是原子操作(线程安全),但对象自己的访问需额外同步。
[*] 内存开销
每个 shared_ptr 需要存储指向对象和控制块(含引用计数)的指针,比 unique_ptr 占用更多内存。
根本用法 

   一、创建 shared_ptr
1、使用 make_shared(C++14+ 推荐)
安全且高效,避免直接使用 new
#include <memory>
auto sptr1 = make_shared<int>(42);       // 创建 int
auto sptr2 = make_shared<MyClass>();   // 创建对象 2、直接构造(C++11 或特殊场景) 
需要显式 new,适用于自定义删除器
shared_ptr<int> sptr(new int(10)); 3、空指针初始化
默认构造函数生成空指针
shared_ptr<int> empty_sptr;// 初始化为 nullptr   二、共享全部权
1、拷贝与赋值
引用计数自动增加
auto sptr1 = make_shared<int>(100);
shared_ptr<int> sptr2 = sptr1;// 引用计数 +1 2、函数转达共享全部权
函数参数或返回值可安全转达 shared_ptr
void process(shared_ptr<MyClass> obj)
{
    // 引用计数 +1,函数结束时 -1
}
auto obj = make_shared<MyClass>();
process(obj);// 安全传递   三、访问资源 
1、操作符 -> 和 *
与裸指针用法同等
struct MyClass
{
        void doSomething()
        {
                cout << "访问成员" << endl;
        }
};

        auto obj = make_shared<MyClass>();
        auto sptr1 = make_shared<int>(10);
        obj->doSomething();// 访问成员
        int value = *sptr1;// 解引用
        cout << value << endl;      // value = 10

    四、引用计数管理 
1、查察引用计数
通过 use_count()(通常用于调试) 
cout << sptr1.use_count();// 输出当前引用计数 2、重置指针
使用 reset() 减少引用计数或释放资源 
sptr1.reset();            // 引用计数 -1,若归零则释放资源
sptr1.reset(new int(20));   // 释放旧资源,管理新资源   循环引用与 weak_ptr 
循环引用问题
两个对象相互持有对方的 shared_ptr,导致引用计数无法归零:
class Node
{
public:
        shared_ptr<Node> _next;
    int _val;
};
int main()
{
        auto node1 = make_shared<Node>();
        auto node2 = make_shared<Node>();
        node1->_next = node2;// node2 引用计数 = 2
        node2->_next = node1;// node1 引用计数 = 2 → 循环引用!
        cout << "node1:" << node1.use_count() << endl;
        cout << "node2:" << node2.use_count() << endl;
       
        return 0;
} 程序运行结束后变量node1 和 node2 都没有办法正常释放,是因为这两句毗连结点的代码导致了循环引用。

[*]当以 make_shared 的方式创建了两个 Node 结点并交给两个智能指针管理后,这两个资源对应的引用计数都被加到了1。
[*]将这两个结点毗连起来后,node1资源 当中的 _next成员 与 node2 一同管理 node2的资源,node2资源 中的_next 成员 与 node1 一同管理 node1的资源,此时这两个资源对应的引用计数都被加到了2。
[*]当node1 和 node2的生命周期也就结束了,此时这两个资源对应的引用计数终极都减到了1。
https://i-blog.csdnimg.cn/direct/e7a530d406f4411580a5920ea0fdb832.png
循环引用导致资源未被释放的缘故原由:

[*]当资源对应的引用计数(use_count)减为0时对应的资源才会被释放,因此node1资源 的释放取决于 node2资源当中的 _next成员,而node2资源的释放取决于node1资源当中的 _next成员。
[*]而node1资源当中的 _next成员的释放又取决于node1资源,node2资源当中的_next成员的释放又取 决于node1资源,于是这就变成了一个死循环,终极导致资源无法释放。
而如果毗连结点时只举行一个毗连操作,那么当node1和node2的生命周期结束时,就会有一个资源对应的引用计数被减为0,此时这个资源就会被释放,这个释放后另一个资源的引用计数也会被减为0,终极两个资源就都被释放了,这就是为什么只举行一个毗连操作时这两个结点就都可以或许精确释放的缘故原由。
线程安全

只管引用计数是原子安全的,但资源自己的访问不提供线程安全包管。若多个线程通过差别的 shared_ptr 访问同一资源,需手动同步:
示例:未加锁导致数据竞争

#include <memory>
#include <thread>

struct Data
{
    int value = 0;
};

void unsafe_increment(std::shared_ptr<Data> data)
{
    for (int i = 0; i < 100000; ++i)
    {
      data->value++; // 非原子操作,存在数据竞争
    }
}

int main()
{
    auto data = std::make_shared<Data>();
    std::thread t1(unsafe_increment, data);
    std::thread t2(unsafe_increment, data);
    t1.join();
    t2.join();
    // 预期 data->value = 200000,实际结果不确定
    std::cout << data->value << std::endl;
    return 0;
}  对共享资源的访问需通过互斥锁(如 std::mutex)或其他同步机制保护:
#include <memory>
#include <thread>
#include <mutex>

struct Data
{
    int value = 0;
    std::mutex mtx; // 为资源添加互斥锁
};

void safe_increment(std::shared_ptr<Data> data)
{
    for (int i = 0; i < 100000; ++i)
    {
      std::lock_guard<std::mutex> lock(data->mtx); // 加锁
      data->value++; // 受保护的原子性操作
    }
}

int main()
{
    auto data = std::make_shared<Data>();
    std::thread t1(safe_increment, data);
    std::thread t2(safe_increment, data);
    t1.join();
    t2.join();
    std::cout << data->value << std::endl; // 输出 200000
    return 0;
} 自定义删除器



[*]当智能指针对象的生命周期结束时,全部的智能指针默认都是以delete的方式将资源释放,这是不太合适的。
[*]因为智能指针并不是只管理以new方式申请到的内存空间,智能指针管理的也可能是管理的是一个文件指针。管理非内存资源指定释放逻辑(如文件、网络毗连)。
struct ListNode
{
        ListNode* _next;
        ListNode* _prev;
        int _val;
};

shared_ptr<ListNode> sp1(new ListNode);   
shared_ptr<FILE> sp2(fopen("test.cpp", "r")); 这时当智能指针对象的生命周期结束时,再以delete的方式释放管理的资源就会导致程序崩溃,因为以new[]的方式申请到的内存空间必须以delete[]的方式举行释放,而文件指针必须通过调用fclose函数举行释放。
这时就需要用到定制删除器来控制释放资源的方式,C++尺度库中的shared_ptr提供了如下构造函数:
template <class U, class D>
shared_ptr (U* p, D del);

// 参数说明:

// p:需要让智能指针管理的资源。
// del:删除器,这个删除器是一个可调用对象,比如函数指针、仿函数、lambda表达式以及被包装器包装后的可调用对象。

// 当shared_ptr对象的生命周期结束时就会调用传入的删除器完成资源的释放,调用该删除器时会将shared_ptr管理的资源作为参数进行传入。
代码如下:
struct ListNode
{
        ListNode* _next;
        ListNode* _prev;
        int _val;
};
void socket_deleter(ListNode* s)
{
        cout << "delete[]: " << s << endl;
        delete[] s;
}

int main()
{
        // 使用函数指针
        shared_ptr<ListNode> socket(new ListNode, socket_deleter);

        // lambda 表达式
        shared_ptr<FILE> sp2(fopen("test.cpp", "r"), [](FILE* ptr) {
                fclose(ptr);
                });

        return 0;
}  
weak_ptr

weak_ptr 是 C++11 引入的智能指针,用于解决 shared_ptr 的循环引用问题,并提供一种非拥有性观察资源的方式。它不会增加引用计数,也无法直接访问资源,需通过 shared_ptr 间接操作。
将 Node 中的 _next 成员的类型换成 weak_ptr 就不会导致循环引用问题了。
class Node
{
public:
        weak_ptr<Node> _next;
    int _val;
};
int main()
{
        auto node1 = make_shared<Node>();
        auto node2 = make_shared<Node>();
        node1->_next = node2;// node2 引用计数 = 1
        node2->_next = node1;// node1 引用计数 = 1
        cout << "node1:" << node1.use_count() << endl;
        cout << "node2:" << node2.use_count() << endl;
       
        return 0;
} 总结

智能指针通过RAII机制自动化资源管理,其核心原理如下:
类型全部权模型核心机制典型用途std::unique_ptr独占全部权禁用拷贝,支持移动单一全部者,明确生命周期std::shared_ptr共享全部权引用计数与控制块多全部者共享资源std::weak_ptr非拥有性观察弱引用,lock()安全访问解决循环引用,缓存观察

[*] 循环引用:
使用weak_ptr替代shared_ptr,打破引用循环。
[*] 混合使用原始指针:
避免用同一裸指针初始化多个shared_ptr,防止重复释放。
[*] 线程安全:
引用计数操作原子安全,但资源访问需加锁。
 

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 【C++】:智能指针