12-C语言的内存管理
文章目录
- 12-C语言的内存管理
- 一、C语言进程的内存布局
- 1.1 程序与进程
- 1.2 虚拟内存与物理内存
- 1.2.1 虚拟内存布局
- 二、栈空间的特点与管理
- 三、静态变量
- 3.1 全局静态变量
- 3.2 局部静态变量
- 3.3 为什么需要静态变量?
- 四、数据段与代码段
- 4.1 数据段
- 4.2 代码段
- 4.3 数据段的特点
- 五、堆内存
- 5.1 堆内存的基本特性
- 5.2 如何申请堆内存
- 5.3 malloc
- 5.4 calloc
- 5.5 `realloc`
- 5.6 释放堆内存`free`
- 六、示例代码
- 总结
一、C语言进程的内存布局
在C语言中,一个程序在运行时,操作系统会为其分配内存空间,这个内存空间可以分为几个不同的区域,每个区域有其特定的用途和特性。这些区域包括:栈、堆、数据段和代码段。理解这些区域有助于编写更高效的代码并调试程序中的内存问题。
1.1 程序与进程
- 程序:静态的二进制文件,存储在磁盘上。
- 进程:运行中的程序实例,动态的,操作系统为其分配各种资源,包括内存。
所有的程序被执行起来之后,系统会为他分配各种资源内存,用来存放该进程中用到的各种变量、常量、代码等等。这些不容的内容将会被存放到内存中不同的位置 (区域),不同的内存区域他的特性是有差别。
每一个进程所拥有的内存都是一个虚拟的内存,所谓的虚拟内存是用物理内存中映射(投影)而来的,对于每一个进程而言所有的虚拟内存布局都是一样的。让每个进程都以为自己独自拥有了完整的内存空间。
1.2 虚拟内存与物理内存
- 虚拟内存:每个进程拥有独立的虚拟内存空间,给进程一种独占整个内存的错觉。这种虚拟内存通过内存管理单元(MMU)映射到物理内存。
- 物理内存:实际的硬件内存,虚拟内存映射到这里。
物理内存(Physical Memory)
虚拟内存(Virtual Memory)
1.2.1 虚拟内存布局
-
代码段(Text Segment)
- 存放程序的可执行代码,包括函数体、控制语句等。
- 该段是只读的,防止程序意外修改指令。
- 通常在内存的低地址区域。
-
数据段(Data Segment)
- 存放静态数据和全局变量。
- 细分为初始化数据段和未初始化数据段(BSS段)。
- 初始化数据段:存放已初始化的全局变量和静态变量。
- BSS段:存放未初始化的全局变量和静态变量,程序开始执行时会被清零。
-
堆(Heap)
- 用于动态分配内存,由程序员控制,使用
malloc
、calloc
、realloc
等函数分配,使用free
释放。 - 从低地址向高地址增长。
- 适合用于需要在程序运行期间动态分配的较大数据。
- 用于动态分配内存,由程序员控制,使用
-
栈(Stack)
- 用于存放函数的局部变量、函数参数和返回地址。
- 栈空间的分配和释放由系统自动管理,遵循LIFO(Last In, First Out)原则。
- 从高地址向低地址增长。
- 由于栈空间有限,存放大数据结构时要谨慎。
二、栈空间的特点与管理
用于存放函数的局部变量、函数参数和返回地址.
-
特点:
- 空间有限,尤其在嵌入式环境中(例如某些系统默认栈大小仅为8MB)。
- 系统自动管理分配和释放。
- 函数调用时分配内存,函数返回时释放内存。
-
管理栈空间:
- 避免在栈上分配大块内存。
- 使用
ulimit
命令查看和修改栈大小限制(临时修改,重启后恢复默认值)。
ulimit -a # 查看当前系统的资源限制,包括栈大小 ulimit -s 10240 # 临时将栈大小修改为 10MB,重启后会回到默认值
注意:
- 每当一个函数被调用的时候, 栈空间会向下增长一段,用来存放该函
- 当一个函数退出的时候 , 栈空间会向上回缩一段,该空间的所有权将
- 栈空间的分配与释放,用户是无法干预的, 全部由系统来完成。
三、静态变量
在C语言中,静态变量有两个主要类型:全局静态变量
和局部静态变量
。这些变量在程序的生命周期内具有持久性,存储在数据段中,而不是栈或堆中。
3.1 全局静态变量
全局静态变量定义在函数体外部,并且具有文件作用域,即只能在定义它的文件中访问。
#include <stdio.h>
static int global_var = 1000; // 全局静态变量
void func1() {
printf("global_var: %d\n", global_var);
}
void func2() {
global_var += 100;
printf("global_var after increment: %d\n", global_var);
}
int main() {
func1(); // 输出: global_var: 1000
func2(); // 输出: global_var after increment: 1100
func1(); // 输出: global_var: 1100
return 0;
}
3.2 局部静态变量
局部静态变量定义在函数体内部,并且具有块作用域,但其生命周期跨越整个程序运行时间。这意味着它在第一次初始化后,其值在函数调用之间保持不变。
#include <stdio.h>
void func() {
int a = 250; // 局部变量
static int b = 100; // 静态局部变量,只初始化一次
printf("a: %d, b: %d\n", ++a, ++b);
}
int main() {
func(); // 输出: a: 251, b: 101
func(); // 输出: a: 251, b: 102
func(); // 输出: a: 251, b: 103
return 0;
}
在上面的代码中,每次调用 func
函数时,变量 a
都会重新初始化为250,而变量 b
仅在第一次调用时初始化为100,此后 b
保留其在上一次函数调用时的值。
3.3 为什么需要静态变量?
- 持久性:静态变量在程序的整个生命周期内都存在。这在需要跨函数调用持久化数据时非常有用。
- 局部静态变量:在希望一个局部变量的值
在多次函数调用之间保持不变
的情况下,可以使用静态局部变量。 - 全局静态变量:在需要将变量的作用域限制在单个文件内时,可以使用全局静态变量。它们不会被其他文件中的函数访问到,增强了数据的封装性。
四、数据段与代码段
4.1 数据段
数据段分为三个部分:.bss、.data和.rodata。
- .bss段:存放未初始化的静态数据,程序运行时会
自动初始化为0
。 - .data段:存放已初始化的静态数据。
- .rodata段:存放
只读数据
(常量),如字符串常量,不允许修改。
#include <stdio.h>
int uninitialized_global; // 位于 .bss 段
int initialized_global = 10; // 位于 .data 段
const char *msg = "Hello, World!"; // 位于 .rodata 段
int main() {
printf("uninitialized_global: %d\n", uninitialized_global); // 输出: 0
printf("initialized_global: %d\n", initialized_global); // 输出: 10
printf("msg: %s\n", msg); // 输出: Hello, World!
return 0;
}
4.2 代码段
代码段存放程序的可执行代码,包括用户编写的函数和编译器生成的系统初始化代码。代码段通常是只读
的,以防止程序无意中修改指令。
#include <stdio.h>
void func() {
printf("This is a function.\n");
}
int main() {
func(); // 输出: This is a function.
return 0;
}
4.3 数据段的特点
- 自动初始化:未初始化的静态数据会自动初始化为0。
- 初始化执行一次:静态数据的初始化语句在程序加载时执行一次。
- 持久性:静态数据的内存从程序开始运行到程序结束都存在,与进程共存亡。
五、堆内存
堆内存,又称动态内存
或自由内存
,是唯一
一个由开发者随意分配与释放的内存空间。堆内存的具体申请大小和使用时长由程序员决定。
5.1 堆内存的基本特性
- 大空间:相对于栈空间,堆空间大很多,堆的大小受限于物理内存,系统不会对堆空间进行限制。
- 从下往上增长:堆内存是从低地址向高地址增长的。
- 匿名内存:堆内存没有名字,只能通过指针来访问。(不像栈空间那样有名字)
- 手动管理:堆内存的申请和释放由程序员自行管理,申请的内存需要手动释放,直到程序退出。
5.2 如何申请堆内存
5.3 malloc
malloc
函数用于向系统申请内存,但不会清空这块内存。
malloc (向系统申请内存)
头文件:
#include <stdlib.h>
函数原型:void *malloc(size_t size);
参数分析:
- size ‐‐> 需要申请的内存 (字节为单位)
返回值:
- 成功 返回一个指向成功申请到内存的指针(入口地址)
- 失败 返回 NULL
#include <stdlib.h>
#include <stdio.h>
int main() {
// 申请可以存放10个整数的堆空间
int *p = (int *)malloc(10 * sizeof(int));
if (p == NULL) {
// 申请失败
perror("malloc failed");
return 1;
}
// 初始化并打印数组
for (int i = 0; i < 10; i++) {
*(p + i) = i;
printf("*(p+%d): %d\n", i, *(p + i));
}
// 打印指针地址--> &p 打印的是栈空间地址 p中所存放的地址为堆空间地址
printf("&p: %p -> %p\n", (void *)&p, (void *)p);
// 释放内存
free(p);
return 0;
}
由终端输入存入malloc所申请的内存中:
注意在上面的截图中,scanf("%[\n]s",p);–>[\n] 表示读取所有非换行符 \n 的字符,直到遇到换行符为止。避免了在输入空格时就截至了的情况。
5.4 calloc
calloc
函数用于向系统申请内存,会将内存清空(初始化为0)。
calloc (向系统申请内存)
头文件:
- #include <stdlib.h>
函数原型:
- void *calloc(size_t nmemb, size_t size);
参数分析:
- nmemb ‐‐ > N 块内存(连续的)
- size ‐‐ > 每一块内存的大小尺寸
返回值:
- 成功 返回一个指向成功申请到内存的指针(入口地址)
- 失败 返回 NUL
#include <stdlib.h>
#include <stdio.h>
int main() {
// 申请10块连续的内存,每块大小为sizeof(int)
int *p = (int *)calloc(10, sizeof(int));
if (p == NULL) {
// 申请失败
perror("calloc failed");
return 1;
}
// 初始化并打印数组
for (int i = 0; i < 10; i++) {
*(p + i) = i + 998;
printf("*(p+%d): %d\n", i, *(p + i));
}
// 释放内存
free(p);
return 0;
}
5.5 realloc
realloc
函数用于重新分配内存大小,可以扩大或缩小内存。
realloc (重新申请空间)
头文件:
- #include <stdlib.h>
函数原型:
- void *realloc(void *ptr, size_t size);
参数分析:
- ptr ‐‐> 需要 扩容/缩小 的内存的入口地址
- size ‐‐> 目前需要的大小
返回值:
- 成功 返回修改后的地址
- 失败 NULL
#include <stdlib.h>
#include <stdio.h>
int main() {
// 申请10块连续的内存,每块大小为sizeof(int)
int *p = (int *)calloc(10, sizeof(int));
if (p == NULL) {
// 申请失败
perror("calloc failed");
return 1;
}
// 初始化并打印数组
for (int i = 0; i < 10; i++) {
*(p + i) = i + 998;
printf("*(p+%d): %d\n", i, *(p + i));
}
// 打印重新分配前的指针地址
printf("重新分配前p: %p\n", (void *)p);
// 重新分配内存--> // 如果重新申请内存需要另找宝地 , 那么 原本的地址p会被释放掉
int *p1 = (int *)realloc(p, 128 * sizeof(int));
if (p1 == NULL) {
// 重新分配失败
perror("realloc failed");
free(p);
return 1;
}
// 原指针已经被释放了,但是为了防止野指针,所以将原指针置为NULL,防止成为野指针
p = NULL;
// 打印重新分配后的指针地址
printf("重新分配后p1: %p\n", (void *)p1);
// 打印原数据
for (int i = 0; i < 10; i++) {
printf("*(p1+%d): %d\n", i, *(p1 + i));
}
// 释放内存
free(p1);//注意不是p
return 0;
}
5.6 释放堆内存free
free
函数用于释放堆内存。
free(释放堆内存)
头文件:
-#include <stdlib.h>函数原型:
- void free(void *ptr);
参数分析:
- ptr ‐‐> 需要释放的内存的入口地址
返回值;
- 无
#include <stdlib.h>
#include <stdio.h>
int main() {
// 申请内存
int *p = (int *)malloc(10 * sizeof(int));
if (p == NULL) {
perror("malloc failed");
return 1;
}
// 使用内存...
// 释放内存
free(p);
return 0;
}
释放内存的含义是将当前内存的所有权交回给系统,但不会清空内存的内容。释放内存后,需要将指针置为NULL,防止成为野指针。
总结:
malloc
申请内存但不清空。calloc
申请内存并初始化为0。realloc
重新分配内存大小。free
释放内存。
释放内存后,记得将指针置为NULL,防止成为野指针。
六、示例代码
以下代码展示了栈、堆和数据段的使用:
#include <stdio.h>
#include <stdlib.h>
// 全局变量(位于数据段)
int global_var = 10;
int uninitialized_global_var;
void function() {
// 局部变量(位于栈)
int local_var = 20;
printf("Local variable: %d\n", local_var);
}
int main() {
// 动态分配内存(位于堆)
int *heap_var = (int *)malloc(sizeof(int));
if (heap_var == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return 1;
}
*heap_var = 30;
printf("Heap variable: %d\n", *heap_var);
function();
// 打印全局变量
printf("Global variable: %d\n", global_var);
printf("Uninitialized global variable: %d\n", uninitialized_global_var);
// 释放堆内存
free(heap_var);
return 0;
}
总结
- 代码段:存放程序的可执行代码,通常只读。
- 数据段:存放全局变量和静态变量,分为已初始化和未初始化部分。
- 堆:用于动态内存分配,从低地址向高地址增长。
- 栈:用于存放局部变量和函数调用信息,从高地址向低地址增长。