【C语言】动态内存管理

目录

一、为什么要动态内存分配

二、malloc

三、free

四、calloc

五、realloc

六、常见动态内存分配的错误

(1)解引用NULL 指针

(2)越界访问动态分配内存

(3)用 free 释放非动态分配内存

(4)用 free 释放动态分配内存的一部分

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

(6)动态分配内存忘记释放(内存泄漏)

七、动态内存经典笔试题

(1)题目1

(2)题目2

(3)题目3

(4)题目4:

八、柔性数组

(1)柔性数组的概念

(2)柔性数组的特点

(3)柔性数组的使用

(4)柔性数组的优势

九、总结 C/C++ 中程序内存区域划分


一、为什么要动态内存分配

        以往的申请内存的方法:

// 定义变量
int a = 10; //一次性开辟一块空间 - 4个字节

// 定义数组
int arr[5]; //一块连续的空间 - 20 个字节

        但是这样方法在申请内存的时候,内存的大小就固定了,不能再调整。但在有些时候,比如用 S 类型的结构体存储一个学生的信息:

struct S
{
	char name[20];
	int age;
};

        再定义一个 S 结构体类型的数组,存储一个班级所有学生的信息:

struct S s[30];

        但是一个班级的学生,如果是20个,那么分配30个就会浪费;如果是32个,分配30个又会不够。

        因此,我们就希望根据键盘的输入申请变量内存大小,或者能在后续的代码中调整变量内存的大小。这种在程序运行时,或者后续编程时,才能确定需要的内存空间大小的情况,就需要用到动态内存分配。让程序员自己申请、调整、释放空间,更灵活,但更容易出错(比如忘记释放内存,程序又一直在运行,就容易导致内存不够,其它程序无法正常运行)。

二、malloc

        函数原型:

void* malloc (size_t size);

        作用:向内存申请一块连续可用的空间,并返回指向这块空间的指针。

        参数:

  • size 是需要分配的内存块大小(单位是字节)。
  • 如果 size 设置为0,malloc 的行为是未定义的,由编译器决定(避免出错,不要设置为0)。

        返回值:

  • 申请成功,则返回指向开辟好的空间的指针;申请失败,则返回 NULL(一定要检查返回值)
  • 返回值类型为 void*,具体是什么类型自己指定,因此需要强制转换

        示例:

int main()
{
    // 申请 10 个 int 类型变量的大小
	int* p = (int*)malloc(40); // 或者 10 * sizeof(int)
    // 检查返回值
	if (p == NULL)
	{
		perror("malloc");
		return 1;//异常返回
	}
	//使用空间
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i + 1;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}

	return 0;
}

        注意:并不是想申请多少内存空间都可以,不能超过内存的大小。如下,VS2019,x86(x64 可申请的内存空间比 x86 大)环境,malloc 申请内存空间,就超过了可分配内存的大小,出现错误:

三、free

        就像在图书馆借了书不还,不还太多,那么图书馆就空了。申请了内存空间不释放,内存空间就没有了,无法使用内存就会导致意外情况的发生,即内存泄漏(这是指申请内存的程序一直运行的情况。运行结束,操作系统会自动回收内存)。

        C语言中,使用 free 函数回收内存,函数原型如下:

void free (void* ptr);

        使用:

  • 如果 ptr 指向的内存不是动态分配的,free 的行为则是未定义的。如下,VS 2019 中报错:

  • 如果 ptr 指向的 NULL,free 则什么也不做。
  • 用 free 释放空间后,将指针设置为 NULL 是必要的(否则是野指针,即指向不可用内存的指针)。应像如下写:

        因为 free 释放后, p 实际上还是存储着原来的动态分配的内存空间的地址,厚着脸皮访问也行,但是会出现问题,所以 free 释放后要置 NULL,让指针无法访问被回收的空间:

四、calloc

        函数原型:

void* calloc (size_t num, size_t size);

        作用:为 num 个大小为 size (单位字节)的元素开辟一块空间,并且把空间的每个字节初始化为0效率会变低),而 malloc 不会初始化。如下:

        malloc,没初始化:

        calloc 会初始化为0:

        因此,想效率高,选 malloc;想初始化为0,选calloc

五、realloc

        函数原型:

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

        作用:如果想要调整过去申请过的内存空间的大小,就使用 relloc。

        参数:

  • ptr:要调整的内存地址。
  • size:调整之后新的内存空间大小(单位为字节)。
  • 如果 ptr 为NULL,则与 malloc 是一样的效果。如下:
//realloc函数也可以实现malloc的功能
int main()
{
	realloc(NULL, 20);//等价于 malloc(20)

	return 0;
}

        返回值:

  • 为调整后内存的起始位置。
  • 三种情况:

        情况1: ptr 指向的原有空间之后,有足够的空间。(返回 ptr)

        情况2: ptr 指向的原有空间之后,没有足够的空间,但能找到新的 size 大小的连续空间。(返回新的地址)

        情况3: ptr 指向的原有空间之后,没有足够的空间,并且不能找到新的 size 大小的连续空间。(返回 NULL)

        示例:

// 申请 20 Byte 空间
int * ptr = (int*)malloc(20);

//20 Byte 空间不够,调整为 40 Byte
int* p = (int*)realloc(ptr, 40);

        三种情况图解:

        不能把返回值直接赋值给 ptr,而是赋值给一个新的指针变量 p 。因为如果 realloc 失败,会返回 NULL,这时直接把返回值赋值给 ptr,就哪内存空间也用不了了。下面是正确的示例:

        注意:旧的空间操作系统会自动回收,所以不需要 free。

        malloc、free、calloc、realloc,都包含在头文件  <stdlib.h> 中。

六、常见动态内存分配的错误

(1)解引用NULL 指针

int main()
{
	int *p = (int*)malloc(40);
	*p = 20;//如果malloc失败,那么p的值是NULL,相当于没有空间,还存值

	return 0;
}

        解决办法:在 malloc、calloc 申请空间后,一定要检查返回值

	if (ptr == NULL)
	{
		perror("malloc"); // 或 calloc
		return 1;
	}

(2)越界访问动态分配内存

int main()
{
	//误认为申请了40个空间,就是40个int的大小
	int *p = (int*)malloc(40);//10*sizeof(int)
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//NULL

	int i = 0;
	for (i = 0; i < 40; i++) //越界访问
	{
		p[i] = i;
	}

	return 0;
}

        解决办法:malloc 时,最好写成如下形式:

// 申请10个int类型的内存空间大小
int *p = (int*)malloc(10*sizeof(int));

(3)用 free 释放非动态分配内存

int main()
{
	int* p = malloc(40);
	if (p == NULL)
	{
		return 1;
	}

	int arr[5] = {0};//栈区的空间
	p = arr;

	free(p);//释放栈区的空间,出错
	p = NULL;
	return 0;
}

(4)用 free 释放动态分配内存的一部分

        不能从中间的位置开始释放,只能从头开始释放一整块动态分配的内存。

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

        多次释放直接报错:

       解决办法,free 后指针置 NULL,free(NULL) 意味着不执行任何操作:

(6)动态分配内存忘记释放(内存泄漏)

        错误示范1:

        错误示范2:

七、动态内存经典笔试题

(1)题目1

        以下代码存在什么问题?

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

        图解:

        问题1:在空地址存储数据,程序崩溃,没有输出。

        问题2:返回主函数后,系统收回 p 形参的空间,但 malloc 的动态内存还存在,没有释放,内存泄漏

        解决方法1(传指针的地址):

        解决方法2(返回动态内存首地址):

(2)题目2

        以下代码存在什么问题?

char* GetMemory(void)
{
    char p[] = "hello world";
    return p;
}

void Test(void)
{
    char* str = NULL;
    str = GetMemory();
    printf(str);
}

int main()
{
    Test();
    return 0;
}

        图解:

        问题:返回栈空间地址,非法访问内存空间,输出未知的内容。如下图所示:

        扩展1:

        上面的代码没有问题,虽然 z 被销毁了,但是会先把返回值暂存在寄存器,返回主函数后,再从寄存器返回值。 

        扩展2:

        上面这个代码就是错的,返回值 &a 暂存在寄存器,寄存器再把返回值赋值给 p,但是此时局部变量a的内存空间已经被收回,继续通过指针p访问就是非法的,这就是返回栈空间地址的问题。

        扩展2的运行结果:

        扩展2输出未知内容的解释:

(3)题目3

        下面代码有何问题?

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;
}

        问题:free后,指针 str 未置NULL,成为野指针,造成后续进入 if 语句,非法访问。

        解决:

(4)题目4:

        下面代码有何问题?

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;
}

        问题:未 free 释放动态内存。

八、柔性数组

(1)柔性数组的概念

        C99中,结构体允许最后一个成员是大小未知的数组,叫柔性数组。形式如下:

struct S
{
	int n;
    //柔性数组
	int arr[];//或 int arr[0]
};

        但有些编译器只支持 arr[] 的写法。

(2)柔性数组的特点

  • sizeof 返回的结构体大小不包括柔性数组的内存。如下:

  • 结构中的柔性数组成员前面必须至少有一个其他成员(否则这个结构体就不知道大小了)。
  • 包含柔性数组成员的结构体进行动态内存分配,并且分配的内存应该大于结构体的大小(多了柔性数组成员的大小)。

(3)柔性数组的使用

// 柔性数组的使用
struct S
{
	int n;
	int arr[];//柔性数组
};

int main()
{
	//struct S s;//不会这样写
	//会这样写
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 20);
	// 检查
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	// 使用
	ps->n = 100;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		ps->arr[i] = i + 1;
	}
	// 扩大动态内存
	struct S* tmp = (struct S*)realloc(ps, sizeof(struct S) + 40);
	// 检查
	if (tmp == NULL)
	{
		perror("realloc");
		return 1;
	}
	ps = tmp;
	// 使用
	for (i = 5; i < 10; i++)
	{
		ps->arr[i] = i + 1;
	}
	// 输出
	for (i = 0; i < 10; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	// 释放
	free(ps);
	ps = NULL;

	return 0;
}

        运行结果:

        动态内存分配图解及验证:

(4)柔性数组的优势

        使用柔性数组,实现了:

  • 结构体的所有内存空间都是在堆上开辟的。
  • 数组的大小是可以调整的。

        也可以用其它的方法(不使用柔性数组),来实现上面的效果。给结构体变量分配动态的内存空间;把柔性数组成员替换成指针,让指针指向一块动态分配内存空间。代码如下:

struct S2
{
	int n;
	int* arr;
};

int main()
{
	struct S2* ps = (struct S2*)malloc(sizeof(struct S2));
	if (ps == NULL)
	{
		perror("malloc-1");
		return 1;
	}
	ps->n = 100;
	ps->arr = (int*)malloc(5 * sizeof(int));
	if (ps->arr == NULL)
	{
		perror("malloc-2");
		return 1;
	}
	//使用
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		ps->arr[i] = i + 1;
	}
	int* ptr = (int*)realloc(ps->arr, 10 * sizeof(int));
	if (ptr == NULL)
	{
		perror("realloc");
		return 1;
	}
	else
	{
		ps->arr = ptr;
	}
	for (i = 5; i < 10; i++)
	{
		ps->arr[i] = i + 1;//6 7 8 9 10
	}

	for (i = 0; i < 10; i++)
	{
		printf("%d ", ps->arr[i]);//1 2 3 4 5 6 7 8 9 10
	}

	//释放
	free(ps->arr);
	ps->arr = NULL;
	free(ps);
	ps = NULL;

	return 0;
}

        动态内存分配图解及验证:

        对比两种方法,柔性数组的优势:

  • 内存释放方便:柔性数组只需要释放一次内存,而方法2要分别释放两次(结构体的空间 + arr指针指向的空间。两块不连续的空间,起始地址不同,要分别释放)。
  • 内存碎片更少,避免内存空间的浪费:方法2不连续的内存空间更多,内存中不连续的空间太多,会导致剩下的夹缝中的内存很细碎,无法存储其它需要大块连续内存空间的数据,从而造成空间浪费。
  • 连续的内存有利于提高访问速度

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

九、总结 C/C++ 中程序内存区域划分

        内存映射段目前不需要知道是什么东西;代码段是存放程序编译之后,代码和常量的二进制指令。

        以左边的代码为例子:紫色框出的是全局变量、静态变量,存放在数据段;绿色框出的是局部变量、有关函数调用的信息,存放在栈区;黄色框出来的是动态分配的内存空间,在堆区;红色框出来的是初始化局部变量的常量,存放在代码段。

        总结:

  • 栈区(stack):① 函数的局部变量、形参、返回值、返回到的地址等函数信息,都在栈区创建函数执行结束,存储这些信息的空间也会被自动释放。② 栈区的内存分配在处理器的指令集中运算,效率高,但可分配的空间有限
  • 堆区(heap):① 由程序员释放,程序员不释放,程序结束后会被操作系统回收。② 分配方式类似于链表。
  • 数据段(静态区):存放全局变量、静态变量,程序结束后由系统释放。
  • 代码段:存放函数体的二进制代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值