[C++面试] RAII资源获取即初始化(重点)-CSDN博客
一、入门
1、unique_ptr 和 shared_ptr 默认能否管理动态数组?需要注意什么?
unique_ptr:可通过特化删除器管理数组,需显式指定delete[]
std::unique_ptr<int[]> arr(new int[5]{1,2,3,4,5});
shared_ptr:默认使用delete
而非delete[]
,直接管理数组会导致未定义行为。需自定义删除器
std::shared_ptr<int> arr(new int[5], [](int* p) { delete[] p; });
2、当容器中存储智能指针时,需要注意什么?
- 容器的拷贝 / 移动操作会触发智能指针的拷贝 / 移动语义,引用计数自动管理。
- 避免存储悬空指针:确保容器中的智能指针生命周期正确。
std::vector<std::shared_ptr<int>> vec;
vec.push_back(std::make_shared<int>(42));
- shared_ptr 拷贝语义:容器拷贝时会增加引用计数,多个容器共享同一资源
- unique_ptr 移动语义:移动后原容器指针变为 nullptr,避免重复释放
std::vector<std::shared_ptr<int>> vec1 = {std::make_shared<int>(42)};
auto vec2 = vec1; // 引用计数变为2
std::vector<std::unique_ptr<int>> vec3 = {std::make_unique<int>(42)};
auto vec4 = std::move(vec3); // vec3变为空
3、为什么 auto_ptr
被废弃
- 不安全的所有权转移:auto_ptr通过拷贝构造或赋值转移所有权,原指针变为空,若后续误用会导致未定义行为。
- 与容器不兼容:auto_ptr无法存入STL容器,因容器元素需支持拷贝,而auto_ptr的拷贝会转移所有权
std::auto_ptr<int> p1(new int(5));
std::auto_ptr<int> p2 = p1; // p1变为空指针
std::cout << *p1; // 运行时崩溃
4、std::make_shared
和std::make_unique
的额外优势
- 异常安全性:若new与shared_ptr构造分离,中间发生异常会导致内存泄漏,而make_shared是原子操作。
- 内存优化:make_shared将对象与控制块合并分配,减少内存碎片(make_unique在C++14引入)
二、进阶
1、shared_ptr 的默认删除器和自定义删除器在内存管理上有何不同?
- 默认删除器:使用
delete
释放对象,无法处理非堆内存(如文件句柄)。 - 自定义删除器:可释放任意资源(如文件、网络连接),且不影响引用计数的线程安全。
// 自定义删除器释放文件句柄
std::shared_ptr<FILE> file(fopen("test.txt", "r"), [](FILE* f) { fclose(f); });
2、当智能指针所在函数抛出异常时,资源是否会被正确释放?
会被正确释放。智能指针利用 RAII 机制,在离开作用域时(包括异常场景)自动释放资源,避免内存泄漏。
void test() {
auto ptr = std::make_shared<int>(42);
throw std::runtime_error("Exception!"); // ptr会被正确释放
}
3、混合使用智能指针和原始指针可能导致什么问题?
悬空指针:若原始指针指向的对象被智能指针释放,后续使用原始指针会导致未定义行为
int* raw = new int(10);
std::shared_ptr<int> sp(raw);
delete raw; // sp变为悬空指针
重复释放:多个智能指针管理同一原始指针(如p2(raw))会导致重复释放。
4、 循环引用如何解决?
场景:父子节点互相持有shared_ptr
导致引用计数永不为零。
解决方案:将其中一个指针改为weak_ptr,如树形结构的父节点用weak_ptr,子节点用shared_ptr
class Node {
public:
std::shared_ptr<Node> child;
std::weak_ptr<Node> parent; // 打破循环
};
三、高阶
1、如何安全地在基类和派生类之间使用智能指针?
使用std::dynamic_pointer_cast
进行安全的向下转型
class Base {};
class Derived : public Base {};
// 创建子类实体,赋值给基类指针
std::shared_ptr<Base> base = std::make_shared<Derived>();
// 指向子类实体的基类指针,还原回子类
std::shared_ptr<Derived> derived = std::dynamic_pointer_cast<Derived>(base);
if (derived) { /* 安全使用 */ }
dynamic_pointer_cast 的安全转型,必须用于 shared_ptr 之间的转型
std::shared_ptr<Base> base = std::make_shared<Derived>();
if (auto derived = std::dynamic_pointer_cast<Derived>(base)) {
// 安全使用
}
使用
static_pointer_cast
、dynamic_pointer_cast
进行类型转换,保留引用计数
2、shared_ptr 的引用计数如何保证线程安全?
- 使用原子操作(如
std::atomic<int>
)或互斥锁保护引用计数。 - C++ 标准库中的
std::shared_ptr
保证引用计数操作的原子性,但对象访问仍需额外同步。
3、 shared_ptr 的性能瓶颈是什么?如何优化?
- 开销:引用计数的原子操作和控制块的内存分配。
- 优化:
- 使用
std::make_shared
减少内存分配次数。一次分配控制块和对象内存,减少内存碎片,提升缓存利用率 - 避免在性能关键路径中频繁操作引用计数。
- 对性能要求极高时,可考虑对象池或手动管理资源。
- 使用
4、 智能指针与 RAII 类、对象池、裸指针相比,各自的优缺点是什么?
- 智能指针:自动管理内存,避免泄漏,但有额外开销。
- RAII 类:可管理任意资源,但需自定义类。
- 对象池:减少内存分配次数,适合高频创建销毁的场景。
- 裸指针:灵活但易出错,需手动管理。
5、可以在构造函数中调用shared_from_this()
吗?
不能,因对象尚未被shared_ptr
管理,引发std::bad_weak_ptr
异常
正确用法:仅在对象已被shared_ptr管理后调用,如在成员函数中。
class Worker : public std::enable_shared_from_this<Worker> {
public:
void start() {
auto self = shared_from_this(); // 安全
}
// 构造函数中调用会导致异常!
};
四、其他
1、为什么 unique_ptr 只能通过std::move
转移所有权?
- 禁止拷贝以确保独占性,
std::move
显式转移所有权,避免隐式错误。
2、智能指针类型选择
工厂模式用 unique_ptr,缓存用 shared_ptr
3、通过get()
获取裸指针后,若其生命周期长于shared_ptr
,会导致悬空指针
std::shared_ptr<int> sp = std::make_shared<int>(42);
int* raw = sp.get();
sp.reset(); // raw成为悬空指针
4、误用weak_ptr:未检查lock()结果直接使用,引发访问已释放对象。