C++八股——内存分配

发布于:2025-05-12 ⋅ 阅读:(16) ⋅ 点赞:(0)

1. 虚拟内存空间

程序进程的虚拟内存空间是操作系统为每个进程提供的独立、连续的逻辑地址空间,与物理内存解耦。其核心目的是隔离进程、简化内存管理,并提供灵活的内存访问控制。

(图片取自于:【C++】内存管理分布_c++内存分布-CSDN博客

自下而上为低地址到高地址。

以Linux x86-64架构为例:

虚拟内存空间分为用户空间内核空间

  • 用户空间(低地址 → 高地址):进程可直接访问的区域,通常占据虚拟地址空间的大部分。
  • 内核空间(高地址保留区):操作系统内核专用,进程无法直接访问,需通过系统调用交互。

用户空间分为

  • 代码段(Text Segment)

    • 地址范围0x400000 附近(起始地址)。
    • 内容:编译后的可执行机器指令(如程序的main函数)。
    • 权限:只读(Read-Only)、可执行(Execute),不可修改。
    • 特点:多个进程可共享同一代码段(如动态库)。
  • 初始化数据段(Data Segment)

    • 地址范围:紧邻代码段。
    • 内容:全局变量、静态变量(已显式初始化,如 int a = 10;)。
    • 权限:可读写(Read-Write),不可执行。
  • 未初始化数据段(BSS Segment)

    • 地址范围:紧邻初始化数据段。
    • 内容:未显式初始化的全局/静态变量(如 int b;),默认值为0。
    • 权限:可读写,不可执行。
  • 堆(Heap)

    • 地址范围:向高地址动态增长。
    • 内容:动态分配的内存(如 malloc()new)。
    • 权限:可读写,不可执行。
    • 管理:由程序员控制分配/释放,可能产生内存碎片。
  • 内存映射区域(Memory Mapping Segment)

    • 地址范围:堆与栈之间的动态区域。

    • 内容

      • 动态链接库(如libc.so)。
      • 文件映射(mmap系统调用,如加载共享内存或大文件)。
      • 匿名映射(用于大块内存分配,如某些malloc实现)。
    • 权限:可自定义(读/写/执行)。

  • 栈(Stack)

    • 地址范围:用户空间顶端附近,向低地址增长。

    • 内容

      • 函数调用栈帧(局部变量、函数参数、返回地址)。
      • 线程栈(每个线程有独立栈空间)。
    • 权限:可读写,不可执行。

    • 管理:自动分配/释放,由编译器控制,大小有限(可能栈溢出)。

  • 环境变量与命令行参数

    • 地址范围:栈的顶部区域。
    • 内容argv(命令行参数)、envp(环境变量)的字符串数组。
    • 权限:可读写。

内核空间

  • 地址范围:64位系统中通常为高地址的 0xffff800000000000 以上。
  • 内容
    • 内核代码、数据结构。
    • 进程页表、硬件驱动、中断处理程序等。
  • 权限:仅内核态可访问,用户态访问会触发段错误。

2. malloc和free

参考文章:malloc 底层实现及原理_malloc底层原理-CSDN博客

malloc

  • 当分配的大小小于128KB时,通过brk()系统调用从堆段分配内存
  • 当分配的大小大于等于128KB时,通过mmap()系统调用从文件映射段分配内存

注意:这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。


free

在实际分配内存时会多分配16字节(位于元数据之前)用来记录内存块描述信息,其中包括内存块的大小。

当调用free释放内存时,传入的指针会首先向前偏移16字节,获取内存块的信息,然后再释放。

  • 对于brk()申请的小空间,会标记为空间,并不会直接释放。当连续空闲空间大于128KB时会执行内存紧缩操作(trim)
  • 对于mmap()申请的大空间,会调用munmap()归还给操作系统

3. new和delete

参考文章:【C++】内存管理分布_c++内存分布-CSDN博客

new 的底层步骤

  • 内存分配:调用 operator new 函数(内部通常基于 malloc)申请指定大小的内存。
  • 对象构造:在分配的内存上调用对象的构造函数(初始化成员变量等)。
  • 异常处理:若内存不足,operator new 抛出 std::bad_alloc 异常(除非使用 nothrow 版本)。

底层展开:

void* ptr = operator new(sizeof(MyClass)); // 内部调用 malloc
MyClass::MyClass(ptr);                     // 构造函数

delete 的底层步骤

  • 对象析构:调用对象的析构函数(清理资源,如释放句柄)。
  • 内存释放:调用 operator delete 函数(内部通常基于 free)释放内存。

底层展开:

MyClass::~MyClass(obj);      // 析构函数
operator delete(obj);        // 内部调用 free

mallocfree的对比

特性 new/delete malloc/free
语言层面 C++ 运算符,支持重载 C 标准库函数,不可重载
内存初始化 调用构造函数/析构函数 仅分配/释放原始内存,无初始化逻辑
类型安全 返回类型明确指针(如 MyClass* 返回 void*,需手动类型转换
内存大小计算 自动计算类型所需大小(如 new int 需手动指定字节数(如 malloc(4)
错误处理 内存不足时抛出异常(可捕获) 返回 NULL,需检查返回值
数组支持 支持 new[]delete[] 需手动计算数组大小,无内置支持
底层扩展性 可自定义 operator new/operator delete 无法修改 malloc/free 行为
适用场景 C++ 对象管理(含构造/析构) 原始内存操作或与 C 代码交互

异常与错误处理

  • new:失败时抛出 std::bad_alloc,可通过 try-catch 捕获。

    try {
        int* arr = new int[1000000000000];
    } catch (const std::bad_alloc& e) {
        std::cerr << "内存不足: " << e.what() << std::endl;
    }
    
  • malloc:失败时返回 NULL,需显式检查。

    int* arr = (int*)malloc(1000000000000 * sizeof(int));
    if (arr == nullptr) {
        perror("malloc 失败");
    }
    

内存对齐与重载

  • new:支持自定义内存对齐(C++17 的 align_val_t)和重载。

    // 自定义 operator new
    void* operator new(size_t size, const char* tag) {
        std::cout << "通过标签分配: " << tag << std::endl;
        return malloc(size);
    }
    MyClass* obj = new ("DEBUG") MyClass();
    
  • malloc:对齐由实现决定,无法直接定制。

数组处理

  • new[]:自动计算数组元素总大小,并为每个元素调用构造函数。

    MyClass* arr = new MyClass[5]; // 调用 5 次构造函数
    delete[] arr;                   // 调用 5 次析构函数
    
  • malloc:需手动计算总字节数,且不构造对象。

    MyClass* arr = (MyClass*)malloc(5 * sizeof(MyClass));
    free(arr); // 不会调用析构函数,可能导致资源泄漏
    

4. 内存池

内存池(Memory Pool)是一种预先分配并统一管理内存资源的技术,旨在优化动态内存分配的效率和性能。

4.1 基本概念

  • 预先分配:在程序初始化阶段,一次性向操作系统申请一大块连续内存(称为“池”)。
  • 自主管理:程序自行管理池内的内存分配与回收,避免频繁调用系统级函数(如malloc/free)。
  • 按需分配:从池中划分小块内存供程序使用,释放时标记为可复用而非立即归还操作系统。

4.2 核心优势

特性 传统malloc/free 内存池
分配速度 需遍历复杂数据结构(如Bins) 直接定位空闲块,速度极快
内存碎片 易产生外部/内部碎片 碎片可控,甚至完全消除(固定块)
系统调用开销 频繁调用brk/mmap 仅初始化和销毁时调用
线程安全 全局锁可能引发竞争 可设计为线程私有池或无锁结构
适用场景 通用、动态需求 高频次、固定/可预测内存需求

4.3 实现方式

固定大小内存池

  • 机制:池中所有内存块大小相同(如4KB)。

  • 管理:使用空闲链表(Free List)跟踪可用块。

    struct MemoryBlock {
        MemoryBlock* next; // 指向下一个空闲块
    };
    MemoryBlock* free_list; // 空闲链表头
    
  • 操作

    • 分配:从链表头部取一个块,时间复杂度O(1)。
    • 释放:将块插回链表头部,时间复杂度O(1)。
  • 优点:零碎片、极快分配。

  • 缺点:无法处理变长需求,可能浪费内存。

可变大小内存池

  • 机制:支持不同大小的内存请求。
  • 管理
    • 分离空闲链表:为不同大小范围维护多个链表(如8B、16B、32B…)。
    • 伙伴系统(Buddy System):按2的幂次分割内存块,合并相邻空闲块。
      • 分配时向上取整到最近的2^n大小。
      • 释放时检查“伙伴块”是否空闲,若空闲则合并。
  • 优点:灵活支持变长请求,减少碎片。
  • 缺点:管理复杂度高,存在内部碎片。

4.4 典型应用场景

  • 高频次小对象分配

    • 如网络服务器为每个请求分配临时缓冲区。
    • 示例:Nginx使用内存池管理HTTP请求资源。
  • 实时系统

    确保内存分配时间确定性,避免传统malloc的不可预测延迟。

  • 游戏开发

    快速创建/销毁大量游戏实体(如子弹、粒子效果),通过对象池(Object Pool)实现。

  • 嵌入式系统

    资源受限环境,需严格控制内存使用和碎片。

4.5 示例

#define BLOCK_SIZE 64     // 固定块大小
#define POOL_SIZE 100     // 池中块数量

typedef struct MemoryBlock {
    struct MemoryBlock* next;
} MemoryBlock;

MemoryBlock* free_list = NULL;

// 初始化内存池
void init_pool() {
    static char pool[BLOCK_SIZE * POOL_SIZE];
    for (int i = 0; i < POOL_SIZE; i++) {
        MemoryBlock* block = (MemoryBlock*)(pool + i * BLOCK_SIZE);
        block->next = free_list;
        free_list = block;
    }
}

// 分配内存
void* pool_alloc() {
    if (!free_list) return NULL; // 池耗尽
    MemoryBlock* block = free_list;
    free_list = free_list->next;
    return (void*)block;
}

// 释放内存
void pool_free(void* ptr) {
    MemoryBlock* block = (MemoryBlock*)ptr;
    block->next = free_list;
    free_list = block;
}

(注:以上内容参考自DeepSeek)


网站公告

今日签到

点亮在社区的每一天
去签到