🐱作者:一只大喵咪1201 🐱专栏:《C语言学习》 🔥格言:你只管努力,剩下的交给时间!
在之前本喵写的所有程序都是在栈区开辟空间的,这里的内存空间在创建变量的时候便开辟好了,大小也是确定的,在整个程序执行的过程中,已经开辟的空间大小是无法改变的。 然而很多时候,创建变量的时候是无法知道所需内存空间大小的,只有在执行的过程中才能确定需要多大的内存空间,如果按照以前内存的开辟方式,大小是无法改变的,在这里本喵给大家介绍一下动态内存管理。 所谓动态内存:
- 内存空间是开辟在堆区上的
- 内存空间的大小是可以调整的
🍉动态内存函数的介绍
C语言中提供了专门的库函数来开辟动态的内存空间。
🍓malloc
void* malloc (size_t size);
这是malloc函数的声明,需要引用头文件stdlib.h,它的作用是开辟一块动态内存空间
- 形参:size_t类型的整数,它是指要开辟多少个字节的动态内存空间
- 返回类型:void*类型的空指针
- 动态内存空间开辟成功,返回的是这块空间的首地址,用什么类型的指针变量接收就要将它强转为什么类型。
- 动态内存空间开辟失败,返回空指针NULL,在使用的时候要对返回指针的有效性进行判断。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
free(p);
p = NULL;
return 0;
}
这段代码中开辟了40个字节的动态内存空间,将0到9的十个int类型的数据存在这块空间中,并且打印。
- 在开辟好动态内存空间以后,必须对返回的指针变量进行有效性判断,判断动态内存空间是否开辟成功。
- 在使用完动态内存空间以后,必须将动态内存空间释放掉,也就是还给操作系统的,以便其他数据使用,并且将指针置为空指针,让它失忆。
- 如果不释放动态内存空间会导致内存泄漏,在下面本喵会详细讲解。
- 在动态内存空间释放以后,指向动态内存空间的指针便成了野指针,所以必须让它失忆。
该函数的使用我们已经清楚了,接下来本喵演示一下它在内存中干了什么 它在堆区中连续开辟了40个字节的空间,这40个字节的空间中放入了0到10十个int类型的数据,返回的指针p指向这块空间的首地址。
🍓free
void free (void* ptr);
这是free函数的声明,需要引用头文件stdlib.h,它的作用是释放开辟出来的动态内存空间
- 形参:void*类型的空指针,这里就是用来接收动态内存函数开辟空间后返回的地址
- 返回类型:空类型,这里什么都不返回
在free函数释放掉空间以后,需要将指向动态内存空间的指针变量失忆,虽然此时开辟的动态内存空间已经还给了操作系统,但是指针还是指向原来的地址,是一个野指针。
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
return 0;
}
还是上面的例子,我们在这里没有释放开辟的动态内存空间,但是在程序执行结束的时候,还是会自动释放内存空间。 如果某个程序是7*24小时执行的,并且会开辟动态内存,那么就必须要在使用完动态内存空间以后将它释放掉,否则就会发生内存泄漏。
free和动态内存函数(malloc,calloc,realloc)成对使用。
🍓calloc
void* calloc (size_t num, size_t size);
这是calloc函数的声明,需要引用头文件stdlib.h,它的作用和malloc一样,也是开辟一块动态内存空间
- size_t num:表示要开辟的动态内存空间可以存放多少个元素
- size_t szie:表示每个元素所占内存的大小
- 返回类型:void*类型的空指针,和malloc一样,返回的是动态内存空间的首地址,使用的时候需要强制类型转化
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
perror("calloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
p[i] = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
free(p);
p = NULL;
return 0;
}
结果与上面malloc一样。 那么它与malloc有什么区别呢? 在调试中我们可以看到,calloc在开辟好动态内存后顺便将内存空间初始化为0 而malloc在开辟好动态内存空间后并没有初始化,空间中的内容是随机值 所以说:
calloc和malloc的区别是:
- calloc在开辟动态内存空间的同时会用0初始化,而malloc不会
- calloc和malloc在开辟动态内存空间大小的表达上有所不同
🍓realloc
void* realloc (void* ptr, size_t size);
这是realloc函数的声明,需要引用头文件stdlib.h,它的作用是调整已经开辟了的动态内存空间的大小
- void* ptr:这是一个void*类型的空指针,用于接收已经开辟了的动态内存空间的首地址
- size_t:表示调整后动态内存空间的大小,是在前面空间大小的基础上做加减后得到的值
- 返回类型:void*类型的指针,指向调整后动态内存空间的首地址,但是返回的指针有两种情况,后面本喵会详细讲解
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
int* str = (int*)realloc(p, 80);
if (str != p)
{
p = str;
}
for (i = 10; i < 20; i++)
{
*(p + i) = i;
}
for (i = 0; i < 20; i++)
{
printf("%d ", p[i]);
}
free(p);
p = NULL;
return 0;
}
我们在malloc开辟的40个字节的空间上又成功增加了40个字节,并且赋了相应的值。 realloc用来调整malloc和calloc开辟的动态内存空间
realloc调整内存空间有俩种情况:
由malloc或者calloc开辟的动态内存空间是红色的框,里面是数字0到9,一个40个字节大小。 由realloc调整扩大的部分是黑色的框,里面是数字10到19。 其他颜色的框表示其他类型的数据在堆区中的位置。
- 此时的扩容是在原有空间的基础上直接扩大的,最后返回的是扩容后整个空间的首地址
同样,由malloc或者calloc开辟的空间是红色框。 蓝色框是其他类型的数据在堆区中的位置,由于红色框与蓝色框之间的内存大小不够40个字节, 所以在堆区中重新找了一个地方开辟了一块空间,这个空间大小是80个字节,是在原本40个字节的基础上扩容了40个字节。 在realloc调整的过程中,将原来空间中的内容复制到新空间中,并且将原来的空间释放掉。
- 此时的扩容是开辟了一个新的内存空间,大小是调整后的大小,返回的是整个新空间的首地址
正是因为有这两种情况,所以在上面的代码中有
int* str = (int*)realloc(p, 80);
if (str != p)
{
p = str;
}
这是为了使用方便进行的操作,如果返回的地址不是原来空间的首地址,也就是第二种情况,就需要将地址赋值给原来的指针变量,保证在程序中只使用一个指针变量来管理动态内存空间。
这样开辟动态内存空间会使堆栈中存在许多小的没有被使用的内存块,这个现象就叫做是内存碎片化。
- 内存碎片化是对内存空间的一种浪费,可以通过内存池的方法来减少这种浪费。
realloc也可以开辟动态内存空间
int main()
{
int* p = (int*)realloc(NULL, 40);
if (p == NULL)
{
perror("realloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
p[i] = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
free(p);
p = NULL;
return 0;
}
- 只需要将空指针NULL传递给形参中的指针变量,就可以实现malloc和calloc同样的功能,开辟一个新的动态内存空间
realloc开辟的动态内存空间同样没有初始化,其内容是随机值。
🍉常见的动态内存错误
- 对NULL指针的解引用
int main()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;
free(p);
p = NULL;
return 0;
}
如果动态内存开辟失败就会返回空指针,空指针我们是无法使用的,所以要进行指针的有效性判断
if (p == NULL)
{
perror("realloc");
return 1;
}
来避免对空指针的误使用。
- 对动态开辟的空间越界访问
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
perror("calloc");
return 1;
}
int i = 0;
for (i = 0; i <= 10; i++)
{
p[i] = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
free(p);
p = NULL;
return 0;
}
只是开辟了40字节,也就是10个int类型的动态类型空间,这十个空间中只能放下数字0到9,当放数字10的时候就是超出了开辟的10个元素的空间,此时就是越界访问了,可以看到,程序直接奔溃了。
- 对非动态开辟的空间使用free释放
int main()
{
int a = 10;
int* p = &a;
free(p);
p = NULL;
return 0;
}
此时的指针是int类型变量a的地址,它是在栈区存放,该内存空间并不是动态开辟的,所以用free释放的时候程序也会奔溃。
free只能释放动态开辟的内存空间
- 使用free释放开辟动态内存空间的一部分
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("mmalloc");
return 1;
}
p++;
free(p);
p = NULL;
return 0;
}
将动态开辟内存空间返回的地址进行加1,指针p指向的便不再是这个空间的首地址,而是第二个int类型的地址,此时将指针传给free释放,意味着释放除第一个int类型空间以外的全部空间,这是不被允许的,程序同样也会奔溃。
free释放的动态内存空间必须是整块内存空间,不能是一部分。
- 对同一块内存释放多次
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
free(p);
free(p);
return 0;
}
在这里,动态开辟的内存在使用过后通过free释放了,然后再执行一些程序,此时程序员忘记了已经释放过了,又来了一次释放,一共对同一块动态内存空间释放了两次,这是不被允许的,程序会奔溃。
所以要在释放完以后及时将指针变量置为空指针,这里是不仅是为了避免野指针的问题,也是因为free函数接收的指针如果是空指针的话它什么也不会做,不会去释放内存
free(p);
p = NULL;
- 忘记释放动态开辟的内存(内存泄漏)
这是一个很严重的问题,如果没有释放动态开辟的内存空间,那么这个空间一直都是不能被其他数据所使用的,就会白白浪费这么一块空间,这就是内存泄漏。 尤其是不停的开辟动态内存空间,而且不释放,此时内存会越用越少
void test()
{
while (1)
malloc(10);
}
int main()
{
test();
return 0;
}
这段代码就是不停的在堆区上开辟动态内存空间,每次开辟10个字节。 可以看到,这时内存会飙高,这就是内存泄漏,最终会导致没有内存,全部被浪费掉。
🍉C程序内存的开辟
我们在程序中创建变量的时候,它们都会在内存中开辟内存空间,不同类型的变量在内存中的不同位置开辟空间来使用。
- 全局变量和static修饰的变量是存放在静态区的,也就是图中的数据段
- 局部变量全部存放在栈区,而且是从高地址向低地址开辟空间使用
- 字符常量存放在代码只读区,也就是图中的代码段
- 动态开辟的空间放在堆区,而且是从低地址向高地址开辟空间使用
- 内核空间是我们用户无法访问的
🍉柔性数组
你可能没有听说过柔性数组,但是它是确实存在的,在C99中,结构体类型中的最后一个成员变量是数组,而且大小没有确定,这个数组就是柔性数组。
struct Stu
{
int n;
int arr[0];
};
这样的数组arr就是一个柔性数组,这样写在有些编译器中会报错,写成
struct Stu
{
int n;
int arr[];
};
就可以了。 这就是柔性数组的样子。
struct Stu
{
int n;
int arr[0];
};
int main()
{
struct Stu s;
printf("%d\n", sizeof(struct Stu));
return 0;
}
可以看到,它的结果是4,并没有计算柔性数组的大小,只计算了成员变量n的大小。 也就是说,柔性数组是不能存放在栈区上的,它需要存放在堆区上,它的空间是动态开辟的。
柔性数组具有以下特点:
- 柔性数组前必须至少有一个其他类型的成员变量
- sizeof返回的结构体大小不包括柔性数组的大小
- 包含柔性数组的结构体要用malloc等函数开辟动态内存空间来储存,并且开辟的大小要大于结构体的大小以适应柔性数组的预期大小
typedef struct Stu
{
int n;
int arr[0];
}Type_s;
int main()
{
Type_s* ps = (Type_s*)malloc(sizeof(Type_s) + 40);
if (ps == NULL)
{
perror("malloc");
return 1;
}
ps->n = 10;
int i = 0;
for (i = 0; i < ps->n; i++)
{
ps->arr[i] = i;
}
Type_s* str = (Type_s*)realloc(ps, sizeof(Type_s) + 80);
if (str != ps)
{
ps = str;
}
ps->n = 20;
for (i = 10; i < ps->n; i++)
{
ps->arr[i] = i;
}
for (i = 0; i < ps->n; i++)
{
printf("%d ", ps->arr[i]);
}
free(ps);
ps = NULL;
return 0;
}
以上代码就是先创建一个40个字节的柔性数组,将0到9共十个int类型的数据放进去,再将其扩容到80个字节,继续放入10到19共十个int类型的数据,此时柔性数组arr中有20int类型的元素。
接下来本喵画图来演示一下:
- 第一次开辟的是44个字节,包括成员变量i的4个字节,以及柔性数组的40个字节
- 扩容后又在原本的柔性数组内存空间的基础上扩大40个字节
- 柔性数组的动态内存空间和柔性数组的动态内存空间是紧挨着的
以上代码的功能可以以另一种方式来实现,通过一个int*类型的指针变量
typedef struct Stu
{
int n;
int* arr;
}Type_p;
int main()
{
Type_p* ps = (Type_p*)malloc(sizeof(Type_p));
if (ps == NULL)
{
perror("malloc");
return 1;
}
ps->n = 10;
ps->arr = (int*)malloc(40);
if (ps->arr == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < ps->n; i++)
{
ps->arr[i] = i;
}
ps->n = 20;
int* str = (int*)realloc(ps->arr, 80);
if (str)
{
ps->arr = str;
}
for (i = 10; i < ps->n; i++)
{
ps->arr[i] = i;
}
for (i = 0; i < ps->n; i++)
{
printf("%d ", ps->arr[i]);
}
free(ps->arr);
ps->arr = NULL;
free(ps);
ps = NULL;
return 0;
}
可以实现和柔性数组同样的效果,但是它们的开辟方式有所不同
- 这种方式的实质是开辟了一个动态内存空间,将其地址赋值给结构体中的成员变量arr,通过arr中的地址来操作存放数组的动态内存空间
- 扩容也是存放数组的40个字节的空间的基础上扩容
- 存放数组的整个动态内存空间和结构体的动态内存空间很有可能不挨着
所以说,柔性数组是有优势的
- 柔性数组在结构体中,释放了结构体开辟的动态内存空间也就释放了柔性数组,只需要进行一次释放即可。
- 指针变量的方式中,存放数组的动态内存空间和存放结构体的内存空间是俩块独立的空间,只有先释放了存放数组的空间,才能再释放存放结构体的空间。如果释放反了,先释放结构体的空间,那么此时结构体中的指针变量也被释放了,存放数组的空间就找不到了,就得不到释放,所以要严格遵循释放顺序。
- 柔性数组是结构体中的一部分,也是采用内存对齐的方式存放在动态开辟的内存空间中,所以访问数得快。同时也减少了内存碎片。
- 指针变量的方式中,结构体开辟的动态空间与存放数组的动态空间之间可能存在内存碎片,这样就减缓了访问速度。
🍉总结
动态内存管理使我们写的程序更加紧凑,空间的利用率更高。合理的动态内存创建和释放可以很大程度提高我们的代码质量。希望本喵在文中对动态内存管理的详细介绍对各位有所帮助。
|