std::unique_ptr
std::unique_ptr
是 C++11 引入的一个智能指针类型,用于管理动态分配的内存。它的主要特点是
“独占所有权”:在给定时间只有一个 std::unique_ptr 可以拥有某个对象,当这个 std::unique_ptr 被销毁(例如,离开其作用域)时,它所管理的对象也会被删除。这可以防止内存泄漏和双重释放等问题。
1. 实现原理
1.1 基本概念
std::unique_ptr
是一个模板类,定义在 <memory>
头文件中,其基本定义如下:
template <typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
public:
// 类型别名
using pointer = T*; // 被管理对象指针类型
using element_type = T; // 被管理对象类型
using deleter_type = Deleter; // 自定义删除器,会从析构函数调用
// 构造函数
constexpr unique_ptr() noexcept;
explicit unique_ptr(pointer p) noexcept;
unique_ptr(pointer p, Deleter d) noexcept;
// 析构函数
~unique_ptr();
// 禁用拷贝构造函数和赋值运算符重载
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
// 移动构造函数
unique_ptr(unique_ptr&& u) noexcept;
// 移动赋值运算符
unique_ptr& operator=(unique_ptr&& u) noexcept;
// 获取被管理对象指针
T* get() const noexcept;
// 获取自定义删除器
const Deleter& get_deleter() const noexcept;
// 运算符重载,如同访问被管理对象指针
T& operator*() const;
T* operator->() const noexcept;
T& operator[]( std::size_t i ) const;
explicit operator bool() const noexcept;
// 释放所有权,并返回一个指向被管理对象的指针。
T* release() noexcept;
// 重置被管理对象指针
void reset(pointer p = pointer()) noexcept;
// 交换被管理对象指针
void swap(unique_ptr& u) noexcept;
private:
pointer ptr; // 被管理对象指针
Deleter deleter; // 自定义删除器
};
1.2 构造与析构
// 构造函数
constexpr unique_ptr() noexcept;
explicit unique_ptr(pointer p) noexcept;
unique_ptr(pointer p, Deleter d) noexcept;
// 析构函数
~unique_ptr();
std::unique_ptr
的构造函数可以接受裸指针,也可以接受自定义的删除器:
- 默认构造函数初始化
ptr
为nullptr
。 - 接受裸指针的构造函数将
ptr
初始化为传入的指针。 - 接受删除器的构造函数将
deleter
初始化为传入的自定义删除器。
析构函数会在 std::unique_ptr
被销毁时调用自定义删除器删除管理的对象,若无删除器则默认调用 delete
1.3 移动语义
// 禁用拷贝构造函数和赋值运算符重载
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
// 移动构造函数
unique_ptr(unique_ptr&& u) noexcept;
// 移动赋值运算符
unique_ptr& operator=(unique_ptr&& u) noexcept;
std::unique_ptr
禁用了拷贝构造和赋值运算符重载操作,以确保一个对象只有一个所有者。但是,std::unique_ptr
支持移动构造和移动赋值操作:
- 移动构造函数会将被移动对象的
ptr
和deleter
移动到新对象,并将被移动对象的ptr
置为nullptr
。 - 移动赋值运算符会释放当前对象所拥有的指针,然后将被赋值对象的
ptr
和deleter
移动过来,并将被赋值对象的ptr
置为nullptr
。
1.4 访问被管理对象指针
// 获取被管理对象指针
T* get() const noexcept;
// 运算符重载,如同访问被管理对象指针
T& operator*() const;
T* operator->() const noexcept;
T& operator[]( std::size_t i ) const;
explicit operator bool() const noexcept;
std::unique_ptr
提供了一些方法来访问和修改被管理对象指针:
get()
返回被管理对象指针。operator*
和operator->
,operator[]
,operator bool
允许像使用被管理对象指针一样使用std::unique_ptr
。
1.5 交换智能指针
// 交换被管理对象指针以及关联的删除器
void swap(unique_ptr& u) noexcept;
swap()
方法用于交换两个 std::unique_ptr
对象管理的指针和关联的删除器。
1.6 重置和释放智能指针
// 释放所有权,并返回一个指向被管理对象的指针。
T* release() noexcept;
// 重置被管理对象指针
void reset(pointer p = pointer()) noexcept;
1.7 获取删除器
// 获取自定义删除器
const Deleter& get_deleter() const noexcept;
可以为 std::unique_ptr 提供一个自定义的删除器,以便在销毁对象时使用自定义的删除逻辑。
2. 应用
被管理对象以CTest
为例,讲述如何应用。
class CTest
{
public:
// 构造函数
CTest(int num)
: m_nNum(num)
{
std::cout << "CTest::CTest() " << m_nNum << std::endl;
}
// 析构函数
~CTest()
{
std::cout << "CTest::~CTest() " << m_nNum << std::endl;
}
// 自定义删除器
static void CustomDelete(CTest* p)
{
std::cout << "CTest::CustomDelete()" << std::endl;
}
int m_nNum{ 0 };
};
2.1 初始化
- 构造一个空的智能指针,并重置一个被管理对象指针
int main() {
// 构造一个空的智能指针
std::unique_ptr<CTest> ptr;
if (!ptr) {
std::cout << "被管理对象指针为空!" << std::endl;
}
// 重置
ptr.reset(new CTest(1));
if (ptr) {
ptr->PrintNum();
}
}
输出结果
- 构造并传入被管理对象指针,仔细观察被管理对象被释放的时机
int main() {
std::cout << " main() begin " << std::endl;
// 构造并传入被管理对象指针
std::unique_ptr<CTest> ptr(new CTest(0));
{
std::unique_ptr<CTest> ptr1(new CTest(1));
}// ptr1离开作用域
std::cout << " main() end " << std::endl;
return 0;
}// ptr离开作用域
输出结果
- 构造并传入被管理对象指针以及删除器
int main() {
// 构造并传入被管理对象指针以及删除器
std::unique_ptr<CTest, std::function<void(CTest*)>> ptr(new CTest(1), CustomDelete);
}
输出结果
- 构造并传入被管理对象数组首地址以及lambda表达式删除器
int main() {
// 构造并传入被管理对象数组首地址以及lambda表达式删除器
std::unique_ptr<CTest[], std::function<void(CTest*)>> ptr(new CTest[3]{ 1,2,3 }, [](CTest* p) {delete[] p; });
}
输出结果
- 使用 make_unique 初始化
int main() {
// 使用 make_unique 初始化
auto ptr = std::make_unique<CTest>(0);
}
输出结果
2.2 release与reset区别
release()
函数仅将被管理对象指针置nullptr
reset()
函数会将被管理对象指针置nullptr,且释放其内存。
int main() {
// reset
std::unique_ptr<CTest> ptr(new CTest(0));
ptr.reset();
if (!ptr) {
std::cout << "ptr is nullptr!" << std::endl;
}
// release
std::unique_ptr<CTest> ptr1(new CTest(1));
ptr1.release();
if (!ptr1){
std::cout << "ptr1 is nullptr!" << std::endl;
}
return 0;
}
输出结果
2.3 get获取被管理对象指针
int main() {
auto pMgrObjPoint = new CTest(0);
std::unique_ptr<CTest> ptr(pMgrObjPoint);
auto pTemp = ptr.get();
std::cout << "pMgrObjPointL: " << pMgrObjPoint << std::endl;
std::cout << "pTemp: " << pTemp << std::endl;
return 0;
}
输出结果
2.4 get_deleter获取删除器
int main() {
std::unique_ptr<CTest, std::function<void(CTest*)>> ptr(new CTest(0), CustomDelete);
auto& pTemp = ptr.get_deleter();
pTemp(nullptr);
return 0;
}
输出结果
2.5 运算符重载,等同于操作被管理对象指针
int main() {
std::unique_ptr<CTest> ptr(new CTest(0));
// -> 运算符重载
ptr->PrintNum();
// * 运算符重载
(*ptr).PrintNum();
// bool 运算符重载
if (ptr){
std::cout << "ptr is not nullptr" << std::endl;
}
std::unique_ptr<CTest[]> ptr1(new CTest[2]{ 1,2 });
// [] 运算符重载
ptr1[1].PrintNum();
return 0;
}
输出结果
2.6 独占所有权
std::unique_ptr
满足可移动构造 (MoveConstructible) 和可移动赋值 (MoveAssignable) ,但不满足可复制构造 (CopyConstructible) 或可复制赋值 (CopyAssignable) 。
-
可移动构造 (MoveConstructible):可以通过移动构造函数创建一个新的
std::unique_ptr
实例,将所有权从一个std::unique_ptr
转移到另一个。std::unique_ptr<int> ptr1 = std::make_unique<int>(10); std::unique_ptr<int> ptr2 = std::move(ptr1); // 移动构造
-
可移动赋值 (MoveAssignable):可以通过移动赋值运算符将所有权从一个
std::unique_ptr
实例转移到另一个。std::unique_ptr<int> ptr1 = std::make_unique<int>(10); std::unique_ptr<int> ptr2; ptr2 = std::move(ptr1); // 移动赋值
但是,std::unique_ptr
不具有以下特性:
-
不可复制构造 (CopyConstructible):无法通过复制构造函数创建一个新的
std::unique_ptr
实例,因为std::unique_ptr
的设计就是为了确保每个对象只能有一个所有者。std::unique_ptr<int> ptr1 = std::make_unique<int>(10); std::unique_ptr<int> ptr2 = ptr1; // 错误,不能复制构造
-
不可复制赋值 (CopyAssignable):无法通过复制赋值运算符将一个
std::unique_ptr
赋值给另一个。std::unique_ptr<int> ptr1 = std::make_unique<int>(10); std::unique_ptr<int> ptr2; ptr2 = ptr1; // 错误,不能复制赋值
这种设计使得 std::unique_ptr
能够独占管理其指向的对象的生命周期,确保对象不会被多个 std::unique_ptr
实例管理,从而避免了双重删除等问题。
3. 总结
3.1 RAII 机制
RAII 机制,利用对象的生命周期管理资源(如内存、文件句柄、网络连接等)基本思想,在对象构造初始化时,绑定资源,离开作用域后,在对象析构中释放资源。
3.2 独占所有权
禁用拷贝构造以及赋值运算符重载,支持移动构造及移动赋值。