解密C语言内存分配奥秘,遨游动态内存管理海洋

发布于:2024-04-30 ⋅ 阅读:(31) ⋅ 点赞:(0)

目录

一.C语言内存分区

1.栈区 

2.堆区

3.全局(静态)区

4.常量区

5.代码区

二.动态内存管理

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

2.malloc和free

3.calloc和realloc

3.1calloc函数

3.2realloc函数

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

4.1对NULL指针的解引用操作

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

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

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

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

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

6.柔性数组

 6.2柔性数组的特点

6.3柔性数组的使用

6.4柔性数组的优势


一.C语言内存分区

C语言内存区从低地址到高地址分为代码区、常量区、全局(静态)区、堆区、栈区。

1.栈区 

栈区介绍

  • 栈区由编译器自动分配释放,由操作系统自动管理,无须手动管理
  • 栈区上的内容只在函数范围内存在,当函数运行结束,这些内容也会被自动销毁。
  • 栈区高地址向低地址使用,其大小在编译时确定,速度快,但自由性差,最大空间小。
  • 栈区先进后出原则,即先入栈的最后出栈;就如同进屋子,乌泱泱涌进去一堆人,最先进去的在里面,最后进去的在门口;先进去的人要等门口的人出去了才能出去。

栈区存放内容

  • 临时创建的局部变量const定义的局部变量
  • 函数调用和返回时,参数和返回值

2.堆区

堆区介绍

  • 堆区由程序员分配内存和释放。
  • 堆区低地址向高地址方向生长,其大小由系统内存/虚拟内存上限决定,速度较慢但自由行高,空间也大。

3.全局(静态)区

全局(静态)区介绍

  • 存储全局变量和静态变量
  • 全局段由.bss段和.data段组成

其中

  • .bss段存储未初始化或初始化为0的静态变量和全局变量,
  • .bss段不占用可执行文件的空间,内容由操作系统初始化
  • .data段存储已初始化的静态变量和全局变量
  • .data段占用可执行文件空间,内容由程序初始化。

4.常量区

  • 字符串,数字等常量存放在常量区
  • const修饰的全局变量存放在常量区
  • 在程序运行期间,常量区的内容不可更改

5.代码区

  • 我们写的代码存放在代码区,其内容不能修改
  • 字符串常量和define定义的常量也可能存放在代码区

二.动态内存管理

了解了C语言的内存分区,现在我们就来学习一下动态内存管理。动态内存函数在<stdlib.h>库中首先,为什么要有动态内存分配?

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

我们已经掌握的内存开辟方式是在栈上开辟的,而由上分可得知,这个东西会被销毁掉。而且这种开辟空间的方式还有两个特点

  • 空间开辟大小是固定的
  • 数组在声明的时候,必须指定数组的长度,数组空间无法修改。
	//在栈上开辟空间
	int a = 1;
	int arr[10];//在栈上开辟10个连续的空间

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

于是,C语言引入了动态内存开辟,让程序员自己可以申请和释放空间。而动态内存开辟操作的内存空间是堆区。

2.malloc和free

C语言提供了一个动态内存开辟的函数:

	void* malloc(size_t size);

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

  • 如果开辟成功,则返回一个指向开辟好的空间的指针。
  • 如果开辟失败,则返回一个NULL指针,因此我们需要检查malloc的返回值
  • 返回值的类型由自己决定
  • 如果size为0,则malloc的行为是标准中未定义的,取决于编译器。

相对应的,C语言也提供了另外一个函数free,专用用来释放和回收动态开辟的内存。

函数原型如下:

	void* free(void* ptr);
  •  如果参数ptr指向的空间不是动态开辟的,则free函数的行为是未定义的
  • 如果参数ptr是NULL指针,则函数则什么事都不做。

现在我们使用一下这个函数

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* ptr = (int*)malloc(10 * sizeof(int));//开辟10个整型空间
	if (NULL != ptr)//判断动态内存有没有开辟成功
	{
		int i = 0;
		for (i; i < 10; i++)
		{
			*(ptr + i) = i;
		}
	}
	free(ptr);//释放ptr所指向的动态内存
	ptr = NULL;//及时置空,防止别人使用
	return 0;
}

3.calloc和realloc

3.1calloc函数

calloc函数的原型如下:

void* calloc(size_t num, size_t size);
  • 函数功能是为num个大小为size的元素开辟一块空间,并把这块空间的字节全部初始化为0。
  • 这个函数开辟的空间和malloc开辟的空间的区别在于这个函数可以将空间的字节初始化为0。

现在我们来使用一下这个函数 

所以如果我们要求对申请的空间内容初始化的话,使用calloc函数就会很方便。 

3.2realloc函数

在使用动态内存过程中,我们可能会发现我们过去申请的空间过大了或者过小了,从而导致我们的内存不够灵活,这时我们就需要对已经动态申请的内存进行一些调整,这时就需要我们的realloc函数。realloc函数的存在便是为了调整这块空间。

realloc函数的函数原型如下:

void* realloc (void* ptr, size_t size);
  • ptr是要调整的内存地址
  • size是调整之后的新大小
  • 返回值是调整之后的内存的起始位置
  •  这个函数在调整原内存空间大小的基础上,还会将原来的数据移动到新的内存空间内。
  • realloc在调整内存空间是存在两种情况的

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

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

如果是情况1的话,直接在原有内存之后追加空间即可,原有的数据不会发生变化。

如果是情况2的话,原有空间之后没有足够多的空间时,编译器会在堆空间中另外找一个大小合适的连续空间来使用。这样函数返回的就是一个新的内存地址

代码出真知,实践一波。

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* arr = (int*)malloc(10 * sizeof(int));
	if (NULL == arr)
	{
		perror("开辟失败");
		return 1;
	}
	int* tmp = (int*)realloc(arr, sizeof(int) * 15);
	//使用tmp是为了防止内存开辟失败
	if (NULL == tmp)
	{
		perror("开辟失败");
		return 1;
	}
	arr = tmp;//把tmp给arr。
	return 0;
}

 可以看出,这时属于情况1。但是如果我们把realloc调整的空间从15改为100会怎么样呢?

	int* arr = (int*)malloc(10 * sizeof(int));
	int* tmp = (int*)realloc(arr, sizeof(int) * 100);

可以看出,这时内存找了另外一块空间来存放数据,也就是情况2. 

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

4.1对NULL指针的解引用操作

这点很容易理解,指向空的指针自然不会有内容可以被提取出来。

void test1()
{
	int* p = (int*)malloc(sizeof(int));
	int* p = NULL;
	free(p);
}

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

我们在对数组进行访问时知道,不可越界访问其内容;对于动态开辟的内存也同理。

void test2()
{
	int* p = (int*)malloc(10*sizeof(int));
	if (!p)
	{
		return -1;
	}
	for (int i = 0; i <= 10; i++)
	{
		*(p + i) = i;//当i等10时会越界访问。
	}
	free(p);
}

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

我们刚刚已经介绍了free是用来释放动态开辟的内存空间的,如果用来释放非动态开辟的内存空间自然会出现bug。

void test4()
{
	int* p = 10;
	free(p);
}

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

我们是可以通过修改指针指向的方式让我们创建的指针不再指向动态开辟的内存的首地址的。

修改之后的指针,再进行释放,就会导致动态开辟的内存释放不充分。

void test3()
{
	int* p = (int*)malloc(10*sizeof(int));
	p++;
	free(p);//此时没有完全释放动态开辟的内存空间
}

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

释放过一次了,再释放一次,不过是徒增代码量罢了。

void test5()
{
	int* p = (int*)malloc(10 * sizeof(int));
	free(p);
	free(p);//重复释放
}

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

如果我们动态开辟的内存使用完毕之后不释放的话,可能会导致内存泄漏。

内存泄漏即这块内存不能被程序再次使用,也不会被系统自动分配。从而导致了空间浪费。

因此特别重要的一点是,我们动态开辟的空间一定要记得free掉。

释放后还要记得置空!!!

6.柔性数组

6.1柔性数组简介

在C99标准中,结构中的最后一个元素允许是未知大小的数组的

例如:

typedef struct trousers
{
	int i;
	int arr[0];//柔性数组成员
}trousers;

这段代码或许会在某些编译器上报错,我们也可以这样写

typedef struct trousers
{
	int i;
	int arr[];//柔性数组成员
}trousers;

 6.2柔性数组的特点

柔性数组有以下特点

  • 结构中的柔性数组成员前面必须至少有一个其他成员。
  • sizeof返回的结构大小不包括柔性数组的内存,即使用内存对齐计算结构体大小时不会计算柔性数组成员的大小。
  • 包含柔性数组成员的结构体使用malloc函数进行内存的动态分配,并且分配的内存一定要大于结构体大小,以便于柔性数组有地方存。

6.3柔性数组的使用

typedef struct trousers
{
	int i;
	int arr[];//柔性数组成员
}trousers;
int main()
{
	trousers* p = (trousers*)malloc(sizeof(trousers) + 100 * sizeof(int));
	for (int i = 0; i < 100; i++)
	{
		p->arr[i] = i;
	}
    free(p);
    p=NULL;
	return 0;
}

这样,我们便给予了柔性数组100个数据。 

6.4柔性数组的优势

上一段代码,其实我们还可以这样设计

typedef struct trousers
{
	int i;
	int *p;
}trousers;
int main()
{
	trousers* pa = (trousers*)malloc(sizeof(trousers));
	pa->p = (int*)malloc(100 * sizeof(int));
	for (i = 0; i < 100; i++)
	{
		pa->p[i] = i;
	}
	free(pa->p);
    pa->p=NULL;
	free(pa);
	p = NULL;
	return 0;
}

 我们通过这样的方式也实现了柔性数组的效果,但是使用柔性数组是更有优势的,这是为什么呢?

好处1:方便内存释放和置空

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

好处2:有利于提高访问速度

连续的内存地址有益于提高访问速度,也有益于减少内存碎片。
 


网站公告

今日签到

点亮在社区的每一天
去签到