动态内存管理

在本篇当中我们将对动态内存相关的知识进行学习,了解malloc,calloc等函数的作用以及学习如何使用这些函数,并且避免一些常见的动态内存中的错误,还有学习柔性数组是什么,有什么作用。接下来就开始本篇的学习吧!!!


1.为什么要有动态内存分配

在之前的学习中已经了解了利用数组来开辟连续的内存空间 ,但是数组的大小一旦确定了就不能修改了 ,有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的⽅式就不能满足了,使用这时就要用到c语言动态内存开辟,让程序员自己可以申请和释放空间

同时动态内存所在的内存位置也是与局部变量不同的,局部变量的数组是在内存中的栈区,而对于动态内存都是在内存中的堆区

2.malloc与free

2.1 malloc

c语言中提供了一个用于内存开辟的函数

对于malloc这个函数作用是开辟内存空间参数是需要开辟的内存空间大小 ,这个函数向内存申请⼀块连续可⽤的空间,并返回指向这块空间的指针。如果开辟空间成功就返回该空间的起始地址,开辟失败就返回空指针

例如当要申请10大小为int的内存空间就可以用malloc来实现

cpp 复制代码
#include<stdlib.h>
int main()
{
  int*p=(int*)malloc(10*sizeof(int));
  if(p==NULL)//开辟空间失败
{
 perror("malloc");
 return 1;
}
 return 0;
}

在以上代码中申请内存空间时由于malloc的返回值是空指针 ,所以为了能用int*的指针p来接收所以将malloc的返回值强制类型转换为int*
同时当申请内存空间失败时会返回空指针,这时再对p使用就会造成程序的错误,所以我们就需再创建一个if语句使得p==NULL 时就结束程序,因此在以上代码中就在p==NULL时return 1

在此之后我们就可以对这块申请的内存空间进行使用了
例如将这块空间每个整型都存放5

cpp 复制代码
#include<stdlib.h>
int main()
{
  int*p=(int*)malloc(10*sizeof(int));
  if(p==NULL)//开辟空间失败
{
 perror("malloc");
 return 1;
}
for(int i=0;i<10;i++)
{
 *(p+i)=5;
}
 return 0;
}

2.2 free

C语言提供了另外⼀个函数free,专门是用来做动态内存的释放和回收的

free的作用是对动态内存的回收和释放, 例如以上当我们使用malloc开辟内存空间时,不同于数组在出了作用域系统就会自己释放空间 ,malloc在申请后如果不释放 即使之后一直未用到这块空间,除非整个程序结束数据都会一直保留在该内存块当中,这就会使得内存的利用率下降

注:如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的

所以在以上使用malloc后要进行free释放内存空间
且当使用free释放内存空间后还要将p=NULL ,这是为什么呢?
由于free(p)后就将之前申请的40个字节的空间的使用权限还给了操作系统 ,但这时的p指针还指向之前那块空间的起始地址,这时就会使得p指针变为了野指针 ,所以使用free释放内存空间后还要将指向该内存起始地址的指针赋值为NULL

cpp 复制代码
#include<stdlib.h>
int main()
{
  int*p=(int*)malloc(10*sizeof(int));
  if(p==NULL)//开辟空间失败
{
 perror("malloc");
 return 1;
}
for(int i=0;i<10;i++)
{
 *(p+i)=5;
}
free(p);
p=NULL;

 return 0;
}

3.calloc与realloc

3.1 calloc

C语⾔还提供了⼀个函数叫 calloc , calloc 函数也用来动态内存分配。

calloc的作用也是用来开辟内存空间作用是开辟num个大小为size字节大小的内存块。不同于malloc,calloc的参数有两个,一个是需要开辟内存块的大小,另一个是需要开辟的内存块的数量
calloc的返回值也是所开辟的内存空间的起始地址。并且calloc在开辟完空间后会将空间内数据都初始化为0

在使用calloc申请内存空间后,当不再使用时也需要使用free将内存空间释放并将指针置为空指针

在使用malloc时就不会对开辟的内存空间进行初始化,开辟完时内存空间内的值是随机数

例如以下代码

cpp 复制代码
#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdlib.h>
#include<stdio.h>
int main()
{
	int* p = (int*)calloc(10 ,sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}
	free(p);
	p = NULL;

	return 0;
}

使用malloc申请空间后会将空间内的值都初始化为0,所以以上代码输出结果为10个0

3.2 realloc

在之前了解了动态内存开辟的两个函数malloc与realloc,那么如果要对开辟的内存空间进行调整就需要用到realloc

realloc的作用是对原来已经开辟的内存空间进行调整 。这个函数的参数有两个,第一个ptr是原来已经动态内存开辟的内存空间的起始地址,另一个size是调整后内存空间的大小

在使用realloc时,会出现以下两种情况

当出现情况1时剩下的空间不足以开辟时,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。并先将旧的数据拷贝到新空间中,返回的是一个新空间的内存地址。

当是情况1的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。同时该情况下返回的还是原空间的地址。

所以如果就要将以上用malloc开辟的内存空间调整为40字节就可以用realloc来实现

cpp 复制代码
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)//开辟空间失败
	{
		perror("malloc");
		return 1;
	}
	int* ptr = (int*)realloc(p, 20 * sizeof(int));
	if (ptr != NULL)
	{
		p = ptr;
	}
	free(p);
	p = NULL;

	return 0;
}

在使用realloc调整内存空间时候可能会出现失败的情况,所以这时我们就先不用原来的指针p来接收使用realloc后返回的地址 ,而是先将返回的地址放在一个新的指针ptr 内,当ptr不为空指针时再将ptr赋给p这样做的目的是防止当使用realloc调整内存失败后原开辟的内存空间也无法使用

realloc也是可以像malloc和calloc一样开辟内存空间的,只需要将realloc的第一个参数传NULL 就可以实现该功能了

cpp 复制代码
#include<stdlib.h>
#include<stdio.h>
int main()
{
	int* p = (int*)realloc(NULL, 10 * sizeof(int));
	if (p == NULL)//开辟空间失败
	{
		perror("malloc");
		return 1;
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}
	free(p);
	p = NULL;

	return 0;
}

同时在使用realloc开辟内存空间时,内存空间内的值也是随机值,所以以上代码输出结果如下

4. 常见的动态内存的错误

1 对NULL指针的解引用操作

cpp 复制代码
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}

例如在以上代码中使用malloc开辟内存空间时可能会开辟失败返回空指针NULL ,这时不进行判断直接对p进行解引用可能就会对空指针进行解引用 ,这样程序就会崩溃

2.对动态开辟空间的越界访问

cpp 复制代码
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);
}

例如在以上代码当中使用malloc只开辟了10个大小为int的内存空间,而之后的for循环内最后
*(p+10)已经超出了以上内存空间的范围,这时就是在对内存的越界访问 ,这种行为是非法的

3.对非动态开辟内存使用free释放

cpp 复制代码
void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
}

例如在以上代码中的变量a不是动态内存开辟的 ,所以就不能对变量a所在的内存空间用free进行释放

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

cpp 复制代码
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}

例如在以上代码中对p++后p就不再指向内存块的起始地址 ,这时再使用free释放内存空间程序就会崩溃

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

cpp 复制代码
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}

例如在以上代码中已经对malloc开辟的内存空间free一次,之后又free,并且之前也没将p置为空指针,这样程序就会崩溃

6.动态开辟内存忘记释放(内存泄漏)

cpp 复制代码
void test()
{
int *p = (int *)malloc(100);
 if(NULL != p)
 {
 *p = 20;
 }
}

int main()
{
test();
while(1);
}

例如在以上代码中在每次调用test函数时都使用malloc进行内存开辟,但每次都不对内存进行释放 ,这就会使得开辟后的内存空间一直占据在内存当中,程序一直执行开辟过的内存空间就再也找不到了,这就是内存泄漏。最终程序一直运行就会使得内存完全被占满,程序就会发生崩溃

切记:动态开辟的空间一定要释放,并且正确释放

5. 动态内存经典笔试题分析

5.1 题目1

cpp 复制代码
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}

请问运行Test 函数会有什么样的结果?

在以上代码在中Test中调用了GetMemory函数,参数为变量str,所以这时为传值调用,因为形参是实参的一份临时拷贝,所以这时的p不再是原来的str,只是将str的内容拷贝到p内。之后用malloc开辟100字节空间给p,就会使p里面存放内存空间的起始地址,但这时str内还是存放NULL,所以这时使用strcpy将hello world拷贝到str内就是对空指针的使用程序就会崩溃,同时malloc申请的内存空间一直没被释放还会造成内存泄漏

5.2 题目2:

cpp 复制代码
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}

请问运行Test 函数会有什么样的结果?

在以上代码中调用GetMemory时返回的是栈空间的地址 ,这时在出GetMemory局部范围时p所指向的内存空间就会还给操作系统,这时p就变为野指针,输出结果就是随机的

5.3 题目3:

cpp 复制代码
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}

请问运行Test 函数会有什么样的结果?

在以上代码当中先将指针str初始化为NULL,之后调用GetMemory时是传址调用,所以不会再拷贝一份原来的变量,GetMemory内用二级指针char** p来接收&str,这时*p就是和使用str一样,所以使用malloc开辟100字节的内存空间后就将起始地址传给str,之后再用strcpy将hello拷贝到str内,打印str,到此都是没错误的但最后没有对开辟的内存空间进行free释放,会造成内存泄漏

5.4 题目4:

cpp 复制代码
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}

请问运行Test 函数会有什么样的结果?

在以上代码中先使用malloc开辟了100字节的空间,并将起始地址传给str,再使用strcpy将hello拷贝到str的内存空间内,这时再free(str)后就存在问题,未将str置为空指针,这时再使用strcpy将world拷贝到str的内存空间内就形成对内存的越界访问所以要将以上代码改为正确就要在free(str)后加上str=NULL

6. 柔性数组

也许你从来没有听说过柔性数组(flexible array) 这个概念,但是它确实是存在的。
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做**『柔性数组』成员** 。
例如:

cpp 复制代码
struct st_type
{
int i;
int a[0];//柔性数组成员
};

有些编译器会报错无法编译可以改成:

cpp 复制代码
struct st_type
{
int i;
int a[];//柔性数组成员
};

6.1 柔性数组的特点

• 结构中的柔性数组成员前⾯必须⾄少⼀个其他成员。
• sizeof 返回的这种结构大小不包括柔性数组的内存。
• 包含柔性数组成员的结构用malloc ()函数进⾏内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

例如

cpp 复制代码
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
int main()
{
printf("%d\n", sizeof(type_a));//输出的是4
return 0;
}

6.2 柔性数组的使用

cpp 复制代码
#include <stdlib.h>
#include <stdio.h>
struct S
{
	int n;
	int arr[];
};

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 20*sizeof(int));
	if(ps == NULL)
	{
		perror("malloc()");
		return 1;
	}
	//使用这些空间
	ps->n = 100;
	int i = 0;
	for (i = 0; i < 20; i++)
	{
		ps->arr[i] = i + 1;
	}

	//调整ps指向空间的大小
	struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 40 * sizeof(int));
	if (ptr != NULL) 
	{
		ps = ptr;
		ptr = NULL;
	}
	else
	{
		return 1;
	}
	//使用
	for (i = 0; i < 40; i++)
	{
		printf("%d ", ps->arr[i]);
	}

	//释放空间
	free(ps);
	ps = NULL;

	return 0;
}

在以上代码还有什么方法可以实现同样的效果呢?
当结构体内的数组是指针时就可以写成以下形式

cpp 复制代码
#include <stdlib.h>
#include <stdio.h>
struct S
{
	int n;
	int* arr;
};

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	int*tmp = (int*)malloc(20*sizeof(int));
	if (tmp != NULL)
	{
		ps->arr = tmp;
	}
	else
	{
		return 1;
	}
	ps->n = 100;
	int i = 0;
	//给arr中的20个元素赋值为1~20
	for (i = 0; i < 20; i++)
	{
		ps->arr[i] = i + 1;
	}
	//调整空间
	tmp = (int*)realloc(ps->arr, 40*sizeof(int));
	if (tmp != NULL)
	{
		ps->arr = tmp;
	}
	else
	{
		perror("realloc");
		return 1;
	}
	//
	for (i = 0; i < 40; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	//释放
	free(ps->arr);
	ps->arr = NULL;

	free(ps);
	ps = NULL;

	return 0;
}

6.3 柔性数组的优势

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

第一个好处是:方便内存释放
如果我们的代码是在⼀个给别⼈用的函数中,你在里面做了⼆次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户⼀个结构体指针,用户做⼀次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎⽚。(其实,也没多高了,反你跑不了要用做偏移量的加法来寻址)

7. 总结C/C++中程序内存区域划分

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

相关推荐
XH华4 小时前
初识C语言之二维数组(下)
c语言·算法
南宫生4 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
sanguine__4 小时前
Web APIs学习 (操作DOM BOM)
学习
数据的世界016 小时前
.NET开发人员学习书籍推荐
学习·.net
四口鲸鱼爱吃盐7 小时前
CVPR2024 | 通过集成渐近正态分布学习实现强可迁移对抗攻击
学习
Uu_05kkq7 小时前
【C语言1】C语言常见概念(总结复习篇)——库函数、ASCII码、转义字符
c语言·数据结构·算法
OopspoO9 小时前
qcow2镜像大小压缩
学习·性能优化
嵌入式科普9 小时前
十一、从0开始卷出一个新项目之瑞萨RA6M5串口DTC接收不定长
c语言·stm32·cubeide·e2studio·ra6m5·dma接收不定长
A懿轩A9 小时前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列
居居飒9 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin