欢迎光临
我们一直在努力

【C++】C++11 智能指针

目录

    • 1. 为什么必须抛弃裸指针?
    • 2. 智能指针发展
      • 2.1 auto_ptr (C++98)
      • 2.2 unique_ptr (C++11)
      • 2.3 shared_ptr (C++11)
    • 3. 深度解析 shared_ptr
      • 3.1 内存优化:make_shared 与 直接 new 的区别
      • 3.2 定制删除器(Deleter)的必要性
    • 4. 线程安全陷阱
    • 5. 循环引用灾难与 weak_ptr
      • 5.1 循环引用是如何发生的?
      • 5.2 weak_ptr 解决循环引用
    • 6. 总结

在现代 C++ 开发中,内存管理是核心痛点。结合 CSDN 优质技术博客的深度解析与底层代码拆解,本文将全面剖析 C++ 智能指针的演进、底层原理、内存布局以及多线程安全陷阱。

1. 为什么必须抛弃裸指针?

传统 C/C++ 内存泄漏通常分为堆内存泄漏(忘记释放 new/malloc 内存)和系统资源泄漏(未关闭句柄、套接字等)。除了疏忽导致外,最大的隐患是异常安全问题。

void Func() {
int* p1 = new int;
// 若后续逻辑(或函数调用)抛出异常,直接跳转至 catch 块,delete p1 将永远无法执行
cout << div() << endl;
delete p1;
}

工程实践中,事前预防远胜于事后检测。C++ 采用 RAII(Resource Acquisition Is Initialization,资源获取即初始化)思想彻底解决这一问题。其核心机制是:在对象构造时获取并接管资源,在生命周期内保持有效,在对象析构时由编译器自动调用析构函数释放资源。

2. 智能指针发展

C++ 智能指针经历了从 C++98 到 C++11 的重大演进,标准库大量吸收了 Boost 库的核心实现原理。

2.1 auto_ptr (C++98)

image-20260730143121048

auto_ptr 采用“管理权转移”机制:拷贝或赋值时,原对象的指针会被强行置空,资源完全交由新对象管理。 原对象内部指针被强行置空。

#include <memory>
using namespace std;
int main() {
auto_ptr<Date> ap1(new Date);
// 拷贝时,管理权限转移,ap1 被悬空
auto_ptr<Date> ap2(ap1);
// ap1->_year++; // 致命错误:此时 ap1 为空指针,解引用将导致程序崩溃
return 0;
}

  • 致命缺陷:拷贝行为隐式剥夺了原对象的所有权,若后续代码无意中访问原对象,会导致对空指针的解引用,直接引发 Core Dump(崩溃)。目前已被诸多公司明令禁止,并在 C++11 后被正式废弃。

2.2 unique_ptr (C++11)

针对 auto_ptr 的痛点,unique_ptr 采用了简单粗暴的防拷贝机制:直接 delete 拷贝构造函数和赋值运算符,强制实现独占所有权语义。

#include <memory>
using namespace std;

int main() {
unique_ptr<Date> up1(new Date);

// unique_ptr<Date> up2(up1); // 编译报错:不支持拷贝

// 若业务确实需要转移管理权,必须显式使用 std::move
unique_ptr<Date> up3 = std::move(up1);
// 此时 up1 为空,资源完全由 up3 独占接管

return 0;
}

  • 特性:由于没有额外的维护开销,它与裸指针的性能几乎一致(零开销抽象)。
  • 转移所有权:必须显式调用 std::move() 才能将管理权转移给另一个 unique_ptr,语义极其清晰。

2.3 shared_ptr (C++11)

shared_ptr 允许多个智能指针管理同一块内存资源。它在内部维护了一个引用计数,新对象拷贝时计数 +1,对象销毁析构时计数 -1。当计数归零时,说明自己是最后一个使用者,触发 delete 释放底层资源。 并允许传入定制删除器以管理非 new 申请的资源

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

// 自定义文件句柄删除器
class Fclose {
public:
void operator()(FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
}
};

int main() {
// 1. 基础共享与引用计数测试
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3(sp2);
cout << sp1.use_count() << endl; // 输出 3

// 2. 高效初始化:make_shared 一次性分配内存,性能更优且避免内存碎片
shared_ptr<Date> sp6 = make_shared<Date>(2024, 8, 5);

// 3. 定制删除器:管理数组与文件
shared_ptr<Date[]> sp4(new Date[5]); // 内部正确调用 delete[] 释放数组
shared_ptr<FILE> sp5(fopen("test.cpp", "r"), Fclose()); // 析构时自动调用 fclose

return 0;
}

3. 深度解析 shared_ptr

标准的 shared_ptr 并非只有一个指针那么简单,其内部通常包含两个指针:

  • 指向所管理对象内存的数据指针。
  • 指向控制块(Control Block)的指针。控制块中存储了**强引用计数(use_count)、弱引用计数(weak_count)以及自定义删除器(Deleter)**等。
  • 结合提供的底层模拟代码,分析核心机制:

    namespace bit {
    template<class T>
    class shared_ptr {
    public:
    // 构造:分配控制块(此处用 _pcount 模拟)
    shared_ptr(T* ptr = nullptr) : _ptr(ptr), _pcount(new int(1)) {}

    // 支持定制删除器
    template<class D>
    shared_ptr(T* ptr, D del) : _ptr(ptr), _pcount(new int(1)), _del(del) {}

    // 拷贝:引用计数累加
    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;
    }

    ~shared_ptr() { release(); }
    T* get() const { return _ptr; }
    int use_count() const { return *_pcount; }

    private:
    void release() {
    if ((*_pcount) == 0) {
    _del(_ptr); // 调用删除器
    delete _pcount;
    _ptr = nullptr;
    _pcount = nullptr;
    }
    }
    T* _ptr;
    int* _pcount; // 控制块(引用计数)
    std::function<void(T*)> _del = [](T* ptr) { delete ptr; }; // 默认删除器
    };
    }

    3.1 内存优化:make_shared 与 直接 new 的区别

    在标准的 std::shared_ptr<int> p(new int(10)) 初始化中,存在两次内存分配:一次为 new int(10) 分配数据内存,一次为控制块分配内存。

    而使用 std::make_shared:

    • 优势:只需进行一次内存分配,将数据对象与控制块放置在一块连续内存中。这不仅提升了性能(避免碎片化、提高缓存命中率),还避免了在两次分配之间若抛出异常导致的数据内存泄漏风险。

    3.2 定制删除器(Deleter)的必要性

    智能指针默认使用 delete 释放内存。但若资源是 new [] 出来的数组、malloc 的堆空间,或者是 fopen 产生的文件句柄,必须使用自定义删除器防止程序崩溃或系统资源泄漏。

    // 仿函数删除文件句柄
    class Fclose {
    public:
    void operator()(FILE* ptr) {
    cout << "fclose:" << ptr << endl;
    fclose(ptr);
    }
    };

    // 业务应用
    bit::shared_ptr<FILE> sp5(fopen("test.cpp", "r"), Fclose());
    // 使用 Lambda 表达式释放 malloc 内存
    bit::shared_ptr<int> sp6((int*)malloc(40), [](int* ptr) { free(ptr); });

    4. 线程安全陷阱

    shared_ptr 的线程安全性需要分为两个维度严格界定:

  • 控制块(引用计数)是线程安全的:标准库通过原子操作(或加锁机制)保护引用计数的 ++ 和 –。因此,多线程同时对同一个 shared_ptr 对象进行拷贝析构是安全的。
  • 管理的底层对象资源不是线程安全的:智能指针不管堆上数据的线程安全问题。如果多线程中同时通过 shared_ptr 修改其指向的数据资源(如 copy->_year++),必须由开发者在业务层使用 mutex 加锁同步,否则会导致数据错乱。
  • 5. 循环引用灾难与 weak_ptr

    5.1 循环引用是如何发生的?

    在构建双向链表或图结构时,若节点内使用 shared_ptr 互指,将引发灾难性后果。

    struct ListNode {
    bit::shared_ptr<ListNode> _next;
    bit::shared_ptr<ListNode> _prev;
    ~ListNode() { cout << "~ListNode()" << endl; }
    };
    // 互相链接后,n1 和 n2 的内部引用计数皆为 2
    n1->_next = n2;
    n2->_prev = n1;

    离开作用域时,n1 和 n2 析构,引用计数减为 1。但此时 n1 被 n2._prev 持有,n2 被 n1._next 持有。因为外部对象无法释放,内部成员也不会被析构,双方互相钳制,造成无法挽回的内存泄漏。

    5.2 weak_ptr 解决循环引用

    weak_ptr 是专门为解决 shared_ptr 循环引用而生的观察者指针。 weak_ptr 不支持 RAII 管理资源,主要用作 shared_ptr 的辅助工具。它不增加引用计数,可完美打破双向链表中的循环引用死锁。

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

    // 场景一:打破循环引用
    struct ListNode {
    int _data;
    // 使用 weak_ptr 替代 shared_ptr 作为成员,避免互相钳制
    weak_ptr<ListNode> _next;
    weak_ptr<ListNode> _prev;

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

    // 场景二:资源状态探测与安全提升
    int main() {
    std::weak_ptr<ListNode> wp;
    std::shared_ptr<ListNode> sp;

    {
    std::shared_ptr<ListNode> n1(new ListNode);
    wp = n1; // weak_ptr 观测 shared_ptr,但不增加强引用计数
    cout << wp.expired() << endl; // 输出 0 (false),表示资源存活

    n1->_data++;

    // 安全访问数据:使用 lock() 尝试将 weak_ptr 提升为 shared_ptr
    sp = wp.lock();
    } // 离开作用域,n1 和 sp 析构,资源被彻底释放

    cout << wp.expired() << endl; // 输出 1 (true),表示底层资源已被释放

    return 0;
    }

    • 原理:weak_ptr 指向 shared_ptr 管理的资源,但绝不会增加控制块中的强引用计数(use_count)。
    • 底层实现逻辑:

    namespace bit {
    template<class T>
    class weak_ptr {
    public:
    weak_ptr() = default;
    weak_ptr(const shared_ptr<T>& sp) : _ptr(sp.get()) {}
    weak_ptr<T>& operator=(const shared_ptr<T>& sp) {
    _ptr = sp.get();
    return *this;
    }
    private:
    T* _ptr = nullptr; // 不干预强引用计数的递增
    };
    }

    • 状态探测与资源访问:由于 weak_ptr 不保证资源存活,不能直接重载 operator* 或 operator->(不支持 RAII)。使用标准库时,必须通过 wp.expired() 判断资源是否已被释放,并通过 wp.lock() 原子的将 weak_ptr 提升(Promote)为一个有效的 shared_ptr,才能安全访问数据。

    6. 总结

  • 优先使用 std::unique_ptr,只在需要绝对共享所有权时才使用 std::shared_ptr。
  • 尽可能使用 std::make_unique 和 std::make_shared 进行初始化,以提升性能并保障异常安全性。
  • 警惕双向链表、树的父子节点互指以及观察者模式中的循环引用,强制采用 weak_ptr 作为反向指针打破闭环。
  • 赞(0)
    未经允许不得转载:171主机测评 » 【C++】C++11 智能指针
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址