我们已经掌握的内存开辟方式有:
1、int a=10;//在栈空间上开辟四个字节 2、char arr[10]={0};//在栈空间上开辟10个字节的连续空间
上述的开辟空间的方式有两个特点:
动态内存分配的原因:
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知 道,那数组的编译时开辟空间的方式就不能满足了。
动态内存分配的意义:
C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。
首先给大家介绍一下C语言提供的一个动态内存开辟的函数:
void* malloc (size_t size);
返回值: 指向要申请空间的指针,由于不确定要给什么类型的数据开辟空间,所以这里指针类型是void*类型,具体在使用的时候由用户自己来确定。
函数参数:size_t类型的参数由于开辟的空间大小不可能是负数,所以这里用size_t类型(无符号整型),size为要开辟空间的大小,例如:我要开辟10个字节的大小,函数参数就可以表示为10*sizeof(int)。
功能:函数可向堆区申请一片连续可用的空间。
注意事项:
再来给大家介绍一个C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:
void free (void* ptr);
返回值:无。
函数参数:指向动态内存开辟的那片空间的指针。
功能:释放动态开辟的内存。
注意事项:
给大家举个例子:
#include<stdio.h>
#include<stdlib.h>
//malloc
int main()
{
//申请20个字节空间,存放5个整型
int* p = (int*)malloc(5 * sizeof(int));
//判断返回值
//开辟失败
if (p == NULL)
{
perror("malloc");
return 1;
}
//开辟成功
//使用这块空间
int i = 0;
for (i = 0; i < 5; i++)
{
*(p+i) = i + 1;
}
for (i = 0; i < 5; i++)
{
printf("%d\n", p[i]);
}
//当不再使用这块空间,就主动还回去
free(p);//p必须是上面原来开闭空间所用到的指针,如果是别的指针,没反应
p = NULL;//释放过后必须置为空,要不就变成了野指针
return 0;
}运行结果:

C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。原型如下:
void* calloc (size_t num, size_t size);
返回值: 指向要动态内存开辟的那片空间。
函数参数:num:为个数 size:为大小。
例如:我要开辟10个整形大小的空间函数参数为(10 , sizeof(int))
功能:为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0
malloc函数与calloc函数的比较:
与函数 malloc 的区别在于 calloc 会在返回地址之前把申请的空间的每个字节全部初始化为0。
例子:
//calloc
int main()
{
//int* p = (int*)malloc(10 * sizeof(int));
int* p=(int*)calloc(10, sizeof(int));//会初始化为0
//caolloc=malloc+memset
//判断是否开辟成功
if(p==NULL)
{
perror("calloc");
return 1;
}
//开辟成功后使用
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d\n", p[i]);
}
//释放
free(p);
p=NULL;
return 0;
}运行结果:

结论: 如果我们要动态内存开辟一片空间并且要全部初始化为0,我们就可以使用calloc函数来完成,如果不想要全部初始化为0,那么就用malloc函数。
void* realloc (void* ptr, size_t size);
返回值: 调整过后空间的起始地址
函数参数:ptr:要调整空间的地址,size:要调整空间新的大小
功能:有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
注意事项:这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
情况一:当是情况1的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
情况二:当是情况2的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找⼀个合适大小的连续空间来使用。这样函数返回的是⼀个新的内存地址。
如果大家还没有理解的话,这里我给大家画个图更直观的感受一下:

运行代码:由于不容易看到这两种情况使用情况一我们用X86环境,情况二用X64环境
//realloc
int main()
{
//开辟空间,存放1-5
int* p = (int*)malloc(5 * sizeof(int));
//开辟失败
if (p == NULL)
{
perror("malloc");
return 1;
}
//开辟成功
//使用
int i = 0;
for (i = 0; i < 5; i++)
{
p[i] = i + 1;
}
//继续存放6-10,需要增容
int* p2 = realloc(p, 10 * sizeof(int));
if (p2 == NULL)
{
perror("realloc");
free(p);
p = NULL;
return 1;
}
p = p2;
for (i = 5; i < 10; i++)
{
p[i] = i + 1;
}
free(p);
p = NULL;
return 0;
}图一: 这种情况为情况一,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化,我们也可以观察到二者地址相同。

图二: 这种情况为情况二,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找⼀个合适大小的连续空间来使用。这样函数返回的是⼀个新的内存地址。我们也可以观察到二者地址不相同。

#include<stdio.h>
#include<stdlib.h>
int main()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
return 0;
}解释:这里开辟空间后,我们没有判断开辟空间是否成功就对p进行解引用操作,如果这里开辟失败,p就是个空指针,对空指针解引用操作就会报错。
改进:在使用之前,要先对malloc,realloc,calloc这种函数的返回值进行判断,如果为NULL,将它的错误信息打印出来,并直接返回,避免了对空指针的使用。
//2.对动态开辟内存空间的越界访问
int main()
{
int* p = (int*)malloc(5 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i <= 5; i++)
{
p[i] = i + 1;//当i=5就越界了
}
return 0;
}解释: 这里我们动态内存开辟了5个整型的空间,但我们在使用的时候i=5的时候越界了,这样就会导致错误
//3.对非动态开辟内存使用free释放
int main()
{
int arr[] = { 1,2,3,4 };
int* p = arr;
//使用
//.....
free(p);//只能释放malloc,calloc,realloc申请的空间
return 0;
}解释: free只能释放malloc,realloc,calloc函数在堆区申请的空间,栈区上的空间会自己销毁的。
//4. 使用free释放一块动态开辟内存的一部分
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i < 5; i++)
{
/**p = i + 1;
p++;*/ //err
*(p + i) = i + 1;//正确的
}
//free(p);//free释放空间时,必须要给这块空间的起始地址,不能给中间地址
p = NULL;
return 0;
}解释: 在释放p时,我们要看p的地址是否为起始地址,这里我们使用p++的情况,指针就会往后走,从而导致指针指向的不是起始位置。
改进:可以使用*(p+i),这样就不会导致p指针往后走,p指向的一直是起始地址.
//5.对同一块内存多次释放
int* test()
{
int* p = (int*)malloc(5 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
free(p);
}
int main()
{
int*ptr=test();
//....
free(ptr);//二次释放
return 0;
}解释: 对同一块动态内存开辟的空间多次释放也会报错,所以要避免。
//6.动态开辟内存忘记释放(内存泄露)
int* test()
{
int* p = (int*)malloc(5 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
}
int main()
{
test();
//....
return 0;
}注意:
//经典笔试题一
#include<string.h>
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}运行结果:

解析: 1. 程序崩溃 原因是: 1.str传递给GetMemory函数的时候,采用的值传递形参变量p其实是str的一份拷贝 当我们把malloc申请的空间的起始地址存放在p中时不会修改str,str依然为NULL所以当GetMemory函数返回后,再去调用strcpy函数需要将“hello world”拷贝到str指向的空间时,程序崩溃。 2. 因为malloc申请的空间没有free释放,存在内存泄露
修改后:
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
//释放
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}//经典笔试题二
char* GetMemory(void)
{
char p[] = "hello world";
return p;//出了函数就销毁了
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}运行结果:

解析: p是数组名,也就是首元素地址,也就是h的地址,p是临时变量,是在栈区中存储的,而p出来GetMemory()函数就会销毁,地址中存放的就不一定是原来数据了,所以就会打印出来随机值 改进: 因为像malloc、calloc、realloc这类在堆区开辟空间的函数,我们就可以在变量前加static来修饰变量,即把存储位置改为静态区,这样就不会导致临时变量出了函数销毁的情况了
改进:
//改进
char* GetMemory(void)
{
static char p[] = "hello world";//存放在静态区了
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}//经典笔试题三
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);//没有释放
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
int main()
{
Test();
return 0;
}运行结果:

解析: 虽然这里可以正常打印出hello但是我们可以细心的发现str使用过后没有释放
改进:
//改进
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);//没有释放
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}//经典笔试题四
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);//成为野指针
if (str != NULL)
{
strcpy(str, "world");//非法访问内存
printf(str);
}
}
int main()
{
Test();
return 0;
}打印结果:

解析: 1.这里str使用过后释放,但是没有将str置为NULL,所以这时的str就是野指针。 2.calloc函数开辟空间后,也没有进行判断str是否为空
改进:
//改进
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
str = NULL;
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}改进后的打印结果:

解释:释放后置为NULL,判断是否为NULL,不进行srtcpy,所以什么也不打印
也许你从来没有听说过柔性数组这个概念,但是它确实是存在的。
接下来我给大家解释一下柔性数组的概念:C99中,结构中的最后⼀个元素允许是未知大小的数组,这就叫做柔性数组成员。
给大家举个最简单的例子:
struct s { int i; char c; int a[];//柔性数组前面至少要有1个成员 };
给大家看一段代码:
#include<stdio.h>
struct s
{
int i;
char c;
int a[];//柔性数组前面至少要有1个成员
};
int main()
{
printf("%zu", sizeof(struct s));//8---不计算柔性数组
return 0;
}
解释: sizeof返回的这种结构大小不包括柔性数组的内存。
#include<stdlib.h>
struct S
{
int n;
int arr[];//柔性数组
};
int main()
{
struct S*ps=(struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
if (ps == NULL)
{
perror("malloc");
return 1;
}
//使用
ps->n = 100;
int i = 0;
for (i = 0; i < 5; i++)
{
ps->arr[i] = i + 1;
}
//调整
struct S*tmp=realloc(ps, sizeof(struct S) + 10 * sizeof(int));
ps = tmp;
if (tmp == NULL)
{
perror("realloc");
return 1;
}
//释放
free(ps);
ps = NULL;
return 0;
}解释:创建一个struct S的结构体,结构体成员包含了一个整型n和一个整型数组arr,malloc为结构体动态内存开辟空间,首先开辟一个整型n的大小的空间,再创建柔性数组的空间大小,然后判断开辟是否成功,把n的值赋为100,用for循环,将柔性数组的元素分别赋为1,2,3,4,5,再用realloc函数将结构体中柔性数组的空间扩大为10个整型,然后再进行判断.
上面的代码也可以用另外一种方法实现:
struct S
{
int n;
int* arr;
};
int main()
{
struct S*ps=(struct S*)malloc(sizeof(struct S));
if (ps == NULL)
{
perror("malloc");
return 1;
}
ps->n = 100;
int* ptr = (int*)malloc(5 * sizeof(int));
if (ptr == NULL)
{
perror("malloc2");
return 1;
}
ps->arr = ptr;
int i = 0;
for (i = 0; i < 5; i++)
{
ps->arr[i] = i + 1;
}
//空间不够,扩容
ptr = realloc(ps->arr, 10 * sizeof(int));
if (ptr == NULL)
{
perror("realloc");
return 1;
}
//释放两次
free(ps->arr);
free(ps);
ps = NULL;
return 0;
}第一个好处是:方便内存释放 如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。 第二个好处是:这样有利于访问速度. 连续的内存有益于提高访问速度,也有益于减少内存碎片。其实,这样觉得也没多高了,反正你跑不了要用做偏移量的加法来寻址)

总结:这篇文章给大家总结了malloc、calloc、realloc、free函数的使用以及注意事项,同时给大家总结了常见的动态内存的错误,以及动态内存经典笔试题和柔性数组的概念。
如果这篇文章让大家对动态内存管理这方面知识有扩展和新的认识,希望大家给博主一键三连,你们的支持是我最大的动力,感谢大家的支持🌹🌹🌹