指针进阶:动态内存分配与智能指针

指针这东西,C++ 里绕不开。基础用法大家都会,但真正让指针变得危险又强大的,是动态内存分配。我见过太多项目崩溃在内存泄漏上,也见过老手在 shared_ptr 循环引用里翻车。今天咱们就把这块彻底讲透。

一、new 和 delete:手动管理内存

动态内存分配,说白了就是程序运行时跟操作系统要内存。C 里用 malloc/free,C++ 里用 new/delete。区别在哪?new 不光分配内存,还会调用构造函数。

int* p = new int(42);        // 分配一个 int,初始化为 42
delete p;                     // 释放内存

int* arr = new int[10];       // 分配数组
delete[] arr;                 // 注意是 delete[]

嗯,这里要注意:new 和 delete 必须配对。new[] 必须配 delete[]。混用会导致未定义行为——程序可能崩,也可能不崩,但数据已经坏了。

⚠️ 我曾经踩过的坑: 写一个网络库时,用 new 分配了缓冲区,却用 free 去释放。结果在 Windows 上跑得好好的,换到 Linux 上就随机崩溃。查了两天才发现是这个问题。记住:C++ 里永远不要混用 new/delete 和 malloc/free。

二、为什么需要智能指针?

手动管理内存,最怕两件事:忘记 delete 导致内存泄漏,或者多次 delete 导致 double free。你想想看,一个函数里如果有多个 return 分支,每个分支前都要记得 delete,多容易漏?

智能指针就是来解决这个问题的。它把内存管理的责任从程序员手里拿走了,交给 RAII(资源获取即初始化)机制。说白了,智能指针对象销毁时,自动释放它管理的资源。

三、unique_ptr:独占所有权

unique_ptr 是我个人用得最多的智能指针。它的语义很明确:一个资源只能被一个 unique_ptr 拥有。不能拷贝,只能移动。

#include <memory>

std::unique_ptr<int> p1(new int(10));
// std::unique_ptr<int> p2 = p1;  // 编译错误!不能拷贝
std::unique_ptr<int> p2 = std::move(p1);  // 可以移动

// 推荐用 make_unique(C++14 起)
auto p3 = std::make_unique<int>(20);

为什么推荐 make_unique?两个原因:一是代码更简洁,二是异常安全。如果 new 表达式和 unique_ptr 构造之间抛出异常,可能会泄漏内存。make_unique 一步到位,不会有这个问题。

💡 我的习惯: 在项目中,只要资源所有权是独占的,一律用 unique_ptr。比如工厂方法返回的对象、树结构中的子节点。它零开销,性能跟裸指针一样。

四、shared_ptr:共享所有权

有些场景下,多个对象需要共享同一个资源。比如多个视图共享同一个数据模型。这时候就需要 shared_ptr。

shared_ptr 内部维护一个引用计数。每次拷贝,计数加一;每次析构,计数减一。计数归零时,自动释放资源。

auto sp1 = std::make_shared<int>(100);
{
    auto sp2 = sp1;  // 引用计数变为 2
    // 使用 sp2
}  // sp2 析构,计数变为 1
// sp1 析构时,计数归零,释放内存

这里有个性能细节:shared_ptr 的引用计数是原子操作,线程安全。但原子操作有开销,比 unique_ptr 慢。所以能用 unique_ptr 就别用 shared_ptr。

五、weak_ptr:打破循环引用

shared_ptr 有个大坑:循环引用。两个对象互相持有对方的 shared_ptr,引用计数永远降不到零,内存泄漏。

struct Node {
    std::shared_ptr<Node> next;
    // 如果两个 Node 互相指向,就循环引用了
};

// 解决方案:用 weak_ptr
struct Node {
    std::weak_ptr<Node> next;  // 不增加引用计数
};

weak_ptr 是一种弱引用。它不增加引用计数,只是观察资源是否还存在。使用时需要调用 lock() 获取一个 shared_ptr,如果资源已被释放,lock() 返回空。

🔑 核心原则: 所有权归属用 unique_ptr,共享所有权用 shared_ptr,观察者或打破循环用 weak_ptr。这个原则我在项目中一直坚持,从来没出过内存问题。

六、指针与函数

指针作为函数参数,有两种常见用途:输出参数和避免拷贝。

// 输出参数(C 风格)
void getValue(int* out) {
    *out = 42;
}

// 现代 C++ 推荐用引用
void getValue(int& out) {
    out = 42;
}

// 指针作为函数参数,避免拷贝大对象
void process(const std::vector<int>* data) {
    // 处理 data
}

我个人建议:能用引用就别用指针。引用更安全,不可能为空。指针可以为空,调用方必须检查。

函数指针也是 C++ 的重要特性,虽然现代 C++ 更推荐用 std::function 和 lambda。

// 函数指针
int add(int a, int b) { return a + b; }
int (*funcPtr)(int, int) = add;
int result = funcPtr(3, 4);  // 7

// 现代 C++ 用 std::function
std::function<int(int, int)> func = add;
// 或者 lambda
auto func2 = [](int a, int b) { return a + b; };

七、知识体系总览

下面这张图把本章的核心知识点串起来了。你看一遍,心里就有谱了。

指针进阶知识体系 指针进阶 动态内存分配 智能指针 指针与函数 new / delete 配对使用 new[] / delete[] 必须匹配 RAII 资源管理原则 unique_ptr:独占所有权 shared_ptr:共享所有权 weak_ptr:打破循环引用 输出参数 vs 引用 函数指针 / std::function lambda 表达式 核心原则:优先智能指针,避免裸指针管理生命周期

八、避坑指南

最后,分享几个我实际项目中遇到的教训:

  • 不要返回裸指针指向内部数据。 调用方可能忘记 delete,或者指针悬空。返回 shared_ptr 或 unique_ptr 更安全。
  • shared_ptr 的循环引用。 我维护过一个缓存系统,对象互相引用,内存只增不减。用 weak_ptr 替换其中一个方向就解决了。
  • make_shared 和 shared_ptr 构造的区别。 make_shared 一次分配对象和控制块,性能更好。但如果你需要自定义删除器,只能用 shared_ptr 构造。
  • unique_ptr 可以转换为 shared_ptr。 这是合法的,因为独占所有权可以变成共享所有权。反过来不行。
💡 我的建议: 新项目直接禁用裸 new/delete。用 make_unique 和 make_shared 替代。代码审查时,看到裸 new 一律打回。这个规矩执行半年,团队的内存 bug 减少了 80%。

指针进阶这块,说白了就是学会用工具代替手工。智能指针就是 C++ 给你的好工具,别浪费了。


公众号:蓝海资料掘金营,微信deep3321