前言
作者:小蜗牛向前冲
名言:我可以接收失败,但我不能接收放弃
如果觉的博主的文章还不错的话,还请 点赞,收藏,关注👀支持博主。如果发现有问题的地方欢迎❀大家在评论区指正。 大家好啊!小蜗牛又来为大家分享新的文章了。不知道大家有没有在使用数组时发现超出空间的情况,这时我们往往要重新定义数组的大小。那么这个数组的空间到底定义多大呢?定小了可能等下数组空间又不够了,定大了可能会存在许多内存空间的浪费。那么有什么更好的办法解决吗?
有的,那就是动态内存分配,下面博主会为大家一一道来。
一为什么存在动态内存分配
我们以往开辟空间的方式:
int a = 0;//在栈区开辟4个字节的空间 int arr[10] = { 0 };//在栈区开辟40个字节连续的空间
特点:
1 开辟的空间大小是固定的。
2数组在声明时,必须指数组大小或者直接初始化数组,它所需要的内存在编译时分配
但有时侯,对于空间的需求在我们写代码的时候是并不知道的,要编译完之后才会知道,这就导致我们可能又要去修改分配内存空间的大小,这是不便的。这时候我们便可以试试用动态内存分配来解决这个问题。
二动态内存函数的介绍
1 malloc函数
C语言提供了一个动态内存开辟的函数:
参数
stze:
内存块的大小,以字节为单位。
size_t是无符号整数类型。
返回值
成功时,指向函数分配的内存块的指针。
此指针的类型始终为 void*,可以将其转换为所需类型的数据指针,以便可取消引用。
如果函数未能分配请求的内存块,则返回空指针。
注意:
malloc函数成功开辟空间返回的是,指向开辟空间的指针。
开辟失败的时候返回的是,一个空指针(NULL),所以在用malloc函数开辟空间时一定要去判断是否能否开辟成功。
返回值是void*,所以在接收用malloc开辟的空间时,要强转为自己需要的类型。
如果参数stez是0,malloc的行为是标准是未定义的,取决于编译器。
2 free函数
C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:
参数
ptr
指向先前使用 malloc、calloc 或 realloc 分配的内存块的指针。
注意:
free函数用来释放动态开辟的内存。
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
如果参数 ptr 是NULL指针,则函数什么事都不做。
alloc和free都声明在 stdlib.h 头文件中。
代码举例:
int main() { //动态内存的开辟 int* ptr= (int *)malloc(40); //判断空间是否开辟成功 int i = 0; if (ptr != NULL) { //使用 for (i = 0;i < 10;i++) { *(ptr + i) = i; } } for (i = 0;i < 10;i++) { printf("%d ", *(ptr+i)); } free(ptr);//回收空间 ptr = NULL;//防止出现野指针 return 0; }
3 calloc函数
C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。原型如下:
作用
分配和零初始化数组
为 num 元素数组分配一个内存块,每个元素的大小都长字节,并将其所有位初始化为零。
有效结果是分配了零初始化的(数字*大小)字节的内存块。
如果 size 为零,则返回值取决于特定的库实现(它可能是也可能不是空指针),但返回的指针不应被取消引用
参数
num
要分配的元素数。
size
每个元素的大小。
size_t 是无符号整数类型。
代码举例:
int main() { int i = 0; scanf("%d", &i);//要分配的元素数 int* data = (int*)calloc(i, sizeof(int)); //判断 if (data == NULL) { perror(data); return 1; } //使用 int j = 0; for (j = 0;j < i;j++) { data[j] = j; printf("%d ", data[j]); } //释放 free(data); data = NULL; return 0; }
首先我来看道calloc函数的第一个功能,能将分配空间中的元素都初始化为0。
其次,否真的分配的内存空间。
所以如何我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。
4 realloc函数
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时 候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小 的调整。
函数原型如下:
功能
更改 ptr 所指向的内存块的大小。
该函数可以将内存块移动到新位置(其地址由函数返回)。
内存块的内容将保留到新旧大小中较小的一个,即使该块被移动到新位置也是如此。如果新大小较大,则新分配部分的值不确定。
如果 ptr 是空指针,则该函数的行为类似于 malloc,分配一个新的大小字节块并返回指向其开头的指针。
参数
ptr
指向先前使用 malloc、calloc 或 realloc 分配的内存块的指针。
或者,这可以是一个空指针,在这种情况下,分配一个新块(就像调用malloc一样)。
size
内存块的新大小,以字节为单位。
size_t是无符号整数类型
realloc函数调整空间后存在二种情况
情况1:原有空间之后有足够大的空间
情况2:原有空间之后没有足够大的空间
对于情况1要扩展的空间直接在原有空间之间后扩展,原空间的数据不发生变化。
对于情况2由于后续空间不足以扩展,所以realloc函数会重新在堆区找足够大一块空间,把原空间的数据放到新空间中,并让ptr重新指向新空间。
代码举例:
int main() { int* ptr = (int*)calloc(5, sizeof(int)); if (ptr != NULL) { int i = 0; for (i = 0;i < 5;i++) { *(ptr + i) = i; printf("%d ", *(ptr + i)); } } else { perror(ptr);//报错信息 } printf("\n"); //增容 int* p = NULL; p = realloc(ptr, 10 * sizeof(int)); printf("增容成功\n"); if (p != NULL) { ptr = p; int i = 0; for (i = 0;i < 10;i++) { *(ptr + i) = i; printf("%d ", *(ptr + i)); } } //释放 free(ptr); ptr = NULL; return 0; }
三 常见的动态内存错误
虽然我们在使用malloc,calloc和realloc开辟动态空间很方便,但也容易引起一些错误。下面我们就一起看看吧。
1 对NULL指针的解引用操作
void test() { int* p = (int*)malloc(INT_MAX); *p = 20;//如果p是NULL,就会有问题 free(p);//释放 }
其中INT_MAX是一个比较大的数,内存分配可能会失败,当分配空间失败时,p被置为NULL,后面对空指针解引用是不可以取的。
我们可以这样避免
void test() { int* p = (int*)malloc(INT_MAX); if (p == NULL) { return 1; } *p = 20;//如果p是NULL,就会有问题 free(p);//释放 }
2 对动态开辟空间的越界访问
void test() { int i = 0; int* p = (int*)malloc(10 * sizeof(int)); if (NULL == p) { perror(p); } for (i = 0; i <= 10; i++) { *(p + i) = i;//当i是10的时候越界访问 } free(p); p = NULL;
这里就告诉我们在写代码还是要多思考。
3对非动态开辟内存使用free释放
void test() { int a = 10; int *p = &a; free(p);//ok? }
这里我们是对非动态空间继续释放,这肯定是不可取的,a变量开辟的空间是在栈区是,而free释放的空间是在堆区。
4使用free释放一块动态开辟内存的一部分
void test() { int *p = (int *)malloc(100); p++; free(p);//p不再指向动态内存的起始位置 }
这里程序会崩溃。
5 对同一块动态内存多次释放
void test() { int *p = (int *)malloc(100); free(p); free(p);//重复释放 }
哈哈,别认为这不存在噢,当我们写代码写多了,会有的噢。
6动态开辟内存忘记释放(内存泄漏)
void test() { int *p = (int *)malloc(100); if(NULL != p) { *p = 20; } } int main() { test(); while(1); }
忘记释放不再使用的动态开辟的空间会造成内存泄漏。
切记: 动态开辟的空间一定要释放,并且正确释放 。
四几个经典的笔试题
学完上面的知识点,我们来实战在训练一下。
题目1:
void GetMemory(char *p) { p = (char *)malloc(100); } void Test(void) { char *str = NULL; GetMemory(str); strcpy(str, "hello world"); printf(str); }
请问运行Test 函数会有什么样的结果?
我们发现什么都没打印,为什么呢?
其中在于p变量是个形参,出函数就会销毁,那么分配的空间就找不到了,使用str并没有分到内存空间,就无法完成拷贝。
其实我们稍作改动将可以完成代码的实现,将传值调用改为传址调用就可以了。
题目2:
char *GetMemory(void) { char p[] = "hello world"; return p; } void Test(void) { char *str = NULL; str = GetMemory(); printf(str); }
请问运行Test 函数会有什么样的结果?
我们发现打印出来了随机值,为什么呢?
这是因为函数使用完后就会销毁,p其实是个野指针,p指向的空间在出函数后就会被回收。
题目3:
void GetMemory(char **p, int num) { *p = (char *)malloc(num); } void Test(void) { char *str = NULL; GetMemory(&str, 100); strcpy(str, "hello"); printf(str); }
请问运行Test 函数会有什么样的结果?
虽然结果是对的,但大家发现没malloc函数开辟的空间并没有被回收,这会造成内存空间的泄漏,所以我们在使用完动态函数开辟的空间后一定要回收内存。
题目4:
void Test(void) { char *str = (char *) malloc(100); strcpy(str, "hello"); free(str); if(str != NULL) { strcpy(str, "world"); printf(str); } }
请问运行Test 函数会有什么样的结果?
怎么会是world呢?不少会一疑问,str空间不是被free释放吗?
其实空间虽然是释放了,但str仍然记得那块空间的地址,我们*还是能找到那块空间,但是那块空间已经是不属于我们了,这时str相当于野指针是相当危险的。为了避免这种错误的结果出现,我们最好在free释放了str所指向的空间时,在将str置为空指针(str=NULL).
五 C/C++程序的内存开辟
这里简单和大家分享一下,C/C++程序的内存开辟。
C/C++程序内存分配的几个区域:
1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分 配方式类似于链表。
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
六 柔性数组
也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。 C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
下面就是柔性数组:
typedef struct s { int a; int arr[];//柔性数组 }s;
1 柔性数组的特点
结构中的柔性数组成员前面必须至少有一个其他成员。
sizeof 返回的这种结构大小不包括柔性数组的内存。
包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
我们可以看到,这个结构体的大小是不包含柔性数组的大小的。
2 柔性数组的使用
//代码1 typedef struct s { int a; int arr[];//柔性数组 }s; int main() { //代码1 int i = 0; s* p = (s*)malloc(sizeof(s) + 100 * sizeof(int));//为结构体和柔性数组开辟空间 //业务处理 p->a = 100; for (i = 0; i < 100; i++) { p->arr[i] = i; } free(p); p = NULL; return 0; }
用malloc为柔性数组开辟的100的连续的空间。
3 柔性数组的优势
下面我们在另外一个方式,实现代码1的结果。
//代码2 struct s { int n; int* arr; }; int main() { struct s* p = (struct s*)malloc(sizeof(struct s));//为结构体开辟空间 if (p == NULL) { return 1; } p->n = 100; p->arr = (int*)malloc(40);//为arr指针开辟空间 if (p->arr == NULL) { perror("");//报错信息 return 1; } //使用 int i = 0; for (i = 0;i < 10;i++) { p->arr[i] = i; } //空间释放 free(p->arr); free(p); p = NULL; }
上述 代码1 和 代码2 可以完成同样的功能,但是 方法1 的实现有两个好处:
第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好 了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎片。
总结
在这篇博客中,我们主要学习了动态内存函数和柔性数组,对于动态内存函数我们主要还是要注意,使用后一定要记得用free释放空间,柔性数组的优点方便内存释放和提高访问速度。
最后送大家于自己一句话:
纸上得来终觉浅 绝知此事要躬行!