智能指针

基本概念

问题背景

在 C++ 中,手动管理资源(内存、文件描述符、互斥锁、数据库连接等)时,容易因异常、提前返回等意外情况导致资源泄漏。例如:

class A {
    int size;
    char *p;
public:
    A(int s=1):size(s){p = new char[s];}
    ~A(){delete [] p;} // 析构函数释放堆内存
};

void someFunction() {
    A *p = new A(100); // 分配资源
    // 若此处提前返回或抛出异常,delete p 无法执行,导致内存泄漏
    ...
    delete p; // 手动释放资源,可靠性低
}

这种直接指向资源、无自动管理能力的指针称为原始指针(raw pointer),其核心缺陷是:无法保证资源在任意场景下被妥善释放。

智能指针核心定义

智能指针是 C++ 提供的类模板,用于自动化管理资源,核心特征:

  • 本质是栈对象(局部变量),而非真正的指针;
  • 内部封装了指向资源的原始指针;
  • 离开作用域时,自动调用析构函数释放资源(无需手动操作);
  • 重载 operator->operator* 运算符,用法与普通指针一致。
智能指针内部结构
智能指针对象(栈上)
├─ 成员:原始指针 *p(指向堆/资源)
├─ 重载:operator->、operator*
└─ 析构函数:自动释放 *p 指向的资源
    ↓
资源(堆内存/文件描述符/互斥锁等)

智能指针分类详解

auto_ptr(C++17 已废弃)

基本用法

std::auto_ptr 是早期智能指针,核心作用是自动释放资源,需包含头文件 <memory>

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

class A {
    int size;
    char *p;
public:
    A(int s=1):size(s){cout<<"构造"<<endl; p = new char[s];}
    ~A(){cout<<"析构"<<endl; delete [] p;}
    void resize(int newSize) { // 重新分配堆内存
        size = newSize;
        delete [] p;
        p = new char[size];
    }
    void info(void){cout << size << endl;} // 输出大小
};

void someFunction(void) {
    auto_ptr<A> ap(new A(20)); // 封装原始指针
    ap->info();      // 等价于 (*ap).info(),输出 20
    ap->resize(100); // 操作资源
    ap->info();      // 输出 100
} // 离开作用域,ap 析构,自动释放 A 对象资源

// 执行结果:
// 构造
// 20
// 100
// 析构

核心缺陷(废弃原因)

auto_ptr 支持拷贝构造和赋值运算,但会导致原指针失去资源控制权(逻辑与语法矛盾),编译器无语法限制,极易踩坑:

void someFunction(void) {
    auto_ptr<A> sp1(new A(20));
    
    auto_ptr<A> sp2(sp1); // 拷贝构造:sp1 失去控制权
    sp1->info();          // 异常!sp1 已失效
    
    auto_ptr<A> sp3;
    sp3 = sp2;            // 赋值运算:sp2 失去控制权
    sp2->info();          // 异常!sp2 已失效
    
    sp3->resize(666);     // 仅最后一个指针有效
    sp3->info();          // 正常输出 666
}

结论auto_ptr 仅能单独使用,禁止拷贝构造和赋值操作,因设计缺陷被 C++17 废弃,不推荐使用。

shared_ptr(共享所有权智能指针)

shared_ptr 是 C++11 引入的核心智能指针,支持多指针共享同一资源,通过引用计数解决重复释放问题,语法与逻辑自洽。

基本用法

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

void someFunction(void) {
    shared_ptr<A> sp1(new A(30)); // 构造智能指针,引用计数=1
    
    shared_ptr<A> sp2(sp1);       // 拷贝构造,引用计数=2
    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 依次析构,引用计数降至 0,释放资源

核心原理:引用计数

shared_ptr 内部维护一个静态引用计数器,核心逻辑:

  1. 当智能指针关联资源时,计数器 +1
  2. 智能指针析构(离开作用域)时,计数器 -1
  3. 计数器降至 0 时,自动调用资源的析构函数释放资源。
shared_ptr 引用计数机制
资源(A 对象)
└─ 引用计数器:3
   ├─ shared_ptr sp1(关联)
   ├─ shared_ptr sp2(关联)
   └─ shared_ptr sp3(关联)

常见问题与解决方案

问题 1:重复关联导致重复释放

直接用同一原始指针构造多个 shared_ptr,会导致计数器独立,析构时重复释放资源:

void someFunction(void) {
    A *p = new A(20);
    shared_ptr<A> sp1(p); // 计数器=1
    shared_ptr<A> sp2(p); // 错误!计数器=1(独立计数)
    // 析构时 sp1 和 sp2 均释放 p,导致双重释放崩溃
}

解决方案:多个 shared_ptr 共享资源时,通过拷贝构造或赋值操作创建,而非直接用原始指针重复构造:

shared_ptr<A> sp1(new A(20));
shared_ptr<A> sp2(sp1); // 正确:拷贝构造,计数器=2
shared_ptr<A> sp3 = sp1; // 正确:赋值,计数器=3
问题 2:循环引用导致内存泄漏

两个类互相持有对方的 shared_ptr,会导致引用计数无法降至 0,资源永久无法释放:

class B; // 前置声明
class A {
public:
    shared_ptr<B> spb; // A 持有 B 的 shared_ptr
    A(int s){cout<<"A 构造"<<endl;}
    ~A(){cout<<"A 析构"<<endl;}
};
class B {
public:
    shared_ptr<A> spa; // B 持有 A 的 shared_ptr
    B(int s){cout<<"B 构造"<<endl;}
    ~B(){cout<<"B 析构"<<endl;}
};

void someFunction(void) {
    shared_ptr<A> spa(new A(100)); // 计数器=1
    shared_ptr<B> spb(new B(200)); // 计数器=1
    spa->spb = spb; // A 的 spb 关联 B,B 计数器=2
    spb->spa = spa; // B 的 spa 关联 A,A 计数器=2
} // 析构时:spa 计数器=1,spb 计数器=1,均不释放,内存泄漏
循环引用逻辑
shared_ptr spa → A 对象 → shared_ptr spb
                    ↑
                    ↓
shared_ptr spb → B 对象 → shared_ptr spa

解决方案:weak_ptr(弱引用指针)
weak_ptrshared_ptr 的辅助指针,核心特性:

  • 仅能通过 shared_ptr 或其他 weak_ptr 构造;
  • 构造/析构不影响引用计数;
  • 无法直接访问资源,需通过 lock() 转换为 shared_ptr 后操作。

修改代码如下:

class B;
class A {
public:
    weak_ptr<B> spb; // 改为 weak_ptr
    A(int s){cout<<"A 构造"<<endl;}
    ~A(){cout<<"A 析构"<<endl;}
};
class B {
public:
    weak_ptr<A> spa; // 改为 weak_ptr
    B(int s){cout<<"B 构造"<<endl;}
    ~B(){cout<<"B 析构"<<endl;}
};

void someFunction(void) {
    shared_ptr<A> spa(new A(100));
    shared_ptr<B> spb(new B(200));
    spa->spb = spb; // weak_ptr 不增加 B 的计数器(仍为1)
    spb->spa = spa; // weak_ptr 不增加 A 的计数器(仍为1)
    
    // 访问资源:先判断是否过期,再 lock() 转换
    if(!spa->spb.expired()) { // expired():判断资源是否释放
        spa->spb.lock()->info(); // lock():返回 shared_ptr,访问资源
    }
} // 析构时:spa 计数器=0(释放 A),spb 计数器=0(释放 B),无泄漏
weak_ptr 解决循环引用
shared_ptr spa → A 对象 → weak_ptr spb(虚线,不计数)
                    ↑
                    ↓
shared_ptr spb → B 对象 → weak_ptr spa(虚线,不计数)
问题 3:多线程访问失序

shared_ptr 本身的引用计数是线程安全的,但资源的访问并非线程安全,多线程并发读写会导致数据竞争:

#include <thread>
#include <mutex>
using namespace std;

class A {
public:
    int x;
    A(int x=0):x(x){}
    void setX(int x){this->x = x;}
};

mutex m; // 互斥锁

void routine1(shared_ptr<A> sp) {
    while(1) {
        m.lock(); // 加锁保证原子操作
        if (sp->x % 2 == 0)
            cout << sp->x << "是偶数" << endl;
        m.unlock(); // 解锁
        usleep(10*1000);
    }
}

void routine2(shared_ptr<A> sp) {
    srand(time(NULL));
    while(1) {
        m.lock(); // 加锁保证原子操作
        sp->setX(rand()%1000);
        m.unlock(); // 解锁
    }
}

int main() {
    shared_ptr<A> sp(new A);
    thread t1(routine1, sp);
    thread t2(routine2, sp);
    t1.detach();
    t2.detach();
    pthread_exit(NULL);
}

解决方案:对共享资源的访问添加互斥锁(mutex),保证同一时间只有一个线程操作资源。

unique_ptr(独占所有权智能指针)

unique_ptr 是 C++11 引入的轻量级智能指针,核心特性:资源独占,禁止拷贝构造和赋值操作,性能优于 shared_ptr

基本用法

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

void someFunction(void) {
    unique_ptr<A> up(new A(20)); // 独占资源
    up->info(); // 正常输出 20
    up->resize(200);
    up->info(); // 正常输出 200
    
    // 错误!禁止拷贝构造
    // unique_ptr<A> up2(up);
    // 错误!禁止赋值操作
    // unique_ptr<A> up3; up3 = up;
} // 离开作用域,up 析构,释放资源

核心原理:屏蔽拷贝与赋值

unique_ptr 通过 = delete 显式删除拷贝构造和赋值运算符,从语法上禁止资源共享:

template <typename T>
class myUniquePtr {
    T *p;
public:
    myUniquePtr(T *p=nullptr):p(p){}
    ~myUniquePtr(){delete p;} // 析构释放资源
    
    // 显式删除拷贝构造和赋值运算符
    myUniquePtr(const myUniquePtr &r) = delete;
    myUniquePtr &operator=(const myUniquePtr &r) = delete;
    
    // 重载运算符,支持指针用法
    T *operator->(){return p;}
    T &operator*(){return *p;}
    
    // 辅助接口
    T *release() { // 释放所有权,返回原始指针
        T *temp = p;
        p = nullptr;
        return temp;
    }
    void reset(T *newP=nullptr) { // 重置资源
        delete p;
        p = newP;
    }
};

拓展:移动语义(C++11+)

unique_ptr 虽禁止拷贝,但支持移动语义(std::move),可将资源所有权转移给另一个 unique_ptr

void someFunction(void) {
    unique_ptr<A> up1(new A(20));
    unique_ptr<A> up2 = move(up1); // 移动语义:up1 失去所有权,up2 独占
    up2->info(); // 正常输出 20
    // up1->info(); 错误!up1 已失效
}

拓展(新增)

智能指针的选择策略

场景 推荐智能指针 核心原因
资源独占,无需共享 unique_ptr 性能最优,语法禁止共享,无引用计数开销
资源需多指针共享 shared_ptr + weak_ptr 支持共享所有权,weak_ptr 解决循环引用
兼容旧代码(C++17 前) 无(避免 auto_ptr) auto_ptr 设计缺陷,已废弃

进阶技巧

使用 make_shared 构造 shared_ptr(推荐)

std::make_shared 是构造 shared_ptr 的更安全方式,避免原始指针暴露,且内存分配更高效(一次分配资源和计数器):

// 推荐:make_shared 构造
shared_ptr<A> sp = make_shared<A>(20);
// 不推荐:直接用原始指针构造
shared_ptr<A> sp(new A(20));

自定义删除器

默认情况下,智能指针使用 delete/delete[] 释放资源,可自定义删除器处理特殊资源(如数组、文件描述符):

// 示例:管理数组(默认 delete 不适用数组,需自定义删除器)
shared_ptr<int> sp(new int[10], [](int *p){delete[] p;});
// 示例:管理文件描述符
#include <fcntl.h>
#include <unistd.h>
shared_ptr<int> fd_ptr(new int(open("test.txt", O_RDONLY)), 
                       [](int *fd){close(*fd); delete fd;});

weak_ptr 的其他接口

  • use_count():返回关联资源的引用计数(仅作参考,非原子操作);
  • lock():若资源未释放,返回非空 shared_ptr;否则返回空 shared_ptr
  • reset():解除与资源的关联。

线程安全性补充

  • shared_ptr引用计数是线程安全的(原子操作);
  • shared_ptr 指向的资源并非线程安全,需手动加锁保护;
  • unique_ptr 无共享场景,线程安全取决于资源的访问方式。

posted @ 2025-12-29 08:57  Jaklin  阅读(2)  评论(0)    收藏  举报