在 C++ 11 当中,智能指针有三个,分别是 shared_ptr
、weak_ptr
和 unique_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(); // 通过 * 解引用后访问成员
需要注意的几点:
- 引用计数是原子操作,线程安全的;
- 当引用计数降为 0 时,对象会被自动删除;
- 移动操作不会改变引用计数,只是转移所有权;
- 使用
make_shared
创建时,引用计数初始为 1; - 使用
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 引用计数发生改变的情况
下面十种情况总结了会导致改变引用计数的情况。
-
创建新的 shared_ptr
// 引用计数从 0 变为 1 auto ptr1 = std::make_shared<MyClass>("Object1");
-
复制构造或赋值
// 引用计数 +1 auto ptr2 = ptr1; // 复制构造 auto ptr3 = ptr1; // 复制构造 // 引用计数 +1 std::shared_ptr<MyClass> ptr4; ptr4 = ptr1; // 赋值操作
-
作为函数参数传递
void func(std::shared_ptr<MyClass> ptr) { // 函数调用时引用计数 +1 // 函数返回时引用计数 -1 }
-
从函数返回
std::shared_ptr<MyClass> createObject() { auto ptr = std::make_shared<MyClass>("New"); return ptr; // 返回时引用计数不变 }
-
reset() 操作
ptr1.reset(); // 引用计数 -1 ptr1.reset(new MyClass("New")); // 原对象引用计数 -1,新对象引用计数为 1
-
作用域结束
{ auto ptr2 = ptr1; // 引用计数 +1 auto ptr3 = ptr1; // 引用计数 +1 } // 作用域结束,ptr2 和 ptr3 销毁,引用计数 -2
-
移动语义操作
// 移动构造,原指针引用计数不变,新指针引用计数为 1 auto ptr2 = std::move(ptr1); // ptr1 变为空,ptr2 获得所有权 // 移动赋值,原对象引用计数 -1,新对象引用计数为 1 ptr1 = std::move(ptr2); // ptr2 变为空,ptr1 获得所有权
-
容器操作
std::vector<std::shared_ptr<MyClass>> vec; vec.push_back(ptr1); // 引用计数 +1 vec.pop_back(); // 引用计数 -1 vec.clear(); // 所有元素引用计数 -1
-
swap 操作
std::shared_ptr<MyClass> ptr2 = std::make_shared<MyClass>("Object2"); ptr1.swap(ptr2); // 交换所有权,引用计数不变
-
使用 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 被销毁时,网络连接会自动关闭
}
这些工业级场景展示了自定义删除器在实际应用中的重要性:
- 资源管理:确保资源(数据库连接、文件句柄、网络连接)在不再需要时被正确释放;
- 异常安全:即使发生异常,资源也能被正确清理。然而,在析构函数中,不建议抛出异常,因为析构函数的中异常没有被处理的合适地方。
- 封装性:将资源清理逻辑封装在删除器中,使代码更加清晰和可维护
3.2 哪些情况应该使用自定义删除器而不是析构函数?
大多数情况下, 使用 shared_ptr
的默认删除器就足够了。但是,在下面的情况中,需要自定义删除器:
- 管理非通过
new
分配的内存。
例如,内存是通过malloc
,calloc
,aligned_alloc
等C风格函数分配的,需要用 free 来释放。 - 管理通过
new[]
分配的数组,C++17 之前。
当shared_ptr
管理一个动态分配的数组时,需要使用delete[]
而不是delete
来释放。 - 管理非内存资源。
例如,文件句柄(需要fclose
),网络套接字(需要close
或closesocket
),数据库连接(需要特定的关闭函数),或者任何通过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. 最佳实践总结
- 优先使用
make_shared
构造共享指针,而不是直接构造或通过原生指针构造; - 避免循环引用(使用
weak_ptr
解决); - 尽量不要使用
get()
获取原始指针; - 注意线程安全性。
shared_ptr
内部的引用计数是线程安全的,但对指针所指向对象的修改不是线程安全的。因此,多个线程操作shared_ptr
指向的对象时,需要使用同步机制。或者,尽量在单个线程内使用shared_ptr
。
shared_ptr
是 C++ 中最常用的智能指针之一,它提供了安全的内存管理和对象共享机制。通过合理使用 shared_ptr
,可以大大减少内存泄漏和悬空指针的问题。但是要注意其使用场景和性能影响,避免过度使用。