智能

智能指针 属于 C++

1 个练习

关于 智能指针

智能指针是 C++ 的一项现代特性,旨在提供自动内存管理,帮助避免内存泄漏和悬空指针,这两者通常与裸指针相关。 智能指针相当于裸指针的包装器,额外提供一些功能,例如在不再需要指针时自动释放内存。

通用语法

智能指针通常在 C++ 标准库中以类模板的形式实现。 最常用的两种智能指针是std::unique_ptr和std::shared_ptr。

独占指针

std::unique_ptr是一种独占对象所有权的智能指针。 它保证在任何时刻都只有一个std::unique_ptr对象拥有该资源。 当拥有所有权的std::unique_ptr被销毁或重置时,它会自动析构对象并释放其内存。

#include <memory>
// Declaring and defining a unique pointer
auto rightful_king_of_england = std::make_unique<std::string>("Excalibur");

// Unique pointers cannot be copied or assigned
auto mordred = rightful_king_of_england; // Error: Cannot copy a unique_ptr

std::make_unique() 的优势

创建std::unique_ptr时,最好使用std::make_unique(),而不是直接用new分配内存。 std::make_unique()有以下几个优势:

  1. 异常安全:std::make_unique()能保证异常安全。 如果在构造对象的过程中抛出异常,内存会被自动释放,从而避免内存泄漏。
  2. 清晰:使用std::make_unique()能让代码更清晰、更简洁。 它省去了显式指定所要分配类型的需要,因为模板实参会自动推导出来。
  3. 优化机会:与用new手动分配内存相比,编译器有机会更有效地优化std::make_unique(),从而可能带来性能提升。
  4. 避免误用:手动构造std::unique_ptr时,有可能会删除底层资源。 而当std::unique_ptr在作用域结束时试图删除它,就会导致未定义行为。

共享指针

std::shared_ptr是一种允许多个std::shared_ptr对象共享同一资源所有权的智能指针。 它会记录有多少个共享指针在引用该资源,只有当最后一个拥有该资源的共享指针离开作用域或被重置时,才会释放内存。

// Declaring and defining a shared pointer to a dynamically allocated string
auto martian_congressional_republic = std::make_shared<std::string>("protomolecule");

// Creating more shared pointer that shares ownership
auto outer_planets_alliance = martian_congressional_republic;
auto united_nations = martian_congressional_republic;
Caution

在 C++17 及更低版本中,并不直接支持通过std::make_shared<T[]>把std::shared_ptr用于数组。 虽然可以用std::make_shared<T[]>分配数组,但直接由它来创建共享指针可能会导致未定义行为,因为单个对象与数组在内存管理上存在差异。 要想用共享指针有效地管理数组,可以考虑改用std::vector或自定义删除函数。 在 C++17 中处理数组分配和共享指针时,务必确保与你的编译器和标准库实现兼容。

std::make_shared() 的优势

与std::make_unique()类似,std::make_shared()也带来诸多好处,例如更高的内存效率、异常安全和更好的可读性。 它把控制块和被管理对象的内存分配合并为一次操作,从而提升效率并降低内存泄漏的风险。 此外,模板实参的自动推导让代码更简洁,也提升了可读性。 在 C++ 中使用std::shared_ptr对象时,std::make_shared()有助于写出更干净、更安全、更高效的代码。

弱指针

std::weak_ptr是与std::shared_ptr配套的类,它提供一种不拥有所有权的“弱”引用,指向由共享指针管理的对象。 它允许访问资源,却不会影响资源的生命周期。 在需要打破循环引用以避免内存泄漏的场景中,弱指针很有用。

// Creating a shared pointer
auto your_account = std::make_shared<std::string>("secret_subscription_password");
// Creating a shared pointer that shares ownership
auto your_flatmates_account = your_account;

// Creating a weak pointer from the shared pointer
auto your_flatmates_boyfriends_account = your_flatmates_account;
// if your_account and your_flatmates_account are deleted, there is no more reference to the shared pointer.
// your_flatmates_boyfriends_account will be a null pointer and cannot use the associated object any longer.

std::weak_ptr 与循环所有权

std::weak_ptr的设计目的,是解决使用std::shared_ptr时可能出现的循环所有权问题,也叫循环引用。

在循环所有权的情形中,两个或更多std::shared_ptr对象互相引用,形成一个环,其中任何对象都无法被删除,因为它们彼此持有强引用,从而导致内存泄漏。

std::weak_ptr为这个问题提供了一种解决办法:允许对共享对象持有弱引用,而不增加其引用计数。 这意味着它可以观察并访问共享对象,却不会阻止该对象被删除。 如果指向该共享对象的所有强引用都被释放,该对象就会被销毁,观察它的弱指针也会自动重置为nullptr。

下面是一个简短的示例,演示如何用std::weak_ptr打破循环所有权:

struct Node {
    Node() { std::cout << "Node constructed\n"; }
    ~Node() { std::cout << "Node destructed\n"; }

    std::weak_ptr<Node> next;
};

// Creating two nodes
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();

// Creating a cycle by making each node point to the other
node1->next = node2;
node2->next = node1;

// Release strong references to nodes
node1.reset();
node2.reset();

// Nodes are automatically destructed due to weak references

“Node constructed”会打印两次,随后是两行“Node destructed”。

当next不是弱指针而是std::shared_ptr时,就会造成循环所有权的局面,导致内存泄漏,因为两个节点会互相持有强引用,阻止彼此被销毁。

struct Node {
    Node() { std::cout << "Node constructed\n"; }
    ~Node() { std::cout << "Node destructed\n"; }

    std::shared_ptr<Node> next; // Change to shared_ptr
};

在这种情况下,node1和node2通过std::shared_ptr互相持有强引用,形成一个环。 当程序退出并尝试用reset()释放这些强引用时,两个节点的引用计数都仍然不为零,因为每个节点都持有对另一个节点的强引用。 因此,这些节点不会被析构,从而导致内存泄漏。 “Node constructed”会打印两次,但之后不会出现任何一行“Node destructed”。

悬空指针与std::weak_ptr

当指针引用了一个已被删除的对象时,就会产生悬空指针,解引用该指针时会导致未定义行为。 std::weak_ptr通过提供对std::shared_ptr所管理对象的一种不拥有所有权的弱引用,帮助避免悬空指针。 下面介绍如何用std::weak_ptr避免悬空指针:

  1. 创建一个std::weak_ptr:不要直接持有std::shared_ptr,而是为共享对象创建一个std::weak_ptr。

  2. 检查有效性:在使用std::weak_ptr之前,先用expired()函数检查它是否有效。 如果关联的共享对象已被删除,该函数会返回true。

  3. 锁定std::weak_ptr:要安全地访问共享对象,可以使用lock()函数;如果对象仍然有效,它会返回一个指向同一对象的std::shared_ptr。 如果对象已被删除,lock()会返回一个空的std::shared_ptr。

下面是一个代码示例,演示如何用std::weak_ptr避免悬空指针:

auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();

// Create weak pointers
node1->next = node2;
node2->next = node1;

// Check validity and lock
if (auto lockedNode2 = node1->next.lock()) {
    // Use lockedNode2 safely
} else {
    // Handle case where node2 has been deleted
}

在这个示例中,node1->next和node2->next都是std::weak_ptr对象。 在访问共享对象之前,先用expired()函数检查有效性。 然后用lock()安全地访问共享对象。 这种做法能确保在使用std::weak_ptr时避免悬空指针。

使用建议

默认使用智能指针:用std::unique_ptr表示独占所有权,用std::shared_ptr表示共享所有权。 把裸指针留给不拥有所有权的引用,或者在与遗留代码交互时使用。 大多数情况下,std::unique_ptr足以处理独占所有权,因为它提供轻量的内存管理,没有引用计数的开销。 std::shared_ptr应当少用,因为它会带来开销和复杂性,除非确实需要共享所有权。 std::weak_ptr专门用于打破循环依赖或观察共享对象,但并不常用。

通过 GitHub 编辑 该链接会在新窗口或标签页中打开

学习 智能指针