C/C++内存管理

内存管理是C/C++开发的基石,也是面试与工程实践中的高频考点。本文将从内存分布、动态内存管理方式、底层实现原理等维度,系统梳理malloc/free与new/delete的核心知识与差异。


一、C/C++ 内存分布

1. 内存区域划分

C/C++程序的虚拟地址空间通常分为以下几个核心区域:

|----------|--------------------|--------------------|
| 区域 | 存储内容 | 特点 |
| 内核空间 | 操作系统内核代码和数据 | 用户代码无法直接访问 |
| 栈(Stack) | 非静态局部变量、函数参数、返回值等 | 向下增长,由编译器自动分配释放 |
| 内存映射段 | 动态库、共享内存、文件映射等 | 高效的I/O映射方式,用于进程间通信 |
| 堆(Heap) | 动态内存分配(malloc/new) | 向上增长,由用户手动管理 |
| 数据段 | 全局变量、静态变量(static) | 程序运行期间一直存在 |
| 代码段 | 可执行代码、字符串常量 | 只读,防止程序意外修改指令 |

  1. 代码示例与变量位置分析
cpp 复制代码
int globalVar = 1;                 // 数据段(全局区)
static int staticGlobalVar = 1;    // 数据段(静态全局区)

void Test()
{
    static int staticVar = 1;      // 数据段(静态局部区)
    int localVar = 1;              // 栈

    int num1[10] = {1, 2, 3, 4};   // 栈(数组名num1在栈,元素也在栈)
    char char2[] = "abcd";          // 栈(数组char2在栈,字符串拷贝到栈上)
    const char* pchar3 = "abcd";    // pchar3在栈;"abcd"在代码段(常量区)

    int* ptr1 = (int*)malloc(sizeof(int) * 4);  // ptr1在栈;指向的空间在堆
    int* ptr2 = (int*)calloc(4, sizeof(int));   // ptr2在栈;指向的空间在堆
    int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4); // ptr3在栈;指向的空间在堆

    free(ptr1);
    free(ptr3);
}

详细解读

  1. 全局变量与静态变量

◦ globalVar:全局变量,存储在数据段,程序运行期间一直存在。

◦ staticGlobalVar:静态全局变量,存储在数据段,作用域限制在当前文件。

◦ staticVar:静态局部变量,存储在数据段,只在第一次进入Test函数时初始化,生命周期贯穿整个程序。

  1. 局部变量与数组

◦ localVar:非静态局部变量,存储在栈上,函数执行完毕后自动释放。

◦ num1:数组名是一个指针常量,存储在栈上,它指向的10个int元素也连续存储在栈上。

◦ char2[]:数组名char2在栈上,字符串"abcd"会被拷贝到栈上的数组空间中,因此*char2也在栈上。

◦ pchar3:指针变量本身在栈上,但它指向的字符串常量"abcd"存储在代码段(只读常量区)。

  1. 动态内存分配

◦ ptr1, ptr2, ptr3:这三个指针变量都存储在栈上。

◦ 它们通过malloc/calloc/realloc函数在堆上申请了内存空间,因此*ptr1, *ptr2, *ptr3指向的内存都在堆上。

◦ 注意:realloc调整了ptr2指向的内存块大小,成功后ptr2指向的内存已被ptr3接管,因此代码中只需要free(p3)。

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

1. 函数区别

|----------------------------------|--------------------|--------------|---------------------|
| 函数 | 功能 | 初始化 | 注意事项 |
| malloc(size_t size) | 申请指定字节数的内存 | 不初始化,内容为随机值 | 需手动计算大小,返回void* |
| calloc(size_t num, size_t size) | 申请num个大小为size的连续内存 | 自动将所有字节初始化为0 | 等价于malloc + memset |
| realloc(void* ptr, size_t size) | 调整已分配内存块的大小 | 新扩展的内存不初始化 | 若ptr为NULL,等价于malloc |
| free(void* ptr) | 释放已分配的内存 | - | 必须成对使用,避免内存泄漏 |

2. 代码示例

cpp 复制代码
void Test ()
{
    int* p2 = (int*)calloc(4, sizeof(int));
    int* p3 = (int*)realloc(p2, sizeof(int)*10);

    // 这里不需要free(p2),因为realloc成功后,p2指向的内存已被管理,p3是新的指针
    free(p3);
}

3. 面试题

  1. malloc/calloc/realloc的区别?

malloc:只分配,不初始化。

calloc:分配并初始化全0。

realloc:调整已有内存大小,可能会移动内存块。

  1. malloc的实现原理?(glibc中malloc实现原理)

底层通过brk或mmap系统调用向操作系统申请内存。

维护内存块链表(bins),优先在空闲链表中查找合适的块,避免频繁系统调用。

采用伙伴系统或类似算法管理内存碎片。

三、C++内存管理方式:new/delete

1. 操作内置类型

cpp 复制代码
void Test()
{
    // 动态申请一个int类型的空间
    int* ptr4 = new int;

    // 动态申请一个int类型的空间并初始化为10
    int* ptr5 = new int(10);

    // 动态申请10个int类型的空间
    int* ptr6 = new int[3];

    delete ptr4;
    delete ptr5;
    delete[] ptr6; // 注意:释放数组必须用delete[]
}

注意:

• 申请/释放单个元素:new / delete

• 申请/释放数组:new[] / delete[],必须配对使用。

2. 操作自定义类型

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

    ~A()
    {
        cout << "~A():" << this << endl;
    }
private:
    int _a;
};

int main()
{
    // new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间还会调用构造函数和析构函数
    A* p1 = (A*)malloc(sizeof(A)); // 只开空间,不调用构造
    A* p2 = new A(1);              // 开空间 + 调用构造函数

    free(p1);      // 只释放空间,不调用析构
    delete p2;     // 调用析构函数 + 释放空间

    // 内置类型是几乎是一样的
    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;
}

核心结论

  1. 自定义类型

◦ malloc/free:只负责堆空间的分配与释放,不会调用构造函数和析构函数。如果对象内部持有资源(如文件句柄、动态内存),使用 free 会导致资源泄漏。

◦ new/delete:除了分配/释放空间,还会自动调用构造函数初始化对象和调用析构函数清理资源,保证了对象生命周期的完整性。

  1. 内置类型(如 int)

◦ 由于没有构造和析构函数,malloc/free 和 new/delete 的行为几乎一致,都只是单纯的内存管理。

  1. 数组处理

◦ new T[N]:会连续调用 N 次构造函数。

◦ delete[]:会连续调用 N 次析构函数。

◦ 必须配对使用:new[] 必须与 delete[] 配对,new 必须与 delete 配对,否则会导致未定义行为(如内存泄漏或程序崩溃)。

注意:

• new:先调用operator new分配空间,再调用构造函数。

• delete:先调用析构函数,再调用operator delete释放空间。

四、operator new与operator delete函数

1. 基本概念

• new和delete是用户操作符,其底层是通过调用全局函数operator new和operator delete来实现的。

• operator new和operator delete是可以被重载的全局函数。

2. operator new的实现(简化版)

cpp 复制代码
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
    void* p;
    while ((p = malloc(size)) == 0)
    {
        if (_callnewh(size) == 0)
        {
            // 如果申请内存失败,抛出bad_alloc异常
            static const std::bad_alloc nomem;
            _RAISE(nomem);
        }
    }
    return (p);
}

原理:

• 本质上是通过malloc来申请空间。

• 如果malloc成功,直接返回。

• 如果失败,尝试执行用户提供的_callnewh空间不足应对措施。

• 如果用户没有提供措施或措施失败,则抛出bad_alloc异常。

最核心结论(面试必问)

  1. new = operator new + 构造函数

operator new:只开空间 ; 之后编译器自动调用构造函数

  1. operator new 底层就是 malloc

  2. 内存不足时: 先重试 ; 再看有没有 new_handler ; 都不行 → 抛 bad_alloc

  3. malloc 失败返回 NULL ;operator new 失败抛异常

总结:operator new 就是"带异常、带重试的 malloc 封装"。

3. operator delete的实现(简化版)

cpp 复制代码
void operator delete(void *pUserData)
{
    if (pUserData == NULL)
        return;

    // ... 内部处理 ...
    _free_dbg(pUserData, _NORMAL_BLOCK); // 最终调用free释放空间
}

原理:

• 本质上是通过free来释放空间。

最核心结论(面试必背)

  1. operator delete 底层 = 最终调用 free

  2. Debug 模式多了内存检测,但本质不变

  3. delete 真正做两件事:调用析构函数 ;调用 operator delete → 释放内存

  4. operator delete 只负责释放空间,不负责析构!

总结:operator delete 就是带安全检查、最终包了一层 free 的释放函数。

4. new[] 搭配 delete[] 的原因

new[] 会多开4个字节,存"对象个数",delete[] 要靠这4个字节才知道要调用多少次析构函数。

如果混用,会内存泄漏 / 崩溃 / 未定义行为。

  1. 当你写:A* p = new A[5];

编译器实际做了这 3 件事:

  1. 计算总大小:5个对象 + 4个字节(存个数 5)

  2. 多开 4 字节:真实开辟大小 = 5 * sizeof(A) + 4;这 4 个字节用来存 5,告诉系统有多少个对象。

  3. 调用 5 次构造函数

  4. 当你用正确的 delete[] :delete[] p;

编译器做 2 件事:

  1. 往前读4个字节,拿到对象个数 5

  2. 对 5 个对象依次调用析构函数

  3. 把整块内存(包括那4个字节)一起释放

✅ 完全正确

  1. 如果你错误用 delete delete p; // 错!

编译器只会做:

  1. 只调用 1 次析构函数:剩下4个对象没析构 → 内存泄漏

  2. 只释放一个对象大小:系统不知道有5个 → 堆损坏、崩溃

❌ 未定义行为(崩溃/泄漏/报错)

最关键结论(面试必背)

  1. new[] = 开空间 + 存对象个数 + 多次构造

  2. delete[] = 读个数 + 多次析构 + 释放整块空间

  3. delete 不知道有多少个对象,只会析构1个

  4. 内置类型(int/char...)一般不会崩,因为没有析构函数,但依然是未定义行为!

最简单记忆口诀

谁开的空间,谁来释放: new → delete ; new[] → delete[]

五、new和delete的实现原理

1. 内置类型

• new:调用operator new分配空间,若失败则抛异常。

• delete:调用operator delete释放空间。

• new[]:调用operator new[]分配空间。

• delete[]:调用operator delete[]释放空间。

2. 自定义类型

• new的原理:

  1. 调用operator new函数申请空间。

  2. 在申请的空间上执行构造函数,完成对象的构造。

• delete的原理:

  1. 在空间上执行析构函数,完成对象中资源的清理工作。

  2. 调用operator delete函数释放对象的空间。

• new T[N]的原理:

  1. 调用operator new[]函数,在operator new[]中实际调用operator new完成N个对象空间的申请。

  2. 在申请的空间上执行N次构造函数。

• delete[]的原理:

  1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理。

  2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间。

六、定位new表达式(placement-new)

1. 核心概念

定位new表达式是在已分配的原始内存空间中,显式调用构造函数来初始化一个对象。

2. 使用格式

cpp 复制代码
new (place_address) type;
// 或
new (place_address) type(initializer-list);

• place_address:必须是一个指针,指向已分配的内存。

• initializer-list:类型的初始化列表。

3. 使用场景

• 主要配合内存池使用。

• 因为内存池分配出的内存没有初始化,如果是自定义类型的对象,需要用定位new来显式调用构造函数进行初始化。

4. 代码示例

cpp 复制代码
class A
{
public:
    // 构造函数:打印地址,初始化 _a ;析构函数:打印地址 ;没有写拷贝构造、赋值重载,默认即可
    A(int a = 0)
        : _a(a)
    {
        cout << "A():" << this << endl;
    }

    ~A()
    {
        cout << "~A():" << this << endl;
    }
private:
    int _a;
};

int main()
{
    // p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
    // malloc:只开空间,不调用构造;此时 p1 指向一块内存,但还不是合法对象
    A* p1 = (A*)malloc(sizeof(A));
    new(p1)A;      // 定位new,在p1指向的空间上构造一个A对象,在已经有的内存上,调用构造函数
    p1->~A();      // 显式调用析构函数,只做清理工作,不释放内存
    free(p1);      // 释放空间,真正释放 malloc 出来的空间

    A* p2 = (A*)operator new(sizeof(A));
    // operator new 底层就是 malloc;作用:只开空间,不构造
    new(p2)A(10);  // 定位new,带参构造
    p2->~A(); // 显示析构
    operator delete(p2); // 底层就是 free,释放空间

    return 0;
}

运行输出(大致样子)

cpp 复制代码
A():0xXXXXXX
~A():0xXXXXXX
A():0xYYYYYY
~A():0xYYYYYY

每个对象:构造一次,析构一次,完全匹配。

核心总结

  1. malloc / operator new :只分配内存,不调用构造

  2. free / operator delete :只释放内存,不调用析构

  3. 定位 new:new(地址) 类名(参数) :在已有地址上调用构造函数

  4. 显式析构:p->~类名() :只调用析构,不释放内存

  5. 析构函数是对象的方法,只负责对象内部。内存是 malloc / operator new 分配的,必须由 free / operator delete 归还。

C++ 设计就是:构造 / 析构 管对象生命周期 ;new / delete、malloc / free 管内存分配释放。

构造:造对象 析构:毁对象(不清内存)

malloc / operator new:拿内存 free / operator delete:还内存

平常写的 new / delete 到底干了啥?

A* p = new A;

等价于:1. operator new 开空间 2. new(p) A 定位new构造

delete p;

等价于:1. p->~A() 显式析构 2. operator delete(p) 释放空间

七、malloc/free和new/delete的区别

1. 本质

malloc/free:库函数

new/delete:C++ 运算符

2. 初始化

malloc:只开空间,不初始化

new:可以直接初始化(如 new int(10))

3. 空间大小

malloc:必须手动计算字节数 ;例:malloc(sizeof(int)*4)

new:只需写类型,自动计算大小 ;例:new int[4]

4. 返回值

malloc:返回 void*,必须强转

new:返回对应类型指针,不用强转

5. 失败处理

malloc 失败:返回 NULL

new 失败:抛出 bad_alloc 异常

6. 自定义类型

malloc/free:只开空间/释放空间,不调用构造、析构

new/delete:

new:开空间 + 调用构造函数

delete:调用析构函数 + 释放空间

malloc/free 只负责内存;new/delete 不仅负责内存,还会自动调用构造与析构函数。

|--------|-------------|---------------------|
| 特性 | malloc/free | new/delete |
| 本质 | 库函数 | C++运算符 |
| 初始化 | 不初始化 | 可初始化(如 new int(10)) |
| 空间大小 | 手动计算字节数 | 自动计算,数组用 [] |
| 返回值 | void*,必须强转 | 对应类型指针,无需强转 |
| 内存申请失败 | 返回 NULL | 抛出 bad_alloc 异常 |
| 自定义类型 | 只开辟/释放空间 | 自动调用构造函数和析构函数 |


掌握C/C++内存管理,不仅是写出高效代码的基础,更是理解程序运行机制的关键。从内存分布到new/delete的底层实现,每一个细节都值得我们深入探究,在实践中不断积累,稳步进阶。

相关推荐
回敲代码的猴子2 小时前
2月8日上机
开发语言·c++·算法
凌晨7点2 小时前
DSP学习F28004x数据手册:第13章-ADC
单片机·嵌入式硬件·学习
Mr YiRan2 小时前
函数指针与指针运算
c语言
No丶slovenly2 小时前
flutter笔记-输入框
前端·笔记·flutter
rongyili882 小时前
Dify 外部知识库集成 Milvus 实战指南
开发语言·python·milvus
liuchangng2 小时前
Agent Skills 核心笔记_20260212095535
笔记
IT猿手2 小时前
MOEA/D(基于分解的多目标进化算法)求解46个多目标函数及一个工程应用,包含四种评价指标,MATLAB代码
开发语言·算法·matlab·多目标算法
Benny_Tang2 小时前
AtCoder Beginner Contest 445(ABC445) A-F 题解
c++·算法
野犬寒鸦2 小时前
从零起步学习并发编程 || 第九章:Future 类详解及CompletableFuture 类在项目实战中的应用
java·开发语言·jvm·数据库·后端·学习