06-内存管理
内存模型
内存管理是C++编程中的核心概念,理解内存模型对于编写高效、安全的程序至关重要。C++提供了直接的内存控制能力,这既是其优势也是其挑战。掌握内存管理技术可以避免内存泄漏、提高程序性能,并编写出更加健壮的代码。
内存布局
程序运行时,内存被划分为不同的区域,每个区域有不同的用途和特性。理解这些区域的特点有助于更好地管理内存。
代码段(Text Segment):
存储程序的机器码指令
只读区域,程序运行时不能修改
通常位于内存的低地址区域
包含程序的执行逻辑
数据段(Data Segment):
存储已初始化的全局变量和静态变量
程序启动时分配,程序结束时释放
分为只读数据段和可读写数据段
包含程序的全局状态信息
BSS段(Block Started by Symbol):
存储未初始化的全局变量和静态变量
程序启动时自动初始化为0
不占用可执行文件的空间
提高程序加载效率
堆(Heap):
动态分配的内存区域
通过new/delete或malloc/free管理
内存分配和释放的时间不确定
需要程序员手动管理
栈(Stack):
存储局部变量和函数调用信息
自动管理,后进先出(LIFO)
内存分配和释放速度快
大小有限,通常几MB
栈内存
栈内存是自动管理的内存区域,具有高效、安全的特点。它是函数调用和局部变量存储的主要场所。
栈内存的特点:
自动分配和释放,无需程序员干预
分配速度快,只需要移动栈指针
内存连续,缓存友好
大小有限,通常几MB
后进先出,符合函数调用模式
栈内存的使用:
void function() {
int localVar = 10; // 栈上分配
int array[100]; // 栈上数组
// 函数结束时自动释放
}
栈内存的优势:
无需手动管理,避免内存泄漏
分配速度快,性能优秀
自动清理,异常安全
内存局部性好,缓存命中率高
栈内存的限制:
大小有限,不适合大对象
生命周期固定,灵活性差
递归深度受限
不能动态调整大小
堆内存
堆内存是动态分配的内存区域,提供了更大的灵活性,但也带来了更多的管理复杂性。
堆内存的特点:
手动管理,程序员控制分配和释放
大小灵活,可以分配任意大小的内存
生命周期灵活,可以跨函数使用
分配速度相对较慢
可能产生内存碎片
堆内存的使用:
void function() {
int* ptr = new int(10); // 堆上分配
int* array = new int[100]; // 堆上数组
delete ptr; // 释放单个对象
delete[] array; // 释放数组
}
堆内存的优势:
大小灵活,适合大对象
生命周期灵活,可以跨函数使用
可以动态调整大小
适合不确定大小的数据结构
堆内存的挑战:
需要手动管理,容易出错
可能产生内存泄漏
分配速度较慢
可能产生内存碎片
动态内存分配
动态内存分配是C++中重要的内存管理技术,正确使用可以大大提高程序的灵活性。通过动态分配,可以在运行时决定需要多少内存,这对于处理不确定大小的数据特别有用。
new和delete
new和delete是C++的动态内存分配操作符,提供了类型安全的内存管理。它们比C语言的malloc和free更加安全,因为它们会自动调用构造函数和析构函数。
new和delete的优势:
类型安全,编译器检查类型匹配
自动调用构造函数和析构函数
支持异常处理
与C++对象模型集成良好
单个对象分配:
int* ptr = new int(42); // 分配并初始化
delete ptr; // 释放内存
数组分配:
int* arr = new int[10]; // 分配数组
delete[] arr; // 释放数组
对象分配:
MyClass* obj = new MyClass(); // 调用构造函数
delete obj; // 调用析构函数
对象数组分配:
MyClass* objs = new MyClass[5]; // 调用构造函数
delete[] objs; // 调用析构函数
注意事项:
new和delete必须配对使用
new[]和delete[]必须配对使用
不要混用new/delete和malloc/free
释放后要将指针设为nullptr
内存泄漏
内存泄漏是动态内存管理中的常见问题,需要特别注意。内存泄漏会导致程序占用越来越多的内存,最终可能导致系统崩溃。
内存泄漏的原因:
忘记调用delete释放内存
异常导致delete未执行
指针丢失,无法释放内存
循环引用导致无法释放
错误示例:
void badFunction() {
int* ptr = new int(42);
// 忘记delete ptr;
// 内存泄漏!
}
防止内存泄漏的方法:
使用RAII(资源获取即初始化)
使用智能指针
异常安全编程
代码审查和测试
正确示例:
class Resource {
private:
int* data;
public:
Resource() : data(new int(42)) {}
~Resource() { delete data; }
};
异常安全
异常安全是动态内存管理中的重要概念,确保程序在异常情况下也能正确管理内存。
异常安全问题:
void riskyFunction() {
int* ptr1 = new int(42);
int* ptr2 = new int(84); // 如果这里抛出异常,ptr1泄漏
delete ptr1;
delete ptr2;
}
异常安全解决方案:
void safeFunction() {
unique_ptr<int> ptr1(new int(42));
unique_ptr<int> ptr2(new int(84));
// 即使抛出异常,智能指针也会自动释放
}
异常安全保证级别:
基本保证:不泄漏资源,对象处于有效状态
强保证:操作要么成功,要么保持原状态
无异常保证:操作不会抛出异常
智能指针
智能指针自动管理内存,避免手动 new/delete 的风险。C++11 提供三种智能指针应对不同场景。
unique_ptr详解
unique_ptr 独占所有权,不可拷贝只能移动。适用于明确单一所有者的场景,零开销。
#include <memory>
// 创建
unique_ptr<int> ptr1(new int(42));
auto ptr2 = make_unique<int>(42);
// 移动语义
unique_ptr<int> ptr3 = move(ptr1); // ptr1变为nullptr
// 自定义删除器
auto deleter = [](int* p) {
cout << "Deleting: " << *p << endl;
delete p;
};
unique_ptr<int, decltype(deleter)> ptr4(new int(42), deleter);
// 数组版本
unique_ptr<int[]> arr = make_unique<int[]>(10);
weak_ptr详解
shared_ptr<int> shared = make_shared<int>(42);
weak_ptr<int> weak = shared;
// 检查是否有效
if (!weak.expired()) {
auto locked = weak.lock();
cout << "Value: " << *locked << endl;
}
// 获取引用计数
cout << "Use count: " << weak.use_count() << endl;
RAII(资源获取即初始化)
RAII 是 C++ 资源管理的核心模式:构造函数获取资源,析构函数释放资源。利用栈对象自动析构特性。
基本概念
RAII 将资源生命周期绑定到对象生命周期,自动管理文件、锁、内存等资源。
class FileHandle {
private:
FILE* file;
public:
FileHandle(const char* filename) : file(fopen(filename, "r")) {
if (!file) {
throw runtime_error("Cannot open file");
}
}
~FileHandle() {
if (file) {
fclose(file);
}
}
// 禁用拷贝
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// 允许移动
FileHandle(FileHandle&& other) noexcept : file(other.file) {
other.file = nullptr;
}
FileHandle& operator=(FileHandle&& other) noexcept {
if (this != &other) {
if (file) fclose(file);
file = other.file;
other.file = nullptr;
}
return *this;
}
};
锁管理
#include <mutex>
class LockGuard {
private:
mutex& mtx;
public:
LockGuard(mutex& m) : mtx(m) {
mtx.lock();
}
~LockGuard() {
mtx.unlock();
}
// 禁用拷贝和移动
LockGuard(const LockGuard&) = delete;
LockGuard& operator=(const LockGuard&) = delete;
LockGuard(LockGuard&&) = delete;
LockGuard& operator=(LockGuard&&) = delete;
};
内存池
简单内存池实现
class SimpleMemoryPool {
private:
char* memory;
size_t size;
size_t used;
public:
SimpleMemoryPool(size_t poolSize) : size(poolSize), used(0) {
memory = new char[size];
}
~SimpleMemoryPool() {
delete[] memory;
}
void* allocate(size_t bytes) {
if (used + bytes > size) {
return nullptr; // 内存不足
}
void* ptr = memory + used;
used += bytes;
return ptr;
}
void deallocate(void* ptr) {
// 简单实现:不实际释放,只是重置
used = 0;
}
};
内存对齐
对齐规则
struct AlignedStruct {
char c; // 1字节
int i; // 4字节,对齐到4字节边界
double d; // 8字节,对齐到8字节边界
};
// 总大小:24字节(1 + 3填充 + 4 + 8)
struct PackedStruct {
char c;
int i;
double d;
} __attribute__((packed)); // GCC属性,取消对齐
// 总大小:13字节
对齐控制
#include <cstddef>
// 查询对齐要求
cout << alignof(int) << endl; // 4
cout << alignof(double) << endl; // 8
// 对齐分配
void* ptr = aligned_alloc(16, 64); // 16字节对齐,64字节大小
free(ptr);
内存调试
Valgrind使用
# 内存泄漏检查
valgrind --leak-check=full ./program
# 内存错误检查
valgrind --tool=memcheck ./program
# 性能分析
valgrind --tool=callgrind ./program
AddressSanitizer
# 编译时启用
g++ -fsanitize=address -g -o program source.cpp
# 运行时检查内存错误
./program
自定义内存检查
class MemoryTracker {
private:
static size_t allocated;
static size_t deallocated;
public:
static void* allocate(size_t size) {
allocated += size;
cout << "Allocated: " << size << " bytes" << endl;
return malloc(size);
}
static void deallocate(void* ptr, size_t size) {
deallocated += size;
cout << "Deallocated: " << size << " bytes" << endl;
free(ptr);
}
static void report() {
cout << "Total allocated: " << allocated << " bytes" << endl;
cout << "Total deallocated: " << deallocated << " bytes" << endl;
cout << "Leaked: " << allocated - deallocated << " bytes" << endl;
}
};
常见内存错误与检测
内存错误类型
// 1. 内存泄漏 - 分配后忘记释放
void leak() {
int* p = new int(42);
return; // 忘记delete,内存泄漏!
}
// 2. 重复释放 - Double Free
void doubleFree() {
int* p = new int(42);
delete p;
delete p; // 未定义行为!
}
// 3. 悬空指针 - Use After Free
void useAfterFree() {
int* p = new int(42);
delete p;
*p = 100; // 未定义行为!
}
// 4. 数组越界
void bufferOverflow() {
int* arr = new int[10];
arr[10] = 42; // 越界!
delete[] arr;
}
// 5. 错误的delete方式
void wrongDelete() {
int* p = new int(42);
delete[] p; // 错误!应该用delete
int* arr = new int[10];
delete arr; // 错误!应该用delete[]
}
检测工具使用
Valgrind(Linux必备):
# 编译时加调试信息
g++ -g -o program source.cpp
# 内存泄漏检测
valgrind --leak-check=full --show-leak-kinds=all ./program
# 输出示例
# ==12345== LEAK SUMMARY:
# ==12345== definitely lost: 40 bytes in 1 blocks
# ==12345== indirectly lost: 0 bytes in 0 blocks
# 未初始化内存使用
valgrind --track-origins=yes ./program
AddressSanitizer(推荐,速度快):
# 编译时启用(GCC/Clang)
g++ -fsanitize=address -g -o program source.cpp
# 运行程序自动检测
./program
# 检测类型:
# - 堆缓冲区溢出
# - 栈缓冲区溢出
# - Use after free
# - Use after return
# - Double free
LeakSanitizer(内存泄漏专用):
# 编译时启用
g++ -fsanitize=leak -g -o program source.cpp
# 运行结束时报告泄漏
./program
静态分析(编译期检查):
# Clang静态分析器
clang++ --analyze source.cpp
# Cppcheck
cppcheck --enable=all source.cpp
# Clang-Tidy
clang-tidy source.cpp -- -std=c++17
预防最佳实践
// ✅ 1. 使用智能指针
std::unique_ptr<int> p(new int(42)); // 自动释放
// ✅ 2. RAII封装资源
class FileHandle {
FILE* fp;
public:
FileHandle(const char* name) : fp(fopen(name, "r")) {}
~FileHandle() { if (fp) fclose(fp); }
};
// ✅ 3. 使用容器代替数组
std::vector<int> vec(10); // 自动管理内存
// ✅ 4. 删除后置空指针
delete p;
p = nullptr; // 防止悬空指针
// ✅ 5. 禁用拷贝或正确实现深拷贝
class NoCopy {
public:
NoCopy(const NoCopy&) = delete;
NoCopy& operator=(const NoCopy&) = delete;
};
核心思想:让编译器和库管理内存,而不是手动管理。