6000字总结动态内存管理

目录

一、内存划分

二、四大函数

①malloc

②free

③calloc

④realloc

三、易错分析

问题一:

问题二: 

问题三:

问题四:

问题五: 

问题六:

四、经典面试题

面试题一:

面试题二:

面试题三:

五、柔性数组

1.前言

2.特点

3.优势


一、内存划分

要理解动态内存管理,首先要了解C程序对内存划分的主要形式:

栈区

①在执行函数时,函数局部变量的存储单元都可以在上创建,函数执行结束时这些存储单元自动被释放

②栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。(栈溢出问题)

③ 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。

向下增长指从栈依次申请的地址在减小

堆区

①一般由程序员释放, 若程序员不释放,程序结束时可能由OS(operate system)回收 。分配方式类似于链表。

②动态内存开辟在堆区上。

向上增长指从堆依次申请的地址在增加

数据段(静态区)

①存放全局变量、静态数据。

②程序结束后由系统释放

代码段

①存放函数体(类成员函数和全局函数)的二进制代码。

②其中的数据只可被读取,不可被修改

  6000字总结动态内存管理_第1张图片(上图来源比特科技


 

二、四大函数

①malloc

作用:在堆区申请一块size_t大小(单位字节)的空间。成功返回动态开辟空间的地址,失败返回空指针NULL 。(比如开辟的空间太大了就会失败)

 注意点:1.size大小为0的情况是未定义的,其结果取决于编译器的处理

                2.由于malloc返回值为void*类型,在用指针接收时最好先强制类型转换

                3.小心内存开辟失败返回空指针

#include
int main()
{
	int*p = (int *)malloc(40);
	return 0;
}

看!我们很轻松的动态开辟了一块指定大小的空间。但不知道你是不是有这样的疑惑:一直开辟空间那电脑的内存不会越来越小吗? 其实当我们程序结束时,动态开辟的空间会被自动回收,当然我们也可以选择主动出击——使用free函数。

②free

作用:释放动态开辟的空间(memblock为指向动态开辟空间的指针)

注意点:1.free(NULL),函数不执行任何操作

               2.不能用free函数释放非动态开辟的空间

               3.free只是释放空间,并没有清除指针,也就是说你和女朋友分手了,但你仍然牢                   牢记着人家的电话号码(空间地址)。若此时对指针解引用,就犯了非法访问内存                 的错误,所以要及时将指针赋值为空,对人家彻底死心。

#include
#include
int main()
{
	int i = 0;
	int*p = (int *)malloc(10*sizeof(int));
    assert(p);//监测是否开辟成功
	for (i = 0; i < 10; i++)
	{
		p[i] = i;//或者为*(p+i),但不可以是p++。
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ",p[i]);
	}
	free(p);
	p = NULL;//最好置为NULL
	return 0;
}

 malloc和free的组合拳,完美的完成了动态内存开辟的过程。但其实动态开辟内存不只是malloc的特权,calloc也具有,只不过二者的作用有所不同

③calloc

作用:向堆区申请一块空间,存放num个size大小的元素。成功返回指向开辟空间的指针,失败则返回NULL

区别:calloc开辟空间后会将每个元素初始化为0。所以malloc开辟空间效率更高,calloc会自动赋值为0,各有所长。

 calloc开辟——初始化为06000字总结动态内存管理_第2张图片

 malloc开辟——未初始化

6000字总结动态内存管理_第3张图片

认识了malloc,calloc这类动态开辟空间的函数,聪明的你或许会想到如果开辟的空间不够怎么办。别担心,C语言给出了realloc函数调整动态开辟空间的大小。 

④realloc

 作用:将动态开辟的内存大小调整为size(单位字节)

 注意要深刻理解realloc调整大小的两种情况:

情况一:

6000字总结动态内存管理_第4张图片

 若原有空间充足,则就直接在原有内存之后直接追加空间,原来空间的数据不发生变化。

情况二: 

6000字总结动态内存管理_第5张图片

原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用,复制原来的内容再追加。这样函数返回的是一个新的内存地址

malloc创建的p所指向的空间

6000字总结动态内存管理_第6张图片

 realloc后p1指向的空间

6000字总结动态内存管理_第7张图片

realloc之后p指向的空间6000字总结动态内存管理_第8张图片

 我们根据上面的分析可以得出以下结论(空间不足时):

1.返回一个新的地址指向新开辟的空间

2.原空间的内容会被复制到新开辟的空间

3.原空间的内容被释放

 当传入的指针为空指针时,此时realloc的功能相当于malloc的。


 

三、易错分析

试着分析一下以下代码有什么缺陷或者不足

问题一:

#include
#include
//缺陷版
int main()
{
	int i = 0;
	int*p = (int *)malloc(5*sizeof(int));
	for (i = 0; i < 5; i++)
	{
		p[i] = i;
	}
	p = realloc(p, 1000000);//ok?
	free(p);
	p = NULL;
}



//完善版
int main()
{
	int i = 0;
	int*p = (int *)malloc(5*sizeof(int));
    assert(p);
	for (i = 0; i < 5; i++)
	{
		p[i] = i;
	}
	int *p1 = (int *)realloc(p, 1000);
    if(p1!=NULL)
    {
        p=p1;//为了程序的连贯性,最好都赋值给最开始使用的p
    }
	free(p);
	p = NULL;
}

1.无论是malloc或者是realloc都要小心内存开辟失败返回NULL的情况,所以检验必不可少

2.“p = realloc(p, 1000000);”的写法如果失败那原来的地址也找不到了,可谓是赔了夫人又折兵。

问题二: 

void test()
{
	int i = 0;
	int *p = (int *)malloc(10 * sizeof(int));
	if (NULL == p)
	{
		exit(EXIT_FAILURE);
	}
	for (i = 0; i <= 10; i++)//ok?
	{
		*(p + i) = i;
	}
	free(p);
}

 1.注意越界访问的问题,"i<10"才不会让程序崩溃

问题三:

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

 1.不能用free释放不是动态开辟的内存

问题四:


void test()
{
	int *p = (int *)malloc(100);
	p++;//ok?
	free(p);
}

 1.p的地址不能改变,否则在free时会产生释放部分空间错误

问题五: 

void test()
{
	int *p = (int *)malloc(100);
	free(p);
	free(p);
}

1.很显然该程序犯了重复释放的错误

2.在这段简短的代码中可以轻而易举的发现问题,但如果在很长的代码中我们如何避免?

   ①做到谁开辟谁释放空间

   ②养成将指针置为NULL的习惯,这样即使被free也是free(NULL),如前文所说无事发生

问题六:

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

1.执行上述程序时,打开任务管理器可以发现CPU被占用的空间一直增大(电脑有保护机制,到一定程度就停止增长),一直开辟空间但是不释放使我们空余的内存越来越少,这也是就我们所说的内存泄漏问题

2.所以动态开辟的内存一定要释放并且正确释放。


 

四、经典面试题

面试题一:

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

 请问运行的结果是什么?              程序崩溃

 分析:初学者往往很容易进入这个误区,认为str成功指向了开辟的空间。可仔细一想,str真的如我们所想吗?在这里我们混淆了传址操作传参操作,图中的操作属于传参操作,p不过是str的一份临时拷贝,改变p对str没有一点影响。当然上面的代码也存在没有检查返回值,没有主动释放内存的错误。

我们如何纠正呢,在接下来的面试题里做详细解答。

面试题二:

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

请问上述程序的结果是什么? 打印随机值。

分析:如果你认为GetMemory函数返回的是p的地址,没错,你想的很对。但此时的p所指向的内容不再是“"hello world"”,为什么呢?p[ ]数组在函数中创建,正如文章开头提到的内存布局,函数申请的局部变量开辟在栈区上,而开辟在栈区上变量的特点是当这个函数结束,变量也随即销毁,所以p只是记住当时的地址,里面的内容不再是"hello world"。如果此时对str进行解引用操作,则会出现非法访问内存的错误。

面试题三:

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

请问上述程序的结果是什么? 打印“world”

分析:不知道有没有小伙伴们认为没有打印结果的,如果有,我猜想应该是对free的作用产生误解。free只是对堆区的内存进行释放,并不会将str指向的地址赋值为空指针。

如何修改面试一的程序:

分析完上面三个面试题后相信你对动态内存开辟有了更深的理解,我们再回到第一个问题,如何将他修改正确呢?其实面试题一中代码之所以失败的原因在于没有将p与str建立起练习,以此为突破口,我们可以有以下两个思路:1.传址操作  2.返回p的地址。

//采用传址操作
void GetMemory(char **p)
{
	*p = (char *)malloc(100);
}
int main()
{
	char *str = NULL;
	GetMemory(&str);
	if (str != NULL)
	{
		strcpy(str, "hello world");
		printf(str);
	}
	free(str);
	str = NULL;
	return 0;
}

//返回p的地址
char* GetMemory(char *p)
{
	p = (char *)malloc(100);
	return p;
}
int main()
{
	char *str = NULL;
	str=(char *)GetMemory(str);
	if (str != NULL)
	{
		strcpy(str, "hello world");
		printf(str);
	}
	free(str);
	str = NULL;
	return 0;
}

 

五、柔性数组

1.前言

  我们要向实现数组长度的动态变化,以下代码可以吗? 

const int n = 0;
int main()
{
	int arr[n] = { 0 };
	return  0;
}

显然不可以,[ ]里必须是常量,即使加const修饰,也还是变量,只不过这个变量不可以被修改罢了。那我们是否可以使用一个指向动态开辟的空间的指针呢?这样的方法确实是可以的,但在这里会介绍一种更优的方法——柔性数组(C99中新增)。我们会在最后比较两者的优劣。

2.特点

1.结构中的柔性数组成员必须在最后一个
2.sizeof(struct s) 返回的这种结构大小不包括柔性数组的大小。
3.包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

使用示范:

struct s
{ 
	int n;
	int arr[0];
};
int main()
{
	int i = 0;
	struct s*p = (struct s*)malloc(sizeof(struct s)+40);//注意struct的大小不包括动态开辟的数组
	if (p != NULL)
	{
		for (i = 0; i < 10; i++)
		{
			p->arr[i] = i;
		}
	}
	free(p);
	p = NULL;
	return 0;
}

3.优势

我们先试着用指针实现上面的功能

#include
struct s
{ 
	int n;
	int *arr;
};
int main()
{
	struct s*ps =(struct s*) malloc(sizeof(struct s));
    ps->n=10;
	if (ps != NULL)
	{
		ps->arr = (struct s*)malloc(ps->n*sizeof(int));//用arr接收开辟空间的地址
		if (ps->arr != NULL)
		{
			int i = 0;
			for (i = 0; i < 10; i++)
			{
				ps->arr[i] = i;
			}
		}
	}
	free(ps->arr);
	ps->arr = NULL;
	free(ps);
	ps = NULL;

	return 0;
}

相比于用指针动态开辟数组空间,柔性数组有以下三个优势:

1.释放空间更加方便

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

2.减少内存碎片

使用柔性数组创建的n与arr是一起创建的,在空间上连续。而使用指针创建的结构体和结构体内指针指向空间是不连续的,其间往往会有被浪费的内存碎片

6000字总结动态内存管理_第9张图片

3.提高访问速度

在计算机中CPU读取速度          硬盘<内存<高速缓存<寄存器

计算机在读取数据时遵循“局部性原理”,即接下来访问的内存80%的概率在当前内存附近,所以寄存器会预先读入周围的数据,如果数据不连续,那意味着寄存器命中失败,则要从高速缓存到内存甚至到硬盘搜索,直到找到为止。速度自然慢了。

你可能感兴趣的:(c学习笔记,c)