指针与动态内存:C++内存管理的核心

说实话,指针这个话题,是C++里最容易让人头疼的部分之一。我当年刚学的时候,也被它绕得晕头转向。但等你真正理解了,你会发现它其实没那么神秘——说白了,指针就是一个存放地址的变量。

今天我们就来聊聊指针与动态内存管理。我会把我在项目中踩过的坑、积累的经验都分享给你。

指针的声明与使用

指针的声明很简单:在类型后面加个星号。比如:

int* ptr;   // 声明一个指向int的指针
double* dp; // 声明一个指向double的指针
char* cp;   // 声明一个指向char的指针

嗯,这里要注意:声明指针时,它不会自动初始化。如果你直接用它,可能会指向一个随机的内存地址——这就是传说中的野指针。我刚开始写代码时就吃过这个亏。

正确的做法是:

int value = 42;
int* ptr = &value;  // & 取地址运算符

cout << *ptr;      // * 解引用运算符,输出 42
警告:未初始化的指针是危险的。我曾经在项目中因为忘记初始化指针,导致程序随机崩溃,查了两天才找到原因。记住:声明指针时,要么立即初始化,要么赋值为 nullptr。

指针运算

指针不是普通的数字,它的加减运算和类型有关。举个例子:

int arr[] = {10, 20, 30, 40, 50};
int* p = arr;  // 指向数组第一个元素

cout << *p;        // 输出 10
p++;                // 移动一个 int 的大小(通常是4字节)
cout << *p;        // 输出 20
p += 2;             // 再移动两个 int
cout << *p;        // 输出 40

为什么会这样?因为指针的加减是以它指向的类型大小为单位的。int 占4字节,所以 p++ 实际上让地址增加了4,而不是1。

我个人习惯用指针遍历数组时,会特别注意边界条件。你想想看,如果指针越界了,程序不会报错,但数据已经被污染了——这种bug最难找。

操作 含义 示例
ptr++ 指向下一个元素 int* p = arr; p++ 指向 arr[1]
ptr-- 指向上一个元素 p-- 回到 arr[0]
ptr + n 向后移动n个元素 p + 3 指向 arr[3]
ptr1 - ptr2 两个指针之间的元素个数 &arr[4] - &arr[0] = 4

new 与 delete:手动管理内存

C++ 里,我们可以用 new 在堆上分配内存,用 delete 释放它。这给了我们极大的灵活性,但也带来了责任。

// 分配单个对象
int* p = new int(42);
delete p;  // 记得释放

// 分配数组
int* arr = new int[10];
delete[] arr;  // 数组要用 delete[]
核心原则:每个 new 必须对应一个 delete,每个 new[] 必须对应一个 delete[]。配错对会导致未定义行为。

我在项目中见过太多内存泄漏了。有一次,一个同事写了个循环,每次迭代都 new 一个对象,但忘了 delete。程序跑了三天,内存占用从 100MB 涨到了 8GB,服务器直接挂了。嗯,从那以后,我对内存管理就格外小心。

智能指针入门:让内存管理更安全

手动管理内存太容易出错了。C++11 引入了智能指针,它们会在适当的时候自动释放内存。说白了,就是让指针自己学会「用完就扔」。

unique_ptr:独占所有权

unique_ptr 表示独占所有权——同一时间只能有一个 unique_ptr 指向某个对象。它不能被拷贝,只能被移动。

#include <memory>

std::unique_ptr<int> uptr(new int(100));
// std::unique_ptr<int> uptr2 = uptr;  // 错误!不能拷贝
std::unique_ptr<int> uptr3 = std::move(uptr);  // 可以移动

// 使用 make_unique(C++14 推荐)
auto uptr4 = std::make_unique<int>(200);

我个人强烈推荐用 make_unique 而不是直接 new。为什么?因为如果构造函数抛出异常,make_unique 能保证不会内存泄漏。这是我在代码审查时经常强调的一点。

shared_ptr:共享所有权

shared_ptr 使用引用计数来管理共享对象。当最后一个 shared_ptr 被销毁时,对象才被释放。

std::shared_ptr<int> sptr1 = std::make_shared<int>(300);
{
    std::shared_ptr<int> sptr2 = sptr1;  // 引用计数变为2
    // 在这里 sptr1 和 sptr2 都指向同一个对象
}  // sptr2 销毁,引用计数变为1

// sptr1 销毁时,引用计数变为0,对象被释放
提示:使用 shared_ptr 时要注意循环引用问题。如果两个对象互相持有对方的 shared_ptr,它们的引用计数永远不会归零,导致内存泄漏。这时候就需要 weak_ptr 来打破循环。

weak_ptr:弱引用

weak_ptr 是一种不控制对象生命周期的智能指针。它必须从 shared_ptr 构造,并且不能直接解引用。要访问对象,需要先调用 lock() 方法获取一个 shared_ptr。

std::shared_ptr<int> sp = std::make_shared<int>(400);
std::weak_ptr<int> wp = sp;

// 使用前检查对象是否还存在
if (auto locked = wp.lock()) {
    std::cout << *locked << std::endl;
} else {
    std::cout << "对象已被释放" << std::endl;
}

weak_ptr 最常见的用途就是解决循环引用。我在一个图形引擎项目中就遇到过:两个对象互相引用,用 shared_ptr 导致内存泄漏,换成 weak_ptr 就解决了。

知识体系总览

下面这张图总结了本章的核心内容,你可以把它当作一个快速参考:

指针与动态内存管理 指针基础 • 声明:int* ptr; • 取地址:&value • 解引用:*ptr • 空指针:nullptr 指针运算 • ptr++ / ptr-- • ptr + n / ptr - n • 指针相减 • 单位:sizeof(类型) 动态内存 • new / delete • new[] / delete[] • 堆 vs 栈 • 内存泄漏风险 智能指针(C++11) unique_ptr 独占所有权 不可拷贝,可移动 shared_ptr 共享所有权 引用计数 weak_ptr 弱引用 打破循环引用

避坑指南

最后,分享几个我亲身经历过的教训:

  • 我曾经在析构函数里忘了 delete 成员指针,导致内存泄漏。后来我改用智能指针,再也没犯过这个错。
  • 我曾经在函数返回局部变量的地址,结果调用方拿到的是野指针。记住:永远不要返回栈上变量的地址。
  • 我曾经在 delete 之后继续使用指针,导致程序崩溃。解决办法:delete 后立即将指针置为 nullptr。
  • 我曾经在 shared_ptr 的循环引用上卡了三天。最后用 weak_ptr 轻松解决——有时候,换个思路比硬扛更有效。

指针和动态内存管理是 C++ 的精华所在,也是最容易出问题的地方。我的建议是:能用智能指针就别用裸指针,除非你很清楚自己在做什么。记住这一点,能帮你省下大量调试时间。

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