智能指针是 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()有以下几个优势:
std::make_unique()能保证异常安全。
如果在构造对象的过程中抛出异常,内存会被自动释放,从而避免内存泄漏。std::make_unique()能让代码更清晰、更简洁。
它省去了显式指定所要分配类型的需要,因为模板实参会自动推导出来。new手动分配内存相比,编译器有机会更有效地优化std::make_unique(),从而可能带来性能提升。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;
在 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避免悬空指针:
创建一个std::weak_ptr:不要直接持有std::shared_ptr,而是为共享对象创建一个std::weak_ptr。
检查有效性:在使用std::weak_ptr之前,先用expired()函数检查它是否有效。
如果关联的共享对象已被删除,该函数会返回true。
锁定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专门用于打破循环依赖或观察共享对象,但并不常用。