目录
1、为什么要有动态内存分配
我们已经掌握的内存开辟方式有:
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
2、malloc 和 free
2.1 malloc函数
malloc
函数是 C 语言中用于动态分配内存的函数之一,其声明在 <stdlib.h>
头文件中。该函数用于在程序运行时从堆(heap)中动态分配指定大小的内存空间,并返回一个指向该内存空间起始地址的指针。
void *malloc(size_t size);
size_t size
:需要分配的内存空间大小,以字节为单位。
malloc
函数返回一个 void
类型的指针,需要根据实际情况进行类型转换后使用。如果内存分配成功,则返回指向分配内存起始地址的指针;如果内存分配失败,则返回 NULL
。
2.2 free函数
free
函数是 C 语言中用于释放动态分配内存的函数,其声明在 <stdlib.h>
头文件中。当程序不再需要动态分配的内存空间时,应该使用 free
函数将该内存空间释放,以便系统可以重新利用这部分内存。
void free(void *ptr);
void *ptr
:指向先前由malloc
、calloc
或realloc
返回的内存空间起始地址的指针。
使用 free
函数后,内存空间将被标记为可用,并可以被后续的内存分配操作重新使用。但需要注意的是,尝试释放已经释放过的内存空间或者尝试释放静态分配的内存空间会导致未定义的行为,因此务必确保只对动态分配的内存空间使用 free
函数。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *arr;
int n = 5;
// 动态分配包含5个整数的内存空间
arr = (int *)malloc(n * sizeof(int));
if (arr == NULL)
{
perror("malloc");
return 1;
}
// 将数组元素初始化为 0
for (int i = 0; i < n; i++)
{
arr[i] = 0;
}
// 打印数组元素
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
// 释放动态分配的内存
free(arr);
arr=NULL;
return 0;
}
注:在 free 释放开辟的动态内存后,原来指向该内存首地址的指针仍然指向该地址,为了避免该指针成为野指针,在 free 释放完开辟的动态内存后最好将指向该内存的指针设置为NULL;
free(arr);
arr=NULL;
3、calloc 和 realloc
3.1 calloc函数
calloc
函数是 C 语言中用于动态分配内存并初始化为零的函数,其声明在 <stdlib.h>
头文件中。与 malloc
函数不同的是,calloc
函数在分配内存空间的同时会将其初始化为零,这是 calloc
和 malloc
的主要区别之一。
void *calloc(size_t num, size_t size);
size_t num
:需要分配的元素个数。size_t size
:每个元素的大小,以字节为单位。
calloc
函数会分配 个字节的内存空间,并将所有位初始化为零。如果内存分配成功,则返回指向分配内存起始地址的指针;如果内存分配失败,则返回 NULL
。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (NULL != p)
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
}
free(p);
p = NULL;
return 0;
}
3.2 realloc 函数
realloc
函数是 C 语言中用于重新分配动态分配内存空间的函数,其声明在 <stdlib.h>
头文件中。当需要调整先前分配的内存空间的大小时,可以使用 realloc
函数来实现。realloc
函数会尝试扩大或缩小先前分配的内存块,并保留原始内存块中的数据。
void *realloc(void *ptr, size_t size);
void *ptr
:指向先前由malloc
、calloc
或realloc
返回的内存空间起始地址的指针。size_t size
:重新分配后的内存空间大小,以字节为单位。
如果重新分配成功,则返回指向新分配内存起始地址的指针;如果重新分配失败,则返回 NULL
。值得注意的是,realloc
函数可能会将原内存块移动到新的位置,因此在调用 realloc
后应该谨慎处理原指针。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *arr;
int n = 5;
// 动态分配包含5个整数的内存空间
arr = (int *)malloc(n * sizeof(int));
if (arr == NULL)
{
printf("内存分配失败\n");
return 1;
}
// 重新分配为包含10个整数的内存空间
arr = (int *)realloc(arr, 10 * sizeof(int));
if (arr == NULL)
{
printf("内存重新分配失败\n");
return 1;
}
// 打印数组元素
for (int i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
// 释放动态分配的内存
free(arr);
arr=NULL;
return 0;
}
3.3 realloc 和 malloc 区别
malloc返回类型是 void 型指针,再根据实际需求进行强制类型转换,那么根据两个函数的定义就可以发现,当 realloc 函数第一个参数是空指针时那么就和 malloc 函数功能相同:
int* p = (int*)realloc(NULL,40);
3.4 realloc 函数存在的问题
3.2 部分的示例代码首先使用 malloc
分配了包含5个整数的内存空间,然后使用 realloc
函数将内存空间重新分配为包含10个整数的空间。但是存在一个问题: 若 realloc 函数重新分配失败怎么办?会返回NULL,那么想重新分配的指针 arr = NULL,其原来指向的内存也不存在了。
realloc
函数在重新分配内存空间时可能会返回 NULL
,主要是由于以下几种情况导致的:
-
内存分配失败:当系统无法满足请求的内存空间大小时,
realloc
函数会失败并返回NULL
。这通常发生在内存不足或者系统资源受限的情况下。 -
内存空间连续性不足:如果原内存块之后没有足够的连续空间来扩展到请求的大小,
realloc
也会失败并返回NULL
。这种情况下,系统无法在原地扩展内存空间,需要将原内存块移动到新的位置,如果新位置无法提供足够的连续空间也会导致失败。 -
其他系统限制:某些系统可能会施加其他限制,例如内存碎片化程度过高、操作系统限制等,都有可能导致
realloc
返回NULL
。
情况1:
当是情况1的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发⽣变化。
情况2:
realloc函数会在内存的堆区重新找一个空间(满足新的空间大小需求的),同时会把旧的数据拷贝到新的空间,然后释放旧的空间,同时返回新的空间的起始地址。
由于上述的两种情况,realloc函数的使⽤就要注意⼀些:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* ptr = (int*)malloc(100);
if (ptr != NULL)
{
//...
}
else
{
return 1;
}
//扩展容量
//代码1 - 直接将realloc的返回值放到ptr中
ptr = (int*)realloc(ptr, 1000);//如果申请失败会如何?
//代码2 - 先将realloc函数的返回值放在p中,不为NULL,再放ptr中
int* p = NULL;
p = realloc(ptr, 1000);
if (p != NULL)
{
ptr = p;
}
else
{
perror("realloc");
return 1;
}
free(ptr);
return 0;
}
4、常见的动态内存的错误
(一) 对NULL指针的解引用操作
//可以先进行判断
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(100);
if (p == NULL)
{
perror("malloc");
return 1;
}
*p = 20;
free(p);
return 0;
}
(二) 对动态开辟空间的越界访问
(三)对非动态开辟内存使用free释放
(四)使用free释放一块动态开辟内存的一部分
使用 free 释放空间时给的不是起始地址。
(五)对同一块动态内存多次释放
(六)动态开辟内存忘记释放(内存泄漏)
如下情况,在 test 函数中申请了100个字节的空间,并定义了p指向了这个空间。当函数调用结束后,函数定义的局部变量 p 随之销毁,同时也没有变量保存这块空间的地址,申请的这块内存空间后续将无法使用,因为不知道这块空间的地址。
#include<stdio.h>
#include<stdlib.h>
void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while (1);
}
5、动态内存经典笔试题分析
题目一:
#include<stdio.h>
#include<stdlib.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;
}
分析上述代码,当将一个指针作为参数传递给函数时,实际上传递的是指针变量所存储的地址值的副本,而不是实际指针变量本身。这意味着在函数内部对传递进来的指针进行修改时,只会影响到形式参数的副本,而不会影响到原始指针变量的值。 在 GetMemory
函数中,参数 p
被传递为指针的副本,而不是指针本身。所以函数内部对 p
进行的内存分配不会影响到 Test
函数中原始指针 str
的值。当 GetMemory
函数返回时,分配的内存空间将丢失,因为只是修改了传递进来的副本指针 p
的值,而原始指针 str
仍然是 NULL
。因此,strcpy
和 printf
函数中使用的 str
指针仍然是 NULL
,对空指针解引用就会导致程序的崩溃。
修改后代码如下:
题目二:
分析上述代码。在 GetMemory
函数中,它尝试返回一个指向局部变量 p
的指针。然而,一旦 GetMemory
函数执行完毕并返回,p
所在的内存空间将被释放,因为它是一个自动变量(在栈上分配)。因此,当 Test
函数尝试使用从 GetMemory
返回的指针时,实际上它指向的是一个无效的内存位置,这可能导致未定义的行为。
可以在GetMemory函数中定义char p[ ]前加static,这样当执行完函数后这块空间仍然不会释放:
6、柔性数组
6.1 补充 typedef 创建结构体
使用 typedef
可以将复杂的数据类型简化为一个更容易记忆和理解的别名,使得代码更加清晰,减少出错的可能性。在 C 语言中,typedef
经常与结构体、枚举等复杂数据类型一起使用,方便在程序中定义新的类型名称。
在如下代码中,通过 typedef
将 struct st_type
定义的结构体类型命名为 type_a
,这样以后可以直接使用 type_a
来声明结构体变量,而不需要每次都写完整的 struct st_type
。这种方式使得代码更加简洁明了,提高了代码的可读性和可维护性。
#include <stdio.h>
// 定义结构体 st_type
struct st_type
{
int i;
char c;
};
// 使用 typedef 创建结构体别名 type_a
typedef struct st_type type_a;
int main()
{
// 声明一个结构体变量并赋初值
type_a my_struct;
my_struct.i = 10;
my_struct.c = 'A';
// 打印结构体变量的成员值
printf("i: %d\n", my_struct.i);
printf("c: %c\n", my_struct.c);
return 0;
}
6.2 柔性数组的特点
在 C 语言中,柔性数组(Flexible Array Member)是一种特殊的结构体成员,它允许在结构体的末尾定义一个长度不确定的数组。柔性数组通常用于动态分配内存,使得结构体可以容纳可变长度的数据。(1)在结构体中;(2)最后一个成员;(3)未知大小的数组。例如:
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
有些编译器会报错⽆法编译可以改成:
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;
(1)结构中的柔性数组成员前⾯必须至少⼀个其他成员。
(2)sizeof 返回的这种结构大小不包括柔性数组的内存。
(3)包含柔性数组成员的结构用 malloc() 函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
6.3 柔性数组的使用
#include <stdio.h>
#include <stdlib.h>
// 定义包含柔性数组的结构体
struct FlexArray
{
int length;
int data[]; // 柔性数组,实际长度在运行时确定
};
int main()
{
int n = 5;
// 计算结构体大小并分配内存,前面的sizeof(struct FlexArray)分配给柔性数组前面的变量,后面的 n * sizeof(int)为柔性数组的大小
struct FlexArray* flex = (struct FlexArray*)malloc(sizeof(struct FlexArray) + n * sizeof(int));
if (flex == NULL)
{
printf("Memory allocation failed\n");
return 1;
}
flex->length = n;
// 使用柔性数组存储数据
for (int i = 0; i < n; i++)
{
flex->data[i] = i * 2;
}
// 打印柔性数组中的数据
printf("Data stored in flexible array:\n");
for (int i = 0; i < flex->length; i++)
{
printf("%d ", flex->data[i]);
}
// 释放内存
free(flex);
return 0;
}
6.4 柔性数组的优势
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
int i;
int* p_a;
}type_a;
int main()
{
int i = 0;
type_a* p = (type_a*)malloc(sizeof(type_a));
p->i = 100;
p->p_a = (int*)malloc(p->i * sizeof(int));
//处理
for (i = 0; i < 100; i++)
{
p->p_a[i] = i;
}
for (int i = 0; i < 100; i++)
{
printf("%d ", p->p_a[i]);
}
//释放空间
free(p->p_a);
p->p_a = NULL;
free(p);
p = NULL;
return 0;
}
假如将6.3中的柔性数组换成 int*的指针,同样可以实现相同的功能 ,但是柔性数组更有优势: