C++ 智能指针

C++ 智能指针(Smart Pointer)是 C++11 引入的用于自动管理动态内存的模板类,其核心作用是通过RAII(资源获取即初始化)机制,在智能指针生命周期结束时自动释放所管理的内存,从而避免传统裸指针(Raw Pointer)可能导致的内存泄漏、重复释放、悬垂指针等问题。

智能指针通过 RAII(Resource Acquisition Is Initialization) idiom 来解决这些问题:

  • 获取资源即初始化:在构造函数中获取资源(分配内存)。
  • 释放资源即析构:在析构函数中自动释放资源。只要智能指针对象超出作用域,无论是因为正常执行还是异常,其析构函数都会被调用,从而保证资源被释放。

所有智能指针都定义在 <memory> 头文件中。

1、std::unique_ptr - 独占所有权指针

std::unique_ptr独占式智能指针,其管理的资源只能被一个 unique_ptr 拥有不允许拷贝(避免多个指针同时管理同一资源),但允许移动(转移所有权)。

1.1 基本用法

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

class MyClass {
public:
    MyClass(int id) : id(id) {
        cout << "MyClass(" << id << ") 构造" << endl;
    }
    ~MyClass() {
        cout << "MyClass(" << id << ") 析构" << endl;
    }
    void print() {
        cout << "MyClass id: " << id << endl;
    }
private:
    int id;
};

int main() {
    // 1. 创建 unique_ptr(管理动态对象)
    unique_ptr<MyClass> ptr1(new MyClass(1)); // 直接初始化(不推荐,可能抛异常)
    auto ptr2 = make_unique<MyClass>(2);     // 推荐:make_unique(更安全,避免内存泄漏)

    // 2. 访问资源(重载 * 和 ->)
    (*ptr1).print();  // 等价于 ptr1->print()
    ptr2->print();

    // 3. 转移所有权(只能通过 move,原指针变为空)
    unique_ptr<MyClass> ptr3 = move(ptr1); // ptr1 失去所有权,变为空
    if (ptr1 == nullptr) {
        cout << "ptr1 已为空" << endl;
    }
    ptr3->print(); // 仍可访问资源

    // 4. 主动释放资源(reset())
    ptr3.reset(); // 释放资源,ptr3 变为空
    if (ptr3 == nullptr) {
        cout << "ptr3 已释放资源" << endl;
    }

    return 0; // ptr2 离开作用域,自动释放资源
}

输出结果

MyClass(1) 构造
MyClass(2) 构造
MyClass id: 1
MyClass id: 2
ptr1 已为空
MyClass id: 1
MyClass(1) 析构
ptr3 已释放资源
MyClass(2) 析构

1.2 注意事项

  • 推荐使用 make_uniquemake_unique<T>(args) 直接在内部构造对象,避免裸 new 可能导致的异常安全问题(如 new 后未传给智能指针前抛异常,导致内存泄漏)。
  • 禁止拷贝unique_ptr 的拷贝构造和拷贝赋值被删除(= delete),只能通过 std::move 转移所有权:
    unique_ptr<MyClass> ptr1 = make_unique<MyClass>(1);
    // unique_ptr<MyClass> ptr2 = ptr1; // 编译错误:禁止拷贝
    unique_ptr<MyClass> ptr2 = move(ptr1); // 正确:转移所有权
    
  • 数组支持unique_ptr 可管理动态数组(自动调用 delete[]):
    auto arr_ptr = make_unique<int[]>(5); // 管理包含5个int的数组
    arr_ptr[0] = 10; // 支持下标访问
    
  • 定制删除器:默认使用 delete 释放资源,可自定义删除器(如释放文件指针、网络连接等):
    // 自定义删除器(释放文件指针)
    auto file_deleter = [](FILE* f) {
        if (f) {
            fclose(f);
            cout << "文件已关闭" << endl;
        }
    };
    
    // 创建带自定义删除器的 unique_ptr
    unique_ptr<FILE, decltype(file_deleter)> file_ptr(fopen("test.txt", "w"), file_deleter);
    

1.3 应用场景

  • 首选用于管理动态生命周期资源。
  • 作为类的成员变量,表示独占资源。
  • 在函数中动态创建对象并返回所有权(编译器会进行返回值优化 RVO,可能连移动都不需要)。
    std::unique_ptr<MyClass> createObject() {
        return std::make_unique<MyClass>();
    }
    auto obj = createObject();
    

2、std::shared_ptr - 共享所有权指针

多个 shared_ptr 可以共享同一个对象的所有权。通过引用计数机制来跟踪有多少个 shared_ptr 指向同一个对象。当最后一个 shared_ptr 被销毁时,对象才会被删除。

2.1 基本使用

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

int main() {
    // 1. 创建 shared_ptr(推荐用 make_shared)
    shared_ptr<MyClass> ptr1 = make_shared<MyClass>(1);
    cout << "引用计数: " << ptr1.use_count() << endl; // 1

    // 2. 拷贝 shared_ptr(引用计数 +1)
    shared_ptr<MyClass> ptr2 = ptr1; // 拷贝,计数变为 2
    cout << "ptr1 计数: " << ptr1.use_count() << endl; // 2
    cout << "ptr2 计数: " << ptr2.use_count() << endl; // 2

    // 3. 转移部分指针的所有权(计数不变)
    shared_ptr<MyClass> ptr3 = move(ptr1); // ptr1 变为空,计数仍为 2(ptr2 和 ptr3 共享)
    cout << "ptr1 是否为空: " << (ptr1 == nullptr ? "是" : "否") << endl; // 是
    cout << "ptr3 计数: " << ptr3.use_count() << endl; // 2

    // 4. 释放部分指针(计数 -1)
    ptr2.reset(); // ptr2 释放,计数变为 1
    cout << "ptr3 计数: " << ptr3.use_count() << endl; // 1

    return 0; // ptr3 离开作用域,计数变为 0,资源释放
}

输出结果

MyClass(1) 构造
引用计数: 1
ptr1 计数: 2
ptr2 计数: 2
ptr1 是否为空: 是
ptr3 计数: 2
ptr3 计数: 1
MyClass(1) 析构

2.2 注意事项

  • 推荐使用 make_sharedmake_shared<T>(args) 一次性分配对象和引用计数的内存,效率高于 shared_ptr<T>(new T(args))(后者可能分配两次内存)。
  • 循环引用问题:两个 shared_ptr 互相引用会导致引用计数无法归零,造成内存泄漏(需用 weak_ptr 解决):
    class B; // 前向声明
    class A {
    public:
        shared_ptr<B> b_ptr; // A 持有 B 的 shared_ptr
        ~A() { cout << "A 析构" << endl; }
    };
    class B {
    public:
        shared_ptr<A> a_ptr; // B 持有 A 的 shared_ptr
        ~B() { cout << "B 析构" << endl; }
    };
    
    int main() {
        auto a = make_shared<A>();
        auto b = make_shared<B>();
        a->b_ptr = b; // 互相引用
        b->a_ptr = a;
        // 离开作用域时,a和b的引用计数均为1(互相持有),无法释放,导致内存泄漏
        return 0;
    }
    
    输出结果(无析构输出,内存泄漏):
    (无任何析构信息,A和B对象未释放)
    
  • 定制删除器:与 unique_ptr 类似,可自定义删除器(所有共享该资源的 shared_ptr 需使用相同删除器):
    // 自定义删除器(释放数组)
    auto arr_deleter = [](int* p) {
        delete[] p;
        cout << "数组已释放" << endl;
    };
    
    shared_ptr<int> arr_ptr(new int[5], arr_deleter);
    

2.3 应用场景

  • 需要多个指针共享同一个对象时。
  • 需要将指针存入标准容器时(unique_ptr C++11 不能直接存入容器,C++17 可以,但语义仍是独占)。
  • 实现缓存的场景。

3、std::weak_ptr - 弱引用指针

std::weak_ptr 是一种弱引用智能指针,它指向 shared_ptr 管理的资源,但不增加引用计数,主要用于解决 shared_ptr 的循环引用问题

3.1 基本使用

struct B;
struct A {
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyed\n"; }
};
struct B {
    std::shared_ptr<A> a_ptr; // 如果这里是shared_ptr,会产生循环引用
    ~B() { std::cout << "B destroyed\n"; }
};

// 循环引用导致内存泄漏:
{
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b_ptr = b; // b的引用计数为2
    b->a_ptr = a; // a的引用计数为2
} // 离开作用域,a和b的引用计数都减为1,永远不会变为0,对象无法被销毁!

// 使用weak_ptr打破循环引用:
struct B;
struct A {
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyed\n"; }
};
struct B {
    std::weak_ptr<A> a_weak_ptr; // 使用weak_ptr而不是shared_ptr
    ~B() { std::cout << "B destroyed\n"; }
};

{
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b_ptr = b;
    b->a_weak_ptr = a; // 这里不会增加a的引用计数!(a的计数仍为1)
} // 离开作用域,a的计数变为0,先被销毁。然后b的计数变为0,也被销毁。
// 输出: A destroyed \n B destroyed

3.2 注意事项

  • 不能直接访问资源weak_ptr 没有重载 *->,必须通过 lock() 获取 shared_ptr 后才能访问资源(避免访问已释放的资源)。
  • 检查资源有效性expired() 方法可判断资源是否已释放(true 表示已释放):
    weak_ptr<MyClass> wp;
    {
        auto sp = make_shared<MyClass>(1);
        wp = sp; // wp 指向 sp 管理的资源
        cout << "资源是否有效: " << (!wp.expired() ? "是" : "否") << endl; // 是
    } // sp 销毁,资源释放
    cout << "资源是否有效: " << (!wp.expired() ? "是" : "否") << endl; // 否
    

3.3 适用场景

  • 打破 shared_ptr 的循环引用
  • 实现缓存观察者模式等,观察者不需要拥有被观察对象的所有权。

4、常见问题

  1. std::unique_ptrstd::shared_ptr 的根本区别是什么?
    根本区别在于所有权语义
    unique_ptr 表示独占所有权,一个对象只能由一个 unique_ptr 拥有,它不能被拷贝,只能移动。
    shared_ptr 表示共享所有权,多个 shared_ptr 可以共享同一个对象的所有权,通过引用计数机制管理生命周期。

为什么更推荐使用 make_shared 而不是直接 new 来创建 shared_ptr
主要有两个原因:

  1. 性能make_shared 通常只进行一次内存分配,同时为对象和控制块分配内存,而 new 需要两次分配。
  2. 异常安全make_shared 避免了在函数参数求值过程中可能发生异常而导致的内存泄漏问题。
  1. 什么是循环引用?weak_ptr 是如何解决它的?
    循环引用是指两个或多个对象通过 shared_ptr 互相持有,导致它们的引用计数永远无法降为 0,从而无法被析构,产生内存泄漏。weak_ptr 通过提供一种不增加引用计数的“弱引用”来打破这种循环。它将循环中的某一个 shared_ptr 替换为 weak_ptr,这样就不会阻止所指向对象的销毁。

  2. 能否从一个 weak_ptr 直接访问资源?如果不能,应该如何做?
    不能。因为 weak_ptr 不拥有资源,它不知道资源是否已被释放。必须使用 lock() 方法,它会返回一个 shared_ptr。如果资源还存在,这个 shared_ptr 是有效的(并且会增加引用计数);如果资源已被释放,则返回一个空的 shared_ptr必须检查 lock() 的返回值

  3. 智能指针能否用于管理数组?
    可以。

  • std::unique_ptr 完全支持数组:std::unique_ptr<T[]>,并且重载了 operator[]。使用 make_unique<T[]>(size) (C++17)。
  • std::shared_ptr 对数组的支持不如 unique_ptr 直接。在 C++17 及以后,可以用 std::make_shared<T[]>(size),但 API 不如 unique_ptr 完善。在 C++17 之前,通常需要为 shared_ptr 指定一个删除器,如 std::shared_ptr<int> sp(new int[10], std::default_delete<int[]>());
  1. 智能指针的大小是多少?开销有多大?
  • std::unique_ptr:大小通常等于一个指针(例如 8 字节 on x64),开销极小,几乎就是封装了一个原生指针和一些编译期决定的逻辑。
  • std::shared_ptr:大小通常是两个指针(例如 16 字节 on x64)。一个指向管理的对象,另一个指向包含引用计数等的控制块。其开销包括额外的内存分配(控制块)和维护引用计数的原子操作。
  • std::weak_ptr:大小通常和 shared_ptr 一样(两个指针),它也需要访问控制块。
posted @ 2025-09-17 15:34  xclic  阅读(91)  评论(0)    收藏  举报