Modern C++ 智能指针浅谈

· · 科技·工程

-1. 前言

这篇文章本蒟蒻咕了好久,大多数时间都在查资料。

您可以将这篇文章看作 C-style 指针浅谈 的续集。

本文参考 cppreference,由 千问 AI 查找文章内容问题、找错别字与贡献结尾。

虽然有 AI 核查,但仍然可能有问题。若有,您可以私信我或在评论区指出。

1. 什么是智能指针?

在 C/C++ 中,动态内存通常通过 malloc/freenew/delete 手动管理。然而,这种模式极易引发内存泄漏、重复释放、野指针等问题。为解决这一痛点,C++ 引入了智能指针 —— 一种基于 RAII(Resource Acquisition Is Initialization,资源获取即初始化)原则的封装类,能够在对象生命周期结束时自动释放所管理的资源。

现代 C++(C++11 起)标准库提供了以下智能指针类型:

所有这些智能指针都定义在 <memory> 头文件中,使用时注意包含。

2. Modern C++ 指针与 C-Style 指针的区别是什么

1. 安全性

C-Style 指针类型转换灵活,但容易出现逻辑错误。

考虑下面一段代码:

#include <iostream>
int main() {
    int x = 10;
    void *ptr = &x;
    float *fptr = (float *)ptr; // 编译通过但逻辑错误
    std::cout << *fptr;
    return 0;
}

在我的电脑上运行结果为 1.4013 \text{E} -44,与定义的 10 完全没有关联。

而智能指针就不会这样。他直接禁止了你这样做。

#include <memory>
#include <iostream>
int main() {
    int x = 10;
    std::unique_ptr<int> ptr{&x};
    // std::unique_ptr<float> fptr{ptr};
    // 会直接编译错误:no instance of constructor
    // std::cout << *fptr;
    return 0;
}

2. 内存管理

C-Style 指针需要手动管理内存(malloc() / free()new / delete),容易出现内存泄漏、二次释放等问题。

虽然内存泄漏对 OI 的影响并不会很大,但在工程中就不一定了。

而智能指针则不会,他们在作用域后就会立即销毁。

3. std::auto_ptr

1. 基本用法

#include <memory>
int main() {
    std::auto_ptr<int> p(new int(42));
    // 作用域结束时自动调用 delete
}

2. 为何被废弃?

尽管初衷良好,auto_ptr 存在致命缺陷:

1. 复制即转移所有权

auto_ptr 执行复制(如赋值、函数传参)时,源指针会自动置空,这违背了“复制应保持原对象不变”的语义直觉。

  std::auto_ptr<int> p1(new int(114514));
  std::auto_ptr<int> p2 = p1;  // p1 变为 0!
  // *p1; // 未定义行为!

2. 无移动语义支持

C++11 引入了移动语义,而 auto_ptr 的设计早于该机制,无法很好的与现代 C++ 兼容。

std::auto_ptr 在 C++11 中被标记为废弃,并在 C++17 标准中被彻底移除。

不应在新代码中使用 auto_ptr

4. std::unique_ptr

1. 基本特性

2. 基本用法

#include <memory>
#include <iostream>
int main() {
    auto p1 = std::make_unique<int>(114514);
    // 使用 C++14 make_unique 创建一个 unique_ptr
    auto p2 = std::unique_ptr<int>(new int(1919810));
    // 直接使用 new 创建(不推荐)
    // std::unique_ptr 有意显式删除了
    // operator=(unique_ptr&) 与 unique_ptr(unique_ptr&),
    // 使其无法复制
    // 感谢 gtafics dalao 指出了问题所在
    auto p3 = std::move(p1);  // 显式转移所有权
    // p1 == nullptr, p2 拥有资源
    std::cout /*<< *p1 << " "*/ << *p2 << " " << *p3;
    // 若使用 p1 则会出现错误
    // Assertion 'get() != pointer()' failed.
}

除非明确需要共享,否则优先使用 unique_ptr

5. std::shared_ptr

1. 基本特性

2. 基本用法

#include <memory>
#include <iostream>
int main() {
    auto p1 = std::make_shared<int>(100);
    // 使用 C++11 make_shared 创建一个 shared_ptr
    auto p2 = std::shared_ptr<int>(new int(100));
    // 使用 new 直接创建(不推荐)
    {
        std::shared_ptr<int> p3 = p1;  // 引用计数变为 2
        std::cout << "use_count: " << p1.use_count() << "\n";
    }
    std::cout << "use_count: " << p1.use_count() << "\n";
    // p2 超出作用域析构,计数减为 1
}

引用计数可通过 p.use_count() 查询。

6. std::weak_ptr

1. 为什么需要它?

当两个 shared_ptr 互相持有对方(例如双向链表节点),引用计数永不归零,导致内存泄漏。

weak_ptr 不增加引用计数,仅“观察”对象是否存在。

2. 基本用法

#include <memory>
#include <iostream>
struct node {
    int val;
    std::shared_ptr<node> next;
    std::weak_ptr<node> prev;  // 避免循环引用
};
int main() {
    auto n1 = std::make_shared<node>(node{1});
    auto n2 = std::make_shared<node>(node{2});
    n1->next = n2;
    n2->prev = n1;
    if (auto p = n2->prev.lock()) { // .lock() 转为 `shared_ptr` 
        std::cout << "n2->prev = " << p->value << "\n";
    }
}

weak_ptr 不能直接解引用,必须通过 .lock() 转为 shared_ptr(若对象仍存在)。

7. 创建智能指针的最佳方法

C++14 引入 std::make_unique,C++11 已有 std::make_shared。推荐使用它们而非直接 new

// 推荐
auto p1 = std::make_unique<std::vector<int>>(1919, 810);
auto p2 = std::make_shared<std::string>("Hello shared_ptr!");

// 不推荐(除非需要自定义删除器)
std::unique_ptr<int> p3(new int(114));
std::unique_ptr<int> p3(new int(514));

8. 自定义删除器(Deleter)

std::unique_ptr 支持自定义资源释放逻辑,适用于管理非 new 分配的资源,例如文件句柄 FILE 等。

默认 Deleter 为 std::default_delete,内部实现就是调用关键字 delete

#include <memory>
#include <iostream>
#include <cstdio>
int main() {
    auto file_deleter = [](FILE* f) {
    // 这个示例使用了 lambda 来定义 Deleter
    // 也可以使用标准库的做法
    // 写一个struct/class,在其中重载 operator()
        if (f) {
            std::fclose(f);
        }
    };
    std::unique_ptr<FILE, decltype(file_deleter)> fp(
        std::fopen("test.txt", "w"), file_deleter
    );
    fputs("Hello unique_ptr!", fp.get());
    return 0;
}

9. 常见陷阱与调试建议

1. 循环引用

```cpp
#include <memory>
#include <iostream>
struct node {
    int val;
    std::shared_ptr<node> next;
    std::shared_ptr<node> prev;  // 循环引用
};
int main() {
    auto n1 = std::make_shared<node>(node{1});
    auto n2 = std::make_shared<node>(node{2});
    n1->next = n2;
    n2->prev = n1;
    return 0;
}

2. 混用裸指针初始化多个智能指针

int* raw = new int(10);
std::shared_ptr<int> p1(raw);
std::shared_ptr<int> p2(raw); // 双重 delete!
// return value 3221226356

3. 调试技巧

10. 总结

智能指针 内存大小(典型) 引用计数 控制块 移动/复制语义 启用/弃用时间
auto_ptr 8 字节 复制即转移(非标准移动) Since C++98, Deprecated in C++11, Removed in C++17
unique_ptr 8 字节 可移动,不可复制 Since C++11
shared_ptr 16 字节 可复制,可移动 Since C++11
weak_ptr 16 字节 共享 共享 可复制,可移动 Since C++11

11. The End

By Qwen AI.

auto_ptr 的失败教训,到 unique_ptr/shared_ptr/weak_ptr 的成熟体系,C++ 智能指针的演进体现了语言对安全性与性能的双重追求。

如今,Modern C++ 已为我们提供了强大而安全的工具。善用它们,让内存管理不再是“玄学”,而是可预测、可验证的工程实践。

总结

  • auto_ptr 是历史产物,不应使用;
  • unique_ptr 是默认选择;
  • shared_ptr 按需使用;
  • weak_ptr 专治循环引用。

—— 愿你在 Modern C++ 的世界里,写出既高效又安全的代码。

:::info[AI 使用说明]

本文由 千问 AI 查找文章内容问题、找错别字与贡献结尾。

:::