18、【C++】智能指针

目录

一、智能指针概述

1.1 为什么需要智能指针(内存泄漏问题)

动态内存管理中,程序员常因忘记释放内存或异常抛出导致内存泄漏:

void leak() {
    int* ptr = new int(10);
    // ... 若此处抛出异常,ptr未释放,导致内存泄漏
    delete ptr;
}

智能指针通过RAII(资源获取即初始化) 机制,在对象生命周期结束时自动释放资源,避免内存泄漏。

1.2 RAII原理

RAII核心思想:将资源的生命周期与对象的生命周期绑定,对象构造时获取资源,析构时释放资源。智能指针是RAII的典型应用:

template <typename T>
class SmartPtr {
public:
    SmartPtr(T* ptr) : _ptr(ptr) {} // 获取资源
    ~SmartPtr() { delete _ptr; } // 释放资源
private:
    T* _ptr;
};

void safe() {
    SmartPtr<int> ptr(new int(10)); // 资源与ptr绑定
    // ... 异常抛出时,ptr析构,资源释放
}

1.3 智能指针的分类

1.3.1 独占所有权(unique_ptr)
  • 特性:同一时间只能有一个智能指针拥有资源所有权。
  • 不可拷贝:拷贝构造和赋值运算符被删除,仅支持移动。
  • 适用场景:独占资源(如动态数组、文件句柄)。
1.3.2 共享所有权(shared_ptr)
  • 特性:多个智能指针可共享同一资源,通过引用计数管理资源释放。
  • 引用计数:当引用计数为0时,释放资源。
  • 适用场景:共享资源(如多线程共享数据)。
1.3.3 弱引用(weak_ptr)
  • 特性:不拥有资源所有权,不影响引用计数,用于解决shared_ptr的循环引用。
  • 依赖shared_ptr:需从shared_ptr创建,通过lock()获取可用的shared_ptr。

二、std::unique_ptr

2.1 unique_ptr的特性(独占所有权)

  • 独占性:同一资源只能被一个unique_ptr拥有。
  • 不可拷贝unique_ptr(const unique_ptr&) = delete
  • 可移动:支持通过std::move转移所有权。
  • 大小优化:与裸指针大小相同(通常8字节),删除器作为模板参数可能增加大小。

2.2 unique_ptr的基本用法

2.2.1 创建与初始化
#include <memory>

// 创建unique_ptr
std::unique_ptr<int> up1(new int(10)); // C++11
auto up2 = std::make_unique<int>(20); // C++14(推荐,异常安全)

推荐使用std::make_unique:避免资源泄露(如unique_ptr(new int, deleter)在构造时可能抛出异常)。

2.2.2 所有权转移(std::move)
std::unique_ptr<int> up1 = std::make_unique<int>(10);
// std::unique_ptr<int> up2 = up1; // 错误:不可拷贝
std::unique_ptr<int> up2 = std::move(up1); // 正确:转移所有权,up1变为nullptr
2.2.3 释放所有权(release)
std::unique_ptr<int> up = std::make_unique<int>(10);
int* raw_ptr = up.release(); // 释放所有权,up变为nullptr
delete raw_ptr; // 需手动释放资源

2.3 unique_ptr与数组

unique_ptr支持动态数组,自动使用delete[]释放:

// 创建数组
std::unique_ptr<int[]> up(new int[5]); // C++11
auto up_arr = std::make_unique<int[]>(5); // C++14

// 访问元素
up_arr[0] = 1;
up_arr[1] = 2;

2.4 unique_ptr的删除器(Deleter)

unique_ptr可指定删除器(释放资源的函数/对象),用于非new分配的资源:

// 函数指针作为删除器
void file_deleter(FILE* fp) {
    fclose(fp);
    std::cout << "file closed" << std::endl;
}

// 创建时指定删除器
std::unique_ptr<FILE, decltype(&file_deleter)> up(fopen("data.txt", "r"), file_deleter);

// lambda作为删除器(C++11+)
auto deleter = [](FILE* fp) { fclose(fp); };
std::unique_ptr<FILE, decltype(deleter)> up2(fopen("data.txt", "r"), deleter);

2.5 unique_ptr与auto_ptr的区别

std::auto_ptr是C++98的智能指针,因设计缺陷(拷贝时转移所有权)已被废弃:

std::auto_ptr<int> ap1(new int(10));
std::auto_ptr<int> ap2 = ap1; // ap1失去所有权,访问ap1会UB

unique_ptr通过禁用拷贝解决此问题,更安全。

2.6 unique_ptr的实现原理

unique_ptr的核心是移动语义删除器封装

template <typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
public:
    // 构造函数
    explicit unique_ptr(T* ptr = nullptr) : _ptr(ptr) {}

    // 移动构造
    unique_ptr(unique_ptr&& other) noexcept : _ptr(other._ptr) {
        other._ptr = nullptr; // 源对象释放所有权
    }

    // 移动赋值
    unique_ptr& operator=(unique_ptr&& other) noexcept {
        if (this != &other) {
            _deleter(_ptr); // 释放当前资源
            _ptr = other._ptr;
            other._ptr = nullptr;
        }
        return *this;
    }

    // 析构函数
    ~unique_ptr() {
        if (_ptr) _deleter(_ptr);
    }

    // 禁止拷贝
    unique_ptr(const unique_ptr&) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;

private:
    T* _ptr;
    Deleter _deleter;
};

三、std::shared_ptr

3.1 shared_ptr的特性(共享所有权)

  • 共享性:多个shared_ptr可指向同一资源。
  • 引用计数:通过引用计数跟踪拥有资源的shared_ptr数量。
  • 线程安全:引用计数的增减是原子操作,线程安全。

3.2 引用计数(Reference Counting)

3.2.1 引用计数的变化
  • 创建:引用计数=1。
  • 拷贝:引用计数+1。
  • 析构:引用计数-1,若计数=0,释放资源。
3.2.2 查看引用计数(use_count)
auto sp1 = std::make_shared<int>(10);
std::cout << sp1.use_count() << std::endl; // 1

auto sp2 = sp1; // 拷贝,引用计数+1
std::cout << sp1.use_count() << std::endl; // 2
std::cout << sp2.use_count() << std::endl; // 2

3.3 shared_ptr的基本用法

3.3.1 创建与初始化
// C++11
std::shared_ptr<int> sp1(new int(10));
// C++11(推荐)
auto sp2 = std::make_shared<int>(20); // 效率更高,控制块与对象同分配

std::make_shared优势:一次性分配对象和控制块内存,减少内存碎片。

3.3.2 拷贝与赋值
auto sp1 = std::make_shared<int>(10);
auto sp2 = sp1; // 拷贝,引用计数=2
std::shared_ptr<int> sp3;
sp3 = sp1; // 赋值,引用计数=3
3.3.3 重置与释放(reset)
auto sp = std::make_shared<int>(10);
sp.reset(); // 引用计数-1,若=0则释放资源,sp变为nullptr
sp.reset(new int(20)); // 释放旧资源,管理新资源,引用计数=1

3.4 shared_ptr的控制块(Control Block)

shared_ptr的引用计数存储在控制块中,包含:

  • 强引用计数:拥有资源的shared_ptr数量。
  • 弱引用计数:引用资源的weak_ptr数量。
  • 删除器:释放资源的函数对象。
  • 分配器:内存分配相关。

控制块创建时机

  • std::make_shared:同时创建对象和控制块。
  • shared_ptr(new T):先创建对象,再创建控制块。

3.5 shared_ptr的线程安全性

  • 引用计数:原子操作,线程安全。
  • 指向的对象:非线程安全,需额外同步(如互斥锁)。
// 线程安全:引用计数增减
auto sp = std::make_shared<int>(10);
std::thread t1([sp]() { auto sp2 = sp; });
std::thread t2([sp]() { auto sp3 = sp; });

// 线程不安全:对象修改
auto sp = std::make_shared<int>(10);
std::thread t1([sp]() { *sp = 20; });
std::thread t2([sp]() { *sp = 30; }); // 数据竞争

3.6 shared_ptr的循环引用问题

两个shared_ptr相互引用,导致引用计数无法归零,资源泄漏:

struct Node {
    std::shared_ptr<Node> next;
};

int main() {
    auto n1 = std::make_shared<Node>();
    auto n2 = std::make_shared<Node>();
    n1->next = n2; // n1引用n2,n2引用计数=2
    n2->next = n1; // n2引用n1,n1引用计数=2
    // 析构时n1和n2引用计数=1,无法释放,内存泄漏
    return 0;
}

解决:使用std::weak_ptr打破循环引用。

3.7 shared_ptr的实现原理

template <typename T>
class shared_ptr {
public:
    // 构造函数
    shared_ptr(T* ptr) : _ptr(ptr) {
        _control_block = new ControlBlock();
        _control_block->strong_count = 1;
    }

    // 拷贝构造
    shared_ptr(const shared_ptr& other) {
        _ptr = other._ptr;
        _control_block = other._control_block;
        _control_block->strong_count++;
    }

    // 析构函数
    ~shared_ptr() {
        if (--_control_block->strong_count == 0) {
            delete _ptr; // 释放资源
            if (_control_block->weak_count == 0) {
                delete _control_block; // 释放控制块
            }
        }
    }

private:
    T* _ptr; // 指向资源
    struct ControlBlock {
        int strong_count; // 强引用计数
        int weak_count;   // 弱引用计数
    }* _control_block;
};

四、std::weak_ptr

4.1 weak_ptr的特性(弱引用)

  • 无所有权:不增加强引用计数,不影响资源释放。
  • 依赖shared_ptr:需从shared_ptr创建。
  • 解决循环引用:打破shared_ptr的相互引用。

4.2 weak_ptr的基本用法

4.2.1 lock()获取shared_ptr

lock()返回指向资源的shared_ptr,若资源已释放则返回nullptr:

auto sp = std::make_shared<int>(10);
std::weak_ptr<int> wp = sp; // 从shared_ptr创建weak_ptr,强引用计数不变

// 获取shared_ptr
if (auto locked = wp.lock()) { // locked是shared_ptr<int>
    std::cout << *locked << std::endl; // 10
} else {
    std::cout << "资源已释放" << std::endl;
}
4.2.2 expired()判断对象是否存活
std::weak_ptr<int> wp;
{
    auto sp = std::make_shared<int>(10);
    wp = sp;
    std::cout << wp.expired() << std::endl; // false(资源存活)
}
std::cout << wp.expired() << std::endl; // true(资源已释放)

4.3 weak_ptr解决循环引用

修改3.6节的循环引用示例:

struct Node {
    std::weak_ptr<Node> next; // 使用weak_ptr
};

int main() {
    auto n1 = std::make_shared<Node>();
    auto n2 = std::make_shared<Node>();
    n1->next = n2; // weak_ptr不增加强引用计数
    n2->next = n1; // weak_ptr不增加强引用计数

    // 析构时n1和n2的强引用计数=1-1=0,资源释放
    return 0;
}

4.4 weak_ptr的应用场景

4.4.1 观察者模式

观察者通过weak_ptr观察主题,主题销毁后观察者自动失效:

class Subject;
class Observer {
private:
    std::weak_ptr<Subject> _subject; // 弱引用主题
};

class Subject {
private:
    std::vector<std::weak_ptr<Observer>> _observers; // 存储弱引用观察者
};
4.4.2 缓存机制

缓存对象通过weak_ptr管理,当对象不被使用时自动释放:

std::unordered_map<Key, std::weak_ptr<Value>> cache;

std::shared_ptr<Value> get_value(const Key& key) {
    if (auto cached = cache[key].lock()) {
        return cached; // 返回缓存对象
    }
    auto value = std::make_shared<Value>();
    cache[key] = value; // 缓存weak_ptr
    return value;
}

五、自定义删除器(Custom Deleter)

删除器用于指定智能指针释放资源的方式,适用于非new分配的资源(如malloc、文件句柄)。

5.1 删除器的作用

  • 释放资源:调用自定义函数释放资源(如fclosefree)。
  • 数组支持std::default_delete[]用于数组释放。

5.2 函数对象作为删除器

struct FileDeleter {
    void operator()(FILE* fp) const {
        if (fp) {
            fclose(fp);
            std::cout << "file closed" << std::endl;
        }
    }
};

std::unique_ptr<FILE, FileDeleter> up(fopen("data.txt", "r"));

5.3 lambda表达式作为删除器

auto deleter = [](int* p) {
    std::cout << "deleting int: " << *p << std::endl;
    delete p;
};

std::unique_ptr<int, decltype(deleter)> up(new int(10), deleter);
std::shared_ptr<int> sp(new int(20), deleter); // shared_ptr删除器不影响类型

5.4 数组删除器(std::default_delete[])

智能指针默认使用std::default_delete(调用delete),数组需指定std::default_delete[]

std::unique_ptr<int[], std::default_delete<int[]>> up(new int[5]); // 数组删除器
auto sp = std::shared_ptr<int>(new int[5], std::default_delete<int[]>());

六、智能指针的异常安全

智能指针确保异常抛出时资源释放:

void safe_operation() {
    auto sp = std::make_shared<int>(10);
    risky_operation(); // 抛出异常时,sp析构,资源释放
}

与try-catch配合

try {
    auto sp = std::make_shared<Resource>();
    throw std::runtime_error("error");
} catch (const std::exception& e) {
    // sp已析构,资源释放
}

七、智能指针的应用场景

7.1 unique_ptr的应用

  • 独占资源:动态数组、文件句柄、网络连接。
  • 工厂函数返回值:返回对象所有权。
std::unique_ptr<Product> create_product() {
    return std::make_unique<ConcreteProduct>();
}

7.2 shared_ptr的应用

  • 共享资源:多线程共享数据、大型对象共享访问。
  • 容器存储:存储动态对象,自动管理生命周期。
std::vector<std::shared_ptr<Shape>> shapes;
shapes.push_back(std::make_shared<Circle>());
shapes.push_back(std::make_shared<Square>());

7.3 weak_ptr的应用

  • 解决循环引用:观察者模式、树结构父节点引用子节点。
  • 临时访问共享资源:不影响资源生命周期的临时访问。

八、常见误区与注意事项

8.1 shared_ptr的循环引用

问题:两个shared_ptr相互引用,导致资源泄漏。
解决:使用weak_ptr打破循环。

8.2 将this指针托管给shared_ptr

问题:对象未被shared_ptr管理时,将this托管会导致二次释放。
解决:继承std::enable_shared_from_this

class MyClass : public std::enable_shared_from_this<MyClass> {
public:
    std::shared_ptr<MyClass> get_shared_ptr() {
        return shared_from_this(); // 安全获取this的shared_ptr
    }
};

8.3 删除器不匹配

问题:使用错误删除器导致未定义行为(如用delete释放new[]数组)。
解决:数组使用std::default_delete[]

8.4 过度使用shared_ptr

问题:所有场景都用shared_ptr,导致不必要的引用计数开销。
解决:优先使用unique_ptr,必要时才用shared_ptr。

九、扩展内容

9.1 std::auto_ptr的缺陷(已废弃)

  • 拷贝转移所有权:导致悬挂指针。
  • 不支持数组:删除数组时调用delete而非delete[]

9.2 boost智能指针简介

Boost库提供更多智能指针:

  • boost::scoped_ptr:不可移动的unique_ptr。
  • boost::intrusive_ptr:侵入式引用计数,对象自带计数成员。

9.3 自定义智能指针(简易实现)

实现一个简化的unique_ptr:

template <typename T>
class MyUniquePtr {
public:
    MyUniquePtr(T* ptr = nullptr) : _ptr(ptr) {}
    ~MyUniquePtr() { delete _ptr; }

    MyUniquePtr(MyUniquePtr&& other) noexcept : _ptr(other._ptr) {
        other._ptr = nullptr;
    }

    MyUniquePtr& operator=(MyUniquePtr&& other) noexcept {
        if (this != &other) {
            delete _ptr;
            _ptr = other._ptr;
            other._ptr = nullptr;
        }
        return *this;
    }

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

    MyUniquePtr(const MyUniquePtr&) = delete;
    MyUniquePtr& operator=(const MyUniquePtr&) = delete;

private:
    T* _ptr;
};

以上内容,全面覆盖了C++智能指针的核心知识点,包括unique_ptr、shared_ptr、weak_ptr的特性、用法、实现原理及应用场景。合理使用智能指针可显著提高代码的安全性和可读性,减少内存泄漏。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐