[C++面试] RAII资源获取即初始化(重点)-CSDN博客
[C++面试] 智能指针面试点(重点)-CSDN博客
[C++面试] 智能指针面试点(重点)续1-CSDN博客
[C++面试] 智能指针面试点(重点)续2-CSDN博客
[C++面试] 智能指针面试点(重点)续3-CSDN博客
一、入门
1、使用智能指针或者RAII技术过程中,如果出现了异常,会发生什么?
栈展开保证析构:C++的异常处理机制会确保所有已构造的局部对象析构函数被调用,无论是否发生异常
RAII
- RAII(资源获取即初始化)的核心是通过对象的构造函数获取资源,析构函数释放资源。
- 当异常发生时,C++会执行栈展开(Stack Unwinding),即销毁当前作用域及调用链中所有局部对象,触发析构函数。这一机制确保了即使抛出异常,资源仍会被自动释放
智能指针同理:栈展开(Stack Unwinding)
void processData() {
auto ptr = std::make_unique<int>(42); // RAII对象构造时获取内存
throw std::runtime_error("模拟异常"); // 抛出异常
// 后续代码不会执行,但ptr的析构函数仍会被调用
} // 栈展开时,unique_ptr析构,自动释放内存
shared_ptr有什么不同的吗?
void sharedExample() {
auto ptr1 = std::make_shared<int>(10);
auto ptr2 = ptr1; // 引用计数+1
throw std::logic_error("错误");
} // 栈展开时,引用计数减至0,释放内存
析构顺序:局部对象ptr2
和ptr1
会按照构造的逆序依次析构
ptr2
析构:引用计数从2减少到1。ptr1
析构:引用计数从1减少到0,此时对象的内存被释放,控制块(包含引用计数)也被销毁
多线程场景下呢?
shared_ptr
的引用计数(_M_use_count
)是原子变量(_Atomic_word
),析构时的递减操作是线程安全的.
即使异常发生在多线程环境下,引用计数的修改也不会导致竞争条件
二、进阶
1、智能指针相互间赋值
1.1 unique_ptr
之间的赋值
左值不能直接赋值:unique_ptr
表示独占所有权,禁止普通左值之间的赋值,避免多个指针指向同一对象。
unique_ptr<int> a = make_unique<int>(5);
unique_ptr<int> b;
b = a; // 编译错误,a 是左值,直接赋值违反独占原则
右值可通过移动赋值:若为右值(如临时对象或通过 std::move
转换),则可移动赋值。此时原 unique_ptr
失去所有权(变为 nullptr
)
unique_ptr<int> a = make_unique<int>(5);
unique_ptr<int> b = std::move(a); // 合法,a 变为 nullptr,b 接管对象
1.2 unique_ptr、
shared_ptr
的赋值
unique_ptr
右值可赋值给 shared_ptr
:shared_ptr
有显式构造函数,可接收 unique_ptr
右值(如临时对象或 std::move
转换后的对象)。此时 shared_ptr
接管对象所有权:
unique_ptr<int> uptr = make_unique<int>(10);
shared_ptr<int> sptr1 = std::move(uptr); // 合法,uptr 变为 nullptr
shared_ptr<int> sptr2(make_unique<int>(20)); // 合法,临时 unique_ptr 作为右值
shared_ptr<int> sptr3 = make_unique<int>(42); // 隐式移动构造。在构造shared_ptr时传入unique_ptr的右值(如函数返回值),编译器会自动完成移动操作
unique_ptr
左值不可直接赋值给 shared_ptr
:若直接用 unique_ptr
左值初始化 shared_ptr
,会编译错误
unique_ptr<int> uptr = make_unique<int>(10);
shared_ptr<int> sptr3(uptr); // 编译错误,uptr 是左值
1.3 总结
- 允许转换的方向:
unique_ptr
→shared_ptr
(通过移动语义)。 - 禁止转换的方向:
shared_ptr
→unique_ptr
unique_ptr
之间仅允许右值移动赋值;unique_ptr
可通过右值(如std::move
或临时对象)赋值给shared_ptr
,但左值不行。
2、 shared_ptr<int*>有什么问题?
// 正确用法:shared_ptr<int> 管理 int 对象
shared_ptr<int> sp1 = make_shared<int>(10); // 直接管理 int 值 10
// 非常规用法:shared_ptr<int*> 管理 int* 指针
// 管理 int*,需确保 int* 指向的内存正确释放(此处虽合法但不推荐)
shared_ptr<int*> sp2 = make_shared<int*>(new int(20));
shared_ptr<int>
:直接管理一个 int
类型的对象,智能指针内部存储的是 int
对象的指针(如 int*
),并负责该 int
对象的内存释放。符合智能指针的设计初衷,直接管理对象内存。
shared_ptr<int*>
:管理一个 int*
类型的指针(即指针本身成为智能指针管理的对象)。这种用法违背了智能指针简化对象管理的原则,因为 int*
指向的对象仍需确保其生命周期正确(虽然此处 int*
由 new int
分配,delete
合法,但代码逻辑易混淆)。
std::shared_ptr<int*> sptr(
new int*(new int(42)),
[](int** p) {
delete *p; // 释放内层 int 对象
delete p; // 释放外层 int* 指针
}
);
需在自定义删除器中显式释放内层内存。
使用 shared_ptr<T[]>
(C++17 及以上)
int main() {
// 创建 shared_ptr 管理动态数组(C++17 起支持)
std::shared_ptr<int[]> arr(new int[5]{1, 2, 3, 4, 5});
// 直接通过 operator[] 访问元素
for (int i = 0; i < 5; ++i) {
arr[i] = i * 10; // 修改元素
std::cout << arr[i] << " "; // 输出:0 10 20 30 40
}
// 无需手动释放,shared_ptr 自动调用 delete[]
return 0;
}
自定义删除器(兼容 C++11/14)
int main() {
// 定义删除器(Lambda 表达式)
auto array_deleter = [](int* ptr) {
delete[] ptr; // 必须显式调用 delete[]
std::cout << "动态数组内存已释放\n";
};
// 创建 shared_ptr 并传入删除器
std::shared_ptr<int> arr(new int[5]{1, 2, 3, 4, 5}, array_deleter);
// 通过 get() 获取原始指针访问元素
int* raw_ptr = arr.get();
for (int i = 0; i < 5; ++i) {
raw_ptr[i] = i * 10; // 修改元素
std::cout << raw_ptr[i] << " "; // 输出:0 10 20 30 40
}
// 析构时自动调用 array_deleter
return 0;
}
三、其他
警告:
永远不要将资源分配结果赋值给原始指针。无论使用哪种资源分配方法,都应当立即将资源指针存储在智能指针 unique_ptr 或 shared_ptr 中,或使用其他 RAII 类。RAII 代表 Resource Acquisition Is Initialization (资源获取即初始化)。RAII 类获取某个资源的所有权,并在适当的时候进行释放。—— 《C++20高级编程(上册)》