动态内存管理与柔性数组
文章目录
- 动态内存管理与柔性数组
- 前言
- 动态内存管理函数
- malloc-函数
- calloc-函数
- realloc-函数
- free-函数
- C/C++程序的内存开辟
- 经典的动态内存面试题:
- 题目1.
- 题目2:
- 题目3:
- 题目4:
- 柔性数组
- 柔性数组的特点:
- 柔性数组空间的开辟
- 柔性数组的使用
- 柔性数组的优势
- 优势一:
- 优势二:
前言
我们通常都是创建变量和创建数组来存储数据,这两种使用内存的方式都是在内存申请固定大小的空间,创建变量每种类型就限定了开辟内存时的空间大小,使用数组在声明的时候,指定数组的长度也就是确定在内存空开辟的空间。但是对于空间大小的需要有时候是在程序运行的时候才知道,此时就只能试试动态内存开辟,接下来就来介绍一些开辟内存的函数。
动态内存管理函数
malloc-函数
void* malloc (size_t size);
malloc函数的功能是开辟指定字节大小的内存空间,如果失败则返回NULL,成功则返回开辟空间的首地址。传参是只需要传入开辟内存所需的字节个数。
假设我们开辟40个字节的空间
int main()
{
int* ps = (int*)malloc(40);//返回值为void*,需强转
if (ps == NULL)
{
perror("malloc:");
}
free(ps);
ps = NULL;
return;
注意:
- malloc所开辟的空间,不会对空间的内容进行初始化所以空间中的值为随机值。
- 使用malloc开辟空间后最好其返回值进行判断避免出现访问空指针。
- 使用完空间后需对开辟的空间进行释放,避免造成内存泄漏。
calloc-函数
void* calloc (size_t num, size_t size);
calloc函数和malloc函数同样是开辟指定大小的空间,如若开辟成功则返回成功开辟空间的首地址,失败则返回NULL。但它有两个参数,第一个参数为存放元素的个数,第二个参数为每个元素的字节大小。
在使用calloc函数开辟40个整型的空间
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调整空间大小
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:原有空间之后有足够大的空间
情况一:我们在原有空间上再开辟40个字节的空间,如果原有空间的后面依旧有足够的空间够用,就在后面开辟40 个字节,并返回该内存空间的首地址(原有空间的首地址)。
情况二:原有空间之后有没有足够大的空间
情况二:原有空间后没有足够的空间,此时会再堆内存中重新开辟一块足够大的空间,同时会把原有空间中的数据拷贝到新空间中来,并且会返回新空间的首地址。
free-函数
void free (void* ptr);
free函数的作用就是把动态开辟空间的内存全部都释放掉,即还给操作系统。把动态开辟的内存释放后,该内存就没有使用权限了。
注意
- 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
- 如果参数 ptr 是NULL指针,则函数什么事都不做。
举个例子
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);
}
运行结果
使用free释放一块动态开辟内存的一部分
free函数的第一个参数是需要释放空间的首地址,所需传参是必须传动态开辟空间的首地址,而不能是其后面的地址,不能只释放一部分,只能全部释放。
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);
}
运行依旧报错
对同一块动态内存多次释放
动态开辟的空间只能释放一次,不能多次释放。因为第一个释放后,第二次再释放的话是无法访问该内存的,避免进行多次释放可以在第一次释放完后将该指针置为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++程序内存分配的几个区域:
- 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结 束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是 分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返 回地址等。
- 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分 配方式类似于链表。
- 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
- 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
经典的动态内存面试题:
题目1.
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;
柔性数组存在于结构体中,为结构体中的 最后一个成员且数组的大小没有指定。
柔性数组的特点:
- 结构中的柔性数组成员前面必须至少一个其他成员。
- sizeof 返回的这种结构大小不包括柔性数组的内存。
- 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大 小,以适应柔性数组的预期大小。
- 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是无法找到所要释放的空间的。
第一种使用柔性数组
第二种模拟柔性数组的功能
使用柔性数组的时候我们只需要释放一次内存空间即可,并且访问速度快,因为它是结构体的成员都在结构体所在的内存空间里,空间是连续的,所以访问更快。而第二种虽然模拟实现了柔性数组的功能,但是我们需要释放两次动态开辟的内存,并且释放的顺序不能错,先释放结构体成员内的指针所指向的空间,再释放指向结构体的指针。我们在使用需要对访问两次才能得到数组的元素,因为我们单独为数组开辟了空间,而这块空间与结构体所占有的空间不是连续的所以访问速度也慢了。
上述 代码1 和 代码2 可以完成同样的功能,但是 方法1 的实现有两个好处:
优势一:
方便内存释放:
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给 用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你 不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好 了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
优势二:
这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎片。