这次我们来对动态内存进行认识,那么为什么会存在动态内存分配呢?目前我们申请内存的方式有两种:一是通过创建变量来申请,二是创建数组来申请。这两种方式在创建完成后都是无法再增加空间。那么,如果我们想要了解所申请的空间的大小或是想要增加所申请的空间的大小,就需要用到动态内存管理。
目录
动态内存函数
malloc
那么,什么是malloc呢?
malloc是用来申请空间的,我们注意它的参数size_t size,从下面我们可以得知它是需要申请元素的大小。它的返回值是void*类型的,也就是说,它可以申请任意类型的空间,最后返回申请空间的起始地址。
接下来我们就使用malloc来开辟10个整型大小的空间:
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)malloc(10 * sizeof(int)); return 0; }
我们这里的返回值不要用void*来进行接收,如果使用void*,那么这个变量会无法进行使用。因此,我们既然创建了int类型的空间,那我们就用int*来接收它的地址,用int*来进行接收,自然malloc申请的空间就需要进行强制类型转换。
我们接着往下看:
如果malloc申请成功,则返回申请空间的起始地址。如果申请失败,返回一个空指针。也就是说,我们需要对malloc的返回值进行判断,如果失败,打印它的错误信息:
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)malloc(10 * sizeof(int)); if (p == NULL) { perror("malloc"); return 1;//异常返回 } return 0; }
既然我们申请了空间,那么我们就对这片空间进行使用,我们使用这片空间存放10个整型并对其进行打印:
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)malloc(10 * sizeof(int)); if (p == NULL) { perror("malloc"); return 1;//异常返回 } int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i; } for (i = 0; i < 10; i++) { printf("%d ", p[i]); } printf("\n"); return 0; }
运行结果如下:
free
既然我们申请了空间,也就自然需要对申请的空间进行释放,释放的方式有两种:
1.free进行释放
使用free进行释放,是主动进行释放,一般都会使用主动释放,防止出现意外。
2.程序退出后,申请的空间会被操作系统回收。
这就是一种被动释放了,如果程序没有退出的话,这片申请的空间就会一直存在。
那我们接下来就来认识free:
那么free该如何进行使用呢?我们往下看:
我们首先来看它的参数:void* ptr。它的参数是一个地址,再看它是没有返回值的,那么我们只需要把起始地址放进去就可以了。接着往下看:如果释放空间的地址不是NULL,对该地址所指向的空间进行释放;如果释放的空间的地址是NULL,那么free不会进行操作。
那我们就对刚才申请的空间进行释放:
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)malloc(10 * sizeof(int)); if (p == NULL) { perror("malloc"); return 1;//异常返回 } int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i; } for (i = 0; i < 10; i++) { printf("%d ", p[i]); } printf("\n"); free(p); p = NULL; return 0; }
可以看到,我使用free释放完之后把p赋为了空指针,这又是为什么?我们进行调试:
可以看到,若是只使用free的话,p的地址还是存在的,但是其中的值被释放了。也就是说,p成了野指针,这时及其危险的,因此我们需要将p赋为空指针。
关于free,我们还需要注意一点:
如果参数p指向的空间不是动态开辟的,那free函数的行为是未定义的。
也就是说free只能对动态内存空间进行释放。
calloc
calloc也是用来开辟内存空间的,那么它和malloc有什么区别呢?
它比malloc多了size_t num,就是需要申请的元素的个数。也就是说calloc相当于把malloc申请元素大小的*换成了,。紧接着我们就可以看到,calloc会将空间初始化为0。那我们就用calloc来开辟空间并进行打印:
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)calloc(10, sizeof(int)); 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; }
运行结果如下:
realloc
realloc的出现让动态内存变得灵活起来,realloc是可以调节所申请的空间的大小的,那么我们就来对其详细查看:
我们先来看它的参数:(void* ptr, size_t size);void* ptr是开辟空间的起始地址,size_t size是指需要调整的大小,它的返回值还是void*。但是需要注意的是realloc是可能开辟空间失败的,它与malloc开辟失败后一样,返回NULL,也就是说,我们在使用realloc增加了空间之后需要进行判断,看查返回的地址是不是NULL,再把它赋给原来的地址:
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)calloc(10, sizeof(int)); if (p == NULL) { perror("calloc"); return 1; } int i = 0; for (i = 0; i < 10; i++) { printf("%d\n", *(p + i)); } //调整为20个整型的空间 int* ptr = (int*)realloc(p, 20 * sizeof(int)); if (ptr != NULL) { p = ptr; } free(p); p = NULL; return 0; }
我们还需要注意的是,realloc在地址为空的时候,与malloc等价。也就是说,当赋予的起始地址为空时,realloc与malloc可以相互替换:
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)realloc(NULL, 40);//等价于malloc(40) if (p == NULL) { perror("realloc"); return 1; } free(p); p = NULL; return 0; }
常见的动态内存错误
下面就是开辟动态内存会出现的错误,需要避免。
对空指针的解引用
那么什么时候会出现对空指针的解引用呢?就比如在使用malloc开辟空间后不进行判断:
int main() { int* p = (int*)malloc(20 * sizeof(int)); //没有进行判断 *p = 20; free(p); p = NULL; return 0; }
这样的话如果malloc开辟空间失败的话程序就会出错,需要注意。
对动态内存开辟空间的越界访问
那我们就拿malloc开辟的空间进行举例:
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)calloc(10, sizeof(int)); if (p == NULL) { perror("calloc"); return 1; } int i = 0; for (i = 0; i <= 10; i++) { p[i] = i; } for (i = 0; i <= 10; i++)//这里进行越界 { printf("%d\n", *(p + i)); } free(p); p = NULL; return 0; }
运行结果如下:
对非动态内存开辟的空间进行free释放
就比如对整型进行释放:
#include<stdio.h> #include<stdlib.h> int main() { int a = 10; int* p = &a; //释放 free(p); p = NULL; return 0; }
运行结果如下:
使用free释放开辟的动态内存空间的一部分
什么时候会出现free只释放一部分的空间呢?下面进行举例:
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)calloc(10, sizeof(int)); if (p == NULL) { perror("calloc"); return 1; } int i = 0; for (i = 0; i < 5; i++) { *p = i; p++; } free(p); p = NULL; return 0; }
这样一来,指针就指向了第六个元素,之后才使用free进行释放。
对同一块内存多次释放
对同一块内存多次释放在写比较大型的代码时出现得比较多,我们对这种情况进行模拟:
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)malloc(20 * sizeof(int)); if (p == NULL) { perror("malloc"); return 1; } //进行释放 free(p); //..... //进行释放 free(p); p = NULL; return 0; }
运行结果如下:
结果报错,所以同一块空间是不能进行多次释放的,想要避免也很简单,我们只需要每次释放后都把释放的地址赋为空指针就可以了:
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)malloc(20 * sizeof(int)); if (p == NULL) { perror("malloc"); return 1; } //进行释放 free(p); p = NULL; //..... //进行释放 free(p); p = NULL; return 0; }
运行结果如下:
没有报错,因此我们需要养成使用动态内存开辟后就使用free进行释放的习惯。
动态内存忘记进行释放(内存泄漏)
这一点我们之前就提到过,程序若是没有退出,并且没有使用free进行释放,空间就会一直被占用:
#include<stdio.h> #include<stdlib.h> void test() { int* p = (int*)malloc(40); if (*p == 4) { return; } free(p); p = NULL; } int main() { test(); while (1) { } return 0; }
我们可以看到,代码在执行到函数的if判断时就极有可能返回,但是free函数没有执行,在main函数中又出现了while(1)一直循环。这样一来内存就会一直使用,创建并且没有释放,那么就会出现内存泄漏的问题。
柔性数组
柔性数组的概念
那么什么是柔性数组呢?
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
也就是说,柔性数组满足以下3点:
1.柔性数组存在于结构体中。
2.它是结构体的最后一个成员。
3.必须是未知大小的数组。
那么柔性数组应该如何进行创建呢?我们来编写代码:
typedef struct S1 { int i; int a[0]; }S1;
我们知道,数组的大小是不可能为0的,也就是说a[0]的大小是未知的,那么int a[0]就是柔性数组成员,但是如果我们写成a[0]的话有些编译器会进行报错,如果编译器不支持,那么我们就如下创建:
typedef struct S1 { int i; int a[];//柔性数组成员 }S1;
柔性数组的特点
那么柔性数组有什么特点呢?
1.结构中的柔性数组成员前面必须至少一个其他成员。
也就是说柔性数组是最后进行创建,并且不能单独创建。
2.sizeof 返回的这种结构大小不包括柔性数组的内存。
sizeof是不计算柔性数组的大小的,那我们就编写代码来进行验证:
#include<stdio.h> typedef struct S1 { int i; int a[0]; }S1; int main() { S1 s1; printf("%zd\n", sizeof(s1)); return 0; }
运行结果如下:
我们可以看到,sizeof计算的大小并没有将柔性数组算进去。
3.包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
也就是说,我们用malloc开辟空间时,若包含柔性数组成员,那么我们就需要额外的空间来对柔性数组成员进行存放,就比如:
struct S { char c; int i; int a[0]; }S;
它包含了一个char类型,一个int类型和一个柔性数组成员,那么我们使用malloc开辟时就不能只开辟8个字节:
#include<stdio.h> #include<stdlib.h> struct S { char c; int i; int a[0]; }S; int main() { struct S* pc = (struct S*)malloc(sizeof(struct S) + 20); if (pc == NULL) { perror("malloc"); return 1; } //.... return 0; }
柔性数组的使用
柔性数组的使用很简单,就比如之前我们为柔性数组开辟了20个字节,那么我们对它进行赋值:
#include<stdio.h> #include<stdlib.h> struct S { char c; int i; int a[0]; }S; int main() { struct S* pc = (struct S*)malloc(sizeof(struct S) + 20); if (pc == NULL) { perror("malloc"); return 1; } int j = 0; pc->c = 'w'; pc->i = 20; for (j = 0; j < 5; j++) { pc->a[j] = j; } printf("%c\n", pc->c); printf("%d\n", pc->i); for (j = 0; j < 5; j++) { printf("%d ", pc->a[j]); } printf("\n"); free(pc); pc = NULL; return 0; }
运行结果如下:
如果想要增加空间,使用realloc就可以了。
柔性数组的优势
那么我们为什么要使用柔性数组呢?柔性数组又有什么优势呢?
1.方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
2.有利于提高访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片。
小结
对动态内存的认识到这里就结束啦,之后就是文件操作,文件操作的作用也是很大的呢。好啦,大家一起努力,我们下次见!