【C语言】_12.动态内存管理

目录

1.为什么存在动态内存分配

2.动态内存函数的介绍

2.1 malloc和free

2.2.calloc

2.3 realloc

3.常见的动态内存错误

3.1 对NULL指针的解引用操作

 3.2 对动态内存开辟空间的越界访问

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

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

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

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

4.例题

 5.C/C++程序的内存开辟

 6.柔性数组


正文:


1.为什么存在动态内存分配

现在我们了解到的内存开辟方式有:

int a=20;
//在栈空间上开辟了四个字节

char arr[10]={0};
//在栈空间上开辟10个字节的连续空间

但上述内存开辟空间的方式具有两个特点:

① 空间开辟的大小是固定的;

② 数组字声明的时候必须制定数组的长度,它所需要的内存在编译时分配。

但是在很多情况下我们需要的内存空间大小需要在程序运行的时候才能知道,这种开辟的方式就难以满足更改空间大小的要求。所以我们引入动态内存分配;

常见的动态内存管理函数有:malloc free callloc realloc等等。

【C语言】_12.动态内存管理_第1张图片


2.动态内存函数的介绍

2.1 malloc和free

【C语言】_12.动态内存管理_第2张图片

【C语言】_12.动态内存管理_第3张图片

#include
#include
int main()
{
	//申请空间
	int* ptr=(int*)malloc(40);
	int* p = ptr;
	if (p == NULL)
	{
		perror("malloc");
	}
	//
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*p = i;
		p++;
	}
	//释放空间
	free(ptr);
	ptr = NULL;
	return 0;
}

ps:

(1)在上述代码中,创建int* 类型的p变量的意义是:防止初始化后ptr指向的空间发生变化,无法对应地址进行空间释放;

(2)free(ptr); 操作后再进行ptr=NULL;的操作是为了防止野指针的出现与非法访问;

(3)malloc向堆区申请空间也可能会失败:
【C语言】_12.动态内存管理_第4张图片

 (4)当我们不释放动态申请的内存时,动态申请的内存由操作系统自动回收,若程序不结束,则动态内存不会自动回收,就会形成内存泄漏;


2.2.calloc

【C语言】_12.动态内存管理_第5张图片

调试打开监视内存,可以看到已全部初始化为0:

【C语言】_12.动态内存管理_第6张图片

#include
#include
int main()
{
	int* p=(int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("calloc");
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", * (p + i));
	}
	free(p);
	ptr = NULL;
	return 0;
}

2.3 realloc

【C语言】_12.动态内存管理_第7张图片

 当reallc函数追加空间时,会遇到两种情况:

【C语言】_12.动态内存管理_第8张图片

 第一种情况:在p指向的原有空间后有足够空间可以开辟新的空间,则realloc函数直接在原有空间后直接追加空间,并返回起始地址p;

第二种情况:在p指向的原有空间后没有足够的空间能满足新开辟的要求,则realloc函数在内存中重新寻找足够大的空间,找到后将原有空间内容移至新空间后返回新开辟的空间的起始地址,并且,realloc会主动free原有空间;

#include
#include
int main()
{
	int* p=(int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc");
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	//空间不够,希望能放20个元素->考虑扩容
	int*pc=(int*)realloc(p, 80);
	if (pc != NULL)
	{
		p = pc;
	}
	free(p);
	p = NULL;
	return 0;
}

3.常见的动态内存错误

3.1 对NULL指针的解引用操作

错误示例:

【C语言】_12.动态内存管理_第9张图片

解决方法:对malloc函数返回值进行判空:

#include
#include
int main()
{
	int* p = (int*)malloc(1000);
	if (p == NULL)
	{
		perror("malloc");
	}
	int i = 0;
	for (i = 0; i < 250; i++)
	{
		*(p + i) = i;
	}
	free(p);
	p = NULL;
	return 0;
}

 3.2 对动态内存开辟空间的越界访问

错误示例:

【C语言】_12.动态内存管理_第10张图片

 解决方法:对内存边界进行检查;

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

错误示范:

#include
int main()
{
	int a = 10;
	int* p = &a;
	//...
	free(p);
	p = NULL;
	return 0;
}

对于栈区的局部变量,其内存由编译器释放。

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

错误示例:

#include
int main()
{
	int* p = (int*)malloc(100);
	if (p == NULL)
	{
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*p = i;
		i++;
	}
	free(p);            //必须从起始位置开始释放,不能从中间某一段进行释放
	p = NULL;
	return 0;
}

解决方法:创建临时指针变量储存初始地址:

#include
int main()
{
	int* p = (int*)malloc(100);
	int* ptr = p;
	if (p == NULL)
	{
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*p = i;
		i++;
	}
	free(ptr);
	ptr = NULL;
	return 0;
}

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

错误示例:

#include
int main()
{
	int* p = (int*)malloc(1000);
	if (p == NULL)
		return 1;
	free(p);
	//...
	free(p);
	p = NULL;

	return 0;
}

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

错误示范:

#include
void test()
{
	int* p = (int*)malloc(100);
	//...
}
int main()
{
	test();
	//...
	return 0;
}

4.例题

4.1 

【C语言】_12.动态内存管理_第11张图片

 修改后可正常运行的代码为:

#include
#include
#include
void GetMemory(char** p)
{
	*p = (char*)malloc(100);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str);
	strcpy(str, "hello world");
	printf(str);
	free(str);
	str = NULL;
}
int main()
{
	Test();
	return 0;
}

4.2

【C语言】_12.动态内存管理_第12张图片

注:返回栈空间地址是不需要我们回收的,操作系统自动回收,故而易出现野指针问题;

 4.3

#include
#include
#include
void GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
}
void Test()
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
	free(str);
	str = NULL;
}
int main()
{
	Test();
	return 0;
}
//代码正确

4.4

 【C语言】_12.动态内存管理_第13张图片

 5.C/C++程序的内存开辟

【C语言】_12.动态内存管理_第14张图片

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

 6.柔性数组

在C99标准中,结构中最后一个元素允许是位置大小的数组,这就叫做柔性数组成员:

如:

struct S1
{
	int num;
	double d;
	int arr[];  //柔性数组成员
};
struct S2
{
	int num;
	double d;
	int arr[0];//柔性数组成员
};

//以上两种写法均是正确的柔性数组写法。具体写法取决于编译器允许的书写形式

6.1 柔性数组的特点

(1)结构中的柔性数组成员前面必须至少有一个其他成员;

(2)sizeof返回结构大小时,不包括柔性数组成员内存;

【C语言】_12.动态内存管理_第15张图片

 (3)包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小;

6.2 柔性数组的使用

#include
#include
struct S3
{
	int num;
	int arr[0];
};
int main()
{
	struct S3* ps=(struct S3*)malloc(sizeof(struct S3)+40);
  //初步希望arr数组内放10个整型 即为柔性数组预留内存
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	//赋值
	ps->num = 100;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->arr[i] = i;
	}
	//打印
	for (i = 0; i < 10; i++)
	{
		printf("%d ",ps->arr[i]);
	}
	//扩容
	struct S3* ptr=(struct S3*)realloc(ps, sizeof(struct S3) + 80);
	if (ptr == NULL)
	{
		perror("realloc");
		return 1;
	}
	else
	{
		ps = ptr;
	}
	for (i = 10; i < 20; i++)
	{
		ps->arr[i] = i;
	}
	for (i = 0; i < 20; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	//释放
	free(ps);
	ps = NULL;
	return 0;
}

6.3 柔性数组的优势

对于数组可大可小的要求,我们也可以通过数组指针实现:

#include
#include
struct S4
{
	int num;
	int* arr;
};
int main()
{
	struct S4* ps=(struct S4*)malloc(sizeof(struct S4));
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	ps->arr = (int*)malloc(40);
	if (ps->arr == NULL)
	{
		free(ps);
		ps = NULL;
		return 1;
	}
	free(ps->arr);
	ps->arr = NULL;
	free(ps);
	ps = NULL;
	return 0;
}

既然如此,为何还要创建柔性数组。

接下来阐明柔性数组的优势:

(1)方便内存释放:
一次性开辟的空间是连续存放的,故而一次性可以完全释放;而通过指针实现的可变长数组,则需要多次释放;

(2)有利于访问速度:

在访问一个数据时,为方便访问,该数据周围的数据会一同加载至寄存器中,当CPU访问时,在寄存器中命中率就会较高,故而连续内存有利于提高访问速度,而分散存放内存的访问速度相对较低;

(3)有利于减少内存碎片:

连续存放的内存有利于提高内存利用率;


||终

你可能感兴趣的:(C语言,函数,c语言,开发语言)