1.使用unique_ptr应在需要独占所有权且自动释放资源时;2.通过make_unique初始化更安全;3.只能移动不可复制,用std::move转移所有权;4.可作为函数返回值自动移动;5.支持自定义删除器处理非内存资源;6.适合存储于容器中管理对象集合;7.避免在需共享所有权时使用,应选shared_ptr;8.防止悬挂指针需避免移动后使用原指针、不返回裸指针、注意作用域;9.与多态结合时需基类有虚析构函数;10.选择智能指针依据所有权需求:独占选unique_ptr,共享选shared_ptr,观察选weak_ptr。

unique_ptr应该在你需要确保一个对象只有一个所有者,并且该对象会在所有者销毁时自动销毁的情况下使用。这是避免内存泄漏和简化资源管理的强大工具。

在独占所有权场景下,unique_ptr是你的首选。它明确表达了所有权的唯一性,并在超出作用域时自动释放所管理的资源。这比手动new和delete更安全,也更清晰。

解决方案
使用unique_ptr的关键在于理解它的移动语义。由于它代表独占所有权,所以不能被复制,只能被移动。

-
创建和初始化:
#include <memory> std::unique_ptr<int> ptr(new int(10)); // 直接初始化 auto ptr2 = std::make_unique<int>(20); // 推荐:更安全,避免异常安全问题
std::make_unique是推荐的初始化方式,因为它将内存分配和对象构造合二为一,避免了在new和构造函数之间抛出异常导致内存泄漏的风险。 -
所有权转移:
std::unique_ptr<int> ptr3 = std::move(ptr); // ptr的所有权转移到ptr3,ptr不再拥有对象
std::move是转移所有权的关键。移动后,原始的unique_ptr会变成空指针,尝试解引用它会导致未定义行为。 -
作为函数返回值:
std::unique_ptr<int> create_int(int value) { return std::make_unique<int>(value); // 返回时自动移动所有权 } auto ptr4 = create_int(30);函数返回值是移动所有权的常用方式。编译器会自动进行移动操作,无需显式调用
std::move。 -
自定义删除器:
如果你需要自定义资源释放的方式(例如,释放文件句柄或网络连接),可以使用自定义删除器。
void file_deleter(FILE* fp) { if (fp) { fclose(fp); } } std::unique_ptr<FILE, decltype(&file_deleter)> file_ptr(fopen("test.txt", "w"), file_deleter);这里,
file_deleter函数会在file_ptr销毁时被调用,确保文件句柄被正确关闭。注意decltype的使用,它可以自动推导函数指针的类型。 -
与容器一起使用:
unique_ptr可以安全地存储在容器中,例如std::vector。std::vector<std::unique_ptr<int>> vec; vec.push_back(std::make_unique<int>(40)); vec.push_back(std::make_unique<int>(50));
当
vec销毁时,它会依次销毁其中存储的unique_ptr,从而释放所有分配的int对象。
什么时候不应该使用unique_ptr?
如果你需要多个指针指向同一个对象,并且需要共享所有权,那么unique_ptr就不是合适的选择。你应该考虑使用shared_ptr。此外,如果对象的生命周期由程序的其他部分管理,例如由操作系统或第三方库管理,那么使用智能指针可能不是最佳方案。
如何避免在使用 unique_ptr 时出现悬挂指针?
悬挂指针(dangling pointer)是指向已经被释放的内存的指针。使用unique_ptr的主要目的之一就是避免悬挂指针,但错误的使用仍然可能导致问题。
-
避免在移动后使用原始指针:
unique_ptr移动所有权后,原始指针会变成空指针。尝试解引用原始指针会导致未定义行为。std::unique_ptr<int> ptr1 = std::make_unique<int>(60); std::unique_ptr<int> ptr2 = std::move(ptr1); // 不要在这里使用 ptr1,它已经为空 if (ptr1) { // 检查 ptr1 是否为空 *ptr1 = 70; // 错误!ptr1 为空指针 } -
避免返回指向
unique_ptr管理对象的裸指针:如果函数返回了
unique_ptr管理对象的裸指针,调用者可能会在unique_ptr销毁后仍然持有该指针,从而导致悬挂指针。int* get_int_ptr(std::unique_ptr<int>& ptr) { // 传递引用,避免所有权转移 return ptr.get(); // 返回裸指针 } std::unique_ptr<int> my_ptr = std::make_unique<int>(80); int* raw_ptr = get_int_ptr(my_ptr); // 在 my_ptr 销毁后,raw_ptr 将变成悬挂指针更好的做法是避免返回裸指针,或者使用引用。
-
注意作用域和生命周期:
确保
unique_ptr的生命周期覆盖了它所管理对象的使用范围。如果unique_ptr在对象被使用之前销毁,就会导致悬挂指针。int* ptr; { std::unique_ptr<int> my_ptr = std::make_unique<int>(90); ptr = my_ptr.get(); // ptr 指向 my_ptr 管理的对象 } // my_ptr 在这里销毁,ptr 变成悬挂指针 *ptr = 100; // 错误!ptr 是悬挂指针 -
使用调试工具:
使用调试工具(例如,Valgrind、AddressSanitizer)可以帮助你检测悬挂指针和其他内存错误。
unique_ptr 如何与多态一起使用?
unique_ptr 非常适合与多态一起使用,尤其是在实现面向对象设计时。它可以确保派生类对象的生命周期得到正确管理,即使它们是通过基类指针访问的。
-
存储基类指针:
你可以将指向派生类对象的
unique_ptr存储在基类指针的容器中。#include <iostream> #include <vector> class Base { public: virtual void print() { std::cout << "Base" << std::endl; } virtual ~Base() = default; // 必须是虚析构函数 }; class Derived : public Base { public: void print() override { std::cout << "Derived" << std::endl; } }; int main() { std::vector<std::unique_ptr<Base>> vec; vec.push_back(std::make_unique<Derived>()); // 存储派生类对象 vec.push_back(std::make_unique<Base>()); for (auto& ptr : vec) { ptr->print(); // 调用虚函数 } }重要的是,基类必须有一个虚析构函数。这确保了在销毁派生类对象时,会调用派生类的析构函数,从而避免资源泄漏。
-
工厂模式:
unique_ptr可以用于实现工厂模式,用于创建和返回不同类型的对象。std::unique_ptr<Base> create_object(int type) { if (type == 1) { return std::make_unique<Derived>(); } else { return std::make_unique<Base>(); } } auto obj = create_object(1); obj->print(); // 调用 Derived::print() -
避免对象切片:
在使用多态时,需要注意对象切片(object slicing)问题。对象切片是指将派生类对象赋值给基类对象时,派生类特有的信息会被丢失。使用
unique_ptr可以避免对象切片,因为它存储的是指针,而不是对象本身。// 错误示例:对象切片 Base base = Derived(); // Derived 对象被切片成 Base 对象 base.print(); // 调用 Base::print()
使用
unique_ptr可以避免这个问题:std::unique_ptr<Base> base_ptr = std::make_unique<Derived>(); base_ptr->print(); // 调用 Derived::print()
如何选择合适的智能指针(unique_ptr, shared_ptr, weak_ptr)?
选择合适的智能指针取决于你对对象所有权和生命周期的需求。
-
unique_ptr:- 用途: 独占所有权。一个对象只有一个所有者,并且该对象会在所有者销毁时自动销毁。
-
适用场景: 默认选择。当你确定一个对象只有一个所有者时,使用
unique_ptr。 - 优势: 开销最小,因为它没有引用计数。
-
shared_ptr:- 用途: 共享所有权。多个指针可以指向同一个对象,并且该对象会在所有指针都销毁时自动销毁。
- 适用场景: 当多个对象需要共享对一个资源的所有权时。例如,多个线程需要访问同一个数据结构。
- 优势: 方便共享资源,避免手动管理内存。
- 劣势: 引用计数会带来一定的开销。循环引用会导致内存泄漏。
-
weak_ptr:-
用途: 观察者。
weak_ptr不拥有对象的所有权,只是观察对象是否存在。如果对象被销毁,weak_ptr会自动变成空指针。 - 适用场景: 当你需要访问一个对象,但不想影响它的生命周期时。例如,缓存、观察者模式。
-
优势: 可以打破
shared_ptr之间的循环引用,避免内存泄漏。 -
使用方法: 需要先通过
weak_ptr::lock()方法获取一个shared_ptr才能访问对象。
-
用途: 观察者。
选择流程:
-
是否需要独占所有权? 如果是,选择
unique_ptr。 -
是否需要共享所有权? 如果是,选择
shared_ptr。 -
是否只需要观察对象,而不需要拥有所有权? 如果是,选择
weak_ptr。
避免循环引用:
在使用 shared_ptr 时,需要特别注意循环引用问题。循环引用是指两个或多个对象互相持有对方的 shared_ptr,导致它们的引用计数永远不会降为 0,从而造成内存泄漏。
可以使用 weak_ptr 来打破循环引用。例如:
class A;
class B;
class A {
public:
std::shared_ptr<B> b; // 如果这里使用 shared_ptr,会造成循环引用
};
class B {
public:
std::weak_ptr<A> a; // 使用 weak_ptr 打破循环引用
};










