目录

第九章 动态内存管理

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. 深入分析

  1. 解释new和malloc的区别
  2. 为什么delete[]需要知道数组大小?(提示:数组长度通常存储在内存中)
  3. 分析placement new的用途和使用场景
  4. 讨论在哪些情况下应该使用动态内存,哪些情况下应该使用栈内存

9.9 小结

本章深入学习了C++的动态内存管理:

- new/delete 是C++动态内存管理的基础运算符 - 数组分配 需要使用new[]和delete[]配对 - 类的内存管理 需要遵循三/五法则,正确实现构造函数、析构函数和赋值运算符 - 内存问题 包括内存泄漏、重复释放和悬挂指针,需要格外注意 - RAII原则 是管理资源的最佳实践 - 智能指针(unique_ptr、shared_ptr、weak_ptr)是现代C++管理动态内存的推荐方式

掌握动态内存管理是成为优秀C++程序员的关键,正确的内存管理习惯可以避免大量难以调试的错误。