目录
第九章 动态内存管理
C++提供了强大的动态内存管理能力,允许程序在运行时分配和释放内存。这种灵活性对于处理大小可变的数据结构、实现复杂算法至关重要。本章将全面介绍C++动态内存管理的机制、最佳实践以及常见问题。
9.1 为什么需要动态内存
9.1.1 静态内存的局限
在程序编译时确定的内存分配称为静态内存分配,它有以下限制:
- 大小固定:数组大小必须在编译时确定 - 生命周期:变量的生命周期由作用域决定 - 灵活性差:无法根据运行时需求调整内存大小
// 静态内存分配的问题 void problemExample() { int arr[1000]; // 如果只需要10个元素,浪费内存 // 如果需要10000个元素,会溢出 // 无法根据用户输入决定数组大小 int n; cin >> n; // int arr2[n]; // 在标准C++中,这是不允许的(VLA是GCC扩展) }
9.1.2 动态内存的优势
- 内存大小在运行时确定 - 内存的生命周期由程序员控制 - 可以实现复杂的数据结构(链表、树、图等) - 提高内存使用效率
9.2 new 和 delete 运算符
9.2.1 基本用法
分配单个对象
#include <iostream> using namespace std; int main() { // 动态分配一个int int *p = new int; // 分配内存,未初始化 int *p2 = new int(10); // 分配并初始化为10 cout << "*p2 = " << *p2 << endl; // 10 // 使用内存 *p = 100; cout << "*p = " << *p << endl; // 释放内存 delete p; delete p2; // 释放后置空指针(好习惯) p = nullptr; p2 = nullptr; return 0; }
分配数组
#include <iostream> using namespace std; int main() { int n; cout << "输入数组大小: "; cin >> n; // 动态分配数组 int *arr = new int[n]; // 初始化数组 for (int i = 0; i < n; i++) { arr[i] = i + 1; } // 使用数组 cout << "数组元素: "; for (int i = 0; i < n; i++) { cout << arr[i] << " "; } cout << endl; // 释放数组(注意使用delete[]) delete[] arr; arr = nullptr; return 0; }
9.2.2 new/delete 的注意事项
#include <iostream> using namespace std; void newDeleteTips() { // 1. new和delete必须配对使用 int *p1 = new int(10); delete p1; // 正确 // delete p1; // 错误!重复释放 // 2. new[]和delete[]必须配对 int *arr = new int[10]; // delete arr; // 错误!应该用delete[] delete[] arr; // 正确 // 3. 不能释放非动态分配的内存 int x = 10; int *p2 = &x; // delete p2; // 运行时错误! // 4. 内存分配可能失败 try { // 在C++11之前,new失败会抛出bad_alloc异常 int *big = new int[1000000000]; delete[] big; } catch (bad_alloc &e) { cout << "内存分配失败: " << e.what() << endl; } // C++11之后可以使用nothrow版本 int *p3 = new (nothrow) int[1000000000]; if (p3 == nullptr) { cout << "内存分配失败" << endl; } } int main() { newDeleteTips(); return 0; }
9.3 动态内存与类
9.3.1 类的动态成员
当类包含动态分配的内存时,需要特别注意构造函数、析构函数和赋值运算符。
#include <iostream> #include <cstring> using namespace std; class String { private: char *data; int length; public: // 默认构造函数 String() { data = new char[1]; data[0] = '\0'; length = 0; cout << "默认构造函数" << endl; } // 带参数的构造函数 String(const char *str) { length = strlen(str); data = new char[length + 1]; strcpy(data, str); cout << "带参数构造函数: " << str << endl; } // 拷贝构造函数(深拷贝) String(const String &other) { length = other.length; data = new char[length + 1]; strcpy(data, other.data); cout << "拷贝构造函数" << endl; } // 赋值运算符(深拷贝) String &operator=(const String &other) { cout << "赋值运算符" << endl; if (this != &other) { // 检查自赋值 // 先释放原有内存 delete[] data; // 分配新内存并复制 length = other.length; data = new char[length + 1]; strcpy(data, other.data); } return *this; } // 析构函数 ~String() { cout << "析构函数: " << (data ? data : "null") << endl; delete[] data; } // 显示字符串 void display() const { cout << data << endl; } int getLength() const { return length; } }; int main() { String s1("Hello"); String s2 = s1; // 调用拷贝构造函数 String s3; s3 = s1; // 调用赋值运算符 s1.display(); s2.display(); s3.display(); return 0; }
9.3.2 三/五法则
如果一个类需要自定义析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个,通常三者都需要自定义。C++11之后,移动构造函数和移动赋值运算符也需要考虑。
#include <iostream> using namespace std; class Resource { private: int *data; int size; public: // 构造函数 Resource(int s = 10) : size(s) { data = new int[size]; for (int i = 0; i < size; i++) { data[i] = i; } cout << "构造函数" << endl; } // 析构函数 ~Resource() { delete[] data; cout << "析构函数" << endl; } // 拷贝构造函数(Rule of Three/Five) Resource(const Resource &other) : size(other.size) { data = new int[size]; for (int i = 0; i < size; i++) { data[i] = other.data[i]; } cout << "拷贝构造函数" << endl; } // 拷贝赋值运算符 Resource &operator=(const Resource &other) { cout << "拷贝赋值运算符" << endl; if (this != &other) { // 复制并交换惯用法 int *newData = new int[other.size]; for (int i = 0; i < other.size; i++) { newData[i] = other.data[i]; } delete[] data; data = newData; size = other.size; } return *this; } // C++11: 移动构造函数 Resource(Resource &&other) noexcept : data(other.data), size(other.size) { other.data = nullptr; other.size = 0; cout << "移动构造函数" << endl; } // C++11: 移动赋值运算符 Resource &operator=(Resource &&other) noexcept { cout << "移动赋值运算符" << endl; if (this != &other) { delete[] data; data = other.data; size = other.size; other.data = nullptr; other.size = 0; } return *this; } void print() const { for (int i = 0; i < size; i++) { cout << data[i] << " "; } cout << endl; } }; int main() { Resource r1(5); r1.print(); Resource r2 = r1; // 拷贝构造 Resource r3; r3 = r1; // 拷贝赋值 Resource r4 = std::move(r1); // 移动构造 return 0; }
9.4 常见的动态内存问题
9.4.1 内存泄漏
内存泄漏是指程序动态分配的内存没有被释放,导致可用内存逐渐减少。
#include <iostream> using namespace std; // 内存泄漏示例 void memoryLeak() { int *p = new int[1000]; // 使用p... // 忘记delete[] p; } // 更隐蔽的内存泄漏 void hiddenLeak() { int *p = new int[100]; if (someCondition()) { return; // 提前返回,导致内存泄漏 } delete[] p; } // 循环中的内存泄漏 void loopLeak() { for (int i = 0; i < 1000000; i++) { int *p = new int; // 使用p... // 忘记delete p; } } // 正确的做法 void noLeak() { int *p = new int[1000]; try { // 使用p... } catch (...) { delete[] p; // 异常时也要释放 throw; } delete[] p; } // 使用RAII避免内存泄漏(推荐) void raiiApproach() { // 使用智能指针,自动管理内存 // unique_ptr<int[]> p(new int[1000]); // 或使用vector // vector<int> vec(1000); }
9.4.2 重复释放
void doubleFree() { int *p = new int(10); delete p; // delete p; // 错误!重复释放,未定义行为 } void anotherDoubleFree() { int *p1 = new int(10); int *p2 = p1; delete p1; // delete p2; // 错误!p2和p1指向同一块内存 }
9.4.3 使用已释放的内存(悬挂指针)
void danglingPointer() { int *p = new int(10); delete p; // p现在是悬挂指针 // *p = 20; // 错误!使用已释放的内存 p = nullptr; // 释放后置空是好习惯 } int *returnDangling() { int *p = new int(10); delete p; return p; // 返回悬挂指针! }
9.5 动态内存的最佳实践
9.5.1 RAII原则
RAII(Resource Acquisition Is Initialization)是C++中管理资源的核心原则。
#include <iostream> using namespace std; // RAII类示例 class FileHandler { private: FILE *file; public: FileHandler(const char *filename, const char *mode) { file = fopen(filename, mode); if (!file) { throw runtime_error("Failed to open file"); } } ~FileHandler() { if (file) { fclose(file); } } // 禁止拷贝 FileHandler(const FileHandler &) = delete; FileHandler &operator=(const FileHandler &) = delete; // 允许移动 FileHandler(FileHandler &&other) noexcept : file(other.file) { other.file = nullptr; } void write(const char *data) { if (file) { fprintf(file, "%s", data); } } }; void useFile() { FileHandler file("test.txt", "w"); file.write("Hello, RAII!"); // 文件会自动关闭,即使发生异常 }
9.5.2 使用智能指针
现代C++推荐使用智能指针管理动态内存。
#include <iostream> #include <memory> using namespace std; void smartPointerDemo() { // unique_ptr: 独占所有权 unique_ptr<int> up1(new int(10)); cout << *up1 << endl; // 更好的创建方式(C++14) auto up2 = make_unique<int>(20); // 转移所有权 unique_ptr<int> up3 = move(up1); // up1现在为nullptr // 数组版本 auto arr = make_unique<int[]>(10); for (int i = 0; i < 10; i++) { arr[i] = i; } } void sharedPointerDemo() { // shared_ptr: 共享所有权,引用计数 shared_ptr<int> sp1 = make_shared<int>(100); cout << "引用计数: " << sp1.use_count() << endl; // 1 { shared_ptr<int> sp2 = sp1; cout << "引用计数: " << sp1.use_count() << endl; // 2 } cout << "引用计数: " << sp1.use_count() << endl; // 1 // 当引用计数为0时,内存自动释放 } class Node; using NodePtr = shared_ptr<Node>; class Node { public: int data; // weak_ptr用于打破循环引用 weak_ptr<Node> parent; NodePtr child; Node(int d) : data(d) { cout << "Node " << d << " created" << endl; } ~Node() { cout << "Node " << data << " destroyed" << endl; } }; void weakPointerDemo() { NodePtr parent = make_shared<Node>(1); NodePtr child = make_shared<Node>(2); parent->child = child; child->parent = parent; // weak_ptr不会增加引用计数 }
9.6 内存池与自定义分配器
9.6.1 内存池的概念
内存池预先分配一大块内存,然后从中分配小块内存,减少频繁的系统调用。
#include <iostream> #include <vector> using namespace std; class MemoryPool { private: struct Block { char data[64]; // 固定大小的块 }; vector<Block *> pool; vector<Block *> available; public: MemoryPool(size_t size = 100) { for (size_t i = 0; i < size; i++) { Block *block = new Block(); pool.push_back(block); available.push_back(block); } } ~MemoryPool() { for (auto block : pool) { delete block; } } void *allocate() { if (available.empty()) { throw runtime_error("Pool exhausted"); } Block *block = available.back(); available.pop_back(); return block; } void deallocate(void *ptr) { available.push_back(static_cast<Block *>(ptr)); } bool isFull() const { return available.empty(); } }; int main() { MemoryPool pool(10); vector<void *> allocated; for (int i = 0; i < 5; i++) { allocated.push_back(pool.allocate()); } for (auto ptr : allocated) { pool.deallocate(ptr); } return 0; }
9.7 综合示例
实现一个动态数组类(完整版)
#include <iostream> #include <stdexcept> using namespace std; template <typename T> class DynamicArray { private: T *data; int size; int capacity; void resize(int newCapacity) { T *newData = new T[newCapacity]; for (int i = 0; i < size; i++) { newData[i] = data[i]; } delete[] data; data = newData; capacity = newCapacity; } public: DynamicArray(int initCapacity = 4) : size(0), capacity(initCapacity) { data = new T[capacity]; } // 拷贝构造函数 DynamicArray(const DynamicArray &other) : size(other.size), capacity(other.capacity) { data = new T[capacity]; for (int i = 0; i < size; i++) { data[i] = other.data[i]; } } // 赋值运算符 DynamicArray &operator=(const DynamicArray &other) { if (this != &other) { T *newData = new T[other.capacity]; for (int i = 0; i < other.size; i++) { newData[i] = other.data[i]; } delete[] data; data = newData; size = other.size; capacity = other.capacity; } return *this; } // 移动构造函数 DynamicArray(DynamicArray &&other) noexcept : data(other.data), size(other.size), capacity(other.capacity) { other.data = nullptr; other.size = 0; other.capacity = 0; } // 移动赋值运算符 DynamicArray &operator=(DynamicArray &&other) noexcept { if (this != &other) { delete[] data; data = other.data; size = other.size; capacity = other.capacity; other.data = nullptr; other.size = 0; other.capacity = 0; } return *this; } ~DynamicArray() { delete[] data; } void push_back(const T &value) { if (size >= capacity) { resize(capacity * 2); } data[size++] = value; } void pop_back() { if (size > 0) { size--; } } T &operator[](int index) { if (index < 0 || index >= size) { throw out_of_range("Index out of range"); } return data[index]; } const T &operator[](int index) const { if (index < 0 || index >= size) { throw out_of_range("Index out of range"); } return data[index]; } int getSize() const { return size; } int getCapacity() const { return capacity; } bool empty() const { return size == 0; } void clear() { size = 0; } T *begin() { return data; } T *end() { return data + size; } void print() const { cout << "["; for (int i = 0; i < size; i++) { cout << data[i]; if (i < size - 1) cout << ", "; } cout << "]" << endl; } }; int main() { DynamicArray<int> arr; for (int i = 1; i <= 10; i++) { arr.push_back(i); } cout << "数组内容: "; arr.print(); cout << "大小: " << arr.getSize() << endl; cout << "容量: " << arr.getCapacity() << endl; arr[5] = 100; cout << "修改后: "; arr.print(); DynamicArray<int> arr2 = arr; cout << "拷贝后: "; arr2.print(); return 0; }
9.8 习题
基础题
1. new/delete基础
编写程序,使用new动态分配一个int、一个double和一个包含10个元素的int数组,然后使用delete/delete[]释放它们。
2. 动态字符串
实现一个函数,接收一个C风格字符串,返回其副本(使用动态内存分配)。调用者负责释放返回的内存。
char *strCopy(const char *source);
3. 二维动态数组
编写程序,动态分配一个m×n的二维数组,初始化并打印,然后正确释放内存。
进阶题
4. 简单矩阵类
实现一个Matrix类,支持: - 动态分配二维矩阵 - 矩阵加法 - 矩阵乘法 - 正确的内存管理(深拷贝)
5. 实现String类
完善本章中的String类,添加以下功能: - 字符串连接(+运算符) - 字符串比较 - 获取子串 - 查找字符/子串
6. 对象池
实现一个简单的对象池类,用于管理某一类型对象的分配和回收,避免频繁的new/delete操作。
综合题
7. 内存泄漏检测
编写一个简单的内存分配跟踪系统,能够: - 重载new/delete运算符 - 跟踪所有分配和释放操作 - 程序结束时报告未释放的内存
8. 自定义向量类
基于本章的DynamicArray模板类,实现一个完整的Vector类,功能类似于std::vector: - push_back/pop_back - insert/erase - reserve/capacity - begin/end迭代器 - emplace_back
9. 链表内存管理
实现一个双向链表类,使用自定义内存池管理节点内存,而不是直接使用new/delete。
思考题
10. 深入分析
- 解释new和malloc的区别
- 为什么delete[]需要知道数组大小?(提示:数组长度通常存储在内存中)
- 分析placement new的用途和使用场景
- 讨论在哪些情况下应该使用动态内存,哪些情况下应该使用栈内存
9.9 小结
本章深入学习了C++的动态内存管理:
- new/delete 是C++动态内存管理的基础运算符 - 数组分配 需要使用new[]和delete[]配对 - 类的内存管理 需要遵循三/五法则,正确实现构造函数、析构函数和赋值运算符 - 内存问题 包括内存泄漏、重复释放和悬挂指针,需要格外注意 - RAII原则 是管理资源的最佳实践 - 智能指针(unique_ptr、shared_ptr、weak_ptr)是现代C++管理动态内存的推荐方式
掌握动态内存管理是成为优秀C++程序员的关键,正确的内存管理习惯可以避免大量难以调试的错误。
