前言
本篇博客为大家介绍C语言中又一重要的内容------动态内存管理,这一部分的内容在后面学习C++,包括数据结构的学习中都有大量的运用,所以希望大家可以掌握好这里的内容,如果你对此感兴趣,请继续往下阅读,下面进入正文部分。
1. 为什么要有动态内存分配
目前阶段,我们可能只有以下两种开辟内存的方式:
但是上述的开辟空间的方式有两个特点:
• 空间开辟大小是固定的。
• 数组在申明的时候,必须指定数组的长度,数组空间⼀旦确定了大小不能调整
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。 C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。
2. malloc和free
2.1 malloc
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
return 0;
}
2.2 free
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 5; i++)
{
*(p + i) = i + 1;
}
free(p);
p = NULL;
return 0;
}
这里大家注意看代码,在我们使用完空间后,我们需要将申请的空间返还给操作系统,这里就需要用到free函数,这里要强调的是**free函数的参数是要释放的内存空间的起始地址,**然后在释放完空间后我们需要将p置为空指针(因为p是野指针)。
3.calloc和realloc
3.1 calloc
这里为大家说明一下:
calloc函数的功能是为num个大小为size的字节开辟空间,**并且把空间的每一个字节都初始化为0,**这是与malloc最大的区别,大家要清楚这一点。
3.2 realloc
ptr是要调整的内存的地址;
size是调整后的新大小;
返回值为调整后的内存起始位置;
在realloc函数进行扩容时,可能遇到两种情况,大家来看下面的图'
第一种情况,是原本的空间后面还有足够的尚未分配的空间,这时realloc只需要在原来的基础上加上相应的空间,最后返回该空间的起始地址即可;
第二种情况,在原本空间后面未分配空间不足时,realloc就会在堆区上重新开辟一块儿空间来满足足新的空间大小,将原来空间的数据拷贝到新的空间中;然后释放旧的空间,并返回新的内存空间的起始地址。
#include<stdlib.h>
int main()
{
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;
}
int* ptr = (int*)realloc(p, 40);
if (ptr != NULL)
{
p = ptr;
int i = 0;
for (i = 5; i < 10; i++)
{
*(p + i) = i + 1;
}
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
}
else
{
perror("realloc");
free(p);
p = NULL;
}
return 0;
}
大家注意上面的代码,我们在使用realloc申请空间时。一定要用一个临时指针去接受realloc的返回值,因为一旦realloc申请失败,将返回NULL,这个时候如果我们没有使用临时指针,就会对原本存在的数据造成影响;最后大家要记住,使用完后要释放空间,并置为NULL。
4. 常见的动态内存的错误
4.1 对NULL解引用操作
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
这里大家看到,上面的代码就是对NULL进行了解引用的操作,这个代码是无法运行的。在我们开辟完空间后,一定要进行判断或者进行断言,防止后面对NULL进行操作。
4.2 对动态开辟空间的越界访问
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
exit(EXIT_FAILURE);
}
for(i=0; i<=10; i++)
{
*(p+i) = i;//当i是10的时候越界访问
}
free(p);
}
这里想必大家可以轻松理解,这与我们前面学过的数组越界访问基本上是同理,我们可以将malloc开辟的空间想象成一个数组。
4.3 对非动态开辟内存使用free释放
void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
}
这里也是一个很常见的错误,我们一定要清楚free是释放动态开辟的空间,其他的空间是无法进行释放的,这段代码就是一个典型的错误释放,a是静态变量,存放在栈区;而动态开辟的空间是在堆区。
4.4 使用free释放⼀块动态开辟内存的⼀部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
这里大家要注意,释放空间时,free函数的参数必须是动态内存的起始地址,不能是其他的。
4.5 对同⼀块动态内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
这个就比较好理解了,对同一个空间只能释放一次;想避免这个问题,其实也有办法,就是在我们释放完后,立马将其置为NULL,这样就算后面我们不小心又释放了一次,其实也无伤大雅,因为给free函数传NULL,它什么事都不会做。
4.6 动态开辟内存忘记释放(内存泄漏)
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
忘记释放不再使⽤的动态开辟的空间会造成内存泄漏。
切记:动态开辟的空间⼀定要释放,并且正确释放。
5. 动态内存经典笔试题分析
5.1 题目一
#include<stdio.h>
#include<string.h>
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test()
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}
请问运行Test函数会得到什么结果?
我们来看这段代码在VS中的运行结果;
大家看到,程序是无法运行的;证明上面的代码是存在问题的,那么具体有哪些问题呢?下面我们进行分析。
首先,在使用完开辟的空间后并没有进行释放,可能导致内存泄漏,这是一个问题,但是这不是造成程序崩溃的主要原因。
程序崩溃的主要原因在于这段代码对NULL进行了解引用操作,形成非法访问。为什么会这样呢?大家来看传递给GetMemory函数里面的参数,是str,这里传递是指针变量本身,所以这里就相当于是传值调用,那么前面我们学过,形参是实参的一份临时拷贝,p有自己的空间,只是接受了str的内容,这时候malloc申请的空间放到p中是和str没关系的,所以当程序走出GetMemory函数后,p指向的那块儿空间就找不到了,所以strcpy里的str就是NULL,那么这个时候想把"hello world"拷贝到空指针所指向的空间中,就会发生对空指针解引用的操作。
那么有人会问,这段代码能不能改成正确的呢?答案当然是可以,我们无非就是想在malloc开辟的空间里拷贝题目所给字符串。这个时候我们可以进行传址调用,大家来看下面的代码。
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void Test()
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
大家可以看到这里我们改进了代码,采用了传址调用,注意str本身就是一级指针,所以&str就是二级指针,那么我们就需要用二级指针去接受,对p解引用就可以得到str的地址。
5.2 题目二
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
char* GetMemory()
{
char p[] = "hello world";
return p;
}
void Test()
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
请问这段代码运行会得到什么结果?
我们先给出运行结果;
大家可以发现,这里并不能打印出我们想要的结果,那么为什么会这样呢?下面我们来进行分析;
这里GetMemory里创建了一个字符数组,我们知道其数组名p就代表字符串的首字符地址,所以这里返回的就是字符串首字符地址;那么当程序走出GetMemory函数后,使用str指针去访问p数组就属于非法访问,因为p数组的内存空间已经还给了操作系统,这里的str是野指针。
6. 柔性数组
C99中,结构中的最后⼀个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
这里有两种表示方法:
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;
6.1 柔性数组的特点
• 结构中的柔性数组成员前面必须至少⼀个其他成员。
• sizeof返回的这种结构大小不包括柔性数组的内存。
• 包含柔性数组成员的结构用malloc函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
#include<stdio.h>
typedef struct st_type
{
int n;
int a[];//柔性数组成员
}type_a;
int main()
{
//printf("%zd", sizeof(type_a));
struct st_type* ps=(struct st_type*)malloc(sizeof(type_a) + 5 * sizeof(int));
if (ps == NULL)
{
perror("malloc");
return 1;
}
ps->n = 100;
int i = 0;
for (i = 0; i < 5; i++)
{
ps->a[i] = i;
}
//调整空间
struct st_type* ptr=(struct st_type*)realloc(ps, sizeof(type_a) + 10 * sizeof(int));
if (ptr != NULL)
{
ps = ptr;
}
//...
free(ps);
ps = NULL;
return 0;
}
这里大家可以看到,我们不仅可以用malloc来进行内存的动态分配,还可以用realloc来进行内存分配。而且大家可以发现一个点,realloc可以真正实现柔性数组的"柔",想大就大,想小就小。
6.2 柔性数组的优势
7.总结
本篇文章为大家介绍了C语言中动态内存管理的内容,主要包括重要的四个函数,以及一些关于动态内存常出现的问题,还拓展了柔性数组的内容,这个知识大家作为了解;最后,希望本篇博客可以为大家带来帮助,感谢阅读!