1. 机算机的内存逻辑
要搞明白c语言内存是怎么回事,就要先搞清楚计算机内存基本逻辑。我们不讲深奥的计算机原理,我们来看一下运行在计算机上的操作系统是怎么管理内存的。
操作系统将内存划分为几个段(不同的操作系统可能有些差异),通常情况下分为栈、堆、全局量、常量、代码空间,如下图:
在linux系统中,内核通过代码将物理内存虚拟出来一层,中间通过内核的数据结构对内存进行转换,这就是我们通常说的虚拟内存,如下图:
上图只是大概描述了虚拟内存和物理内存之间的关系,操作系统实际实现要比这复杂得多。在linux内核以及redis、php的zend引擎等对内存的管理都是都是其最核心也是最复杂的内容之一,其中的源码也是精华所在,例如redis用于管理集合的跳跃表,其核心思想就是链表这一类基础的数据结构。所以,操作系统也好,各类优秀的开源软件也好,究其本质都是数据结构+算法,这也是为什么算法和数据结构很重要的原因。
下面我们来看一段代码:
int dev = 10;
const int test = 20;
void get_config(char *path)
{
int flag;
int *px = NULL;
px = malloc(sizeof(int));
*px = 100;
}
我们来分析一下:
- 代码中的dev不在任何函数体内是一个全局变量。
- 变量test是是通过const关键字声明,是一个常量。
- 函数get_config的形参path是一个局部变量,只在get_config函数内有效。
- flag是在get_config函数体内,所以也是一个局部变量。
- px刚开始声明为NULL是一个局部变量,但后面通过malloc申请了内存。
此时,这段代码的内存分布情况如下图:
各个内存区的生命周期如下:
栈:栈空间会随着函数结束而回收,生命周期限于函数内。
堆:贯穿整个程序的生命周期,后面的数据结构也是操作的堆空间。
全局量:贯穿整个程序的生命周期,全局可用。
常量:限所在代码片的生命周期。如果在函数内部那生命周期就是函数的生命周期。
当然,真实的物理地址并不是像上图一样去存储数据的,而是将代码经过编译器转化成汇编,汇编转换成机器码,运行程序的时候以高低电位的形式保存在真实的物理内存中。这部分深挖下去的话跨不过计算机组成原理这道坎,这是一块复杂又庞大的内容,这里不再深入。
2. c语言的动态内存分配
2.1. malloc和free
void *malloc(size_t size);
malloc逻辑如下:
- malloc分配的是一段连续的内存空间。
- malloc的参数是需要分配内存的字节数。(一般我可以通过sizeof来计算字符长度)
- malloc返回的是一个申请内存的起始地址。
- malloc申请内存失败返回一个NULL指针。
- 在老版本的编译器,需要对malloc的返回结果进行强制类型转换。
- 在要求内存对齐的机器上,malloc返回的起始地址始终能满足对齐的要求。
void free(void *p)
free逻辑如下:
- free的参数只可以是NULL指针,从malloc、calloc或realloc返回的指针。
- 向free传递NULL指针不会产生任何效果。
2.2. calloc和realloc
void *calloc(size_t num_element, size_t element_size)
calloc的逻辑如下:
- 接收两个参数,一个是元素的总数,一个是每个元素的字节数。
- 和malloc不同,calloc在返回指针之前会将内存初始化为0。
void *realloc(void *p, size_t new_size)
realloc的逻辑如下:
- 接收两个参数,malloc或calloc分配的内存指针,要分配内存的大小(字节数)。
- 如果用于扩大内存,则新内存紧接在原内存块的后面。
- 如果用于缩小内存,则新从原内存末尾截取掉要缩小的内存,保留未被截取的原内存内容。
- 如果原先内存无法改变大小,则重新分配一块内存,并将原内存内容复制到新内存里,因为这样,realloc之后就不能使用之前地址,而应该使用realloc返回的新地址。
- 如果realloc的第一个参数是NULL效果和free一样。
2.3. 使用动态内存要注意的坑
2.3.1. 分配的内存没有free
由于使用malloc和calloc分配的内存是在堆区,对于整个程序是全局的,在使用的时候要特别小心,对于分配的内存一定要有对应的释放操作。
while(1) {
int *p = malloc(sizeof(int));
...
}
上面我们在一个循环里使用了malloc分配了一块大小为int的内存地址,但是我们没有使用free来释放。这样实际会造成内存溢出造成没有内存可用最终导致最严重的系统崩溃。
下面我们再看一段代码
int build_data_from_file()
{
int *p_m; FILE *fp;
fp = fopen("test.json", "w");
p_m = malloc(sizeof(int));
if (fp == -1) {
return 0;
}
....
free(p_m);
return 1;
}
上面我们看到如果fp==-1的情况下,内存得不到释放,同样也会导致内存溢出。
2.3.2. 内存释放后被使用
int *p_t;
p_t = malloc(sizeof(int));
free(p_t);
*p_t = 100;
上面我们申请了一块内存,紧接着又释放了内存,然后我们又将分配到的指针的位置赋值为100。由于在赋值之前已经使用free释放了内存,这个时候p_t是一个NULL在许多编译器是编译不通过的。
2.3.3. 越界访问
int *p_a;
p_t = malloc(20*sizeof(int));
int arr[20] = p_t;
arr[22] = 200;
上面我们分配了一个大小为20元素为int的指针,当我们使用arr[22]的时候已经超出了p_a指针所指向的最大指针,这个时候arr[22]指向了哪里我们是不知道的,这类bug也是最难排查的,控制这类问题一个比较好的方法是在操作之前严格判断长度。
动态内存分配还有其它很多异常,比如释放了一个不是malloc分配的内存地址,再比如释放了部分内存等等。以上是经常出现的几个高频bug,相信能在编程的时候考虑到这些问题应该能对提高代码的健壮性有很大的帮助。