C/C++内存分布

我们先来看看下面的这一段代码和相关问题

代码语言:javascript

AI代码解释

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

程序运行时的内存分区(栈、堆、数据段 / 静态区、代码段 / 常量区),本质是操作系统和编译器为了适配不同数据的特性(生命周期、访问频率、可修改性、分配方式),对内存进行的精细化管理—不同区域有不同的管理规则,最终实现内存使用效率更高、程序运行更稳定、资源管控更灵活的目标. 下面结合每个区域的核心特性,解释为什么必须划分这些区: 一、代码段(常量区):只读、共享、持久,适配 “执行 / 只读数据” 的需求. 代码段存储程序的机器指令、字符串常量(如 “abcd”)、const 修饰的只读常量,划分这个区的核心原因: ①防止误修改,保证程序安全:指令是程序运行的核心,字符串常量是不可变的,把它们放到只读内存区域(操作系统会标记该区域为只读),能避免代码/常量被程序意外改写(比如如果指针误写代码段,会直接触发内存保护错误,而不是悄悄改了指令导致程序崩溃). ②节省内存,支持共享:多个进程如果运行同一个程序(比如同时打开多个记事本),它们的指令部分可以共享同一块代码段内存,不用每个进程都复制一份指令,大幅节省系统内存. ③优化执行效率:代码段的内容固定不变,CPU 缓存可以针对性优化(缓存命中率更高),提升程序执行速度. 二、数据段(静态区):持久、可修改,适配 “全局 / 静态数据” 的需求 数据段存储全局变量、静态变量(static)细分初始化数据段(已赋值)和未初始化数据段(未赋值),划分这个区的核心原因: ①适配 “持久生命周期” 的需求:全局/静态变量的生命周期和程序一致(从程序启动到退出),不需要像局部变量那样频繁创建 / 销毁,单独划分区域可以让操作系统在程序启动时一次性分配内存,退出时统一释放,避免和 “动态内存(栈/堆)” 混淆. ②优化内存占用和初始化效率:(未初始化的全局/静态变量):操作系统会在程序启动时自动将其清 0,且 不需要存储在可执行文件中(只记录大小),能大幅减小可执行文件的体积; 已初始化的静态数据:编译器提前确定其大小和初始值,程序启动时直接加载到内存,无需运行时动态计算. ③保证全局可见性:全局变量需要在整个程序范围内访问,单独的静态区能让编译器/操作系统统一管理其内存地址,避免被栈/堆的动态内存覆盖或干扰. 三、栈区:自动、高效,适配 “局部 / 临时数据” 的需求 栈区存储局部变量、函数参数、函数调用上下文(返回地址、栈帧指针),划分这个区的核心原因: ①极致的分配/销毁效率:栈遵循先进后出(FILO)规则,内存分配/释放仅通过移动栈指针(CPU 的ESP/EBP 寄存器)完成,不需要复杂的内存分配算法(比如堆的空闲链表查找),速度比堆快几个数量级 —而局部变量(如localVar、num1数组)的生命周期仅在函数调用内,正好适配这种 “快速创建、自动销毁” 的特性. ②自动管理,减少人为错误:栈的内存由编译器/CPU 自动维护(函数进入时入栈,函数返回时出栈),程序员无需手动申请/释放,避免了堆内存常见的 “内存泄漏”“重复释放” 等问题. ③隔离性和安全性:栈的大小是固定的(通常几 MB),且每个线程有独立的栈,不同线程的局部变量互不干扰;栈溢出(比如递归太深)会被系统及时检测并终止,避免影响整个程序的内存空间. 四、堆区:动态、灵活,适配 “手动管理的动态数据” 的需求 堆区存储malloc/new分配的动态内存(如*ptr1),划分这个区的核心原因: ①突破栈的限制,适配动态需求:栈的大小有限(默认几 MB),无法存储大内存(比如大数组、大对象); 很多数据的生命周期无法提前确定(比如运行时根据用户输入创建的对象),栈的 “函数结束即销毁” 规则不适用 —堆允许程序员手动控制内存的分配(何时申请)和释放(何时free/delete),生命周期完全由代码决定. ②灵活的内存分配:堆的大小仅受物理内存/虚拟内存限制,支持任意大小、任意时机的内存请求,能满足程序运行时的动态内存需求(比如链表、树、动态数组的内存分配).

【说明】 1️⃣栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的. 2️⃣内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库.用户可使用系统接口创建共享共享内存,做进程间通信.(小编后续学习到Linux系统会重点介绍). 3️⃣堆用于程序运行时动态内存分配,堆是可以上增长的. 4️⃣数据段--存储全局数据和静态数据. 5️⃣代码段--可执行的代码/只读常量.


2. C语言中动态内存管理方式:malloc/calloc/realloc/free

代码语言:javascript

AI代码解释

void Test ()
{
// 1.malloc/calloc/realloc的区别是什么?
int* p2 = (int*)calloc(4, sizeof (int));
int* p3 = (int*)realloc(p2, sizeof(int)*10);
// 这里需要free(p2)吗?不需要.
//原因:realloc的行为是——若重新分配内存成功,原内存块(p2指向的内存)会被自动释放,此时p2会变成“悬空指针”(指向已释放的内存).
//若此时再执行free(p2),会导致重复释放内存,触发未定义行为(程序崩溃、内存异常等).
//当前代码中free(p3)已经释放了realloc分配的新内存,无需再处理p2.
free(p3 );
}

函数

函数原型(简化)

功能描述

malloc

void* malloc(size_t size)

分配size 字节的未初始化内存

calloc

void* calloc(size_t num, size_t size)

分配num 个 size 字节的内存

realloc

void* realloc(void* ptr, size_t size)

调整ptr指向的内存块大小为size 字节

malloc的实现原理是什么?可以看看这个视频 Glibc中malloc实现原理


3.C++内存管理方式

C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理.


3.1new/delete操作内置类型

代码语言:javascript

AI代码解释

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

注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[],注意:匹配起来使用.


3.2new和delete操作自定义类型

代码语言:javascript

AI代码解释

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)); // C
 int* p4 = new int;
free(p3);
delete p4;
 A* p5 = (A*)malloc(sizeof(A)*10);
 A* p6 = new A[10];
 free(p5);
 delete[] p6;
 return 0;
}

注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会.


4.operator new与operator delete函数(重点)

new和delete是用户进行动态内存申请和释放的操作符,operator new和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间.

代码语言:javascript

AI代码解释

/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间
失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常.
*/
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)
     if (_callnewh(size) == 0)
     {
         // report no memory
         // 如果申请内存失败了,这里会抛出bad_alloc 类型异常
         static const std::bad_alloc nomem;
         _RAISE(nomem);
     }
     return (p);
}
/*
operator delete: 该函数最终是通过free来释放空间的
*/
void operator delete(void *pUserData)
{
     _CrtMemBlockHeader * pHead;
     RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
     if (pUserData == NULL)
         return;
     _mlock(_HEAP_LOCK);  /* block other threads */
     __TRY
         /* get a pointer to memory block header */
         pHead = pHdr(pUserData);
          /* verify block type */
         _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
         _free_dbg( pUserData, pHead->nBlockUse );
     __FINALLY
         _munlock(_HEAP_LOCK);  /* release other threads */
     __END_TRY_FINALLY
     return;
}
/*
free的实现
*/
#define free(p)   _free_dbg(p, _NORMAL_BLOCK)

通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常.operator delete 最终是通过free来释放空间的.

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐