第十五讲:动态内存管理

目录

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

2、malloc函数和free函数

2.1、malloc函数

2.2、free函数

3、calloc函数和realloc函数

3.1、calloc函数

3.2、realloc函数

4、柔性数组

4.1、柔性数组的特点

4.2、柔性数组的使用

4.3、柔性数组的优势

5、总结C/C++中程序内存区域划分

5.1、栈区

5.2、堆区

5.3、(数据段)静态区

5.4、代码段

6、常见的动态内存的错误

6.1、对NULL指针的解引用操作

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

6.3、对⾮动态开辟内存使⽤free释放

6.4、使⽤free释放⼀块动态开辟内存的⼀部分

6.5、对同⼀块动态内存多次释放

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

7、动态内存经典笔试题分析

7.1、题目1

7.2、题目2

7.3、题目3

7.4、题目4


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

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

int a = 10;
char arr[10] = { 0 };

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

1·空间开辟的大小是固定的。

2·数组在声明的时候,必须指定数组的长度,数组空间一旦确定了大小就不能调整。

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

C语言引入了动态内存开辟,使得程序员自己可以申请和释放空间,这样就变得相对灵活了。

2、malloc函数和free函数

2.1、malloc函数

函数原型为:

void *malloc(size_t size)

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

如果开辟成功则返回一个指向开辟的空间的指针。

如果失败则返回NULL,因此这个函数的返回值要检查。

这个函数的返回值类型为void*,所以在具体使用的时候使用者自己来决定开辟空间的类型。

如果参数size为零,函数的行为是未定义的,取决于编译器。

size的单位是字节。使用这个函数时要包含stdlib.h这个头文件。

这个函数申请到的空间是不会初始化空间内容的。

代码实例:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* arr = malloc(sizeof(int) * 10);
	if (arr == NULL)
	{
		perror("fail malloc");
		return -1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(arr + i));
	}

	free(arr);
	arr = NULL;
	return 0;
}

运行结果为:

2.2、free函数

C语言中这个函数是专门用来做动态内存的释放和回收的。函数原型为:

void free(void *ptr)

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

如果参数ptr为空指针,则该函数什么都不做。使用这个函数时要包含stdlib.h这个头文件。

代码实例:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int arr[10] = {0};
	int* ptr = (int*)malloc(sizeof(int) * 10);
	if (ptr == NULL)
	{
		perror("fail malloc");
		return -1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(ptr + i) = 0;
	}

	free(ptr);
	ptr = NULL;

	return 0;
}

通过监视我们可以看到:

可见free后ptr所指向的内容被释放掉了。

3、calloc函数和realloc函数

3.1、calloc函数

函数原型为:

void *calloc(size_t nitems, size_t size)

这个函数的功能是为nitems个大小为size的元素开辟空间,并且将空间的每个字节初始化为0,这一点与上面的malloc函数不同。空间开辟失败也会返回空指针。

代码实例:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));

	}
	free(p);
	p = NULL;
	return 0;
}

运行结果为:

3.2、realloc函数

有时候我们会发现空间开辟大了,或者是小了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整,realloc函数就可以做到对动态内存开辟大小的调整。

函数原型为:

void *realloc(void *ptr, size_t size)

ptr是要调整的地址,size为调整之后大小。

调整失败会返回空指针。

参数ptr为空指针,但size不为0,那么行为就等于malloc函数。

参数size为0,则realloc函数的行为free函数,此时,原有的指针已经被free掉了,不能再使用了,此时函数的返回值为NULL;

函数的返回值返回调整后的空间的内存地址,但要注意的是,这个返回的地址可能与调整前的地址相同也可能不同。

realloc函数调整空间是有两种情况的

情况1·原有空间之后有足够大的空间。

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

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

当是情况2 的时候,原有空间之后没有⾜够多的空间时,扩展的⽅法是:在堆空间上另找⼀个合适⼤⼩ 的连续空间来使⽤。这样函数返回的是⼀个新的内存地址。会将原有空间的内容拷贝到新内存中,释放旧的内存空间。

realloc函数的使用要注意一下:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* ptr = (int*)malloc(sizeof(int)*10);
	if (ptr == NULL)
	{
		perror("fail malloc");
		return -1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(ptr+i));
	}
	int* p = NULL;
	p = (int*)realloc(ptr, sizeof(int) * 250);
	//先将realloc函数的返回值放在p中,不为NULL,在放ptr中
	if (p != NULL)
	{
		ptr = p;
	}
	free(ptr);

	return 0;
}

4、柔性数组

在c99中结构体中最后一个元素允许是大小未知的数组,这个数组就是柔性数组成员

例如:

#include<stdio.h>
struct type
{
	int i;
	int arr[];//柔性数组成员
};

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

struct type2
{
	int j;
	int arr2[0];//柔性数组成员
};

4.1、柔性数组的特点

1·结构中的柔性数组成员前⾯必须⾄少⼀个其他成员。

2·sizeof 返回的这种结构⼤⼩不包括柔性数组的内存。

3·包含柔性数组的结构体使用malloc函数进行动态内存分配,并且分配的内存空间应大于结构体的大小以适应柔性数组的大小。

例如:

#include<stdio.h>
struct type1
{
	int i;
	int arr1[];//柔性数组成员
};
struct type2
{
	int j;
	int arr2[0];//柔性数组成员
};
int main()
{
	printf("%d %d", sizeof(struct type1), sizeof(struct type2));
	return 0;
}

运行结果为:

4.2、柔性数组的使用

#include<stdio.h>
#include<stdlib.h>

typedef struct type
{
	int i;
	int arr[];
}Type;

int main()
{
	Type* pf = malloc(sizeof(int) + sizeof(int) * 10);
	if (pf == NULL)
	{
		perror("fail malloc");
		return -1;
	}
	pf->i = 10;
	for (int i = 0; i < 10; i++)
	{
		pf->arr[i] = i + 1;
	}
	free(pf);
	pf = NULL;
	return 0;
}

这样相当于柔性数组arr获得了10个整形的连续空间

4.3、柔性数组的优势

上面的Type也可以设计成下面的结构,也能完成同样的效果:

#include<stdio.h>
#include<stdlib.h>

typedef struct type
{
	int i;
	int* arr;
}Type;

int main()
{
	Type test = { 0 };
	test.i = 10;
	test.arr = (int*)malloc(sizeof(int) * 10);
	if (test.arr == NULL)
	{
		perror("fail malloc");
		return -1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		test.arr[i] = i + 1;
	}
	free(test.arr);
	test.arr = NULL;

	return 0;
}

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

第⼀个好处是:⽅便内存释放 如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯做了⼆次内存分配,并把整个结构体返回给⽤ ⼾。⽤⼾调⽤free可以释放结构体,但是⽤⼾并不知道这个结构体内的成员也需要free,所以你不能 指望⽤⼾来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返 回给⽤⼾⼀个结构体指针,⽤⼾做⼀次free就可以把所有的内存也给释放掉。

第⼆个好处是:这样有利于访问速度. 连续的内存有益于提⾼访问速度,也有益于减少内存碎⽚。(其实,我个⼈觉得也没多⾼了,反正你 跑不了要⽤做偏移量的加法来寻址)

5、总结C/C++中程序内存区域划分

内存分配的几个区域:

5.1、栈区

在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时 这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内 存容量有限。 栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。

5.2、堆区

⼀般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配⽅ 式类似于链表。

5.3、(数据段)静态区

存放全局变量、静态数据。程序结束后由系统释放。

5.4、代码段

存放函数体(类成员函数和全局函数)的⼆进制代码。

实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。 但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序 结束才销毁 所以生命周期变长。

6、常见的动态内存的错误

6.1、对NULL指针的解引用操作

void test()
{
	int* p = (int*)malloc(INT_MAX / 4);
	*p = 20;//如果p的值是NULL,就会有问题,所以在使用前应该进行判断
	free(p);
}

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

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

6.3、对⾮动态开辟内存使⽤free释放

void test()
{
	int a = 10;
	int* p = &a;
	free(p);//不可对非动态开辟的内存使用free
}

6.4、使⽤free释放⼀块动态开辟内存的⼀部分

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

6.5、对同⼀块动态内存多次释放

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

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

void test()
{
	int* p = (int*)malloc(100);
	if (NULL != p)
	{
		*p = 20;
	}
}
int main()
{
	test();
	while (1);
}

忘记释放不再使⽤的动态开辟的空间会造成内存泄漏。

以上代码仅仅只是介绍一些常见的错误,并非完整的代码。

7、动态内存经典笔试题分析

7.1、题目1

void GetMemory(char* p)
{
	p = (char*)malloc(100);//开辟的空间未能释放。
}
void Test(void)
{
	char* str = NULL;
	GetMemory(str);//这里本质上为传值调用,函数结束后str仍为空指针。
	strcpy(str, "hello world");
	printf(str);
}

7.2、题目2

char* GetMemory(void)
{
	char p[] = "hello world";
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();//出函数后p所指向的空间已经被释放了,所以str也就成野指针了。
	printf(str);
}

7.3、题目3

void GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
}
//未去释放动态开辟的内存空间

7.4、题目4

void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);           //释放完空间后,要把str给置为空指针,否则可能会出现问题。
	if (str != NULL)
	{
		strcpy(str, "world");//str已经在前面释放了,所以这里是非法访问。
		printf(str);
	}
}

到这里基本上就说完了,欢迎在评论区留言。

  • 15
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值