当前位置: 首页 > article >正文

C语言进阶5:动态内存管理

本章重点

  1. 为什么存在动态内存分配
  2. 动态内存函数:
    • malloc
    • free
    • calloc
    • realloc
  3. 常见的动态内存错误
  4. 笔试题
  5. C/C++程序的内存开辟
  6. 通讯录(动态数组改造)
  7. 柔性数组

    文章目录

    • 1.什么是动态内存分配
    • 2.动态内存函数:
      • 2.1 [malloc](https://legacy.cplusplus.com/reference/cstdlib/malloc)
      • 2.2 [free](https://legacy.cplusplus.com/reference/cstdlib/free)
      • 2.3 [calloc](https://legacy.cplusplus.com/reference/cstdlib/calloc)
      • 2.4 [realloc](https://legacy.cplusplus.com/reference/cstdlib/realloc)
    • 3.常见的动态内存错误
      • 3.1 对NULL指针的解引用操作
      • 3.2 对动态开辟空间的越界访问
      • 3.3 对非动态开辟内存使用free释放
      • 3.4 使用free释放一块动态开辟内存的一部分
      • 3.5 对同一块动态内存多次释放
      • 3.6 动态开辟内存忘记释放(内存泄漏)
    • 4.经典笔试题
      • 4.1 请问运行Test 函数会有什么样的结果?
      • 4.3 运行Test 函数会有什么样的结果?
      • 4.4 运行Test 函数会有什么样的结果?
    • 5.C/C++程序的内存开辟
    • 6.通讯录(动态数组改造)
      • contact.h - 函数声明
      • contact.c - 通讯录实现
      • test.c - 测试通讯录
    • 7.柔性数组
      • 7.1 柔性数组的特点:
      • 7.2 柔性数组的使用
      • 7.3 柔性数组的优势

1.什么是动态内存分配

我们已经掌握的内存开辟方式有:

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

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

  1. 空间开辟大小是固定的
  2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配

但是对于空间的需求,不仅仅是上述的情况。
有时候我们需要的空间大小在程序运行的时候才能知道,
那么数组在编译时开辟空间的方式就不能满足了。

2.动态内存函数:

2.1 malloc

void* malloc (size_t size);

这个函数就是向内存申请一块连续可用的空间,并返回这块空间的指针。

使用malloc函数需要引入头文件:stdlib.h

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

  1. 如果开辟成功,则返回一个指向开辟好空间的指针。
  2. 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  3. 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
  4. 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器
#include<stdlib.h>
#include<errno.h>
#include<string.h>
int main()
{
	int num = 0;//向内存申请4个字节的空间
	int arr[10];//向内存申请40个字节的空间
 
	int* p = (int*)malloc(40);//向内存申请40个字节的空间
	//我们一般用int*类型来接受malloc的返回值,
	//因为malloc返回值是void*,所以需要强制转换
 
	if (p == NULL)//如果申请失败会返回空指针,所以需要进行判断一次
	{
		printf("%s\n", strerror(errno));//一种返回错误的方法
		return 1;
	}
	return 0;
}

像 int num,int arr[10] 这些静态内存是在栈区里面开辟的
栈区里面开辟的空间,进栈时创建,出栈时释放

而像 malloc 这样的动态内存时在堆区里面开辟的
堆区里面开辟的空间,只有在程序结束时才自动释放,一般都需要用free函数手动释放

2.2 free

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

void free (void* ptr);

free函数用来释放动态开辟的内存。

  1. 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
  2. 如果参数 ptr 是NULL指针,则函数什么事都不做。

malloc和free都声明在 stdlib.h 头文件中。

int main()
{
	int* p = (int*)malloc(40);
 
	int* ptr = p;
 
	if (p == NULL)
	{
		printf("动态内存p申请失败\n");
		return 1}
	//使用:
	int i = 0;
	for (i = 0;i < 10;i++)
	{
		*p = i;
		p++;
	}
 
	free(p);//释放p是错的
	//因为p在使用的过程中发生了改变,不是动态申请空间的首元素地址
	//而free需要传入动态开辟空间的首地址,才能释放干净、
	
	//所以需要将首元素地址保留下来或用i来推进数组*(p + i) = i;
	free(ptr);
    
	ptr = NULL;//这里需要把ptr再置为空,否则会出现野指针
 
	return 0;
}

2.3 calloc

C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。原型如下:

void* calloc (size_t num, size_t size);

  1. 函数的功能是:
    把 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
  2. 与函数 malloc 的区别只在于:
    calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
    //这里一般都用sizeof(类型)来输入,因为不用平台各类大小可能不一样
	//跟malloc一样返回值是void*,需要强制类型转换
	int i = 0;
	if (p == NULL)
	{
		perror("calloc");//另一种返回错误的方法
		return 1;
	}
	for (i = 0;i < 10;i++)
	{
		*(p + i) = i; //这与数组类似,可以通过 *(p+i)的方式找到下标元素
		//这样做的好处是防止p地址的改变
	}
	free(p);
    p=NULL;
 
	return 0;
}

2.4 realloc

realloc函数的出现让动态内存管理更加灵活。

有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,
那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。
那 realloc 函数就可以做到对动态开辟内存大小的调整。

函数原型如下:

void* realloc (void*ptr, size_t size);
把原来ptr的内存调整为size的大小

int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)//判断malloc是否开辟失败
	{
		return 1;
	}
	int i = 0;
	for (i = 0;i < 10;i++)
	{
		printf("%d ", *(p + i) = i);
	}
 
	//现在空间用完了,如果还需要增加空间
	int* ptr = (int*)realloc(p, 80);
	//当realloc开辟失败的时候,返回的是NULL
 
	if (ptr != NULL)//这里现用ptr来进行验证,看realloc是否开辟成功
	{
		p = ptr;//开辟成功后再将其地址转给p,否则开辟失败的话,p原本开辟的空间也会丢失
		ptr = NULL;//如果下面的代码不再用ptr,则将其置为空
	}
 
	free(p);
	p = NULL;
 
	return 0;
}
  1. ptr 是要调整的内存地址
  2. size 调整之后新大小
  3. 返回值为调整之后的内存起始位置。
  4. 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。
  5. realloc在调整内存空间的是存在两种情况:

情况1:原有空间之后又足够大的空间
当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。

情况2:原有空间之后没有足够大的空间

当是情况2 的时候,原有空间之后没有足够多的空间时,
扩展的方法是:
在堆空间上另找一个合适大小的连续空间来使用。
这样函数返回的是一个新的内存地址。

3.常见的动态内存错误

3.1 对NULL指针的解引用操作

int main()
{
	int* p = (int*)malloc(INT_MAX / 4);
	*p = 20;//如果p的值是NULL,就会有问题
	free(p);
    p=NULL;
 
    return 0;
}

这里应该先要判断malloc函数开辟空间是否成功,calloc和realloc也一样

正确写法:

int main()
{
	int* p = (int*)malloc(10);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	else
	{
		*p = 5;
	}
	free(p);
	p = NULL;
 
    return 0;
}

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

int main()
{
	int* p = (int*)malloc(20);
	if (p == NULL)
		return 1;
	//使用
	int i = 0;
	for (i = 0;i < 20;i++)
	{
		*(p + i) = i;//注意malloc开辟的空间
		//这里会发生越界访问
	}
	//释放
	free(p);
	p = NULL;
 
	return 0;
}

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

int main()
{
   int num = 10;
   int* p = &num;

   free(p);//只能释放动态的,不然会崩掉
   p = NULL;  

   return 0;
}

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

int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
		return 1;
 
	int i = 0;
	for (i = 0;i < 20;i++)
	{
		*p = i;
		p++;
	}
	
	free(p);
	//这里p的起始地址发生了改变,不再指向动态内存的起始位置,会发生错误
	p = NULL;
 
	return 0;
}

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

int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
		return 1;
 
	int i = 0;
	for (i = 0;i < 20;i++)
	{
		*(p + i) = i;
	}
	
	free(p);
	//如果将p=NULL那么下面的free将不会发生错误
    //所以在每次free之后记得置空
 
	//....
	
 
	free(p);//如果两次释放相同内存也会出错
	p = NULL;
 
	return 0;
}

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

int* get_memory()
{
	int* p = (int*)malloc(40);
	//......
	return p;
}
int main()
{
	int* ptr = get_memoty();
 
	//这里在调用后记得及时free(ptr),否则会发生错误
	//忘记释放不再使用的动态开辟的空间会造成内存泄漏。
	return 0;
}
切记: 动态开辟的空间一定要释放,并且正确释放!

4.经典笔试题

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

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;
}

会出错,
错误1:没有对动态开辟的内存进行释放
错误2:GetMemory(str); str传入的形参,所以str不会发生改变。
错误3:strcpy(str, “hello world”); 因为str仍为空,这里会发生内存访问出错,
修改:

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 运行Test 函数会有什么样的结果?
```c
char* GetMemory(void)
{
	char p[] = "hello world";
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{
	Test();
	return 0;
}

会打印随机值

因为 char p[] = “hello world”; 再出了其作用域GetMemory函数后会被销毁

return p;返回的是栈空间的地址。容易出错!

其返回的地址里面的东西将被系统删除

这种问题叫做:返回栈空间地址问题!!!

记得栈上的空间,出了作用域会被销毁

如何修改?

可以给char p[] = "hello world"前加上 static,以防止被销毁

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

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

会打印hello,但是没有free(str)会发生内存泄漏

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

void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL) //str已经被free掉,而且没有置空,所以是野指针
	{                //会发生非法访问
		strcpy(str, "world");
		printf(str);
	}
}
int main()
{
	Test();
	return 0;
}

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

请添加图片描述

6.通讯录(动态数组改造)

test.c - 测试通讯录
contact.c - 通讯录实现
contact.h - 函数声明

contact.h - 函数声明

#pragma once
#include<stdio.h>
#include<string.h>
#include<assert.h>
#include<stdlib.h>
#define NAME_MAX 20
#define SEX_MAX 5
#define ADDR_MAX 30
#define TELE_MAX 30

#define DEFAULT_SZ 3
#define INC_SZ 2
typedef struct PeoInfo
{
	char name[NAME_MAX];
	int age;
	char sex[SEX_MAX];
	char addr[ADDR_MAX];
	char tele[TELE_MAX];
}PeoInfo;

typedef struct Contact
{
	PeoInfo* data;//指向存放人的信息的空间
	int sz;//当前已经存放信息个数
	int capacity;//当前通讯录的最大容量
}Contact;


//初始化通讯录
void InitContact(Contact* pc);
//销毁通讯录
void DestroyContact(Contact* pc);
//增加联系人
void ADDContact(Contact* pc);

//删除指定联系人
void DelContact(Contact* pc);

//显示通讯录中的信息
void ShowContact(const Contact* pc);

//查找指定联系人
void SearchContact(const Contact* pc);

//修改指定联系人
void ModifyContact(const Contact* pc);

contact.c - 通讯录实现

#define _CRT_SECURE_NO_WARNINGS 1
#include"contact.h"
void InitContact(Contact* pc)
{
	assert(pc);
	pc->sz = 0;
	PeoInfo* ptr = (PeoInfo*)calloc(DEFAULT_SZ,sizeof(PeoInfo));
	if (ptr == NULL)
	{
		perror("InitContact::calloc");
		return;
	}
	pc->data =ptr ;
	pc->capacity = DEFAULT_SZ;

}
void DestroyContact(Contact* pc)
{
	assert(pc);
	free(pc->data);
	pc ->data=NULL;
	pc->capacity = 0;
	pc->sz = 0;
	pc = NULL;
}

int FindByName(const Contact* pc, char name[])
{
	assert(pc);
	int i = 0;
	for (i = 0; i < pc->sz; i++)
	{
		if (strcmp(pc->data[i].name, name) == 0)
		{
			return i;
		}
	}
	return -1;
}

void check_capacity(Contact* pc)
{
	if (pc->sz == pc->capacity)
	{
		//增加容量
		PeoInfo* ptr= (PeoInfo*)realloc(pc->data, (pc->capacity+ INC_SZ)*sizeof(PeoInfo));
		if (ptr == NULL)
		{
			perror("check_capacity::reallo");
			return;
		}
		pc->data = ptr;
		pc->capacity += INC_SZ;
		printf("增容成功!\n");

	}
}
void ADDContact(Contact* pc)
{
	assert(pc);
	check_capacity(pc);

	
	//增加一个人信息
	printf("请输入名字:>");
	scanf("%s", pc->data[pc->sz].name);
	printf("请输入年龄:>");
	scanf("%d", &(pc->data[pc->sz].age));
	printf("请输入性别:>");
	scanf("%s", pc->data[pc->sz].sex);
	printf("请输入地址:>");
	scanf("%s", pc->data[pc->sz].addr);
	printf("请输入电话:>");
	scanf("%s", pc->data[pc->sz].tele);
	pc->sz++;
	printf("添加成功!\n");
}
void ShowContact(const Contact* pc)
{
	assert(pc);
	int i = 0;
	printf("|%-20s|%-4s\t|%-5s\t|%-20s\t|%-12s|\n","名字","年龄","性别","地址","电话");
	printf("|--------------------|----------|-------|-----------------------|------------|\n");
	for (i = 0; i < pc->sz; i++)
	{
		printf("|%-20s|%-4d\t|%-5s\t|%-20s\t|%-12s|\n", pc->data[i].name, pc->data[i].age, pc->data[i].sex, pc->data[i].addr, pc->data[i].tele);
	}
}

void DelContact(Contact* pc)
{
	assert(pc);
	char name[NAME_MAX] = { 0 };
	if (pc->sz == 0)
	{
		printf("通讯录为空,无法删除\n");
		return;
	}
	//删除
	//找到要删除的人
	printf("请输入要删除的人:>");
	scanf("%s",name);
	int ret=FindByName(pc, name);
	if (-1 == ret)
	{
		printf("删除信息不存在\n");
		return;
	}
	int i = 0;
	//删除
	for (i = ret; i < pc->sz - 1; i++)
	{
		pc->data[i] = pc->data[i + i];
	}
	printf("删除成功!\n");
}

void SearchContact(const Contact* pc)
{
	assert(pc);
	char name[NAME_MAX] = { 0 };
	printf("请输入查找人姓名:>");
	scanf("%s", name);
	int pos = FindByName(pc, name);
	if (-1 == pos)
	{
		printf("查找信息不存在\n");
		return;
	}
	printf("|%-20s|%-4s\t|%-5s\t|%-20s\t|%-12s|\n", "名字", "年龄", "性别", "地址", "电话");
	printf("|--------------------|----------|-------|-----------------------|------------|\n");
	printf("|%-20s|%-4d\t|%-5s\t|%-20s\t|%-12s|\n", pc->data[pos].name, pc->data[pos].age, pc->data[pos].sex, pc->data[pos].addr, pc->data[pos].tele);
}

void ModifyContact(const Contact* pc)
{
	assert(pc);
	char name[NAME_MAX] = { 0 };
	printf("请输入修改人姓名:>");
	scanf("%s", name);
	int pos = FindByName(pc, name);
	if (-1 == pos)
	{
		printf("查找信息不存在\n");
		return;
	}
	printf("请输入名字:>");
	scanf("%s", pc->data[pos].name);
	printf("请输入年龄:>");
	scanf("%d", &(pc->data[pos].age));
	printf("请输入性别:>");
	scanf("%s", pc->data[pos].sex);
	printf("请输入地址:>");
	scanf("%s", pc->data[pos].addr);
	printf("请输入电话:>");
	scanf("%s", pc->data[pos].tele);
	printf("修改成功!\n");
}

test.c - 测试通讯录

#define _CRT_SECURE_NO_WARNINGS 1
#include"contact.h"

void menu()
{
	printf("***********************************\n");
	printf("*****   1. add      2. del    *****\n");
	printf("*****   3. search   4. modify *****\n");
	printf("*****   5. show     6. sort   *****\n");
	printf("*****   0. exit               *****\n");
	printf("***********************************\n");
}
int main()
{
	int input;
	//创建通讯录
	Contact con;
	//初始化通讯录
	InitContact(&con);
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			ADDContact(&con);
			break;
		case 2:
			DelContact(&con);
			break;
		case 3:
			SearchContact(&con);
			break;
		case 4:
			ModifyContact(&con);
			break;
		case 5:
			ShowContact(&con);
			break;
		case 6:
			break;
		case 0:
			DestroyContact(&con);
			printf("退出通讯录\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);
	return 0;
}

7.柔性数组

##blue##
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

struct s
{
	int n;
	float s;
	int arr[];//柔性数组成员
	//结构体最后一个成员为数组,且未知大小
	//若结构体成员只有一个,且为数组时,则不算时柔性数组 
};
struct a
{
	int a[];//这就不是柔性数组
};
int main()
{
	printf("%d", sizeof(struct s));//8,柔性数组的大小并没有被算进去
	return 0;
}

7.1 柔性数组的特点:

  1. 结构中的柔性数组成员前面必须至少一个其他成员。
  2. sizeof 返回的这种结构大小不包括柔性数组的内存。
  3. 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

7.2 柔性数组的使用

struct S
{
	int n;
	float s;
	int arr[];//[柔性]数组成员
              //差不多就是利用malloc让该数组可大可小
};
 
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S)+sizeof(int)*4);
	if (ps == NULL)
	{
		return 1;
	}
	ps->n = 100;
	ps->s = 5.5f;
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		scanf("%d", &(ps->arr[i]));
	}
	printf("%d %f\n", ps->n, ps->s);
	for (i = 0; i < 4; i++)
	{
		printf("%d ", ps->arr[i]);
	}
 
	//调整
	struct S*ptr = (struct S*)realloc(ps, sizeof(struct S)+10*sizeof(int));
	if (ptr == NULL)
	{
		return 1;
	}
	else
	{
		ps = ptr;
	}
 
	//释放
	free(ps);
	ps = NULL;
 
	return 0;
}

7.3 柔性数组的优势

上述的柔性数组的创建也可以用一下代码实现:

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;
	p->p_a = (int*)malloc(p->i * sizeof(int));
	//业务处理
	for (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开辟的空间里面继续malloc一块空间,
并且在free的时候,需要将两次开辟的空间全都给free掉

柔性数组的优势:

  1. 使用的人不清楚
    如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。
    所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉
  2. 这样有利于访问速度
    连续的内存有益于提高访问速度,也有益于减少内存碎片。

http://www.kler.cn/a/409258.html

相关文章:

  • 前端三剑客(二):CSS
  • python-MatchObject对象方法
  • 一文学习开源框架OkHttp
  • python Flask指定IP和端口
  • C++:用红黑树封装map与set-1
  • linux-进程间通信
  • Python Selenium:Web自动化测试与爬虫开发
  • C语言指针作业
  • 区块链应用到银行的优势
  • 如何调试 chrome 崩溃日志(MAC)
  • [译]Elasticsearch Sequence ID实现思路及用途
  • 快速了解RDD的创建与处理过程
  • Jedis存储一个-以String的形式的对象到Redis
  • 【Go】-go中的锁机制
  • 【MATLAB源码-第218期】基于matlab的北方苍鹰优化算法(NGO)无人机三维路径规划,输出做短路径图和适应度曲线.
  • MySQL中的ROW_NUMBER窗口函数简单了解下
  • 【网络安全设备系列】3、IPS(入侵防御系统)
  • OpenCV4.9 dnn人脸识别
  • Linux的开发工具(三)
  • 设计模式之 命令模式
  • C++不完整类型(Incomplete Type)的检测与避免
  • Spring Boot 3.x + OAuth 2.0:构建认证授权服务与资源服务器
  • 浅谈TLP184小型平面光耦
  • docker 卸载与安装
  • 生成对抗网络模拟缺失数据,辅助PAMAP2数据集仿真实验
  • HTML 表单实战:从创建到验证