C++ 中如何安全地使用指针和智能指针?

在 C++ 开发中,指针是最常见但也最容易出错的语言特性之一。尤其在大型项目中,指针相关的内存泄漏、悬空指针以及野指针等问题会导致程序崩溃、数据损坏甚至安全漏洞。为了解决这些问题,C++11 之后引入了多种智能指针(std::unique_ptrstd::shared_ptrstd::weak_ptr)以及 RAII(Resource Acquisition Is Initialization)等机制。本文将从使用指针的常见错误、智能指针的适用场景、以及如何正确使用智能指针来避免常见陷阱展开讨论。

1. 传统指针常见错误与风险

错误类型 典型示例 可能后果
失去所有权 int* p = new int; delete p; p = nullptr; 失去指针后仍然使用会导致悬空指针
多重释放 delete p; delete p; 运行时异常或内存破坏
野指针 int* q = nullptr; *q = 10; 程序崩溃
未初始化 int* r; *r = 5; 未定义行为
内存泄漏 int* t = new int; // 未 delete 内存占用持续增长

为避免上述错误,开发者需遵循“所有权规则”:每段内存只能由一个指针负责释放,并且在释放后立即置空。此规则在手动内存管理中容易被忘记。

2. RAII 原则

RAII(Resource Acquisition Is Initialization)是 C++ 的核心资源管理模式。其基本思路是:资源(如内存、文件句柄、网络连接)在对象构造时获得,在对象析构时释放。这样即使出现异常也能保证资源被正确回收。

{
    std::unique_ptr<int[]> arr(new int[10]); // RAII
    // 使用 arr
} // arr 析构时自动 delete[]

RAII 的优势在于:

  • 异常安全:异常抛出时,堆栈会自动销毁局部对象,从而释放资源。
  • 易于维护:资源释放位置集中,减少泄漏风险。

3. 智能指针概览

类型 适用场景 关键特性
`std::unique_ptr
` 需要唯一所有权 不能拷贝,只能移动
`std::shared_ptr
` 需要共享所有权 引用计数,最后一个销毁时释放
`std::weak_ptr
` 观察共享对象但不拥有 防止循环引用

3.1 std::unique_ptr

  • 所有权独占:每个对象只能有一个 unique_ptr。若需要在多个地方使用,只能转移所有权。
  • 性能优越:无引用计数开销。
  • 使用方式
std::unique_ptr <MyClass> p1(new MyClass);
auto p2 = std::move(p1); // p1 现在为空
  • 自定义删除器
auto deleter = [](MyClass* p){ std::cout << "Custom delete\n"; delete p; };
std::unique_ptr<MyClass, decltype(deleter)> p3(new MyClass, deleter);

3.2 std::shared_ptr

  • 引用计数:每个 shared_ptr 增加计数,计数为 0 时销毁。
  • 适用共享资源:如 GUI 共享模型、线程共享数据。
  • 注意循环引用
struct Node {
    std::shared_ptr <Node> next;
    std::shared_ptr <Node> prev;
};

auto a = std::make_shared <Node>();
auto b = std::make_shared <Node>();
a->next = b; b->prev = a; // 循环引用,内存永不释放

解决方案:使用 std::weak_ptr

struct Node {
    std::shared_ptr <Node> next;
    std::weak_ptr <Node> prev; // 弱引用
};

3.3 std::weak_ptr

  • 观察者:不拥有资源,只能访问。
  • 防止循环引用:与 shared_ptr 搭配使用。
  • 使用方式
std::shared_ptr <MyClass> sp = std::make_shared<MyClass>();
std::weak_ptr <MyClass> wp = sp;
if (auto locked = wp.lock()) { // 取得临时 shared_ptr
    locked->doSomething();
}

4. 实践建议

场景 推荐指针 说明
单例或全局对象 `static std::unique_ptr
` 防止多次释放
资源管理类 unique_ptr<T[]> 动态数组
线程安全共享 shared_ptr + mutex 计数线程安全
对象间相互引用 shared_ptr + weak_ptr 防止循环
需要非所有权访问 raw pointer 临时访问,不管理
大型对象持有 `std::shared_ptr
` 共享所有权

5. 常见陷阱与错误示例

  1. 忘记 std::move

    std::unique_ptr <Foo> p1 = std::make_unique<Foo>();
    std::unique_ptr <Foo> p2 = p1; // 编译错误

    必须使用 std::move 转移所有权。

  2. unique_ptr 传递给函数却不返回

    void foo(std::unique_ptr <int> ptr){ /* 使用 */ }
    std::unique_ptr <int> p = std::make_unique<int>(5);
    foo(p); // p 变为空

    如果想保留 p,需传递引用 `foo(std::unique_ptr

    & ptr)`。
  3. 不正确的自定义删除器

    std::unique_ptr <int> p(new int[10], delete[]); // 错误:delete 而非 delete[]

    必须使用匹配的删除器。

6. 小结

  • 安全第一:使用 RAII 与智能指针,尽量避免裸指针。
  • 适用场景:根据所有权需求选择 unique_ptrshared_ptrweak_ptr
  • 循环引用shared_ptrweak_ptr 组合使用,防止内存泄漏。
  • 自定义删除器:满足特殊资源释放需求。

通过遵循上述原则,C++ 开发者可以大幅减少指针相关的错误,提高程序的健壮性与可维护性。

发表评论