C语言内存管理深度解析:从基础概念到实战应用
C语言内存管理深度解析:从基础概念到实战应用
引言
内存管理是计算机科学中的核心概念之一,尤其是在C语言这种编程语言中。内存与数组、指针等概念密切相关,掌握内存的使用和布局对于编写高效、稳定的程序至关重要。
本文旨在深入探讨内存管理的各个方面,包括内存布局、动态内存分配、虚拟内存以及内存管理等方面,讲的知识比较多,建议收藏慢慢学习。
1. 内存管理基础
内存(Memory)是计算机系统中用于存储数据和指令的硬件设备。它是程序运行的基石,所有的变量、常量、函数代码等都需要存储在内存中。内存通常分为物理内存和虚拟内存。
在操作系统的管理中,虚拟内存通常被划分成内核空间和用户空间,我们平时使用的内存空间都是位于用户空间,而用户空间有分成栈区、堆区、数据段、代码段以及内存映射段。
2. 物理内存 && 虚拟内存
任何一个程序,正常运行都需要内存资源,用来存放诸如变量、常量、函数代码等等。这些不同的内容,所存储的内存区域是不同的,且不同的区域有不同的特性。因此我们需要研究其内存布局,逐个了解不同内存区域的特性。
每个C语言进程都拥有一片结构相同的虚拟内存,所谓的虚拟内存,就是从实际物理内存映射出来的地址规范范围,最重要的特征是所有的虚拟内存布局都是相同的,极大地方便内核管理不同的进程。例如三个完全不相干的进程p1、p2、p3,它们很显然会占据不同区段的物理内存,但经过系统的变换和映射,它们的虚拟内存的布局是完全一样的。
- PM:Physical Memory,物理内存。
- VM:Virtual Memory,虚拟内存。
将其中一个C语言进程的虚拟内存放大来看,会发现其内部包下区域:
虚拟内存中,内核区段对于应用程序而言是禁闭的,它们用于存放操作系统的关键性代码,另外由于 Linux 系统的历史性原因,在虚拟内存的最底端 0x0 ~ 0x08048000 之间也有一段禁闭的区段,该区段也是不可访问的。
虚拟内存的各段详细情况:
3. C语言中的内存布局
内存局中最常用和最需要掌握的是堆区,栈区,数据段。
3.1 栈区(Stack)
- 栈空间是有限的,一般为8M,项目所需的内存的较大时,尤其是在嵌入式的环境下,因此栈不能存放尺寸大的变量。
- 当执行、调用函数时,栈会自动向下增长一段,用来存放函数内的局部变量;当退出函数时,栈会自动向上缩减一段,将该函数的局部变量所占内存归还给系统,导致退出函数时,地址会自动销毁,不能够返回给调用的地方。
3.2 堆区(Heap)
- 相比栈内存,堆内存由程序员自己来申请内存,使用完后也得自己释放内存。
- 相比栈内存,堆的总大小仅受限于物理内存,在物理内存允许的范围内,系统对堆内存的申请不做限制。(你的内存条越大,系统越放开,那么你能申请的堆内存也就越大)
- 堆内存是匿名的,访问需要通过指针。
3.3 数据段
- 数据段包含了.bss段,.data段,.rodata段。
- .bss (Block Started bySymbol)段: 存放未初始化的静态数据和全局变量,它们将被系统自动初始化为0
- .data段:存放已初始化的静态数据和全局变量
- .rodata段:存放常量数据
3.4 代码段
- .text段:存放用户代码
- .init段:存放系统初始化代码(执行main函数之前,栈和堆的初始化信息会被先执行)
示例代码(数据段和代码段解析):
#include <stdio.h>
// 全局变量(即没有被花括号{}包含的)
int num1; // 数据段中.bss区域
int num3 = 100; // 数据段中.data区域
// 主函数
int main(int argc, char const *argv[])
{
// (1)、代码段:
/*
.text: 用户代码
.inti: 系统初始化堆栈代码
说明:代码段中的.init存放了栈和堆的初始化信息,这些信息会在
执行main函数之前,完成初始化,将其内存从硬盘中加载到内存条中,形成
内存中的堆栈区域
*/
// (2)、数据段:
// 1、.bss (Block Started bySymbol)段: 存放未初始化的静态数据和全局变量,它们将被系统自动初始化为0
// a、未初始化的全局变量
// b、未初始化的static修饰的局部变量
static int num2;
// 2、.data段:存放已初始化的静态数据和全局变量
// a、已初始化的全局变量
// b、已初始化的static修饰的局部变量
static int num4 = 200;
// 3、.rodata段:存放常量数据
// 1、整型常量:100(整型int), 200L(长整型long int), 300LL(长长整型long long int), 400ULL(无符号长长整型unsigned long long int)
// 2、浮点型常量:3.14(双精度浮点型), 6.18L(长双精度浮点型)
// 3、字符常量:'a', '8'
// 4、字符串常量:"nihao"
// 5、科学计数法常量:e
// 栈区 数据段中rodata段(常量区)
char buf[128] = "haoyangdefantuan";
*(buf+0) = 'a'; // 可以的,因为修改的是buf数组里面的内存(已经将常量区的内存复制于此),而不是常量区的内存
// 栈区 数据段中rodata段(常量区)
char *p = "haoyangdefantua";
*(p+0) = 'a'; // 不可以, 因为p指针只存放地址(这个地址在常量区,所以不能够修改数据),不存放内存
return 0;
}
4. 静态数据
静态数据有两种:
- (1) 全局变量
- (2) static修饰的变量
作用:
(1) 在工程中多个文件同时需要使用同一变量的时候,为了不用多次定义,采用全局变量的写法,在要使用该变量的其他文件中使用 extern 关键字进行声明,使得机器在工程里面寻找这个变量。
文件1:
// File1.c
#include <stdio.h>
int global_var = 10; // 定义全局变量
int main()
{
printf("%d\n", global_var);
}
文件2:
// File2.c
#include <stdio.h>
extern int global_var; // 声明全局变量
int main()
{
printf("%d\n", global_var);
}
(2) static修饰的全局变量:为了保证代码的安全性,不被工程内别的.c文件使用 extern 访问其数据,在定义全局变量的位置使用 static 关键字。
(3) static修饰的局部变量:在调用完函数,退出函数时,希望这个值保留,下次再调用时不会重新初始化,就可以在定义这个局部变量时使用 static 关键字,简单来说就是,将这个局部变量的生命周期延长到程序结束。
void counter()
{
static int count = 0; // 只初始化一次
count++;
printf("Count: %d\n", count);
// 正常情况下,每次调用函数都会初始化一次
// 但static修饰的局部变量会只用初始化一次,生命周期延长到程序结束
}
int main()
{
int i = 0;
while(i<5)
{
counter();
}
return 0;
}
5. 动态内存管理
5.1 malloc && calloc
- **所需头文件:
#include <stdlib.h>
**
- 函数原型:
- void *malloc(size_t size);
- 用于申请一块指定大小的堆内存,参数
size
表示所申请的一块堆内存的大小,单位是字节。成功时返回指向分配好的堆内存的指针,失败则返回
NULL
。 - void *calloc(size_t num, size_t size);
- 用来申请
count
块连续分布、无间隔的堆内存,每块大小为
size
字节。成功返回指向分配好的堆内存的指针,失败返回
NULL
。
5.2 bzero
- **所需头文件:
#include <strings.h>
**
- 函数原型:
- void bzero(void *s, size_t n);
- 参数
s
是指向要清零的内存的指针,
n
是要清零的内存大小,单位是字节,该函数无返回值。
5.3 free
- **所需头文件:
#include <stdlib.h>
**
- 函数原型:
- void free(void ptr)**;*
- 参数
ptr
是堆内存指针,函数无返回值,用于释放之前通过
malloc
、
calloc
等函数申请的堆内存。
5.4 realloc
- 所需头文件:**
#include <stdlib.h>
**
- 函数原型:
- void *realloc(void *ptr, size_t size);
- 功能:用于重新分配堆内存。它可以改变之前通过
malloc
、
calloc
等函数分配的内存块的大小。参数
ptr
是指向原来已分配的堆内存的指针,如果
ptr
为
NULL
,则相当于执行
malloc(size)
;参数
size
表示重新分配后的内存块大小。 - 返回值:成功时返回指向重新分配后的内存块的指针(可能与原来的指针地址相同,也可能不同);如果分配失败,返回
NULL
,并且原来
ptr
所指向的内存块不会被释放;如果
size
为 0 且
ptr
不为
NULL
,则释放
ptr
指向的内存块,返回
NULL。
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
int main(int argc, char const *argv[])
{
// (1)、使用malloc函数申请内存空间
// 1、定义并初始化
char *p_buf = (char *)malloc(sizeof(char)*5); // 申请定义个堆空间
if (p_buf == NULL)
{
fprintf(stderr, "Malloc failed\n"); // 输出错误信息
return 1;
}
/*
一般使用sizeof(数据类型)*数量来确定申请的堆空间大小,因为会更加精准,不浪费空间
也可以使用malloc(100)来申请空间,但是可能会存放浪费空间的现象
*/
bzero(p_buf, sizeof(char)*5); // 通过p_buf指针去到堆空间,将其内存全部清零(malloc申请的空间里面没有初始化,可能有乱码)
// 2、使用
// a、指针样式的赋值操作
for (int i = 0; i < 5; i++)
{
*(p_buf+i) = i;
printf("*(p_buf+%d) == %d\n", i, *(p_buf+i));
}
// b、数组样式的赋值操作
for (int i = 0; i < 5; i++)
{
p_buf[i] = i*10;
printf("p_buf[%d] == %d\n", i, p_buf[i]);
}
// 3、释放堆空间
free(p_buf);
/*
说明:
- 释放堆内存意味着将堆内存的使用权归还给系统。
- 释放堆内存并不会改变指针的指向。
- 释放堆内存并不会对堆内存做任何修改,更不会将内存清零。
*/
for (int i = 0; i < 5; i++)
{
p_buf[i] = i*5;
printf("p_buf[%d] == %d\n", i, p_buf[i]);
}
// (2)、calloc函数
char *p2_buf = (char *)calloc(5, sizeof(char)); // 等同于malloc(sizeof(char)*5);以及已经初始化了
if (p2_buf == NULL)
{
fprintf(stderr, "Calloc failed\n");
return 1;
}
for (int i = 0; i < 5; i++)
{
*(p2_buf+i) = i;
printf("*(p2_buf+%d) == %d\n", i, *(p2_buf+i));
}
// (3)、realloc函数
char *tmp = (char *)realloc(p2_buf, 10 * sizeof(char)); // 重新申请内存空间
if (tmp == NULL)
{
fprintf(stderr, "Realloc failed\n");
return 1;
}
p2_buf = tmp; // 更新原指针
for (int i = 0; i < 10; i++)
{
*(p2_buf+i) = i;
printf("*(p2_buf+%d) == %d\n", i, *(p2_buf+i));
}
// 释放重新分配的内存
free(p2_buf);
return 0;
}
- 释放堆内存的含义:
- 释放堆内存意味着将堆内存的使用权归还给系统。
- 释放堆内存并不会改变指针的指向。
- 释放堆内存并不会对堆内存做任何修改,更不会将内存清零。
6.内存泄露以及解决方案
内存泄漏的基本概念:内存泄漏指的是在程序中动态分配的内存没有被正确释放,导致这部分内存无法再被使用,最终可能耗尽系统内存资源,影响程序性能甚至导致崩溃。
常见的内存泄漏的原因:
1. 忘记释放动态分配的内存:使用 malloc 、 calloc、realloc 分配内存后,没有调用 free 释放内存。
2. 指针丢失:重新赋值指针前没有释放原有内存,导致无法再访问原内存块。
3. 异常路径未释放内存:在程序执行过程中,如果存在异常或错误退出的分支(选择语句,提前退出函数),可能跳过内存释放的代码。
4. 递归释放:在递归函数中分配内存,没有对应正确的释放机制,导致内存泄露。
示例代码:
int *arr = malloc(100 * sizeof(int));
// 忘记 free(arr);
int *ptr = malloc(100);
ptr = malloc(200); // 原内存块丢失,无法释放
void func()
{
int *data = malloc(100);
if (error) return; // 内存泄漏
free(data);
}
解决方法:
1. 使用完动态分配的内存后,及时使用free释放。
2. 释放完内存后,将对应的指针指向NULL,避免指针悬空,指向不可访问区域。
3. 使用检测工具,例如:Valgrind(Linux):检测内存泄漏和非法访问。 Linux系统重安装命令:sudo apt-get install valgrind 编译C程序:gcc -g -o test test.c 使用Valgrind检测内存泄漏: valgrind --leak-check=full ./test
7.结束语
内存管理是 C 语言编程中的关键内容,从基础概念到物理与虚拟内存的区别,再到内存布局、静态与动态内存管理,以及内存泄露问题,都对程序的稳定性和性能有着重要影响。掌握这些知识,能帮助开发者编写出更高效、健壮的代码,在 C 语言编程之路上走得更稳更远 。
每次写C语言笔记时,我都是想了又想,改了又改,因为我认为对于一些问题是比较严谨的,希望能够给读者和自己带来最好的解答,同时也希望看完的朋友,要是有疑问可以在评论区回答,看到后我会马上回答。