C++第五讲:内存管理

C++第五讲:内存管理

这一章是C/C++ 面试第一高频考点,也是理解后续 STL 容器、智能指针的基础。


一、C/C++ 程序内存分区(必考选择题)

1. 内存分区总览

程序运行时,内存会被划分为以下 5 个主要区域(从高地址到低地址):

区域 存储内容 特点
内核空间 操作系统内核代码和数据 用户代码不可访问
栈(Stack) 非静态局部变量、函数参数、返回值 向下增长,自动分配释放,速度快,空间有限
内存映射段 动态库、共享内存、文件映射 用于进程间通信和动态库加载
堆(Heap) 动态申请的内存(malloc/new) 向上增长,手动分配释放,速度慢,空间大
数据段(静态区) 全局变量、静态变量(static 修饰) 程序运行期间一直存在,程序结束后系统释放
代码段(常量区) 可执行代码、只读常量 只读,不可修改

2. 经典面试题解析

看下面这段代码,判断每个变量 / 值所在的内存区域:

cpp 复制代码
int globalVar = 1;          // 数据段(全局变量)
static int staticGlobalVar = 1; // 数据段(全局静态变量)
​
void Test() {
    static int staticVar = 1; // 数据段(局部静态变量)
    int localVar = 1;         // 栈(局部变量)
    int num1[10] = {1,2,3,4}; // 栈(局部数组)
    
    char char2[] = "abcd";    // char2在栈,"abcd"在代码段(常量)
    const char* pChar3 = "abcd"; // pChar3在栈,"abcd"在代码段
    int* ptr1 = (int*)malloc(4*sizeof(int)); // ptr1在栈,指向堆
    
    free(ptr1);
}

答案速记

  • 所有static修饰的变量 + 全局变量 → 数据段

  • 局部变量、数组、指针变量本身 →

  • malloc/new申请的空间 →

  • 字符串常量(如"abcd")、可执行代码 → 代码段

易错点

  • char char2[] = "abcd":数组char2在栈,会把代码段的"abcd"拷贝一份 到栈中,所以可以修改char2[0] = 'x'

  • const char* pChar3 = "abcd"pChar3指向代码段的常量,所以不能修改*pChar3(会崩溃)


二、C 语言动态内存管理

C 语言通过 4 个函数管理堆内存:malloccallocreallocfree,都在<stdlib.h>头文件中。

1. 四个核心函数详解

1.1 malloc:申请空间

cpp 复制代码
void* malloc(size_t size);
  • 作用:申请size 字节的堆空间

  • 特点:不初始化,空间内容是随机值

  • 返回值:成功返回空间首地址,失败返回NULL

  • 示例:

    cpp 复制代码
    int* p = (int*)malloc(4*sizeof(int)); // 申请4个int的空间
    if (p == NULL) { // 必须判空!
        perror("malloc fail");
        return;
    }

1.2 calloc:申请并初始化空间

cpp 复制代码
void* calloc(size_t num, size_t size);
  • 作用:申请num 个 size 字节 的空间,并将所有字节初始化为0

  • 示例:

    cpp 复制代码
    int* p = (int*)calloc(4, sizeof(int)); // 4个int,初始值都是0

1.3 realloc:调整已申请空间的大小

cpp 复制代码
void* realloc(void* ptr, size_t new_size);
  • 作用:将ptr指向的空间调整为new_size字节

  • 两种扩容方式:

    1. 原地扩容:原空间后面有足够空间,直接扩展,返回原地址

    2. 异地扩容:原空间后面空间不足,申请新空间→拷贝数据→释放原空间→返回新地址

  • 注意:不需要手动 free 原空间,realloc 成功会自动释放原空间

  • 示例:

    cpp 复制代码
    int* p = (int*)malloc(4*sizeof(int));
    int* new_p = (int*)realloc(p, 10*sizeof(int)); // 扩容到10个int
    if (new_p != NULL) {
        p = new_p; // 扩容成功,更新指针
    }

1.4 free:释放空间

cpp 复制代码
void free(void* ptr);
  • 作用:释放ptr指向的堆空间

  • 注意事项:

    1. 不能释放NULL指针(free (NULL) 什么也不做,合法)

    2. 不能重复释放同一块空间(会崩溃)

    3. 不能释放栈上的空间(只能释放堆空间)

    4. 释放后必须将指针置为NULL,避免野指针

2. 面试高频题:malloc/calloc/realloc 的区别

  1. 初始化:malloc 不初始化,calloc 初始化为 0,realloc 不初始化新空间

  2. 参数:malloc 传总字节数,calloc 传个数和单个大小,realloc 传原指针和新总大小

  3. 扩容:只有 realloc 可以调整已申请空间的大小


三、C++ 动态内存管理:new/delete

C 语言的内存管理方式在 C++ 中可以继续使用,但对于自定义类型 不够方便(不会调用构造 / 析构函数),因此 C++ 引入了newdelete操作符。

1. 操作内置类型

和 malloc/free 基本类似,只是语法更简洁:

cpp 复制代码
// 1. 申请单个int空间
int* p1 = new int;
// 2. 申请单个int并初始化为10
int* p2 = new int(10);
// 3. 申请3个int的数组
int* p3 = new int[3];
// 4. 申请3个int的数组并初始化(C++11)
int* p4 = new int[3]{1,2,3};
​
// 释放单个空间
delete p1;
delete p2;
// 释放数组空间(必须加[]!)
delete[] p3;
delete[] p4;

核心规则

  • 申请单个元素 → new + delete

  • 申请数组 → new[] + delete[]

  • 必须匹配使用!否则会导致内存泄漏或程序崩溃

2. 操作自定义类型(核心区别)

new/delete 会自动调用构造函数和析构函数,malloc/free 不会,这是 C++ 和 C 动态内存管理的本质区别。

cpp 复制代码
class A {
public:
    A(int a = 0) : _a(a) {
        cout << "A()构造:" << this << endl;
    }
    ~A() {
        cout << "~A()析构:" << this << endl;
    }
private:
    int _a;
};
​
int main() {
    // malloc:只开空间,不调用构造函数
    A* p1 = (A*)malloc(sizeof(A));
    free(p1); // 只释放空间,不调用析构函数
​
    cout << "-----------------" << endl;
​
    // new:1. 开空间 2. 调用构造函数初始化
    A* p2 = new A(10);
    // delete:1. 调用析构函数清理资源 2. 释放空间
    delete p2;
​
    cout << "-----------------" << endl;
​
    // 数组:调用10次构造函数
    A* p3 = new A[10];
    // 数组:调用10次析构函数
    delete[] p3;
​
    return 0;
}

四、new/delete 底层原理

1. operator new 与 operator delete

newdelete是用户使用的操作符,它们的底层会调用系统提供的两个全局函数:

  • operator new:负责申请空间,底层调用malloc

  • operator delete:负责释放空间,底层调用free

1.1 operator new 的特点

  • 申请失败时bad_alloc异常 ,而不是返回NULL

  • 本质是对 malloc 的封装,增加了异常处理机制

1.2 operator delete 的特点

  • 底层直接调用free释放空间

  • 处理空指针时安全(operator delete (NULL) 什么也不做)

2. new 的完整执行流程

2.1 单个对象 new 的流程

cpp 复制代码
A* p = new A(10);
  1. 调用operator new(sizeof(A))申请空间

  2. 在申请的空间上调用 A 的构造函数,传入参数 10

  3. 返回空间的首地址

2.2 数组 new [] 的流程

cpp 复制代码
A* p = new A[10];
  1. 调用operator new[](10*sizeof(A))申请空间

  2. 在申请的空间上依次调用 10 次 A 的默认构造函数

  3. 返回空间的首地址

3. delete 的完整执行流程

3.1 单个对象 delete 的流程

cpp 复制代码
delete p;
  1. 在 p 指向的空间上调用 A 的析构函数清理资源

  2. 调用operator delete(p)释放空间

3.2 数组 delete [] 的流程

cpp 复制代码
delete[] p;
  1. 在 p 指向的空间上依次调用 10 次 A 的析构函数(从后往前)

  2. 调用operator delete[](p)释放空间


五、定位 new(placement-new)

1. 作用

已经分配好的原始内存空间上,手动调用构造函数初始化对象(不申请新空间)。

2. 语法格式

cpp 复制代码
new(地址) 类型(初始化列表);

3. 使用场景

配合内存池使用:内存池提前申请好一大块空间,需要创建对象时,用定位 new 在已有的空间上初始化,避免频繁向系统申请内存,提高效率。

4. 代码示例

cpp 复制代码
int main() {
    // 1. 先申请一块原始空间(不初始化)
    A* p = (A*)malloc(sizeof(A));
    // 此时p指向的空间还不是一个A对象,因为构造函数没执行
​
    // 2. 用定位new调用构造函数,初始化对象
    new(p) A(10); // 等价于A::A(p,10);
​
    // 3. 使用对象
    // ...
​
    // 4. 手动调用析构函数清理资源
    p->~A();
​
    // 5. 释放原始空间
    free(p);
​
    return 0;
}

六、malloc/free vs new/delete(面试必背)

对比项 malloc/free new/delete
本质 C 标准库函数 C++ 操作符
初始化 不初始化 可以初始化(自定义类型调用构造)
参数 手动计算总字节数 自动计算类型大小,数组指定个数
返回值 void*,需要强转 直接返回对应类型指针
失败处理 返回NULL,必须判空 bad_alloc异常,需要捕获
自定义类型 只开 / 释放空间,不调用构造 / 析构 自动调用构造 / 析构函数
数组处理 手动计算数组大小 new[]/delete[]自动处理

七、常见内存错误与避坑指南

  1. 不匹配使用 new/delete 和 new []/delete []

    • 错误:int* p = new int[10]; delete p;

    • 后果:内存泄漏或崩溃

  2. 重复释放同一块空间

    • 错误:free(p); free(p);

    • 后果:程序崩溃

  3. 释放后未置空指针,形成野指针

    • 错误:free(p); // p变成野指针

    • 正确:free(p); p = NULL;

  4. 使用已经释放的空间

    • 错误:free(p); *p = 10;

    • 后果:非法访问,程序崩溃

  5. 内存泄漏

    • 原因:申请的空间没有释放,程序结束后系统才会回收

    • 危害:长期运行的程序(如服务器)会逐渐耗尽内存


八、快速巩固练习题

  1. 写出下面代码的输出结果,并解释原因:

    cpp 复制代码
    class Test {
    public:
        Test() { cout << "Test()" << endl; }
        ~Test() { cout << "~Test()" << endl; }
    };
    ​
    int main() {
        Test* p1 = (Test*)malloc(sizeof(Test));
        free(p1);
    ​
        Test* p2 = new Test[3];
        delete[] p2;
    ​
        return 0;
    }
  2. 为什么 new [] 和 delete [] 必须匹配使用?

相关推荐
Ricky_Theseus1 小时前
vector 与 list 区别 + 使用场景
c++
Tisfy1 小时前
LeetCode 3629.通过质数传送到达终点的最少跳跃次数:埃式筛+BFS
算法·leetcode·宽度优先·质数·埃式筛
Hello.Reader1 小时前
算法基础(九)——循环不变式如何证明一个算法是正确的
java·开发语言·算法
wuweijianlove2 小时前
算法稳定性分析中的输入扰动建模的技术7
算法
代码中介商2 小时前
C++ 异常处理完全指南
开发语言·c++
MATLAB代码顾问2 小时前
粒子群优化算法(PSO)原理与Python高级实现
开发语言·python·算法
Epiphany.5562 小时前
连通块的遍历
c++·算法·蓝桥杯
alxraves2 小时前
超声诊断图像的关键算法概述
算法·安全·健康医疗·制造·信号处理
mask哥2 小时前
15种算法模式java实现详解
java·算法·力扣