C++ 内存管理:原理、机制与面试重点

1. C/C++内存分布

在C/C++的内存结构中, 由低地址到高地址依次为:

  • 代码段:存放程序的机器指令与只读常量

  • 数据段:存放全局变量与静态变量

  • 堆区:手动动态分配的内存(如 new, malloc)

  • 内存映射段:映射文件或设备,支持共享内存等机制

  • 栈区:函数调用时自动分配的局部变量和返回地址

  • 内核空间:操作系统使用的区域,用户程序无法直接访问

2. C语言动态内存管理方式

在了解了内存分布后,我们来看看 C 语言是如何管理内存的。 

通过: stdlib.h 中的 malloc, calloc, realloc, free 等函数手动管理。

malloc()分配指定字节数的内存,内容不初始化
calloc()分配并初始化为0,参数为元素个数和大小
realloc()重新调整已分配内存的大小
free()释放分配的内存

 代码示例:

int* p = (int*)malloc(sizeof(int) * 5); // 分配五个int内存
// 如果 malloc分配失败, 则返回空指针
if(p == NULL)
{
    perror("malloc:"); // 输出错误
    return;
}
free(p); // 释放内存

在使用动态内存分配时, 我们应该注意内存泄漏, 重复释放, 越界访问等问题

3. C++动态内存管理方式

相较于 C 语言直接使用 malloc/free 管理内存,C++ 提供了更高级、更安全的内存管理方式,通过 new/delete 来进行内存管理

3.1 内置类型的动态内存管理

C++ 中对内置类型 (int, double, char...) 的动态分配本质上与 C 语言类似,但引入了 new 语法,具有更好的类型安全性。

代码示例:

// 动态申请一个 int类型的内存空间
int* ptr1 = new int;

// 动态申请一个 char类型的空间并初始化为 'a'
char* ptr2 = new char('a');

//动态申请一个 double类型数组, 未初始化
double* ptr3 = new double[5];

// 动态申请一个 long类型数组, 并初始化
long ptr4 = new long[5]{1, 2, 3, 4, 5};

delete ptr1;   // 释放空间
delete ptr2;
delete[] ptr3; // 释放数组时要使用 delete[] 
delete[] ptr4;  

与 C 语言的动态内存管理不同,C++ 的 new运算符在申请内存时,能够直接返回对应类型的指针,无需进行类型强制转换。同时,它还支持灵活的初始化方式,而 C 语言中的 malloc 和calloc 无法实现自定义初始化,前者不初始化,后者仅能初始化为零。

注意: 当我们使用 new[]申请连续的内存时一定要使用 delete[]来释放, 需要匹配使用, 否则会出现未定义的行为

3.2 自定义类型的动态内存管理

自定义类型 (结构体, 类) 在分配时除了需要分配空间, 还需要调用对应的构造函数, 在释放时也要调用析构函数, 这也是 C 和 C++ 内存管理的本质区别之一

代码示例:

class MyClass
{
public:
    MyClass(int a)
        :_a(a)
    {
        cout << "MyClass 的构造函数调用了" << endl;
    }

    ~MyClass()
    {
        cout << "MyClass 的析构函数调用了" << endl;
    }

private:
    int _a;
};

int main()
{
    MyClass* obj = new MyClass(10); // 调用构造函数对类的成员进行初始化
    delete obj;                     // 释放时调用类的析构函数
    return 0;
}

运行结果:

MyClass 的构造函数调用了
MyClass 的析构函数调用了

相比 malloc/free , new/delete 在申请内置类型时支持初始化,在处理自定义类型时能自动调用构造和析构函数。不仅更安全,也更符合 C++ 的设计哲学,可看作是对 malloc/free 的语法上的升级。

4. operator new 和 operator delete

operator new 和 operator delete 是系统提供的全局函数, new/delete 关键字在底层会分别调用这两个函数来分配释放空间

void* operator new(std::size_t size);       // 分配内存
void operator delete(void* ptr) noexcept;   // 释放内存

当我们使用 new 申请自定义类型空间时: 

MyClass* ptr1 = new MyClass(10);

实际等价于:

void* mem = operator new(sizeof(MyClass)); // 分配内存
MyClass* ptr1 = new(mem) MyClass(10);      // 在内存上构建对象

而 delete:

delete ptr1;

实际上等价于:

ptr1->~MyClass();      // 调用析构函数
operator delete(ptr1); // 清理动态申请的内存

4.1 operator new 的底层原理

C++ 中的 operator new 本质上是对 malloc  的封装,它在底层实际调用了 malloc,当分配失败时尝试调用 _callnewh(内存不足时的处理),最终可能抛出 bad_alloc 异常。

以下是核心流程的简化示例:

void* operator new(std::size_t size) {
    void* p;
    while ((p = malloc(size)) == nullptr) {
        if (_callnewh(size) == 0) {
            throw std::bad_alloc(); // 分配失败时抛出异常
        }
    }
    return p;
}

可以看到, operator new 是更安全的, 它在内存不足时会抛出异常来提示使用者

4.2 operator delete 的底层原理

operator delete 实际上最终调用了 free

其实现类似于:

void operator delete(void* ptr)
{
    if(ptr == nullptr)
        return;
    free(ptr); // 调用free释放内存
}

 4.3 面试经典题目

全局 operator new 和类内重载的区别?

全局 operator new 是默认的内存分配函数,适用于所有对象;类内重载的 operator new 则只影响该类的对象分配方式。

我们可以为某个类单独提供内存分配和释放逻辑:

class MyClass
{
    void* operator new(size_t size)
    {
        cout << "MyClass 的operator new 调用了" << endl;
        return malloc(size);
    }
    void operator delete(void* ptr)
    {
        if(ptr == nullptr)
            return;
        cout << "MyClass 的operator delete 调用了" <<endl;
        free(ptr);
    }

private:
    int _a;
}

如果类中提供了自己的 operator new,那么用 new 创建该类对象时将优先调用类内版本,不会再用全局版本, 类内也可以重载多个版本。        

operator new[] , operator delete[] 也可以进行重载

5. operator new[] 和 operator delete[]

5.1 new T[] , delete[] 的原理

new T[]

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

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

delete[]

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

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

5.2 为什么new T[]申请的内存一定要使用delete[]释放?

C++ 中的 new[] 做了 额外的隐藏操作,不仅仅是调用 operator new[] 分配数组空间,还偷偷加了头部信息

当我们这样使用时:

MyClass* ptr = new MyClass[3];

内存分布可能是这样的:

在对象数组前,编译器通常会额外插入一段头部信息,以便在 delete[] 时能正确地调用每个元素的析构函数。
- 指针 ptr 实际指向的是第一个对象,而不是整块内存的起始位置。
- 只有 delete[] 能正确识别并回退这段头部信息,从而完整析构所有对象并释放内存。

如果使用 delete 来释放 new[] 分配的内存,将无法找到头部信息,导致释放地址不正确           (跳过了头部),可能引发内存泄漏甚至会导致程序崩溃

因此,new[] 必须与 delete[] 成对使用,绝不可与 delete 混用。

6. 定位 new

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

使用格式:

new (place_address) type 或者 new (place_address) type(initializer-list)

place_address必须是一个指针,initializer-list是类型的初始化列表

代码示例:

// ptr 现在指向的只不过是与 MyClass对象相同大小的一段空间,还不能算是一个对象,因为构造函数没
// 有执行
MyClass* ptr = (MyClass*)malloc(sizeof(MyClass));
new(ptr) MyClass(10); // 在空间上构造对象

ptr->~MyClass(); 
free(ptr)

定位 new 的使用场景极少, 了解即可

7. 总结 new/delete 和 malloc/free 的区别

malloc/free 和 new/free 都是从堆上申请空间, 并且都需要手动释放, 不同的地方是:

1. malloc/free是函数,new/delete是操作符

2. malloc申请的空间不会初始化,new可以初始化

3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型,如果是多个对象,[]中指定对象个数即可

4. malloc的返回值为void*, 在使用时必须强转,new不需要, 直接返回对应的类型

5. malloc申请空间失败时,返回的是NULL,new申请失败会抛异常

6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new 在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成资源的释放 

  

8. 结语

理解内存管理的本质,并不仅仅是为了掌握 new 和 delete 的语法,而是为了写出更安全、更具工程能力的 C++ 代码。随着语言的发展,RAII、智能指针、标准容器已经逐渐替代了手动内存管理。但理解背后的机制,仍然是每一个 C++ 开发者的必修课。 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值