智能指针面试题

1.介绍一下shared_ptr的底层实现

image-20250408093323071

std::shared_ptr的底层结构分为两部分:

  • 控制块(Control Block):一个动态分配的小型数据结构,用于存储引用计数和其他元信息。

    • 强引用计数(Strong Reference Count):记录有多少个 shared_ptr 引用了该对象。
    • 弱引用计数(Weak Reference Count):记录有多少个 std::weak_ptr 引用了该对象(用于支持 std::weak_ptr)。
    • 删除器(Deleter):可选的自定义函数,用于释放对象时执行特定操作。
    • 分配器(Allocator):可选的自定义分配器,用于管理内存分配和释放。
  • 原始指针(Raw Pointer):保存一个指向实际对象的原始指针

2.介绍一下shared_ptr的引用计数的操作

增加引用计数:当一个 shared_ptr 被拷贝(通过拷贝构造函数或赋值操作符)时,控制块中的强引用计数加 1。

减少引用计数:当一个 shared_ptr 被销毁或重置时,控制块中的强引用计数减 1。

  • 强引用计数变为 0:
    • 调用删除器(如果有)来释放原始指针指向的对象。
    • 如果弱引用计数也为 0,则释放控制块本身。

3.weak_ptr能直接访问对象吗

std::weak_ptr 本身不能直接访问对象std::weak_ptr 可以通过检查控制块的状态来判断对象是否仍然有效

间接访问:

  • 检查对象是否仍然有效: 调用 std::weak_ptr::lock() 方法,尝试将 std::weak_ptr 提升为一个 std::shared_ptr

    • 强引用计数为 0(即对象已经被销毁),lock() 返回一个空的 std::shared_ptr
    • 强引用计数大于 0(即对象仍然有效),lock() 返回一个新的 std::shared_ptr,并增加强引用计数。
  • 提升为 std::shared_ptr 如果 std::weak_ptr::lock() 成功返回一个有效的 std::shared_ptr,则可以通过这个 std::shared_ptr 访问对象。

4.shared_ptr和unique_ptr的区别

  • 所有权:

    • std::unique_ptr:实现了独占所有权语义。
    • std::shared_ptr:实现了共享所有权语义。
  • 性能与内存开销:

    • std::unique_ptr:性能较高,因为它不需要额外的内存开销来存储引用计数或其他元数据。
    • std::shared_ptr:性能较低,因为每次增加或减少引用计数都需要原子操作。还需要额外的控制块来存储引用计数和其他信息。
  • 移动语义 vs 复制语义:

    • std::unique_ptr:支持移动语义,但不支持复制语义。
    • std::shared_ptr:支持复制和赋值操作。

5.shared_ptr的循环引用问题是什么?如何解决?

循环引用: std::shared_ptr 的循环引用问题是指两个或多个对象通过 std::shared_ptr 相互持有对方,导致它们的强引用计数永远不会降为 0,从而无法释放内存。

解决方案:用 std::weak_ptr 替代部分 std::shared_ptr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <memory>

class B; // 前向声明
class A {
public:
std::shared_ptr<B> b_ptr; // A 持有 B 的 shared_ptr
~A() { std::cout << "A is destroyed\n"; }
};
class B {
public:
std::weak_ptr<A> a_ptr; // 使用 weak_ptr 持有 A,此处如果是shared_ptr就变成了循环引用
~B() { std::cout << "B is destroyed\n"; }
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();

a->b_ptr = b; // A 持有 B
b->a_ptr = a; // B 使用 weak_ptr 持有 A

// 此时 a 的强引用计数为 1,b 的强引用计数为 2
} // 离开作用域后,a 和 b 的局部变量被销毁,引用计数降为 0,对象被正确释放

6.shared_ptr是否线程安全?

image-20250408100616337

7.shared_ptr的构造方法有哪几种,为什么尽量使用make_shared?

构造方法:

  • 默认构造函数:

    1
    std::shared_ptr<int> ptr; // 空 shared_ptr
  • 从原始指针构造

    1
    2
    int* rawPtr = new int(42);
    std::shared_ptr<int> ptr(rawPtr); // 用裸指针初始化 shared_ptr

    注:这种方式需要手动分配内存(如 new),容易导致资源泄漏或双重释放。

  • 使用 std::make_shared

    1
    auto ptr = std::make_shared<int>(42);
  • 使用删除器:

    1
    std::shared_ptr<int> ptr(new int(42), [](int* p) { delete p; });

为什么尽量使用make_shared:

  • 减少内存分配次数:裸指针需要两次内存分配,而 std::make_shared 将对象和控制块合并到一次内存分配中,减少了内存分配的开销。

    1
    2
    3
    4
    5
    6
    // 使用裸指针构造 shared_ptr
    int* rawPtr = new int(42); // 分配对象
    std::shared_ptr<int> ptr1(rawPtr); // 分配控制块

    // 使用 make_shared
    auto ptr2 = std::make_shared<int>(42); // 合并对象和控制块的一次分配
  • 异常安全性: 在分配对象后、构造 std::shared_ptr 前抛出异常,则会导致内存泄漏。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 使用裸指针可能引发异常安全性问题
    try {
    int* rawPtr = new int(42); // 分配对象
    throw std::runtime_error("Error"); // 抛出异常,rawPtr 未被管理
    std::shared_ptr<int> ptr(rawPtr); // 这行代码不会被执行
    } catch (...) {
    // 内存泄漏
    }
    // 使用 make_shared 是安全的
    try {
    auto ptr = std::make_shared<int>(42); // 单步完成分配和管理
    throw std::runtime_error("Error"); // 异常安全,ptr 会正确释放资源
    } catch (...) {
    // 资源被正确释放
    }

image-20250408101900500