MST

星途 面试题库

面试题:C++内存管理之中的智能指针

请详细阐述C++ 中智能指针(如std::unique_ptr、std::shared_ptr 和 std::weak_ptr)的原理及应用场景。在一个复杂的对象继承体系中,如果使用智能指针管理对象,需要注意哪些问题以避免内存泄漏和悬空指针?
29.2万 热度难度
编程语言C++

知识考点

AI 面试

面试题答案

一键面试

std::unique_ptr

  1. 原理
    • std::unique_ptr 是一种独占式智能指针,它拥有对所指向对象的唯一所有权。当 std::unique_ptr 被销毁(例如离开其作用域)时,它会自动释放所指向的对象。这是通过在析构函数中调用 delete 来实现的。
    • 它采用移动语义,不支持拷贝构造和赋值运算符(除非是从 std::nullptr_t 赋值),但支持移动构造和移动赋值。这使得在函数间传递 std::unique_ptr 时,不会发生对象的拷贝,只是所有权的转移。
  2. 应用场景
    • 当你希望一个对象只有一个所有者,并且在所有者销毁时对象也随之销毁的情况。例如,在函数内部创建一个临时对象,并且希望函数结束时对象自动释放,就可以使用 std::unique_ptr
    • 管理动态分配的数组时,可以使用 std::unique_ptr<T[]>,它会在析构时调用 delete[] 来正确释放数组内存。

std::shared_ptr

  1. 原理
    • std::shared_ptr 是一种共享式智能指针,允许多个 std::shared_ptr 指向同一个对象。它通过引用计数机制来管理对象的生命周期。每个 std::shared_ptr 内部都维护一个指向引用计数的指针,当有新的 std::shared_ptr 指向该对象时,引用计数加 1;当 std::shared_ptr 被销毁时,引用计数减 1。当引用计数变为 0 时,对象被自动释放。
    • 它支持拷贝构造和赋值运算符,这些操作会相应地增加或减少引用计数。
  2. 应用场景
    • 当多个对象需要共享对某个资源的访问时,std::shared_ptr 非常有用。比如在实现缓存机制时,多个模块可能需要访问同一个缓存对象,使用 std::shared_ptr 可以确保缓存对象在所有使用者都不再需要时才被释放。
    • 在实现观察者模式时,主题对象和观察者对象之间可以通过 std::shared_ptr 来共享状态信息,同时保证对象生命周期的正确管理。

std::weak_ptr

  1. 原理
    • std::weak_ptr 是一种弱引用智能指针,它指向由 std::shared_ptr 管理的对象,但不会增加对象的引用计数。它主要用于解决 std::shared_ptr 可能出现的循环引用问题。std::weak_ptr 可以从 std::shared_ptr 创建,通过 lock 成员函数可以尝试获取一个有效的 std::shared_ptr,如果对象已经被释放,lock 会返回一个空的 std::shared_ptr
  2. 应用场景
    • 在存在循环引用的情况下,比如两个类 AB 相互引用,如果都使用 std::shared_ptr,会导致对象无法释放。这时可以在其中一个类中使用 std::weak_ptr 来打破循环引用。例如,A 类中包含一个指向 B 类对象的 std::shared_ptr,而 B 类中包含一个指向 A 类对象的 std::weak_ptr
    • 用于实现缓存机制中,当缓存对象可能被其他地方释放,但又希望在需要时检查其是否还存在的情况。

在复杂对象继承体系中使用智能指针的注意事项

  1. 内存泄漏
    • 避免循环引用:在继承体系中,如果存在基类和派生类之间相互引用或者派生类之间相互引用的情况,要特别小心循环引用。如前面提到的,使用 std::weak_ptr 来打破循环引用,防止内存泄漏。例如,基类中持有指向派生类对象的 std::shared_ptr,派生类中可以持有指向基类对象的 std::weak_ptr
    • 正确的析构函数:确保基类的析构函数是虚函数。当通过基类的智能指针删除派生类对象时,如果基类析构函数不是虚函数,派生类的析构函数不会被调用,可能导致资源未正确释放。例如:
class Base {
public:
    // 应该声明为虚析构函数
    virtual ~Base() {}
};
class Derived : public Base {
public:
    ~Derived() {}
};
std::shared_ptr<Base> ptr = std::make_shared<Derived>();
// 如果Base的析构函数不是虚函数,Derived的析构函数可能不会被调用
  1. 悬空指针
    • 注意对象生命周期:在复杂继承体系中,要清楚对象的创建和销毁时机。例如,当通过基类智能指针获取派生类对象的指针并保存时,要确保在使用该指针时,对应的对象仍然存在。一种解决方法是在保存指针的同时,保存一个对应的 std::weak_ptr,每次使用指针前通过 weak_ptrlock 方法检查对象是否还存在。
    • 避免意外的对象释放:当使用 std::unique_ptr 时,要注意不要在对象还在被其他地方使用时就意外地移动了 std::unique_ptr,导致悬空指针。在复杂继承体系中,要仔细考虑对象所有权的转移,确保在对象不再被需要之前不会被提前释放。