动态内存分配复习

为什么要存在动态内存分配呢?

int val = 20; // 在栈空间上开辟4个字节

char arr[10] = { 0 }; //在栈空间上开辟10个字节的连续空间

但是上述的开辟空间的方式有两个特点:
1.空间开辟大小是固定的(太过具有局限性)
2.数组在什么的时候,必须指定数组的长度,它所需要的内存在编译时分配。

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

malloc
#include<stdlib.h> //malloc的头文件
void* malloc(size_t size); // 声明 (()里面是总大小)

#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
    
    
	//向内存申请10个整形的空间
	int* p = (int*)malloc(10 * sizeof(int)); //当申请成功的时候就会返回申请到的起始地址
	if (p == NULL) // 但也有一定的可能会申请失败,就会返回一个空指针
	{
    
    
		//打印错误的原因
		printf("%s\n", strerror(errno));
	}
	//  当动态申请的空间不再使用的时候
   //就应该还给操作系统,引入了free这个函数。
    free(p);
    p = NULL;
	return 0;
}

calloc
void* calloc(size_t num,size_t size); //声明
1.函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0.
2.与malloc的区别只是在于calloc会在返回地址之前把申请的空间的每个字节初始化为0.

#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
    
    
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
    
    
		printf("%s\n", strerror(errno));
	}
}

realloc
1.realloc函数的出现让动态内存管理更加的灵活
2.有时我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理内存,我们一定会对内存的大小做灵活的调整。
void* realloc(void* ptr, size_t size); // 原型
其中:
1.ptr是要调整的内存地址
2.size调整之后新的大小
3.返回值为调整之后的内存起始位置
4.这个函数调整原内存空间的大小基础上,还会将原来内存中的数据移动到新的空间
5.realloc在调整内存空间是存在两种情况的:如果realloc后面的空间足够大,就直接追加,然后返回原来的旧地址,但是当后面的空间不是足够大的时候,就需要重新考虑拿一块很大的空间来进行开辟,把原有的数据会拷贝下来,释放旧的内存空间,然后返回新开辟的地址。
int* p = realloc(NULL,40); // 此时这个函数相当于malloc(40)
注意:在开辟的时候把realloc返回的地址用一个新的指针来接收

    int* ptr = realloc(p, INT_MAX);
    if (ptr != NULL)
    {
    
    
		p = ptr;
     }
    // realloc也要记住释放空间
    free(p);
    p = NULL;

常见动态内存错误
1.对空指针的解引用(对其所开辟的指针一定要进行判断)

int main()
{
    
    
	int* p = (int*)malloc(40);
	int i = 0;
	for (i = 0; i < 10; i++)
	{
    
    
		*(p + i) = i;
	}
	free(p);
	p = NULL;
	return 0;
}

对于这段代码,你没有对p进行判断,万一他开辟内存失败了,就会返回一个空指针,那么就报错了。
2.越界访问

int main()
{
    
    
	int* p = (int*)malloc(5*sizeof(int));
	if (p == NULL)
	{
    
    
		return 0;
	}
	else 
	{
    
    
		int i = 0;
		for (i = 0; i < 10; i++)
		{
    
    
			*(p + i) = i;
		}
	}
	free(p);
	p = NULL;
	return 0;
}

3.对非动态开辟内存使用free释放(free的是堆区的空间)

int main()
{
    
    
	int a = 10; // 这里是局部变量,是栈区的空间
	int* p = &a;
	*p = 20;
	free(p);
	p = NULL;
	return 0;
}

4.使用free释放动态开辟内存的一部分
这个程序会崩溃,因为你的p一直在变化,最终指向了开辟的最后一个地址的下一个位置处,你在对其进行释放,后面的空间是不属于你的。

int main()
{
    
    
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
    
    
		return 0;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
    
    
		*p++ = i; // 把这里改成p+下标解引用来进行赋值,就会好
	}
	free(p);
	p = NULL;
	return 0;
}

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

int main()
{
    
    
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
    
    
		return 0;
	}
	free(p);
	// ....
	free(p);
	p = NULL; // 在每个free后面进行指针赋值为NULL,也可以很好的解决这个错误,因为你一旦被赋值为NULL,那么后面对这个指针的任何操作都是不管用的。
	return 0;
}

6.动态开辟的内存忘记释放(内存泄漏)
一直在开辟内存,但是从来都不回收。

nt main()
{
    
    
	while (1)
	{
    
    
		malloc(1);
	}
	return 0;
}

经典例题
错误的

//运行程序会造成程序的崩溃
//str以值传递的形式给p,p是Getmemory函数的形参,只能函数内部有效,等Getmemory函数返回以后,动态开辟内存尚未释放,并且无法找到,会造成内存泄漏
void GetMemory(char *p)  // p是所创建的形参变量,在这个函数调用完成以后,这个这个形参就会自动的销毁,且无法在找到这个空间
{
    
     
	p = (char *)malloc(100);
} 
void Test(void) 
{
    
     
	char *str = NULL;   
	GetMemory(str);  
	strcpy(str, "hello world");   // 此时的str还是一个NULL
	printf(str);
}
int main()
{
    
    
	test();
	return 0;
}

改正以后的程序代码:传址

void GetMemory(char **p) 
{
    
    
	*p = (char *)malloc(100);
}
void Test(void)
{
    
    
	char *str = NULL;
	GetMemory(&str);
	strcpy(str, "hello world");   // 此时的str还是一个NULL
	printf(str);

	free(str);
	str = NULL;
}
int main()
{
    
    
	test();
	return 0;
}

第二种改正的方式

//程序可以理解为:p是上司,他所开辟的空间是卧底,在p死掉之前,如果他能告诉别人卧底的身份,那就还会有人知道卧底
char* GetMemory(char *p) 
{
    
     
	p = (char *)malloc(100);
	return p;
} 
void Test(void) 
{
    
     
	char *str = NULL;   
	str = GetMemory(str);  
	strcpy(str, "hello world");   // 此时的str还是一个NULL
	printf(str);
	free(str);
	str = NULL;
}
int main()
{
    
    
	test();
	return 0;
}

第二个例题:

返回栈空间地址的问题(只有栈区会出现这样的问题,在堆区和静态区都不会随便轻易出现非法访问内存的问题)

这里会出现非法访问内存的问题。结果是随机值

char *GetMemory(void)
{
    
     
	char p[] = "hello world";   //这里p创建的是一个局部的数组,当除了这个函数内部以后,就会销毁掉,但是他依旧返回了这个创建了字符数组的
	//地址,虽然此时str接受了这个p的地址,但是此时p里面是什么已经没有人知道了,所以结果是随机值
	return p;
} 
void Test(void) 
{
    
     
	char *str = NULL;   
	str = GetMemory(); 
	printf(str);
}
int main()
{
    
    
	Test();
	return 0;
}

在这里插入图片描述

int* test()
{
    
    
	static int a = 10; // 如果不加static 那么这里所创建的a在栈区的,除了这个函数,就会释放掉,然后你返回了a的地址,但是此时a里面的内容
	//已经被销毁了,所以就会出现非法访问内存的问题。
	//但是因为增加了static是这个变量从栈区变到了静态区,增加了这个局部变量的寿命,遍可以进行后续的修改,也不会报错
	return &a;
}
int main()
{
    
    
	int* p = test();
	*p = 20;
	return 0;
}

第三题:

//一定要有意识:看见malloc 和calloc,realloc就要进行free释放,他们两个是一定一定要成对出现的,不然就会出现内存泄漏的问题
void GetMemory(char **p, int num) 
{
    
     
	*p = (char *)malloc(num); 
} 
void Test(void) 
{
    
     
	char *str = NULL; 
	GetMemory(&str, 100); 
	strcpy(str, "hello"); 
	printf(str);
}
int mian()
{
    
    
	Test();
	return 0;
}

第四题:
结果会打印world,但是问题很大,因为非法访问内存了。

void Test(void) 
{
    
     
	char *str = (char *)malloc(100);   
	strcpy(str, "hello");  
	free(str);   
	if (str != NULL)  // 虽然我把str释放掉了,但是释放并不会把str自动的置成空指针。  
	{
    
     
		strcpy(str, "world");//此时这一块空间已经被释放了,你还要对他进行操作,造成了非法访问内存的问题。     
		printf(str);
	}
}
int main()
{
    
    
	Test();
	return 0;
}

在这里插入图片描述

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些 存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有 限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似 于链表。
  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

有了这幅图,我们就可以更好的理解在《C语言初识》中讲的static关键字修饰局部变量的例子了。

实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。

但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁所以生命周期变长。

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

struct S
{
    
    
	int n;
	int arr[];//结构体中最后一个是未知大小的--柔性数组成员--数组的大小是可以调整的
};
int main()
{
    
    
	//struct S s;
	//printf("%d\n", sizeof(s));// 结果是4,因为在计算柔性数组的大小的时候,是不包含柔性数组成员的
	struct S* ps = (struct S*)malloc(sizeof(struct S)+5*sizeof(int));
	//这里我进行开辟的时候是不知道这个柔型数组成员的大小的,所以我手动的给他设置一个大小,5*sizeof(int)这么大。
	ps->n = 100;
	int i = 0;
	for(i=0;i<5;i++)
	{
    
    
		ps->arr[i] = i;
	}
	struct S* ptr = realloc(ps,44);//此时我认为我原先开辟的柔性数组成员大小不够,我给他调整一下
	if(ptr != NULL)
	{
    
    
		ps = ptr;
	}
	for(i=0;i<10;i++)
	{
    
    
		ps->arr[i] = i;
	}
	//释放
	free(ps);
	ps = NULL;
	return 0;
}

如果只是希望我的结构体中的数组可大可小,还有一种方法。

struct S
{
    
    
	int n;
	int* arr;
};
int main()
{
    
    
	//这里一共使用了两次malloc所以也要进行两次的释放
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	ps->arr = malloc(5 * sizeof(int));
	int i = 0;
	for (i = 0; i < 5; i++)
	{
    
    
		ps->arr[i] = i;
	}
	for (i = 0; i < 5; i++)
	{
    
    
		printf("%d ", ps->arr[i]);
	}
	//调整大小
	int* ptr = realloc(ps->arr, 10 * sizeof(int));
	if (ptr != NULL)
	{
    
    
		ps->arr = ptr;
	}
	for (i = 5; i < 10; i++)
	{
    
    
		ps->arr[i] = i;
	}
	for (i = 5; i < 10; i++)
	{
    
    
		printf("%s ", ps->arr[i]);
	}
	//释放内存
	free(ps->arr); // 因为这里的ps->arr是包含在ps里面的,如果先释放ps指针,那么就找不到ps->arr这个指针了
	ps->arr = NULL;
	free(ps);
	ps = NULL;
	return 0;
}

第二种从形式上来看时更容易出错的,因为你两次使用malloc就要对应两次的free,容易遗忘并且还需要考虑先释放哪一个指针,如果释放错了也会犯错。但是使用柔型数组只需要进行一次malloc内存开辟,每使用一个malloc进行内存的开辟,就会造成内存碎片,使用的越多,内存碎片也就越多,内存的空间利用率就会很低,因为造成了很多不必要的内存浪费。(内存是连续的时候,访问效率更高,访问的速度更快)(寄存器 cache(高速缓冲区) 内存 硬盘 )这个是一个CPU访问的顺序

第一个好处是:方便内存释放

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

第二个好处是:这样有利于访问速度

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

猜你喜欢

转载自blog.csdn.net/MEANSWER/article/details/110284285