C语言动态内存管理(详解版)

文章重点:1.为什么存在动态内存分配。   2.动态内存函数的使用(malloc、free、calloc、realloc)。   3.常见的动态内存错误。   4.柔性数组的概念。

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

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

int val=20;
char arr[10]={0};

二者都是在栈空间上开辟内存

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

1.空间开辟大小是固定的。2.数组在声明的时候,必须要指定数组的长度,它所需要的内存在编译时分配。

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

所以 这时就要试试 动态内存开辟了。

                                                                2.动态内存函数的使用                                                      

在C语言中提供了一些动态内存开辟的函数:malloc、free、calloc、realloc 

malloc:

void* malloc(size_t size);         

***其中size是要向内存申请的空间的大小,单位为字节。

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

这里就要补充说一下动态内存开辟的空间与非动态内存开辟的空间在内存中的区别:

首先,在我们学习语言的时候,常常会将内存简单地划分为如下几个区域:

C语言动态内存管理(详解版)_第1张图片

 所以,使用malloc等动态开辟出的变量通常都会存放在堆区中。

***malloc函数如果开辟成功,则会返回一个指向它开辟好的空间的指针

***malloc函数如果开辟失败,则会返回一个空指针

因此,一定要对malloc函数的返回值进行检查

***malloc函数的返回值是void*的,因为malloc函数并不知道要开辟空间的类型,具体在使用该函数的时候由使用者自己决定

***如果参数size为0,这时malloc函数的行为是标准未定义的,取决于编译器

free:

void free(void* ptr);

***free函数是用来释放动态开辟的内存的

    就是将ptr所指向的空间的使用权还给操作系统了,就不再属于我当前系统了

    但是free函数并没有将ptr的值改变   即ptr还是指向那块空间的

    释放空间之后的ptr就是一个野指针了

    所以要在释放时候将ptr置为空指针

***如果参数ptr所指的空间不是动态内存开辟的 

    这时free函数的行为是标准未定义的

     取决于编译器

***如果参数ptr是一个空指针   则free函数什么也不做

***这里要补充一个问题:

     当我们使用完一块动态开辟的空间之后没有释放的时候,如果程序结束,动态申请的内存由操       作系统自动回收,但是如果程序不结束,动态开辟的内存是不会自动回收的,这时候就会造成       内存泄漏的问题。

calloc:

void* calloc(size_t num,size_t size);

***calloa函数的功能是为num个大小为size的元素开辟一块空间

    并且把每一个字节都初始化为0

realloc:

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

***realloc函数的功能是可以做到对动态开辟内存的大小进行调整

***ptr是要调整的空间的地址

***size是调整之后的大小

***realloc函数的返回值是调整之后的内存空间的起始地址(如果扩容失败,会返回一个空指针)

realloc函数在扩容的时候在内存中会出现两种情况:

1.原空间(即参数ptr所指的空间)小于扩容后的大小(即size):

这时realloc函数会在堆区的另外位置找到一块符合扩容大小的空间,并且将原来空间中的内容挪到新开辟的那块空间中去。

2.原空间(即参数ptr所指的空间)大于等于扩容后的大小(即size):

这时realloc函数就不需要再在堆区中再找空间,而是可以直接在原空间之后继续进行追加空间进行扩容的操作。

所以,在使用realloc函数进行扩容之后,一定要对其返回值做检查,而不是直接使用那块“扩容好”的空间或者直接将原空间的指针置为新开辟的空间(这样一旦扩容失败,原有的数据也找不到了)。

                                                              3.常见的动态内存错误                                                        

对NULL指针的解引用操作:

C语言动态内存管理(详解版)_第2张图片

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

C语言动态内存管理(详解版)_第3张图片

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

C语言动态内存管理(详解版)_第4张图片

 动态开辟内存之后忘记释放:

C语言动态内存管理(详解版)_第5张图片

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

切记:动态开辟的空间一定要释放,且一定要正确释放!!!!! 

                                                                       4.柔性数组                                                                  

在C99中,提出了柔性数组的概念:结构体中最后一个元素允许是未知大小的数组,这个就是柔性数组。

struct S
{

   int a;
   int arr[0];

}


struct S
{

   int a;
   int arr[];

}

以上两种结构体中的结构成员arr都是柔性数组。

柔性数组的特点:

1.结构体中的柔性数组成员前面必须至少有一个其他元素。

2.sizeof返回的这种含有柔性数组的结构体不包含柔性数组的大小,计算的只是柔性数组前面的成员的大小。

3.包含柔性数组成员的结构体需要使用malloc函数进行动态内存分配,并且分配的内存应该大于结构体的大小,以使用柔性数组的大小。

柔性数组的使用:

struct S
{
	int a;
	int arr[];
};
int main()
{
	struct S* p = (struct S*)malloc(sizeof(struct S) + sizeof(int) * 10);
	if (p == NULL)
	{
		perror("malloc:");
		return 1;
	}

	p->a = 0;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		p->arr[i] = i;
		printf("%d ", p->arr[i]);
	}

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

}

看到了这里,不知道你是否会感觉到一些怪异,有没有那种感觉?就是你完全想象不到这个柔性数组的有点在哪?

先看一下如下代码是不是也可以代替柔性数组的功能:

struct S
{
	int a;
	int* arr;

};
int main()
{
	struct S* p1 = (struct S*)malloc(sizeof(struct S));
	if (p1 == NULL)
	{
		perror("malloc:");
	}
	
	p1->arr = (int*)malloc(sizeof(int) * 10);
	if (p1->arr == NULL)
	{
		perror("malloc:");
	}

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		p1->arr[i] = i
			;
		printf("%d ", p1->arr[i]);

	}
	free(p1->arr);
	p1->arr = NULL;

	free(p1);
	p1 = NULL;

	return 0;
}

在这段代码中并没有使用柔性数组,但是可以一样模拟出柔性数组的功能;

我们可以很轻易看出第一段代码与第二段代码的差异

所以就可以体现的柔性数组的好处为:

1.方便内存释放:如果我们的代码时在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户,用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也同样需要free。所以,我们应该把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用于做一个free就可以把所有的内存都给释放掉了。

2.有利于提高访问速度:连续的内存(代码二中虽然连续使用了两次malloc函数来开辟内存空间,但是这两次开辟的空间一般情况下是不会连续的)有益于提高访问速度,也有益于减少内存碎片。

欢迎兄弟们留言挑挑毛病((●'◡'●))

你可能感兴趣的:(C,c语言)