1. 写在前面
在实际的开发过程中,我们处理的数据类型在大多数情况下都不是固定不变的,而是处于不确定、大小可变的情况之中;因此,允许程序在运行时根据需要分配和释放内存具有重要意义,我们将这一过程就称作动态内存管理。笔者今天的博客,主要就是对近期学习动态内存管理相关知识的总结和梳理。内容如下:
2. 为什么存在动态内存分配
当我们在程序中定义变量的时候,就为其在内存中开辟了所需要的空间,如下就是内存开辟的方式之一:
上述开辟内存空间的特点:
- 所需开辟空间大小固定
- 数组声明时,大小需特别指定,其所需要的内存在编译时分配
然后数据对于内存空间的需求,不仅仅只是上述的情况;有时候我们需要的空间大小在程序运行的时候才能知道,这个时候只能采用动态内存管理的方式对内存进行分配和释放,以满足程序正常运行的需求;所以就出现了动态内存管理的方式。
3. 动态内存函数
3.1 malloc
使用介绍
功能:向内存申请一块连续的空间,返回指向该内存空间的地址,分配的内存空间未初始化
返回值类型:void* 类型的指针
参数:
- size_t size : 所申请内存空间的大小,以字节为单位
Tips:malloc申请的内存空间,为堆区上的内存空间。
代码使用
int main()
{
// 申请10个整型的空间
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
printf("%d ", *(p + i));
}
return 0;
}
运行结果
3.2 free
使用介绍
功能:释放已分配的内存空间,将其归还操作系统
返回值类型: void* 类型的指针
参数:
- void* memblock: 指向先前所申请到的内存空间的指针
代码使用
int main()
{
// 申请10个整型的空间
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
printf("%d ", *(p + i));
}
// free对申请到的内存空间进行释放,并将指针置空
free(p);
p = NULL;
return 0;
}
Tips : 程序向内存申请开辟的空间,使用结束之后一定要进行释放,归还操作系统,否则会造成内存泄漏(这一点后面也会讲到)!!!
3.3 calloc
使用介绍
功能:向内存申请一块连续的空间,并将其初始化为0,返回指向该内存空间的地址
返回值类型:void* 类型的指针
参数:
- size_t num : 所需申请空间中的元素个数
- size_t size : 每个元素的大小,以字节为单位
代码使用
int main()
{
int* p = (int*)calloc(10, 4);
if (p == NULL)
{
perror("calloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
printf("\n");
return 0;
}
运行结果
3.4 realloc
使用介绍
功能:调整已分配内存的大小,可以动态扩大或缩小内存空间
返回值类型:void* 类型的指针
参数:
- void* memblock:指向所需调整空间大小的指针
- size_t size: 所需新空间的大小
代码使用
int main()
{
//int* p = (int*)malloc(40);
// 申请10个整型的空间
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
printf("%d ", *(p + i));
}
printf("\n");
int* ptr = (int*)realloc(p, 80);
if (ptr != NULL)
{
p = ptr; // 扩容成功
}
for (i = 10; i < 20; i++)
{
*(p + i) = i;
printf("%d ", *(p + i));
}
printf("\n");
free(p);
p = NULL;
return 0;
}
运行结果
4. 常见的动态内存错误
4.1 对NULL指针进行解引用操作
代码示例
int main()
{
int* p = (int*)malloc(1000); --- 1
int i = 0;
for (i = 0; i < 250; i++)
{
*(p + i) = i;
printf("%d ", *(p + i));
}
return 0;
}
解析:
1. 未对指针p是否为NULL,进行判断,因此代码可能会对NULL进行解引用操作
4.2 对动态开辟空间进行越界访问
代码示例
int main()
{
int* p = (int*)malloc(100);
int i = 0;
if (p == NULL)
{
perror("malloc");
return 1;
}
for (i = 0; i < 26; i++) --- 1
{
*(p + i) = i;
}
return 0;
}
解析:
1. 在for循环遍历的过程中,i的取值,超出了固有的内存空间,造成了越界访问
4.3 对非动态开辟空间进行free操作
代码示例
int main()
{
int a = 10;
int* p = &a;
free(p); --- 1
p = NULL;
return 0;
}
解析:
1. 变量a在栈区上开辟静态内存空间,而只有使用动态内存函数开辟的动态内存空间,才能够使用free函数进行
内存释放。
4.4 使用free释放一块动态开辟内存中的一部分
代码示例
int main()
{
int* p = (int*)malloc(100);
p++; --- 1
free(p);//p不再指向动态内存的起始位置
return 0;
}
解析:
1. ++ 操作改变了p的指向位置,此时的p不再指向动态内存的起始位置,因此释放p,就相当于只释放了动态内存中的一部分
4.5 对同一块动态内存多次释放
代码示例
int main()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放 --- 1
return 0;
}
解析:
1. 对动态内存进行重复释放
4.6 动态开辟内存忘记释放
代码示例
void test()
{
int *p = (int *)malloc(100); --- 1
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
解析:
1. 在test函数内部,进行动态内存申请后,没有释放的操作
特别需要我们注意的是:忘记释放不再使用的动态开辟的空间会造成内存泄漏 !!!因此在开辟动态内存空间之后一定要释放,且要正确的释放!
5. 柔性数组
C99标准规定,结构体中最后一个成员,允许是未知大小的数组,这就称作“柔性数组”成员
柔性数组在结构体中的定义
柔性数组的特点
- 结构体中柔性数组成员前必须至少有一个其它成员;
- sizeof返回的该种结构体类的大小不包括柔性数组的内存;
-
包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小。
代码使用
struct S1
{
int i;
char ch;
int arr[];//柔性数组成员
};
int main()
{
int i = 0;
struct S1* p = (struct S1*)malloc(sizeof(struct S1) + 10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
p->i = 100;
for (i = 0; i < 10; i++)
{
p->arr[i] = i;
printf("%d ", p->arr[i]);
}
printf("\n");
free(p);
return 0;
}
运行结果
柔性数组的优势
而上述的柔性数组的功能,也可以由如下结构设计实现:
代码使用
struct S1
{
int i;
int* pa;
};
int main()
{
struct S1* p = (struct S1*)malloc(sizeof(struct S1));
if (p == NULL)
{
perror("malloc");
return 1;
}
p->i = 10;
p->pa = (int*)malloc(p->i * sizeof(int));
if (p->pa == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
p->pa[i] = i;
printf("%d ", p->pa[i]);
}
printf("\n");
//释放空间
free(p->pa);
p->pa = NULL;
free(p);
p = NULL;
}
运行结果
柔性数组的功能,可以完全由另一种结构设计所代替,那为什么还要继续使用柔性数组呢?
因为,柔性数组具有后者独有的优势:
- 方便内存释放:使用柔性数组只需进行一次内存分配,使用结束后方便内存的释放。
- 提高访问速度:柔性数组只进行一次内存申请,所以其内存空间是连续存放的,而连续的内存有益于提高访问速度,也有益于减少内存碎片。
6. 经典编程例题详解
6.1 例题一
题目内容
代码详解
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str); --- 1
strcpy(str, "hello world"); --- 2
printf(str);
}
解析:
1. 传入GetMemory的str指针为实参,指针p为形参,而形参只是实参的一份临时拷贝;即使在函数内部
申请了一块内存空间,当函数运行结束,形参即销毁,因此str仍为NULL。
2. str为NULL,当其传入strcpy函数内部时,strcpy函数对其进行解引用操作,即是对NULL进行解引用
程序运行结果:程序将会崩溃,没有打印内容。
运行结果
6.2 例题二
题目内容
代码详解
char *GetMemory(void)
{
char p[] = "hello world"; --- 1
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory(); --- 2
printf(str);
}
解析:
1. 在GetMemory函数中,创建了字符数组p存储了字符串"hello world",在内存中开辟了一段连续的空间;
本质上,字符数组的数组名p,指向了"hello world"字符串中首元素的地址;最后函数运行结束后,返回了
指针p;
2. 指针str,接收了GetMemory函数返回的指针p;但函数运行结束,其内部为局部临时变量开辟的空间也将被销
毁,因此str此时指向的内存空间是不确定的,即str此时为野指针。
程序运行结果: 打印随机值,因为str所指向的内存空间此时是不确定的、随机的。
运行结果
6.3 例题三
题目内容
代码详解
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num); --- 2
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100); --- 1
strcpy(str, "hello");
printf(str);
}
解析:
1. 因为传入GetMemory函数中的参数,为str指针的地址,于是,函数内部的局部临时变量二级指针p解引用后便与str在内存中指向同一块空间,即传址,使得函数内部的变量与外部的变量建立的联系;
2. 在GetMemeroy函数,malloc申请了一块空间之后,赋值给了*p,即赋值给了str;因此,str此时指向了内存当中一块连续的空间
程序运行结果: 打印字符串"hello"
运行结果
6.4 例题四
题目内容
代码详解
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str); --- 1
if(str != NULL) --- 2
{
strcpy(str, "world");
printf(str);
}
}
解析:
1. 函数malloc申请了一块空间,并让指针str指向了这块空间;strcpy函数,将字符串"hello",拷贝到了
这块空间中,之后对str指向的空间进行释放,但没有对str进行置空操作;
2. 因为str不为空,仍指向内存空间中的一块随机区域,在进入到if语句内部之后,strcpy将字符串"world"
拷贝到了str指向的内存空间中。
程序运行结果: 打印字符串"world" (但这样的程序是错误的,str未置空即相当于野指针)
运行结果
7. 小结
动态内存管理使得程序能够根据实际需求分配内存,从而避免浪费内存资源;但它也带来了一些挑战,如内存泄漏、内碎片和指针错误等。因此,作为开发人员,我们需要熟练掌握动态内存管理的技巧,并使用工具技术来检测和修复内存错误,以确保程序的正确性和稳定性。好啦,今天的博客就到此为止了,希望能够给正在阅读的你带来收获!