MST

星途 面试题库

面试题:C++ 智能指针:unique_ptr 和 shared_ptr 的深度对比与转换

详细对比 C++ 中 unique_ptr 和 shared_ptr 的特点、内存管理机制、性能差异。在何种情况下,需要将 unique_ptr 转换为 shared_ptr,如何安全有效地进行这种转换?请给出代码示例说明。
22.9万 热度难度
编程语言C++

知识考点

AI 面试

面试题答案

一键面试

1. unique_ptr 和 shared_ptr 的特点

  • unique_ptr
    • 独占所有权:一个 unique_ptr 拥有对对象的唯一所有权,同一时刻只能有一个 unique_ptr 指向给定对象。当 unique_ptr 被销毁时,它所指向的对象也会被销毁。
    • 不能复制unique_ptr 不能进行复制构造和赋值操作,但可以进行移动构造和移动赋值,这使得它在对象所有权转移时非常高效。
    • 轻量级unique_ptr 通常实现得非常轻量级,只包含一个指针成员,没有额外的引用计数开销。
  • shared_ptr
    • 共享所有权:多个 shared_ptr 可以指向同一个对象,通过引用计数来管理对象的生命周期。当最后一个指向对象的 shared_ptr 被销毁时,对象才会被释放。
    • 可复制shared_ptr 支持复制构造和赋值操作,每次复制都会增加引用计数,每次销毁都会减少引用计数。
    • 线程安全:在多线程环境下,shared_ptr 的引用计数操作是线程安全的,但对象的访问可能仍需要额外的同步机制。

2. 内存管理机制

  • unique_ptr
    • unique_ptr 对象生命周期结束(例如离开作用域)时,它会自动调用其析构函数。在析构函数中,unique_ptr 会检查它是否拥有对象(即指针是否非空),如果是,则调用 delete 来释放对象的内存。
    • 对于数组类型,unique_ptr 提供了 unique_ptr<T[]> 的特化版本,它会在析构时调用 delete[] 来释放数组内存。
    • 示例代码:
#include <memory>
class MyClass {
public:
    MyClass() { std::cout << "MyClass constructed" << std::endl; }
    ~MyClass() { std::cout << "MyClass destructed" << std::endl; }
};

int main() {
    std::unique_ptr<MyClass> ptr1 = std::make_unique<MyClass>();
    // 离开作用域时,ptr1 会自动销毁 MyClass 对象
    return 0;
}
  • shared_ptr
    • shared_ptr 使用引用计数来跟踪指向对象的 shared_ptr 数量。每次创建一个新的 shared_ptr 指向对象(例如通过复制构造或赋值),引用计数就会增加;每次 shared_ptr 被销毁,引用计数就会减少。
    • 当引用计数降为 0 时,shared_ptr 会自动调用 delete 来释放对象的内存。此外,shared_ptr 还可以自定义删除器,用于在引用计数为 0 时执行特定的清理操作。
    • 示例代码:
#include <memory>
class MyClass {
public:
    MyClass() { std::cout << "MyClass constructed" << std::endl; }
    ~MyClass() { std::cout << "MyClass destructed" << std::endl; }
};

int main() {
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
    std::shared_ptr<MyClass> ptr2 = ptr1; // 引用计数增加
    {
        std::shared_ptr<MyClass> ptr3 = ptr1; // 引用计数再次增加
    } // ptr3 离开作用域,引用计数减少
    // ptr1 和 ptr2 离开作用域时,引用计数减为 0,MyClass 对象被销毁
    return 0;
}

3. 性能差异

  • unique_ptr
    • 由于 unique_ptr 不涉及引用计数的开销,因此在性能上通常更高效,特别是在对象所有权频繁转移的场景下,因为移动操作的开销很小。
    • 它适合于对象所有权明确且单一的情况,例如函数内部局部对象的管理,不需要额外的引用计数维护成本。
  • shared_ptr
    • shared_ptr 的引用计数操作会带来一定的性能开销,包括增加、减少引用计数以及处理引用计数为 0 时的内存释放操作。
    • 在多线程环境下,由于引用计数操作需要保证线程安全,可能会涉及到原子操作,这进一步增加了开销。然而,在需要共享对象所有权的场景下,shared_ptr 提供了方便的内存管理机制,尽管有性能损失,但可以简化代码逻辑。

4. 将 unique_ptr 转换为 shared_ptr 的情况及方法

  • 转换情况
    • 当原本由 unique_ptr 独占管理的对象需要被多个地方共享时,就需要将 unique_ptr 转换为 shared_ptr。例如,在函数内部创建了一个 unique_ptr 管理的对象,现在需要将这个对象传递给多个不同的函数或模块,且这些函数或模块都需要在对象的生命周期内共享该对象,此时就需要进行转换。
  • 转换方法
    • 可以通过 std::shared_ptr 的构造函数来将 unique_ptr 转换为 shared_ptrstd::shared_ptr 提供了一个构造函数,接受一个 std::unique_ptr 参数,这种转换是安全有效的,因为 unique_ptr 会将对象的所有权转移给 shared_ptr
    • 示例代码:
#include <memory>
class MyClass {
public:
    MyClass() { std::cout << "MyClass constructed" << std::endl; }
    ~MyClass() { std::cout << "MyClass destructed" << std::endl; }
};

std::shared_ptr<MyClass> getSharedPtr() {
    std::unique_ptr<MyClass> uniquePtr = std::make_unique<MyClass>();
    // 将 unique_ptr 转换为 shared_ptr
    std::shared_ptr<MyClass> sharedPtr = std::shared_ptr<MyClass>(std::move(uniquePtr));
    return sharedPtr;
}

int main() {
    std::shared_ptr<MyClass> ptr = getSharedPtr();
    // 可以在其他地方继续使用 ptr 共享 MyClass 对象
    return 0;
}

在上述代码中,getSharedPtr 函数内部创建了一个 unique_ptr,然后通过 std::shared_ptr 的构造函数将其转换为 shared_ptr,并返回 shared_ptrstd::move 用于将 unique_ptr 的所有权转移给 shared_ptr,确保转换过程的正确性。