解码智能指针

智能指针核心背景与概念

原始指针的痛点

C++ 中使用原始指针(raw pointer)管理动态资源(堆内存、文件描述符、互斥锁、数据库连接等)时,必须手动调用释放逻辑(如deleteclose等)。但以下场景会导致资源泄漏:

  • 函数提前return,跳过释放语句;
  • 程序抛出异常,释放语句未执行;
  • 程序员手动遗漏释放操作。

示例:原始指针的资源泄漏风险

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

class A {
    int size;
    char* p;
public:
    /**
     * @brief 构造函数:分配堆内存
     * @param s 内存大小,默认值1
     * @note 动态分配char类型数组,资源需在析构中释放
     */
    A(int s = 1) : size(s) {
        cout << "A构造:分配" << s << "字节内存" << endl;
        p = new char[s];
    }

    /**
     * @brief 析构函数:释放堆内存
     * @note 必须调用delete[],否则堆数组内存泄漏;若未执行析构,资源永久占用
     */
    ~A() {
        cout << "A析构:释放" << size << "字节内存" << endl;
        delete[] p;
    }

    /**
     * @brief 调整内存大小
     * @param newSize 新的内存大小
     * @note 先释放旧内存,再分配新内存,避免内存泄漏
     */
    void resize(int newSize) {
        size = newSize;
        delete[] p;
        p = new char[size];
    }

    /**
     * @brief 打印内存大小信息
     * @return void 无返回值
     */
    void info() { cout << "当前内存大小:" << size << endl; }
};

/**
 * @brief 原始指针管理资源的风险示例
 * @return void 无返回值
 * @note 若中间抛出异常/提前return,delete p不会执行,导致内存泄漏
 */
void someFunction() {
    A* p = new A(100); // 分配资源
    // ... 业务逻辑,可能抛出异常/提前return
    delete p; // 手动释放,无法保证一定执行
}

智能指针核心概念

智能指针是 C++ 标准库提供的类模板,核心特性:

  • 本质是栈对象(而非指针),封装原始指针;
  • 重载operator->operator*,支持像普通指针一样访问资源;
  • 栈对象离开作用域时自动调用析构函数,释放封装的资源;
  • 核心目的:自动化资源管理,杜绝资源泄漏。

废弃的 auto_ptr(C++17 移除)

auto_ptr 基本用法

auto_ptr 是早期独占式智能指针,能自动释放资源,但设计存在缺陷。

cpp 复制代码
#include <iostream>
#include <memory> 
// 智能指针头文件using namespace std;

/**
 * @brief auto_ptr基本使用示例
 * @return void 无返回值
 * @note auto_ptr是栈对象,函数退出时自动析构,调用A的析构释放资源
 */
void autoPtrDemo() {
    // 封装原始指针,auto_ptr接管A对象的资源管理权
    auto_ptr<A> ap(new A(20)); 
    
    // 重载operator->,等价于(*ap).info()
    ap->info(); // 输出:当前内存大小:20
    ap->resize(100); 
    ap->info(); // 输出:当前内存大小:100
} // ap离开作用域,析构时调用A的析构,释放内存

int main() {
    autoPtrDemo();
    return 0;
}
// 执行结果:
// A构造:分配20字节内存
// 当前内存大小:20
// 当前内存大小:100
// A析构:释放100字节内存

auto_ptr 的致命缺陷

auto_ptr 的拷贝构造 / 赋值运算符会转移资源所有权,原 auto_ptr 失效(内部原始指针置空),语法允许操作但逻辑错误,极易导致程序崩溃。

cpp 复制代码
/**
 * @brief auto_ptr拷贝/赋值的缺陷示例
 * @return void 无返回值
 * @note 拷贝构造/赋值后,原智能指针失效,解引用会触发空指针异常
 */
void autoPtrProblem() {
    auto_ptr<A> sp1(new A(20)); // sp1接管资源,引用A对象
    auto_ptr<A> sp2(sp1);       // 拷贝构造:sp1的所有权转移给sp2,sp1内部指针置空
    
    // sp1->info() 等价于 (*sp1).info(),但sp1已无资源,解引用空指针触发崩溃
    // sp1->info(); // 运行时异常/程序中断
    
    auto_ptr<A> sp3;
    sp3 = sp2; // 赋值操作:sp2的所有权转移给sp3,sp2内部指针置空
    // sp2->info(); // 同样触发崩溃
    
    sp3->resize(666); 
    sp3->info(); // 正常执行,输出:当前内存大小:666
}

auto_ptr 被废弃的原因

  • 语法支持拷贝 / 赋值,但逻辑上转移所有权,原指针失效,易踩坑;
  • C++11 引入unique_ptr替代 auto_ptr,C++17 正式移除 auto_ptr。

shared_ptr(共享式智能指针)

shared_ptr 核心原理

shared_ptr 是 C++11 引入的共享所有权智能指针 ,核心通过引用计数实现:

  • 每个 shared_ptr 管理的资源对应一个「引用计数」(全局 / 动态分配的计数器);
  • 新增 shared_ptr(拷贝 / 赋值),计数 + 1;
  • shared_ptr 析构时,计数 - 1;
  • 计数减至 0 时,调用资源的析构函数,释放资源。

shared_ptr 基本用法

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

/**
 * @brief shared_ptr基本使用示例
 * @return void 无返回值
 * @note 多个shared_ptr共享同一资源,引用计数保证资源仅释放一次
 */
void sharedPtrDemo() {
    // sp1接管A对象,引用计数=1
    shared_ptr<A> sp1(new A(30)); 
    
    // 拷贝构造:引用计数=2
    shared_ptr<A> sp2(sp1); 
    sp1->info(); // 正常,输出:当前内存大小:30
    sp2->info(); // 正常,输出:当前内存大小:30
    
    shared_ptr<A> sp3;
    sp3 = sp1; // 赋值:引用计数=3
    sp1->info(); // 正常,输出:当前内存大小:30
    sp2->info(); // 正常,输出:当前内存大小:30
    sp3->info(); // 正常,输出:当前内存大小:30
    
    // 修改资源,所有共享指针都可见
    sp1->resize(100); 
    sp1->info(); // 输出:当前内存大小:100
    sp2->info(); // 输出:当前内存大小:100
    sp3->info(); // 输出:当前内存大小:100
} // 函数退出,sp3、sp2、sp1依次析构:
  // sp3析构 → 计数=2;sp2析构 → 计数=1;sp1析构 → 计数=0 → 释放A对象

int main() {
    sharedPtrDemo();
    return 0;
}
// 执行结果:
// A构造:分配30字节内存
// 当前内存大小:30
// 当前内存大小:30
// 当前内存大小:30
// 当前内存大小:30
// 当前内存大小:30
// 当前内存大小:100
// 当前内存大小:100
// 当前内存大小:100
// A析构:释放100字节内存

shared_ptr 的常见问题与解决

问题 1:重复关联原始指针(重复释放)

多个 shared_ptr 直接关联同一个原始指针,会创建多个独立的引用计数,导致资源被多次释放(double free)。

cpp 复制代码
/**
 * @brief shared_ptr重复关联原始指针的错误示例
 * @return void 无返回值
 * @note sp1和sp2各自创建引用计数,析构时均尝试释放p,导致double free
 */
void sharedPtrDoubleFree() {
    A* p = new A(20); // 原始指针
    
    shared_ptr<A> sp1(p); // sp1关联p,引用计数=1
    shared_ptr<A> sp2(p); // sp2关联p,创建新的引用计数=1(而非复用sp1的计数)
    
} // sp2析构 → 计数=0 → 释放p;sp1析构 → 计数=0 → 再次释放p → 崩溃

// 执行结果:
// A构造:分配20字节内存
// A析构:释放20字节内存
// A析构:释放20字节内存
// free(): double free detected in tcache 2
// Aborted (core dumped)

解决方法:通过拷贝 / 赋值创建多个 shared_ptr,而非直接关联同一原始指针。

问题 2:循环引用(内存泄漏)

两个(或多个)shared_ptr 互相引用,导致引用计数无法减至 0,资源永久泄漏。

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

// 前向声明
class B;
class A {
public:
    int size;
    char* p;
    shared_ptr<B> spb; // 引用B的shared_ptr

    /**
     * @brief A类构造函数
     * @param s 内存大小
     * @note 分配堆内存,初始化shared_ptr<B>为空
     */
    A(int s) : size(s) {
        cout << "A构造:分配" << s << "字节内存" << endl;
        p = new char[s];
    }

    /**
     * @brief A类析构函数
     * @note 释放堆内存,若循环引用则不会执行
     */
    ~A() {
        cout << "A析构:释放" << size << "字节内存" << endl;
        delete[] p;
    }

    /**
     * @brief 打印A的大小信息
     * @return void 无返回值
     */
    void info() { cout << "A的大小:" << size << endl; }
};

class B {
public:
    int size;
    char* p;
    shared_ptr<A> spa; // 引用A的shared_ptr

    /**
     * @brief B类构造函数
     * @param s 内存大小
     * @note 分配堆内存,初始化shared_ptr<A>为空
     */
    B(int s) : size(s) {
        cout << "B构造:分配" << s << "字节内存" << endl;
        p = new char[s];
    }

    /**
     * @brief B类析构函数
     * @note 释放堆内存,若循环引用则不会执行
     */
    ~B() {
        cout << "B析构:释放" << size << "字节内存" << endl;
        delete[] p;
    }

    /**
     * @brief 打印B的大小信息
     * @return void 无返回值
     */
    void info() { cout << "B的大小:" << size << endl; }
};

/**
 * @brief shared_ptr循环引用的错误示例
 * @return void 无返回值
 * @note spa和spb互相引用,计数无法归0,析构不执行,内存泄漏
 */
void sharedPtrCycle() {
    shared_ptr<A> spa(new A(100)); // spa计数=1
    shared_ptr<B> spb(new B(200)); // spb计数=1

    spa->spb = spb; // spa的spb引用spb → spb计数=2
    spb->spa = spa; // spb的spa引用spa → spa计数=2

    spa->spb->info(); // 输出:B的大小:200
    spb->spa->info(); // 输出:A的大小:100
} // 函数退出:
  // spb析构 → 计数=1(spa->spb仍引用);spa析构 → 计数=1(spb->spa仍引用);
  // 计数未归0,A和B的析构不执行 → 内存泄漏

**解决方法:使用 weak_ptr(弱引用智能指针)**weak_ptr 是 shared_ptr 的「助手」,核心特性:

  • 只能通过 shared_ptr / 其他 weak_ptr 构造,不接管资源所有权;
  • 构造 / 析构不改变引用计数;
  • 无法直接访问资源,需通过lock()获取 shared_ptr 后访问;
  • 核心接口:
    • use_count():返回关联资源的引用计数;
    • expired():判断关联资源是否已释放(计数 = 0);
    • lock():返回指向资源的 shared_ptr(若资源未释放),否则返回空 shared_ptr。
cpp 复制代码
#include <iostream>
#include <memory>
using namespace std;

// 前向声明
class B;
class A {
public:
    int size;
    char* p;
    weak_ptr<B> spb; // 改为weak_ptr,不增加计数

    A(int s) : size(s) {
        cout << "A构造:分配" << s << "字节内存" << endl;
        p = new char[s];
    }

    ~A() {
        cout << "A析构:释放" << size << "字节内存" << endl;
        delete[] p;
    }

    void info() { cout << "A的大小:" << size << endl; }
};

class B {
public:
    int size;
    char* p;
    weak_ptr<A> spa; // 改为weak_ptr,不增加计数

    B(int s) : size(s) {
        cout << "B构造:分配" << s << "字节内存" << endl;
        p = new char[s];
    }

    ~B() {
        cout << "B析构:释放" << size << "字节内存" << endl;
        delete[] p;
    }

    void info() { cout << "B的大小:" << size << endl; }
};

/**
 * @brief weak_ptr解决循环引用的示例
 * @return void 无返回值
 * @note weak_ptr不增加引用计数,函数退出时计数归0,资源正常释放
 */
void weakPtrSolveCycle() {
    shared_ptr<A> spa(new A(100)); // spa计数=1
    shared_ptr<B> spb(new B(200)); // spb计数=1

    spa->spb = spb; // weak_ptr赋值,spb计数仍=1
    spb->spa = spa; // weak_ptr赋值,spa计数仍=1

    // 检查资源是否有效,再通过lock()获取shared_ptr访问资源
    if (!spa->spb.expired()) {
        shared_ptr<B> tmp_b = spa->spb.lock(); // tmp_b计数=2
        tmp_b->info(); // 输出:B的大小:200
    } // tmp_b析构 → spb计数=1

    if (!spb->spa.expired()) {
        shared_ptr<A> tmp_a = spb->spa.lock(); // tmp_a计数=2
        tmp_a->info(); // 输出:A的大小:100
    } // tmp_a析构 → spa计数=1
} // 函数退出:
  // spb析构 → 计数=0 → 释放B;spa析构 → 计数=0 → 释放A;
  // 无内存泄漏

int main() {
    weakPtrSolveCycle();
    return 0;
}
// 执行结果:
// A构造:分配100字节内存
// B构造:分配200字节内存
// B的大小:200
// A的大小:100
// B析构:释放200字节内存
// A析构:释放100字节内存

问题 3:多线程访问共享资源(线程安全)

shared_ptr 本身是线程安全的(引用计数的增减是原子操作),但共享资源的访问不是线程安全的,多线程并发读写会导致数据竞争。

cpp 复制代码
#include <iostream>
#include <memory>
#include <thread>
#include <mutex>
#include <unistd.h>
#include <cstdlib>
#include <ctime>
using namespace std;

class A {
public:
    int x;
    /**
     * @brief A类构造函数
     * @param x 初始化值,默认0
     * @note 初始化x,无动态资源
     */
    A(int x = 0) : x(x) {}

    /**
     * @brief 设置x的值
     * @param x 新值
     * @return void 无返回值
     */
    void setX(int x) { this->x = x; }
};

mutex m; // 全局互斥锁,保护共享资源访问

/**
 * @brief 线程1:判断x是否为偶数并打印
 * @param sp 共享的shared_ptr<A>
 * @return void 无返回值
 * @note 不加锁会导致读取x时被线程2修改,输出错误结果
 */
void routine1(shared_ptr<A> sp) {
    while (1) {
        m.lock(); // 加锁,保证读取x的原子性
        if (sp->x % 2 == 0) {
            cout << sp->x << "是偶数" << endl;
        }
        m.unlock(); // 解锁
        usleep(10 * 1000); // 休眠10ms,降低CPU占用
    }
}

/**
 * @brief 线程2:随机修改x的值
 * @param sp 共享的shared_ptr<A>
 * @return void 无返回值
 * @note 加锁,保证修改x的原子性
 */
void routine2(shared_ptr<A> sp) {
    srand(time(NULL)); // 初始化随机数种子
    while (1) {
        m.lock(); // 加锁
        sp->setX(rand() % 1000); // 随机设置0-999
        m.unlock(); // 解锁
    }
}

int main() {
    shared_ptr<A> sp1(new A); // 共享资源
    shared_ptr<A> sp2(sp1);

    // 创建线程,传递shared_ptr(拷贝,计数+1)
    thread t1(routine1, sp1);
    thread t2(routine2, sp2);

    t1.detach(); // 分离线程,后台运行
    t2.detach();
    pthread_exit(NULL); // 主线程退出,子线程继续
    return 0;
}
// 加锁后执行结果(无错误):
// 396是偶数
// 596是偶数
// 18是偶数
// 406是偶数

unique_ptr(独占式智能指针)

unique_ptr 核心特性

unique_ptr 是 C++11 引入的独占式智能指针,替代废弃的 auto_ptr,核心特性:

  • 独占资源所有权,同一时间只有一个 unique_ptr 能管理某个资源;
  • 禁止拷贝构造和赋值运算符(语法层面禁用,避免 auto_ptr 的坑);
  • 支持移动语义std::move),将资源所有权转移给另一个 unique_ptr;
  • 效率与原始指针接近,无引用计数开销。

unique_ptr 基本用法

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

/**
 * @brief unique_ptr基本使用示例
 * @return void 无返回值
 * @note unique_ptr禁止拷贝/赋值,仅允许移动语义,独占资源
 */
void uniquePtrDemo() {
    // up独占A对象的资源
    unique_ptr<A> up(new A(20)); 
    up->info(); // 输出:当前内存大小:20
    up->resize(200);
    up->info(); // 输出:当前内存大小:200

    // 错误:unique_ptr禁用拷贝构造
    // unique_ptr<A> up2(up);

    // 错误:unique_ptr禁用赋值运算符
    // unique_ptr<A> up3;
    // up3 = up;

    // 正确:移动语义,up的所有权转移给up4,up变为空
    unique_ptr<A> up4 = move(up);
    // up->info(); // up已空,调用崩溃
    up4->info(); // 输出:当前内存大小:200
} // up4离开作用域,析构释放A对象

int main() {
    uniquePtrDemo();
    return 0;
}
// 执行结果:
// A构造:分配20字节内存
// 当前内存大小:20
// 当前内存大小:200
// 当前内存大小:200
// A析构:释放200字节内存

手动实现 myUniquePtr(模拟 unique_ptr)

cpp 复制代码
#include <iostream>
#include <utility> // std::move
using namespace std;

/**
 * @brief 自定义独占式智能指针类模板
 * @tparam T 资源类型
 * @note 模拟unique_ptr核心特性:禁止拷贝/赋值,支持移动语义,自动释放资源
 */
template <typename T>
class myUniquePtr {
private:
    T* ptr; // 封装的原始指针

    // 禁用拷贝构造(私有+删除,语法层面禁止)
    myUniquePtr(const myUniquePtr&) = delete;
    // 禁用赋值运算符(私有+删除)
    myUniquePtr& operator=(const myUniquePtr&) = delete;

public:
    /**
     * @brief 构造函数:接管原始指针
     * @param p 原始指针,默认nullptr
     * @note 接管资源,后续由myUniquePtr管理释放
     */
    explicit myUniquePtr(T* p = nullptr) : ptr(p) {}

    /**
     * @brief 移动构造函数:转移资源所有权
     * @param other 待移动的myUniquePtr
     * @note 接管other的资源,other的指针置空
     */
    myUniquePtr(myUniquePtr&& other) noexcept {
        ptr = other.ptr;
        other.ptr = nullptr; // 原指针置空,避免重复释放
    }

    /**
     * @brief 移动赋值运算符:转移资源所有权
     * @param other 待移动的myUniquePtr
     * @return myUniquePtr& 自身引用
     * @note 先释放自身资源,再接管other的资源,other置空
     */
    myUniquePtr& operator=(myUniquePtr&& other) noexcept {
        if (this != &other) { // 避免自赋值
            delete ptr; // 释放当前资源
            ptr = other.ptr; // 接管other的资源
            other.ptr = nullptr; // other置空
        }
        return *this;
    }

    /**
     * @brief 析构函数:释放资源
     * @note 自动调用,释放封装的原始指针指向的资源
     */
    ~myUniquePtr() {
        delete ptr; // 释放资源(若ptr非空)
    }

    /**
     * @brief 重载operator->:支持指针式访问
     * @return T* 原始指针
     * @note 等价于(*this).ptr,用于访问资源的成员
     */
    T* operator->() const {
        return ptr;
    }

    /**
     * @brief 重载operator*:解引用访问资源
     * @return T& 资源的引用
     * @note 用于直接访问资源对象
     */
    T& operator*() const {
        return *ptr;
    }

    /**
     * @brief 获取原始指针
     * @return T* 封装的原始指针
     * @note 谨慎使用,避免手动管理资源导致冲突
     */
    T* get() const {
        return ptr;
    }

    /**
     * @brief 释放资源所有权(不释放资源)
     * @return T* 原始指针
     * @note 调用后myUniquePtr不再管理该资源,需手动释放
     */
    T* release() {
        T* tmp = ptr;
        ptr = nullptr;
        return tmp;
    }

    /**
     * @brief 重置资源
     * @param p 新的原始指针,默认nullptr
     * @return void 无返回值
     * @note 先释放当前资源,再接管新资源
     */
    void reset(T* p = nullptr) {
        delete ptr;
        ptr = p;
    }
};

/**
 * @brief 测试自定义myUniquePtr
 * @return void 无返回值
 * @note 验证独占性、移动语义、自动释放
 */
void testMyUniquePtr() {
    myUniquePtr<A> up(new A(50));
    up->info(); // 输出:当前内存大小:50

    // 移动构造
    myUniquePtr<A> up2(move(up));
    // up->info(); // up已空,崩溃
    up2->resize(150);
    up2->info(); // 输出:当前内存大小:150

    // 移动赋值
    myUniquePtr<A> up3;
    up3 = move(up2);
    up3->info(); // 输出:当前内存大小:150
} // up3析构 → 释放A对象

int main() {
    testMyUniquePtr();
    return 0;
}
// 执行结果:
// A构造:分配50字节内存
// 当前内存大小:50
// 当前内存大小:150
// 当前内存大小:150
// A析构:释放150字节内存

智能指针选型总结

智能指针 核心特性 适用场景 注意事项
auto_ptr 独占式,拷贝 / 赋值转移所有权 已废弃,不推荐使用 C++17 移除,易踩坑
shared_ptr 共享式,引用计数 多对象共享同一资源 避免循环引用(配合 weak_ptr)、不重复关联原始指针
weak_ptr 弱引用,不增减计数 解决 shared_ptr 循环引用 需通过 lock () 访问资源,先检查 expired ()
unique_ptr 独占式,禁用拷贝 / 赋值,支持移动 单个对象独占资源,替代 auto_ptr 效率最高,无计数开销,仅支持移动语义
相关推荐
Jeff-Nolan2 小时前
C++运算符重载
java·开发语言·c++
海上彼尚2 小时前
Go之路 - 7.go的函数
开发语言·后端·golang
神仙别闹2 小时前
基于QT(C++)实现(图形界面)连连看
java·c++·qt
Geoking.2 小时前
深度理解 Java synchronized —— 从原理到实战
java·开发语言
sailing-data2 小时前
【UI Qt】入门笔记
开发语言·qt·ui
NZT-482 小时前
C++基础笔记(三)链表list
c++·笔记·链表
Salt_07282 小时前
DAY32 类的定义和方法
开发语言·python·算法·机器学习
未来影子2 小时前
Java领域构建Agent新杀入一匹黑马(agentscope-java)
java·开发语言·python
不会写DN2 小时前
JavaScript call、apply、bind 方法解析
开发语言·前端·javascript·node.js