C++11:shared_ptr 基本用法指南,我看过的最全面的介绍

在 C++ 11 当中,智能指针有三个,分别是 shared_ptrweak_ptrunique_ptr。今天这篇文章介绍 shared_ptr

shared_ptr 用于管理动态分配的内存。它通过引用计数机制实现多个指针共享同一个对象的所有权,当最后一个指向该对象的 shared_ptr 被销毁时,对象会被自动删除。

1. 基本创建和使用

1.1 创建 shared_ptr

// 推荐使用 make_shared
auto ptr1 = std::make_shared<MyClass>("Object1");

// 不推荐直接使用构造函数
std::shared_ptr<MyClass> ptr2(new MyClass("Object2"));

使用 make_shared 创建 shared_ptr 的优点:

  • 更高效:只需要一次内存分配
  • 更安全:避免内存泄漏
  • 更简洁:代码更易读

1.2 shared_ptr 常用 API

只需要浏览一下有哪些 API 即可,用到的时候想不起来再去查参考文档。

// 1. 获取原始指针
MyClass* rawPtr = ptr1.get();  // 获取原始指针,但不影响引用计数

// 2. 检查是否为空
if (ptr1) {  // 或 if (ptr1 != nullptr)
    // 指针不为空
}

// 3. 获取引用计数
size_t count = ptr1.use_count();  // 获取当前引用计数

// 4. 检查是否唯一
bool isUnique = ptr1.unique();  // 检查是否只有一个引用

// 5. 重置指针
ptr1.reset();  // 释放当前对象,ptr1 变为空
ptr1.reset(new MyClass("New"));  // 释放当前对象,指向新对象

// 6. 交换两个 shared_ptr
std::shared_ptr<MyClass> ptr3 = std::make_shared<MyClass>("Object3");
ptr1.swap(ptr3);  // 交换 ptr1 和 ptr3 的所有权

// 7. 类型转换
std::shared_ptr<Derived> derived = std::make_shared<Derived>();
std::shared_ptr<Base> base = derived;  // 向上转换(隐式)
auto derived2 = std::dynamic_pointer_cast<Derived>(base);  // 向下转换

// 8. 比较操作
if (ptr1 == ptr2) {  // 比较是否指向同一个对象
    // 指向同一个对象
}
if (ptr1 < ptr2) {  // 比较指针地址
    // ptr1 的地址小于 ptr2
}

// 9. 访问成员
ptr1->method();  // 通过 -> 访问成员
(*ptr1).method();  // 通过 * 解引用后访问成员

需要注意的几点:

  1. 引用计数是原子操作,线程安全的;
  2. 当引用计数降为 0 时,对象会被自动删除;
  3. 移动操作不会改变引用计数,只是转移所有权;
  4. 使用 make_shared 创建时,引用计数初始为 1;
  5. 使用 get() 获取原始指针不会影响引用计数。

2. 实现原理:引用计数

shared_ptr 通过引用计数跟踪有多少个指针共享同一个对象:

auto ptr1 = std::make_shared<MyClass>("Shared");
auto ptr2 = ptr1;  // 引用计数 +1
auto ptr3 = ptr1;  // 引用计数 +1
// 当 ptr2 和 ptr3 离开作用域时,引用计数 -2

可以通过 use_count() 查看当前引用计数:

std::cout << "Reference count: " << ptr1.use_count() << std::endl;

2.1 引用计数发生改变的情况

下面十种情况总结了会导致改变引用计数的情况。

  1. 创建新的 shared_ptr

    // 引用计数从 0 变为 1
    auto ptr1 = std::make_shared<MyClass>("Object1");
    
  2. 复制构造或赋值

    // 引用计数 +1
    auto ptr2 = ptr1;  // 复制构造
    auto ptr3 = ptr1;  // 复制构造
    
    // 引用计数 +1
    std::shared_ptr<MyClass> ptr4;
    ptr4 = ptr1;  // 赋值操作
    
  3. 作为函数参数传递

    void func(std::shared_ptr<MyClass> ptr) {
        // 函数调用时引用计数 +1
        // 函数返回时引用计数 -1
    }
    
  4. 从函数返回

    std::shared_ptr<MyClass> createObject() {
        auto ptr = std::make_shared<MyClass>("New");
        return ptr;  // 返回时引用计数不变
    }
    
  5. reset() 操作

    ptr1.reset();  // 引用计数 -1
    ptr1.reset(new MyClass("New"));  // 原对象引用计数 -1,新对象引用计数为 1
    
  6. 作用域结束

    {
        auto ptr2 = ptr1;  // 引用计数 +1
        auto ptr3 = ptr1;  // 引用计数 +1
    }  // 作用域结束,ptr2 和 ptr3 销毁,引用计数 -2
    
  7. 移动语义操作

    // 移动构造,原指针引用计数不变,新指针引用计数为 1
    auto ptr2 = std::move(ptr1);  // ptr1 变为空,ptr2 获得所有权
    
    // 移动赋值,原对象引用计数 -1,新对象引用计数为 1
    ptr1 = std::move(ptr2);  // ptr2 变为空,ptr1 获得所有权
    
  8. 容器操作

    std::vector<std::shared_ptr<MyClass>> vec;
    vec.push_back(ptr1);  // 引用计数 +1
    vec.pop_back();       // 引用计数 -1
    vec.clear();          // 所有元素引用计数 -1
    
  9. swap 操作

    std::shared_ptr<MyClass> ptr2 = std::make_shared<MyClass>("Object2");
    ptr1.swap(ptr2);  // 交换所有权,引用计数不变
    
  10. 使用 weak_ptr

    std::weak_ptr<MyClass> weak = ptr1;  // 创建 weak_ptr 不会增加引用计数
    if (auto shared = weak.lock()) {     // 如果对象还存在,引用计数 +1
        // 使用 shared
    }  // shared 销毁,引用计数 -1
    

3. 自定义删除器

当默认的析构函数无法满足我们的要求时,可以为 shared_ptr 指定自定义删除器,语法例子如下:

auto deleter = [](MyClass* p) {
    std::cout << "Custom deleter called" << std::endl;
    delete p;
};
std::shared_ptr<MyClass> ptr(new MyClass("Custom"), deleter);

3.1 工业级场景使用示例

场景一:数据库连接管理

class DatabaseConnection {
public:
    DatabaseConnection(const std::string& connStr) {
        // 建立数据库连接
        std::cout << "Connecting to database: " << connStr << std::endl;
    }
    
    void executeQuery(const std::string& query) {
        // 执行数据库查询
        std::cout << "Executing query: " << query << std::endl;
    }
    
private:
    // 数据库连接句柄
    // DB_HANDLE handle_;
};

// 自定义删除器:确保数据库连接正确关闭
auto dbDeleter = [](DatabaseConnection* conn) {
    std::cout << "Closing database connection" << std::endl;
    // 执行清理操作
    // closeConnection(conn->handle_);
    delete conn;
};

// 使用示例
void databaseExample() {
    auto dbConn = std::shared_ptr<DatabaseConnection>(
        new DatabaseConnection("mysql://localhost:3306/mydb"),
        dbDeleter
    );
    dbConn->executeQuery("SELECT * FROM users");
    // 当 dbConn 被销毁时,数据库连接会自动关闭
}

场景二:文件句柄管理

class FileHandler {
public:
    FileHandler(const std::string& filename) {
        // 打开文件
        std::cout << "Opening file: " << filename << std::endl;
        // file_ = fopen(filename.c_str(), "r");
    }
    
    void readData() {
        // 读取文件数据
        std::cout << "Reading file data" << std::endl;
    }
    
private:
    // FILE* file_;
};

// 自定义删除器:确保文件正确关闭
auto fileDeleter = [](FileHandler* handler) {
    std::cout << "Closing file" << std::endl;
    // 执行文件关闭操作
    // fclose(handler->file_);
    delete handler;
};

// 使用示例
void fileExample() {
    auto file = std::shared_ptr<FileHandler>(
        new FileHandler("data.txt"),
        fileDeleter
    );
    file->readData();
    // 当 file 被销毁时,文件会自动关闭
}

场景三:网络连接管理

class NetworkConnection {
public:
    NetworkConnection(const std::string& host, int port) {
        // 建立网络连接
        std::cout << "Connecting to " << host << ":" << port << std::endl;
        // socket_ = createSocket(host, port);
    }
    
    void sendData(const std::string& data) {
        // 发送数据
        std::cout << "Sending data: " << data << std::endl;
    }
    
private:
    // SOCKET socket_;
};

// 自定义删除器:确保网络连接正确关闭
auto networkDeleter = [](NetworkConnection* conn) {
    std::cout << "Closing network connection" << std::endl;
    // 执行网络连接清理
    // shutdown(conn->socket_, SD_BOTH);
    // closesocket(conn->socket_);
    delete conn;
};

// 使用示例
void networkExample() {
    auto conn = std::shared_ptr<NetworkConnection>(
        new NetworkConnection("example.com", 8080),
        networkDeleter
    );
    conn->sendData("Hello, Server!");
    // 当 conn 被销毁时,网络连接会自动关闭
}

这些工业级场景展示了自定义删除器在实际应用中的重要性:

  1. 资源管理:确保资源(数据库连接、文件句柄、网络连接)在不再需要时被正确释放;
  2. 异常安全:即使发生异常,资源也能被正确清理。然而,在析构函数中,不建议抛出异常,因为析构函数的中异常没有被处理的合适地方。
  3. 封装性:将资源清理逻辑封装在删除器中,使代码更加清晰和可维护

3.2 哪些情况应该使用自定义删除器而不是析构函数?

大多数情况下, 使用 shared_ptr 的默认删除器就足够了。但是,在下面的情况中,需要自定义删除器:

  1. 管理非通过 new 分配的内存。
    例如,内存是通过 malloc, calloc, aligned_alloc 等C风格函数分配的,需要用 free 来释放。
  2. 管理通过 new[] 分配的数组,C++17 之前。
    shared_ptr 管理一个动态分配的数组时,需要使用 delete[] 而不是 delete 来释放。
  3. 管理非内存资源。
    例如,文件句柄(需要 fclose),网络套接字(需要 closeclosesocket),数据库连接(需要特定的关闭函数),或者任何通过C 语言的API获取并需要特定释放函数句柄的资源(如 SDL_Texture* 需要 SDL_DestroyTexture

malloc 分配的内存必须通过 free 来释放,直接使用 delete 会导致未定义行为。因此,当根据一个原生指针构造 shared_ptr ,且原生指针指向 malloc 分配的动态内存时,必须自定义删除器。

下面是一个例子:

#include <iostream>
#include <memory> // For std::shared_ptr
#include <cstdlib>  // For malloc, free

// 自定义删除器函数
void c_style_free(void* ptr) {
    std::cout << "自定义删除器:调用 free(" << ptr << ")" << std::endl;
    free(ptr);
}

struct MyData {
    int value;
    MyData(int v) : value(v) {
        std::cout << "MyData(" << value << ") 构造" << std::endl;
    }
    ~MyData() {
        // 注意:这个析构函数仍然会被调用,在删除器释放内存之前
        // MyData本身没有动态分配的资源,这个析构函数简单
        std::cout << "MyData(" << value << ") 析构" << std::endl;
    }
};

int main() {
    std::cout << "--- 示例:管理malloc分配的内存 ---" << std::endl;
    // 分配内存
    MyData* raw_ptr = static_cast<MyData*>(malloc(sizeof(MyData)));
    if (!raw_ptr) {
        std::cerr << "malloc 失败!" << std::endl;
        return 1;
    }
    // 在分配的内存上构造对象 (placement new)
    new (raw_ptr) MyData(100);

    // 使用自定义删除器创建 shared_ptr
    // 注意:自定义删除器需要知道原始指针类型(void* 或 MyData*)
    // shared_ptr 会在调用删除器前调用 MyData 的析构函数
    // 然后删除器负责释放原始内存块
    std::shared_ptr<MyData> sp1(raw_ptr, [](MyData* p) {
        std::cout << "Lambda 删除器:准备释放 MyData* p 指向的由 malloc 分配的内存 " << static_cast<void*>(p) << std::endl;
        // 首先显式调用析构函数,因为内存不是通过 new MyData 创建的
        // placement new 创建的对象需要手动析构
        p->~MyData(); // <<<<<< 手动调用析构
        free(p);      // <<<<<< 然后释放内存
    });

    std::cout << "sp1 use_count: " << sp1.use_count() << std::endl;
    sp1.reset(); // 强制释放
    std::cout << "sp1 已 reset." << std::endl;
    
    return 0;
}

4. 最佳实践总结

  1. 优先使用 make_shared 构造共享指针,而不是直接构造或通过原生指针构造;
  2. 避免循环引用(使用 weak_ptr 解决);
  3. 尽量不要使用 get() 获取原始指针;
  4. 注意线程安全性。shared_ptr 内部的引用计数是线程安全的,但对指针所指向对象的修改不是线程安全的。因此,多个线程操作 shared_ptr 指向的对象时,需要使用同步机制。或者,尽量在单个线程内使用 shared_ptr

shared_ptr 是 C++ 中最常用的智能指针之一,它提供了安全的内存管理和对象共享机制。通过合理使用 shared_ptr,可以大大减少内存泄漏和悬空指针的问题。但是要注意其使用场景和性能影响,避免过度使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值