搜索

C++ 教程

C++基础

第1章 搭建开发环境与第一个程序 第2章 基本数据类型与变量 第3章 运算符与表达式 第4章 流程控制语句 第5章 数组与字符串 第6章 函数基础 第7章 指针基础 第8章 引用与const限定 第9章 动态内存管理 第10章 结构体与联合体 第11章 函数进阶

面向对象

第12章 面向对象编程入门 第13章 封装与访问控制 第14章 运算符重载 第15章 继承与派生 第16章 多态与虚函数

STL与进阶

第17章 模板编程基础 第18章 标准模板库(STL)容器 第19章 STL算法与迭代器 第20章 异常处理机制 第21章 文件输入输出 第22章 字符串类string 第23章 命名空间与作用域 第24章 类型转换操作符

现代C++

第25章 智能指针 第26章 移动语义与右值引用 第27章 Lambda表达式 第28章 多线程编程基础 第29章 模板元编程入门 第30章 设计模式基础 第31章 C++11/14新特性 第32章 C++17/20核心特性

实战进阶

第33章 性能优化技巧 第34章 调试与测试 第35章 项目结构与构建 第36章 网络编程入门 第37章 数据库连接操作 第38章 图形界面开发基础 第39章 综合项目实战 第40章 进阶学习路线

第9章 动态内存管理

第9章 动态内存管理

入门 👤 已掌握指针基础,想突破固定内存限制的开发者 🔧 处理未知大小的数据集合,构建灵活的数据结构
本章你将学到
  • new/delete操作符的正确用法
  • 动态数组的创建与释放
  • 内存泄漏的成因与排查思路
核心概念堆内存内存泄漏

你的程序在编译时,数组大小就必须确定,比如int arr[100]。但如果用户要输入1000个数字呢?或者数据量在运行时才能确定?这时,你需要向操作系统“借”一块内存,这就是动态内存管理。

堆内存:程序的自助仓库

想象一下,你家(栈内存)的储物间大小固定,放不下的大件物品,你可以去公共仓库(堆内存)租一个位置。程序运行时,new操作符就是租用仓库空间的申请单,delete则是归还钥匙。从技术上讲,堆内存是程序运行时可以动态分配和释放的大容量内存区域,由程序员手动管理。

9.1 new与delete:内存的借与还

我们先从一个最简单的场景开始:在堆上分配一个整数。

// 申请内存:在堆上分配一个int大小的空间,并将地址赋给p
int *p = new int;
*p = 42; // 向这个地址写入值
cout << *p << endl; // 输出: 42

// 归还内存:告诉系统这块内存我不再使用了
delete p;
// 注意:delete之后,p变成了“野指针”,好的习惯是立即置空
p = nullptr;

这个过程可以用一个简单的流程图表示:

声明指针 int *p
→
new申请内存
→
使用内存 *p = 42
→
delete释放内存
→
指针置空 p = nullptr

关键点:配对使用

每一个new都必须对应一个delete。我在早期项目中曾因为一个复杂的条件分支,导致某条路径上只new而没有delete,最终程序运行几小时后内存耗尽崩溃。这是一个非常典型的坑。

9.2 动态数组:突破固定大小的限制

这是动态内存最常用的场景。语法是在类型后面加上方括号[],并指定元素个数。

// 用户输入决定数组大小
int size;
cout << "请输入学生数量: ";
cin >> size;

// 在堆上分配一个大小为size的int数组
int *scores = new int[size];

// 像普通数组一样使用
for (int i = 0; i < size; ++i) {
    scores[i] = i * 10;
}

// 释放内存:注意这里使用 delete[]
delete[] scores;
scores = nullptr;

⚠️ 常见误区

  • 混淆delete形式:对数组使用delete而非delete[]是未定义行为,可能导致只释放了第一个元素的内存,造成内存泄漏或程序崩溃。
  • 忘记置空指针:delete后指针值不变,但指向的内存已无效。再次使用或二次delete会导致严重错误。立即置为nullptr是好习惯。
  • 申请后不检查:在内存紧张的系统(如嵌入式设备)上,new可能失败并抛出std::bad_alloc异常。实际开发中,对于关键的内存申请,需要考虑异常处理。

动态内存管理在程序中的典型层次如下:

应用程序
↓ (申请/释放)
new/delete操作符
↓ (管理)
堆内存池
↓ (底层调用)
操作系统内存管理

9.3 内存泄漏:被遗忘的仓库

如果你从仓库租了位置,却忘了归还,仓库的空间就会被一直占用,即使你再也用不到它。这就是内存泄漏。

内存泄漏:资源的永久丢失

程序通过new分配了内存,但在其生命周期结束前,没有通过delete释放,且没有任何指针指向这块内存,导致系统无法回收。技术上讲,这是已分配内存的引用丢失。

看一个典型的泄漏例子:

void leakyFunction() {
    int *ptr = new int[1000]; // 分配
    // ... 使用ptr ...
    // 函数结束,局部指针ptr被销毁,但new出来的1000个int内存无人记得,也无法释放!
}

内存泄漏的危害是累积的。一个泄漏1KB的函数如果被循环调用一万次,就会悄无声息地“吃掉”10MB内存。在长时间运行的服务端程序或资源受限的嵌入式系统(比如只有512KB RAM的STM32芯片)中,这将是致命的。

编者提示:内存泄漏的排查需要借助工具。在第34章《调试与测试》中,我们会详细介绍如何使用Valgrind、AddressSanitizer等工具来精准定位泄漏点。

9.4 实战:一个简单的动态数组封装

为了安全地使用动态数组,我们可以进行简单的封装。下面是一个IntVector类的雏形,它管理一个动态的整型数组。

class IntVector {
private:
    int *m_data;      // 指向堆数组的指针
    size_t m_size;    // 数组当前大小
public:
    // 构造函数:分配内存
    IntVector(size_t size) : m_size(size) {
        m_data = new int[size]; // 在堆上分配
        // 初始化内存为0(可选,但是个好习惯)
        for (size_t i = 0; i < m_size; ++i) {
            m_data[i] = 0;
        }
    }

    // 析构函数:释放内存!这是防止泄漏的关键
    ~IntVector() {
        delete[] m_data;
        // 这里不需要置nullptr,因为对象即将销毁
    }

    // 提供访问元素的方法
    int& at(size_t index) {
        // 实际开发中应添加边界检查
        return m_data[index];
    }

    size_t size() const { return m_size; }
};

// 使用示例
int main() {
    IntVector vec(10); // 栈上对象,内含堆内存
    vec.at(0) = 100;
    // main函数结束,vec的析构函数自动调用,释放m_data指向的堆内存。
    // 没有内存泄漏!
    return 0;
}

这个简单的类展示了RAII(资源获取即初始化)思想的雏形:在构造函数中获取资源(内存),在析构函数中释放。这样,只要对象本身生命周期管理正确,其内部的资源就不会泄漏。

小型工具脚本

  • 一次性处理数据
  • 内存泄漏影响不大
  • 可直接使用new/delete

长期运行服务

  • 7x24小时不间断
  • 任何泄漏都会被放大
  • 必须用RAII封装或智能指针

嵌入式设备

  • 内存极度有限(如256KB)
  • 不允许泄漏
  • 甚至要避免动态分配

图形/游戏应用

  • 频繁创建销毁对象
  • 性能要求高
  • 常用自定义内存池

9.5 基础内存操作与数据对比

理解不同内存区域的特性和操作方式,是掌握动态内存管理的基础。下面的表格总结了核心区别:

内存区域生命周期管理方式分配速度典型容量操作符/关键字
栈 (Stack)自动(函数作用域)编译器自动极快较小 (MB级,如8MB)变量声明 (int a;)
堆 (Heap)手动(直到delete)程序员手动较慢很大 (GB级)new / delete
全局/静态区程序运行期编译器/系统编译时确定取决于数据static, 全局变量

实际开发中,大部分团队会遵循一个原则:优先使用栈和局部变量,除非数据大小未知、生命周期需要延长或体积过大。栈内存的自动管理能极大减少错误。我在一个图像处理项目中,曾将一个大缓冲区(4MB)错误地放在栈上,导致程序一运行就栈溢出崩溃。正确的做法是用new在堆上分配。

动手试一试

  1. 编写一个程序,让用户输入一个整数N,动态创建一个大小为N的double类型数组,并让用户输入每个元素的值,最后计算并输出平均值。
  2. 故意制造一个内存泄漏:在一个函数中new一个数组,然后不delete就返回。运行程序多次,观察系统任务管理器(Windows)或top命令(Linux)中你的程序内存占用是否会持续增长。
  3. 改进上面的IntVector类,添加一个resize(size_t newSize)函数。其逻辑是:申请新大小的数组,将旧数据拷贝过去(如果新尺寸更大,多余部分初始化为0),然后释放旧数组,更新指针和大小。思考这个过程中可能出错的地方。

检验你的理解

  1. 下面代码有什么问题?如何修正?
    int *createArray(int n) {
        int *arr = new int[n];
        return arr;
    }
    int main() {
        int *myArr = createArray(100);
        // 使用myArr...
        return 0;
    }
  2. 执行delete p;之后,指针p本身的值会变成nullptr吗?此时立即再次执行delete p;会导致什么后果?
  3. 为什么说“动态数组int *p = new int[100];”比“静态数组int arr[100];”更灵活,但也更危险?

本章小结

  • new和delete是C++中手动管理堆内存的基础操作符,必须严格配对使用,对数组要使用delete[]。
  • 动态内存的核心价值在于其大小和生命周期在运行时确定,突破了栈内存的固定大小限制。
  • 内存泄漏是指已分配的堆内存由于失去所有指针引用而无法被释放,是动态内存管理中最常见的错误,在长期运行程序中危害极大。
  • 通过类的构造函数和析构函数封装资源管理(RAII思想),是避免内存泄漏的有效编程范式。
  • 理解栈(自动、快速、容量小)和堆(手动、较慢、容量大)的区别,是做出正确内存分配决策的前提。

意见反馈

微信二维码 微信扫码关注 抖音二维码 抖音扫码关注