MST

星途 面试题库

面试题:C++智能指针的原理与应用

解释C++中智能指针(如std::unique_ptr、std::shared_ptr、std::weak_ptr)的原理,说明它们各自的特点与适用场景。在一个复杂的类继承体系和资源管理场景下,如何合理选择和使用智能指针以避免内存泄漏和悬空指针问题,并编写示例代码。
30.5万 热度难度
编程语言C++

知识考点

AI 面试

面试题答案

一键面试

智能指针原理

  1. std::unique_ptr
    • 原理:std::unique_ptr 采用独占式所有权模型。它拥有指向对象的唯一指针,当 std::unique_ptr 被销毁时(例如离开其作用域),它所指向的对象也会被自动销毁。这是通过 delete 操作符来实现的,std::unique_ptr 的析构函数会调用 delete 来释放其管理的资源。
    • 特点:
      • 独占所有权,同一时间只有一个 std::unique_ptr 可以指向特定对象。
      • 不能进行拷贝构造和拷贝赋值操作,但可以进行移动构造和移动赋值操作,这使得它在性能上较为高效,因为移动操作通常是廉价的。
    • 适用场景:适用于资源管理中,某个对象只应由一个所有者负责的情况,例如在函数内部创建一个对象并返回,std::unique_ptr 可以安全地管理该对象的生命周期,而无需手动 delete
  2. std::shared_ptr
    • 原理:std::shared_ptr 采用引用计数机制。多个 std::shared_ptr 可以指向同一个对象,每个 std::shared_ptr 内部维护一个指向对象的指针以及一个引用计数。每当创建一个新的 std::shared_ptr 指向该对象时,引用计数加 1;当一个 std::shared_ptr 被销毁时,引用计数减 1。当引用计数变为 0 时,对象会被自动释放。
    • 特点:
      • 共享所有权,允许多个 std::shared_ptr 同时指向同一个对象,方便在不同的代码模块间共享资源。
      • 支持拷贝构造和拷贝赋值操作,内部的引用计数机制会自动更新。
      • 由于引用计数的维护,会带来一定的性能开销,尤其是在多线程环境下,需要额外的同步操作来保证引用计数的正确更新。
    • 适用场景:适用于需要在多个地方共享资源的场景,比如在不同的类实例之间传递一个对象,并且这些实例都可能在不同的时间点结束对该对象的使用,std::shared_ptr 可以确保对象在最后一个使用者结束使用后被正确释放。
  3. std::weak_ptr
    • 原理:std::weak_ptr 是为了配合 std::shared_ptr 解决循环引用问题而引入的。它指向由 std::shared_ptr 管理的对象,但并不增加对象的引用计数。std::weak_ptr 可以从一个 std::shared_ptr 创建,通过 lock() 方法可以尝试获取一个有效的 std::shared_ptr,如果对象已被释放(引用计数为 0),lock() 方法会返回一个空的 std::shared_ptr
    • 特点:
      • 不拥有对象的所有权,不会影响对象的引用计数。
      • 主要用于打破 std::shared_ptr 之间的循环引用,避免内存泄漏。
      • 可以用来检查 std::shared_ptr 所管理的对象是否仍然存在。
    • 适用场景:当存在可能的循环引用时,使用 std::weak_ptr 来打破循环。例如在双向链表中,节点之间可能存在相互引用,使用 std::weak_ptr 可以避免循环引用导致的内存泄漏。

在复杂类继承体系和资源管理场景下智能指针的选择与使用

  1. 选择智能指针
    • 如果一个对象只有一个所有者,并且不需要在不同地方共享,优先使用 std::unique_ptr,这样可以获得更好的性能和更清晰的所有权关系。
    • 当需要在多个地方共享资源,且资源的生命周期需要由多个使用者共同决定时,使用 std::shared_ptr。但要注意循环引用问题,如有可能出现循环引用,配合 std::weak_ptr 使用。
    • 在可能存在循环引用的类继承体系中,例如基类和派生类之间存在相互引用,或者不同派生类之间存在相互引用时,合理使用 std::weak_ptr 来打破循环。
  2. 示例代码
#include <iostream>
#include <memory>

class Base {
public:
    virtual ~Base() {
        std::cout << "Base destroyed" << std::endl;
    }
};

class Derived : public Base {
public:
    ~Derived() {
        std::cout << "Derived destroyed" << std::endl;
    }
};

// 演示 std::unique_ptr 的使用
std::unique_ptr<Base> createUnique() {
    return std::make_unique<Derived>();
}

// 演示 std::shared_ptr 和 std::weak_ptr 的使用
class Parent;
class Child {
public:
    std::weak_ptr<Parent> parent;
    ~Child() {
        std::cout << "Child destroyed" << std::endl;
    }
};

class Parent {
public:
    std::shared_ptr<Child> child;
    ~Parent() {
        std::cout << "Parent destroyed" << std::endl;
    }
};

int main() {
    // 使用 std::unique_ptr
    {
        auto up = createUnique();
        // up 离开作用域时,Derived 对象会自动销毁
    }

    // 使用 std::shared_ptr 和 std::weak_ptr
    {
        auto p = std::make_shared<Parent>();
        auto c = std::make_shared<Child>();
        p->child = c;
        c->parent = p;

        // 检查 child 指向的对象是否存在
        auto locked = c->parent.lock();
        if (locked) {
            std::cout << "Parent still exists" << std::endl;
        } else {
            std::cout << "Parent has been destroyed" << std::endl;
        }
    }
    // p 和 c 离开作用域时,Parent 和 Child 对象会自动销毁,由于使用了 std::weak_ptr 打破循环,不会造成内存泄漏
    return 0;
}

在上述代码中:

  • createUnique 函数演示了 std::unique_ptr 的使用,函数返回一个 std::unique_ptr<Base>,指向一个 Derived 对象,当 up 离开作用域时,Derived 对象会自动销毁。
  • ParentChild 类之间存在潜在的循环引用,通过在 Child 类中使用 std::weak_ptr 指向 Parent 来打破循环。当 pc 离开作用域时,ParentChild 对象都会被正确销毁,避免了内存泄漏。std::weak_ptrlock 方法可以检查所指向的对象是否仍然存在。