C++智能指针概念理解的面试题
C++智能指针概念理解的面试题
第一部分:基础概念
-
解释
std::unique_ptr
和std::shared_ptr
在以下方面的区别:- 所有权语义
- 性能开销
- 自定义删除器的存储方式
- 是否支持数组类型
答案:
所有权语义:
unique_ptr:独占所有权,不能复制,只能移动shared_ptr:共享所有权,通过引用计数管理,可以复制
性能开销:
unique_ptr:几乎无额外开销(等同于原始指针)shared_ptr:有控制块和引用计数的开销
自定义删除器的存储方式:
unique_ptr:删除器是类型的一部分,直接存储在对象中(无额外开销)shared_ptr:删除器存储在控制块中(类型擦除,有额外开销)
是否支持数组类型:
unique_ptr:通过unique_ptr<T[]>显式支持数组shared_ptr:不直接支持数组,需自定义删除器
-
考虑以下代码:
std::shared_ptr<int> p1(new int(42));
std::shared_ptr<int> p2 = std::make_shared<int>(42);
- 这两种初始化方式在内存分配上有何不同?
- 为什么推荐使用
make_shared
? - 在什么情况下不能使用
make_shared
?
答案:
内存分配差异:
shared_ptr<int> p1(new int(42))
:两次分配(对象和控制块分开)make_shared<int>(42)
:一次分配(对象和控制块合并)
推荐make_shared
的原因:
- 更高性能(单次分配)
- 更安全(避免内存泄漏)
- 更好的缓存局部性
不能使用make_shared
的情况:
- 需要自定义删除器
- 需要大括号初始化
- 需要weak_ptr长期存在而对象可被释放的场景
第二部分:深入实现
-
假设C++标准库中没有提供
std::weak_ptr
,你如何仅使用std::shared_ptr
来实现一个弱引用指针?请描述你的设计方案,包括:- 如何检测所指向的对象是否已被释放
- 如何实现
lock()
操作来获取可用的shared_ptr
- 如何避免循环引用
答案:
设计方案:
- 使用
shared_ptr
的引用计数结构扩展 - 添加"弱引用计数"跟踪观察者数量
- 对象释放条件:强引用=0(无论弱引用)
实现lock()
:
- 检查强引用计数>0
- 如果对象存在,增加强引用计数并返回新
shared_ptr
- 否则返回空
shared_ptr
避免循环引用:
- 控制块(control block)通常包含哪些信息?
- 在多线程环境下,引用计数如何保证原子性?
- 为什么
std::shared_ptr
的引用计数使用原子操作而不是简单的整数?
答案:
控制块内容:
- 强引用计数
- 弱引用计数
- 自定义删除器
- 分配器(如使用)
- 指向被管理对象的指针
原子性保证:
- 使用原子操作(如
std::atomic
)修改引用计数 - 内存序保证(通常
memory_order_relaxed
用于计数) - 控制块本身线程安全
使用原子操作的原因:
-
多线程环境下安全修改计数
-
避免数据竞争
-
保证内存可见性
第三部分:高级应用
void file_deleter(FILE* fp) {std::fclose(fp);
}std::unique_ptr<FILE, decltype(&file_deleter)> fp(std::fopen("test.txt", "r"), file_deleter);
- 为什么这里
decltype(&file_deleter)
是必要的? - 如果改用lambda表达式作为删除器,代码应该如何修改?
- 比较
std::unique_ptr
和std::shared_ptr
在自定义删除器存储方式上的差异
答案:
decltype(&file_deleter)
必要性:
unique_ptr
的删除器是类型的一部分- 必须明确指定删除器类型
lambda删除器:
auto deleter = [](FILE* fp) { std::fclose(fp); };
std::unique_ptr<FILE, decltype(deleter)> fp(std::fopen("test.txt", "r"), deleter);
存储方式差异:
-
unique_ptr
:删除器作为模板参数,直接存储 -
shared_ptr
:删除器类型擦除,存储在控制块
struct Base { virtual ~Base() = default; };
struct Derived : Base { /*...*/ };std::shared_ptr<Base> p = std::make_shared<Derived>();
- 解释为什么这个方案是类型安全的
- 如果Base的析构函数不是虚函数,会发生什么?
- 如何设计一个工厂函数,返回基类指针但能正确删除派生类对象?
答案:
类型安全原因:
make_shared
创建完整派生类对象- 虚析构函数确保正确析构顺序
- 共享指针保持完整类型信息
非虚析构函数问题:
- 派生类部分不会被析构
- 资源泄漏
- 未定义行为
工厂函数设计:
template<typename Derived, typename... Args>
std::shared_ptr<Base> create(Args&&... args) {return std::shared_ptr<Base>(new Derived(std::forward<Args>(args)...));
}
第四部分:陷阱与最佳实践
std::shared_ptr<int> create_shared() {int* raw = new int(42);std::shared_ptr<int> p1(raw);std::shared_ptr<int> p2(raw);return p1;
}
- 这段代码会导致什么问题?
- 如何修改才能使其安全?
- 解释为什么
std::enable_shared_from_this
能解决类似问题
答案:
问题:
- 两个
shared_ptr
独立管理同一原始指针 - 会导致双重释放
- 引用计数不共享
修改方案:
std::shared_ptr<int> create_shared() {auto p1 = std::make_shared<int>(42);std::shared_ptr<int> p2 = p1; // 共享所有权return p1;
}
enable_shared_from_this
作用:
-
允许对象安全地生成指向自身的
shared_ptr
-
内部使用weak_ptr避免循环引用
-
确保所有
shared_ptr
共享同一控制块 -
讨论在以下场景中智能指针的使用策略:
- 作为类成员变量
- 在容器中存储动态分配的对象
- 跨线程传递对象所有权
- 与第三方C库交互
答案:
类成员变量:
- 优先
unique_ptr
表达独占 - 需要共享时用
shared_ptr
- 观察用
weak_ptr
容器存储:
- 优先
unique_ptr
(明确所有权) - 需要共享时用
shared_ptr
- 考虑
vector<unique_ptr>
替代vector<Base*>
跨线程传递:
shared_ptr
引用计数线程安全- 对象本身需额外同步
- 避免跨线程传递原始指针
与C库交互:
- 自定义删除器包装C接口
unique_ptr
管理C资源- 注意所有权转移语义