一、为什么存在动态内存分配
我们常见的开辟空间方式有:
int a = 3;//在栈上开辟4个字节的空间
char b[10] = { 0 };//在栈上开辟10个字节的空间
上面的开辟空间方式有两个特点:
①开辟的空间是固定的。
②在定义数组时,需要指定数组的大小,它所需要的内存在编译时分配。
以上的开辟空间方法在一些情况下无法满足我们的要求。比如说,我们不知道数组需要多大,需要用户输入之后才能确定大小。也就是说在程序运行时才能确定要分配的空间大小。此时用数组的定义无法实现。我们就需要用到动态内存分配的知识了。
二、动态内存函数的介绍
以下4个函数都需要引用头文件#include<stdlib.h>
1、malloc函数与free函数
以下是malloc函数的声明:
①size:是一个无符号整数,代表的是所需要开辟内存的大小,单位是字节。
②malloc函数会在堆上开辟一块size字节大小的空间,然后返回指向这块空间的指针,如果过开辟空间失败,则会返回NULL。
③如果size的大小为0,这是标准未定义的行为,实现情况根据编译器的不同而不同,可能会报错。
以下是free函数的声明:
①ptr:是一个指针,指向需要释放的内存。
②free函数能够释放动态开辟的内存。动态开辟的内存不需要时一定要用free函数释放,否则可能会出现内存泄漏等问题
③如果ptr是空指针,则什么都不会发生。
④如果ptr指向的空间不是动态开辟的,这种行为是标准未定义的,实现情况根据编译器的不同而不同,可能会报错。
下面是malloc函数与free函数的实例:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* ptr = (int*) malloc(40);//在堆上动态开辟40个字节的空间
if (ptr == NULL)//如果ptr接收的是空指针,则开辟失败
{
perror("空间开辟失败,原因是:");//提示动态内存开辟失败并打印原因
return 0;//结束
}
for (int i = 0;i < 10;i++)
{
ptr[i] = i;//用动态开辟的空间存放数据
printf("%d ", ptr[i]);
}
free(ptr);//释放空间
return 0;
}
代码运行结果如下:
如果内存开辟失败:
2、calloc函数
calloc函数的声明如下:
①nums:要被分配元素的个数。
②size:元素的大小,单位是字节。
③calloc函数也是能够在堆上开辟num个大小为size的空间,与malloc函数不同的是,它会把空间的每个字节初始化为0。
④返回值是一个指向开辟空间的指针。
实例演示如下:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* pa = (int*) malloc(40);
int* pb = (int*) calloc(10, 4);//calloc函数的使用
if ((pa == NULL) || (pb == NULL))
{
perror("空间开辟失败,原因是:");
return 0;
}
//在初始化的情况下打印两块空间的数据
printf("malloc函数开辟的内存未初始化时:\n");
for (int i = 0;i < 10;i++)
{
printf("%d ",pa[i]);
}
printf("\n");
printf("calloc函数开辟的空间未初始化时:\n");
for (int i = 0;i < 10;i++)
{
printf("%d ", pb[i]);
}
free(pa);
free(pb);
return 0;
}
运行结果如下:
3、realloc函数
realloc函数的声明如下:
①realloc函数能够重新调整之前用malloc函数或者calloc函数动态开辟的空间,并返回指向调整后空间的指针,如果调整失败,返回NULL。
②ptr:指向需要调整的空间的指针。
③size:调整后空间的大小,单位是字节。
③该函数在调整空间大小的同时,也会把原来空间中的数据移动到新的内存。
实例演示如下:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* ptr = (int*) calloc(10, 4);//动态开辟10*4个字节大小的空间。
if (ptr == NULL)
{
perror("calloc:Error:");
return 0;
}
//将原本40个字节的空间,调整为100个字节。
//int *ptr= realloc(ptr, 100);
//如果调整失败,realloc函数返回NULL,ptr原本指向的空间就找不到了。
int* p = (int*) realloc(ptr, 100);
//我们先设置一个中间指针,p不为空指针时才把它赋给ptr。
if (p == NULL)
{
perror("raelloc:Error:");
return 0;
}
ptr = p;//调整空间成功
p = NULL;
free(ptr);
ptr = NULL;
return 0;
}
realloc函数在调整空间时可能会出现以下情况:
①原本空间之后有足够的空间,就在原本的空间后追加空间,原空间的数据不发生变化。
②原本空间之后没有足够的空间,就会在堆上另找一处开辟一块合适的空间,并把原本空间的数据转移过去。
三、动态内存的常见错误
1、对NULL指针的解引用操作
int main()
{
int* p = (int*) malloc(40);
*p = 30;//如果p的值是NULL,就会出问题
//所以,最好对malloc函数的返回值进行判断
free(p);
p=NULL;
return 0;
}
2、对动态开辟空间的越界访问
int main()
{
int* p = (int*) maloc(40);
if (p == NULL)
{
perror("malloc:Error:");
return 1;
}
for (int i = 0;i <= 10;i++)
{
p[i] = i;//当i=10时越界访问了
}
free(p);
p = NULL;
return 0;
}
3、对非动态开辟的空间用free函数释放
int main()
{
int a = 11;
free(&a);
return 0;
}
4、对动态开辟空间的一半部分进行释放
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc:Error:");
return 1;
}
for (int i = 0;i < 5;i++)
{
*p++ = i;//在循环的过程中p指向的位置不断移动
}
free(p);//此时p指向的不是动态开辟空间的起始位置,发生错误
p = NULL;
return 0;
}
5、对同一块内存的多次释放
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc:Error:");
return 1;
}
free(p);//第一次释放
//p=NULL;
//如果在第一次释放后将p置为空指针,则不会发生错误
free(p);//再次的释放,发生错误
p = NULL;
return 0;
}
6、动态开辟的内存忘记释放(内存泄漏)
动态开辟的空间,如果不需要使用了,需要用free函数释放。
如果不释放,在程序结束后,也会由操作系统回收。
但是如果一个程序需要长时间运行,此时如果不使用free函数释放,操作系统无法回收,就会出现内存泄漏。通俗地说,就是这块内存你不用,也不还,别人也用不了,这块空间就相当于浪费掉了。
void test()
{
int* p = (int*)malloc(40);
if (p != NULL)
{
*p = 20;
}
//没有释放内存
}
int main()
{
test();
while (1);
//程序一直运行,出现内存泄漏
return 0;
}
四、关于动态内存的题目
题目一
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函数,然后定义一个指针str,把str传给GetMemory函数。注意,此时传的是值,而不是地址。是直接把str传过去,而不是把str的地址传过去。所以p就是str的一份临时拷贝,p指向动态开辟的空间,函数结束后,p被销毁。str也不会有任何改变,依旧是一个空指针。strcpy函数调用失败,程序崩溃。所以最后什么也不会打印。
同时还有一个问题,p被销毁后,没有任何指针指向动态开辟的空间,这也就意味着我们无法主动释放动态开辟的空间,可能会出现内存泄漏。
对这段代码稍加修改:
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;
}
运行结果如下:
题目二
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
运行结果如下:
首先进入Test函数,定义一个字符指针str,进入GetMemory函数,定义一个字符串p,存放“hello world\0”,然后返回p的地址,由str接收。此时注意,函数结束后,函数里的数据会被销毁,空间返回操作系统。也就是说,str虽然接收到地址,但是“hello world\0”,已经被销毁了,str就是一个野指针,str指向的空间可能用来存放别的数据了,打印的结果不可预测。
题目三
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test()
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
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;
}
str指向的空间被释放后,str没有被置为空指针,此时的str就是野指针,此时再用strcpy函数拷贝给str就会非法访问。
五、柔性数组
C99中,结构体的最后一个成员允许是未知大小的数组,这就叫做柔性数组成员。
例如:
struct S
{
int a;
int b;
char c[];//这就是柔性数组成员
};
1、柔性数组的特点
①结构体柔性数组成员前面至少有一个其他成员。
②sizeof返回这种结构体的大小不包括柔性数组的内存
③包含柔性数组成员的结构体应该用malloc函数进行动态内存分配,并且分配的内存大小应该大于结构体大小,以适应柔性数组的预期大小。、
struct S
{
int a;
int b;
char c[];//这就是柔性数组
//char c[0] 这样写也行
};
int main()
{
printf("%d",(int)sizeof(struct S));//打印结果应当是8
return 0;
}
2、柔性数组的使用
#include<stdio.h>
#include<stdlib.h>
struct S
{
int a;
int b;
char c[];//这就是柔性数组
};
int main()
{
// 8个字节,给a,b用 10个字节,给c用
struct S* p = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(char));
if (p == NULL)
{
perror("malloc:Error:");
return 0;
}
p->a = 10;
p->b = 20;
for (int i = 0;i < 10;i++)
{
p->c[i] = i + 65;//使用柔性数组
}
for (int i = 0;i < 10;i++)
{
printf("%c ", p->c[i]);//打印出来看看
}
free(p);
p = NULL;
return 0;
}