C++智能指针的使用及其原理

LVDS系列23:Xilinx Ultrascale系ISERDESE3原语(一) 当FIFO_RD_CLK与CLKDIV对齐时,也就是Latency With Late FIFO Empty情况时,延迟比前一种情况长一个FIFO_RD_CLK时钟周期,也就是在FIFO_RD_CLK上升沿后一点,CLKDIV才成功驱动完FIFO_EMPTY的变化,于是读侧要得到FIFO_EMPTY为0的情况则要等到下一个FIFO_RD_CLK的上升沿采集时;IS_CLK_INVERTED、IS_CLK_B_INVERTED设置CLK和CLK_B是否反转,一般情况下设置0; 阅读详情

本文从异常安全的血泪教训出发,一步步推演出智能指针的设计思路,把它们的原理、用法讲清楚。


目录


一、为什么需要智能指针

1.1 异常 + 裸指针 = 内存泄漏

先看一段看起来很正常的代码:

double Divide(int a, int b) {
    if (b == 0) {
        throw "Divide by zero condition!";  // 可能抛异常
    }
    return (double)a / (double)b;
}

void Func() {
    int* array1 = new int[10];   // ① 申请资源
    int* array2 = new int[10];   // ② 又申请资源(这里也可能抛异常!)

    int len, time;
    cin >> len >> time;
    cout << Divide(len, time) << endl;  // ③ Divide 可能抛异常

    // ④ 如果上面抛异常了,这两行根本执行不到!
    delete[] array1;
    delete[] array2;
}

这段代码中其实有很多问题:

  1. Divide 抛异常 → 跳过两个 delete → 内存泄漏
  2. new int[10] 本身也可能抛 std::bad_alloc → 如果 array2 的 new 失败,array1 也没释放
  3. 如果用 try/catch 补救,每多一个 new 就要多套一层 try/catch,非常麻烦

1.2 用 try/catch 挽救的结果

void Func() {
    int* array1 = new int[10];
    int* array2 = nullptr;

    try {
        array2 = new int[10];           // 如果这里抛异常……

        try {
            int len, time;
            cin >> len >> time;
            cout << Divide(len, time) << endl;  // 或者这里抛异常……
        }
        catch (...) {
            // 释放两个数组,再重新抛出
            delete[] array1;
            delete[] array2;
            throw;
        }
    }
    catch (...) {
        delete[] array1;     // array2 还没分配成功,只释放 array1
        throw;
    }

    delete[] array1;
    delete[] array2;
}

三个资源就套了两层 try/catch。十个资源呢?一百个呢?这就是 C 语言风格手动管理资源在异常面前的无力。


二、RAII:用对象的生命周期管理资源

2.1 核心思想

RAII(Resource Acquisition Is Initialization)是一种利用 C++ 对象生命周期来管理资源的惯用法:

  1. 构造时获取资源:把资源(内存、文件句柄、锁等)交给一个对象管理
  2. 析构时释放资源:对象离开作用域时自动调用析构函数,资源必然被释放
  3. 异常安全:栈展开时局部对象会被自动析构,资源不会泄漏

2.2 手搓一个最简智能指针

template<class T>
class SmartPtr {
public:
    // RAII 核心:构造函数获取资源
    SmartPtr(T* ptr) : _ptr(ptr) {}

    // RAII 核心:析构函数释放资源
    ~SmartPtr() {
        cout << "delete[] " << _ptr << endl;
        delete[] _ptr;
    }

    // 像指针一样访问资源
    T& operator*()  { return *_ptr; }
    T* operator->() { return _ptr; }
    T& operator[](size_t i) { return _ptr[i]; }

private:
    T* _ptr;
};

2.3 使用智能指针后的代码

代码变得清爽多了

void Func() {
    SmartPtr<int> sp1(new int[10]);
    SmartPtr<int> sp2(new int[10]);  // 即使这里抛异常,sp1 也会被正确析构
    SmartPtr<int> sp3(new int[10]);
    SmartPtr<int> sp4(new int[10]);

    int len, time;
    cin >> len >> time;
    cout << Divide(len, time) << endl;  // 抛异常也不怕

    // sp1~sp4 在函数退出(无论正常还是异常)时自动析构
    sp1[5] = 50;
    cout << sp1[5] << endl;
}

不再需要任何 try/catch 手动清理资源。就算 Divide 抛异常、栈展开退出,C++ 保证 sp1~sp4 的析构函数都会被调用,资源全部正确释放。


三、标准库智能指针家族速览

C++ 标准库在 <memory> 头文件中提供了四种智能指针:

智能指针版本核心特点建议
auto_ptrC++98拷贝时转移所有权,原指针悬空绝对不要用!已废弃
unique_ptrC++11独占所有权,不可拷贝,只可移动默认首选,不需要共享就用它
shared_ptrC++11共享所有权,引用计数,支持拷贝需要共享资源时使用
weak_ptrC++11不参与资源管理,绑定 shared_ptr 不增加引用计数解决循环引用问题

四、auto_ptr:C++98 的黑历史

auto_ptr 的拷贝语义是"管理权转移"——拷贝构造后,被拷贝对象被置空,变成悬空指针:

// auto_ptr 的危险行为
auto_ptr<Date> ap1(new Date);
auto_ptr<Date> ap2(ap1);   // 构造时,ap1 被置为 nullptr!

// ap1->_year++;            // 空指针访问,运行时报错!
// ap1 看起来是个正常的对象,但它内部管理的指针是空的
// 编译完全通过,运行时才崩——这是最坑的地方

问题:

  • 拷贝语义不符合直觉(我拷贝了一个东西,原件居然不能用了?)
  • 传给函数、放入容器都会触发隐式所有权转移
  • C++11 直接废弃了它,C++17 彻底移除

很多公司明令禁止使用 auto_ptr。对于auto_ptr的态度就是:别用,忘掉它。


五、unique_ptr:独占所有权的首选

5.1 基本用法

unique_ptr 直接禁止了拷贝,从编译器层面杜绝了 auto_ptr 的问题:

#include <memory>

struct Date {
    int _year, _month, _day;
    Date(int y = 1, int m = 1, int d = 1)
        : _year(y), _month(m), _day(d) {}
    ~Date() { cout << "~Date()" << endl; }
};

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

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

    unique_ptr<Date> up3(move(up1)); // 支持移动,但 up1 悬空

    return 0;
}

5.2 管理 new[] 数组

// 默认使用 delete 释放,管理 new[] 数组需要特化版本
unique_ptr<Date[]> up(new Date[5]);  // 析构时自动调用 delete[]

5.3 自定义删除器

unique_ptr 的删除器通过第二个模板参数指定:

// 方式一:仿函数
class Fclose {
public:
    void operator()(FILE* ptr) {
        cout << "fclose:" << ptr << endl;
        fclose(ptr);
    }
};

unique_ptr<FILE, Fclose> up1(fopen("test.txt", "r"));

// 方式二:lambda
auto fcloseFunc = [](FILE* ptr) { fclose(ptr); };
unique_ptr<FILE, decltype(fcloseFunc)> up2(fopen("test.txt", "r"), fcloseFunc);

unique_ptr 的删除器类型是模板参数的一部分,不同类型删除器的 unique_ptr 是不同的类型。


六、shared_ptr:共享所有权的引用计数

6.1 基本用法

shared_ptr的本质是多个智能指针共同管理一份资源。拷贝的时候,不会让原指针悬空,而是通过“引用计数器+1”的方式表示“有两个/多个指针共同管理这份资源”

shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1);     // 拷贝构造,引用计数 +1
shared_ptr<Date> sp3 = sp2;    // 赋值,引用计数 +1

cout << sp1.use_count() << endl;  // 输出 3——三个 shared_ptr 共同管理

sp1->_year++;
// sp1、sp2、sp3 指向同一个 Date 对象
cout << sp1->_year << endl;  // 2
cout << sp2->_year << endl;  // 2(同一个对象!)
cout << sp3->_year << endl;  // 2

6.2 make_shared

// 直接 new 构造
shared_ptr<Date> sp1(new Date(2024, 9, 11));

// make_shared:更高效(一次分配内存给对象和引用计数块)
shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
auto sp3 = make_shared<Date>(2024, 9, 11);  // auto 自动推导

为什么推荐 make_shared:

  • shared_ptr<Date> sp(new Date) 分两次分配内存:一次 Date,一次引用计数
  • make_shared<Date>() 一次分配一块连续内存,同时包含对象和引用计数,效率更高

6.3 operator bool:判空很方便

operator bool 是类型转换运算符,作用是允许对象隐式转换成 bool 类型,专门用来支持 if(sp)、while(sp)、!sp 这类判断写法。
当你写下 if (sp),编译器会自动调用 sp.operator bool()。 shared_ptr 的规则是:如果智能指针持有有效资源,返回 true;没有托管任何资源(空智能指针),返回 false。

shared_ptr<Date> sp;
if (sp)           // 隐式调用 operator bool()
    cout << "sp 管理着资源" << endl;
if (!sp)
    cout << "sp 是空的" << endl;

6.4 构造函数是 explicit 的

// shared_ptr<Date> sp = new Date(2024, 9, 11);  // 编译错误!
// 构造函数加了 explicit,禁止隐式转换
shared_ptr<Date> sp(new Date(2024, 9, 11));       // 正确:显式调用

6.5 shared_ptr 的自定义删除器

和 unique_ptr 不同,shared_ptr 的删除器通过构造函数参数传递:

// 管理 new[] 数组
shared_ptr<Date> sp1(new Date[5], [](Date* ptr) { delete[] ptr; });

// 管理 FILE*
shared_ptr<FILE> sp2(fopen("test.txt", "r"), Fclose());

shared_ptr<FILE> sp3(fopen("test.txt", "r"), [](FILE* ptr) {
    cout << "fclose:" << ptr << endl;
    fclose(ptr);
});

// 特化版本:管理数组更简单
shared_ptr<Date[]> sp4(new Date[5]);

shared_ptr 的删除器不是模板参数,不同类型删除器的 shared_ptr<T> 是同一个类型,可以互相赋值。


七、手搓 shared_ptr:从零实现引用计数

纸上得来终觉浅,绝知此事要躬行。下面我们一步一步拆解,把一个功能完整的 shared_ptr 造出来。

7.1 第一个问题:引用计数放哪里?

一个 shared_ptr<T> 对象管着两个东西:资源指针和引用计数。引用计数不能用普通 int 成员变量——每个 shared_ptr 对象都有自己的成员,互相独立,没法共享。也不能用 static int——那会让所有 shared_ptr 对象共用一个计数,不同资源之间全串了。

正确的做法是:每创建一份资源管理的起点,就在堆上 new 一个引用计数出来,所有共享这份资源的 shared_ptr 都指向它。

sp1 ──→  [_pcount → 1] ──→ [Date对象]
                          ↖ 堆上的引用计数
sp2 ──→  [_pcount 是同一个指针,值已变成 2]

一个 shared_ptr<T> 只存两个裸指针:

T* _ptr;           // 指向被管理的资源
atomic<int>* _pcount;  // 指向堆上的引用计数

7.2 构造:管理权的诞生

// 构造函数:接管一个裸指针,引用计数从 1 开始
explicit shared_ptr(T* ptr = nullptr)
    : _ptr(ptr)
    , _pcount(new atomic<int>(1))   // 堆上 new 出一个计数,初值为 1
{}

这里三个细节值得停下来想一想:

(1)为什么 explicit? 如果不加,shared_ptr<Date> sp = new Date(...) 就能编译通过,裸指针隐式变成智能指针极易误用。前面说过,标准库的实现同样加了 explicit。

(2)为什么 _pcount 要在堆上 new? 因为一个 shared_ptr 对象本身占的是栈空间,如果 _pcount 是栈上的普通成员 int _pcount,那么 sp1 和 sp2 各有各的 _pcount,彼此不互通。只有把它放在堆上,多个 shared_ptr 对象共享同一个 int*,才能联动。

(3)为什么是 atomic<int>? 多线程环境下,不同线程可能同时对同一个 shared_ptr 做拷贝或析构,这就涉及对引用计数的并发 ++/–。用 atomic<int> 保证这个操作是原子的,不用额外加锁。

7.3 拷贝构造:新成员入伙

// 拷贝构造:加入同一份资源的"管理小组"
shared_ptr(const shared_ptr<T>& sp)
    : _ptr(sp._ptr)        // 指向同一个资源
    , _pcount(sp._pcount)   // 指向同一份引用计数
{
    ++(*_pcount);           // 管理小组多了一个成员
}

拷贝构造做的事情非常简单:指针复制过去,引用计数 +1。不复制资源本身,这就是"共享"的含义。

7.4 析构:最后一个走的关灯

~shared_ptr() {
    // 先把自己的引用计数 -1
    // 如果减到 0,说明自己是最后一个管理者,负责释放一切
    if (--(*_pcount) == 0) {
        _del(_ptr);         // 用删除器释放资源
        delete _pcount;     // 引用计数本身也销毁
    }
}

这个逻辑是整个 shared_ptr 的灵魂:谁都不需要知道别人还在不在,只在自己走的时候看一眼——“我是最后一个吗?是的话我收拾。”

如果析构函数里不判断计数直接 delete,那第一个析构的 shared_ptr 就把资源删了,剩下的 shared_ptr 全变成悬空指针。引用计数正是为此而生。

7.5 拷贝赋值:最复杂的操作

赋值运算符是整段代码中最容易写错的地方,因为它要同时处理"离开旧资源"和"加入新资源"两件事,而且顺序不能错:

shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
    // 步骤 0:如果两个指针指向的是同一份资源,什么也不用做
    //         这里可以用 this != &sp 来判断,但不好
    //         因为两个不同的 shared_ptr 对象可能通过不同的路径管理同一份资源
    if (_ptr != sp._ptr) {
        // 步骤 1:先离开当前的"管理小组"
        if (--(*_pcount) == 0) {
            _del(_ptr);          // 自己是最后一个 → 释放资源
            delete _pcount;      // 释放引用计数
        }

        // 步骤 2:再加入新的"管理小组"
        _ptr = sp._ptr;
        _pcount = sp._pcount;
        ++(*_pcount);
    }
    return *this;
}

为什么用 _ptr != sp._ptr 而不是 this != &sp?

考虑这个场景:

shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1);    // sp1 和 sp2 管理同一份资源
sp1 = sp2;                    // 自己赋值给自己?不,this != &sp2 成立
                              // 但它们管理的是同一份资源!_ptr == sp2._ptr

sp1 和 sp2 是不同的对象(this != &sp2 为 true),但它们的 _ptr 指向同一份资源。如果只用 this != &sp 判断,赋值操作仍然会进入逻辑,先 --(*_pcount) 把计数减到 1,再 ++(*_pcount) 加回 2——虽然结果正确,但做了无谓的原子操作。

另一个常见坑:

shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(new Date);
sp1 = sp2;    // sp1 和 sp2 管理不同资源
              // sp1 原来的资源如果计数减到 0 就释放
              // sp2 的资源计数 +1

sp1 原来独自管理一个 Date,sp2 也是。赋值后 sp1 离开原来的,加入 sp2 的。原来的如果没别人管了(计数归零),顺带释放。

7.6 自定义删除器

默认情况下,shared_ptr 析构时用 delete 释放资源。但如果你管理的是 new[] 出来的数组,或者是 fopen 打开的文件,就需要告诉 shared_ptr 怎么正确释放:

template<class D>
shared_ptr(T* ptr, D del)
    : _ptr(ptr)
    , _pcount(new atomic<int>(1))
    , _del(del)          // 把删除器存起来
{}

删除器的类型是 function<void(T*)>,本质是一个能接收 T* 的可调用对象——可以是函数指针、仿函数、lambda:

function<void(T*)> _del = [](T* ptr) { delete ptr; };  // 默认删除器

有了自定义删除器,析构函数里就不能再写死 delete _ptr 了,而要统一调用 _del(_ptr):

~shared_ptr() {
    if (--(*_pcount) == 0) {
        _del(_ptr);          // 不管怎么删,由删除器决定
        delete _pcount;
    }
}

使用时的灵活性:

// 管理 new[] 数组
shared_ptr<Date> sp1(new Date[10], [](Date* ptr) { delete[] ptr; });

// 管理文件
shared_ptr<FILE> sp2(fopen("test.txt", "r"), [](FILE* ptr) { fclose(ptr); });

// 仿函数删除器
struct Fclose {
    void operator()(FILE* ptr) { fclose(ptr); }
};
shared_ptr<FILE> sp3(fopen("test.txt", "r"), Fclose());

unique_ptr vs shared_ptr 删除器的区别:unique_ptr 的删除器类型是模板参数的一部分(不同删除器 = 不同类型),而 shared_ptr 的删除器是构造函数参数(不同删除器仍是同一类型),所以 shared_ptr 可以放进容器而 unique_ptr 的灵活性稍差。

7.7 像指针一样使用

除了管理生命周期,智能指针还要能让用户像用裸指针一样访问资源:

T& operator*()  { return *_ptr; }   // *sp 得到资源的引用
T* operator->() { return _ptr; }    // sp-> 直接访问资源的成员

T* get() const { return _ptr; }      // 取出裸指针(谨慎使用)
int use_count() const { return *_pcount; }  // 查看当前有多少个管理者

八、weak_ptr 与循环引用

8.1 循环引用问题

看一个双向链表的节点结构:

struct ListNode {
    int _data;

    shared_ptr<ListNode> _next;
    shared_ptr<ListNode> _prev;

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

int main() {
    shared_ptr<ListNode> n1(new ListNode);
    shared_ptr<ListNode> n2(new ListNode);

    cout << n1.use_count() << endl;  // 1
    cout << n2.use_count() << endl;  // 1

    n1->_next = n2;   // n2 的引用计数变为 2
    n2->_prev = n1;   // n1 的引用计数变为 2

    cout << n1.use_count() << endl;  // 2
    cout << n2.use_count() << endl;  // 2

    return 0;
    // n1 析构,n1 的引用计数从 2 变 1(n2._prev 还指着)
    // n2 析构,n2 的引用计数从 2 变 1(n1._next 还指着)
    // 两个节点都没释放!~ListNode() 永远不会打印!
}

循环逻辑:

  1. 右边的节点什么时候释放?← 左边的 _next 析构后
  2. _next 什么时候析构?← 左边节点释放后
  3. 左边节点什么时候释放?← 右边的 _prev 析构后
  4. _prev 什么时候析构?← 右边节点释放后

这就形成了一个死循环:谁都不先释放,内存全部泄漏。

8.2 weak_ptr 破局

weak_ptr 的设计哲学很明确——绑定到 shared_ptr,但不增加引用计数。把它看作一个"旁观者",它可以观察资源还在不在,但不参与资源的生命周期管理。

struct ListNode {
    int _data;

    weak_ptr<ListNode> _next;    // 关键修改!从 shared_ptr 换成 weak_ptr
    weak_ptr<ListNode> _prev;

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

int main() {
    shared_ptr<ListNode> n1(new ListNode);   // n1 计数 = 1
    shared_ptr<ListNode> n2(new ListNode);   // n2 计数 = 1

    n1->_next = n2;   // weak_ptr 绑定 shared_ptr → n2 计数仍为 1!
    n2->_prev = n1;   // weak_ptr 绑定 shared_ptr → n1 计数仍为 1!

    cout << n1.use_count() << endl;  // 1 ← 不受影响
    cout << n2.use_count() << endl;  // 1 ← 不受影响

    return 0;
    // n2 析构:计数 1→0,释放 ListNode B!
    //   → B 的 _prev(weak_ptr)自动置空
    // n1 析构:计数 1→0,释放 ListNode A!
    //   → A 的 _next(weak_ptr)自动置空
    // 两次 ~ListNode(),干净利落。
}

weak_ptr 为什么不能直接管理资源?因为它不参与引用计数,如果允许 weak_ptr 直接 new 资源,资源什么时候释放就没人说得清了。

// weak_ptr 不支持直接管理资源
// weak_ptr<ListNode> wp(new ListNode);  // 编译错误!

// 只能从 shared_ptr 绑定
shared_ptr<ListNode> sp(new ListNode);
weak_ptr<ListNode> wp = sp;   // 正确

8.3 weak_ptr 的常规用法

shared_ptr<string> sp1 = make_shared<string>("111111");
shared_ptr<string> sp2(sp1);
weak_ptr<string> wp = sp1;          // 绑定,不增加计数

cout << wp.use_count() << endl;     // 2(sp1 和 sp2 各持 1)
cout << wp.expired() << endl;       // 0(false,资源还没释放)

// sp1 和 sp2 都转向其他资源后
sp1 = make_shared<string>("222222");
sp2 = make_shared<string>("333333");

cout << wp.expired() << endl;       // 1(true,原来的 string 已释放)

// 安全访问:lock() 返回一个 shared_ptr
wp = sp1;
auto sp3 = wp.lock();               // 返回管理资源的 shared_ptr
if (sp3) {
    *sp3 += "###";                   // 安全:sp3 在就不会被释放
    cout << *sp1 << endl;            // sp1 也能看到修改
}

weak_ptr 的定位:

  • 不支持 RAII:不管理资源,不参与构造/析构决策
  • 不重载 operator/operator->*:不能直接访问资源
  • expired():检查绑定的资源是否已被释放
  • lock():返回一个 shared_ptr,如果资源还在就能安全访问,否则返回空

九、shared_ptr 的线程安全

多线程编程中,shared_ptr 的线程安全性常常被误解。关键要分清楚两件事:引用计数的安全和数据的安全,它们是两个完全不同的层面。

9.1 引用计数层:shared_ptr 内部保证线程安全

// shared_ptr 的引用计数用 atomic<int> 实现
// 多个线程同时拷贝/析构 shared_ptr,计数的 ++/-- 是原子的
shared_ptr<AA> p(new AA);

// 线程1:反复拷贝 p
thread t1([&p]() {
    for (int i = 0; i < 100000; ++i) {
        shared_ptr<AA> copy(p);   // ++引用计数,原子操作
    }  // copy 析构,--引用计数,原子操作
});

// 线程2:也反复拷贝 p
thread t2([&p]() {
    for (int i = 0; i < 100000; ++i) {
        shared_ptr<AA> copy(p);   // 同时 ++/--,原子操作保证不出错
    }
});

t1.join();
t2.join();

// 引用计数最终仍是 1 —— 原子操作保证了精确性
cout << p.use_count() << endl;  // 1

9.2 数据层:使用者自己负责

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

int main() {
    shared_ptr<AA> p(new AA);
    const size_t n = 100000;
    mutex mtx;   // ← 这个锁 shared_ptr 不会替你加!

    auto func = [&]() {
        for (size_t i = 0; i < n; ++i) {
            shared_ptr<AA> copy(p);    // 引用计数:安全(atomic 保证)

            {
                unique_lock<mutex> lk(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
    cout << p.use_count() << endl; // 1
    return 0;
}

9.3 如果引用计数不用 atomic 会怎样?

这是最常见的错误——图省事用普通 int* 做引用计数:

// 错误示范:用 int* 而非 atomic<int>*
template<class T>
class shared_ptr_bad {
private:
    int* _pcount;   // 普通 int,不是 atomic<int>
};

// 多线程场景:
// 线程A:++(*_pcount)  →  读-改-写,不是原子的
// 线程B:--(*_pcount)  →  同时执行,数据竞争!
// 结果:计数少 1 → 导致资源提前释放 / 计数多 1 → 导致资源泄漏

两种后果都不堪设想:计数少 1 导致某个 shared_ptr 变成悬空指针,访问时崩溃;计数多 1 导致最后一个 shared_ptr 析构时以为还有人管着,跳过 delete,资源永久泄漏。

9.4 shared_ptr 对象本身的线程安全——容易被忽略的第三层

还有一个更容易踩的坑:多个线程同时修改同一个 shared_ptr 变量。

shared_ptr<int> p = make_shared<int>(42);

// 线程1:让 p 指向新资源
thread t1([&p]() {
    p = make_shared<int>(100);   // p 本身被修改!
});

// 线程2:拷贝 p
thread t2([&p]() {
    shared_ptr<int> copy = p;     // 同时读取 p!
});

// 危险!p 这个 shared_ptr 对象本身不是线程安全的
// 一个线程在写 p(赋值),另一个线程在读 p(拷贝)
// → 数据竞争!

总结三层线程安全问题:

层面问题谁负责解决方案
引用计数的读写多线程同时 ++/-- 计数shared_ptr 内部atomic<int>
被管理对象的数据多线程同时读写对象成员使用者外层加 mutex
shared_ptr 对象本身多线程同时修改同一个 sp 变量使用者外层加 mutex 或 atomic<shared_ptr<T>>(C++20)

记忆口诀:shared_ptr 保你引用计数不乱,但对象数据得自己锁,shared_ptr 对象本身也得自己保护。


十、内存泄漏

10.1 什么是内存泄漏

内存泄漏的本质是:程序分配了一块堆内存,但失去了指向它的所有指针,这块内存既不能使用也无法释放。

注意,内存泄漏不等于"物理内存消失"——泄漏的内存仍然被你的进程占据着,只是你找不到它了。

void leak_demo() {
    int* p = new int[100];   // 分配 100 个 int
    p = new int[200];        // 又分配 200 个 int,覆盖了 p
    // 原来的 100 个 int 没人指了 → 内存泄漏!
    // p 现在指向新分配的 200 个 int
    delete[] p;              // 只释放了 200 个
    // 100 个 int 永远找不回来了
}

10.2 内存泄漏的常见场景

场景一:忘记 delete

void func() {
    int* p = new int(42);
    // 函数结束,p 本身被销毁,但 *p 留在堆上
    // 没有任何指针指向它 → 泄漏
}

场景二:异常导致跳过 delete

void func() {
    int* p = new int[1000];
    risky_operation();  // 抛异常!
    delete[] p;         // 永远执行不到
}
// 这也正是智能指针要解决的核心问题

场景三:循环引用(shared_ptr 的唯一盲区)

// 两个 shared_ptr 互相指着 → 计数永远到不了 0 → 泄漏
// 详见第八章

场景四:容器里存裸指针

vector<int*> pointers;
pointers.push_back(new int(1));
pointers.push_back(new int(2));
// 容器销毁时,销毁的是指针本身,不是指针指向的内存!
// 每一个 new int 都会泄漏
// 应该用 vector<unique_ptr<int>> 或 vector<shared_ptr<int>>

10.3 内存泄漏的危害:取决于程序的生命周期

int main() {
    // 这个程序泄漏 1GB,但几秒后就结束了
    // 危害程度:★☆☆☆☆
    // 进程退出时,操作系统回收所有物理内存,泄漏的内存也一并回收
    char* ptr = new char[1024 * 1024 * 1024];
    cout << (void*)ptr << endl;
    return 0;
}
程序类型典型例子泄漏危害
一次性命令行工具图片转换脚本、数据导出低:几秒结束,OS 回收
桌面应用浏览器、IDE中:运行几小时内存占用越来越大,用户感知到卡顿
长期服务端数据库、Web 服务器、消息队列高:7×24 小时运行,每次泄漏一点,几天后内存耗尽崩溃
嵌入式/内核路由器固件、航天器控制致命:无法重启,泄漏 = 物理损坏或任务失败

10.4 检测工具

Linux:Valgrind(经典)

# 编译时加 -g 保留调试信息
g++ -g test.cpp -o test

# 运行内存检查
valgrind --leak-check=full ./test

# 输出示例:
# ==12345== LEAK SUMMARY:
# ==12345==    definitely lost: 400 bytes in 1 blocks
# ==12345==    indirectly lost: 0 bytes in 0 blocks
# ==12345==      possibly lost: 0 bytes in 0 blocks
# ==12345==    still reachable: 0 bytes in 0 blocks

Linux:AddressSanitizer(更现代,运行时开销更小)

# 编译时加 -fsanitize=address
g++ -g -fsanitize=address test.cpp -o test

# 直接运行,泄漏时自动报告
./test

# 会精确报告泄漏发生在哪个文件的哪一行

Windows:VLD(Visual Leak Detector)

// 在 main 所在文件最前面加上
#include <vld.h>

int main() {
    // 程序正常运行
    // 程序结束后,VLD 在输出窗口报告所有泄漏
}

10.5 防范策略——从代码到流程

策略方法适用阶段
RAII + 智能指针用 unique_ptr / shared_ptr 管理所有堆资源,禁止裸 new/delete编码阶段
代码审查每次 new 都必须在审查中标注对应的 delete 位置提交前
静态分析Clang-Tidy、Cppcheck 自动检测可疑模式CI 流程
动态检测Valgrind / ASan 跑一遍测试用例提测前
上线前扫描用 VLD 或类似工具对 Release 构建做最终检查发版前

最有效的策略只有一条:从源头杜绝裸 new 和裸 delete。 能用 make_unique / make_shared 就不要手动 new,能用 RAII 就不手动管理。这样绝大多数泄漏问题在编码阶段就被消灭了。


十一、总结

四大智能指针对比

auto_ptrunique_ptrshared_ptrweak_ptr
所有权独占(转移)独占共享不拥有
拷贝支持(危险)禁止支持(计数++)支持(不++)
移动支持支持支持支持
删除器不支持模板参数构造函数参数无
RAII是是是否
operator*是是是否
推荐度禁用!首选需要共享时打破循环引用

选择决策树

你需要智能指针管理资源吗?
│
├── 资源只需要一个所有者?
│   └── 用 unique_ptr ✓
│
├── 资源需要被多个对象共享?
│   ├── 用 shared_ptr
│   └── 注意:如果有互相引用,用 weak_ptr 打破循环
│
└── 只是观察资源,不参与生命周期?
    └── 用 weak_ptr

黄金法则:能用 unique_ptr 就不用 shared_ptr。独占所有权的语义更清晰,性能也更好(没有引用计数的原子操作开销)。


参考资料:C++ standard library - memory

workbench谐响应 定义材料 打开Engineering Data定义材料,在分析时不同部分选择不同材料 建模 打开Geometry建模,我举的例子很简单就一根光秃秃的轴 因为要加轴承,在轴上创建imprint face 因为要用point mass代替轴上原来的盘,在轴上盘的位置新建一个Plane,用Slice方法将轴从XYPlane3分成两段 施加边界条件以及谐响应分析 打开Model 1.在两段轴的交界面处,创建Point Mass,输入质量与转动惯量,位置在轴线上 2.mass point promote to. 阅读详情

相关推荐

手把手教你用VM实现欧姆龙PLC数据采集(含Fins协议配置详解)

本文详细介绍了如何使用VM(VisionMaster)实现与欧姆龙PLC的数据采集,重点解析了Fins协议(包括Fins TCP/UDP)的配置步骤。从网络环境搭建、PLC的IP地址设置,到VM端Fins通讯通道的详细配置与数据点映射,提供了完整的实战指南。文章还涵盖了数据收发解析、常见问题排查清单,帮助工程师快速完成视觉系统与PLC的稳定集成。

weixin_29171087的博客 188

【C++笔记】C++11智能指针的使用及其原理

哈喽,各位小伙伴大家好!上期我们讲了C++的异常机制。今天我们来讲一下C++智能指针的使用及其原理。话不多说,我们进入正题!向大厂冲锋什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存,一般是忘记释放或者发生异常释放程序未能执行导致的。内存泄漏并不是指内存在物理上的消失,而是是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。内存泄漏的危害:普通程序运行⼀会就结束了出现内存泄漏问题也不大,进程正常结束,页表的映射关系解除,物理内存也可以释放。

大白的编程日记. 2047

【GameFramework框架内置模块】8、文件系统(File System)

最近好忙,鸽了好久。不能颓废了,继续发力。今天分享的是GF框架的文件系统。首先,引用比较官方的说法:GF的FileSystem虚拟文件系统,使用类似磁盘存储的感念对零散的文件进行集中管理,优化资源加载时产生的内存分配,也可以对资源进行局部片段加载,极大的提升了资源加载的性能。通俗点讲就是,我这个模块就是加载磁盘文件的,优化加载时产生的内存分配。那么,它是怎么优化的呢?

优快云博客专家,Unity3D高级软件工程师,《Unity 3D从入门到实战》作者。 2448

C++智能指针使用及其原理

下面的程序我们可以观测到Divide会抛异常,但是由于前面我们使用了new,最后也进行了 delete,但是new也会抛异常,如果此时new直接抛异常那么不会有什么问题内存不会泄露 但是如果是后续的Divide抛异常,那么后面的代码就不会继续执行,也就是指向不到delete 那么就会导致内存泄露,此时面对这种情况我们就需要额外在调用Divide的地方套一层try/ catch进行捕获Divide异常然后将new出的资源释放了,然

2401_89521529的博客 911

2026.3.28:langgraph使用mysql进行持久化会话存储(短期记忆和长期记忆)

本文介绍了如何使用MySQL持久化存储LangGraph会话数据(短期和长期记忆)。首先安装必要的依赖包,然后通过Python代码实现将短期记忆写入MySQL数据库。主要步骤包括:1) 建立MySQL连接;2) 创建代理(agent)并配置检查点(checkpointer)和存储(store);3) 实现工具函数如计算器和天气查询;4) 使用中间件跟踪工具和模型调用次数。最后展示了如何从MySQL读取短期记忆数据,完成会话的持久化存储和检索功能。代码示例包含完整的实现细节,包括状态管理、消息处理和数据库操作

拥抱具身智能,昇腾Ascend C 734

群晖、飞牛NAS容器进入终端失败?一步步教你解决Docker Bash错误!(OCI runtime exec failed: exec failed)

群晖、飞牛NAS容器进入终端失败?一步步教你解决Docker Bash错误!(OCI runtime exec failed: exec failed)

XiaoqiangClub的博客 2987

C++ 智能指针的使用及其原理

下⾯程序中我们可以看到,new了以后,我们也delete了,但是因为抛异常导,后⾯的delete没有得到执⾏,所以就内存泄漏了,所以我们需要new以后捕获异常,捕获到异常后delete内存,再把异常抛出,但是因为new本⾝也可能抛异常,连续的两个new和下⾯的Divide都可能会抛异常,让我们处理起来很⿇烦。智能指针放到这样的场景⾥⾯就让问题简单多了。

2301_79875009的博客 987

基于机器学习的网络安全态势感知模型研究与实现

基于机器学习的网络安全态势感知模型研究与实现 1. 背景介绍 1.1 网络安全态势感知的重要性 在当今互联网时代,网络安全已经成为一个至关重要的话题。随着网络攻击手段的不断升级和演变,传统的被动防御方式已经难以满足日益

AI大模型应用之禅 1581

【c++】智能指针的使用及其原理

内存泄漏指因为疏忽或错误造成程序未能释放已经不再使⽤的内存,⼀般是忘记释 放或者发⽣异常释放程序未能执⾏导致的。内存泄漏并不是指内存在物理上的消失,⽽是应⽤程序分 配某段内存后,因为设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。

2401_88230254的博客 607

手把手教你学simulink(69.3)--毫米波5G/6G 系统场景实例设计并实现基于Simulink的毫米波多用户MIMO通信系统模型

毫米波频段(30 GHz到300 GHz)因其高带宽和高速率特性,成为5G和未来6G通信系统的理想选择。然而,毫米波信号的传播特性较差,容易受到大气吸收、雨衰、建筑物遮挡等因素的影响。为了克服这些挑战并提高通信容量,多用户MIMO(MU-MIMO, Multi-User Multiple-Input Multiple-Output)技术被广泛应用。多用户MIMO技术通过在基站和多个用户之间同时传输多个数据流,充分利用空间资源,从而显著提升系统容量和频谱效率。

MHD0815的博客 489

【1.2】Costas环的基本原理概述

Costas环是一种用于载波恢复的相干解调技术,可直接从BPSK等抑制载波信号中恢复本地载波并完成解调。其核心结构包含同相/正交双支路处理,通过混频、低通滤波后,利用I/Q支路信号乘积进行相位误差检测,经环路滤波器调节VCO输出频率,实现载波同步。课程将重点讲解Costas环的MATLAB仿真实现、系统性能分析及FPGA开发,包括信号产生、频偏模拟、滤波器设计等模块,并通过AWGN信道和不同频偏条件下的测试验证系统性能。该技术无需单独提取载波,在数字通信系统中具有重要应用价值。

fpga/matlab/simulink算法仿真工程 450

【C++进阶】智能指针的使用及其原理

本文介绍了C++智能指针的使用场景、设计思路及标准库实现。主要内容包括: 通过异常处理案例展示内存泄漏问题,说明智能指针的必要性; 提出RAII(资源获取即初始化)设计思想,利用对象生命周期管理资源; 设计简易智能指针类,实现资源自动释放和指针行为模拟; 分析智能指针拷贝问题,对比标准库的auto_ptr、unique_ptr等解决方案; 重点指出auto_ptr存在管理权转移缺陷,建议避免使用。 文章通过代码示例和图示,系统讲解了智能指针如何解决资源泄漏问题,并比较了不同智能指针的实现特点。

Linux、C++、图形软件开发等。不一定每次都能写出完美的代码,但会认真搞懂每一个问题。 7376

本地搭建 GitLab 14.1.0版本CI/CD

GitLab 镜像版本 gitlab/gitlab-ce:14.1.0-ce.0 GitLab Runner 镜像版本 gitlab/gitlab-runner:v14.1.0 搭建此套环境是在一台32G的windows电脑上安装的。 使用dokcer安装。 此外还涉及的软件Docker Desktop,WSL 安装GitLab 首先启动 dockcer,打开WSL 打开后 首先定义GitLab的持久化目录 export GITLAB_HOME=/srv/gitlab 然后运行安装GitLab的命令

知无涯 4489

【C++】智能指针的使用及其原理

智能指针是C++中管理动态内存的重要工具,通过RAII(资源获取即初始化)机制自动释放资源。文章分析了智能指针的使用场景,如异常安全处理,比较了C++标准库中的auto_ptr(已废弃)、unique_ptr(独占所有权)、shared_ptr(共享所有权)和weak_ptr(解决循环引用)四种智能指针。重点讲解了shared_ptr的引用计数原理及线程安全问题,并提供了智能指针的实现示例。文章还介绍了智能指针的删除器定制、循环引用问题及解决方案,最后讨论了内存泄漏的危害及检测方法。智能指针能有效避免内存泄

2401_82610555的博客 960

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

C++:智能指针的使用及其原理

C++:智能指针的使用及其原理

2401_88433210的博客 1247

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

C++笔记-智能指针的使用及其原理

可以看出,unique_ptr并不支持拷贝,如果拷贝就会报错,并且在这里我要提一点:unique_ptr是不支持通过=号来进行初始化的,其实上面的auto_ptr也不支持,原因就是如果用=来进行初始化的,这就相当于隐式类型转换,会产生临时对象,也就会去调用拷贝构造函数,而unique_ptr是不能拷贝的,所以不支持这种初始化。而lock的作用我们上面也说了,它会返回一个管理资源的shared_ptr,相当对多了一个shared_ptr智能指针来管理资源,相应的引用计数也会增加。

2301_80236968的博客 1622

【C++】C++11智能指针的使用及其原理

早年的 C++ 教材把 new/delete 写在第一章,结果 80% 的崩溃 dump 追溯到“野指针”“重复释放”“忘记释放”。资源管理本质是生命期管理,而生命期管理应该交给对象,而不是程序员的手指。智能指针就是把“资源 = 对象”这一 RAII 思想落地的官方工具箱。

2402_82668782的博客 986

【C++11】智能指针的使用及其原理

本文系统介绍了C++智能指针的使用与实现原理。首先通过内存泄漏案例展示了智能指针的必要性,分析了RAII设计思想在资源管理中的应用。详细讲解了标准库中的智能指针:auto_ptr(已废弃)、unique_ptr(独占所有权)、shared_ptr(共享所有权)和weak_ptr(解决循环引用)。文章重点剖析了shared_ptr的引用计数机制及其线程安全问题,并演示了如何通过weak_ptr解决循环引用导致的内存泄漏。最后介绍了智能指针的历史渊源(与Boost库的关系)以及内存泄漏的检测与防范方法。通过自定

weixin_58252863的博客 934

c++解决常见内存泄漏问题——智能指针的使用及其原理

在c/c++程序的开发中,一些项目需要向内存手动申请空间,而手动申请的空间在我们不用时,必须手动将其释放,先不说我们其他问题,这种特有的机制,容易让我们手动向内存申请空间而忘记释放的问题。再说其他问题,如某个局部函数内部申请了资源,在它的栈帧销毁之前抛出了异常,此局部函数立即被终止了,而我们申请的资源也没有释放,这就导致了内存泄漏。所以有人提出能否把我们申请的资源交给一个对象来管理,如果我们忘记释放资源让这个对象来释放。

qq_58761784的博客 6122

C++进阶(9)——智能指针的使用及其原理

本文探讨了智能指针的使用场景及实现原理。文章首先通过一个除零异常导致内存泄漏的例子,说明智能指针的必要性。随后介绍了RAII设计原则和智能指针的基本实现思路,包括auto_ptr、unique_ptr和shared_ptr三种主要类型,重点分析了shared_ptr的引用计数机制及其循环引用问题,并提出了weak_ptr的解决方案。文章还讨论了智能指针的线程安全问题和定制删除器功能,最后简要说明了C++11智能指针与Boost库的关系。

2301_80065652的博客 3330

【C++】智能指针的使用及其原理--详解

什么是内存泄漏内存泄漏就是程序写完之后,因为代码疏忽或者逻辑 bug,用完的堆内存没有正常释放。并不是物理内存凭空消失了,而是程序申请到一块内存之后,弄丢了这块内存的使用权,再也没办法把它还给系统,这块内存就被白白占用掉,造成内存浪费。内存泄漏的危害对于需要长时间跑的程序,内存泄漏的影响会被放大,像操作系统服务、后台常驻程序这类。内存一点点不断堆积,程序占用内存会越涨越高,运行响应变得越来越慢,严重的时候直接卡死崩溃。// 1.内存申请了忘记释放// 2.异常安全问题Func();

2501_93351213的博客 428
上一篇: C++异常处理完全指南
下一篇: C++栈与队列进阶实战
jinyishu_
博客等级 码龄1年 53粉丝 82原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值