【C语言进阶篇】动态内存分配和数组的动态内存分配

本文主要介绍了C语言中常用的内存函数和数组的动态内存分配,并稍微提了一嘴柔性数组。动态内存分配是C语言中十分重要的一环,其中对二维数组的动态内存分配是个难点,需要多思考。
【C语言进阶篇】动态内存分配和数组的动态内存分配_第1张图片

目录

      • 1. 为什么存在动态内存分配
      • 2. 动态内存函数
        • malloc
        • free
        • calloc
        • realloc
      • 3. 动态内存分配数组
        • 3.1 一维数组的动态分配和使用
        • 3.2 二维数组的动态内存分配和使用
      • 4. 柔性数组


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

要开辟一个数组,我们可以很简单做到:

int arr[10];
char str[20];
struct S s[10];

我们可以很容易地声明整型数组、字符数组、结构体数组…

但是,这样声明出来的数组有很明显的缺点:

  1. 数组大小固定
  2. 当数组固定大小很大时,可能会栈溢出(Stack overflow)
    【C语言进阶篇】动态内存分配和数组的动态内存分配_第2张图片

但实际上,很多情况下我们并不知道自己要开辟多大的空间,这些很多时候是只有在程序跑起来才知道的,所以这就引出了动态内存分配

动态内存分配出来的空间是在堆区开辟的,这是它与通过定义数组分配出来的空间最本质的区别。



2. 动态内存函数

动态内存分配是需要调用动态内存函数实现的,下面介绍四种内存函数,点击超链接即可转到官方解释。

malloc

【C语言进阶篇】动态内存分配和数组的动态内存分配_第3张图片
malloc 是我们见得最多的动态内存函数。

它会向内存申请一块连续可用的空间,空间大小是 size 个字节,并返回指向这块空间的指针。

它返回的是个指针,所以在使用它时要用指针接收:

char* ch = malloc(sizeof(char) * size1);
int* arr = malloc(sizeof(int) * size2);
struct S* s = malloc(sizeof(stuctt S) * size3);

但这样并不严谨,因为 malloc 返回类型是空指针,所以在接受它的返回值时最好再对它进行强制类型转换:

char* ch = (char*)malloc(sizeof(char) * size1);
int* arr = (int*)malloc(sizeof(int) * size2);
struct S* s = (struct S*)malloc(sizeof(stuctt S) * size3);

但是,当动态内存开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查:

char* ch = (char*)malloc(sizeof(char) * size);
if(ch == NULL)
{
	perror("fun_name");
	return;
}

free

【C语言进阶篇】动态内存分配和数组的动态内存分配_第4张图片
有动态内存分配的地方就一定会有 free 函数。

它的参数是一个指针,指针指向的空间是动态内存分配出来的一块空间。它会释放掉这块空间,将这块空间还给操作系统。

它释放的空间一定是要动态内存分配出来的,这块空间一定是在堆区的,否则会引发异常:
【C语言进阶篇】动态内存分配和数组的动态内存分配_第5张图片
free 去释放栈区的空间,这是万万不能够的!!

此外,如果传过去的指针是个 NULL 空指针,free 就会纯纯摆烂,啥也不干。

释放空间之后,原指针就没有任何意义,但 free 不会自动给它置成空指针,此时他就成为一个野指针,所以我们要即及时将其置成空指针:

int* ptr = (int*)malloc(sizeof(int) * size);
if(ptr == NULL)
{
	perror("fun_name");
	return;
}

...

free(ptr);
ptr = NULL;

calloc

【C语言进阶篇】动态内存分配和数组的动态内存分配_第6张图片
它的返回类型和 malloc 一样,参数部分则有不同:

num :元素个数
size :每个元素的大小(byte)

所以它的作用就是num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为 0

所以它的使用和 malloc 基本一致:

int *p = (int*)calloc(10, sizeof(int));
if(NULL == p)
{
	perror("fun_name");
	return;
}

...

free(p);
p = NULL;

相比 malloc ,它只是把分配的空间全部初始化为 0
【C语言进阶篇】动态内存分配和数组的动态内存分配_第7张图片


realloc

【C语言进阶篇】动态内存分配和数组的动态内存分配_第8张图片
虽然我们能通过 malloccalloc 函数动态内存分配一块空间,但这块空间分配完成后大小也是固定的,如果空间满了需要扩容或空间多了需要缩减,这时 realloc 函数就登场了。

realloc 的作用是对已经动态分配的一块空间再次分配。

它有两个参数:

  1. memblock:要调整的内存地址,这块内存是动态内存分配得到的
  2. size:以字节为单位的新大小

它会返回调整之后的内存起始位置。

关于调整之后的内存起始位置会出现以下两种情况:

  1. 与原来内存的起始位置相同
  2. 与原来内存的起始位置不同
  • 当要缩小原有的内存时,原来的内存空间已经足够,此时它的返回值就是原来内存的起始位置。
  • 当要扩大原有的内存时,又有两种情况:
  1. 原有空间之后有足够大的内存时,直接在原内存的基础上再开辟后边几个连续的空间,此时它的返回值是原来内存的起始位置;
  2. 原有空间之后没有足够大的内存进行扩容时,此时会在堆空间上另找一个合适大小的连续空间来使用,这样函数返回的是一个新的内存地址,与原来内存的起始位置不同。这时会拷贝原来空间的内容到新的空间的相应位置,原空间就被释放掉了。

所以,在使用 realloc 函数时就要注意用一个临时指针接收,当内存调整成功后再将临时指针赋给原指针:

int main()
{
	int* ptr = (int*)malloc(100);
	if (ptr == NULL)
	{
		preeor("main");
		return;
	}	

	...

	//定义一个临时指针接收新地址
	int* tmp = NULL;
	tmp = (int*)realloc(ptr, 1000);
	if (tmp != NULL)
	{
		ptr = tmp;
	}

	...

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


3. 动态内存分配数组

对于动态内存分配我们主要用于动态开辟一维和二维数组。
下面就看看开辟数组的方法和正确使用这块空间并释放。

3.1 一维数组的动态分配和使用

动态开辟一维数组还是比较简单的:

int main()
{
	int size = 10;
	//给整形数组动态分配
	int* arr = (int*)malloc(sizeof(int) * size);
	if (arr == NULL)
	{
		perror("main");
		return;
	}
	//给数组赋值
	for (int i = 0; i < size; i++)
		arr[i] = i;
	free(arr);
	arr = NULL;
	return 0;
}

在上面这段代码中,对动态内存分配的一块空间我们直接用给数组赋值的方式对其赋值,其可行性是源于数组的指针式访问和下标式访问:

*(p + i) <==> p[i]
*(*(arr + i ) + j) <==> arr[i][j]

不过一定要记得使用完这块空间要及时释放。


3.2 二维数组的动态内存分配和使用

对于二维数组的动态开辟,其方法是不唯一的,下面给出三种方法。

  1. 第一种
    由于二维数组在内存中是连续存放的,所以我们可以开辟一大块空间将二维数组当成一维数组存放起来。这样数组元素的存储在内存中是连续的。
    【C语言进阶篇】动态内存分配和数组的动态内存分配_第9张图片

  2. 第二种
    动态开辟一个二维数组,可以开辟一个指针数组,每个元素存放的是一个指针,每个指针都指向一个数组,再分别对每个一维数组分配空间。
    但是,这种方式开辟出来的数组不是连续存储的
    【C语言进阶篇】动态内存分配和数组的动态内存分配_第10张图片

  3. 第三种
    动态开辟一个二维数组,还可以借用数组指针,然后通过对数组指针访问到数组元素,此时开辟出来的数组是连续存储的。其实这种方法和第一种方法有同工异曲之妙,只是这种方式访问数组元素更简单:
    【C语言进阶篇】动态内存分配和数组的动态内存分配_第11张图片

下面是每种方法的代码实现:

//方法1
#define ROW 4
#define COL 4
int main()
{
	//动态开辟
	int* arr = (int*)malloc(sizeof(int) * ROW * COL);
	if (arr == NULL)
	{
		perror("main");
		return;
	}

	//访问数组并赋值
	int count = 1;
	for (int i = 0; i < ROW; i++)
		for (int j = 0; j < COL; j++)
			arr[i * COL + j] = count++;

	//释放内存
	free(arr);
	arr = NULL;

	return 0;
}
//方法2
#define ROW 4
#define COL 4
int main()
{
	//用二级指针动态申请二维数组
	int** arr = (int**)malloc(sizeof(int*) * ROW);
	//这样只开辟了ROW个存放整型指针的空间
	if (arr == NULL)
	{
		perror("main");
		return;
	}

	//对每个一维数组开辟空间
	for (int i = 0; i < ROW; i++)
	{
		arr[i] = (int*)malloc(sizeof(int) * COL);
		//给每个一级指针arr[i]分配COL个整型空间
		if (arr[i] == NULL)
		{
			perror("main");
			return;
		}
	}
	
	//访问数组元素并赋值
	int count = 1;
	for (int i = 0; i < ROW; i++)
		for (int j = 0; j < COL; j++)
			arr[i][j] = count++;

	//释放二维数组的每个一维数组
	for (int i = 0; i < ROW; i++)
	{
		free(arr[i]);
		arr[i] = NULL;
	}
		
		
	//释放二级指针申请的数组
	free(arr);
	arr = NULL;
	
	return 0;
}
//方法3
#define ROW 4
#define COL 4
int main()
{
	//用数组指针形式申请一个ROW行COL列的二维数组
	int(*arr)[COL] = (int(*)[COL])malloc(sizeof(int) * ROW * COL);
	if (arr == NULL)
	{
		perror("main");
		return;
	}
	
	//访问数组成员并对其赋值
	int count = 1;
	for (int i = 0; i < ROW; i++)
		for (int j = 0; j < COL; j++)
			arr[i][j] = count++;
	
	//释放内存
	free(arr);
	arr = NULL;

	return 0;
}

无论是哪种方式,刚用起来肯定生疏,但用多了就熟悉了。

而且,一定要记得释放内存,特别是第二种方式!



4. 柔性数组

柔性数组(flexible array)是一种不完整类型,而 C99 的标准,就支持了这种类型。

标准规定:结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员。

typedef struct S
{
 int i;
 int a[0];//柔性数组成员
}s;

对于有些编译器可能报错,a[0] 改成 a[ ] 即可。

  1. 柔性数组在结构中声明,柔性数组成员前面必须有至少一个其他成员。

  2. sizeof 返回的这种结构大小不包括柔性数组的内存。
    【C语言进阶篇】动态内存分配和数组的动态内存分配_第12张图片

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

//对结构体进行动态内存分配
s* p = (s*)malloc(sizeof(s) + 100 * sizeof(int));
if (p == NULL)
{
	perror("main");
	return;
}

...

free(p);
p = NULL;

你可能感兴趣的:(C语言,c语言,c++,开发语言,经验分享)