unique_ptr
unique_ptr
是 C++11 引入的一种智能指针(一种抽象数据类型),定义在 <memory>
头文件中。它是标准库提供的一种工具,用于管理动态分配的内存,确保该类型的对象维护的指针拥有内存的独占所有权(unique ownership)。换句话说,一个 unique_ptr
类型的对象维护的指针在整个程序的生命周期内只有一个,这不同于 shared_ptr 对象维护的指针可以是同一个,即被共享。当 unique_ptr
对象被销毁(例如离开作用域)时,它所管理的动态内存会自动被释放,从而防止内存泄漏。
unique_ptr
是对裸指针(raw pointer)的封装,旨在替代手动管理内存(new
和 delete
)的传统方式,提供更安全、更现代的内存管理机制。
从代码层面来看,我不能执行如下的操作:
int main()
{
std::unique_ptr<int> x(new int(3));
std::unique_ptr<int> y = x;
}
这段代码执行的逻辑是我在堆上声明了一个 int 类型大小的内存空间并通过直接初始化的方式直接在内存中写入3,然后将表达式求值后的结果,即得到对应的地址通过构造初始化的方式写入栈上 x 对应的内存空间中,而我想将 x 对应的内存以拷贝初始化的方式写入栈上 y 对应的内存空间中,对于 unique_ptr 类型的对象维护的地址出现多次是非法的,这种操作是在编译期通过检查 unique_ptr 类型对象的赋值操作判断的。那么保证 unique_ptr 对象维护的指针在程序的整个生命周期内只有一个的原则,可以实现如下的操作:
int main()
{
std::unique_ptr<int> x(new int(3));
std::cout << x.get() << std::endl;
std::unique_ptr<int> y = std::move(x);
std::cout << x.get() << std::endl;
std::cout << y.get() << std::endl;
}
这个操作不同的区别是我将左值 x 构造成一个将亡值将 x 对应的内存数据移动到到另一块内存中,在 ; 结束后,栈上 x 对应的内存空间被释放,从而保证了 unique_ptr 对象维护的指针只有一个。如果打印 x 值可以得到此时 x 的值为 0。
通过函数的返回值同样可以构造一个将亡值,因此可以实现如下的操作:
std::unique_ptr<int> func()
{
std::unique_ptr<int> res(new int(3));
return res;
}
int main()
{
std::unique_ptr<int> x = func();
}
从内存层面理解 unique_ptr
从内存管理的角度看,unique_ptr
的核心在于其独占所有权和自动释放机制。
-
内存分配
当创建unique_ptr
时(例如通过new
或std::make_unique
),它在堆上分配内存,并将其地址存储在unique_ptr
内部的原始指针中。 -
所有权管理
unique_ptr
的本质是一个轻量级数据类型(通常只包含一个裸指针和可能的删除器),其大小接近于原始指针。它通过 C++ 的移动语义(std::move
)确保内存地址的所有权只能属于一个unique_ptr
实例。移动后,原unique_ptr
的内部指针被置为nullptr
,避免重复释放。 -
内存释放
当unique_ptr
对象超出作用域或被销毁时,其析构函数会自动调用删除器(默认是delete
),释放堆上的内存。 -
删除器(Deleter)
unique_ptr
支持自定义删除器,用于特殊资源管理(例如释放文件句柄)。删除器是编译时绑定的,不会增加运行时开销。auto deleter = [](int* p) { std::cout << "Custom delete\n"; delete p; }; std::unique_ptr<int, decltype(deleter)> ptr(new int(10), deleter);
-
内存开销
- 默认情况下,
unique_ptr
只存储一个原始指针,大小与sizeof(void*)
相同。 - 如果使用自定义删除器,可能增加少量存储(取决于删除器类型)。
- 默认情况下,
使用 unique_ptr
1. 创建 unique_ptr
可以用 std::make_unique
(C++14 引入)或直接用构造函数创建:
#include <memory>
#include <iostream>
int main() {
// 使用 std::make_unique (推荐)
auto ptr1 = std::make_unique<int>(42);
// 直接用构造函数
std::unique_ptr<int> ptr2(new int(10));
std::cout << *ptr1 << " " << *ptr2 << std::endl; // 输出: 42 10
return 0;
}
std::make_unique
是首选方式,因为它更安全(避免了异常情况下内存泄漏的风险)且更简洁。
2. 访问和管理资源
- 用
*
或->
访问管理的对象。 - 用
.get()
获取原始指针(但不转移所有权)。
struct MyClass {
void sayHi() { std::cout << "Hello!\n"; }
};
int main() {
auto ptr = std::make_unique<MyClass>();
ptr->sayHi(); // 输出: Hello!
MyClass* raw = ptr.get(); // 获取原始指针
raw->sayHi(); // 输出: Hello!
return 0;
}
3. 所有权转移
unique_ptr
不允许拷贝,但可以通过 std::move
转移所有权:
int main() {
auto ptr1 = std::make_unique<int>(100);
std::unique_ptr<int> ptr2 = std::move(ptr1); // 转移所有权
// ptr1 现在为空(nullptr),ptr2 拥有资源
if (!ptr1) std::cout << "ptr1 is null\n"; // 输出: ptr1 is null
std::cout << *ptr2 << std::endl; // 输出: 100
return 0;
}
4. 释放和重置
.release()
:放弃所有权,返回原始指针,unique_ptr
变为空。.reset()
:释放当前管理的对象,并可选地接管新对象。
int main() {
auto ptr = std::make_unique<int>(50);
int* raw = ptr.release(); // ptr 放弃所有权,raw 接管
std::cout << *raw << std::endl; // 输出: 50
delete raw; // 需要手动释放
ptr.reset(new int(75)); // 重置为新对象
std::cout << *ptr << std::endl; // 输出: 75
return 0;
}
总之记住 不能拷贝,只能移动:
auto ptr1 = std::make_unique<int>(10);
// std::unique_ptr<int> ptr2 = ptr1; // 错误!不能拷贝
std::unique_ptr<int> ptr2 = std::move(ptr1); // 正确
用作函数参数或返回值,表示所有权转移。
weak_ptr
weak_ptr
是 C++11 引入的一种智能指针,定义在 <memory>
头文件中。它是 shared_ptr
的辅助工具,用于解决 shared_ptr
在特定场景下可能导致的循环引用问题。weak_ptr
本身并不拥有资源的所有权,也不会增加资源的引用计数,而是提供了一种“弱引用”机制,用于访问由 shared_ptr
管理的对象。简单来说:
weak_ptr
是一个非拥有型的智能指针,它指向由shared_ptr
管理的对象。- 它不会影响对象的引用计数(
use_count
),因此不会延长对象的生命周期。 - 当所有
shared_ptr
都被销毁,对象被释放时,weak_ptr
会自动变为“过期”(expired),无法再访问该对象。
weak_ptr
的引入主要是为了解决 shared_ptr
的循环引用问题。
shared_ptr
的循环引用问题
假设有两个类 A
和 B
,它们通过 shared_ptr
互相引用:
#include <memory>
#include <iostream>
class B;
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed\n"; }
};
class B {
public:
std::shared_ptr<A> a_ptr;
~B() { std::cout << "B destroyed\n"; }
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b; // A 持有 B 的 shared_ptr
b->a_ptr = a; // B 持有 A 的 shared_ptr
return 0; // 循环引用导致内存泄漏
}
在上述代码中:
a
和b
分别是A
和B
的shared_ptr
。a->b_ptr
持有b
,b->a_ptr
持有a
,形成循环引用。- 当
main
函数结束时,a
和b
的引用计数并不会降为 0(因为彼此的shared_ptr
还在互相引用),导致A
和B
的对象无法被销毁,产生内存泄漏。
从实际内存角度来讲,在堆上有两个类的实例分别占有两块内存由栈上 a 和 b 两个 shared_ptr 类型的实例分别指向这两块内存,此时这两块内存的引用计数分别为 1,而堆上的这两个对象分别在各自的内存空间中维护一个 shared_ptr 类型,通过 a->b_ptr = b; b->a_ptr = a; 的方式完成互指,即循环引用,这造成的结果是此时这两块内存空间的引用计数分别为 2,而 a 和 b 离开作用域后,引用计数减 1,此时这两块内存空间的引用计数为 1,并不会释放对应的内存空间,造成内存泄露,原因是在原先指向堆上的 shared_ptr 离开作用域后我们没法直接再操作堆上这两块内存空间中对应的 shared_ptr ,可以发现只要出现这种循环引用的现象就会导致内存泄漏。这要断开这种循环引用就能解决这种问题,比如:
#include <memory>
#include <iostream>
class B;
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed\n"; }
};
class B {
public:
std::shared_ptr<A> a_ptr;
~B() { std::cout << "B destroyed\n"; }
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b; // A 持有 B 的 shared_ptr
// b->a_ptr = a; // B 持有 A 的 shared_ptr
return 0; // 循环引用导致内存泄漏
}
离开作用域时可以确保内存的释放,但是如果确实有循环引用的需求怎么办?比如,循环链表,树,图等数据结构,要从根源上解决问题,因此就引入了 weak_ptr 只要确保不增加引用计数就行。
weak_ptr
如何解决
将其中一个 shared_ptr
改为 weak_ptr
,打破循环引用。例如:
class B;
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed\n"; }
};
class B {
public:
std::weak_ptr<A> a_ptr; // 改为 weak_ptr
~B() { std::cout << "B destroyed\n"; }
};
现在:
A
通过shared_ptr
持有B
。B
通过weak_ptr
持有A
,weak_ptr
不增加A
的引用计数。- 当
main
函数结束时,a
的引用计数变为 0,A
被销毁;随后b
的引用计数变为 0,B
被销毁。内存泄漏问题解决。
因此,weak_ptr
的核心作用是避免循环引用,确保资源能被正确释放。
如何优雅使用 weak_ptr
?
weak_ptr
的使用通常与 shared_ptr
配合,以下是常见操作:
3.1 创建 weak_ptr
weak_ptr
通常从 shared_ptr
构造:
std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp; // 从 shared_ptr 创建 weak_ptr
3.2 检查是否过期
由于 weak_ptr
不拥有资源,底层对象可能已经被释放,因此需要检查其有效性,使用 expired()
方法:
if (wp.expired()) {
std::cout << "weak_ptr 已过期\n";
}
3.3 访问对象
weak_ptr
本身不能直接解引用(*wp
或 wp->
是非法的),需要通过 lock()
方法获取一个 shared_ptr
:
if (auto sp_temp = wp.lock()) { // lock() 返回 shared_ptr
std::cout << "值: " << *sp_temp << "\n"; // 安全访问
} else {
std::cout << "对象已释放\n";
}
- 如果
weak_ptr
未过期,lock()
返回有效的shared_ptr
,引用计数会暂时增加。 - 如果已过期,
lock()
返回空的shared_ptr
。
3.4 示例代码
#include <memory>
#include <iostream>
int main() {
std::weak_ptr<int> wp;
{
std::shared_ptr<int> sp = std::make_shared<int>(100);
wp = sp; // wp 指向 sp 管理的对象
if (auto temp = wp.lock()) {
std::cout << "值: " << *temp << "\n"; // 输出 100
}
} // sp 销毁,对象释放
if (wp.expired()) {
std::cout << "weak_ptr 已过期\n"; // 输出此句
}
return 0;
}
为了理解 weak_ptr
的内存机制,我们需要看看 shared_ptr
和 weak_ptr
的底层实现。
当创建 shared_ptr
时(例如通过 std::make_shared
),会分配一个控制块(Control Block),用于管理:
- 强引用计数(
use_count
):表示有多少shared_ptr
拥有该资源。 - 弱引用计数:表示有多少
weak_ptr
引用该资源。 - 资源的指针和析构函数。
控制块的生命周期由强引用和弱引用的总数决定:
- 当强引用计数为 0 时,资源被释放(调用析构函数)。
- 当强引用和弱引用计数都为 0 时,控制块本身被销毁。
4.2 weak_ptr
的作用
- 不影响强引用计数:
weak_ptr
只增加弱引用计数,不影响资源释放。 - 依赖控制块:
weak_ptr
通过控制块追踪资源状态。当强引用计数为 0,资源释放后,控制块保留弱引用计数,weak_ptr
可通过expired()
检测到资源已不可用。 - lock() 的实现:调用
lock()
时,检查控制块中的强引用计数。如果大于 0,则创建一个新的shared_ptr
,强引用计数加 1;否则返回空指针。
4.3 内存管理示例
假设:
shared_ptr<int> sp = std::make_shared<int>(42)
:- 分配资源(
int
)和控制块。 - 强引用计数 = 1,弱引用计数 = 0。
- 分配资源(
weak_ptr<int> wp = sp
:- 弱引用计数增至 1,强引用计数仍为 1。
sp
销毁:- 强引用计数变为 0,资源释放。
- 弱引用计数仍为 1,控制块保留。
wp
调用lock()
:- 检查强引用计数为 0,返回空
shared_ptr
。
- 检查强引用计数为 0,返回空
wp
销毁:- 弱引用计数变为 0,控制块释放。