C++ 内存管理实战:从 5 个经典笔试题看 new/delete 与野指针规避
📅 2026/7/13 14:33:48
👁️ 阅读次数
📝 编程学习
C++ 内存管理实战:从 5 个经典笔试题看 new/delete 与野指针规避
1. 动态内存分配基础陷阱
案例1:GetMemory函数的内存泄漏
void GetMemory(char *p) { p = (char*)malloc(100); // 问题点:修改的是局部指针副本 } void Test() { char *str = NULL; GetMemory(str); // str仍为NULL strcpy(str, "hello"); // 运行时错误 }修正方案:二级指针传递
void GetMemory(char **p) { *p = (char*)malloc(100); if (*p == NULL) { // 良好的健壮性检查 perror("malloc failed"); exit(EXIT_FAILURE); } } void Test() { char *str = NULL; GetMemory(&str); strcpy(str, "hello"); free(str); // 必须配套释放 }关键知识点对比表:
| 错误类型 | 问题表现 | 解决方案 | 内存示意图 |
|---|---|---|---|
| 值传递指针 | 无法修改外部指针 | 使用指针的指针 | [原始栈帧]->[修改副本] |
| 未检查分配 | 可能使用NULL指针 | 添加NULL检查 | malloc失败返回NULL |
| 忘记释放 | 内存泄漏 | RAII原则 | 已分配块未回收 |
2. 返回栈内存引发的野指针
案例2:局部数组返回
char *GetString() { char p[] = "hello world"; // 栈上数组 return p; // 返回局部变量地址 } void Test() { char *str = GetString(); // str成为野指针 printf(str); // 未定义行为 }修正方案1:静态存储期
char *GetString() { static char p[] = "hello world"; // 静态存储期 return p; // 生命周期持续 }修正方案2:动态分配
char *GetString() { char *p = (char*)malloc(20); if (p) strcpy(p, "hello world"); return p; // 调用者需负责释放 }内存生命周期对比:
- 栈内存:函数返回即失效
- 静态区:程序运行期间有效
- 堆内存:需手动管理生命周期
3. delete与delete[]的误用
案例3:混合使用导致的崩溃
class MyClass { public: MyClass() { cout << "ctor" << endl; } ~MyClass() { cout << "dtor" << endl; } }; void Test() { MyClass *p1 = new MyClass[5]; // 调用5次构造函数 MyClass *p2 = new MyClass; delete p1; // 错误!只调用1次析构 delete[] p2; // 错误!错误释放方式 }修正代码:
void Test() { MyClass *p1 = new MyClass[5]; // new[]分配 MyClass *p2 = new MyClass; // new单对象 delete[] p1; // 正确释放数组 delete p2; // 正确释放单对象 }底层机制解析:
- new[]会在分配的内存块头部存储数组大小(通常在前4字节)
- delete[]根据该信息调用对应次数的析构函数
- 基本类型无析构函数时,delete和delete[]效果相同但不应混用
4. 多态对象的删除问题
案例4:基类非虚析构
class Base { public: ~Base() { cout << "Base dtor" << endl; } // 非虚析构 }; class Derived : public Base { int *data; public: Derived() : data(new int[100]) {} ~Derived() { delete[] data; cout << "Derived dtor" << endl; } }; void Test() { Base *p = new Derived(); delete p; // 仅调用Base析构,内存泄漏! }修正方案:虚析构函数
class Base { public: virtual ~Base() { cout << "Base dtor" << endl; } // 虚析构 };对象销毁过程:
- 通过基类指针删除时,虚函数机制确保调用实际类型的析构函数
- 析构顺序:派生类->基类
- 若基类析构非虚,则只会调用基类析构,导致派生类资源泄漏
5. 野指针检测与防御编程
案例5:重复释放与悬挂指针
void Test() { int *p = new int(10); delete p; // 正确释放 *p = 20; // 使用已释放内存 delete p; // 重复释放 }防御性编程技巧:
- 置空策略:
delete p; p = nullptr; // 确保再次访问会立即崩溃- 智能指针方案:
#include <memory> void SafeTest() { std::unique_ptr<int> p(new int(10)); // 自动管理生命周期 // 无需手动delete }- 内存调试工具:
- Valgrind:检测内存错误
- AddressSanitizer:实时内存检查
野指针检测表:
| 检测方法 | 原理 | 优点 | 限制 |
|---|---|---|---|
| 置空法 | 释放后立即置NULL | 简单直接 | 多指针指向时无效 |
| 智能指针 | 引用计数/独占所有权 | 自动管理 | 学习曲线稍高 |
| 内存标记 | 分配时添加标记 | 可检测越界 | 性能开销 |
| 工具检测 | 运行时检查 | 全面检测 | 需要特定环境 |
6. 现代C++内存管理最佳实践
RAII原则应用:
class ResourceGuard { int *resource; public: explicit ResourceGuard(size_t size) : resource(new int[size]) {} ~ResourceGuard() { delete[] resource; } // 禁用拷贝(C++11风格) ResourceGuard(const ResourceGuard&) = delete; ResourceGuard& operator=(const ResourceGuard&) = delete; // 支持移动(C++11) ResourceGuard(ResourceGuard&& other) noexcept : resource(other.resource) { other.resource = nullptr; } }; void ModernTest() { ResourceGuard guard(100); // 资源获取即初始化 // 无需手动释放,异常安全 }智能指针对比:
| 类型 | 所有权 | 线程安全 | 适用场景 |
|---|---|---|---|
| unique_ptr | 独占 | 否 | 单一所有者资源 |
| shared_ptr | 共享 | 是(控制块) | 共享所有权 |
| weak_ptr | 观察 | 是 | 解决循环引用 |
性能优化技巧:
- 内存池技术:
class MemoryPool { struct Block { Block* next; }; Block* freeList; public: void* allocate(size_t size) { if (!freeList) { // 批量分配策略 } void* mem = freeList; freeList = freeList->next; return mem; } };- 对象池模式:
template<typename T> class ObjectPool { std::vector<std::unique_ptr<T>> pool; public: T* acquire() { if (pool.empty()) { return new T(); } auto obj = std::move(pool.back()); pool.pop_back(); return obj.release(); } };
编程学习
技术分享
实战经验