指针与动态内存:C++内存管理的核心
说实话,指针这个话题,是C++里最容易让人头疼的部分之一。我当年刚学的时候,也被它绕得晕头转向。但等你真正理解了,你会发现它其实没那么神秘——说白了,指针就是一个存放地址的变量。
今天我们就来聊聊指针与动态内存管理。我会把我在项目中踩过的坑、积累的经验都分享给你。
指针的声明与使用
指针的声明很简单:在类型后面加个星号。比如:
int* ptr; // 声明一个指向int的指针
double* dp; // 声明一个指向double的指针
char* cp; // 声明一个指向char的指针
嗯,这里要注意:声明指针时,它不会自动初始化。如果你直接用它,可能会指向一个随机的内存地址——这就是传说中的野指针。我刚开始写代码时就吃过这个亏。
正确的做法是:
int value = 42;
int* ptr = &value; // & 取地址运算符
cout << *ptr; // * 解引用运算符,输出 42
指针运算
指针不是普通的数字,它的加减运算和类型有关。举个例子:
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。程序跑了三天,内存占用从 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,对象被释放
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 就解决了。
知识体系总览
下面这张图总结了本章的核心内容,你可以把它当作一个快速参考:
避坑指南
最后,分享几个我亲身经历过的教训:
- 我曾经在析构函数里忘了 delete 成员指针,导致内存泄漏。后来我改用智能指针,再也没犯过这个错。
- 我曾经在函数返回局部变量的地址,结果调用方拿到的是野指针。记住:永远不要返回栈上变量的地址。
- 我曾经在 delete 之后继续使用指针,导致程序崩溃。解决办法:delete 后立即将指针置为 nullptr。
- 我曾经在 shared_ptr 的循环引用上卡了三天。最后用 weak_ptr 轻松解决——有时候,换个思路比硬扛更有效。
指针和动态内存管理是 C++ 的精华所在,也是最容易出问题的地方。我的建议是:能用智能指针就别用裸指针,除非你很清楚自己在做什么。记住这一点,能帮你省下大量调试时间。