好的,各位观众老爷们,今天咱们来聊聊 C++ 里一个既强大又容易被忽视的小家伙——std::unique_ptr
,以及它跟自定义 deleter 之间那些不得不说的故事。
std::unique_ptr
:独一无二的守护者
首先,咱们得明白 std::unique_ptr
是干啥的。简单来说,它就是一个智能指针,负责管理动态分配的对象。它最大的特点就是“独占式”:一个 unique_ptr
只能指向一个对象,而且这个对象的所有权完全归它所有。当 unique_ptr
被销毁时,它会自动释放所指向的对象,避免内存泄漏。
你可以把 unique_ptr
想象成一个非常尽职尽责的管家,他只负责看管一件贵重物品,而且保证在你不需要的时候,把这件物品安全地处理掉。
为什么需要自定义 Deleter?
unique_ptr
默认情况下使用 delete
运算符来释放对象。这对于用 new
分配的内存来说没问题。但是,如果你的对象不是用 new
分配的,或者你需要用其他方式释放资源,那么就需要自定义 deleter。
举个例子:
- 使用
new[]
分配的数组:unique_ptr<int[]>
默认使用delete[]
来释放数组,但如果你的数组不是用new[]
分配的,那就完犊子了。 - 使用 C 风格的
malloc
分配的内存:malloc
必须用free
释放,而不是delete
。 - 文件句柄、网络连接等资源: 这些资源都需要特定的关闭函数,而不是
delete
。 - 使用特定库提供的内存分配函数: 例如,某些图形库或游戏引擎可能有自己的内存管理机制,需要调用特定的释放函数。
Deleter 的几种姿势
自定义 deleter 的方式有很多种,咱们一个个来看:
-
函数对象 (Functor):
这是最常见的方式。你可以定义一个类,重载
operator()
,然后在unique_ptr
的构造函数中传入这个类的实例。#include <iostream> #include <memory> struct MyDeleter { void operator()(int* ptr) { std::cout << "Deleting int at " << ptr << std::endl; delete ptr; } }; int main() { std::unique_ptr<int, MyDeleter> ptr(new int(42)); // 创建 unique_ptr,并传入 deleter return 0; // MyDeleter 会在 ptr 销毁时被调用 }
这个例子中,
MyDeleter
就是一个函数对象。当ptr
被销毁时,MyDeleter::operator()
会被调用,释放int
对象。 -
Lambda 表达式:
Lambda 表达式是 C++11 引入的语法糖,可以让你方便地定义匿名函数。用 lambda 表达式定义 deleter 非常简洁。
#include <iostream> #include <memory> int main() { std::unique_ptr<int, void(*)(int*)> ptr(new int(42), [](int* p) { std::cout << "Deleting int with lambda at " << p << std::endl; delete p; }); // 创建 unique_ptr,并传入 lambda deleter return 0; // lambda deleter 会在 ptr 销毁时被调用 }
注意,这里
unique_ptr
的模板参数需要指定 deleter 的类型,也就是void(*)(int*)
,表示一个接受int*
作为参数,返回void
的函数指针。 -
函数指针:
如果你已经有一个现成的函数,可以用作 deleter,可以直接传递函数指针。
#include <iostream> #include <memory> void myDeleter(int* ptr) { std::cout << "Deleting int with function pointer at " << ptr << std::endl; delete ptr; } int main() { std::unique_ptr<int, void(*)(int*)> ptr(new int(42), myDeleter); // 创建 unique_ptr,并传入函数指针 return 0; // myDeleter 会在 ptr 销毁时被调用 }
同样,
unique_ptr
的模板参数需要指定 deleter 的类型。 -
静态成员函数:
如果你的类中有一个静态成员函数可以用来释放资源,也可以把它作为 deleter。
#include <iostream> #include <memory> struct MyClass { static void deleteInstance(MyClass* ptr) { std::cout << "Deleting MyClass instance at " << ptr << std::endl; delete ptr; } }; int main() { std::unique_ptr<MyClass, void(*)(MyClass*)> ptr(new MyClass(), MyClass::deleteInstance); // 创建 unique_ptr,并传入静态成员函数 return 0; // MyClass::deleteInstance 会在 ptr 销毁时被调用 }
高级组合:超越内存管理
自定义 deleter 的强大之处在于,它不仅仅可以用来释放内存,还可以用来执行其他清理操作。这使得 unique_ptr
成为一个通用的资源管理工具。
-
管理文件句柄:
#include <iostream> #include <fstream> #include <memory> class FileDeleter { public: void operator()(std::ofstream* file) { if (file->is_open()) { file->close(); std::cout << "File closed." << std::endl; } delete file; } }; int main() { std::unique_ptr<std::ofstream, FileDeleter> file(new std::ofstream("example.txt")); if (file->is_open()) { *file << "Hello, world!" << std::endl; } // file 会在 unique_ptr 销毁时自动关闭 return 0; }
这个例子中,
FileDeleter
负责关闭文件句柄。即使程序抛出异常,unique_ptr
也能保证文件被正确关闭。 -
管理网络连接:
#include <iostream> #include <memory> // 假设有一个简单的网络连接类 class NetworkConnection { public: NetworkConnection() { std::cout << "Connection established." << std::endl; } ~NetworkConnection() { std::cout << "Connection closed." << std::endl; } void send(const std::string& message) { std::cout << "Sending: " << message << std::endl; } }; // 自定义 deleter,用于关闭网络连接 struct NetworkDeleter { void operator()(NetworkConnection* connection) { std::cout << "Closing network connection..." << std::endl; delete connection; } }; int main() { std::unique_ptr<NetworkConnection, NetworkDeleter> connection(new NetworkConnection()); connection->send("Hello, server!"); // 连接会在 unique_ptr 销毁时自动关闭 return 0; }
NetworkDeleter
负责关闭网络连接。 -
管理互斥锁:
#include <iostream> #include <thread> #include <mutex> #include <memory> std::mutex mtx; struct LockDeleter { void operator()(std::unique_lock<std::mutex>* lock) { std::cout << "Releasing lock..." << std::endl; delete lock; } }; void threadFunction() { std::unique_ptr<std::unique_lock<std::mutex>, LockDeleter> lock(new std::unique_lock<std::mutex>(mtx)); // 获取锁 std::cout << "Thread acquired lock." << std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟工作 // 锁会在 unique_ptr 销毁时自动释放 } int main() { std::thread t(threadFunction); t.join(); return 0; }
LockDeleter
负责释放互斥锁。 -
RAII(Resource Acquisition Is Initialization)的完美体现:
unique_ptr
+ 自定义 deleter 是 RAII 的一个完美体现。资源在对象构造时获取,在对象析构时释放,保证了资源的安全性。
表格总结:Deleter 的选择
Deleter 类型 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
函数对象 (Functor) | 灵活,可以携带状态 | 代码稍显冗长 | 需要携带状态,或者需要复杂的清理逻辑 |
Lambda 表达式 | 简洁,方便 | 不能携带状态,代码复杂时可读性差 | 简单的清理逻辑,不需要携带状态 |
函数指针 | 可以使用现有的函数 | 必须提前定义函数,不能携带状态 | 已经有现成的函数可以用作 deleter,不需要携带状态 |
静态成员函数 | 可以访问类的静态成员,方便进行类相关的清理操作 | 必须是静态成员函数,不能访问类的非静态成员,不能携带状态 | 需要访问类的静态成员进行清理操作,例如释放类的全局资源,或者进行类的内部状态重置。 |
一些注意事项
- Deleter 的类型: 在使用函数指针或 lambda 表达式作为 deleter 时,一定要注意
unique_ptr
的模板参数。unique_ptr<T, Deleter>
中的Deleter
类型必须与你传入的 deleter 的类型一致。 - 状态: 函数对象可以携带状态,这在某些情况下非常有用。例如,你可以用一个函数对象来记录资源的使用情况。
- 异常安全:
unique_ptr
保证在异常情况下也能正确释放资源,这是 RAII 的重要特性。 std::shared_ptr
: 如果你需要多个指针共享同一个对象的所有权,可以使用std::shared_ptr
。std::shared_ptr
也可以使用自定义 deleter,用法类似。std::weak_ptr
: 如果你需要观察std::shared_ptr
管理的对象,但不希望影响对象的生命周期,可以使用std::weak_ptr
。
实战案例:OpenGL 纹理管理
让我们来一个更实际的例子:管理 OpenGL 纹理。
#include <iostream>
#include <memory>
#include <GL/glew.h> // 请确保已安装 GLEW
// 自定义 deleter,用于删除 OpenGL 纹理
struct GLTextureDeleter {
void operator()(GLuint* texture) {
std::cout << "Deleting OpenGL texture " << *texture << std::endl;
glDeleteTextures(1, texture);
delete texture;
}
};
int main() {
// 初始化 GLEW
glewExperimental = GL_TRUE;
if (glewInit() != GLEW_OK) {
std::cerr << "Failed to initialize GLEW" << std::endl;
return 1;
}
// 创建 OpenGL 纹理
GLuint* texture = new GLuint;
glGenTextures(1, texture);
glBindTexture(GL_TEXTURE_2D, *texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 256, 256, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glBindTexture(GL_TEXTURE_2D, 0);
// 使用 unique_ptr 管理纹理
std::unique_ptr<GLuint, GLTextureDeleter> texturePtr(texture);
std::cout << "OpenGL texture created: " << *texturePtr << std::endl;
// ... 使用纹理 ...
// 纹理会在 unique_ptr 销毁时自动删除
return 0;
}
这个例子中,GLTextureDeleter
负责调用 glDeleteTextures
函数来删除 OpenGL 纹理。unique_ptr
保证纹理在程序结束时被正确释放,避免资源泄漏。
总结
std::unique_ptr
配合自定义 deleter,就像一把瑞士军刀,可以用来管理各种各样的资源,而不仅仅是内存。掌握了这种技巧,你就可以写出更加健壮、更加安全的代码。希望今天的讲解能帮助大家更好地理解和使用 unique_ptr
。记住,代码的艺术在于灵活运用,不要拘泥于形式,大胆尝试,才能写出优雅的代码。
感谢大家的观看,下次再见!