动态内存管理与柔性数组


文章目录

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

前言


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


动态内存管理函数

malloc-函数

void* malloc (size_t size);

malloc函数的功能是开辟指定字节大小的内存空间,如果失败则返回NULL,成功则返回开辟空间的首地址。传参是只需要传入开辟内存所需的字节个数。

假设我们开辟40个字节的空间

#include<stdio.h>
#include<stdlib.h>
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<stdio.h>
#include<stdlib.h>
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<stdio.h>
#include<stdlib.h>


int 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语言_02

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

free-函数

void free (void* ptr);

free函数的作用就是把动态开辟空间的内存全部都释放掉,即还给操作系统。把动态开辟的内存释放后,该内存就没有使用权限了。

注意

  1. 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
  2. 如果参数 ptr 是NULL指针,则函数什么事都不做。

举个例子

#include <stdio.h>
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<num; i++)
{
*(ptr+i) = 0
}
}
free(ptr);//释放ptr所指向的动态内存
ptr = NULL;
return 0;
}

使用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);
}

运行结果

动态内存管理与柔性数组_柔性数组_03

使用free释放一块动态开辟内存的一部分

free函数的第一个参数是需要释放空间的首地址,所需传参是必须传动态开辟空间的首地址,而不能是其后面的地址,不能只释放一部分,只能全部释放。

void test()
{
int *p = (int *)malloc(100);
p++;
free(p);
}

运行依旧报错

动态内存管理与柔性数组_c语言_04

对同一块动态内存多次释放

动态开辟的空间只能释放一次,不能多次释放。因为第一个释放后,第二次再释放的话是无法访问该内存的,避免进行多次释放可以在第一次释放完后将该指针置为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<stdio.h>
#include<stdlib.h>
#include<string.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;
}

程序时先调用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是无法找到所要释放的空间的。

第一种使用柔性数组

动态内存管理与柔性数组_数组_05

第二种模拟柔性数组的功能

动态内存管理与柔性数组_柔性数组_06

使用柔性数组的时候我们只需要释放一次内存空间即可,并且访问速度快,因为它是结构体的成员都在结构体所在的内存空间里,空间是连续的,所以访问更快。而第二种虽然模拟实现了柔性数组的功能,但是我们需要释放两次动态开辟的内存,并且释放的顺序不能错,先释放结构体成员内的指针所指向的空间,再释放指向结构体的指针。我们在使用需要对访问两次才能得到数组的元素,因为我们单独为数组开辟了空间,而这块空间与结构体所占有的空间不是连续的所以访问速度也慢了。

上述 代码1 和 代码2 可以完成同样的功能,但是 方法1 的实现有两个好处:

优势一:

方便内存释放:


如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给 用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你 不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好 了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。


优势二:

这样有利于访问速度.


连续的内存有益于提高访问速度,也有益于减少内存碎片。