C/C++ 面试智能指针
说下你对智能指针的理解
回答1:
因为C++使用内存的时候很容易出现野指针、悬空指针、内存泄露的问题。所以C++11引入了智能指针来管理内存。有四种:
- auto_ptr:已经不用了
- unique_ptr:独占式指针,同一时刻只能有一个指针指向同一个对象
- shared_ptr:共享式指针,同一时刻可以有多个指针指向同一个对象
- weak_ptr:用来解决shared_ptr相互引用导致的死锁问题
回答2:
1.std::unique_ptr
所有权模型:独占所有权,同一时间只能有一个unique_ptr指向资源。
核心特点:
-
不可复制(delete了拷贝构造函数和拷贝赋值),但支持移动语义(std::move)。
-
轻量级,性能接近裸指针。
使用场景:
管理独占资源(如文件句柄、动态数组)。
替代new/delete,作为工厂函数的返回值
std::unique_ptr<int> p1 = std::make_unique<int>(42);
std::unique_ptr<int> p2 = std::move(p1); // 所有权转移
2.std::shared_ptr
所有权模型:共享所有权,通过引用计数管理资源。
核心特点
-
多个shared_ptr可以指向同一资源,引用计数为0时释放资源。
-
支持自定义删除器(如释放文件、网络连接等非内存资源)。
潜在问题
潜在问题
- 循环引用:若两个shared_ptr相互引用,引用计数无法归零,导致内存泄漏。
解决方案
auto p1 = std::make_shared<int>(42);
auto p2 = p1; // 引用计数+1
3.std::weak_ptr
所有权模型:弱引用,不增加引用计数。
核心作用:
-
解决 shared_ptr 的循环引用问题。
-
临时观察资源是否存在(通过lock()升级为shared_ptr)。
示例:
std::weak_ptr<int> wp = p1;
if (auto sp = wp.lock()) { // 若资源存在,升级为shared_ptr
// 使用sp访问资源
}
智能指针的实现原理
unique_ptr:
- 封装裸指针,通过删除移动拷贝操作确保独占所有权。
shared_ptr:
-
包含两个指针:一个指向资源,一个指向控制块(含引用计数和删除器)。
-
引用计数通过原子操作保证线程安全。
weak_ptr:
- 同样指向控制块,但不参与引用计数。
使用智能指针的最佳实践
优先使用make_shared和make_unique
auto p = std::make_shared<Object>(args); // 推荐
-
避免显式new,提高异常安全性。
-
make_shared会合并资源与控制块的内存分配,提升性能。
避免混用裸指针和智能指针
int* raw_ptr = p.get(); // 谨慎使用get(),避免delete或延长生命周期。
自定义删除器
std::shared_ptr<FILE> file( fopen("test.txt", "r"), [](FILE* f) { fclose(f); } );
unique_ptr 能否被另一个 unique_ptr 拷贝呢
回答1:
不能,因为它把它的拷贝构造函数private了。但是它提供了一个移动构造函数,所以可以通过std::move将指针指向的对象交给另一个unique_ptr,转交之后自己就失去了这个指针对象的所有权,除非被显示交回
回答2:
为什么不能拷贝?
1.独占所有权:
-
std::unique_ptr 的核心设计是独占资源的所有权。如果允许拷贝,会导致多个指针指向同一资源,违背了独占所有权的原则。
-
如果允许拷贝,资源释放时会出现重复释放的问题,导致未定义行为。
2.禁止拷贝构造函数和拷贝赋值运算符:
- std::unique_ptr 的拷贝构造函数和拷贝赋值运算符被显式删除(= delete),因此无法直接拷贝。
std::unique_ptr<int> p1 = std::make_unique<int>(42);
std::unique_ptr<int> p2 = p1; // 错误:拷贝构造函数被删除
std::unique_ptr<int> p3;
p3 = p1; // 错误:拷贝赋值运算符被删除
2.如何转移所有权?
虽然 std::unique_ptr 不能拷贝,但可以通过移动语义(std::move)将资源的所有权从一个 std::unique_ptr 转移到另一个。
std::unique_ptr<int> p1 = std::make_unique<int>(42); // p1 拥有资源
std::unique_ptr<int> p2 = std::move(p1); // 将资源从 p1 转移到 p2
if (p1 == nullptr) {
std::cout << "p1 is now null, ownership transferred to p2." << std::endl;
}
std::move 的作用:
将 p1 的资源所有权转移到 p2。
转移后,p1 变为 nullptr,不再拥有资源。
总结
-
std::unique_ptr 不能被拷贝,因为它遵循独占所有权的语义。
-
可以通过 std::move 转移所有权,转移后原指针变为 nullptr。
-
这种设计确保了资源管理的安全性和明确性,避免了内存泄漏和重复释放的问题。
unique_ptr和shared_ptr的区别
unique_ptr
unique_ptr 是独占所有权的智能指针。它确保一个资源在任何时候只有一个 unique_ptr 拥有它。这意味着,unique_ptr 不允许多个指针指向同一个资源,资源的所有权不能被共享。
特点
- 独占所有权:一个 unique_ptr 拥有资源的唯一所有权,不能被拷贝。只能通过 std::move() 转移所有权。
- 内存自动释放:当 unique_ptr 被销毁时,它所管理的资源会自动释放。
- 不可拷贝,但可以转移:unique_ptr 不能被拷贝,避免了资源的多重释放问题,但是可以通过 std::move() 转移所有权。
#include <iostream>
#include <memory>
void uniquePtrExample() {
// 创建 unique_ptr 并指向一个动态分配的整数
std::unique_ptr<int> p1 = std::make_unique<int>(10);
std::cout << "Value of p1: " << *p1 << std::endl;
// 转移所有权
std::unique_ptr<int> p2 = std::move(p1);
// p1 现在是空指针
if (!p1) {
std::cout << "p1 is empty after move." << std::endl;
}
// p2 拥有资源
std::cout << "Value of p2: " << *p2 << std::endl;
}
shared_ptr
shared_ptr 是共享所有权的智能指针。多个 shared_ptr 实例可以共享对同一资源的所有权。每当一个 shared_ptr 被拷贝时,资源的引用计数会增加。只有当所有指向该资源的 shared_ptr 都被销毁时,资源才会被释放。
特点
- 共享所有权:多个 shared_ptr 可以共享同一个资源的所有权,资源的引用计数由 shared_ptr 内部自动管理。
- 引用计数:每次拷贝一个 shared_ptr,引用计数增加,销毁一个 shared_ptr,引用计数减少。资源会在引用计数变为零时释放。
- 线程安全:shared_ptr 是线程安全的,多个线程可以安全地使用同一个 shared_ptr。
#include <iostream>
#include <memory>
void sharedPtrExample() {
// 创建 shared_ptr 并指向一个动态分配的整数
std::shared_ptr<int> p1 = std::make_shared<int>(20);
std::cout << "Value of p1: " << *p1 << std::endl;
// 创建另一个 shared_ptr 并共享资源
std::shared_ptr<int> p2 = p1;
std::cout << "Value of p2: " << *p2 << std::endl;
// 引用计数为 2
std::cout << "Reference count: " << p1.use_count() << std::endl;
// p1 和 p2 都会在其生命周期结束时释放资源
}
主要区别总结
特性 | unique_ptr | shared_ptr |
---|---|---|
所有权 | 独占所有权,不能共享 | 共享所有权,多个指针可以指向同一资源 |
拷贝 | 不允许拷贝,只能转移所有权 (std::move) | 支持拷贝,引用计数增加 |
内存管理 | 资源的释放由 unique_ptr 自动进行 | 引用计数机制,所有 shared_ptr 被销毁时自动释放资源 |
性能 | 性能较高,因为不涉及引用计数 | 相较于 unique_ptr,性能较低,因为需要维护引用计数 |
线程安全 | 非线程安全(不同线程中的 unique_ptr 不可共享) | 线程安全(引用计数是线程安全的,但资源访问需要加锁) |
选择使用的场景
- 使用 unique_ptr:适用于资源的唯一拥有者场景,能够提供高效、简洁的内存管理,不允许共享所有权。
- 如:资源管理、文件句柄、网络连接等。
- 使用 shared_ptr:适用于多个对象需要共享资源所有权的场景,例如资源共享、动态配置、对象池等。
- 如:图形界面的窗口管理、图形对象共享、数据库连接池等。
总结
- unique_ptr 是用来实现独占式所有权的智能指针,不能拷贝,可以转移所有权,适用于资源的唯一拥有者场景。
- shared_ptr 是用来实现共享所有权的智能指针,可以被多个 shared_ptr 对象共享,并通过引用计数管理资源的生命周期,适用于多个对象共享资源的场景。
unique_ptr = unique_ptr 和 shared_ptr=shared_ptr这两个操作有什么后果呢?
回答1:
unique_ptr = unique_ptr :
unique_ptr 是独占所有权的智能指针,它不允许拷贝或赋值操作。直接执行 unique_ptr = unique_ptr 会导致编译错误。unique_ptr 的设计目的是确保资源只能有一个所有者,避免了资源的多重释放问题。
后果:
编译错误:因为 unique_ptr 不允许拷贝和赋值,只能通过 std::move() 转移所有权。假如你尝试执行 unique_ptr = unique_ptr,编译器会报错,提示无法拷贝 unique_ptr 对象。
unique_ptr<int> p1 = make_unique<int>(10);
unique_ptr<int> p2 = make_unique<int>(20);
// 编译错误:unique_ptr 不允许拷贝或赋值
p1 = p2; // Error: unique_ptr不能直接赋值
解决方法:
unique_ptr 只能通过 std::move() 进行所有权转移。例如:
p1 = std::move(p2); // p1 接管 p2 所有权,p2 被置为空
解释:
std::move(p2) 会把 p2 的所有权转移给 p1,此时 p2 成为一个空指针,不能再访问原来的资源。
shared_ptr = shared_ptr:
shared_ptr 是共享所有权的智能指针,允许多个 shared_ptr 对象共享同一个资源。在执行 shared_ptr = shared_ptr 操作时,引用计数会增加,即新的 shared_ptr 会共享原来指针的资源。
后果
正确: shared_ptr 允许多个 shared_ptr 指向同一个资源,通过引用计数管理资源的生命周期。当你将一个 shared_ptr 赋值给另一个时,引用计数会增加,表示更多的指针共享该资源。当所有指向该资源的 shared_ptr 被销毁时,资源才会被释放。
shared_ptr<int> p1 = make_shared<int>(10);
cout << "Reference count of p1: " << p1.use_count() << endl; // 输出: 1
shared_ptr<int> p2 = p1; // 共享所有权,p1 和 p2 都指向同一个资源
cout << "Reference count of p2: " << p2.use_count() << endl; // 输出: 2
解释
当 p2 = p1 时,p1 和 p2 都指向同一个资源。此时,引用计数从 1 增加到 2,表示有两个 shared_ptr 对象共享资源。当这两个 shared_ptr 都被销毁时,资源才会被释放。
总结对比
- unique_ptr = unique_ptr:编译错误,因为 unique_ptr 是独占所有权的,不允许拷贝或赋值。
- 正确操作:通过 std::move() 转移所有权。
- shared_ptr = shared_ptr:合法操作,引用计数增加,多个 shared_ptr 可以共享同一个资源,资源会在最后一个 shared_ptr 被销毁时释放。
实际应用
- unique_ptr:适用于需要确保资源有唯一所有者的场景,避免了多重释放资源的问题。
- shared_ptr:适用于多个对象共享资源的场景,自动管理资源的引用计数,确保资源在所有者全部销毁后才被释放。
shared_ptr的移动赋值时发生了什么事情
- 首先它会检查本指针和参数指针是不是同一个对象,如果是,直接返回
- 然后,先把本指针的引用变量–,如果发现减到了0,就把参数指针和参数引用变量析构掉并置NULL
- 最后,本指针和参数指针指向同一个对象以及引用计数,然后引用计数自增1
shared_ptr什么时候会被释放掉,就是什么时候引用次数为0?
- 在一个shared_ptr析构时,或者被赋值时,强引用计数会减1。减到0就delete资源
shared_ptr 你是怎么实现的?
shared_ptr 的核心实现基于 引用计数(Reference Counting)。它的基本原理是:
- 每个资源 由一个 shared_ptr 所管理,同时维护一个 引用计数(use_count)。
- 当一个新的 shared_ptr 复制已有的 shared_ptr 时,引用计数增加。
- 当一个 shared_ptr 被销毁时,引用计数减少。
- 当引用计数归零时,释放所管理的资源。
自己实现一个 shared_ptr
简化版的 shared_ptr,主要包含:
- 资源指针 (T* ptr)
- 引用计数 (int* count)
- 拷贝构造 & 赋值操作(增加引用计数)
- 析构函数(减少引用计数,归零时释放资源)
#include <iostream>
template <typename T>
class MySharedPtr {
private:
T* ptr; // 资源指针
int* count; // 引用计数
public:
// **构造函数**
explicit MySharedPtr(T* p = nullptr) : ptr(p) {
// ptr 指向新建资源
// count 记录资源的引用次数
if (ptr) {
count = new int(1); // 初始化引用计数为 1
} else {
count = new int(0);
}
}
// **拷贝构造函数(增加引用计数)** -> 复制 ptr 和 count,即共享资源
MySharedPtr(const MySharedPtr& other) : ptr(other.ptr), count(other.count) {
if (ptr) {
(*count)++; // 增加引用计数
}
}
// **赋值运算符(处理自赋值 & 管理引用计数)**
MySharedPtr& operator=(const MySharedPtr& other) {
if (this == &other) // 避免自赋值
return *this;
// 先释放当前对象的资源
release();
// 复制新对象
ptr = other.ptr;
count = other.count;
if (ptr) {
(*count)++; // 增加引用计数
}
return *this;
}
// **释放资源(减少引用计数)**
void release() {
if (ptr && --(*count) == 0) { // 引用计数归零时释放资源
delete ptr;
delete count;
}
}
// **析构函数**
~MySharedPtr() {
release();
}
// **获取原始指针**
T* get() const { return ptr; }
// **重载 `->` 和 `*` 以便像指针一样使用**
T* operator->() const { return ptr; }
T& operator*() const { return *ptr; }
// **获取引用计数**
int use_count() const { return *count; }
};
// **测试 `MySharedPtr`**
int main() {
MySharedPtr<int> p1(new int(42));
std::cout << "p1 count: " << p1.use_count() << std::endl; // 1
MySharedPtr<int> p2 = p1; // 复制 p1
std::cout << "p1 count after copy: " << p1.use_count() << std::endl; // 2
std::cout << "p2 count: " << p2.use_count() << std::endl; // 2
{
MySharedPtr<int> p3 = p2; // 复制 p2
std::cout << "p3 count: " << p3.use_count() << std::endl; // 3
} // p3 作用域结束,引用计数减少
std::cout << "p1 count after p3 destroyed: " << p1.use_count() << std::endl; // 2
return 0;
}
shared_ptr是不是线程安全?
结论
std::shared_ptr 本身是线程安全的,但它管理的对象 不是 线程安全的。
线程安全的部分
1. 引用计数的增加/减少是原子操作
- std::shared_ptr 内部使用了 原子操作 来管理引用计数(std::atomic 或等效实现)。
- 这意味着多个线程可以同时 拷贝 或 销毁 shared_ptr,不会导致未定义行为。
- 例如,多个线程可以同时调用 std::shared_ptr p1 = p2; 或 p1.reset();,不会导致引用计数错误。
线程不安全的部分
1.共享的对象本身不是线程安全的
- shared_ptr 只是管理了对象的生命周期,但不提供对象访问的同步保护。
- 如果多个线程同时 读取/修改 shared_ptr 所管理的对象,而没有额外的同步机制(如 std::mutex),会导致 数据竞争 和 未定义行为。
std::shared_ptr<int> p = std::make_shared<int>(42);
std::thread t1([&]() { *p = 10; }); // 修改值
std::thread t2([&]() { std::cout << *p << std::endl; }); // 读取值
t1.join();
t2.join();
上面的代码 可能会崩溃 或 输出未定义结果,因为 *p 被多个线程同时访问,没有同步保护。
2.不同 shared_ptr 实例对同一对象的修改是线程不安全的
例如:
std::shared_ptr<int> p1 = std::make_shared<int>(42);
std::shared_ptr<int> p2 = p1; // 线程安全(增加引用计数)
std::thread t1([&]() { p1.reset(); }); // 线程1释放对象
std::thread t2([&]() { p2.reset(); }); // 线程2释放对象
这虽然不会导致引用计数错误,但如果 reset() 操作后仍然访问 p1 或 p2,可能导致 访问已释放的对象(悬垂指针)。
如何保证线程安全?
- 只在单个线程中访问 shared_ptr 管理的对象
- 使用 std::mutex 保护对象访问
std::mutex mtx;
std::shared_ptr<int> p = std::make_shared<int>(42);
std::thread t1([&]() {
std::lock_guard<std::mutex> lock(mtx);
*p = 10;
});
std::thread t2([&]() {
std::lock_guard<std::mutex> lock(mtx);
std::cout << *p << std::endl;
});
t1.join();
t2.join();
3.使用 std::atomic<std::shared_ptr>
std::atomic<std::shared_ptr> 允许线程安全地 更新 shared_ptr(但仍不保证管理的对象是线程安全的)。
std::atomic<std::shared_ptr<int>> atomic_p(std::make_shared<int>(42));
std::thread t1([&]() { atomic_p.store(std::make_shared<int>(100)); });
std::thread t2([&]() { std::cout << *atomic_p.load() << std::endl; });
t1.join();
t2.join();
这样可以保证 shared_ptr 本身的更新是安全的,但仍然需要同步对象访问。
总结
线程安全性 | shared_ptr |
---|---|
拷贝/赋值/销毁 | √ 线程安全(原子操作) |
管理的对象访问 | × 线程不安全(需要同步) |
多个 shared_ptr 指向同一对象 | √ 线程安全(引用计数安全) |
多个线程同时修改 shared_ptr | × 线程不安全(需 std::mutex 或 std::atomic) |
如果你在多线程环境下使用 shared_ptr,确保:
- 线程不会同时修改管理的对象,或者使用 std::mutex 保护访问。
- 需要线程安全的 shared_ptr 赋值时,可以使用 std::atomic<std::shared_ptr>。
sharedPtr在64位操作系统下大小有多大
- 两个指针,64字节
shared_ptr和weak_ptr之间是什么关系?
weak_ptr是用来辅助shared_ptr的,每一个weak_ptr它指向weak_ptr而不是实际的操作函数。
std::weak_ptr 是 std::shared_ptr 的 弱引用,它和 shared_ptr 之间有以下关系
1. weak_ptr 解决 shared_ptr 的循环引用问题
问题:shared_ptr 可能导致循环引用,导致内存泄漏
当两个 shared_ptr 相互引用时,它们的引用计数不会降为 0,导致对象无法释放。例如:
#include <iostream>
#include <memory>
class B; // 前置声明
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A 被销毁\n"; }
};
class B {
public:
std::shared_ptr<A> a_ptr;
~B() { std::cout << "B 被销毁\n"; }
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a; // 循环引用
return 0; // 内存泄漏:A 和 B 都不会被销毁
}
解决方案:用 weak_ptr 代替 shared_ptr 解决循环引用
weak_ptr 不会增加引用计数,所以 shared_ptr 之间不会出现循环依赖:
class B; // 前置声明
class A {
public:
std::weak_ptr<B> b_ptr; // 改为 weak_ptr
~A() { std::cout << "A 被销毁\n"; }
};
class B {
public:
std::shared_ptr<A> a_ptr;
~B() { std::cout << "B 被销毁\n"; }
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b; // 现在是 weak_ptr
b->a_ptr = a;
return 0; // A 和 B 都能正常释放
}
2. weak_ptr 不影响 shared_ptr 的引用计数
- std::weak_ptr 不会增加引用计数(use_count() 不变)
- std::weak_ptr 只持有对象的弱引用,如果对象已经被释放,weak_ptr 变成 空指针
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp; // 不增加引用计数
std::cout << "use_count: " << sp.use_count() << std::endl; // 输出 1
sp.reset(); // 释放 shared_ptr
if (wp.expired()) {
std::cout << "对象已被销毁\n"; // wp 变为空指针
}
return 0;
}
3. weak_ptr 需要 lock() 访问对象
weak_ptr 不能直接访问对象,要用 lock() 获取 shared_ptr,然后再使用:
std::weak_ptr<int> wp;
{
std::shared_ptr<int> sp = std::make_shared<int>(42);
wp = sp; // 赋值,不增加引用计数
}
if (std::shared_ptr<int> sp = wp.lock()) { // 获取 shared_ptr
std::cout << *sp << std::endl;
} else {
std::cout << "对象已被销毁\n";
}
为什么推荐用makeshared创建指针?
1. 避免额外的内存分配,提高性能
使用 new 创建 shared_ptr,需要 两次内存分配:
- 分配对象的内存 (new T)
- 分配 shared_ptr 的控制块(用于存储引用计数)
但 std::make_shared 只分配一次内存,控制块和对象存在同一块内存区域,减少了额外的分配和释放操作,提高性能。
std::shared_ptr<int> sp1(new int(42)); // 两次分配(对象 + 控制块)
std::shared_ptr<int> sp2 = std::make_shared<int>(42); // 一次分配(对象+控制块一起分配)
结论: make_shared 比 new + shared_ptr 方式更快、更高效。
2. 避免异常安全问题
如果 new 之后发生异常,可能导致 内存泄漏。
错误示例(可能泄漏内存):
std::shared_ptr<int> sp1(new int(42)); // new 成功后,sp1 可能抛出异常
- new int(42) 先执行
- std::shared_ptr 的构造函数可能抛异常(比如 bad_alloc)
- new 分配的内存不会被释放,造成泄漏
正确做法(异常安全):
std::shared_ptr<int> sp1 = std::make_shared<int>(42);
原因: make_shared 直接分配对象并初始化 shared_ptr,不会导致内存泄漏。
3. 减少 shared_ptr 的构造开销
使用 new 方式:
auto sp1 = std::shared_ptr<int>(new int(42)); // 两步:new + shared_ptr 构造
使用 make_shared 方式:
auto sp2 = std::make_shared<int>(42); // 直接分配和初始化
make_shared 更高效,代码更简洁
4. make_shared 允许数组初始化
C++17 之后,make_shared 支持数组:
auto sp = std::make_shared<int[]>(10); // 分配 10 个 int
结论:
推荐用 std::make_shared,因为它:
✅ 性能更高(减少一次内存分配)
✅ 避免异常安全问题(不会导致内存泄漏)
✅ 代码更简洁(不需要 new)
✅ 减少 shared_ptr 构造开销
⚠️ 但如果 shared_ptr 需要自定义删除器,则必须手动 new,不能用 make_shared。
为什么要用 shared_from_this?
什么是 shared_from_this
shared_from_this 是 std::enable_shared_from_this 提供的一个方法,允许 一个类的成员函数安全地获取自身的 shared_ptr。
为什么要用 shared_from_this?
如果一个对象已经被 std::shared_ptr 管理,直接用 this 构造新的 shared_ptr 会导致严重错误(多重 shared_ptr 造成对象被提前释放)。
错误示例(手动构造 shared_ptr 导致对象重复释放):
class MyClass {
public:
void DangerousMethod() {
// ❌ 直接创建 shared_ptr,会导致双重管理
std::shared_ptr<MyClass> sp_this(this);
}
};
int main() {
std::shared_ptr<MyClass> sp1 = std::make_shared<MyClass>();
sp1->DangerousMethod(); // ⚠️ sp1 和 sp_this 现在都在管理同一个对象
return 0;
} // ❌ 可能导致对象被删除两次(double free)
问题:
- sp1 和 sp_this 都认为自己是 唯一的 shared_ptr,它们会各自删除对象,导致 double free(重复释放) 或 段错误。
解决方案:使用 shared_from_this
#include <iostream>
#include <memory>
class MyClass : public std::enable_shared_from_this<MyClass> {
public:
std::shared_ptr<MyClass> SafeMethod() {
return shared_from_this(); // ✅ 返回正确的 shared_ptr
}
};
int main() {
std::shared_ptr<MyClass> sp1 = std::make_shared<MyClass>();
std::shared_ptr<MyClass> sp2 = sp1->SafeMethod(); // ✅ 共享同一个控制块
std::cout << "sp1.use_count() = " << sp1.use_count() << std::endl; // 输出 2
return 0;
}
为什么 shared_from_this() 安全?
- shared_from_this() 不会创建新的控制块,而是返回现有的 shared_ptr,保证同一个对象只有一个引用计数管理。
shared_from_this 适用场景
- 对象需要在方法内部创建 shared_ptr,但不能改变已有的 shared_ptr 计数
- 观察者模式(Observer Pattern)
- 监听者持有 shared_ptr,如果通知时直接用 this 可能导致悬挂指针
- 回调(Callback)
- 避免 std::bind 或 std::function 误用 this,导致悬挂指针
- 多线程任务调度
- 任务可能在 shared_ptr 作用域外完成,确保对象存活
总结
✅ shared_from_this() 确保 shared_ptr 共享相同的控制块,避免重复管理对象
✅ 防止 this 直接构造 shared_ptr 造成的内存错误
✅ 适用于回调、观察者模式、多线程等场景
⚠️ 使用 shared_from_this 的类必须继承 std::enable_shared_from_this
weak_ptr的原理
std::weak_ptr 是 std::shared_ptr 的辅助指针,它不会影响引用计数,主要用于 解决循环引用问题 和 临时访问 shared_ptr 管理的对象。
weak_ptr 的底层原理
- weak_ptr 和 shared_ptr 共享同一个控制块(Control Block)
- shared_ptr 内部有 引用计数(strong count) 和 弱引用计数(weak count)。
- weak_ptr 仅增加 弱引用计数,不会影响 强引用计数。
- weak_ptr 不能直接使用对象,需要 lock() 转换为 shared_ptr
- weak_ptr 不能直接访问资源,因为可能已经被释放。
- 通过 lock() 方法获得 shared_ptr,如果对象已销毁,lock() 返回 nullptr。
底层数据结构(简化示意图)
struct ControlBlock {
int strong_count; // 强引用计数(shared_ptr 持有)
int weak_count; // 弱引用计数(weak_ptr 持有)
void* ptr; // 指向对象的指针
};
总结
✅ weak_ptr 不增加引用计数,避免循环引用问题
✅ lock() 方法用于安全访问对象,防止 dangling pointer(悬挂指针)
✅ 适用于缓存、观察者模式、循环引用等场景
⚠️ weak_ptr 不能直接使用,需要 lock() 转换成 shared_ptr
weak_ptr的使用场景
1. weak_ptr 解决循环引用
#include <iostream>
#include <memory>
class B; // 前向声明
class A {
public:
std::shared_ptr<B> ptrB;
~A() { std::cout << "A destroyed\n"; }
};
class B {
public:
std::weak_ptr<A> ptrA; // ✅ 使用 weak_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->ptrB = b;
b->ptrA = a; // ✅ 这里使用 weak_ptr,否则 A 和 B 互相引用,永远不会释放
return 0; // ✅ A 和 B 都会被正确销毁
}
如果 ptrA 也是 shared_ptr,会发生内存泄漏
class B {
public:
std::shared_ptr<A> ptrA; // ❌ 这样会导致循环引用,内存永远无法释放
};
2. lock() 获取 shared_ptr
#include <iostream>
#include <memory>
class MyClass {
public:
void show() { std::cout << "Object is alive!\n"; }
};
int main() {
std::shared_ptr<MyClass> sp = std::make_shared<MyClass>();
std::weak_ptr<MyClass> wp = sp; // ✅ 创建 weak_ptr
if (auto shared = wp.lock()) { // ✅ 转换成 shared_ptr
shared->show();
} else {
std::cout << "Object already deleted\n";
}
sp.reset(); // ❌ 释放对象
if (auto shared = wp.lock()) {
shared->show();
} else {
std::cout << "Object already deleted\n"; // ✅ 这里会输出
}
}
weak_ptr 适用场景
- 解决 shared_ptr 循环引用
- 互相引用的对象,需要一个使用 weak_ptr 避免内存泄漏。
- 缓存(Cache)
- 避免缓存对象始终存活,但仍然可以被共享使用。
- 观察者模式(Observer Pattern)
- 监听者使用 weak_ptr 避免对象一直存活。
关于shared_ptr相互引用具体讲一下
weak_ptr 是为了解决 shared_ptr 双向引用的问题。即:
class B;
struct A{
shared_ptr<B> b;
};
struct B{
shared_ptr<A> a;
};
auto pa = make_shared<A>();
auto pb = make_shared<B>();
pa->b = pb;
pb->a = pa;
pa 和 pb 存在着循环引用,根据 shared_ptr 引用计数的原理,pa 和 pb 都无法被正常的释放。
对于这种情况, 我们可以使用 weak_ptr:
class B;
struct A{
shared_ptr<B> b;
};
struct B{
weak_ptr<A> a;
};
auto pa = make_shared<A>();
auto pb = make_shared<B>();
pa->b = pb;
pb->a = pa;
weak_ptr 不会增加引用计数,因此可以打破 shared_ptr 的循环引用。
通常做法是 parent 类持有 child 的 shared_ptr, child 持有指向 parent 的 weak_ptr。这样也更符合语义。
weak_ptr如何检测指针是否被销毁
std::weak_ptr 可以通过 expired() 或 lock() 方法来检测管理的对象是否已经被销毁。
方法1:expired()
- 如果对象已经被销毁,返回 true。
- 如果对象仍然存活,返回 false。
#include <iostream>
#include <memory>
class MyClass {
public:
void show() { std::cout << "Object is alive!\n"; }
};
int main() {
std::weak_ptr<MyClass> wp; // 创建 weak_ptr
{
std::shared_ptr<MyClass> sp = std::make_shared<MyClass>(); // 创建 shared_ptr
wp = sp; // weak_ptr 观察 shared_ptr
std::cout << "Object still exists: " << std::boolalpha << !wp.expired() << "\n";
} // `sp` 离开作用域,对象被销毁
std::cout << "Object still exists: " << std::boolalpha << !wp.expired() << "\n"; // ✅ 这里会输出 false
return 0;
}
输出:
Object still exists: true
Object still exists: false
方法 2:lock()
- lock() 返回一个新的 shared_ptr:
- 如果对象仍然存活,返回有效的 shared_ptr。
- 如果对象已经销毁,返回空的 shared_ptr(nullptr)。
#include <iostream>
#include <memory>
class MyClass {
public:
void show() { std::cout << "Object is alive!\n"; }
};
int main() {
std::weak_ptr<MyClass> wp; // 创建 weak_ptr
{
std::shared_ptr<MyClass> sp = std::make_shared<MyClass>(); // 创建 shared_ptr
wp = sp; // weak_ptr 观察 shared_ptr
if (auto sp2 = wp.lock()) { // 尝试获取 shared_ptr
sp2->show();
} else {
std::cout << "Object already deleted\n";
}
} // `sp` 离开作用域,对象被销毁
if (auto sp2 = wp.lock()) {
sp2->show();
} else {
std::cout << "Object already deleted\n"; // ✅ 这里会输出
}
return 0;
}
输出:
Object is alive!
Object already deleted
结论
方法 | 用途 | 适用场景 |
---|---|---|
expired() | 检查对象是否销毁 | 只想知道对象是否还活着,不需要访问对象 |
lock() | 获取 shared_ptr 访问对象 | 需要访问对象,并避免悬挂指针 |
建议:
只检查对象是否存活时,使用 expired()。
需要访问对象时,使用 lock(),防止访问已销毁对象导致未定义行为
如何将weak_ptr转换为shared_ptr
在 C++ 中,可以使用 weak_ptr.lock() 方法将 weak_ptr 转换为 shared_ptr。
- 如果对象仍然存在,lock() 返回一个有效的 shared_ptr,对象的引用计数会增加。
- 如果对象已被销毁,lock() 返回一个空的 shared_ptr(nullptr)。
#include <iostream>
#include <memory>
class MyClass {
public:
void show() { std::cout << "Object is alive!\n"; }
};
int main() {
std::weak_ptr<MyClass> wp; // 创建 weak_ptr
{
std::shared_ptr<MyClass> sp = std::make_shared<MyClass>(); // 创建 shared_ptr
wp = sp; // weak_ptr 观察 shared_ptr
// 使用 lock() 转换为 shared_ptr
std::shared_ptr<MyClass> sp2 = wp.lock();
if (sp2) {
sp2->show();
} else {
std::cout << "Object already deleted\n";
}
} // sp 离开作用域,对象被销毁
// 再次尝试转换
std::shared_ptr<MyClass> sp3 = wp.lock();
if (sp3) {
sp3->show();
} else {
std::cout << "Object already deleted\n"; // ✅ 这里会输出
}
return 0;
}
Object is alive!
Object already deleted
lock() 的作用
1. 防止悬挂指针:
- weak_ptr 本身不能直接访问对象,但 lock() 可以安全地检查对象是否存在,并返回 shared_ptr。
2. 增加引用计数:
- lock() 成功获取 shared_ptr 后,对象的引用计数会增加,确保对象在作用域内不会被释放。
注意
不要直接使用 expired() + lock(),这种做法可能会引发竞态条件:
if (!wp.expired()) {
std::shared_ptr<MyClass> sp = wp.lock(); // 可能 wp 在这里已经失效
}
正确做法:直接用 lock()
if (auto sp = wp.lock()) {
// 只有 sp 有效时才访问对象
sp->show();
}
总结
- 使用 lock() 来转换 weak_ptr 为 shared_ptr,确保对象存在。
- 避免 expired() + lock(),因为会有竞态条件。
- 适用于缓存、观察者模式等场景,防止悬挂指针。