文章目录
前言
我们知道数据的存储有静态存储和动态存储两种方式,各有各的优点,前面我们所学的都是静态存储,今天我们来学习一下如何进行动态的存储。
一、动态内存分配的意义
我们知道,我们定义一个变量或数组的时候,是在栈区或者静态区开辟内存空间的。
#include<stdio.h>
int num = 100;//全局变量:静态区
int arr[20];
static int st = 1;//静态全局变量:静态区
int main()
{
int count = 0;//局部变量:栈区
int a[10];
static se = 10;//静态局部变量:静态区
}
全局变量和静态变量是在静态区开辟空间的 ;
局部变量和函数的形参是在栈区开辟空间的;
栈区(stack): 在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
堆区(heap): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分
配方式类似于链表。
数据段(静态区) (static): 存放全局变量、静态数据。程序结束后由系统释放。
普通的局部变量存储在栈区,出了作用域就销毁;
但是被static修饰的变量存储在静态区(数据段),生命周期变长,直到程序结束才销毁。
比如:
char ch = 'a'; //栈上开辟1个字节空间
int arr[100] = { 0 };//栈上开辟40个字节的连续空间
可以发现这种开辟空间的方式有两个特点:
1.空间开辟的大小是固定的;
2.数组在定义的时候必须指定数组的长度,他所需要的内存在编译时分配。
这样的话,如果我们arr数组只有10个数据要存储,剩下的90个整型空间就浪费掉了;如果有200个数据要存储,那么数组的空间又不够;不仅如此,有时候我们需要的空间大小在程序运行的时候才能知道;这些情况下,我们就需要用到动态内存开辟。
动态内存开辟是在程序执行的过程中根据程序的需要 动态地分配或者回收存储空间。
动态开辟是在堆区进行的。
二、动态内存函数
以下这几个动态内存函数的头文件都是 stdlib.h
1.malloc
void* malloc(size_t size);
功能:向内存申请一块连续可用的空间,并返回指向这块空间的指针。
注意:
1.空间开辟成功,则返回一个指向这块空间的指针;
2.空间开辟失败,则返回一个空指针NULL;
3.返回值的类型是void*, 程序员自己决定返回类型;
4.如果参数size为0,这种情况是C语言没有定义的,能不能开辟成功取决于编译器。
malloc函数用法会和free函数一起介绍
2.free
void free(void* ptr);
功能:用来释放动态开辟的内存空间(与三种动态开辟函数搭配使用)。
注意:
1.如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的;
2.如果参数 ptr 是NULL指针,则函数什么事都不做;
3.动态开辟的空间使用完后记得使用free释放掉,还给操作系统;
4.释放完动态开辟的内存空间后,ptr一定要置NULL。这是因为free之后那块开辟的空间已经释放掉不存在了,但ptr仍指向那块空间,所以手动置NULL防止后续非法访问。
malloc和free的搭配使用:
#include<stdio.h>
#include<stdlib.h>
int main()
{
//int arr[10] //栈上开辟10个整型空间
int* p = (int*)malloc(10 * sizeof(int));//堆上开辟10个整型空间
if (NULL == p)
{
perror("malloc");//打印错误信息
return 1;//直接返回,后面程序不再执行
}
for (int i = 0; i < 10; i++)
p[i] = i;
for (int i = 0; i < 10; i++)
printf("%d ", p[i]);
free(p);//释放空间
p = NULL;//手动置为空指针
return 0;
}
3.calloc
void *calloc(size_t num, size_t size);
功能:为 num 个大小为 size 的元素开辟一块空间,并把空间的每个字节初始化为 0,并返回一个指向这块空间的指针。
与malloc相比calloc多了一个参数,会把申请空间的每个字节初始化为0;
calloc分配内存空间失败也会返回一个空指针NULL。
int* p1 = (int*)malloc(10 * sizeof(int));//开辟40个空间
int* p2 = (int*)calloc(10, sizeof(int));//开辟10个大小为int的空间也即40个空间
calloc用法:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (NULL == p)
{
perror("calloc");//打印错误信息
return 1;//直接返回,后面程序不再执行
}
for (int i = 0; i < 10; i++)
p[i] = i;
for (int i = 0; i < 10; i++)
printf("%d ", p[i]);
free(p);//释放空间
p = NULL;//手动置为空指针
return 0;
}
4.realloc
void* realloc (void* ptr, size_t size);
功能:使得动态内存管理更加灵活。用于重新调整之前调用 malloc 或 calloc 所分配的 ptr 所指向的内存块的大小,可以对动态开辟的内存进行大小的调整。
注意:
1.ptr 为指针要调整的内存地址。
2.size 为调整之后的新大小。
3.返回值为调整之后的内存起始位置,请求失败则返回空指针。
4.realloc 函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
realloc
函数在调整内存空间的时候存在三种情况:
1.原有空间的后面有足够大的空间,会直接在原有空间之后直接追加空间,原来空间的数据不发生变化。
2.原有空间的后面没有足够的空间,会在在堆空间上另找一个合适大小的连续空间来使用,并且返回一个指向这块新空间的地址。
3.开辟空间失败则返回空指针NULL。
realloc用法:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* arr = (int*)calloc(10, sizeof(int));//申请10个整型空间,并初始化0
if (NULL == arr)
{
perror("calloc");//打印错误信息
return 1;//直接返回,后面程序不再执行
}
int* p = (int*)realloc(arr, 50 * sizeof(int));//重新调整为50个整型空间
if (NULL == p)
{
perror("realloc");
return 1;
}
arr = p;//如果开辟成功,arr指向新开辟的空间,arr之前保存的内容不变
/*
对arr进行操作
*/
free(arr);//释放空间
arr = NULL;//手动置为空指针
return 0;
}
三、常见的动态内存错误
1.对NULL指针的解引用操作
动态内存开辟之后先判断是否为NULL再使用,否则会出现问题
int* p = (int*)malloc(100000000000);
*p = 10;//如果p开辟失败是NULL呢,所以先判断再使用,像上面一样
解决方法: 对动态开辟空间的函数的返回值进行判断
2.对动态开辟空间的越界访问
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (NULL == p)
{
perror("calloc");
return 1;
}
for (int i = 0; i < 20; i++)//p只开辟了10个整型空间,无法访问20个
p[i] = i;
free(p);
p = NULL;
return 0;
}
解决方法: 对边界进行检查,不要越界访问不属于自己开辟的空间
3.对非动态开辟内存使用free释放
int main()
{
int a = 100;
int* p = &a;
free(p);
p = NULL;
return 0;
}
发生错误:
4.使用free释放一块动态开辟内存的一部分
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (NULL == p)
{
perror("calloc");
return 1;
}
for (int i = 0; i < 10; i++)
{
*p = i;
p++;//p不再指向空间的起始位置
}
free(p);//内存泄露
p = NULL;
return 0;
}
p不再指向空间起始位置,此时free掉p只释放了p后面的位置,空间不能完全释放,会造成内存泄露。
发生错误:
解决方法: 创建临时变量保存空间的起始位置或者不改变p所指向的位置。
5.对同一块动态内存多次释放
当我们写代码的时候可能忘了这块空间已经释放,就有可能后面会重复释放。
程序崩溃:
程序正常:
解决方法: 空间释放完及时置空NULL,这样即使后面再次释放,free函数什么也不会做。
6.动态开辟内存忘记释放导致内存泄漏
#include<stdio.h>
#include<stdlib.h>
void test()
{
int* p = (int*)malloc(100);
if (NULL == p)
{
return 0;
}
for (int i = 0; i < 10; i++)
p[i] = i;
}
int main()
{
test();
return 0;
}
如果在函数内部开辟空间,没有及时释放,则后面也无法再释放,因为出了函数之后p销毁,找不到开辟空间的起始地址,想释放也释放不了,造成内存泄漏。同样在主函数内部忘记释放也会造成内存泄露。
内存泄漏: 指的是在程序运行过程中,分配的内存空间没有被正确释放或回收的现象。
内存泄漏的危害:当程序中存在内存泄漏时,每次执行该部分代码都会分配新的内存空间,但是却没有释放之前分配的内存空间,导致程序使用的内存空间不断增加,最终可能导致内存耗尽,程序崩溃或运行变慢。
解决方法: 动态开辟空间使用完之后一定要及时free释放。
总结:
1.根据需要开辟空间,避免空间浪费或者空间不足;
2.动态内存函数malloc、calloc、realloc一定要和free搭配使用,避免空间泄露;
3.空间开辟完一定要先判断再使用,避免对空指针解引用;
4.空间free完一定要置空,避免再次使用导致程序崩溃。
四、动态内存开辟练习题
练习一:
#include<stdio.h>
#include<stdlib.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.str是以值传递的方式传参(值传递并不会影响实参),p是str的临时拷贝,有自己独立的空间,GetMemory函数内部申请的空间放在p中,str仍是NULL,所以strcpy函数使用的时候造成了非法访问,程序会崩溃。
2.GetMemory函数内部申请开辟了空间,但没有及时释放,造成内存泄漏。
3.没有对开辟的空间地址进行判断,容易对空指针解引用。
改正:
将值传递改为址传递,参数用二级指针接收;空间开辟完判断是否为NULL;空间使用完及时释放。
#include<stdio.h>
#include<stdlib.h>
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void test(void)
{
char* str = NULL;
GetMemory(&str);
if (str == NULL)
{
printf("空间开辟失败\n");
return 1;
}
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
test();
return 0;
}
练习二:
#include<stdio.h>
#include<stdlib.h>
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void test(void)
{
char* str = NULL;
str = GetMemory();
if (str == NULL)
{
printf("空间开辟失败\n");
return 1;
}
printf(str);
}
int main()
{
test();
return 0;
}
结果:非法访问内存,打印乱码
结果分析:
1.局部变量p出了函数自动销毁,p所指空间的可能被使用,也可能没有被使用,我们不知道,所以会打印乱码随机值;
2.p不是动态开辟的空间,所以不用free。
改正:
只需修改GetMemory
函数即可,使用static修饰变量或者常量字符串即可,这两种都不会随着函数返回而销毁;
char* GetMemory(void)
{
//static char p[] = "hello world";//p存放在静态区
char* p = "hello world";//常量字符串也存放在静态区
return p;
}
练习三:
#include<stdio.h>
#include<stdlib.h>
void test(void)
{
char* str = (char*)malloc(100);
if (str == NULL)
{
printf("空间开辟失败\n");
return 1;
}
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
test();
return 0;
}
结果:打印world,但是造成非法访问
结果分析:
str指向的空间已经释放掉还给操作系统了,我们没有权限继续访问,但是str没有置空是一个野指针,虽然程序可以正常运行打印结果,但是这样的写法是错误的,会造成非法访问内存。
五、柔性数组
柔性数组的定义和特点
柔性数组是C99 引入的一个新特性,即结构体中的最后一个元素可以是未知大小的数组,并且要求这样的结构体至少包含一个其他类型的成员。
struct S
{
int n;
char str[];//或者char str[0](部分编译器会报错) 都表示数组大小是未知的
};
printf("%d\n", sizeof(struct S));//输出4
柔性数组的特点
1.结构中的柔性数组成员前面必须至少一个其他成员。
2.柔性数组存在于结构体内,但是不占结构体的大小。
3.包含柔性数组成员的结构用malloc() 函数进行内存的动态分配,并且分配的内存应该大于结构的大 小,以适应柔性数组的预期大小。
柔性数组的使用和优点
在堆上给结构体分配内存,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小。
#include<stdio.h>
#include<stdlib.h>
struct S
{
int n;
char str[];
};
int main()
{
//柔性数组str获得了20个char的空间
struct S* p = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(char));
if (NULL == p)
{
printf("malloc fail\n");
return 1;
}
p->n = 250;
for (int i = 0; i < 20; i++)
{
p->str[i] = 'a' + i;
}
free(p);
return 0;
}
对比上下两段代码
struct S
{
int n;
char* str;//这里不一样!!!
};
int main()
{
struct S* p = (struct S*)malloc(sizeof(struct S));
if (NULL == p)
{
printf("malloc p fail\n");
return 1;
}
p->n = 250;
p->str = (char*)malloc(20 * sizeof(char));
if (NULL == p->str)
{
printf("malloc p->str fail\n");
return 1;
}
for (int i = 0; i < 20; i++)
{
p->str[i] = 'a' + i;
}
//释放顺序千万不要搞反,先释放结构体成员,再释放结构体
free(p->str);
p->str = NULL;
free(p);
p = NULL;
return 0;
}
这两段代码实现的功能是一样的,但是有些不同之处,第一段代码一次性分配足够空间;第二段代码是先给结构体分配空间,再给结构体成员分配空间,这两块空间并不是连续的,并且最后需要free两次,置空两次。
第一段代码更好,优点如下:
1.方便内存释放。代码一只需要释放一次,而代码二需要释放两次,并且结构体成员容易忘记释放。
2.访问速度更快。连续的内存有益于提高访问速度,也有益于减少内存碎片。动态开辟空间的次数越多,产生的内存碎片就越多,就会降低内存的使用率。
关于动态内存管理和柔性数组的内容总结到这里就结束了,就感谢您的观看和支持!
后面我会基于动态内存再写一个通讯录小程序,希望大家多多支持!