C语言动态内存管理

一.动态内存的解释

我们之前在C语言中使用int,float,数组,结构体这些时,都会在内存中的栈区上开辟一块固定大小的空间,以便我们使用。比如定义一个整型变量时int a = 0;这时候编译器就会在内存中开辟一个四个字节的空间。但是这种方法的共同点是:它们开辟的空间大小是固定的
但有时候我们需要的空间大小要在运行时才能知道,比如再定义一个数组的时候,你必须在申明的时候指定这个数组的大小。如果数组在定义的时候定义小了,在之后用到的时候很可能会造成数组越界,所以我们一般在申明数组大小的时候往空间大的想,但是这又会造成一个问题:剩余的空间可能会不用,这就造成了空间的浪费。
所以我们就有了动态内存。就是先开辟一些空间,不够用了就增加一点,多了的话,就减少一点,这样就达到了动态的效果。

二.动态内存函数的介绍

2.1malloc函数

2.1.1malloc函数的说明

void *malloc( size_t size );

malloc是开辟一块你指定大小的空间。

参数:需要开辟空间的大小,以字节为单位
返回值
如果开辟成功,返回一个指针,指向给你开辟好的那块空间的地址
如果开辟失败,返回一个NULL指针,所以在开辟之后要做相应的检查
头文件:<stdlib.h>

2.1.2malloc函数的使用

int main()
{
	int num = 0;
	scanf("%d", &num);
	int* ptr = (int*)malloc(num * sizeof(int));
	//malloc开辟了num*4个字节的空间,将地址传给一个int*类型的指针,因为malloc返回的是void*类型的指针,所以要强制类型转换
	if (ptr == NULL)
		//检验ptr是否为空指针,是空指针的话就直接返回,在main函数里return
		//就相当于结束程序了
		return 1;
	int i = 0;
	//循环,和打印数组时类似。
	for (i = 0; i < num; i++)
	{
		*(ptr + i) = 0;
		//因为ptr是一个int*类型的指针,所以+1是跳过4个字节
		printf("%d ", *(ptr + i));
	}
	return 0;
}

我们看一下结果:
在这里插入图片描述

这里有几个需要注意的点:

  • malloc函数返回的是一个void*类型的指针,所以在返回的时候需要用到的类型需要使用者自己决定。
  • 在开辟完空间之后,要进行相应的检测,如果开辟失败会返回一个空指针,如果是空指针,我们就直接返回。

2.2free函数

2.2.1free函数的说明

void free( void *memblock );

free函数一般是与开辟内存空间的函数(malloc,calloc)这类函数一起使用。因为我们通过malloc,calloc函数开辟的空间是在堆区开辟的,堆区有一个特点就是系统不会自动释放你在里面开辟的空间。所以需要我们主动释放
参数:是我们开辟的那块动态内存空间的地址

2.2.2free函数的使用

void free( void *memblock );

头文件:<stdlib.h>

int main()
{
	int num = 0;
	scanf("%d", &num);
	int* ptr = (int*)malloc(num * sizeof(int));
	if (ptr == NULL)
		return 1;
	int i = 0;
	for (i = 0; i < num; i++)
	{
		*(ptr + i) = 0;
		printf("%d ", *(ptr + i));
	}


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

因为ptr指向的就是我们开辟的内存空间的地址,我们free(ptr)就行。这里要注意在free完之后,要将ptr设置为空指针,因为ptr本来指向的是一块我们需要的空间,但是我们将这块空间free掉之后,ptr指向的地址我们也不清楚是做什么的,ptr就变成了野指针,防止以后误用,所以先将它设为空指针

2.3calloc函数

2.3.1calloc函数的说明

void *calloc( size_t num, size_t size );

calloc函数和malloc函数类似,都是开辟一块动态内存空间。
参数

num:你需要开辟的个数
size:每个元素的大小(以字节为单位)

返回值:
一个void*类型的指针,指针指向的是你开辟空间的地址。

既然calloc和malloc都是开辟一块动态内存空间,两者除了参数不同,还有什么不同呢?
我们返回去看我写的malloc函数的代码,会发现,在打印之前,我将ptr+i指向的空间里的元素都初始化为0.也就是malloc开辟的空间需要自己手动初始化为0.但是calloc就不同,calloc函数会在开辟时把每个字节初始化为0.

2.3.2calloc函数的使用

int main()
{
	int* ptr = NULL;//定义一个int*类型的指针
	ptr = (int*)calloc(10, sizeof(int));
	//开辟一块含有10个元素,每个元素四个字节的动态内存空间,
	//并将开辟好的空间地址传给指针ptr

	//检查
	if (ptr == NULL)
		return 1;

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		//这里我们没有将每个元素初始化成0,直接打印
		printf("%d ", *(ptr + i));
	}

	//释放
	free(ptr);
	ptr = NULL;
	return 0;
}

在这里我们开辟了一块空间,空间里有10个元素,每个元素有4个字节,总共40个字节

我们来看结果:
在这里插入图片描述

calloc函数自动初始化为全0.

2.4realloc函数

在文章的开头我就提到过,动态内存的空间可以根据使用者的想法而更改的,但是我们发现malloc,calloc还是开辟的空间,和数组开辟的空间好像没啥区别,甚至malloc他们开辟的空间还有检查和释放,好像还麻烦些。
这就需要用到realloc函数了。

2.4.1realloc函数的说明

void *realloc( void *memblock, size_t size );

realloc函数就是调整一块动态内存空间的大小
参数

void *memblock:你需要调整的那块空间的地址
size_t size:你需要调整的那块空间,在调整之后的大小

返回值:调整完之后那个空间的地址。

注意事项:如果是需要增加空间的话,可能会出现两种情况

1.你刚开始开辟的那块空间后面也有足够大的空间,这是你可以直接添加。
2.如果你后面没有足够大的空间,编译器会自动找一个合适的空间,并将你空间原有的内容自动重新拷贝过去,这样你也不用担心重新找块空间导致数据的流失。

我说这些只是让大家了解一下,因为不管后面有没有足够大的空间,realloc函数它会自己解决,不用我们担心。

2.4.2realloc函数的使用

int main()
{
	int* ptr = NULL;
	ptr = (int*)malloc(40);

	if (ptr == NULL)
		return 1;

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(ptr + i) = i;
		printf("%d ", *(ptr + i));
	}
	printf("\n");

	//此时我们需要打印20个整型的数据
	//原有的空间不够了
	int* pi = NULL;
	pi = (int*)realloc(ptr, 80);
	//将原来的空间地址传过去,将你希望新的空间的大小也传过去

	//同样要对此进行检查
	if (pi == NULL)
		return 1;

	for (i = 0; i < 20; i++)
	{
		*(pi + i) = i;
		printf("%d ", *(pi + i));
	}

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

有几个注意事项:

这里在接受realloc函数的返回值时也可以这样写:
ptr = realloc(ptr, 80);虽然这样可以少定义一个指针,但是会报警告,为什么呢?
我们刚开始用ptr指向了一块我们开辟好的内存空间,然后我们在空间里存放数据,这些都很正常。但是如果realloc函数开辟内存失败了,于是返回了一个NULL给ptr,这会让ptr变成了空指针,这样我们之前存的那些数据的地址不就丢了吗,我们为了避免这些,所以realloc返回的地址由一个新定义的指针接收。

realloc函数中如果第二个参数是0的话,realloc函数就相当于free函数,把之前的空间大小调整为0,就相当于把这块空间free掉了

realloc函数的第一个参数是空指针,realloc函数就相当于malloc函数

三.常见的动态内存错误

3.1对NULL指针的解引用操作

在前面我就讲过,在使用malloc,calloc函数开辟一个空间之后,要对其进行一定的检验。假如我们没有检验,直接使用,恰巧这时候我们在开辟空间时给函数传递了一个很大的数,而导致开辟失败的时候。因为开辟失败就会返回一个空指针,而我们不知道这是空指针,我们在对这个空指针解引用时就会出现错误

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

INT_MAX为 2^31-1 ,即 2147483647

我们在malloc()里放一个很大的数,因为内存里找不到这么大的空间,导致开辟失败,从而返回一个空指针。
我这个编译器虽然不会报错,但是会报一个警告。

在这里插入图片描述

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

初学者可能对malloc函数不熟悉,将malloc(20)认为是开辟的空间含有20个元素。或者是说知道这些,但是在使用的时候不小心,访问到了我们没有开辟的那块空间。这都会导致越界访问,这种错误在数组的使用时也可能会用到。

int main()
{
	int* p = NULL;
	p = (int*)malloc(20);

	if (p == NULL)
		return 1;

	int i = 0;
	for (i = 0; i < 20; i++)
	{
		*(p + i) = i;
		printf("%d ", *(p + i));
	}

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

这就是以为我们开辟了20个元素的空间,在for循环时直接循环20次。

在这里插入图片描述

虽然再打印的时候我们看它挺正常的,确实是打印了0-19的数,但是箭头指向的那块地方说明这个代码就已经不正常了。

int main()
{
	int* p = NULL;
	p = (int*)malloc(20);

	if (p == NULL)
		return 1;

	int i = 0;
	for (i = 0; i <= 5; i++)
	{
		*(p + i) = i;
		printf("%d ", *(p + i));
	}

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

再或者是这样,你也知道我们开辟了20个字节,用int*指针接受,说明是开辟了5个int型元素的空间,但是在运用的时候,本该只需循环5次,这里出现了纰漏循环了6次,这也会导致越界访问。而且这次错误直接让系统崩掉了
在这里插入图片描述

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

我们在做动态开辟内存的练习或者应用时,可能因为定义的指针过多,而导致头脑混乱,本应该free掉的指针没有free,不该free的指针却被你free掉了。
我写过最简单的例子:

int main()
{
	int i = 10;
	int* p = &i;
	//....里面是包含了一大堆动态开辟有关的代码

	free(p);
	//p本来是指向一个整型的指针,却被你不小心free掉了
	p = NULL;
	return 0;
}

系统也会崩溃:
在这里插入图片描述

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

我们在用一个指针接收一块动态内存空间开辟好的地址之后,我们肯定对这个指针进行一个操作啊。但是如果有的初学者对free函数这块不熟悉。在使用指针时,让指针进行++,–这就会导致这个指针本来指向的地址是我们开辟好一块空间的地址,但是在++后,这个指针指向的就不是这个地址了。如果这时候我们在使用free函数,将这个指针free掉,就会发生错误,因为free函数只会释放从我们传进去的这块地址之后的那一块空间,如果对指针进行++操作,就会导致前面有一块空间没有被释放掉

int main()
{
	int* p = NULL;
	p = (int*)malloc(20);

	if (p == NULL)
		return 1;

	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p++);
	}

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

在这里我们的指针一直在++,我们开辟的空间的本来位置已经被我们丢了,这样系统也会崩溃。
在这里插入图片描述

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

这个错误我之前就写过一次,我还好奇了好久,这到底是哪里出了问题?你们先看看我写的代码:

int main()
{
	int* p = NULL;
	p = (int*)malloc(20);

	if (p == NULL)
		return 1;

	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p + i) = i;
		printf("%d ", *(p + i));
	}
	printf("\n");

	//此时我们觉得不够用,想在多开辟一点

	int* pi = NULL;
	pi = (int*)realloc(p, 40);
	//把新开辟好的空间的地址传给pi

	if (pi == NULL)
		return 1;

	p = pi;
	//将新空间的地址赋值给p

	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
		printf("%d ", *(p + i));
	}

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

因为我在刚开始写代码时,有些东西我也不怎么熟悉。在这里我先开辟20个字节的空间,地址传给指针p。后来我有希望在增加20个字节,就把后来新的40个字节的空间传给指针pi,我又把新的地址传给指针p。在结束时,我认为p,pi都是动态内存的地址,应该都free掉。但是,p,pi两个指针都是指向一块相同的位置,我把两个指针都free掉,不就相当于一块空间被连续释放了吗?
但是有人可能会讲,你不把pi的值赋给p,后来在操作新空间的时候直接不管p,直接操作pi不就行了?以后不小心把两个指针都free掉也不用担心了吗?其实这种方法也不可以,因为在后面空间足够的情况下realloc函数是把新开辟的空间,直接加在了原有的空间后面,这时候你不赋值,p和pi指向的地址也是一样的。
所以在使用free函数的时候一定要注意,释放的指针之前有没有释放过

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

上一个错误是释放的次数过多,而这次是因为忘记释放。
首先我们要知道为什么要释放空间,因为动态开辟的空间都是在栈区中开辟的。栈区中开辟的内存,系统不会主动给你释放,需要你自己手动释放。
如果你一直在申请空间,却不释放,最终系统也会崩溃的,如果你写的代码比较少,那还不是特别明显,如果你在写一个很大的项目,这就很有可能会出事的。

四.错误例题

4.1题目1

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

首先这里有个很容易发现的错误:

没有free,但是我们这个代码有个比较严重的错误,比没有free要严重的多,我们这个题目主要讲这个错误。

在这里插入图片描述
在这里我们先定义一个指针str,里面放的是空指针NULL,然后调用str后,把str的内容传给形参p指针,所以p里面的内容也是NULL.
p = (char *)malloc(100);随后我们开辟一块动态内存空间并将其地址传给了指针p。
看到这里错误就很明显了->
在这里插入图片描述

p里面的内容的确已经定义好了,但是str压根就没动,还是空指针,然后再将hello,word拷贝到空指针里,这样是根本行不通的。

解决方案

我们现在的目的是把指针str能够指向新开辟的那块空间。其实我们把str的地址当作参数传过去即可。这是形参指针p里的内容就是str的地址,把新开辟的动态内存空间赋值给指针p,不就相当于内存空间地址赋值给了str吗?

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

现在我们再来看看结果:
在这里插入图片描述
也可以把开辟好的那块空间的地址传过来,用指针接收

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

4.2题目2

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

这和上面那个错题的第二种解决方法很类似,都是将一个地址传出来。但是这里的错误是什么呢?

要注意数组空间的开辟可是和动态内存空间的开辟不同,数组是在栈区中开辟的,这些局部变量的作用域只在它所在的地方,也就是它们所在的那个大括号里面,也就是说char p[] = “hello world”;确实是开辟了一块空间,但是返回的时候,这块空间就自动销毁了,str虽然接收的还是之前的地址,但是地址指向的内容却不复存在了,在打印时肯定也打印不出来。

看结果:
在这里插入图片描述
打印的是一堆乱码,hello word已经被销毁了。str也变成了野指针

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

看完题目一,这个代码的错误应该比较容易发现,这里其实就是没有free,造成空间泄漏。但是free千万不要写在p = (char*)malloc(num);后面,因为这个空间我们还没用就释放掉了,这也是错的。

4.4题目4

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

这个程序虽然能运行,但是能运行不代表它就是对的。
首先,我们这个代码是先使用这个开辟动态内存返回的空间,然后在判断其是不是空指针,顺序上就是错的。其次我们这个str指针还没用完就free释放掉了,这也是不行的,我们看后面strcpy(str, “world”);这个指针还没用完,虽然程序没有检查出来错误->str此时已经是野指针了。但是没检查出来,不代表没错。

五.柔性数组

5.1柔性数组的概念

柔性数组是在C99中存在的一个概念。在结构体中,结构体内部的成员在定义时,最后一个成员如果是数组的话,这个数组的大小可以未定义。像这些:

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

typedef struct st_type1
{
	int i;
	int a[];//柔性数组成员
}type_a;

int a[]和int a [0]都是可以的。但有些编译器a[0]会报错

但是要注意一点,柔性数组的前面必须要有其他成员,如果一个结构体只有柔性数组这一个成员也是不可以

5.2柔性数组的特点

  • 结构中的柔性数组成员前面必须至少一个其他成员。

  • sizeof 返回的这种结构大小不包括柔性数组的内存。

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

5.3柔性数组的使用

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

int main()
{
	//我们为我们的结构体开辟一块动态内存空间
	struct S* pi = (struct S*)malloc(sizeof(struct S) + 4 * sizeof(int));
	//因为结构体大小不包括柔性数组的大小
	//sizeof(struct S):是为了给结构体前两个成员开辟空间
	//4 * sizeof(int)是为柔性数组开辟4个整形的空间

	//判断
	if (pi == NULL)
		return 1;

	int i = 0;
	for (i = 0; i < 4; i++)
	{
		scanf("%d", pi->arr + i);
		//输入数组里每个元素的内容
	}

	for (i = 0; i < 4; i++)
	{
		printf("%d ", *(pi->arr + i));
		//打印数组里的每个内容
	}
	printf("\n");


	pi->a = 5;
	pi->b = 'a';
	//为结构体前两个成员赋值
	printf("%d\n", pi->a);
	printf("%c\n", pi->b);
	//打印

	//释放
	free(pi);
	pi = NULL;
	return 0;
}

这里我们为结构体开辟好空间后,直接将柔性数组当普通数组用就行。但是这样写也发现不了柔性数组的特点呀?既然这样写,还不如在定义结构体时写上int arr[4];所以为了体现柔性数组的特点,我们在写一个代码:

int main()
{
	//我们为我们的结构体开辟一块动态内存空间
	struct S* pi = (struct S*)malloc(sizeof(struct S) + 4 * sizeof(int));
	//因为结构体大小不包括柔性数组的大小
	//sizeof(struct S):是为了给结构体前两个成员开辟空间
	//4 * sizeof(int)是为柔性数组开辟4个整形的空间

	//判断
	if (pi == NULL)
		return 1;

	//增加数组的空间
	struct S *ptr = (struct S*)realloc(pi, sizeof(struct S) + 8 * sizeof(int));

	//判断
	if (ptr == NULL)
		return 1;

	pi = ptr;

	int i = 0;
	for (i = 0; i < 8; i++)
	{
		scanf("%d", pi->arr + i);
		//输入数组里每个元素的内容
	}

	for (i = 0; i < 8; i++)
	{
		printf("%d ", *(pi->arr + i));
		//打印数组里的每个内容
	}
	//释放
	free(pi);
	pi = NULL;
	ptr = NULL;
	return 0;
}

这里为了让你们看清楚,我就直接在用malloc开辟完空间后,直接用realloc来增加空间。在这里就可以看出,数组在用完没有空间后,可以直接用函数来手动增加,这样就不像普通数组那样有局限性。
当然,你也可以这样开辟动态内存空间:

malloc(sizeof(struct S));
malloc(sizeof(int) * 4;

就是将一个结构体分开开辟,这样你在后期改的时候,只用改第二个malloc开辟的空间就行。但是我不推荐这样做。原因有两个:

  • 你因为一个结构体而开辟了两次空间,这就会导致开辟的空间不连续,这就会造成你的空间碎片化,就是说如果你的内存空间,这里一块,那里一块,这样就会导致你内存的利用率不高。
  • 要注意我们这里malloc是两个不同的空间,在free时也要free两次,你就有可能在free时忘记了,这就会造成内存泄漏。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值