【初阶C++】内存管理

前言

2026 年后端、嵌入式、C++ 校招面试中,内存管理是必考核心模块,很多同学只会背堆栈概念,搞不清全局变量、静态变量、字符串常量、动态内存分别存放在哪一段,同时分不清malloc/freenew/delete底层差异、operator new与定位 new 的使用场景。本文完全基于作者自己学习的内容总结,如有错误,还望大家指出,一起学习。

一、C/C++ 程序五大内存分区详解

程序编译链接加载到内存后,操作系统会划分内核空间用户空间,用户空间从上至下分为 5 大区域:栈、内存映射段、堆、数据段(静态区)、代码段(常量区),内核空间用户代码无法读写。

内存布局总览(从高地址→低地址)

  1. 内核空间:操作系统内核占用,应用无权访问
  2. 栈区(向下增长,高地址→低地址)
  3. 内存映射段:动态库、文件映射、进程共享内存
  4. 堆区(向上增长,低地址→高地址)
  5. 数据段(静态区):.data 已初始化全局 / 静态变量、.bss 未初始化全局 / 静态变量
  6. 代码段(.text/.rodata 只读常量区):程序指令、字符串字面量

分区详细说明

1. 栈(堆栈 stack)
  • 存储内容:函数非静态局部变量、函数形参、函数返回地址、临时变量
  • 增长方向:向下(高地址向低地址拓展)
  • 生命周期:函数调用创建栈帧,函数执行完毕自动回收,无需手动释放
  • 限制:栈空间大小固定(Linux 默认 8MB),局部数组过大容易栈溢出
  • 特性:编译器自动管理,分配释放速度极快
2. 内存映射段
  • 核心用途:高效 IO 映射、加载动态共享库、mmap 创建进程间共享内存
  • 2026 年开发场景:大文件读写、零拷贝、多进程通信底层依赖此区域,入门阶段只需了解概念
3. 堆(heap)
  • 存储内容:malloc/calloc/realloc/new动态申请的内存
  • 增长方向:向上(低地址向高地址拓展)
  • 生命周期:完全由程序员手动管理,必须主动free/delete释放;未释放会内存泄漏,程序结束操作系统统一回收
  • 特性:空间无固定上限,受虚拟内存大小限制;频繁分配释放会产生内存碎片
4. 数据段(全局 / 静态区)

细分两段:

  • .data段已初始化非 0的全局变量、静态变量,程序编译时占用可执行文件磁盘空间
  • .bss段:未初始化 / 初始化为 0 的全局、静态变量,磁盘不占空间,程序加载到内存时操作系统自动清零
  • 生命周期:程序启动分配,程序结束才释放,全局生效
5. 代码段(只读常量区 .text + .rodata)
  • .text:编译后的程序机器指令(函数代码),只读,禁止修改
  • .rodata:只读常量,如字符串字面量"abcd"const char*指向的常量字符串
  • 特性:只读,强行修改会触发段错误崩溃

实战代码分区判断题(PPT 原题完整解析)

c

运行

复制代码
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. 代码段 (常量区) 逐行答案 + 详细解释:

  1. globalVar → C 数据段:全局已初始化变量存.data 段
  2. staticGlobalVar → C 数据段:静态全局变量同样存.data
  3. staticVar → C 数据段:static 修饰局部变量,生命周期提升至全局,存放静态区
  4. localVar → A 栈:普通非静态局部变量,函数栈帧内
  5. num1 → A 栈:局部数组,数组本体开辟在栈上
  6. char2 → A 栈:字符数组本体在栈,字符串abcd编译时拷贝到栈数组中,可修改char2[0] = 'x'
  7. *char2 → A 栈:数组第一个字符,位于栈内存
  8. pChar3 → A 栈:指针变量本身是局部变量,存放在栈
  9. *pChar3 → D 代码段:"abcd"是字符串常量,存只读常量区,不能修改pChar3[0]='x'会崩溃
  10. ptr1 → A 栈:指针变量本身在栈
  11. *ptr1 → B 堆:malloc 开辟的内存空间在堆上

二、C 语言动态内存管理:malloc/calloc/realloc/free

C 语言依靠库函数完成堆内存分配,四个函数均定义在<stdlib.h>头文件。

1. 四大函数原型与核心区别

(1)malloc

c

运行

复制代码
void* malloc(size_t size);
  • 功能:分配size字节连续堆内存,不会初始化内存,内存中是随机垃圾值
  • 参数:size 为需要分配的总字节数
  • 返回值:分配成功返回堆首地址;失败返回NULL,使用前必须判空
  • 适用场景:已知总字节,不需要初始化的内存块
(2)calloc

c

运行

复制代码
void* calloc(size_t num, size_t size);
  • 功能:分配num个、每个大小size字节的连续空间,自动将所有字节初始化为 0
  • 参数:num 元素个数,size 单个元素字节大小
  • 对比 malloc:多一步清零操作,分配速度略慢;数组场景优先使用 calloc,省去手动 memset
  • 示例:int* p2 = (int*)calloc(4, sizeof(int));分配 4 个 int,全部初始 0
(3)realloc

c

运行

复制代码
void* realloc(void* ptr, size_t new_size);
  • 功能:调整已分配堆内存大小,扩容 / 缩容 两种扩容逻辑:
  1. 原内存后方有足够空闲堆空间:直接向后拓展,返回原指针地址
  2. 后方空间不足:堆中开辟一块全新满足大小的内存,拷贝旧数据到新空间,释放原内存块,返回新指针
  • 特殊规则:ptr == NULL时,realloc等价于malloc(new_size)
  • 坑点:扩容失败返回NULL,原内存不会释放,不能直接ptr = realloc(ptr, size),会丢失原指针造成内存泄漏
(4)free

c

运行

复制代码
void free(void* ptr);
  • 功能:释放 malloc/calloc/realloc 申请的堆内存
  • 注意:不能释放栈、静态区、常量区指针;重复 free、free 野指针会程序崩溃

面试经典例题解析

c

运行

复制代码
void Test ()
{
int* p2 = (int*)calloc(4, sizeof (int));
int* p3 = (int*)realloc(p2, sizeof(int)*10);
// 这里需要free(p2)吗?
free(p3 );
}

答案:不需要手动 free (p2) realloc 扩容时,如果开辟了新内存,底层会自动释放 p2 指向的旧内存;最后只需要释放最新的 p3 指针即可。如果手动 free (p2) 会导致重复释放,程序崩溃。

面试高频问题 1:malloc/calloc/realloc 三者区别总结

  1. 初始化:malloc 不初始化;calloc 分配后全部置 0;realloc 仅拷贝原有数据,新增空间随机值
  2. 参数形式:malloc 传总字节;calloc 传元素个数 + 单元素大小;realloc 传旧指针 + 新总字节
  3. 用途:malloc 单块无初始化内存;calloc 数组清零场景;realloc 动态调整内存大小

面试高频问题 2:malloc 底层实现原理(glibc 版)

glibc 中 malloc 采用空闲链表管理堆内存:

  1. 操作系统提供堆起始地址,维护空闲内存双向链表
  2. 调用 malloc:遍历链表寻找第一个满足大小的空闲块(首次适配算法),分割内存,用户使用一块,剩余小块放回空闲链表
  3. 调用 free:将释放的内存块放回链表,自动合并相邻空闲块,减少内存碎片
  4. 空闲链表无足够大内存时,向操作系统调用 brk/mmap 拓展堆空间

三、C++ 专属内存管理:new 与 delete 操作符

C 语言malloc/free在 C++ 中可兼容,但无法面向对象处理自定义类型,因此 C++ 设计new/delete操作符,底层依然封装 malloc/free,但增加对象构造、析构逻辑。

3.1 new/delete 操作内置类型

基础语法示例

cpp

运行

复制代码
void Test()
{
// 1.动态申请单个int空间,未初始化
int* ptr4 = new int;
// 2.动态申请单个int并初始化为10
int* ptr5 = new int(10);
// 3.动态申请连续3个int数组空间
int* ptr6 = new int[3];

// 释放单个对象用delete,数组必须配套delete[]
delete ptr4;
delete ptr5;
delete[] ptr6;
}
强制匹配规则(2026 面试高频坑)
  • new 单个变量delete
  • new[] 数组delete[] 内置类型偶尔不匹配不会崩溃,但自定义类型一定会内存泄漏、未调用析构函数。

3.2 new/delete 操作自定义类型(核心差异)

malloc/free 和 new/delete 最本质区分:new 会调用构造函数初始化对象,delete 调用析构函数释放对象内部资源;malloc/free 仅开辟原始内存,不执行构造析构 完整示例代码:

cpp

运行

复制代码
#include <iostream>
using namespace std;
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));
    // new:开辟内存 + 调用构造函数初始化对象
    A* p2 = new A(1);
    
    free(p1); // free仅释放内存,不调用析构
    delete p2;// delete先调用析构清理资源,再释放内存

    // 内置类型对比
    int* p3 = (int*)malloc(sizeof(int));
    int* p4 = new int;
    free(p3);
    delete p4;

    // 对象数组
    A* p5 = (A*)malloc(sizeof(A)*10);
    A* p6 = new A[10]; // 调用10次构造
    free(p5);
    delete[] p6; // 调用10次析构
    return 0;
}

运行结果可以直观看到:malloc 开辟的内存没有构造、free 释放时无析构;new/delete 完整管理对象生命周期。

四、operator new 与 operator delete 底层全局函数(重点)

很多初学者混淆new操作符operator new函数,这里彻底区分:

  1. new / delete操作符,程序员直接使用,完整流程分配 + 构造 / 析构 + 释放
  2. operator new / operator delete系统全局库函数,new 底层内部自动调用它申请堆内存,本质封装 malloc

4.1 operator new 源码简化解析

cpp

运行

复制代码
void *operator new(size_t size)
{
    void *p;
    // 调用malloc申请堆内存
    while ((p = malloc(size)) == nullptr)
    {
        // 内存不足时执行用户自定义内存处理函数
        if (_callnewh(size) == 0)
        {
            // 分配失败抛出bad_alloc异常,区别于malloc返回NULL
            static const std::bad_alloc nomem;
            throw nomem;
        }
    }
    return p;
}

核心逻辑:

  • 底层依赖 malloc 分配堆内存
  • 分配失败不会返回空指针,抛出bad_alloc异常,程序不捕获会直接终止

4.2 operator delete 源码简化解析

cpp

运行

复制代码
void operator delete(void *pUserData)
{
    if (pUserData == nullptr)
        return;
    // 底层最终调用free释放内存
    _free_dbg(pUserData, _NORMAL_BLOCK);
}

核心逻辑:底层封装 free,接收空指针直接返回,不会崩溃。

cpp 复制代码
class A
{
public:
	A(int a1 = 0, int a2 = 0)
		:_a1(a1)
		,_a2(a2)
	{
		cout << "A(int a1 = 0, int a2 = 0)" << endl;
	}

	A(const A& aa)
		:_a1(aa._a1)
	{
		cout << "A(const A& aa)" << endl;
	}

	A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a1 = aa._a1;
		}
		return *this;
	}

	~A()
	{
		//delete _ptr;
		cout << "~A()" << endl;
	}

	void Print()
	{
		cout << "A::Print->" << _a1 << endl;
	}

	A& operator++()
	{
		_a1 += 100;

		return *this;
	}
private:
	int _a1 = 1;
	int _a2 = 1;
};

class B
{
public:
	~B()
	{
		cout << "~B()" << endl;
	}
private:
	int _b1 = 2;
	int _b2 = 2;
};

int main()
{
	//int* p1 = new int[10]; // -> malloc
	//delete p1;             // -> free
	//free(p1);

	B* p2 = new B[10];
	delete p2;

	A* p3 = new A[10];
	delete[] p3; //危险写法

	return 0;
}

这里为什么编译器申请的空间不一样?

编译器分配堆内存大小不一样的根本原因: 当类拥有用户自定义析构函数时,new[]会多开辟一块头部空间,用来保存数组元素个数; 如果类没有自定义析构函数(编译器默认 trivial 平凡析构),不需要记录数量,因此不额外分配头部。

总结底层调用链路

  1. new A执行顺序:operator new(malloc 开辟内存) → 调用 A 构造函数
  2. delete p执行顺序:调用 A 析构函数 → operator delete(free 释放内存)
  3. new A[N]:调用operator new[],内部多次调用 operator new,循环 N 次构造
  4. delete[] p:循环 N 次调用析构,再调用operator delete[]释放整块内存

五、定位 new 表达式 placement-new(拓展知识点)

定义

定位 new 不会开辟新堆内存,在一块已经提前分配好的原始内存上,手动调用构造函数初始化对象

使用语法

cpp

运行

复制代码
new (place_address) type;
new (place_address) type(初始化参数);

place_address必须是一块已经分配完成的内存指针(栈、堆、内存池都可以)。

实战示例(内存池标准用法)

cpp

运行

复制代码
#include <iostream>
using namespace std;
class A
{
public:
    A(int a = 0)
        : _a(a)
    {
        cout << "构造A:" << this << endl;
    }
    ~A()
    {
        cout << "析构~A:" << this << endl;
    }
private:
    int _a;
};
int main()
{
    // 1.malloc提前开辟原始堆内存,无对象、无构造
    A* p1 = (A*)malloc(sizeof(A));
    // 定位new:在已分配内存上调用构造函数创建对象
    new(p1)A(10);
    // 手动调用析构函数(定位new必须手动析构,不能直接delete)
    p1->~A();
    free(p1);

    // 2.operator new开辟内存搭配定位new
    A* p2 = (A*)operator new(sizeof(A));
    new(p2)A(20);
    p2->~A();
    operator delete(p2);
    return 0;
}

核心使用场景:内存池

2026 年高并发、游戏、服务器开发大量使用内存池,预先批量分配大块内存,运行时直接取出内存块,通过定位 new 构造对象,减少频繁 malloc 带来的性能损耗与内存碎片。

注意事项

  1. 定位 new 仅初始化对象,不分配内存,释放内存仍需使用free/operator delete
  2. 销毁对象不能使用 delete,必须手动显式调用析构函数obj->~类名()

六、malloc/free 与 new/delete 完整对比(面试背诵版)

共同点:两者都是在堆区分配内存,都需要程序员手动释放,不释放会造成内存泄漏。 六大核心区别:

  1. 本质类型不同 malloc/free 是库函数,需要包含头文件<stdlib.h>;new/delete 是 C++ 内置操作符,编译器直接识别。
  2. 内存初始化能力 malloc 仅分配原始内存,内容随机;new 支持直接初始化内置类型、自动调用自定义类型构造函数。
  3. 内存大小计算 malloc 需要手动计算总字节malloc(sizeof(A)*10);new 自动推导类型大小new A[10],无需手动计算。
  4. 返回值与类型安全 malloc 返回void*,使用时必须强制类型转换;new 直接返回对应类型指针,类型安全,无需强转。
  5. 分配失败处理机制 malloc 分配失败返回NULL,代码需要 if 判空;new 分配失败抛出bad_alloc异常,需要 try-catch 捕获,默认直接崩溃。
  6. 自定义类型对象处理(最关键) malloc/free 只操作裸内存,不会调用构造、析构函数,对象内部动态资源会泄漏;new 分配后自动调用构造初始化,delete 释放前调用析构清理对象资源。

额外补充区别:

  • new 无扩容接口;realloc 可以动态调整 malloc 分配的内存大小
  • new/delete 支持全局 / 类内重载,自定义内存分配逻辑;malloc/free 不支持重载

七、2026 年高频面试题汇总(附带标准答案)

问题 1:malloc/calloc/realloc 的区别?

  1. malloc 分配指定字节内存,不初始化;
  2. calloc 分配 n 个指定大小元素,内存全部初始化为 0;
  3. realloc 修改已有堆内存大小,自动拷贝旧数据,空间不足会更换内存地址;
  4. 参数形式、适用场景、初始化行为三者均有差异。

问题 2:new/delete 底层是如何实现的?

  1. new:先调用 operator new(底层 malloc)开辟堆内存,再在内存上调用构造函数创建对象;数组 new 会多次调用构造。
  2. delete:先调用对象析构函数释放内部资源,再调用 operator delete(底层 free)释放堆内存;数组 delete 循环调用所有元素析构。

问题 3:malloc 开辟的内存可以用 delete 释放吗?new 的内存能用 free 释放吗?

内置类型下程序可能不崩溃,但属于未定义行为,禁止混用;自定义类型会造成严重内存泄漏:delete 会调用析构,free 不会;free 直接释放内存,不会执行析构清理对象内部堆资源。

问题 4:定位 new 的作用和使用场景?

定位 new 在已分配好的内存上手动调用构造函数,不额外分配堆内存;主要用于内存池优化,减少频繁动态分配的性能开销。

问题 5:栈、堆、静态区生命周期区别?

  1. 栈:函数调用期间存在,函数结束自动销毁;
  2. 堆:程序员手动分配释放,程序结束系统回收;
  3. 静态区:程序启动分配,整个程序运行期间存在,程序退出销毁。

八、开发避坑总结(2026 项目实战踩坑点)

  1. 数组 new 必须配套 delete \[\],自定义类型不匹配会漏调用析构,内存泄漏;
  2. realloc 扩容不要直接赋值原指针,扩容失败返回 NULL 丢失原内存;
  3. 禁止重复 free/delete、释放栈 / 常量区指针,会直接段错误;
  4. 静态局部变量存数据段,不要用来存储大量临时数据,程序全程占用内存;
  5. 字符串字面量存只读常量区,不要尝试修改,会崩溃;局部字符数组 char2 \[\]="abc" 存储在栈,可以修改;
  6. 高并发场景优先内存池 + 定位 new,减少 malloc 频繁分配带来的内存碎片;
  7. new 内存分配失败会抛异常,生产环境建议增加 try-catch 捕获 bad_alloc。
相关推荐
星轨初途1 小时前
LeetCode 热题 100——day11 滑动窗口最大值
数据结构·c++·算法·leetcode·职场和发展
不会代码的小猴11 小时前
21. 泛型编程上
开发语言·c++·笔记·算法
青瓦梦滋11 小时前
传输层UDP/TCP协议
linux·网络·c++·网络协议·tcp/ip·udp
一只旭宝12 小时前
细讲C加加【9】C++ std::function与std::bind详解|仿函数、绑定器、类成员绑定、占位符、成员偏移指针
开发语言·c++·算法
Lhan.zzZ14 小时前
在 Visual Studio 2022 中打造可扩展的动态链接库模块:从零搭建到原理解析
开发语言·c++·visual studio
zh路西法15 小时前
【3D SLAM源码解读系列】(二)Small_gicp——5 个积木搭出最优点云配准
c++·pcl·icp·fastgicp·smallgicp·gicp
fpcc17 小时前
ubuntu26环境下的开发环境安装处理
c++·并行编程
天天有money18 小时前
API中转站与多账号内容运营:如何统一管理不同项目的调用
gpt·ai·chatgpt·产品运营·内容运营
charlie11451419119 小时前
Cinux · 第一次跳进 Ring 3:用户态与特权隔离
开发语言·c++·操作系统·开源项目