1. C++指针基础与内存布局
指针是C++中最强大也最危险的工具之一。本质上,指针就是一个存储内存地址的变量。理解指针的关键在于明白它存储的不是数据本身,而是数据在内存中的位置。
1.1 指针的基本操作
声明指针时使用*符号,获取变量地址使用&符号:
cpp复制int num = 42;
int* ptr = # // ptr现在存储了num的内存地址
指针解引用使用*操作符访问指向的值:
cpp复制cout << *ptr; // 输出42
指针运算允许我们通过加减整数来移动指针位置,这在数组操作中特别有用:
cpp复制int arr[3] = {10, 20, 30};
int* arrPtr = arr;
cout << *(arrPtr + 1); // 输出20
1.2 内存四区模型
C++程序运行时内存分为四个主要区域:
- 代码区:存放函数体的二进制代码,由操作系统管理
- 全局区:存放全局变量、静态变量和常量
- 栈区:由编译器自动分配释放,存放函数参数值、局部变量等
- 堆区:由程序员分配释放,若不释放程序结束时由OS回收
cpp复制int globalVar; // 全局区
static int staticVar; // 全局区
void func() {
int localVar; // 栈区
int* heapPtr = new int; // 堆区分配
// 必须手动delete heapPtr
}
关键区别:栈空间有限(通常几MB)且生命周期随作用域结束,堆空间大得多但需要手动管理。
2. 动态内存管理基础
2.1 传统C风格内存管理
C语言提供了三个关键函数管理堆内存:
malloc:分配指定字节数的未初始化内存
cpp复制int* ptr = (int*)malloc(sizeof(int)*10);
calloc:分配并清零内存
cpp复制int* ptr = (int*)calloc(10, sizeof(int));
realloc:调整已分配内存块大小
cpp复制ptr = (int*)realloc(ptr, 20*sizeof(int));
必须配对使用free释放内存:
cpp复制free(ptr);
常见陷阱:
- 忘记检查分配是否成功(返回NULL)
- 使用未初始化的内存(malloc不初始化)
- 访问已释放的内存
- 内存泄漏(忘记free)
2.2 C++的new/delete运算符
C++引入了更类型安全的操作符:
cpp复制int* ptr = new int(42); // 单个对象
delete ptr;
int* arr = new int[10]; // 数组
delete[] arr;
相比malloc的优势:
- 自动计算类型大小
- 调用构造函数/析构函数
- 类型安全,不需要强制转换
注意:new在失败时会抛出bad_alloc异常而非返回NULL
3. 智能指针:现代C++的内存管理
3.1 unique_ptr:独占所有权指针
cpp复制#include <memory>
void func() {
std::unique_ptr<int> uptr(new int(10));
// 离开作用域自动释放
}
特性:
- 禁止拷贝构造和赋值(保证唯一所有权)
- 可以通过move转移所有权
- 支持自定义删除器
- C++14引入make_unique更安全:
cpp复制auto uptr = std::make_unique<int>(20);
3.2 shared_ptr:共享所有权指针
cpp复制void func() {
auto sptr1 = std::make_shared<int>(30);
{
auto sptr2 = sptr1; // 引用计数+1
} // sptr2析构,引用计数-1
} // sptr1析构,引用计数归零,释放内存
实现原理:
- 使用控制块存储引用计数
- 线程安全的引用计数增减
- 循环引用问题需要weak_ptr解决
3.3 weak_ptr:解决循环引用
cpp复制struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 使用weak_ptr避免循环引用
};
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2;
node2->prev = node1;
特性:
- 不增加引用计数
- 必须通过lock()转换为shared_ptr才能访问对象
cpp复制if(auto spt = weakPtr.lock()) {
// 安全使用spt
}
4. 内存管理实战技巧
4.1 RAII原则
资源获取即初始化(Resource Acquisition Is Initialization)是C++核心思想:
cpp复制class FileHandle {
FILE* file;
public:
explicit FileHandle(const char* name) : file(fopen(name, "r")) {}
~FileHandle() { if(file) fclose(file); }
// 禁用拷贝
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
};
void processFile() {
FileHandle f("data.txt"); // 文件自动关闭
}
4.2 自定义内存管理
对于性能关键场景,可以重载new/delete:
cpp复制class MemoryPool {
// 实现内存池...
public:
void* allocate(size_t size);
void deallocate(void* p);
};
void* operator new(size_t size, MemoryPool& pool) {
return pool.allocate(size);
}
void operator delete(void* p, MemoryPool& pool) {
pool.deallocate(p);
}
4.3 常见内存问题诊断
- 内存泄漏检测:
- Valgrind工具
- Windows CRT调试堆
- 重载new/delete记录分配
- 越界访问检测:
- AddressSanitizer
- 调试模式填充保护字节
- 使用智能指针的注意事项:
- 不要混合使用裸指针和智能指针
- 避免循环引用
- 注意多线程环境下的原子操作
5. 现代C++内存管理最佳实践
- 优先使用栈对象:自动管理生命周期
- 使用智能指针替代裸指针:
- 优先unique_ptr
- 需要共享时用shared_ptr
- 避免原始new/delete
- 使用容器替代手动数组:
cpp复制std::vector<int> vec(100); // 替代int* arr = new int[100]
- 遵循三/五法则:正确处理拷贝/移动语义
- 使用移动语义减少拷贝:
cpp复制std::vector<std::string> processStrings() {
std::vector<std::string> result;
// ...填充数据
return result; // 移动而非拷贝
}
实际项目经验表明,遵循这些原则可以将内存相关错误减少90%以上。智能指针虽然引入少量开销,但带来的安全性提升在大多数场景下都值得。
