【C++篇】智能指针详解(二):原理剖析与高级话题

文章目录

    • C++智能指针详解(二):原理剖析与高级话题
    • 一、智能指针的实现原理
      • [1.1 auto_ptr的实现](#1.1 auto_ptr的实现)
      • [1.2 unique_ptr的实现](#1.2 unique_ptr的实现)
      • [1.3 shared_ptr的实现](#1.3 shared_ptr的实现)
      • [1.4 支持自定义删除器](#1.4 支持自定义删除器)
    • 二、shared_ptr的循环引用问题
      • [2.1 什么是循环引用](#2.1 什么是循环引用)
      • [2.2 循环引用的原理分析](#2.2 循环引用的原理分析)
      • [2.3 weak_ptr:打破循环引用](#2.3 weak_ptr:打破循环引用)
      • [2.4 weak_ptr的使用](#2.4 weak_ptr的使用)
      • [2.5 weak_ptr的简单实现](#2.5 weak_ptr的简单实现)
    • 三、智能指针的线程安全
      • [3.1 两个层面的线程安全](#3.1 两个层面的线程安全)
      • [3.2 引用计数的线程安全](#3.2 引用计数的线程安全)
      • [3.3 指向对象的线程安全](#3.3 指向对象的线程安全)
    • 四、C++11与Boost的关系
      • [4.1 Boost库简介](#4.1 Boost库简介)
      • [4.2 智能指针的演进历史](#4.2 智能指针的演进历史)
    • 五、内存泄漏的检测与预防
      • [5.1 什么是内存泄漏](#5.1 什么是内存泄漏)
      • [5.2 内存泄漏的检测与预防](#5.2 内存泄漏的检测与预防)
    • 六、智能指针使用的注意事项
      • [6.1 不要混用智能指针和裸指针](#6.1 不要混用智能指针和裸指针)
      • [6.2 不要用get()返回的指针构造新智能指针](#6.2 不要用get()返回的指针构造新智能指针)
      • [6.3 小心移动后的对象](#6.3 小心移动后的对象)
      • [6.4 避免循环引用](#6.4 避免循环引用)
      • [6.5 智能指针不是万能的](#6.5 智能指针不是万能的)
    • 七、总结与展望
      • [7.1 全系列回顾](#7.1 全系列回顾)
      • [7.2 核心要点总结](#7.2 核心要点总结)
      • [7.3 实践建议](#7.3 实践建议)
      • [7.4 继续学习](#7.4 继续学习)
    • 八、常见问题解答

C++智能指针详解(二):原理剖析与高级话题

💬 欢迎讨论:本文是C++智能指针系列的第二篇,将深入剖析智能指针的实现原理,并探讨循环引用、线程安全等高级话题。如果你在学习过程中有任何疑问,欢迎在评论区留言交流!

👍 点赞、收藏与分享:这是系列的完结篇,建议结合第一篇一起学习。如果觉得有帮助,请分享给更多的朋友!

🚀 系列回顾:在第一篇中,我们学习了智能指针的使用场景、RAII设计思想以及标准库智能指针的使用方法。本篇将继续深入学习。


一、智能指针的实现原理

1.1 auto_ptr的实现

虽然auto_ptr已被废弃,但了解它的实现有助于理解其设计缺陷。

核心思想:拷贝转移所有权

cpp 复制代码
namespace bit
{
    template<class T>
    class auto_ptr
    {
    public:
        explicit auto_ptr(T* ptr = nullptr)
            : _ptr(ptr)
        {}
        
        // 拷贝构造:转移所有权
        auto_ptr(auto_ptr<T>& ap)
            : _ptr(ap._ptr)
        {
            // 被拷贝对象失去所有权
            ap._ptr = nullptr;
        }
        
        // 拷贝赋值:转移所有权
        auto_ptr<T>& operator=(auto_ptr<T>& ap)
        {
            if (this != &ap)
            {
                // 释放当前资源
                if (_ptr)
                    delete _ptr;
                
                // 转移ap的资源
                _ptr = ap._ptr;
                ap._ptr = nullptr;
            }
            return *this;
        }
        
        ~auto_ptr()
        {
            if (_ptr)
            {
                cout << "delete: " << _ptr << endl;
                delete _ptr;
            }
        }
        
        // 模拟指针行为
        T& operator*() { return *_ptr; }
        T* operator->() { return _ptr; }
        
    private:
        T* _ptr;
    };
}

为什么这种设计不好?

cpp 复制代码
int main()
{
    bit::auto_ptr<int> ap1(new int(10));
    bit::auto_ptr<int> ap2(ap1);  // ap1被悬空
    
    // cout << *ap1 << endl;  // 崩溃!ap1已经是nullptr
    cout << *ap2 << endl;     // OK
    
    return 0;
}

拷贝后原对象悬空,这违反了拷贝语义的直觉,容易导致错误。

1.2 unique_ptr的实现

unique_ptr通过禁用拷贝来解决auto_ptr的问题。

核心思想:独占所有权,只支持移动

cpp 复制代码
namespace bit
{
    template<class T>
    class unique_ptr
    {
    public:
        explicit unique_ptr(T* ptr = nullptr)
            : _ptr(ptr)
        {}
        
        ~unique_ptr()
        {
            if (_ptr)
            {
                cout << "delete: " << _ptr << endl;
                delete _ptr;
            }
        }
        
        // 禁用拷贝
        unique_ptr(const unique_ptr<T>&) = delete;
        unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;
        
        // 支持移动
        unique_ptr(unique_ptr<T>&& up)
            : _ptr(up._ptr)
        {
            up._ptr = nullptr;
        }
        
        unique_ptr<T>& operator=(unique_ptr<T>&& up)
        {
            if (this != &up)
            {
                if (_ptr)
                    delete _ptr;
                
                _ptr = up._ptr;
                up._ptr = nullptr;
            }
            return *this;
        }
        
        // 模拟指针行为
        T& operator*() { return *_ptr; }
        T* operator->() { return _ptr; }
        
        T* get() const { return _ptr; }
        
    private:
        T* _ptr;
    };
}

使用示例

cpp 复制代码
int main()
{
    bit::unique_ptr<int> up1(new int(10));
    
    // 编译错误:拷贝被禁用
    // bit::unique_ptr<int> up2(up1);
    
    // OK:支持移动
    bit::unique_ptr<int> up3(std::move(up1));
    
    // 移动后up1为空,必须显式使用std::move
    // 这样就不容易出错
    
    return 0;
}

1.3 shared_ptr的实现

shared_ptr是最复杂的智能指针,核心是引用计数机制。

引用计数的设计难点

cpp 复制代码
// 错误的设计
template<class T>
class shared_ptr
{
private:
    T* _ptr;
    static int _count;  // 静态成员?所有对象共享一个计数!
};

这样不行!不同的资源需要不同的引用计数。

正确的设计:引用计数也在堆上

cpp 复制代码
#include <iostream>
using namespace std;

namespace bit
{
    template<class T>
    class shared_ptr
    {
    public:
        // 构造函数
        explicit shared_ptr(T* ptr = nullptr)
            : _ptr(ptr)
            , _pcount(nullptr)
        {
            if (_ptr)
            {
                _pcount = new int(1);  // 只有非空资源才分配计数
            }
            cout << "shared_ptr ctor: " << _ptr << endl;
        }

        // 拷贝构造:共享资源,增加引用计数
        shared_ptr(const shared_ptr<T>& sp)
            : _ptr(sp._ptr)
            , _pcount(sp._pcount)
        {
            if (_pcount)
            {
                ++(*_pcount);
            }
        }

        // 拷贝赋值
        shared_ptr<T>& operator=(const shared_ptr<T>& sp)
        {
            if (this != &sp)
            {
                // 先释放当前资源
                release();

                // 再共享新资源
                _ptr = sp._ptr;
                _pcount = sp._pcount;
                if (_pcount)
                {
                    ++(*_pcount);
                }
            }
            return *this;
        }

        // 析构函数
        ~shared_ptr()
        {
            release();
        }

        // 释放资源
        void release()
        {
            if (_pcount)
            {
                --(*_pcount);
                if (*_pcount == 0)
                {
                    cout << "delete: " << _ptr << endl;
                    delete _ptr;
                    delete _pcount;
                }
            }

            _ptr = nullptr;
            _pcount = nullptr;
        }

        // 模拟指针行为
        T& operator*() const
        {
            return *_ptr;
        }

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

        // 获取裸指针
        T* get() const
        {
            return _ptr;
        }

        // 获取引用计数
        int use_count() const
        {
            return _pcount ? *_pcount : 0;
        }

        // 判空
        explicit operator bool() const
        {
            return _ptr != nullptr;
        }

    private:
        T* _ptr;        // 被管理的资源
        int* _pcount;   // 堆上的引用计数
    };
}

关键点解析

  1. 引用计数在堆上:每个资源对应一个独立的计数
  2. 拷贝时增加计数:多个shared_ptr共享同一个计数
  3. 析构时减少计数:计数归零时释放资源
  4. 赋值运算符:先释放旧资源,再共享新资源

⚠️说明:这是一个教学版 shared_ptr,省略了控制块、weak_ptr 支持、线程安全等复杂细节,但完整体现了 "引用计数在堆上 +RAII 自动释放" 这一核心思想。标准库的实现会更复杂,但基本原理一致。

测试代码

cpp 复制代码
int main()
{
    bit::shared_ptr<int> sp1(new int(10));
    cout << "引用计数: " << sp1.use_count() << endl;  // 1
    
    {
        bit::shared_ptr<int> sp2(sp1);
        cout << "引用计数: " << sp1.use_count() << endl;  // 2
        cout << "引用计数: " << sp2.use_count() << endl;  // 2
        
        bit::shared_ptr<int> sp3 = sp2;
        cout << "引用计数: " << sp1.use_count() << endl;  // 3
    }
    
    cout << "引用计数: " << sp1.use_count() << endl;  // 1
    
    return 0;
}

1.4 支持自定义删除器

实际的shared_ptr还支持自定义删除器。

实现思路:使用function包装可调用对象

cpp 复制代码
#include <functional>

namespace bit
{
    template<class T>
    class shared_ptr
    {
    public:
        explicit shared_ptr(T* ptr = nullptr)
            : _ptr(ptr)
            , _pcount(ptr ? new int(1) : nullptr)   
            , _del([](T* p) { delete p; })         
        {}

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

        // 拷贝构造:共享资源,增加引用计数
        shared_ptr(const shared_ptr<T>& sp)
            : _ptr(sp._ptr)
            , _pcount(sp._pcount)
            , _del(sp._del)
        {
            if (_pcount)
            {
                ++(*_pcount);
            }
        }

        // 拷贝赋值
        shared_ptr<T>& operator=(const shared_ptr<T>& sp)
        {
            if (this != &sp)                       
            {
                release();

                _ptr = sp._ptr;
                _pcount = sp._pcount;
                _del = sp._del;

                if (_pcount)
                {
                    ++(*_pcount);
                }
            }
            return *this;
        }

        ~shared_ptr()
        {
            release();
        }

        void release()
        {
            if (_pcount)                           
            {
                --(*_pcount);
                if (*_pcount == 0)
                {
                    _del(_ptr);                     
                    delete _pcount;
                }
            }
            _ptr = nullptr;
            _pcount = nullptr;
        }

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

        int use_count() const
        {
            return _pcount ? *_pcount : 0;          
        }

        explicit operator bool() const
        {
            return _ptr != nullptr;
        }

    private:
        T* _ptr = nullptr;
        int* _pcount = nullptr;
		std::function<void(T*)> _del;
    };
}

使用自定义删除器

cpp 复制代码
int main()
{
    // 管理数组
    bit::shared_ptr<int> sp1(new int[10], [](int* p) {
        cout << "delete[] " << p << endl;
        delete[] p;
    });
    
    // 管理FILE*
    bit::shared_ptr<FILE> sp2(fopen("test.txt", "r"), [](FILE* fp) {
        cout << "fclose " << fp << endl;
        fclose(fp);
    });
    
    return 0;
}

⚠️说明:本文为了演示"shared_ptr 能携带自定义删除策略",把删除器作为成员存到了 shared_ptr 本体中。 但标准库

std::shared_ptr 的删除器存放在"控制块(control block)"里,多个 shared_ptr共享同一个控制块,因此删除器也只存一份;拷贝 shared_ptr 只增加引用计数,不会复制删除器对象


二、shared_ptr的循环引用问题

2.1 什么是循环引用

循环引用是shared_ptr最容易遇到的陷阱,会导致内存泄漏。

经典场景:双向链表

cpp 复制代码
struct ListNode
{
    int _data;
    shared_ptr<ListNode> _next;
    shared_ptr<ListNode> _prev;
    
    ListNode(int data = 0)
        : _data(data)
    {}
    
    ~ListNode()
    {
        cout << "~ListNode()" << endl;
    }
};

int main()
{
    shared_ptr<ListNode> n1(new ListNode(1));
    shared_ptr<ListNode> n2(new ListNode(2));
    
    cout << "n1引用计数: " << n1.use_count() << endl;  // 1
    cout << "n2引用计数: " << n2.use_count() << endl;  // 1
    
    // 建立双向连接
    n1->_next = n2;
    n2->_prev = n1;
    
    cout << "n1引用计数: " << n1.use_count() << endl;  // 2
    cout << "n2引用计数: " << n2.use_count() << endl;  // 2
    
    return 0;
}
// 程序结束,但~ListNode()永远不会被调用!

输出

bash 复制代码
n1引用计数: 1
n2引用计数: 1
n1引用计数: 2
n2引用计数: 2

注意:析构函数没有被调用!这就是内存泄漏。

2.2 循环引用的原理分析

让我们分析为什么会发生内存泄漏:

初始状态

bash 复制代码
n1 (引用计数1) → [节点1]
n2 (引用计数1) → [节点2]

建立连接后

bash 复制代码
n1 (引用计数1) → [节点1] ←─┐
                  ↓ _next   │ _prev
                [节点2] ─────┘
                  ↑
n2 (引用计数1) ───┘

节点1的引用计数:1(n1) + 1(_prev) = 2

节点2的引用计数:1(n2) + 1(_next) = 2

main函数结束时

  1. n1析构,节点1的引用计数:2 - 1 = 1
  2. n2析构,节点2的引用计数:2 - 1 = 1
  3. 节点1要释放,需要等_next析构
  4. _next是节点2的成员,节点2要释放才会析构_next
  5. 节点2要释放,需要等_prev析构
  6. _prev是节点1的成员,节点1要释放才会析构_prev
  7. 形成循环依赖,谁都不会释放!

逻辑上的死锁

bash 复制代码
节点1释放 ← 依赖 ← _next析构 ← 依赖 ← 节点2释放
   ↑                                      ↓
   └──── 依赖 ←── _prev析构 ←── 依赖 ──────┘

2.3 weak_ptr:打破循环引用

weak_ptr是专门设计来解决这个问题的。

weak_ptr的特点

  1. 不增加引用计数:绑定到shared_ptr时不影响计数
  2. 不管理资源:不参与资源的释放
  3. 可以检测资源是否有效:通过expired()检查

解决方案

cpp 复制代码
struct ListNode
{
    int _data;
    shared_ptr<ListNode> _next;
    weak_ptr<ListNode> _prev;  // 改用weak_ptr
    
    ListNode(int data = 0) : _data(data) {}
    
    ~ListNode()
    {
        cout << "~ListNode(" << _data << ")" << endl;
    }
};

int main()
{
    shared_ptr<ListNode> n1(new ListNode(1));
    shared_ptr<ListNode> n2(new ListNode(2));
    
    cout << "n1引用计数: " << n1.use_count() << endl;  // 1
    cout << "n2引用计数: " << n2.use_count() << endl;  // 1
    
    n1->_next = n2;
    n2->_prev = n1;  // weak_ptr绑定,不增加引用计数
    
    cout << "n1引用计数: " << n1.use_count() << endl;  // 1
    cout << "n2引用计数: " << n2.use_count() << endl;  // 2
    
    return 0;
}

输出

bash 复制代码
n1引用计数: 1
n2引用计数: 1
n1引用计数: 1
n2引用计数: 2
~ListNode(1)
~ListNode(2)

完美!析构函数被正确调用了。

原理分析

bash 复制代码
n1 (引用计数1) → [节点1]
                  ↓ _next
                [节点2] ─weak_ptr─> [节点1]
                  ↑
n2 (引用计数1) ───┘

节点1的引用计数:1(n1)(_prev不计数)

节点2的引用计数:1(n2) + 1(_next) = 2

当main结束:

  1. n1析构,节点1引用计数归零,节点1释放
  2. 节点1释放时,_next析构,节点2引用计数减1
  3. n2析构,节点2引用计数归零,节点2释放
  4. 成功打破循环!

2.4 weak_ptr的使用

基本操作

cpp 复制代码
int main()
{
    shared_ptr<int> sp1(new int(10));
    
    // weak_ptr绑定shared_ptr
    weak_ptr<int> wp1 = sp1;
    weak_ptr<int> wp2(sp1);
    
    // 检查是否过期
    cout << wp1.expired() << endl;  // 0 (false)
    
    // 获取引用计数
    cout << wp1.use_count() << endl;  // 1
    
    // wp不增加引用计数
    cout << sp1.use_count() << endl;  // 1
    
    return 0;
}

安全访问资源:lock()

weak_ptr不能直接访问资源(没有*和->运算符),需要通过lock()获取shared_ptr:

cpp 复制代码
int main()
{
    shared_ptr<string> sp1(new string("hello"));
    weak_ptr<string> wp = sp1;
    
    // 方式1:使用lock()获取shared_ptr
    if (auto sp2 = wp.lock())
    {
        cout << *sp2 << endl;  // OK
        *sp2 += " world";
        cout << *sp1 << endl;  // hello world
    }
    else
    {
        cout << "资源已释放" << endl;
    }
    
    return 0;
}

检测资源是否有效

cpp 复制代码
int main()
{
    weak_ptr<string> wp;
    
    {
        shared_ptr<string> sp(new string("test"));
        wp = sp;
        
        cout << "内层作用域" << endl;
        cout << "expired: " << wp.expired() << endl;  // 0
        cout << "use_count: " << wp.use_count() << endl;  // 1
    }
    
    cout << "外层作用域" << endl;
    cout << "expired: " << wp.expired() << endl;  // 1 (过期)
    cout << "use_count: " << wp.use_count() << endl;  // 0
    
    auto sp2 = wp.lock();
    if (!sp2)
    {
        cout << "资源已释放,lock返回空shared_ptr" << endl;
    }
    
    return 0;
}

2.5 weak_ptr的简单实现

cpp 复制代码
namespace bit
{
    template<class T>
    class weak_ptr
    {
    public:
        weak_ptr() : _ptr(nullptr) {}
        
        // 绑定shared_ptr
        weak_ptr(const shared_ptr<T>& sp)
            : _ptr(sp.get())
        {}
        
        weak_ptr<T>& operator=(const shared_ptr<T>& sp)
        {
            _ptr = sp.get();
            return *this;
        }
        
        // weak_ptr之间可以拷贝
        weak_ptr(const weak_ptr<T>& wp)
            : _ptr(wp._ptr)
        {}
        
        weak_ptr<T>& operator=(const weak_ptr<T>& wp)
        {
            _ptr = wp._ptr;
            return *this;
        }
        
        T* get() const { return _ptr; }
        
    private:
        T* _ptr;
    };
}

⚠️提示:真正的 weak_ptr 必须与 shared_ptr 共享同一个"控制块(control block)",通过控制块中的
weak_count/shared_count 才能实现 expired() / lock() 的正确语义。只保存 T*无法判断对象是否已析构,因此这里只是"演示接口形状",不是可用实现。


三、智能指针的线程安全

3.1 两个层面的线程安全

使用智能指针时,需要考虑两个层面的线程安全:

  1. 引用计数的线程安全:多线程同时拷贝/析构智能指针
  2. 指向对象的线程安全:多线程同时访问智能指针指向的对象

它们是两个独立的问题!

3.2 引用计数的线程安全

问题演示

cpp 复制代码
#include <thread>
#include <mutex>
#include <atomic>

struct AA
{
    int _a1 = 0;
    int _a2 = 0;
    
    ~AA()
    {
        cout << "~AA()" << endl;
    }
};

int main()
{
    bit::shared_ptr<AA> p(new AA);
    const size_t n = 100000;
    
    auto func = [&]()
    {
        for (size_t i = 0; i < n; ++i)
        {
            // 拷贝构造会++引用计数
            bit::shared_ptr<AA> copy(p);
            // copy析构会--引用计数
        }
    };
    
    thread t1(func);
    thread t2(func);
    
    t1.join();
    t2.join();
    
    cout << "引用计数: " << p.use_count() << endl;
    
    return 0;
}

可能的问题

  1. 程序崩溃:引用计数操作不是原子的
  2. 内存泄漏:AA对象没有被释放

解决方案:使用原子操作

cpp 复制代码
#include <atomic>

namespace bit
{
    template<class T>
    class shared_ptr
    {
    public:
        explicit shared_ptr(T* ptr = nullptr)
            : _ptr(ptr)
            , _pcount(ptr ? new std::atomic<int>(1) : nullptr) 
        {}

        // 拷贝构造:共享资源,原子递增计数
        shared_ptr(const shared_ptr<T>& sp)
            : _ptr(sp._ptr)
            , _pcount(sp._pcount)
        {
            if (_pcount)
            {
                _pcount->fetch_add(1, std::memory_order_relaxed); 
            }
        }

        // 拷贝赋值
        shared_ptr& operator=(const shared_ptr& sp)
        {
            if (this != &sp)
            {
                release();

                _ptr = sp._ptr;
                _pcount = sp._pcount;

                if (_pcount)
                {
                    _pcount->fetch_add(1, std::memory_order_relaxed);
                }
            }
            return *this;
        }

        ~shared_ptr()
        {
            release();
        }

        void release()
        {
            if (_pcount)
            {
                if (_pcount->fetch_sub(1, std::memory_order_acq_rel) == 1)
                {
                    delete _ptr;
                    delete _pcount;
                }
            }
            _ptr = nullptr;
            _pcount = nullptr;
        }

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

        int use_count() const
        {
            return _pcount ? _pcount->load(std::memory_order_relaxed) : 0;
        }

        explicit operator bool() const { return _ptr != nullptr; }

    private:
        T* _ptr = nullptr;
        std::atomic<int>* _pcount = nullptr; 
    };
}

标准库的实现

标准库的shared_ptr保证了引用计数操作的线程安全性,使用了原子操作或其他同步机制。

补充:标准库只保证对同一个控制块的引用计数增减是线程安全的;但如果多个线程同时读写同一个 shared_ptr
对象本身
(例如同时对同一个 shared_ptr 赋值/reset),仍需要外部同步。

3.3 指向对象的线程安全

shared_ptr不保证指向对象的线程安全!

cpp 复制代码
#include <thread>
#include <mutex>
#include <atomic>

int main()
{
    shared_ptr<AA> p(new AA);
    const size_t n = 100000;
    
    auto func = [&]()
    {
        for (size_t i = 0; i < n; ++i)
        {
            shared_ptr<AA> copy(p);
            
            // 多线程同时访问AA对象
            copy->_a1++;  // 数据竞争!
            copy->_a2++;  // 数据竞争!
        }
    };
    
    thread t1(func);
    thread t2(func);
    
    t1.join();
    t2.join();
    
    // 预期:200000,实际:基本小于200000
    cout << p->_a1 << endl;
    cout << p->_a2 << endl;
    
    return 0;
}

解决方案:使用互斥锁

cpp 复制代码
int main()
{
    shared_ptr<AA> p(new AA);
    const size_t n = 100000;
    mutex mtx;
    
    auto func = [&]()
    {
        for (size_t i = 0; i < n; ++i)
        {
            shared_ptr<AA> copy(p);
            
            {
                unique_lock<mutex> lock(mtx);
                copy->_a1++;
                copy->_a2++;
            }
        }
    };
    
    thread t1(func);
    thread t2(func);
    
    t1.join();
    t2.join();
    
    cout << p->_a1 << endl;  // 200000
    cout << p->_a2 << endl;  // 200000
    
    return 0;
}

总结

方面 是否安全 负责人
引用计数操作 shared_ptr保证
指向的对象 使用者负责

四、C++11与Boost的关系

4.1 Boost库简介

Boost库是什么?

  • C++标准库的扩展和补充
  • 高质量、可移植、开源的C++库集合
  • 很多C++11/14/17的特性都来自Boost

Boost与C++标准的关系

  • Boost社区为C++标准化提供参考实现
  • 很多Boost库被纳入C++标准
  • C++标准委员会成员参与Boost开发

4.2 智能指针的演进历史

C++98时代

  • 标准库:auto_ptr(唯一的智能指针)
  • Boost库:scoped_ptrshared_ptrweak_ptr

C++ TR1时代

  • 技术报告引入了Boost的shared_ptr
  • TR1不是正式标准,但被广泛支持

C++11时代

  • unique_ptr(对应Boost的scoped_ptr)
  • shared_ptr(参考Boost实现)
  • weak_ptr(参考Boost实现)
  • 废弃auto_ptr

对应关系

Boost C++11 说明
scoped_ptr unique_ptr 独占所有权
scoped_array unique_ptr<T[]> 数组版本
shared_ptr shared_ptr 共享所有权
shared_array shared_ptr<T[]> 数组版本
weak_ptr weak_ptr 弱引用

五、内存泄漏的检测与预防

5.1 什么是内存泄漏

定义

程序在运行过程中动态分配了内存,但在不再使用时未能正确释放

导致该内存无法再次被程序访问或使用 ,从而造成内存浪费,这种现象称为内存泄漏


内存泄漏 vs 内存溢出

  • 内存泄漏(Memory Leak)

    已分配的内存没有被释放,内存占用持续增加,但单次分配可能是成功的。

  • 内存溢出(Out Of Memory)

    程序在申请内存时,所需内存超过系统可提供的内存,导致分配失败甚至程序崩溃。

👉 二者关系:长期内存泄漏很容易最终导致内存溢出


常见原因

  1. new / malloc 后忘记 delete / free
  2. 异常或提前 return,导致释放代码未执行
  3. 智能指针 循环引用 (如 shared_ptr 相互引用)
  4. 指针重新赋值前未释放原内存
  5. 容器中存放裸指针,清空容器时未释放指针指向的内存

典型内存泄漏示例

cpp 复制代码
void MemoryLeak1()
{
    int* p = new int(10);
    // 函数结束前忘记 delete
}

再看一个更隐蔽的例子:

cpp 复制代码
void MemoryLeak2()
{
    int* p = new int(10);
    p = new int(20);  // 原来指向的内存丢失,发生泄漏
}

⚠️ 指针重新赋值前,如果不释放旧内存,就会造成泄漏。


5.2 内存泄漏的检测与预防

1️⃣ 事前预防(最重要)

  • 尽量避免使用裸指针,优先使用 RAII 思想
  • 使用 std::unique_ptrstd::shared_ptr
  • 明确资源的所有权和生命周期
  • 遵循"谁申请,谁释放"的原则

2️⃣ 事中检测

  • Code Review:重点检查资源的申请与释放是否成对
  • 使用静态分析工具(如 clang-tidy、cppcheck)
  • 编写单元测试,覆盖异常路径和边界条件

3️⃣ 事后查错

  • 使用内存检测工具定位泄漏位置

    • Linux:valgrind
    • Windows:Visual Studio 内存诊断工具
  • 观察程序运行过程中内存占用是否持续增长

  • 修复后进行回归测试,防止问题反复出现


完整示例:使用智能指针避免内存泄漏

cpp 复制代码
// 错误的做法
void BadCode()
{
    int* p1 = new int(10);
    int* p2 = new int(20);
    
    if (some_condition)
    {
        throw "error";  // p1 和 p2 未释放,发生内存泄漏
    }
    
    delete p1;
    delete p2;
}
cpp 复制代码
// 正确的做法
void GoodCode()
{
    auto p1 = std::make_unique<int>(10);
    auto p2 = std::make_unique<int>(20);
    
    if (some_condition)
    {
        throw "error";  // 栈展开时自动释放,无内存泄漏
    }
    
    // 不需要手动 delete
}

总结一句话

让对象管理资源,而不是让人去记得释放资源。


六、智能指针使用的注意事项

6.1 不要混用智能指针和裸指针

错误示例

cpp 复制代码
int main()
{
    int* p = new int(10);
    
    shared_ptr<int> sp1(p);
    shared_ptr<int> sp2(p);  // 危险!
    
    return 0;
}  // sp1和sp2都会delete p,导致double free

正确做法

cpp 复制代码
int main()
{
    shared_ptr<int> sp1(new int(10));
    shared_ptr<int> sp2(sp1);  // 正确:拷贝构造
    
    return 0;
}

6.2 不要用get()返回的指针构造新智能指针

错误示例

cpp 复制代码
int main()
{
    shared_ptr<int> sp1(new int(10));
    
    // 危险!两个独立的shared_ptr管理同一资源
    shared_ptr<int> sp2(sp1.get());
    
    return 0;
}  // double free

正确做法

cpp 复制代码
int main()
{
    shared_ptr<int> sp1(new int(10));
    shared_ptr<int> sp2 = sp1;  // 正确
    
    return 0;
}

6.3 小心移动后的对象

cpp 复制代码
int main()
{
    unique_ptr<int> up1(new int(10));
    unique_ptr<int> up2(std::move(up1));
    
    // 危险!up1已经是nullptr
    // *up1 = 20;  // 崩溃
    
    if (up1)  // 应该先检查
    {
        *up1 = 20;
    }
    
    return 0;
}

6.4 避免循环引用

cpp 复制代码
// 错误:循环引用
class Node
{
public:
    shared_ptr<Node> next;
    shared_ptr<Node> prev;  // 会导致内存泄漏
};

// 正确:使用weak_ptr打破循环
class Node
{
public:
    shared_ptr<Node> next;
    weak_ptr<Node> prev;  // 使用weak_ptr
};

6.5 智能指针不是万能的

不适合用智能指针的场景

  1. 需要自定义内存分配器
  2. 对性能要求极高的场景
  3. 需要与C接口交互
cpp 复制代码
// C接口
extern "C" void c_function(int* data);

void CallCFunction()
{
    shared_ptr<int> sp(new int(10));
    
    // 传递给C函数
    c_function(sp.get());  // OK,但要确保函数不会保存指针
}

七、总结与展望

7.1 全系列回顾

通过两篇文章,我们全面学习了C++智能指针:

第一篇:基础与使用

  • 智能指针的使用场景
  • RAII设计思想
  • auto_ptr、unique_ptr、shared_ptr的使用
  • 自定义删除器
  • make_shared等实用技巧

第二篇:原理与高级话题

  • auto_ptr、unique_ptr、shared_ptr的实现原理
  • 循环引用问题与weak_ptr解决方案
  • 智能指针的线程安全
  • C++11与Boost的关系
  • 内存泄漏的检测与预防

7.2 核心要点总结

智能指针的本质

智能指针是基于RAII思想设计的资源管理工具,它利用对象的生命周期自动管理动态资源。

三种智能指针的选择

场景 推荐 原因
单一所有权 unique_ptr 零开销,明确语义
共享所有权 shared_ptr 引用计数,自动释放
观察资源 weak_ptr 不增加引用计数

循环引用的解决

使用weak_ptr打破循环引用,它不增加引用计数,不参与资源管理。

线程安全

  • 引用计数操作是线程安全的(标准库保证)
  • 指向的对象不是线程安全的(需要自己保证)
  • 指针本身不是线程安全的(需要自己保证)

内存泄漏预防

  1. 优先使用智能指针
  2. 遵循RAII原则
  3. 定期使用检测工具
  4. 注意循环引用

7.3 实践建议

1. 默认使用智能指针

cpp 复制代码
// 推荐
auto ptr = make_unique<MyClass>();

// 不推荐(除非有特殊需求)
MyClass* ptr = new MyClass();

2. 优先使用unique_ptr

cpp 复制代码
// 如果不需要共享,使用unique_ptr
unique_ptr<Data> CreateData()
{
    return make_unique<Data>();
}

3. 必要时使用shared_ptr

cpp 复制代码
// 需要共享时才使用shared_ptr
class Image { /*...*/ };

class Button
{
    shared_ptr<Image> _icon;  // 多个按钮共享同一图标
};

4. 用weak_ptr解决循环引用

cpp 复制代码
// 双向关系中,一侧使用weak_ptr
class Parent;
class Child
{
    weak_ptr<Parent> _parent;  // 避免循环引用
};

5. 使用make_shared/make_unique

cpp 复制代码
// 推荐:一次内存分配
auto sp = make_shared<Data>(args);

// 不推荐:两次内存分配
shared_ptr<Data> sp(new Data(args));

7.4 继续学习

推荐阅读

  • 《Effective Modern C++》
  • 《C++ Primer》
  • 《More Effective C++》

深入主题

  • 自定义内存分配器
  • 智能指针与STL容器
  • 智能指针的性能优化
  • 引用计数的实现细节

实践项目

  • 实现一个完整的智能指针类库
  • 使用智能指针重构现有项目
  • 分析开源项目中的智能指针使用

八、常见问题解答

Q1: unique_ptr和shared_ptr如何选择?

A: 默认使用unique_ptr。只有在明确需要共享所有权时才使用shared_ptr。

Q2: 为什么要废弃auto_ptr?

A: auto_ptr的拷贝语义会导致被拷贝对象悬空,容易产生bug,unique_ptr通过禁止拷贝解决了这个问题。

Q3: weak_ptr有什么用?

A: 主要用于打破shared_ptr的循环引用,以及在不增加引用计数的情况下观察资源。

Q4: 智能指针有性能开销吗?

A: unique_ptr几乎零开销。shared_ptr有引用计数的开销,但通常可以接受。

Q5: 可以把this指针给智能指针管理吗?

A: 不能直接这样做!需要使用enable_shared_from_this机制(这是一个高级话题)。


通过这两篇文章的学习,我们全面掌握了C++智能指针的使用和原理。智能指针是现代C++的核心特性,它让内存管理变得简单和安全。希望这个系列对你有所帮助!

C++智能指针系列到此完结!感谢你的阅读,如果对你有帮助,记得点赞、收藏、分享!期待在评论区看到你的学习心得和使用经验!❤️

相关推荐
安科瑞刘鸿鹏172 小时前
当宿舍开始“提前预警”,用电安全会发生什么变化?
运维·服务器·网络·数据库
herinspace2 小时前
管家婆软件套接字服务器打不开怎么解决
运维·服务器·数据库
txzz88882 小时前
CentOS-Stream-10 搭建FTP服务器之虚拟用户访问(一)
linux·服务器·centos·ftp虚拟用户访问·vsftp
_OP_CHEN2 小时前
【算法基础篇】(三十九)数论之从质数判定到高效筛法:质数相关核心技能全解析
c++·算法·蓝桥杯·埃氏筛法·acm/icpc·筛质数·欧拉筛法
Pcr_C2 小时前
Qt事件循环深度解析与实战指南
开发语言·c++·qt·开源
汉克老师2 小时前
GESP2025年12月认证C++一级真题与解析(编程题2(手机电量显示))
c++·while循环·多分支结构
科技块儿2 小时前
企业网络安全管理:如何部署IP离线库进行内部设备监控与合规审计?
服务器·网络·tcp/ip
Tipriest_2 小时前
linux /etc/profile.d 目录介绍
linux·运维·服务器
山上三树2 小时前
codedump
linux·服务器