【C++】内存管理(上)

🫧个人主页:小年糕是糕手

💫个人专栏:《C++》《C++同步练习》《数据结构》《C语言》

🎨你不能左右天气,但你可以改变心情;你不能改变过去,但你可以决定未来!



目录

一、初识内存管理

二、内存管理的核心维度

2.1、内存划分

[一、代码段(Text Segment / 常量区)](#一、代码段(Text Segment / 常量区))

作用:

特点:

[二、数据段(Data Segment / 静态区)](#二、数据段(Data Segment / 静态区))

作用:

特点:

三、堆(Heap)

作用:

特点:

四、栈(Stack)

作用:

特点:

[2.2、实际内存布局示例(Linux x86-64)](#2.2、实际内存布局示例(Linux x86-64))

三、C/C++内存分布

[四、C语言中动态内存管理方式:malloc / calloc / realloc / free](#四、C语言中动态内存管理方式:malloc / calloc / realloc / free)

[4.1、malloc / calloc / realloc的区别](#4.1、malloc / calloc / realloc的区别)

4.2、malloc的实现原理(了解)


一、初识内存管理

内存管理是操作系统(OS)和编程语言运行时核心功能之一,本质是高效、安全地分配、使用、回收计算机内存资源,避免内存泄漏、野指针、内存碎片等问题,确保程序稳定运行且资源利用率最大化。

我们为什么需要内存管理?

一、解决物理限制问题

  • 内存稀缺:物理内存有限,但程序需求很大

  • 多任务需求:需要同时运行多个程序

二、提供关键功能

  • 隔离保护:防止程序互相干扰或攻击

  • 地址抽象:让程序以为自己独占整个内存空间

  • 内存共享:安全地共享代码库和公共数据

三、提升系统能力

  • 扩展内存:虚拟内存让程序能用比物理内存更大的空间

  • 效率优化:通过缓存、预取等技术加速访问

  • 动态管理:按需分配,避免浪费

一句话总结:内存管理让有限的内存资源能被多个程序安全、高效地共享使用,是现代计算系统能够工作的基础。

二、内存管理的核心维度

2.1、内存划分

我们以进程的角度来进行内存划分:

一、代码段(Text Segment / 常量区)
作用:
  • 存储程序的执行代码:机器指令、函数体等

  • 包含只读的常量:字符串常量、const修饰的全局变量

特点:
  1. 只读性:防止程序意外修改自身代码

  2. 可共享:多个进程可以共享同一份代码副本(如多个实例运行同一程序)

  3. 大小固定:编译链接后确定,运行时不变

  4. 位于内存低地址区域

二、数据段(Data Segment / 静态区)
作用:
  • 存储全局变量和静态变量

    • 初始化的全局变量(全局区)

    • 未初始化的全局变量(BSS段)

    • 静态局部变量和静态全局变量

特点:
  1. 生命周期最长:从程序启动到结束一直存在

  2. 自动初始化

    • 显式初始化的变量:初始值在编译时确定

    • 未显式初始化的变量:系统自动初始化为0(或NULL)

  3. 位于代码段之上

  4. 大小在编译时确定

三、堆(Heap)
作用:
  • 动态内存分配区域

  • 存储mallocnewcallocrealloc等分配的内存

特点:
  1. 动态性:大小在运行时动态变化

  2. 手动管理

    • 程序员负责申请和释放内存

    • 可能产生内存泄漏(未释放)或悬空指针

  3. 向上增长:向高地址方向扩展

  4. 碎片化风险:频繁分配释放可能导致内存碎片

  5. 分配速度相对较慢:需要寻找合适大小的空闲块

四、栈(Stack)
作用:
  • 存储函数调用上下文和局部变量

    • 函数参数

    • 返回地址

    • 局部变量

    • 函数调用的寄存器保存区

特点:
  1. 自动管理:编译器自动分配和释放

  2. 后进先出(LIFO):函数调用和返回符合栈的特性

  3. 向下增长:向低地址方向扩展

  4. 速度快:只需移动栈指针即可分配内存

  5. 大小有限:通常较小(Linux默认8MB),可能发生栈溢出

  6. 生命周期短:随函数调用开始,随函数返回结束

对比表格

特性 数据段 代码段
存储内容 局部变量、函数参数、返回地址 动态分配的内存 全局/静态变量 程序代码、常量
管理方式 自动(编译器) 手动(程序员) 自动(系统) 自动(系统)
生命周期 函数调用期间 直到显式释放 整个程序运行期间 整个程序运行期间
大小 固定且较小 动态变化,理论上受限于系统 编译时确定 编译时确定
生长方向 向下(低地址) 向上(高地址) - -
访问速度 最快 较慢
线程共享 每个线程独立 进程内共享 进程内共享 进程内共享
2.2、实际内存布局示例(Linux x86-64)

text

复制代码
高地址
┌─────────────────┐
│   内核空间      │
├─────────────────┤
│   栈 (向下增长)  │ ← 栈指针(SP)
│    ...          │
├─────────────────┤
│    ...          │
├─────────────────┤
│   堆 (向上增长)  │ ← 堆指针(brk)
│    ...          │
├─────────────────┤
│   未初始化数据段(BSS)│
├─────────────────┤
│   已初始化数据段    │
├─────────────────┤
│   代码段         │
└─────────────────┘
低地址

编程注意事项

  1. 栈溢出:避免大局部变量或深度递归

  2. 堆管理 :确保配对使用malloc/freenew/delete

  3. 数据段:过度使用全局变量会增加内存占用

  4. 代码段:常量字符串在此区域,不可修改

三、C/C++内存分布

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

cpp 复制代码
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.代码段(常量区)

globalVar在哪里?____

staticGlobalVar在哪里?____

staticVar在哪里?____

localVar在哪里?____

num1 在哪里?____

答案:C、C、C、A、A

解析:

我们根据表格可以看出,也可以看下面这个表格:

分区 作用 特点
栈(Stack) 存储局部变量、函数调用上下文 自动分配 / 释放(栈帧)、连续内存、大小固定(几 MB)
堆(Heap) 动态分配内存(如 new/malloc) 手动 / 自动回收、非连续、大小灵活(可达 GB 级)
全局 / 静态区 存储全局变量、静态变量 程序启动分配、退出释放
常量区 存储字符串常量、const 变量 只读、程序退出释放
代码区 存储程序指令(二进制) 只读、可共享

char2在哪里?____

*char2在哪里?___

pChar3在哪里?____

*pChar3在哪里?____

ptr1在哪里?____

*ptr1在哪里?____

答案:A、A、A、D、A、B

解析:

我们首先来看char2,他是一个数组,我们知道数组名代表整个数组那他就是在栈上;pchar3大家可能都觉得他有个const修饰那他应该在常量区,这个const修饰的是指向内容,并不修饰本身,所以他也在栈上;数组名代表整个数组,这里char2解引用就代表首元素的地址解引用(实际就是a),这个数组在栈上,所以解引用也是在栈上;pChar3也在栈上,解引用找他的指向内容,他指向这个常量字符串,这个常量字符串在常量区,所以*pChar也在常量区;我们来总结一下其实就是在常量区有个"abcd\0",我们在栈上定义一个数组char2,我们将常量区的"abcd\0"拷贝一份给他,在栈上再定义一个局部的指针变量,这个局部的指针变量指向这个字符串首元素的地址,当我们解引用就是常量区的"abcd\0"的a,给出图解如下:

常量区的数据不可以修改!

我们在栈上定义了一指针变量ptr1,然后malloc一块空间,指针指向那快空间,然后那块空间在堆区上,所以解引用*ptr1就在堆区上;

  • ptr1Test()函数内的局部指针变量 ,局部变量会存储在栈区(函数调用时分配栈帧,函数结束后自动释放)。

  • ptr1是通过malloc动态分配内存得到的指针,malloc会从堆区 申请内存空间;*ptr1ptr1指向的内存空间,因此存储在堆区 (需手动调用free释放)。

【说明】

  1. 栈又叫堆栈 -- 非静态局部变量 / 函数参数 / 返回值等等,栈是向下增长的。
  2. 内存映射段是高效的 I/O 映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。(Linux 课程如果没学到这块,现在只需要了解一下)
  3. 堆用于程序运行时动态内存分配,堆是可以上增长的。
  4. 数据段 -- 存储全局数据和静态数据。
  5. 代码段 -- 可执行的代码 / 只读常量。

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

4.1、malloc / calloc / realloc的区别

大家要是想详细了解可以移步我以前写过的一篇博客:【C语言】C 动态内存管理全解析:malloc/calloc/realloc 与柔性数组实战-CSDN博客https://blog.csdn.net/2501_91731683/article/details/150438447这里我们简单为大家讲解一下大致的区别:

malloc是动态内存开辟但是不会初始化;calloc是动态内存开辟并且初始化为0;realloc是在原有的动态内存空间上去扩容(如果原有空间后没有足够连续内存,会重新分配一块更大的内存,把原数据拷贝过去,再释放原内存)

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

	// 这里需要free(p2)吗?
	free(p3);
}

这里的p2不需要free,这里的realloc可能原地扩容也可能异地扩容,原地扩容他们俩地址是一样的,释放一次就可以了,要是异地扩容也不需要我们去释放p2,因为realloc申请一块新的空间然后进行拷贝原空间,结束后会释放原空间,再把新空间返回给p3。

4.2、malloc的实现原理(了解)

这部分现阶段我们并不做重点去了解,大家要是感兴趣可以自行去学习了解,等我们学的多了再回头来看这些,这里有个b站的视频链接,大家感兴趣可以去学习:

gilbc中malloc实现原理https://www.bilibili.com/video/BV117411w7o2/?spm_id_from=333.788.videocard.0


相关推荐
ShineWinsu2 小时前
对于C++:类和对象的解析—下(第二部分)
c++·面试·笔试·对象··工作·stati
码农水水2 小时前
国家电网Java面试被问:TCP的BBR拥塞控制算法原理
java·开发语言·网络·分布式·面试·wpf
2013092416272 小时前
1968年 Hart, Nilsson, Raphael 《最小成本路径启发式确定的形式基础》A* 算法深度研究报告
人工智能·算法
如何原谅奋力过但无声2 小时前
【力扣-Python-滑动窗口经典题】567.字符串的排列 | 424.替换后的最长重复字符 | 76.最小覆盖子串
算法·leetcode
浮尘笔记2 小时前
Go语言临时对象池:sync.Pool的原理与使用
开发语言·后端·golang
qq_336313932 小时前
java基础-网络编程-TCP
java·网络·tcp/ip
咕噜咕噜啦啦3 小时前
Java期末习题速通
java·开发语言
BHXDML3 小时前
第七章:类与对象(c++)
开发语言·c++
盐真卿3 小时前
python2
java·前端·javascript
玄冥剑尊3 小时前
贪心算法进阶
算法·贪心算法