C语言|动态内存管理与柔性数组

动态内存管理与柔性数组

在你立足处深挖下去,就会有泉水涌出!别管蒙昧者们叫嚷:“下边永远是地狱!”
博客主页:KC老衲爱尼姑的博客主页
博主的github,平常所写代码皆在于此
刷题求职神器
共勉:talk is cheap, show me the code
作者是爪哇岛的新手,水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!
刷题求职神器
在下给诸位推荐一款巨好用的刷题求职神器,如果还有小伙伴没有注册该网站,可以点击下方链接直接注册,注册完后就可以立即刷题了。
C语言|动态内存管理与柔性数组
文章图片

传送门:牛客网

文章目录
  • 动态内存管理与柔性数组
    • 前言
    • 动态内存管理函数
      • malloc-函数
      • calloc-函数
      • realloc-函数
      • free-函数
    • C/C++程序的内存开辟
    • 经典的动态内存面试题:
      • 题目1.
      • 题目2:
      • 题目3:
      • 题目4:
    • 柔性数组
      • 柔性数组的特点:
      • 柔性数组空间的开辟
      • 柔性数组的使用
      • 柔性数组的优势
        • 优势一:
        • 优势二:

前言
我们通常都是创建变量和创建数组来存储数据,这两种使用内存的方式都是在内存申请固定大小的空间,创建变量每种类型就限定了开辟内存时的空间大小,使用数组在声明的时候,指定数组的长度也就是确定在内存空开辟的空间。但是对于空间大小的需要有时候是在程序运行的时候才知道,此时就只能试试动态内存开辟,接下来就来介绍一些开辟内存的函数。
动态内存管理函数 malloc-函数
void* malloc (size_t size);

malloc函数的功能是开辟指定字节大小的内存空间,如果失败则返回NULL,成功则返回开辟空间的首地址。传参是只需要传入开辟内存所需的字节个数。
假设我们开辟40个字节的空间
#include #include int main() { int* ps = (int*)malloc(40); //返回值为void*,需强转 if (ps == NULL) { perror("malloc:"); } free(ps); ps = NULL; return;

注意:
  1. malloc所开辟的空间,不会对空间的内容进行初始化所以空间中的值为随机值。
  2. 使用malloc开辟空间后最好其返回值进行判断避免出现访问空指针。
  3. 使用完空间后需对开辟的空间进行释放,避免造成内存泄漏。
calloc-函数
void* calloc (size_t num, size_t size);

calloc函数和malloc函数同样是开辟指定大小的空间,如若开辟成功则返回成功开辟空间的首地址,失败则返回NULL。但它有两个参数,第一个参数为存放元素的个数,第二个参数为每个元素的字节大小。
在使用calloc函数开辟40个整型的空间
#include #include int main() { int* ps = (int*)calloc(40,sizeof(int)); //返回值为void*,需强转 if (ps == NULL) { perror("malloc:"); } else { free(ps); ps = NULL; } return; }

注意:
calloc函数开辟的内存空间的内容会被初始化为0.
realloc-函数
void* realloc (void*ptr, size_t size);

realloc函数可以是动态内存的管理更加灵活,原因如下,有时我们会发现申请的空间过大或者过小,这机会造成内存浪费或者不利于我们使用空间。这时realloc就可以解决以上难题。realloc函数可以对已经动态开辟好的内存进行调整,它的第一个参数为原有空间的起始地址,第二个参数为调整后空间的新大小。返回值同malloc函数和calloc函数一致。申请空间成功返回该空间的首地址,失败则返回NULL。
我们在原有空间上再开辟40个空间,使用realloc调整空间大小
#include #includeint main() { int* ptr = (int*)malloc(40); if (ptr == NULL) { perror("malloc:"); } int* ps = (int*)realloc(ptr,80); free(ps); ps = NULL; return 0; }

realloc在调整内存空间的是存在两种情况:
使用malloc开辟40个字节的空间,当需要在扩大40个字节的时候有两种情况
  • 情况1:原有空间之后有足够大的空间
C语言|动态内存管理与柔性数组
文章图片

情况一:我们在原有空间上再开辟40个字节的空间,如果原有空间的后面依旧有足够的空间够用,就在后面开辟40 个字节,并返回该内存空间的首地址(原有空间的首地址)。
情况二:原有空间之后有没有足够大的空间
C语言|动态内存管理与柔性数组
文章图片

情况二:原有空间后没有足够的空间,此时会再堆内存中重新开辟一块足够大的空间,同时会把原有空间中的数据拷贝到新空间中来,并且会返回新空间的首地址。
free-函数
void free (void* ptr);

free函数的作用就是把动态开辟空间的内存全部都释放掉,即还给操作系统。把动态开辟的内存释放后,该内存就没有使用权限了。
注意
  1. 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
  2. 如果参数 ptr 是NULL指针,则函数什么事都不做。
举个例子
#include int main() { //代码1 int num = 0; scanf("%d", &num); int arr[num] = {0}; //代码2 int* ptr = NULL; ptr = (int*)malloc(num*sizeof(int)); if(NULL != ptr)//判断ptr指针是否为空 { int i = 0; for(i=0; i

使用free函数释放后,需将指针ptr置为空指针,因为ptr指向的空间被释放后,该内存就无法访问了,相当于ptr成了野指针即使它存了该空间的首地址,为避免我们去使用它去访问非法空间,故将它置为NULL。
常见的动态内存错误
1 对NULL指针的解引用操作
当我们使用malloc,calloc,realloc开辟空间是,若是开辟空间失败是会返回NULL。假如我们没有对返回的指针进行检测就容易在后面使用的时候对NULL解引用。
void test() { int *p = (int *)malloc(INT_MAX/4); *p = 20; //如果p的值是NULL,就会有问题 free(p); }

对动态开辟空间的越界访问
假如我们只开辟了40个字节,我们绝不能去访问第41个字节。这就好比手上只有100快钱,但是在商店确要用100快钱买200块的东西。显然是不可能的。
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); }

对非动态开辟内存使用free释放
上文已经说到,free是对动态开辟的空间进行释放,不能对变量开辟或者数组开辟的空间进行释放。
void test() { int a = 10; int *p = &a; free(p); }

运行结果
C语言|动态内存管理与柔性数组
文章图片

使用free释放一块动态开辟内存的一部分
free函数的第一个参数是需要释放空间的首地址,所需传参是必须传动态开辟空间的首地址,而不能是其后面的地址,不能只释放一部分,只能全部释放。
void test() { int *p = (int *)malloc(100); p++; free(p); }

运行依旧报错
C语言|动态内存管理与柔性数组
文章图片

对同一块动态内存多次释放
动态开辟的空间只能释放一次,不能多次释放。因为第一个释放后,第二次再释放的话是无法访问该内存的,避免进行多次释放可以在第一次释放完后将该指针置为NULL即可,如此第二次释放的时候因为是空指针所以free什么都不会做。
void test() { int *p = (int *)malloc(100); free(p); free(p); //重复释放 }

动态开辟内存忘记释放(内存泄漏)
切记每次使用完动态开辟的空间之后需要对该空间进行释放,并且将该指针置空。如果忘记将动态开辟的空间释放会造成内存泄漏。
void test() { int *p = (int *)malloc(100); if(NULL != p) { *p = 20; } } int main() { test(); while(1); }

C/C++程序的内存开辟 C/C++程序内存分配的几个区域:
  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结 束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是 分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返 回地址等。
  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分 配方式类似于链表。
  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
经典的动态内存面试题: 题目1.
#include #include #include 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; }

程序时先调用Test(),进入Text()函数后为str赋值后调用GetMemory函数,进入 GetMemory函数动态开辟100个字节,并将该空间的首地址赋值给p。当执行完GetMemory函数时,存放p的单元被销毁。所以str依旧没有发生变化,既然p没有被销毁也无法改变str,因为GetMemory函数采用传值调用是无法改变str的。因此str依旧是空指针,当strcpy时自然不能将字符串拷贝到str中。
题目2:
char *GetMemory(void) { char p[] = "hello world"; return p; } void Test(void) { char *str = NULL; str = GetMemory(); printf(str); } int main() { Test(); return 0; }

题目2与题目1相差无几,进入 GetMemory()函数使用数组开辟内存,最后返回数组名。实际上一出函数p就会被销毁。即使返回给str,str也不能访问数组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); }

GetMemory(); 函数采用传址调用,进入GetMemory()函数后动态开辟100个字节的空间并将该空间的首地址赋值给str是会改变str的值,所以是能打印出来hello,但是该程序存在漏洞就是没有对动态开辟的内存进行释放,会造成内存泄漏。
题目4:
void Test(void) { char *str = (char *) malloc(100); strcpy(str, "hello"); free(str); if(str != NULL) { strcpy(str, "world"); printf(str); } }

动态开辟100个字节并将该空间的首地址赋值给str,然后使用free函数释放了动态开辟的内存。也就是str无法访问该空间了。后面将world的拷贝到str中,也就是将world的首地址赋值给了str,所以能打印出来world。
柔性数组 柔性数组:C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
例如:
typedef struct st_type { int i; int a[0]; //柔性数组成员 }type_a;

柔性数组存在于结构体中,为结构体中的 最后一个成员且数组的大小没有指定。
柔性数组的特点:
  1. 结构中的柔性数组成员前面必须至少一个其他成员。
  2. sizeof 返回的这种结构大小不包括柔性数组的内存。
  3. 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大 小,以适应柔性数组的预期大小。
  4. sizeof计算包含柔性数组的结构体时不会计算出柔性数组包含的内存大小
比如使用sizeof计算包含柔性数组的结构体
struct s { int i; int arr[]; }; int main() { printf("%d", sizeof(struct s)); //打印结果为4 return 0; }

柔性数组空间的开辟
柔性数组空间的开辟需使用malloc函书动态开辟内存,使得柔性数组的成员能存放5个整型的元素。
struct S { int i; int arr[]; }; int main() { struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int)); //sizeof计算结构体大大小并没有包含数组的内存大小 return 0; }

柔性数组的使用
typedef struct st_type { int i; int a[0]; //柔性数组成员 }type_a; //结构体变量int main() { int i = 0; type_a* p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int)); //业务处理 p->i = 100; for (i = 0; i < 100; i++) { p->a[i] = i; } free(p); return 0; }

这样柔性数组成员a,相当于获得了100个整型元素的连续空间。
柔性数组的优势
模拟实现柔性数组的功能
typedef struct st_type { int i; int* p_a; }type_a; int main() { type_a* p = (type_a*)malloc(sizeof(type_a)); p->i = 100; //对结构体成员i初始化 p->p_a = (int*)malloc(p->i * sizeof(int)); for (int i = 0; i < 100; i++) { p->p_a[i] = i; // } free(p->p_a); p->p_a = NULL; free(p); p = NULL; return 0; }

柔性数组是结构体中的数组,该大小不定可以使用动态内存管理的函数来改变其空间大小。我们模拟实现其功能就是在结构体中定义一个指针,并用malloc函数开辟空间,然后使结构体中的指针指向动态开辟的内存即可。当我们使用完动态开辟的空间之后需要对内存释放,此时需先释放p->p_a指向的空间,再释放 p指向的空间。如果先释放p的空间,在释放p->p_a指向的空间的话,p->pa是无法找到所要释放的空间的。
第一种使用柔性数组
C语言|动态内存管理与柔性数组
文章图片

第二种模拟柔性数组的功能
C语言|动态内存管理与柔性数组
文章图片

使用柔性数组的时候我们只需要释放一次内存空间即可,并且访问速度快,因为它是结构体的成员都在结构体所在的内存空间里,空间是连续的,所以访问更快。而第二种虽然模拟实现了柔性数组的功能,但是我们需要释放两次动态开辟的内存,并且释放的顺序不能错,先释放结构体成员内的指针所指向的空间,再释放指向结构体的指针。我们在使用需要对访问两次才能得到数组的元素,因为我们单独为数组开辟了空间,而这块空间与结构体所占有的空间不是连续的所以访问速度也慢了。
上述 代码1 和 代码2 可以完成同样的功能,但是 方法1 的实现有两个好处:
优势一: 方便内存释放:
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给 用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你 不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好 了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
优势二: 这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎片。
最后的话
各位看官如果觉得文章写得不错,点赞评论关注走一波!谢谢啦!。如果你想变强那么点我点我 牛客网。
【C语言|动态内存管理与柔性数组】C语言|动态内存管理与柔性数组
文章图片

    推荐阅读