动态内存管理

目录

1. 为什么要有动态内存分配

2. malloc和free

2.1 malloc

2.2 free

3. calloc和realloc

3.1 calloc

3.2 realloc

4.常见的动态内存的错误

4.1 对NULL指针的解引用操作

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

4.3 对⾮动态开辟内存使用free释放 

4.4 使用free释放⼀块动态开辟内存的⼀部分 

4.5 对同⼀块动态内存多次释放

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

5. 动态内存经典笔试题分析

5.1 题目1:

5.2 题目2:

​5.3 题目3:

5.4 题目4: 

6. 柔性数组

6.1 柔性数组的特点:

6.2 柔性数组的使用

6.3 柔性数组的优势 

7. 总结C/C++中程序内存区域划分


1. 为什么要有动态内存分配

我们已经掌握了开辟内存的方式有两种。

//1.在栈上开辟四个字节的空间
int a= 0;
//2.在栈上开辟十个字节的连续空间
char arr[10] = {'\0'};

但是上述的开辟空间的⽅式有两个缺陷:

空间开辟大小是固定的,比如a申请的是四个字节,以后我想变成八个字节,是做不到的。第二行代码,向内存申请了十个字节,我将来想变成二十个字节那也是做不到的,开辟的大小是不可改变的,比如我第二行代码arr数组,可以放是个元素,我放5个,那么5个字节的空间就会浪费,放10个的话就刚刚好,但是放15个的话又会不够。

数组在申明的时候,必须指定数组的长度,数组空间⼀旦确定了大小不能调整。

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

C语⾔引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。

2. malloc和free

2.1 malloc

malloc全写memory allocation(内存分配)

C语言提供了⼀个动态内存开辟的函数:

void* malloc (size_t size);

malloc - C++ Reference

上面的代码我们用数组的方式申请10个char类型的空间,现在我们可以使用malloc函数申请10个字节的空间:

#include <stdio.h>
#include <stdlib.h>//malloc函数头文件

int main()
{
	//申请10个字符类型的空间
	//void* p = malloc(10 * sizeof(char));
	/*
	开辟10个char类型的空间,返回的是void*的指针,放到p指针变量里面
	但是我是申请了10个字符类型的空间,未来我要以字符的视角来看待这块内存,
	所以我们可以强转为char*类型的指针,然后放到char*类型的指针变量里面。
	*/
	char* p = (char*)malloc(10 * sizeof(char));//malloc所申请的空间是连续的
	if (p == NULL)//对于malloc返回的值是需要判断的
	{
		//空间开辟失败
		perror("malloc");//打印失败原因
		return 1;//空间开辟失败就之间返回,非0表示异常返回
	}
	//开辟成功.可以使用这10个字节的空间
	for (int i = 0; i < 10; i++)
	{
		*(p + i) = 'a' + i;//往里面循环放入字符
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%c ", *(p + i));//通过地址打印放进去的字符
	}

	return 0;//0表示正常返回
}

输出结果:

malloc申请的空间和数组的空间有什么区别呢?

1.malloc申请的空间叫动态内存,动态内存的大小是可以调整的。

2.开辟空间的位置不一样,但是使用上是一样的。

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

如果开辟成功,则返回⼀个指向开辟好空间的指针。

如果开辟失败,则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查。

返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者自己来决定。

如果参数size为0,malloc的⾏为是标准是未定义的,取决于编译器。

当我们将INT_MAX(2147483647)传给malloc作为参数的时候, 该函数返回的是空指针,并且屏幕上打印没有足够的空间,所以不能开辟成功,任何内存都是资源,不能无截止的开辟。

2.2 free

起始我们刚刚写的代码还没有写完,malloc是用来申请空间的,申请空间,使用完之后还是需要释放空间的,malloc申请的空间是需要释放的。

C语⾔提供了另外⼀个函数free,专门是用来做动态内存的释放和回收的。

void free (void* ptr);

free - C++ Reference

#include <stdio.h>
#include <stdlib.h>
int main()
{
	//申请10个字符型空间
	char* p = (char*)malloc(10 * sizeof(char));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//使用这10个字节的空间
	for (int i = 0; i < 10; i++)
	{
		*(p + i) = 'a' + i;
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%c ", *(p + i));
	}
	//释放这10个字节的空间
	free(p);//这10个字节的地址在p里面,所以传p就可以
	p = NULL;//将p置为空指针,否则会变成野指针
	return 0;
}

前面我们提到malloc开辟的空间和我们创建的连续数组有两种区别,那么还有没有别的区别呢?

 

free函数⽤来释放动态开辟的内存。

如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。

如果参数ptr是NULL指针,则函数什么事都不做。 

将来对于malloc函数和free函数需要成对使用,有来有回,有申请有释放。

3. calloc和realloc

3.1 calloc

C语⾔还提供了⼀个函数叫 calloc , calloc 函数也⽤来动态内存分配。

void* calloc (size_t num, size_t size);

calloc - C++ Reference

函数的功能是为num个大小为size的元素开辟⼀块空间,并且把空间的每个字节初始化为0。

与函数malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0。

示例代码:

#include <stdio.h>
#include <stdlib.h>
int main()
{
	//申请10个整型的空间
	//malloc(10*sizeof(int)); //对于malloc来说是一次性算好的
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)//calloc也是一样的,一旦空间申请失败就返回空指针,需要判断
	{
		perror("calloc");
		return 1;
	}
	//使用空间
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);//p[i] <==> *(p + i)
	}
	//释放空间
	free(p);//calloc申请的空间也是在堆上申请的,所以释放空间也是free释放
	p = NULL;
	return 0;
}

3.2 realloc

realloc函数的出现让动态内存管理更加灵活。

有时会我们发现过去申请的空间太小了,有时候我们⼜会觉得申请的空间过大了,那为了合理的使用内存,我们⼀定会对内存的大小做灵活的调整。那realloc函数就可以做到对动态开辟内存大小的调整。

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

realloc - C++ Reference

ptr是要调整的内存地址。

size是调整之后新大小。

返回值为调整之后的内存起始位置。

这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

realloc在调整内存空间的是存在两种情况:

情况1:原有空间之后有足够大的空间

情况2:原有空间之后没有足够大的空间

情况1: 

当是情况1的时候就会比较简单,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发⽣变化,返回的是旧地址。

情况2:

当是情况2的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找⼀个合适大小的连续空间来使用,意思就是在别的地方直接开辟80个字节的空间,将旧的空间的数据拷贝到新的空间,然后释放旧的空间,这样函数返回的是⼀个新的内存地址。

除了上面的两种情况,还有可能会返回NULL。

#include <stdio.h>
#include <stdlib.h>
int main()
{
	//申请10个整型的空间
	int* p = (int*)malloc(10*sizeof(int)); //对于malloc来说是一次性算好的
	//int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)//calloc也是一样的,一旦空间申请失败就返回空指针,需要判断
	{
		perror("calloc");
		return 1;
	}
	//使用10个整型的空间
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);//p[i] <==> *(p + i)
	}
	//调整空间 - 希望变成20个整型
	//这里不要用p来接收,如果realloc开辟失败的话就会返回NULL,用p接收会报p修改为空
	//指针,原来的10个整型的空间也会找不到,所以这里只能用临时变量来接收
	//如果缩小的话realloc函数第二个参数变小就可以了
	int * tem = (int*)realloc(p, 80*sizeof(int));
	if (tem == NULL)
	{
		//如果临时变量是空指针就说明扩容失败,打印错误信息退出就可以了
		perror("realloc");
		return 1;
	}
	//如果临时变量不等于空指针,就可以将临时变量的地址赋值给p,然后将p置为NULL
	p = tem;
	tem = NULL;
	//使用扩容后的20个整型的空间
	
	 
	
	 
	//释放空间
	free(p);//calloc申请的空间也是在堆上申请的,所以释放空间也是free释放
	p = NULL;
	return 0;
}

realloc函数的参数不能乱传,不能随便传一个地址,想调整哪个空间把哪个空间的起始地址传进去,并且这块空间必须是动态开辟的。

4.常见的动态内存的错误

4.1 对NULL指针的解引用操作

#include <stdio.h>
#include <stdlib.h>
int main()
{
	//申请空间
	int* p = (int*)malloc(1000 * sizeof(int));
	//使用
	for (int i = 0; i < 10; i++)
	{
		p[i] = i;
	}
	//销毁
	free(p);
	p = NULL;
	return 0;
}

未来对malloc的返回值一定要进行判断。

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

动态内存开辟的空间也是有大小的,并不是想怎么用就怎么用。

#include <stdio.h>
#include <stdlib.h>
int main()
{
	//申请空间
	int* p = (int*)malloc(10 * sizeof(int));//申请可10个整型的空间 - 40个字节
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//使用
	for (int i = 0; i < 40; i++)//循环了40次,肯定会越界访问 - 整型的形式访问而不是字节
	{
		p[i] = i;
	}
	//销毁
	free(p);
	p = NULL;
	return 0;
}

4.3 对⾮动态开辟内存使用free释放 

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int a = 10;
	int* p = &a;
	//.......
	free(p);
	p = NULL;
	return 0;
}

指针变量p不是动态内存开辟的,所以不能使用free来释放。

4.4 使用free释放⼀块动态开辟内存的⼀部分 

#include <stdio.h>
#include <stdlib.h>
int main()
{
	//申请空间
	int* p = (int*)malloc(100 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//使用
	for (int i = 0; i < 5; i++)
	{
		p[i] = i;//p[i] = *(p+i)

		/**p = i;
		p++;*/
	}
	//销毁
	free(p);
	p = NULL;
	return 0;
}

4.5 对同⼀块动态内存多次释放

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

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

切记:动态开辟的空间⼀定要释放,并且正确释放。

malloc和calloc是用来开辟内存的,那么realloc只能用来调整空间吗?

#include <stdio.h>
#include <stdlib.h>
int main()
{

	int* p = (int*)realloc(NULL, 40);//==malloc(40);
	/*
	realloc第一个参数传空指针,第二个参数传40,就等价于malloc(40),直接申请40个
	字节的空间,因为它没法调整,传空指针没法调整,
	*/
	if (p == NULL)
	{
		perror("realloc");
		return 1;
	}

	//使用

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

其实并不是的,realloc函数也是可以用来开辟内存空间的。

5. 动态内存经典笔试题分析

5.1 题目1:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}

解析:

上面的代码,printf这样传参也是可以的。

#include <stdio.h>
int main()
{
	printf("haha\n");//给printf的不是字符串本身,而是字符串首字符的地址

	//那么

	char* p = "haha\n";//常量字符串赋给p的时候也是给首字符的地址
	//所以printf接收到字符串的首字符的地址的时候是可以实现打印的
	printf(p);

	return 0;
}

 那么,上面的代码修改正确应该怎么写呢?

从代码可以看出,malloc申请的100个字节的空间是放到p里面的,实际上它是想放到str里面,然后将hello world拷贝到这100个字节的空间中,然后实现打印,但是以值传递的时候,将str的值传递给p的时候,p是一块独立的空间,把地址放到p里面,不会影响str,我们知道,函数传参,值传递的时候,形成是实参的一份临时拷贝,修改形参不会影响实参,所以str以后为空指针,拷贝的时候就会失败,那我们就可以传地址。

#include <stdio.h>
#incluide <stdlib.h>
#include <string.h>
//写法1
void GetMemory(char** p)
{
	*p = (char*)malloc(100);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str);
	strcpy(str, "hello world");
	printf(str);
	free(str);
	str = NULL;
}
int main()
{
	Test();
	return 0;
}

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

运行:

5.2 题目2:

#include <stdio.h>
char* GetMemory(void)
{
	char p[] = "hello world";
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{
	Test();
	return 0;
}

解析:

5.3 题目3:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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 main()
{
	Test();
	return 0;
}

解析:

5.4 题目4: 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}
int main()
{
	Test();
	return 0;
}

解析:

6. 柔性数组

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

什么是柔性数组呢?

1.在结构体中

2.最后一个成员

3.位置大小的数组

//写法1
struct S
{
	int n;
	char c;
	double d;
	int arr[];//未知大小的数组 - arr就是柔性数组的成员
};

//写法2
struct S2
{
	int n;
	char c;
	double d;
	int arr[0];//写成0的时候也是未知大小的数组 - arr就是柔性数组的成员
};

有的编译器支持第一种写法,有的编译器支持第二种写法,就像vs编译器,两种写法都支持。

6.1 柔性数组的特点:

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

如果前面没有其它成员,而且柔性数组的大小是未知的,那么结构的大小就没法算。

2.sizeof返回的这种结果大小不包括柔性数组的内存。

如果前面没有其它成员,而且不包含柔性数组,那这个结构体大小是0吗,很明显是不可能的。 

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

6.2 柔性数组的使用

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct S
{
	int n;
	int arr[];
};

int main()
{
	//struct S s;//包含柔性数组的结构不会这样创建变量,这样创建只有n的4个字节
	//柔性数组是没有大小的
	//                                      n  4字节    arr   80字节
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));
	//站在ps的角度,这边分配了84个字节的空间,并且是struct S类型的数据,那么
	//就可以访问里面的变量n和数组arr
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	//使用
	ps->n = 110;
	for (int i = 0; i < 20; i++)
	{
		ps->arr[i] = i + 1;
	}
	//销毁
	free(ps);
	ps = NULL;
	return 0;
}

 那既然是柔性数组,那么柔性体现在哪里呢?

因为前面的n和arr都是malloc来的,那么就可以通过realloc来调整这块空间,一旦通过realloc来调整这块空间的时候,那么数组的空间就可大可小,后面这块空间在柔性的变长变短。

#include
struct S
{
	int n;
	int arr[];
};

int main()
{
	//struct S s;//包含柔性数组的结构不会这样创建变量,这样创建只有n的4个字节
	//柔性数组是没有大小的
	//                                      n  4字节    arr   80字节
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));
	//站在ps的角度,这边分配了84个字节的空间,并且是struct S类型的数据,那么
	//就可以访问里面的变量n和数组arr
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	//使用
	ps->n = 110;
	for (int i = 0; i < 20; i++)
	{
		ps->arr[i] = i + 1;
	}
	//调整ps指向空间的大小
	struct S* tem = (struct S*)realloc(ps, sizeof(struct S) + 40 * sizeof(int));
	if (tem == NULL)
	{
		perror("realloc");
		return 1;
	}
	ps = tem;
	tem = NULL;
	//使用调整后的空间
	ps->n = 0;
	for (int i = 0; i < 40; i++)
	{
		printf("%d\n", ps->arr[i]);
	}
	//销毁
	free(ps);
	ps = NULL;
	return 0;
}

6.3 柔性数组的优势 

#include <stdio.h>
#include <stdlib.h>
struct S
{
	int n;
	int* arr;
};

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	int* tmp = (int*)malloc(20 * sizeof(int));
	if (tmp != NULL)
	{
		ps->arr = tmp;
		tmp = NULL;
	}
	else
	{
		return 1;
	}
	//使用
	ps->n = 100;
	for (int i = 0; i < 20; i++)//赋值为1~20
	{
		ps->arr[i] = i + 1;
	}
	//调整空间为40个字节
	tmp = (int*)realloc(ps->arr, 40 * sizeof(int));
	if (tmp != NULL)
	{
		ps->arr = tmp;
		tmp = NULL;
	}
	else
	{
		perror("malloc");
		return 1;
	}
	//使用调整后的空间
	ps->n = 200;
	for (int i = 0; i < 40; i++)
	{
		printf("%x ", ps->arr[i]);
	}
	//销毁
	//先释放ps->arr,如果先释放ps的话就找不到ps->arr
	free(ps->arr);
	ps->arr = NULL;//其实这句不写也可以,随后ps也会被释放的
	free(ps);
	ps = NULL;
	return 0;
}

6.2中的代码我们使用了一次malloc.而上面的代码和我们6.2中的代码实现的效果一样,但是使用了两次malloc,那就得用两次free,那么维护起来就容易出错,所以我觉得6.2中的代码更好一些。

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

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

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

连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个⼈觉得也没多⾼了,反正你 跑不了要用做偏移量的加法来寻址)

C语言结构体里的成员数组和指针 | 酷 壳 - CoolShell

7. 总结C/C++中程序内存区域划分

在学校计算机语言的时候是这样讨论的,学习操作系统的时候会变。

C/C++程序内存分配的几个区域:

1. 栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限, 栈区主要存放运行函数而分配的局部变量,函数参数,返回数据,返回地址等。

《函数栈帧的创建和销毁》

2. 堆区(heap):⼀般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。

3. 数据段(静态区):(static)存放全局变量,静态数据。程序结束后由系统释放。

4. 代码段:存放函数体(类成员函数和全局函数)的⼆进制代码,代码段的数据是不能被修改的。

  • 7
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值