C语言动态内存详解

动态内存开辟和扩容

  • 动态内存介绍
  • malloc
  • calloc
  • realloc
  • 动态内存常见错误
  • 动态内存面试题
  • 柔性数组

动态内存介绍

在C语言中,要想开辟一块空间,比如说数组,变量,但是不论是数组还是变量,在制定数据类型并定义好后,都不能改变大小。
C语言动态内存详解_第1张图片
在定义好变量和数组之后,变量和数组的大小是不能被改变的。
假如我们开辟了40个字节的数字,当数组被数据放满后再想添加数据怎么办呢?在C语言中有一个概念叫做动态内存,我们知道数组名代表的就是数组的地址,数组就是在内存中开辟一块连续的空间,而动态内存就是在内存中开辟一块空间,然后把这块空间的地址返回给一个指针接收,当这块动态内存不够用时,我们可以对它进行扩容,可以理解成可以变长的数组。

malloc

malloc的定义:
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
如果开辟成功,则返回一个指向开辟好空间的指针。
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
在这里插入图片描述
开辟好内存的首地址 malloc(要开辟的字节个数)
如果开辟成功返回内存的首地址,开辟失败返回NULL
这里返回的是void*的指针,接收地址需要进行强制类型转换

#define _CRT_SECURE_NO_WARNINGS
#include
#include

int main()
{
	//malloc
	//开辟一个20个字节的空间
	int* p = (int*)malloc(5 * sizeof(int));//这里需要将malloc的返回值强制转换成int类型
										//使用说明类型的变量接收返回值,就强制转换成什么类型
	
	//判断是否位空指针
	if(p == NULL)
	{
		perror(""malloc);
		return 1;
	}
	//在这块空间中存入数据
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		p[i] = i;
	}

	//打印输出
	for (i = 0; i < 5; i++)
	{
		printf("%d ",p[i]);
	}

	//释放动态内存
	free(p);
	p = NULL;

	return 0;
}

free:
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
如果参数 ptr 是NULL指针,则函数什么事都不做。
动态内存和数组的释放:
数组在程序结束后,会自动销毁,然后把内存返回给操作系统,而使用动态内存开辟的空间是需要free函数来手动释放的,在释放完动态内存后,而指针p却还存着之前开辟空间的地址,而之前的空间已经被释放,这个时候p就成为了野指针,所以需要将p置为空。
而不论是malloc,calloc还是realloc,都需要和free配对,也就是说只要使用了动态内存,就要使用free,不然就会造成内存泄漏。

错误示范:

int arr[10];
arr = (int*)malloc(20);

这样的代码是贬义不过去的,虽然arr代表的数组的地址,而malloc的返回值也是一个地址,但是,arr在创建好之后,地址是不能被改变的,而由malloc开辟的动态内存地址在后期是可以改变的。

calloc

calloc的定义:
开辟好内存的首地址 calloc(要开辟多少个元素,元素的大小)
函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0
与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0
在这里插入图片描述
C语言动态内存详解_第2张图片
C语言动态内存详解_第3张图片
malloc和calloc在使用上是很相似的,区别就是malloc只能制定开辟多少个字节,calloc可以制定开辟多少个元素,并且初始化成0.

realloc

realloc的定义:
开辟好内存的首地址 calloc(地址,扩容后的大小)
ptr 是要调整的内存地址
size 调整之后新大小
返回值为调整之后的内存起始位置。
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。

在这里插入图片描述
realloc的作用:
在使用malloc或者calloc后,想要扩容内存,这个时候就要用到realloc
C语言动态内存详解_第4张图片

我们来调试看一下是否像上图说的那样开辟成功:
首先看到开辟好以后,里面是随机值:
C语言动态内存详解_第5张图片
开辟好的地址:
C语言动态内存详解_第6张图片

然后赋值:
C语言动态内存详解_第7张图片
扩容空间,后5位变成随机值:
C语言动态内存详解_第8张图片
再次赋值:
C语言动态内存详解_第9张图片
扩容完成后的地址:
C语言动态内存详解_第10张图片
扩容后返回的地址分两种情况:
情况1:原有空间之后有足够大的空间
情况2:原有空间之后没有足够大的空间
我们俩画图演示一下:
情况1:
C语言动态内存详解_第11张图片
C语言动态内存详解_第12张图片
开辟好之后返回内存1原有的地址

情况2:
C语言动态内存详解_第13张图片
C语言动态内存详解_第14张图片
注意:内存和内存中间是有额外空间的,并不是挨着的
我们在vs上示范一下:
C语言动态内存详解_第15张图片
使用malloc开辟好的空间地址是:80 3a fa 64
在使用realloc扩容10000个字节后,原有空间后面的内存看到是不够的,这个时候会找一块新的大小合适空间拷贝过去,然后返回这个块空间的地址:
C语言动态内存详解_第16张图片
此时返回的地址变成了:10 e8 fa 64

动态内存常见错误

1,对NULL指针的解引用操作

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

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

}

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

void test()
{
 int a = 10;
 int *p = &a;
 free(p);//ok?
}

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

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

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

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

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

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

动态内存面试题

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

void GetMemory(char *p)
{
 p = (char *)malloc(100);
}
void Test(void)
{
 char *str = NULL;
 GetMemory(str);
 strcpy(str, "hello world");
 printf(str);
}

首先我们创建了一个 char类型的指针,赋为NULL,然后调用GetMemory函数,函数内部用一个指针接收,然后开辟一块100字节的动态内存,将地址存入p,这个时候在使用strcpy拷贝字符串到str中,注意这里的p是一个单独的指针,str传过去只是把NULL赋给了p,所以str里面还是NULL,而且在函数内部开辟的动态内存并没有被释放。

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

char *GetMemory(void)
{
 char p[] = "hello world";
 return p;
}
void Test(void)
{
 char *str = NULL;
 str = GetMemory();
 printf(str);
}

在函数内部创建了一个数组,并且赋值,然后返回数组的地址,注意:函数在使用结束后,里面所创建的变量和数组都会被销毁,这个时候把p的地址传给str,str就成为了野指针。

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

这里将str的地址传给一个二级指针来接收,二级指针解引用,就是str,将开辟好的100个字节的动态内存地址返回给str,然后再拷贝字符串。
但是没有释放动态内存。

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

void Test(void)
{
 char *str = (char *) malloc(100);
 strcpy(str, "hello");
 free(str);
 if(str != NULL)
 {
 strcpy(str, "world");
 printf(str);
 }
}

这里将开辟的动态内释放后,但是str依旧存着之前的地址,这个时候str就成了野指针,再对它进行赋值,就会造成越界访问

柔性数组

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

struct S
{
	int a;
	int b[];
};

在结构体中,最后一个数组可以不设置长度,或者将大小设置位0,这就叫做柔性数组。柔性数组不能单独放在结构体中,前面必须有其他变量或者数组。

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

在使用sizeof对带有柔性数组的结构体计算大小的时候,柔性数组是不参与的,只会计算柔性数组之前的变量大小。
C语言动态内存详解_第17张图片

柔性数组的使用:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

struct S
{
	int a;
	int b[];//柔性数组
};

int main()
{
	//使用一个struct s的指针来接收
	//先计算出柔性数组之前变量的大小
	//再加上柔性数组的大小
	struct S* p = (struct S*)malloc(sizeof(struct S) + sizeof(int) * 10);
	return 0;
}

柔性数组扩容:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

struct S
{
	int a;
	int b[];//柔性数组
};

int main()
{
	//使用一个struct s的指针来接收
	//先计算出柔性数组之前变量的大小
	//再加上柔性数组的大小
	struct S* p = (struct S*)malloc(sizeof(struct S) + sizeof(int) * 10);
	
	//使用realloc对柔性数组进行扩容
	//还是先计算出柔性数组之前变量的大小
	//再对柔性数组进行扩容
	p = (struct S*)realloc(p, sizeof(struct S) + sizeof(int) * 20);

	return 0;
}

柔性数组的其他用法:
既然柔性数组是可以扩容的,那我们可以将柔性数组改成一个指针,再用这个指针进行malloc开辟动态内存,在使用realloc扩容:

#define _CRT_SECURE_NO_WARNINGS
#include
#include

struct S
{
	int a;
	int* b;//柔性数组
};

int main()
{
	//先开辟好柔性数组之前的变量空间
	struct S* p = (struct S*)malloc(sizeof(struct S));

	//再对柔性数组进行开辟
	p = (struct S*)malloc(sizeof(int) * 10);

	//之间对柔性数组扩容,不用管之前的变量空间
	p->b = (int*)realloc(p, sizeof(int) * 20);
	return 0;
}

以上就是动态内存的详解,如有错误,欢迎指正

你可能感兴趣的:(c语言,开发语言,visual,studio)