C++ 内存管理

内存管理

  • [1. C/C++内存分布](#1. C/C++内存分布)
    • 进程地址空间各区域功能说明
    • [1. 代码段(Code Segment / 文本段 / 常量区)](#1. 代码段(Code Segment / 文本段 / 常量区))
    • [2. 数据段(Data Segment / 静态区)](#2. 数据段(Data Segment / 静态区))
    • [3. 堆(Heap)](#3. 堆(Heap))
    • [4. 代码区(Code Segment)](#4. 代码区(Code Segment))
    • [5. 栈(Stack / 堆栈)](#5. 栈(Stack / 堆栈))
  • [2. C语言中动态内存管理方式:malloc/calloc/realloc/free](#2. C语言中动态内存管理方式:malloc/calloc/realloc/free)
    • 示例代码
    • [1. malloc/calloc/realloc 的区别](#1. malloc/calloc/realloc 的区别)
    • [2. malloc 的实现原理](#2. malloc 的实现原理)
  • [3. C++内存管理方式](#3. C++内存管理方式)
  • [4. operator new 与 operator delete 函数](#4. operator new 与 operator delete 函数)
  • [5. new和delete的实现原理](#5. new和delete的实现原理)

1. C/C++内存分布

进程地址空间各区域功能说明

进程地址空间是操作系统为每个进程划分的虚拟内存地址范围,并非物理内存的直接映射。不同区域有严格的功能划分和访问规则,从低地址到高地址依次为:

1. 代码段(Code Segment / 文本段 / 常量区)

  • 核心功能:存储程序中可执行的机器指令(编译后的二进制代码)、只读常量(如字符串常量、const修饰的只读全局常量等)。
  • 访问权限:只读(READ-ONLY),禁止写入操作,防止程序指令被篡改,提升安全性。
  • 存储内容示例
    • 函数的二进制执行代码(如Test()函数的指令)。
    • 字符串常量(如代码char* c = "abcd"中的"abcd")。
    • const修饰的全局只读常量。
  • 特性
    • 多个进程可共享同一份代码段(如系统库函数的代码),节省内存。
    • 程序运行期间大小固定,不会动态变化。

2. 数据段(Data Segment / 静态区)

  • 核心功能 :存储全局数据、静态数据,分为初始化数据段和未初始化数据段。
    • 初始化数据段:存储已初始化的全局变量、静态变量(如globalVar、staticGlobalVar、staticVar)。
    • 未初始化数据段:存储未初始化的全局变量、静态变量,程序启动时操作系统会自动将其初始化为0。
  • 访问权限:可读可写(READ-WRITE),运行期间可修改变量值。
  • 特性
    • 生命周期与进程一致,程序启动时分配内存,程序退出时释放。
    • 大小在编译期确定,运行期间不会动态扩展/收缩。
    • 全局变量和静态变量默认存储在此区域,无需手动分配/释放。

3. 堆(Heap)

  • 核心功能:程序运行时动态内存分配的核心区域,用于存储运行期按需创建的数据。
  • 访问权限:可读可写(READ-WRITE)。
  • 特性
    • 地址空间向上增长(从低地址向高地址扩展如上面的图片中的样子)。
    • 生命周期由程序员控制,需手动通过malloc/calloc/realloc申请,free释放,若未释放会导致内存泄漏。
    • 大小不固定,可随动态内存分配/释放灵活变化。
    • 堆的管理由操作系统的内存管理器完成,频繁申请/释放会产生内存碎片。

4. 代码区(Code Segment)

  • 核心功能
    • 存储程序编译后的可执行机器指令(如函数的二进制执行代码),是CPU能直接解析运行的核心内容。
    • 存储程序中的只读常量(如字符串常量、const修饰的全局只读常量),保障常量不被篡改。
    • 作为程序运行的基础载体,所有进程的执行逻辑均依赖代码区的指令序列。
  • 访问权限:默认只读(READ-ONLY),禁止写入操作,防止程序指令或只读常量被恶意/误操作修改,提升程序运行安全性。
  • 特性
    • 是进程地址空间的基础核心区域,程序启动时优先加载,退出时最后释放。
    • 多个进程可共享同一份代码区内容(如系统标准库函数的指令),大幅节省内存资源。
    • 大小在编译期确定,程序运行期间固定不变,无动态扩展/收缩可能。

5. 栈(Stack / 堆栈)

  • 核心功能:存储非静态局部变量、函数参数、函数返回值、函数调用的上下文信息(如返回地址、ebp寄存器值等)。
  • 访问权限:可读可写(READ-WRITE)。
  • 特性
    • 地址空间向下增长(从高地址向低地址扩展如图所示)。
    • 由操作系统自动管理,函数调用时分配栈帧,函数返回时释放栈帧,无需程序员干预。
    • 栈的大小固定(通常为几MB),超出会触发栈溢出(Stack Overflow)会直接结束程序。
    • 存储的变量生命周期为函数调用周期,函数执行结束后变量立即销毁。
    • 栈帧是栈的基本单位,每个函数调用对应一个独立的栈帧,保护函数调用的独立性。
示例代码
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";
    const char* pChar3 = "abcd";
    int* ptr1 = (int*)malloc(sizeof(int) * 4);
    int* ptr2 = (int*)calloc(4, sizeof(int));
    int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
    free(ptr1);
    free(ptr3);
}

假设不同分区为以下所示:

A. 栈

B. 堆

C. 数据段 (静态区)

D. 代码段 (常量区)

那么代码中的不同变量所在的分区为表中所示

变量 位置 详细解析
globalVar C globalVar 是全局初始化变量,存储在数据段(.data 区)。
staticGlobalVar C staticGlobalVar 是全局静态初始化变量,存储在数据段(.data 区)。
staticVar C staticVar 是局部静态初始化变量,存储在数据段(.data 区)。
localVar A localVar 是局部非静态变量,存储在栈区(Test 函数的栈帧中)。
num1 A num1 是局部非静态数组,存储在栈区,数组元素均在栈帧内。
char2 A char2 是局部非静态字符数组,存储在栈区。
*char2 A *char2 是 char2 数组首元素的解引用,数组本身在栈区,因此指向栈区的数组元素('a')。
pChar3 A pChar3 是局部非静态指针变量,存储在栈区。
*pChar3 D *pChar3 指向字符串常量 "abcd",该常量存储在代码段(只读常量区)。
ptr1 A ptr1 是局部非静态指针变量,存储在栈区。
*ptr1 B *ptr1 是 ptr1 指向的动态内存,由 malloc 分配,存储在堆区。

衍生问题

问题 答案 详细解析
sizeof(num1) 40 num1 是 int 类型数组,包含 10 个元素,int 占 4 字节,10*4=40;sizeof 计算数组总字节数,与数组元素初始化与否无关。
sizeof(char2) 5 char2 是字符数组,存储 "abcd" 时编译器自动追加 '\0' 作为字符串结束符,共 5 个 char 元素,char 占 1 字节,总字节数为 5。
strlen(char2) 4 strlen 是库函数,仅统计 '\0' 之前的有效字符数,"abcd" 共 4 个字符,因此结果为 4。
sizeof(pChar3) 4/8 pChar3 是指针变量,32 位系统下指针占 4 字节,64 位系统下指针占 8 字节;sizeof 计算指针本身的字节数,与指向的内容无关。
strlen(pChar3) 4 pChar3 指向代码段的字符串常量 "abcd",strlen 统计 '\0' 前字符数,结果为 4。
sizeof(ptr1) 4/8 ptr1 是指针变量,32 位系统占 4 字节,64 位系统占 8 字节;所有指针类型的 sizeof 结果仅与系统位数相关。

sizeof 和 strlen 核心区别说明

  • 本质属性
    • sizeof:C/C++ 的内置运算符,编译期间完成计算,不涉及运行时逻辑。
    • strlen:C 标准库函数(<string.h>),运行期间遍历字符串,直到遇到 '\0' 停止计算。
  • 计算范围
    • sizeof:计算变量 / 数据类型占用的总字节数,包含 '\0'、数组未初始化部分等所有内存。
    • strlen:仅统计字符串中 '\0' 之前的有效字符个数,不包含 '\0',也不处理非字符串数据。
  • 适用场景
    • sizeof:可用于任意数据类型(int、char、数组、指针、结构体、类等),无格式要求。
    • strlen:仅适用于以 '\0' 结尾的字符串(char*),对非字符串类型(如 int 数组)使用会导致未定义行为(越界访问)。
  • 参数要求
    • sizeof:参数可以是类型名(如 sizeof (int))、变量名(如 sizeof (num1))、表达式(如 sizeof (1+2))。
    • strlen:参数必须是指向以 '\0' 结尾的字符串的指针(char*),不能直接传入类型名。
  • 返回值意义
    • sizeof:返回值表示内存占用的字节数,反映数据的存储大小。
    • strlen:返回值表示字符串的有效长度,反映字符串的逻辑长度。### 1. C/C++内存分布

2. C语言中动态内存管理方式:malloc/calloc/realloc/free

示例代码

cpp 复制代码
void Test (){
    // malloc申请内存 未初始化
    int* p1 = (int*) malloc(sizeof(int));
    free(p1); // 释放malloc申请的内存
    
    // calloc申请内存 自动初始化
    int* p2 = (int*)calloc(4, sizeof (int));
    
    // realloc调整p2指向的内存大小
    int* p3 = (int*)realloc(p2, sizeof(int)*10);
    // 无需free(p2) realloc成功后p2已失效
    free(p3 ); // 释放realloc调整后的内存
}

1. malloc/calloc/realloc 的区别

  • 核心区别总结
    • malloc
      • 功能:向堆区申请指定字节数的连续内存空间。
      • 初始化:申请的内存未初始化,内容为随机值。
      • 参数:仅需传入申请的字节数(例如:malloc(sizeof(int)*4))。
    • calloc
      • 功能:向堆区申请指定数量 + 指定大小的连续内存空间。
      • 初始化:申请的内存自动初始化为 0。
      • 参数:需传入元素个数和单个元素大小(例如:calloc(4, sizeof(int)))。
      • 等价于:malloc(4*sizeof(int)) + memset(申请的内存, 0, 4*sizeof(int))
    • realloc
      • 功能:调整已申请动态内存的大小(扩大或缩小)。
      • 内存处理:
        • 若原内存后有足够连续空间,直接扩容,返回原地址。
        • 若原内存后无足够空间,重新申请新内存,拷贝原数据,释放原内存,返回新地址。
      • 参数:需传入原内存指针和新的总字节数(例如:realloc(p2, sizeof(int)*10))。
      • 注意:成功调用后原指针失效,无需释放原指针,仅释放新返回的指针。
  • 关键注意点
    • realloc 调整内存后,原指针(如示例中的 p2)无需调用 free,否则会导致重复释放。
    • 三者申请的内存均在堆区,需手动调用 free 释放,否则会造成内存泄漏。

2. malloc 的实现原理

  • 核心原理(简要说明)
    • 底层依赖 :基于操作系统的内存管理接口(如 Linux 的 brk/sbrk/mmap)实现。
    • 内存池机制malloc 并非每次申请都直接向操作系统要内存,而是先向系统申请一块大内存作为内存池。后续小内存申请直接从内存池分配,减少系统调用开销。
    • 空闲链表管理:用链表管理已释放的空闲内存块,记录块的大小和地址。申请内存时遍历空闲链表,按适配策略(如首次适配或最佳适配)找到合适的内存块分配。
    • 内存对齐:分配的内存会按系统要求对齐(如 8 字节或 16 字节对齐),保证内存访问效率。
    • 扩容机制:若内存池不足,会再次向操作系统申请新的大块内存,补充内存池。

3. C++内存管理方式

C语言内存管理方式在C++中可继续使用,但在自定义类型场景下存在局限且使用繁琐,因此C++提出专属内存管理方式,通过newdelete操作符实现动态内存管理。

3.1 new/delete操作内置类型

核心用法对比

实现方式 代码示例 核心特点
C语言(malloc/free) int* p1 = (int*)malloc(sizeof(int)); int* p3 = (int*)malloc(sizeof(int)*10); free(p1); free(p3); malloc是函数,需手动强转类型,仅完成内存申请/释放
C++(new/delete) int* p2 = new int; int* p2_init = new int(10); int* p4 = new int[10]; delete p2; delete[] p4; new是操作符,无需强转类型,支持单个元素初始化

核心知识点

  1. 申请释放单个元素空间,必须匹配使用newdelete
  2. 申请释放连续空间,必须匹配使用new[]delete[]
  3. 内置类型数组无法通过new int[10](10)方式统一初始化。
  4. 内置类型场景下,new/delete仅简化语法,功能等价于malloc/free

3.2 new和delete操作自定义类型

cpp 复制代码
class A{  
public:  
    A()   
    {  
        _a = 0;  
        cout << "A()" << endl;  
    }  
    ~A()   
    {  
        cout << "~A()" << endl;  
    }  
private:  
    int _a;  
};  

void Test2()  
{  
    A* p1 = (A*)malloc(sizeof(A));   
    A* p4 = new A;   
    free(p1);  
    delete p4;  
}  

核心知识点

  • 内置类型场景,new/deletemalloc/free功能效果一致。
  • 自定义类型场景,核心差异:
    • malloc仅申请内存空间,不调用构造函数。
    • free仅释放内存空间,不调用析构函数。
    • new在申请空间后自动调用构造函数,完成对象初始化。
    • delete在释放空间前自动调用析构函数,完成对象资源清理。
  • C++中优先使用new/delete管理自定义类型内存。

4. operator new 与 operator delete 函数

4.1 operator new 与 operator delete 函数(重点)

核心概念

  • new/delete是用户层的内存申请/释放操作符。
  • operator new/operator delete是系统提供的全局函数,并非运算符重载。
  • new底层调用operator new申请空间,delete底层调用operator delete释放空间。
  • operator new用法与malloc一致。

核心代码实现

cpp 复制代码
void Test3()  
{  
    size_t size = 2;   
    void* p3 = malloc(size * 1024 * 1024 * 1024);   
    cout << p3 << endl;  
    free(p3);  
    try   
    {  
        void* p4 = operator new(size * 1024 * 1024 * 1024);  
        cout << p4 << endl;  
        operator delete (p4);  
    }  
    catch(exception& e)  
    {  
        cout << e.what() << endl;  
    }  
}  

核心知识点

  • 内存大小计算需使用无符号整形(size_t):有符号整形范围为 − 2 31 ∼ 2 31 − 1 -2^{31} \sim 2^{31}-1 −231∼231−1, 2 × 1024 × 1024 × 1024 = 2 31 2 \times 1024 \times 1024 \times 1024 = 2^{31} 2×1024×1024×1024=231,超出范围会导致溢出。
  • mallocoperator new的核心区别:
    • malloc申请空间失败返回NULL
    • operator new申请空间失败抛出异常,异常是面向对象处理错误的标准方式。
  • operator new底层实现逻辑:调用malloc申请内存空间,申请成功直接返回,申请失败则执行用户提供的空间不足应对措施,无应对措施则抛异常。
  • operator delete底层实现逻辑:最终通过free释放空间,释放失败直接终止进程。
  • 设计operator delete仅为与operator new成对出现,功能与free无区别。
  • new的完整逻辑:operator new申请空间 + 调用构造函数初始化。
  • delete的完整逻辑:调用析构函数清理资源 + operator delete释放空间。
  • new相比malloc的核心优势:自动调用构造函数初始化、申请失败抛异常。
  • delete相比free的核心优势:自动调用析构函数清理资源。

5. new和delete的实现原理

5.1 内置类型

核心知识点

  1. 内置类型场景下,new/malloc、delete/free 功能基本一致。
  2. 核心差异点:
    • 单个元素空间使用 new/delete,连续空间使用 new[]/delete[],需严格匹配。
    • new 申请空间失败时抛出异常,malloc 申请失败返回 NULL。

5.2 自定义类型

new的实现原理

  1. 调用 operator new 函数完成内存空间申请。
  2. 在申请到的内存空间上执行构造函数,完成对象初始化。

delete的实现原理

  1. 在目标内存空间上执行析构函数,完成对象内资源的清理。
  2. 调用 operator delete 函数释放该内存空间。

new T[N]的实现原理

  1. 调用 operator new[] 函数,该函数内部实际调用 operator new 函数,完成 N 个对象所需连续内存空间的申请。
  2. 在申请到的连续空间上依次执行 N 次构造函数,完成 N 个对象的初始化。

delete[]的实现原理

  1. 在待释放的连续内存空间上依次执行 N 次析构函数,完成 N 个对象的资源清理。
  2. 调用 operator delete[] 函数释放空间,该函数内部实际调用 operator delete 函数完成内存释放。

核心代码实现与原理模拟

cpp 复制代码
class B {
public:
    B(int b = 0)
        : _b(b) {
        cout << "B()" << endl;
    }
    ~B() {
        cout << "~B()" << endl;
    }
private:
    int _b;
};

void Test4() {
    // 标准 new/delete 使用方式
    B* p1 = new B;
    delete p1;

    // 手动模拟 new 的实现过程
    // 步骤 1:调用 operator new 申请内存空间
    B* p2 = (B*)operator new(sizeof(B));
    // 步骤 2:使用定位 new 在已申请空间上执行构造函数初始化
    // 语法:new(内存空间指针)类名(构造函数参数)
    new(p2) B(1);

    // 手动模拟 delete 的实现过程
    // 步骤 1:显式调用析构函数清理资源(析构函数公有可显式调用,构造函数不可)
    p2->~B();
    // 步骤 2:调用 operator delete 释放内存空间
    operator delete(p2);
}

关键补充知识点

  • 定位 new(placement new):用于在已申请的内存空间上显式调用构造函数完成对象初始化,语法为 new(空间指针)类型(构造参数)。
  • 构造函数不可显式调用,仅能由编译器自动调用或通过定位 new 间接调用。
  • 析构函数为公有属性时可显式调用,用于手动触发资源清理。
  • 堆区申请 4G 大小空间的前提:编译器需调整为 64 位,32 位系统地址空间不足无法支持。

7. 内存泄漏

7.1 什么是内存泄漏 内存泄漏的危害

  • 内存泄漏的定义:内存泄漏是指因程序设计疏忽或错误,导致已分配且不再使用的内存未能被释放的情况。内存泄漏并非物理内存消失,而是程序失去对该段内存的控制权,造成内存资源的浪费。
  • 内存泄漏的危害:长期运行的程序受内存泄漏影响极大,如操作系统、后台服务、服务器程序等。泄漏的内存会持续占用系统资源,导致可用内存逐渐减少,程序响应速度越来越慢。严重时会耗尽系统所有可用内存,造成程序卡死、系统崩溃。
  • 典型场景示例:
    • 服务器后台服务:电商平台的订单处理服务,每处理一个订单就泄漏 1KB 内存,单日百万级订单量会导致泄漏约 1GB 内存,运行一周后内存耗尽,服务无响应,订单无法处理。
    • 操作系统组件:系统桌面进程存在内存泄漏,每打开/关闭一个文件就泄漏少量内存,长期使用后系统可用内存不足,桌面卡顿、操作延迟,最终需重启系统。
    • 嵌入式设备程序:智能家居的控制芯片程序,因内存泄漏持续占用有限的嵌入式内存,运行数月后内存耗尽,设备失去响应,无法执行开关灯、调节温度等指令。
相关推荐
sali-tec2 小时前
C# 基于OpenCv的视觉工作流-章38-BF特征匹配
图像处理·人工智能·opencv·算法·计算机视觉
历程里程碑2 小时前
链表-----
数据结构·线性代数·算法·链表·矩阵·lua·perl
小强开学前2 小时前
自定义 Drawable 实现任意高度纯圆角背景及玻璃效果
android
一叶落4382 小时前
167. 两数之和 II - 输入有序数组【C语言题解】
c语言·数据结构·算法·leetcode
地平线开发者2 小时前
征程6 MCU safetylib sample
算法·自动驾驶
Barkamin2 小时前
归并排序的简单实现
数据结构
老赵的博客2 小时前
qwebengineview 锲入网页并关闭
c++
biter down2 小时前
C++ 单例模式:饿汉与懒汉模式
开发语言·c++·单例模式
秃了也弱了。2 小时前
ElasticSearch:优化案例实战解析(持续更新)
android·java·elasticsearch