首先,为什么叫动态分配内存?

因为内存宝贵,存储在静态内存空间的全局变量,常量不能在使用完成之后立即释放,需要随着程序的结束而终止,如果程序规模庞大,就会引起内存泄漏;而动态内存分配可以按需申请内存,用完就将内存归还给操作系统。

全局变量 局部变量

  • 全局变量:static修饰的变量 或者 不在任何花括号内的变量
  • 局部变量:函数中的变量,随着函数栈帧而创建和消亡,main函数里的变量也是局部变量,只不过它的生命周期和全局变量一样长
  • 堆空间:巨大的空间,申请和回收

malloc

用来动态地分配一个独立的指定大小的内存单元,返回一个void类型的指针,可以转化成任意形式的指针。

1
int *ptr=(int *)malloc(5*sizeof(int));

以32位机器为例,int占4字节,如果空间总长度不满20字节,返回NULL指针

循环为数组动态申请内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<stdio.h>
#include<stdlib.h>

int main(){
int n;
printf("Enter number of elements: ");
scanf("%d",&n);

int *ptr=(int *)malloc(n*sizeof(int));

if(ptr==NULL){
printf("Memory not allocated.\n");
exit(0);
}else{
printf("Memory successfully allocated using malloc.\n");
for(int i=0;i<n;++i){
ptr[i]=i;
}
printf("The elements of the array are: ");
for(int i=0;i<n;++i){
printf("%d ",ptr[i]);
}
}
printf("\n");
return 0;
}

calloc

contiguous allocation,用于动态分配指定数量、指定类型连续的内存块,和malloc函数相比,calloc是为指定长度的对象,分配能够容纳其指定个数的存储空间。

1
int *ptr=(int *)calloc(5,sizeof(int));

在内存中为 5 个元素分配连续空间,每个元素的大小为整型数。

内存泄漏

内存溢出:OOM Out Of Memory,程序在申请内存时,没有足够的内存空间供其使用,比如申请一个int型变量,但是让它存了long类型的数。

内存泄漏:Memory Leak,程序在申请内存后,没有及时释放申请过的内存空间,结果这部分内存空间就像被互斥锁了,无法访问,长此以往,最终会导致内存溢出,内存迟早会被占光。

  • 指针重新赋值
1
2
3
char *p=(char *)malloc(4*sizeof(char));
char *q=(char *)malloc(4*sizeof(char));
p=q;

p原先所指向的内存空间没有释放变成了孤立的内存。

  • 错误的内存释放
1
2
3
4
char **p=(char **)malloc(sizeof(char *));
p[2]=(char *)malloc(4*sizeof(char));
char *np=p[2];
free(p);

释放了由 p 指向的内存块,即第一步中使用 malloc 分配的内存。这样做将导致内存泄漏,因为第二步中分配的内存(p[2]即np 所指向的内存)并没有被释放。

  • 没有对函数返回的值做正确的处理
1
2
3
4
5
6
7
char *f(){
return (char *)malloc(10);
}

void f1(){
f();
}

函数 f 动态分配了一个大小为10的字符数组,并返回其首地址。然后,在函数 f1 中,调用了函数 f,但没有保存或使用其返回的指针,也没有释放由 f 分配的内存。

野指针

  • 指针未初始化
1
2
int *q;
*q=5;
  • 越界访问
1
2
3
4
5
6
int arr[10]={0};
int *q=arr;
for(int i=0;i<=10;i++){
*p=i;
p++;
}

如何避免?

除了避免越界访问,需要对指针指向地址初始化,可以使用malloc分配堆内存,如果分配失败,则返回NULL,使用时,判断指针是否为NULL就可以避免了。

free

既然有动态分配内存,那么也有动态取消分配内存,因为使用函数malloc和calloc分配的内存不会被自行取消分配。

realloc

顾名思义,re-allocated,再次分配,当已经使用malloc或者calloc分配了一部分存储空间,并且已经使用完,需再分配,即使用realloc。

在现有内存空间下扩展存储空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include<stdio.h>
#include<stdlib.h>

int main(){
int n,n1;
printf("Enter number of elements: ");
scanf("%d",&n);

int *ptr=(int *)calloc(n,sizeof(int));

if(ptr==NULL){
printf("Memory not allocated.\n");
exit(0);
}else{
printf("Memory successfully allocated using calloc.\n");
for(int i=0;i<n;++i){
ptr[i]=i;
}

printf("The elements of the array are: ");
for (int i = 0; i < n; ++i) {
printf("%d ", ptr[i]);
}
printf("\n");

printf("Enter bigger number of elements: ");
scanf("%d",&n1);
ptr=(int *)realloc(ptr,n*sizeof(int));

for (int i = n; i < n1; ++i) {
ptr[i]=i;
}

printf("The elements of the new array are: ");
for (int i = 0; i < n1; ++i) {
printf("%d ", ptr[i]);
}
printf("\n");

free(ptr);
}
return 0;
}

什么时候使用

在C语言中,当一个函数执行完毕后,其局部变量通常会被销毁,内存被释放。因此,如果你在函数中创建了一个局部变量,并尝试返回指向该局部变量的指针,那么在函数返回后,该指针就会指向一个已经被销毁的内存区域。

为了避免这个问题,你可以使用动态内存分配,比如使用 malloc 函数,来在堆上分配内存。动态分配的内存在调用 free 函数之前一直存在,因此你可以在函数外部访问它。

比如说:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>

int* test(){
int a=10;
return &a;
}

int main(){
int *p=test();
if(p!=NULL){
printf("%d",*p);
}
return 0;
}

上述代码导致未定义行为,在test函数中,返回一个局部变量a的地址,当test函数执行完毕时,局部变量a超出了其作用域,其占用的内存不再保证有效,所以说main函数尝试解引用会导致未定义行为

未定义行为意味着程序的行为是不可预测的,它可能在不同的编译器上产生不同的结果;所以说我们使用malloc来在堆上分配内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<stdio.h>
#include<stdlib.h>

int *test() {
int a = 10;
int *p = (int *) malloc(sizeof(int));
if (p == NULL) {
return NULL;
}
*p = a;
return p;
}

int main() {
int *p = test();
if (p != NULL) {
printf("%d", *p);
free(p);
}
return 0;
}

当然也可以使用static关键字修饰局部变量,这样a就存在静态区域,从程序开始到程序结束。

malloc是如何分配内存的

malloc不是系统调用,而是一个库函数,malloc在申请内存的时候,有两种方式向操作系统申请堆内存。

  • 方式1:通过 brk() 系统调用从堆分配内存

通过brk函数将堆顶指针向高地址移动,获取新的内存空间。

  • 方式2:通过 mmap() 系统调用在文件映射区分配内存

通过mmap系统调用中私有匿名映射的方式,在文件映射区分配一款内存,也就是从文件映射区偷了一块内存。

  • 使用场景

malloc() 源码里默认定义了一个阈值:

如果用户分配的内存小于 128 KB,则通过 brk() 申请内存;
如果用户分配的内存大于 128 KB,则通过 mmap() 申请内存;

free之后内存真的释放了吗?

通过brk申请内存,free后堆内存还存在,不会把内存归还给操作系统,而是缓存在malloc的内存池中,待下次使用;如果通过mmap方式申请内存,释放后归还给操作系统。

有没有想过一个问题,free函数只是传入一个内存地址,是怎么直到要释放多大的内存的?

malloc返回给用户态的内存起始地址比进程的堆空间起始地址多了16个字节,这一部分就是用来保存内存块的描述信息,比如该内存块的大小。