C语言进阶---动态内存管理

本章重点

  1. _为什么存在动态内存分配 _
  2. 动态内存函数的介绍
  • _ malloc_
  • _ free_
  • _ calloc_
  • _ realloc_
  1. 常见的动态内存错误
  2. 几个经典的笔试题
  3. C/C++程序的开辟
  4. 柔性数组

正文开始@马上回来了

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

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

int a =20;//在栈空间开辟四个字节
int arr[10] = {0};//在栈空间上开辟了连续40个字节的空间

以上两种开辟空间的方式有两个特点:

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

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

2. 动态内存函数的介绍

2.1 malloc和free

C语言提供了一个动态内存开辟的函数malloc,函数原型如下:
void* malloc (size_t size);
这个函数向内存申请一块连续可用的空间,并且返回指向这块空间的指针。

  • 如果开辟成功,则返回一个指向开辟好空间的指针。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 返回值是void*类型,所以malloc函数并不知道开辟空间的类型,具体在使用的时候自己来决定。
  • 如果参数size为0,malloc的行为是标准未定义的,取决于编译器。

C语言提供了另外一个函数free,专门用来做动态内存释放和回收的,函数原型如下:
void free (void* ptr);
free函数用来释放动态开辟的内存。(至于为什么动态开辟的内存需要释放,后面会详细讲解。)

  • 如果参数ptr指向的空间不是动态开辟的,那么free函数的行为是未定义的。
  • 如果参数ptr是空指针NULL,则函数什么事都不做。

malloc和free函数都声明在stdlib.h头文件中。
下面看看这两个函数的简单实用:

#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
int main()
{
	int num = 0;
	scanf("%d", &num);//输入你想要开辟空间的大小,单位是字节
	int * p = (int*)malloc(sizeof(int)*num);
	//INT_MAX 最大范围,超过了这个值无法开辟空间成功
	if (p == NULL)//判空
	{
		perror("malloc");//打印错误信息
		return 1;//如 如果没有开辟成功,到这里程序就结束了
	}
    //使用
	int ptr = p;//用ptr记录malloc返回的起始地址
	int i = 0;
	for (i = 0; i < num; i++)//循环遍历malloc开辟的连续空间
	{
		*p = i;//赋值
		printf("%d ", *p++);//打印
	}
	//free 释放
	free(ptr);
	//置空,防止野指针非法访问
	ptr = NULL;
	return 0;
}
int main()
{
	int num = 0;
	scanf("%d", &num);//输入你想要开辟空间的大小,单位是字节
	int* p = (int*)malloc(sizeof(int) * num);
	//INT_MAX 最大范围,超过了这个值无法开辟空间成功
	if (p == NULL)//判空
	{
		perror("malloc");//打印错误信息
		return 1;//如 如果没有开辟成功,到这里程序就结束了
	}
	int i = 0;
	for (i = 0; i < num; i++)//循环遍历malloc开辟的连续空间
	{
		*(p + i) = i;
		printf("%d ", *(p + i));
	}
	//free 释放
	free(p);
	//置空,防止野指针非法访问
	p = NULL;
	return 0;
}

你可能会疑惑:

  • 为什么malloc在使用之前要判空呢?

因为malloc不一定能够每次都能帮我们成功的开辟出一块连续的空间,比如说当你的参数是INT_MAX时,这时 已经超出了int类型最大值的范围,因此malloc函数会开辟空间失败,我们用perror打印出错误信息然后用 return结束。

  • 为什么动态开辟的内存空间我们要释放呢

我们调用malloc函数来帮我们开辟了一块连续的空间,我们把这块空间用完之后,如果程序运行结束了,动 态申请的内存由操作系统自动回收,如果程序运行不结束,操作系统是不会帮我们回收的,这样这块空间就 一直被占用会造成空间泄漏的问题。

  • 为什么malloc返回的指针所指向的空间在被 free函数释放之后还要置空?

因为malloc开辟的空间被free释放之后,这块空间就不在属于我们使用,指针p(ptr)如果再去访问这块空间就属于野指针去访问非法空间的问题了。

  • 代码1与代码2的区别?

在于对malloc返回的地址的使用不同。代码1是通过改变malloc返回的指针自身来访问这块连续的空间,因此在free释放前需要一个变量来记录改变前malloc返回的起始地址。代码2则是通过指针+整数与解引用的运算相结合来访问这块连续的空malloc返回的起始地址并未发生改变。

2.2calloc

C语言还提供了一个函数叫calloc,calloc函数也用动态内存分配。函数原型如下:
void* calloc (size_t num, size_t size);

  • 函数的功能是把num个大小为size的元素开辟一块空间,并且把每个空间的字节初始化为0。
  • 与函数malloc的区别在于在返回地址之前会把申请的空间的每个字节初始化为0。

举个例子:

int main()
{
	int * p =(int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
		printf("%d ", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}

image.png
所以如果我们要对申请的空间要求初始化,那么可以很方便的使用calloc函数来完成。

2.3realloc

  • relloc函数的出现让动态内存的分配管理更加灵活。
  • 有时候我们会发现过去申请的内存小了或者大了,那么可以使用realloc函数对动态开辟的空间大小做调整。

函数原型如下:
void* realloc (void* ptr, size_t size);

  • prt是要调整的内存地址
  • size是要调整之后的大小
  • 返回值为调整之后的内存起始位置
  • realloc在调整内存的时候存在两种情况:

C语言进阶---动态内存管理_第1张图片
情况1:原有空间之后有足够大的空间
当是情况1的时候,要扩展内存就直接在原有内存之后直接追加空间,原来空间的数据不发生变化。
情况2:原有空间后面没有足够大的空间
当是情况2的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上找另一个合适大小的连续空间来使用。这样函数返回的就是一个新的内存地址。
由于上述两种情况,realloc函数的使用就要注意一些。要避免realloc空间开辟失败又将原来malloc返回的地址弄丢的情况。
举个例子:

int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	
	//p = (int*)realloc(p, 80);
	//这样写的话,如果realloc开辟空间失败会返回NULL,p原本指向的malloc空间返回的起始地址也会被弄丢
	int *ptr = NULL;//创建指针ptr,并将其置空
	ptr=(int*)realloc(p, 80);
	if (ptr != NULL)//判空
	{
		p = ptr;
	}
	//使用
	int i = 0;
	for (i = 0; i < 20; i++)
	{
		*(p + i) = i;
		printf("%d ", *(p + i));
	}
    //释放
	free(p);
	p = NULL;
	return 0;
}

3.常见的动态内存错误

3.1 对NULL指针的解引用操作


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

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

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));//只开辟了10个int类型的空间
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 11; i++)//循环遍历了11个空间,i=10的时候是越界访问
	{
		*(p + i) = i;
		printf("%d ", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}

更正对内存布置进行检查:

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));//只开辟了10个int类型的空间
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)//循环遍历了10个空间
	{
		*(p + i) = i;
		printf("%d ", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}

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

int main()
{
	int a = 10;//a 是局部变量
	int* p = &a;
	free(p);
	return 0;

}

局部变量是在栈空间开辟的,程序调用结束时由编译器自动销毁。

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

void test() 
{
	int* p = (int*)malloc(100);
	p++;
	free(p);//p不在指向动态内存的起始地址
}
int main()
{
	test();
	return 0;
}

更正:如果要通过更改内存空间的起始地址来访问内存开辟的空间,那么应该提前使用一个的变量来记录内存空间起始位置的地址

int main()
{
	int num = 0;
	scanf("%d", &num);//输入你想要开辟空间的大小,单位是字节
	int * p = (int*)malloc(sizeof(int)*num);
	//INT_MAX 最大范围,超过了这个值无法开辟空间成功
	if (p == NULL)//判空
	{
		perror("malloc");//打印错误信息
		return 1;//如 如果没有开辟成功,到这里程序就结束了
	}
    //使用
	int ptr = p;//用ptr记录malloc返回的起始地址
	int i = 0;
	for (i = 0; i < num; i++)//循环遍历malloc开辟的连续空间
	{
		*p = i;//赋值
		printf("%d ", *p++);//打印
	}
	//free 释放
	free(ptr);
	//置空,防止野指针非法访问
	ptr = NULL;
	return 0;
}

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

void test()
{
	int* p = (int*)malloc(100);
	free(p);
	free(p);//重复释放
}

3.6 动态开辟内存忘记释放

void test()
{
	int* p = (int*)malloc(100);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	*p = 20;//未使用free释放动态开辟的空间
}
int main()
{
	test();
	while (1);//程序死循环
	return 0;
}

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

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

void test()
{
	int* p = (int*)malloc(100);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	*p = 20;
	//free释放
	free(p);
	p = NULL;
}
int main()
{
	test();
	while (1);//程序死循环
	return 0;
}

4.几个经典的面试题

4.1 题目1:

函数传参无法正常返回地址问题:

void Getmemory(char* p)//形参是实参的一份临时拷贝 p是指针变量 p指向NULL *p是NULL
{
	p = (char*)malloc(100);//p由指向NULL 变为指向malloc开辟空间的起始地址
}
void Test(void)
{
	char* str = NULL;
	GetMemory(str);//传的是实参
	strcpy(str, "helllo world");
	printf(str);
}
//程序会崩溃无法打印

更正:通过传实参的地址来找到实参在内存当中的位置然后再改变实参的值:

void GetMemory(char** p)//char**p接收str的地址,是一个二级指针
{
	*p = (char*)malloc(100);//*p==str 由指向NULL 变为指向malloc开辟空间的起始地址
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str);//转str的地址
	strcpy(str, "helllo world");
	printf(str);
	free(str);
	str = NULL;
}
int main()
{
	Test();
	return 0;
}
//程序会正常打印"hello world"

4.2题目2:

返回栈空间问题:

char* GetMemory(void)
{
	char p [] = "hello world";//栈空间开辟数组
	return p;//p放在寄存器当中,这个函数可以成功返回数组名p,也就是数组的首元素地址

}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{
	Test();
	return 0;
}
//栈空间在用完之后会被系统回收,因此创建的数组p在GetMemory调用完之后就已经被销毁无法使用了
//str属于是野指针去访问了一块被销毁的空间,因此会打印一些随机值
int* test()
{
	int a = 10;
	return &a;//a的地址被存放在寄存器当中
}
int main()
{
	int* p = test();//p接收到a的地址
	printf("%d\n", *p);//p已经是野指针  打印10 因为调用test的栈空间还没有被覆盖
	printf("%d\n", *p);// 打印随机值 有一次调用test函数,栈空间被新随机值给覆盖
	return 0;
}


4.3题目3:

void GetMemory(char** p, int num)
{
    assert(*p);
	*p = (char*)malloc(num);
}
void tset(void)
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
	//在使用完后没有free
}
int main()
{
	test();
	return 0;
}

更正:

void GetMemory(char** p, int num)
{
    assert(*p);
	*p = (char*)malloc(num);
}
void tset(void)
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
	//free释放
	free(str);
	str = NULL;
}
int main()
{
	test();
	return 0;
}

4.4题目4:

void test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);//free释放
	if (str != NULL)
	{
		strcpy(str, "world");//str指向的空间已经被释放,str成了野指针非法访问
		printf(str);//打印出world 但这个代码是错误的
	}
}
int main()
{
	test();
	return 0;
}

更正:

void test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);//free释放
	str = NULL;//置空
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);//无法打印
	}
}
int main()
{
	test();
	return 0;
}

5.C/C++程序的开辟

image.png

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

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

有了这幅图,我们就可以更好的理解在《C语言初识》中讲的static关键字修饰局部变量的例子了。
实际上局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被static修饰的变量存在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁,所以生命周期变长。

6.柔性数组

也许你重来没有听说过**_柔性数组 _** (flexible array)这个概念,但是它确实是存在的。
c99中,结构中的最后一个元素允许是未知大小的数组,这就叫【柔性数组】成员。

6.1 柔性数组的创建方式:

struct s1
{
	int i ;
	int arr[];//柔性数组成员,未指定大小
};

struct s2
{
	int i;
	int arr[0];//柔性数组成员,这里的0未指定大小
};

6.2 柔性数组的大小

typedef struct s
{
	int i;
	int arr[];//柔性数组成员,未指定大小
}s;

int main()
{
	printf("%d ", sizeof(s));//4
	return 0;
}

6.3 柔性数组的特点:

  • 结构中的柔性数组成员前面必须至少一个其他成员。
  • sizeof返回的这种结构大小不包括柔性数组的内存。
  • 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

6.4 柔性数组的使用

typedef struct s
{
	int num;
	int arr[];//柔性数组
}s;
int main()
{
	s* p = (s*)malloc(sizeof(s) + 10 * sizeof(int));//malloc开辟空间的大小要大于结构体的大小
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		p->arr[i] = i;//->成员访问操作符
		printf("%d ", p->arr[i]);
	}
	return 0;
}
typedef struct s
{
	int num;
	int arr[];//柔性数组
}s;
int main()
{
	s* p = (s*)malloc(sizeof(s) + 10 * sizeof(int));//malloc开辟空间的大小要大于结构体的大小
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//realloc扩容
	s* ptr = NULL;
	ptr = (s*)realloc(p, sizeof(s) + 20 * sizeof(int));
	if (ptr != NULL)
	{
		p = ptr;
	}
	int i = 0;
	for (i = 0; i < 20; i++)
	{
		p->arr[i] = i;//->成员访问操作符
		printf("%d ", p->arr[i]);
	}
	//free释放
	free(p);
	p = NULL;
	return 0;
}

6.5 柔性数组的优势

6.5.1 不使用柔性数组的例子
typedef struct s
{
	int i;
	int* arr;
}s;
int main()
{
	s* p = (s*)malloc(sizeof(s));//第一次malloc开辟一个结构体类型的空间
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	p->arr = (int*)malloc(10 * sizeof(int));//找到arr然后又使用malloc开辟一块空间
	if (p->arr == NULL)//arr所指向的地址开辟空间失败
	{
		perror("malloc");
		free(p);//将第一次malloc开辟的起始地址置空
		p = NULL;
		return 2;
	}
	//使用 ......0
	//释放
	free(p->arr);//先找到arr开辟的地址
	p->arr = NULL;
	free(p);//在找到p开辟的地址
	p = NULL;

	return 0;
}

image.png

你可能感兴趣的