【C++基础】内存管理——malloc/free和new/delete之间的盘根错节

引言

在 C++ 世界里,内存是一切的舞台。无论是创建一个对象、调用一个函数,还是操作一块数组,背后都离不开内存的分配与释放。相比于 C 的 malloc / free,C++ 又引入了更高级的 new / delete,它们不仅分配原始的字节空间,还会调用构造与析构函数,为对象的生命周期保驾护航。理解这些机制,能让我们更清晰地把握程序运行的底层逻辑,避免常见的内存泄漏与野指针问题。

内存布局

文章开始之前我们先通过一段程序了解一下不同的变量分别存储在内存中的哪块区域

代码示例

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);
}

通过这段C语言的程序,试着选择各变量存放在哪个内存区域:

选项:A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)

  • globalVar在哪里? ____
  • staticGlobalVar在哪里?____
  • staticVar在哪里?____
  • localVar在哪里?____
  • num1 在哪里?____
  • char2在哪里?____
  • *char2在哪里?___
  • pChar3在哪里?____
  • *pChar3在哪里?____
  • ptr1在哪里?____
  • *ptr1在哪里?____

解答

变量 存放区域 说明
globalVar 数据段(静态区) 已初始化的全局变量,存放在数据段
staticGlobalVar 数据段(静态区) 已初始化的静态全局变量,和全局变量一样放在数据段
staticVar 数据段(静态区) 函数内的静态局部变量,生命周期与程序同在,存放在数据段
localVar 普通局部变量,函数调用时压栈
num1 局部数组,内存在栈上分配
char2 局部数组,存在栈上,存放拷贝出来的 "abcd"
*char2 数组元素 'a' 'b' 'c' 'd' '\0' 都在栈上
pChar3 局部指针变量,本身在栈上
*pChar3 常量区 "abcd" 字符串字面量存放在常量区,指针指向这里
ptr1 局部指针变量,存放在栈上
*ptr1 malloc 分配的空间在堆上

说明

  • (Stack):存放局部变量、函数调用的栈帧;当前函数运行完毕,自动释放
  • (Heap):使用malloc/realloc/calloc/new动态分配的区域;需手动释放
  • 数据段/静态区 (Static):存放全局变量和static修饰的局部变量;程序运行结束,自动释放
  • 代码段/常量区 :使用const修饰的变量、字符串常量

易错点

1. const常量和字符常量
  • const char* pChar3 = "abcd";为什么pChar3在栈区,*pChar3在常量区?

    • const修饰的是pChar3指向的值,char* const pChar3修饰的才是pChar3本身
    • pChar3其实指向的是字符常量 "abcd"
  • char char2[] = "abcd";"abcd"是字符常量,char2指向它,为什么*char2却是在栈区

    • 编译器将常量区的"abcd"拷贝了一份到栈区,char2就指向了这块栈区地址
2. 堆和栈
  • 堆和栈都是动态增长的,栈是向下增长,堆是向上增长,中间是一块"空旷的虚拟空间",两者可能相遇(冲突) 内存布局,如下图:

拓展

mallocrealloccalloc的区别

  • malloc :按字节在堆上申请空间;
    • 初始化都是随机值
  • calloc :分配一块大小为 num * size 字节的连续内存;
    • size:元素字节大小,num:元素个数;
    • 初始化为0
  • realloc :调整一块已经分配的内存空间大小(malloc/realloc/calloc分配过的)
    • 新空间>旧空间,使用malloc申请新空间,旧空间的数据拷贝到新空间,再将旧空间释放
    • 新空间<旧空间,释放旧空间多余的部分
    • 扩展的新空间,初始化的是随机值

都是 成功,返回void*新地址;失败,返回NULL

二、new和delete

在C++中使用newdelete关键字进行内存管理,和C语言中的mallocfree功能相同,都是在堆上申请/释放内存,注意弥补了malloc和free的一些缺陷,使用更灵活,更智能;

  • new申请成功后返回的是申请类型的指针,所以要使用相同类型的指针接收;

1.基本操作

语法:

申请: 单块内存:类型* 变量名 =new 类型(初始化值); 连续内存:类型* 变量名 =new 类型[元素个数] 释放: 单块内存:delete 变量名; 连续内存:delete[ ] 变量名;

  • new T → 必须用 deletenew T[n] → 必须用 delete[]

内置类型

对于内置类型,newmalloc基本没有什么差异;使用new申请内存可以进行初始化,而malloc不能初始化 代码示例:

cpp 复制代码
void Test1()
{
	//申请一个int(4字节)类型的空间
	int* p1 = new int;

	//申请一个int类型的空间,并初始化
	int* p2 = new int(1);

	//申请一段int类型的连续空间(数组)
	int* p3 = new int[10];

	//释放内存
	delete p1;
	delete p2;
	delete[] p3;
}

自定义类型

newmalloc主要的区别就是在申请自定义类型内存时,new会调用该只定义类型的构造函数,而malloc只会按字节大小直接申请内存;

代码示例:

cpp 复制代码
class A
{
public:
    A()
    {
        cout << "A()" << endl;
    }
    ~A()
    {
        cout << "~A()" << endl;
    }
};

int main()
{
	Test1();
	//申请一块A类型的空间
	//调用一次构造函数
	A* p1 = new A;

	//申请三块连续的A类型空间
	//调用三次构造函数
	A* p2 = new A[3];
	
	delete p1;
	delete[] p2;
	return 0;
}

2. new/delete底层原理

  • new的底层是调用系统的全局函数void* operator new(size_t _Size)
  • new[]的底层是调用系统的全局函数void* operator new[](size_t _Size)
  • delete的底层是调用系统的全局函数void* operator delete(void* _Block)
  • delete[]的底层是调用系统的全局函数void operator delete[](void* _Block)

operator new() / operator new[] ()

  • operator new函数底层调用就是malloc()申请内存;

  • operator new在申请自定义类型 内存空间时,会在申请成功的内存空间上调用当前自定义类型的构造函数

  • 使用new [N]申请N个连续的自定义类型时,会调用Noperator new;并且,如果显示实现了析构函数,编译器还会自动在申请的内存前面额外申请4个字节,用于存放数字N ,如果没有显示实现,则不会记录;这样为了在delete时,记录需要调用析构函数多少次;

  • malloc申请成功,operator new就返回申请的内存地址,失败则抛出异常错误bad_alloc

operator new源码(简化版)
cpp 复制代码
void* __CRTDECL operator new(size_t const size)
{
    for (;;)
    {
        if (void* const block = malloc(size))
        {
            return block;
        }

        if (_callnewh(size) == 0)
        {
            if (size == SIZE_MAX)
            {
                __scrt_throw_std_bad_array_new_length();
            }
            else
            {
                __scrt_throw_std_bad_alloc();
            }
        }

        // The new handler was successful; try to allocate again...
    }
}

可以看到源码中直接调用了malloc函数(p=malloc(size)

operator delete() / operator delete[] ()

  • operator delete函数底层调用就是free()去释放内存;(对于内置类型的空间,free也可以替代delete释放内存,但是不建议)
  • 使用delete[]释放对象时,会调用operator delete去释放内存,调用次数根据额外空间记录的个数

总结

特性 内置类型 (int) 自定义类型 (class A)
构造函数调用 有(逐个构造)
析构函数调用 有(逆序逐个析构)
new初始化方式 默认随机值/零初始化 按构造函数逻辑执行
delete[]行为 仅释放内存 析构+释放内存

写到这里,关于 new/delete 和 malloc/free 的小故事就先告一段落啦。本文只是抛砖引玉,难免有遗漏和偏差,还请大家多多指正。希望这些内容能帮你在和内存"斗智斗勇"的路上少踩几个坑,多几分笃定,如果对你有帮助,麻烦你 👍点赞 ⭐收藏 ❤️关注 吧~

相关推荐
程序猿二饭2 小时前
Spring Boot 项目启动报错:MongoSocketOpenException 连接被拒绝排查日记
后端
齐穗穗2 小时前
springboot集成websocket
spring boot·后端·websocket
玉衡子2 小时前
四、索引优化实战
java·后端
程序员爱钓鱼2 小时前
Go语言实战案例 — 工具开发篇:编写一个进程监控工具
后端·google·go
canonical_entropy3 小时前
不同的工作需要不同人格的AI大模型?
人工智能·后端·ai编程
IT_陈寒3 小时前
Vite 5.0 终极优化指南:7个配置技巧让你的构建速度提升200%
前端·人工智能·后端
小熊学Java3 小时前
基于 Spring Boot+Vue 的高校竞赛管理平台
vue.js·spring boot·后端
钢门狂鸭9 小时前
关于rust的crates.io
开发语言·后端·rust
脑子慢且灵10 小时前
[JavaWeb]模拟一个简易的Tomcat服务(Servlet注解)
java·后端·servlet·tomcat·intellij-idea·web