前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >【C语言动态内存管理】—— 智能分配与精准释放之道,打造高效内存循环

【C语言动态内存管理】—— 智能分配与精准释放之道,打造高效内存循环

作者头像
换一颗红豆
发布2024-12-20 11:05:10
发布2024-12-20 11:05:10
59100
代码可运行
举报
运行总次数:0
代码可运行

1、引入

在之前的学习中我们已经学会了内存开辟的方式,如定义变量或者定义数组都会在栈上开辟空间,申请内存!

代码语言:javascript
代码运行次数:0
复制
int main()
{
	//定义int变量,在栈上开辟4个字节
	int value = 10;
	//定义char数组,在栈上开辟10字节的连续空间
	char ch[10] = { 0 };
	//定义double数组,同样在栈上开辟5x8=40个字节的连续空间
	double dp[5] = { 0.0 };
}

上面我们开辟内存空间的方式有如下特点:

1、内存空间开辟的大小是固定的,int类型变量4个字节,double类型变量8个字节,char类型变量1个字节 2、数组声明时必须指定大小,数组大小一旦确定就不能修改。 3、申请的空间可能过大或者过小,造成数据溢出或者内存浪费

上面这种内存分配我们叫做静态内存分配,在程序编译阶段就确定了内存的分配大小和布局

在实际编程中,程序处理的数据量常常是不固定的,有时我们需要的内存空间在程序运行时才能确定,这时静态内存分配无法满足我们对内存申请的需求,为此,C语言引入了动态内存分配,动态内存分配允许程序根据实际输入的数据量来分配内存,而不是预先定义一个可能过大或过小的固定大小的内存空间。这样可以避免浪费内存(当预先分配的空间远超实际需求时),也能防止数据溢出(当预先分配的空间小于实际需求时)。

2、内存空间分布

在讲解动态内存分配前,我们先来认识下内存空间时如何划分的,我们的程序中的变量都储存在内存中的什么地方。

2.1、代码区(Text Segment)

定义和功能:

代码区是存储程序机器指令的内存区域。当程序被编译后,编译器将 C 语言代码转换为机器语言指令,这些指令就存放在代码区。这是程序的只读部分,因为在程序执行过程中,这些指令通常不会被修改

存储内容细节:

除了可执行的机器指令外,代码区还可能包含一些只读的数据,如常量字符串。例如,const char *message = "Hello, World!";中的字符串"Hello, World!"通常会存储在代码区。这是因为这些字符串在程序运行过程中不需要修改,将它们放在只读的代码区可以提高内存使用效率和程序的安全性。

分配时机和生命周期:

代码区的内存是在程序编译时就确定分配的。编译器会根据程序的代码内容计算出所需的内存空间,并在程序加载到内存时将这些指令放入代码区。这个区域的内存在整个程序运行期间一直存在,直到程序结束。它的大小和内容在程序运行过程中基本保持不变,除非有特殊的动态加载库或代码修改技术(如自修改代码,这种情况在常规的 C 语言程序中很少见)

访问权限与安全性:

代码区通常具有只读权限,这是为了防止程序在运行过程中意外地修改自己的指令,从而导致程序逻辑混乱或安全漏洞。操作系统和硬件会对代码区的访问进行保护,如果程序试图写入代码区,可能会引发异常或错误。这种保护机制有助于维护程序的稳定性和安全性。


2.2、数据区(Data Segment)

2.2.1、全局初始化数据区(.data)

定义与功能:

这个区域存储已经初始化的全局变量和静态变量。全局变量是在函数外部定义的变量,而静态变量可以是在函数内部定义的具有静态存储持续时间的变量。这些变量在程序启动时就被赋予了初始值,并且在整个程序运行过程中可以被不同的函数访问和修改。

存储内容细节:

存储的数据类型可以是各种基本数据类型(如int、float、char等),也可以是自定义的结构体等复合数据类型。数据的存储格式和大小取决于变量的类型和编译器的实现。

分配时机与生命周期:

在程序编译时,编译器会确定这些变量所需的内存空间,并在程序加载到内存时进行初始化和分配。这些变量的生命周期与程序相同,从程序启动开始就存在,直到程序结束才被销毁


2.2.2、全局未初始化数据区(.bss)

定义与功能:

存储未初始化的全局变量和静态变量。未初始化的全局变量会被放置在全局未初始化数据区。在程序启动时,系统会自动将这个区域中的变量初始化为默认值(对于基本数据类型,如int通常初始化为 0)。这个区域的存在主要是为了提高内存使用效率,因为未初始化的变量不需要在可执行文件中占用实际的存储字节来保存初始值,只需要记录变量的名称和类型等信息,在程序加载时再进行初始化。

存储内容细节:

和全局初始化数据区类似,存储的是各种未初始化的全局和静态变量。变量的大小和类型决定了在这个区域中占用的内存空间。

分配时机与生命周期:

在编译时确定变量的信息,在程序加载时分配内存并初始化。其生命周期也是从程序启动到程序结束。


2.3、堆区(Heap)

定义与功能:

堆是一个由程序员手动管理的内存区域,主要用于动态内存分配。它提供了一种灵活的方式来获取和释放内存,使得程序能够在运行时根据实际需求分配任意大小的内存块。与栈不同,堆的内存分配和释放不是由系统自动完成的,而是需要程序员通过特定的函数(如malloc、calloc、realloc和free)来操作。

存储内容细节:

堆可以存储各种类型的数据结构和数据量。例如,大型的动态数组、复杂的树结构或图结构(如二叉树的节点、图的顶点和边等)如果是通过动态分配内存创建的,就存储在堆中。堆中的内存块通常是不连续的,随着程序的运行,内存块被不断地分配和释放,可能会导致内存碎片的产生。内存碎片是指堆中存在许多小的、不连续的空闲内存块,这可能会影响后续的内存分配效率。

分配时机与生命周期:

堆内存是在程序运行时通过调用动态分配函数来分配的。例如,当执行malloc函数时,系统会在堆中查找足够大小的空闲内存块并分配给程序。堆内存的生命周期由程序员控制,从分配开始,直到通过free函数释放为止。如果程序没有正确地释放堆内存,就会导致内存泄漏,这可能会逐渐耗尽系统的内存资源,导致程序或系统出现故障。

管理方式与复杂性:

堆的管理相对复杂。由于堆内存是手动分配和释放的,程序员需要小心地跟踪每个内存块的分配情况,确保每个分配的内存块都有相应的释放操作。而且,不同的操作系统和编译器对堆的管理方式可能会有所不同,这也增加了堆内存管理的复杂性。例如,在多线程环境下,还需要考虑线程安全问题,以防止多个线程同时访问和修改堆中的同一个内存块导致的数据不一致或错误。


2.4、栈区(Stack)

定义与功能:

栈是一种用于存储程序执行过程中临时数据的内存区域。它主要用于函数调用和局部变量的存储。当一个函数被调用时,函数的参数、局部变量和返回地址等信息会被压入栈中。栈的操作遵循 “后进先出”(LIFO)原则,即最后进入栈的元素最先被弹出。

存储内容细节:

栈存储的数据包括函数的参数值、局部变量的值以及函数调用的返回地址等。对于局部变量,其存储方式和变量的类型有关。栈中的数据存储是连续的,每个函数调用都会在栈上分配一个连续的内存区域,称为栈帧。栈帧的大小在编译时可以大致确定,主要取决于函数内部局部变量的数量和大小以及函数调用的嵌套深度。

分配时机与生命周期:

栈内存是在程序运行时自动分配和释放的。当一个函数被调用时,系统会自动为这个函数的栈帧分配内存,将函数的参数和局部变量等信息压入栈中。当函数执行结束后,系统会自动弹出这个函数的栈帧,释放相应的内存。因此,局部变量的生命周期是从函数被调用开始,到函数结束结束。这种自动管理的方式使得栈的使用相对简单和高效,但也意味着栈的大小是有限的,如果函数调用的嵌套太深或者局部变量占用的空间过大,可能会导致栈溢出。

效率与局限性:

栈的操作非常高效,因为它的内存分配和释放是由系统自动完成的,而且栈的存储结构简单,数据的访问和操作速度很快。然而,栈的大小是有限的,通常由操作系统或编译器预先设定。如果程序在栈上分配了过多的内存(如递归函数调用过深),就可能会导致栈溢出,这是一种常见的程序错误,会导致程序崩溃或出现未定义行为。


2.5、堆和栈向上向下增长问题

堆和栈的“向上增长”或“向下增长”描述的是 内存地址 的变化方向,即在程序运行过程中,当内存分配或释放时,内存地址是增大还是减小。

2.5.1、 栈的向下增长

特点:栈区的内存分配从高地址向低地址方向增长。 原因:这是由系统的内存布局决定的,大多数体系结构(如 x86、x64)设计栈从高地址向低地址增长,以便与堆的向上增长避免冲突。 每次函数调用时,局部变量、返回地址等会被压入栈中,占用新的内存地址; 新分配的栈内存的地址比之前的内存地址更低。

代码语言:javascript
代码运行次数:0
复制
高地址
 |         函数1的局部变量(最新)
 |         函数2的局部变量
 |         返回地址
 |         ...
 ↓ 栈向下增长
低地址
2.5.2、堆的向上增长

特点:堆区的内存分配从低地址向高地址方向增长。 原因:堆是动态分配的内存区域,通常从预留的低地址开始分配,随着分配的内存增加,新的内存地址会变大。 每次调用 malloc 或 calloc 时,堆区分配的内存地址比之前的更高; 堆的增长通常会接近栈的方向,但两者有明确的边界,防止冲突。

代码语言:javascript
代码运行次数:0
复制
高地址
 ↑ 堆向上增长
 |         堆分配的第3块内存(最新)
 |         堆分配的第2块内存
 |         堆分配的第1块内存
低地址

这样设计的原因:

防止冲突:堆从低地址向高地址增长,栈从高地址向低地址增长,两者方向相反,便于动态扩展,最大化内存利用率。 内存管理简单:这种布局使操作系统能更方便地检测栈溢出(当栈与堆碰撞时)或堆内存不足。


2.6、命令行参数区

定义与功能:

当程序从命令行启动时,命令行中输入的参数会存储在这个区域。这些参数可以被程序读取并用于控制程序的行为。例如,在一个命令行工具程序中,如果程序接受文件名作为参数,那么这个文件名就会存储在命令行参数区,程序可以读取这个文件名并打开相应的文件进行处理。

存储内容细节:

存储的是字符串形式的命令行参数,这些参数以数组的形式存储,通常第一个参数是程序本身的名称(在 C 语言中,可以通过argv[0]访问),后续的参数是用户在命令行输入的其他信息。这些字符串的长度和内容由用户在命令行输入决定。

分配时机与生命周期:

在程序从命令行启动时,操作系统会将命令行参数存储在这个区域,其生命周期从程序启动开始,到程序结束结束。在程序运行过程中,程序可以随时读取这些参数来获取用户的输入信息,以决定程序的后续操作。


2.7、内核区(供内核使用)

在典型的计算机内存布局图中,内核区并不在上述用户程序所涉及的内存区域(如代码区、数据区、堆区、栈区等)之中。它是操作系统内核所使用的内存区域,通常位于内存的较高地址部分,并且受到硬件和操作系统的严格保护。 对于用户程序来说,这个区域是不可直接访问的。这是为了确保操作系统的稳定性和安全性,防止用户程序的错误操作或恶意行为干扰内核的正常运行。

内核区的功能和内容

进程管理相关数据:内核需要维护系统中所有进程的信息,包括进程控制块(PCB)。PCB 包含了进程的状态(如运行、就绪、阻塞等)、进程 ID、优先级、程序计数器、寄存器内容等信息。这些信息用于进程的调度、切换和管理。例如,当系统进行进程切换时,内核会从内核区存储的 PCB 中获取相关进程的状态信息,以确保正确地恢复进程的执行环境。

内存管理数据结构:

内核负责管理整个系统的物理内存,包括内存的分配和回收。在内核区会有内存分配的数据结构,如页表。页表用于将虚拟内存地址转换为物理内存地址,它记录了虚拟地址空间和物理地址空间的映射关系。例如,当用户程序访问内存时,CPU 通过查询内核维护的页表来确定实际访问的物理内存位置。

关于内核区的内容和作用,现阶段了解即可,在学过操作系统中有关进程的创建、调度和切换之后,大家会对内核区有更深刻的理解!

内存区域

存储内容

分配时机

生命周期

管理方式

访问权限

典型应用场景

代码区

程序机器指令、只读数据(如常量字符串)

编译时

程序整个运行期间

由编译器分配,只读且固定

只读

存储程序可执行代码及常量数据

数据区(全局初始化数据区)

已初始化的全局变量和静态变量、字符串常量

编译时

程序整个运行期间

编译时确定,程序加载时初始化

可读可写

保存具有全局作用域且已初始化的数据

数据区(全局未初始化数据区)

未初始化的全局变量和静态变量

编译时确定信息,程序加载时分配并初始化

程序整个运行期间

编译时确定,程序加载时初始化

可读可写

存放全局未初始化数据,启动时初始化为默认值

堆区

动态分配的内存数据(如动态数组、动态数据结构等)

运行时

从分配开始,到free释放结束

程序员手动通过malloc等函数分配和free释放

可读可写

用于动态内存分配,如大型数据结构动态构建

栈区

函数参数、局部变量、返回地址等

函数调用时

函数调用开始到结束

系统自动分配和释放

可读可写

函数执行过程中的临时数据存储

内核区

进程管理数据(如进程控制块)、内存管理数据结构(如页表)、设备驱动程序、中断处理程序、系统调用处理程序

系统启动时内核加载阶段

系统运行期间一直存在

由操作系统内核自身的内存管理机制管理,对用户程序严格限制访问

受保护,用户程序一般无法访问,内核内部为可读可写

管理系统进程、内存、设备等核心功能,处理系统调用和中断等事件


3、内存管理函数

3.1、malloc

头文件:

代码语言:javascript
代码运行次数:0
复制
#include<stdlib.h>

函数原型:

代码语言:javascript
代码运行次数:0
复制
void* malloc (size_t size);

参数和返回值:

函数的参数size是一个size_t类型的值,size_t是一种无符号整数类型,用于表示内存块的大小,单位是字节。 返回值是一个void *类型的指针,如果分配成功,返回指向分配的内存块的起始地址的指针;如果分配失败(例如没有足够的内存可用),则返回NULL。

类型转换问题:

在 C 语言中,malloc函数返回的是一个void *类型的指针。在实际使用时,通常需要将其转换为具体的数据类型指针。正确的类型转换是很重要的。例如,在分配一个int类型的内存空间时,应该将返回的指针转换为int *类型,如int *ptr = (int *)malloc(sizeof(int));。如果不进行正确的类型转换,可能会导致编译器发出警告或者程序出现错误。

功能概述:

malloc函数的主要功能是在堆(heap)上分配一块指定大小的连续内存空间。堆是一个由程序员手动管理的内存区域,用于动态内存分配。例如,如果你想在程序中创建一个动态数组来存储整数,并且在运行时才能确定数组的大小n,就可以使用malloc来分配内存。

示例:

代码语言:javascript
代码运行次数:0
复制
int main()
{
	//将返回值强转为匹配的类型int*
	int* ptr = (int*)malloc(sizeof(int) * 10);
	//检查返回值是否为空,如果不为空说明malloc开辟成功
	if (ptr == NULL)
	{
		perror("malloc failed");
		exit(1);
	}
	return 0;
}

内存释放问题:

这段代码中,我们使用malloc成功在堆上申请开辟了40个字节的空间,并将这块空间的起始地址赋值给ptr,但是我们动态申请的堆空间在程序退出后并没有释放,会造成内存泄露的问题,释放动态申请的内存需要使用free函数!

3.2、free

头文件:

代码语言:javascript
代码运行次数:0
复制
#include<stdlib.h>

函数原型:

代码语言:javascript
代码运行次数:0
复制
void free (void* ptr);

参数和返回值:

函数的参数ptr是一个指向之前通过malloc、calloc或者realloc等函数分配的内存的起始地址的指针。这个指针必须是有效的,即它必须是之前成功分配内存后返回的指针,否则可能会导致程序出现错误。如果ptr是NULL,则函数什么都不会做。free函数无返回值!

功能概述:

free函数的主要功能是释放之前通过动态内存分配函数(如malloc)在堆(heap)上分配的内存空间。当程序不再需要使用这些动态分配的内存时,就应该使用free函数将其释放,以便系统能够重新利用这些内存

示例:

代码语言:javascript
代码运行次数:0
复制
int main() {
	int* ptr = (int*)malloc(sizeof(int) * 10);
	if (ptr == NULL) {
		perror("malloc failed!");
		exit(1);
	}
	for (int i = 0; i < 10; ++i)
		ptr[i] = i;
	free(ptr); //释放动态申请的内存
	return 0;
}

悬空指针问题:

在释放内存后,ptr指针本身的值不会改变,但是它所指向的内存已经被释放了。如果在释放内存后继续访问ptr所指向的内存,就会导致悬空指针问题。悬空指针是指指针所指向的内存已经不存在或者不可访问,但指针仍然存在并且可能被误用来访问已经释放的内存。这可能会导致程序出现未定义行为,如程序崩溃、数据损坏等。所以在释放完动态申请的内存后,我们要手动的将指针置为NULL!,程序访问NULL指针就会强制报错!

例如:

代码语言:javascript
代码运行次数:0
复制
int main() {
	int* ptr = (int*)malloc(sizeof(int) * 10);
	if (ptr == NULL) {
		perror("malloc failed!");
		exit(1);
	}
	free(ptr); //释放动态申请的内存 
	//*ptr = 10;  此时ptr是悬空指针,不能访问已经释放的内存,会导致未定义的错误
    ptr=NULL;//手动置为NULL,防止出现错误
	return 0;
}

3.3、calloc

头文件:

代码语言:javascript
代码运行次数:0
复制
#include<stdlib.h>

函数原型:

代码语言:javascript
代码运行次数:0
复制
void* calloc (size_t num, size_t size);

参数和返回值:

函数有两个参数,num表示元素的数量,size表示每个元素的大小。 返回值是一个void *类型的指针,如果分配成功,返回的指向分配的内存空间的起始地址的指针;如果分配失败(例如内存不足),则返回NULL。

功能概述:

calloc函数主要用于在堆上分配内存空间,它和malloc函数类似,但有一个重要的区别:calloc在分配内存后会自动将内存空间初始化为 0。这在很多情况下非常有用,例如当需要创建一个数组并且希望所有元素初始值为 0 时,calloc就可以很好地完成这个任务。

示例:

代码语言:javascript
代码运行次数:0
复制
int main() {
    int* ptr = (int*)calloc(5, sizeof(int));
    if (ptr == NULL) {
        printf("calloc failed");
        exit(1);
    }// 此时ptr所指向的内存空间已经全被calloc初始化为0
    free(ptr);
    ptr=NULL;
    return 0;
}

调试窗口观察:

malloc和calloc对比:

比较项目

malloc

calloc

函数原型

void *malloc(size_t size);

void *calloc(size_t num, size_t size);

参数含义

size:需要分配的字节数

num:元素个数;size:每个元素的大小

内存初始化

分配的内存未初始化

分配的内存会被初始化为 0

性能差异

由于不需要初始化内存,分配速度可能稍快

因为要初始化内存为 0,可能稍慢,但在大多数情况下差异不明显


3.4、realloc

动态申请的内存也会出现大小不匹配的问题,可能随着程序的运行,我们之前开辟的空间已经不够了,需要对原来的空间扩容,也有可能我们开辟的空间过大了,造成了内存浪费,这时我们需要对开辟的空间进行动态调整,realloc就是我们用来动态调整开辟空间大小的函数

头文件:

代码语言:javascript
代码运行次数:0
复制
#include<stdlib.h>

函数原型:

代码语言:javascript
代码运行次数:0
复制
void* realloc (void* ptr, size_t size);

参数和返回值:

ptr:是一个指向之前通过malloc、calloc或者realloc函数分配的内存块的指针。这个指针用于定位要重新分配大小的原始内存块。 size:是重新分配后内存块的新大小,单位是字节。 返回值是一个void *类型的指针,如果重新分配成功,返回的指针指向重新分配后的内存块的起始地址;如果分配失败,则返回NULL,并且原始的内存块不会被释放(除非返回值为NULL且原始内存块无法保留,这种情况很少见)。realloc在使用也要将返回值转换成对应的指针类型!

功能概述:

realloc函数主要用于动态地改变已经分配的内存块的大小。realloc会根据参数大小动态调整开辟的空间,并且将原来内存空间中的数据转移到新开辟的空间中,realloc函数的出现让动态内存管理更加灵活

realloc扩容的两种情况:

3.4.1、原地扩容

当使用realloc函数进行内存重新分配时,如果在原始内存块的末尾有足够的连续空闲空间来满足新的大小要求,那么realloc会在原地扩展内存块的大小。这意味着它不会移动原始内存块中的数据,只是调整内存块的边界,使其能够容纳更多的数据。这种情况是比较理想的,因为不需要进行数据的复制,效率相对较高。

3.4.2、异地扩容

原始内存块的末尾没有足够的连续空闲空间来满足新的大小要求时,realloc会在内存的其他位置寻找足够大小的连续空闲空间来重新分配内存。在这种情况下,realloc会将原始内存块中的数据复制到新的内存块中,然后释放原始的内存块。新的内存块的地址与原始内存块的地址不同。

3.4.3、 realloc的返回值

基于realloc的扩容机制,realloc在使用时要特别注意返回值问题,不能直接将返回值赋值给ptr,如果扩容失败返回值NULL会将ptr覆盖,不仅会导致原有开辟内存空间丢失,还会导致动态开辟的内存无法释放,造成内存泄漏

对realloc的返回值进行判断,如果不为NULL,说明扩容成功,此时可以赋值给ptr

代码语言:javascript
代码运行次数:0
复制
int main() {
	int* ptr = (int*)malloc(sizeof(int) * 20);
	if (ptr == NULL) {
		perror("malloc failed!\n");
		exit(1);
	}//先将返回值赋值给临时指针tmp
	int* tmp = (int*)realloc(ptr, sizeof(int*) * 30);
	if (tmp == NULL) { //如果为空,说明扩容失败,打印错误并退出
		perror("realloc failed!\n");
		exit(1);
	}//如果不为空,将tmp赋值给ptr并且释放tmp
	ptr = tmp;
	free(tmp);
	//业务处理...
	free(ptr); //最后释放ptr
    ptr=NULL;
	return 0;
}

3.4.4、realloc的特殊情况

当用realloc重新调整动态申请的内存大小为0时,会发生什么行为?

标准规定下的行为,根据 C 标准,当使用realloc函数将内存大小重新分配为 0 时,其行为等同于使用free函数释放内存。也就是说,realloc会释放掉原来分配的内存块,并且返回NULL。这是一种合理的设计,因为大小为 0 的内存块没有实际的存储用途。

代码语言:javascript
代码运行次数:0
复制
int main()
{
	int* ptr = (int*)malloc(sizeof(int) * 5);
	if (ptr == NULL) {
		perror("malloc failed!\n");
		exit(1);
	}
	ptr=realloc(ptr, 0); //相当于free(ptr);并返回NULL赋值给ptr,避免出现悬空指针
	return 0;
}

当我们用realloc调整ptr大小为0时,相当于free(ptr),并返回NULL,此时我们将返回值重新赋值给ptr,就相当于我们手动给ptr置为NULL,防止出现悬空指针的问题!


4、动态内存管理的常见错误

4.1、动态开辟内存的越界访问

代码语言:javascript
代码运行次数:0
复制
int main()
{
	int* ptr = (int*)calloc(sizeof(int), 10);
	if (ptr == NULL) {
		perror("calloc failed!\n");
		exit(1);
	}
	for (int i = 0; i <= 10; ++i) {
		printf("%d, ", ptr[i]);//当i=10时发生越界访问,可能会导致程序出错
	}
    printf("\n");
    free(ptr);
    ptr=NULL;
	return 0;
}

程序运行结果:(越界访问

代码语言:javascript
代码运行次数:0
复制
[zwy@iZhmy2hep8rb16Z code01]$ gcc test.c -o test -std=c99
[zwy@iZhmy2hep8rb16Z code01]$ ./test
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 135121, 

4.2、对非动态开辟的内存使用free

代码语言:javascript
代码运行次数:0
复制
int main()
{
	int arr[5] = { 1,2,3,4,5 };
	int* ptr = arr; //ptr不是动态开辟的内存,不能free
	free(ptr);   
	return 0;
}

对不是malloc,calloc,realloc等函数动态开辟的内存使用free释放,会导致程序崩溃!

程序崩溃:(发生段错误

代码语言:javascript
代码运行次数:0
复制
[zwy@iZhmy2hep8rb16Z code01]$ gcc test.c -o test -std=c99
[zwy@iZhmy2hep8rb16Z code01]$ ./test
Segmentation fault

4.3、使用free释放动态开辟内存的一部分

代码语言:javascript
代码运行次数:0
复制
int main()
{
	int* ptr = (int*)malloc(sizeof(int) * 10);
	if (ptr == NULL) {
		perror("malloc failed!\n");
		exit(1);
	}
	for (int i = 0; i < 5; ++i) {
		ptr++;
	}//此时ptr已经不指向动态开辟内存的起始空间
	free(ptr);//只释放了动态开辟内存的一部分
    ptr=NULL;
	return 0;
}

程序崩溃:

代码语言:javascript
代码运行次数:0
复制
[zwy@iZhmy2hep8rb16Z code01]$ gcc test.c -o test -std=c99
[zwy@iZhmy2hep8rb16Z code01]$ ./test
*** Error in `./test': free(): invalid pointer: 0x00000000008cd024 ***
======= Backtrace: =========
/lib64/libc.so.6(+0x81329)[0x7f510f0f9329]
./test[0x400662]
/lib64/libc.so.6(__libc_start_main+0xf5)[0x7f510f09a555]
./test[0x400549]
======= Memory map: ========
00400000-00401000 r-xp 00000000 fd:01 1320389                            /home/zwy/Test/TestCpp/code01/test
00600000-00601000 r--p 00000000 fd:01 1320389                            /home/zwy/Test/TestCpp/code01/test
00601000-00602000 rw-p 00001000 fd:01 1320389                            /home/zwy/Test/TestCpp/code01/test
008cd000-008ee000 rw-p 00000000 00:00 0                                  [heap]
7f5108000000-7f5108021000 rw-p 00000000 00:00 0 
7f5108021000-7f510c000000 ---p 00000000 00:00 0 
7f510ee62000-7f510ee77000 r-xp 00000000 fd:01 281031                     /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7f510ee77000-7f510f076000 ---p 00015000 fd:01 281031                     /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7f510f076000-7f510f077000 r--p 00014000 fd:01 281031                     /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7f510f077000-7f510f078000 rw-p 00015000 fd:01 281031                     /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7f510f078000-7f510f23c000 r-xp 00000000 fd:01 265442                     /usr/lib64/libc-2.17.so
7f510f23c000-7f510f43b000 ---p 001c4000 fd:01 265442                     /usr/lib64/libc-2.17.so
7f510f43b000-7f510f43f000 r--p 001c3000 fd:01 265442                     /usr/lib64/libc-2.17.so
7f510f43f000-7f510f441000 rw-p 001c7000 fd:01 265442                     /usr/lib64/libc-2.17.so
7f510f441000-7f510f446000 rw-p 00000000 00:00 0 
7f510f446000-7f510f468000 r-xp 00000000 fd:01 281343                     /usr/lib64/ld-2.17.so
7f510f65b000-7f510f65e000 rw-p 00000000 00:00 0 
7f510f665000-7f510f667000 rw-p 00000000 00:00 0 
7f510f667000-7f510f668000 r--p 00021000 fd:01 281343                     /usr/lib64/ld-2.17.so
7f510f668000-7f510f669000 rw-p 00022000 fd:01 281343                     /usr/lib64/ld-2.17.so
7f510f669000-7f510f66a000 rw-p 00000000 00:00 0 
7fff6c9c6000-7fff6c9e7000 rw-p 00000000 00:00 0                          [stack]
7fff6c9e8000-7fff6c9ea000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
Aborted

free 函数要求传入的指针必须是指向最初通过 malloc、calloc 或 realloc 等动态内存分配函数所分配内存块的起始地址。而在上述代码中,经过指针移动后,free(ptr) 传递的是已经移动过位置的指针,它试图释放的并非是完整的、最初分配的那片内存,而只是从当前指针位置往后的那部分内存,会导致程序崩溃!


4.4、对动态开辟的内存多次free

代码语言:javascript
代码运行次数:0
复制
int main()
{
	int* ptr = (int*)malloc(sizeof(int) * 10);
	if (ptr == NULL) {
		perror("malloc failed!\n");
		exit(1);
	}
	free(ptr);
	free(ptr);//重复free导致程序崩溃
	ptr = NULL;
	return 0;
}

程序崩溃:

代码语言:javascript
代码运行次数:0
复制
[zwy@iZhmy2hep8rb16Z code01]$ gcc test.c -o test -std=c99
[zwy@iZhmy2hep8rb16Z code01]$ ./test
*** Error in `./test': double free or corruption (fasttop): 0x0000000001027010 ***
======= Backtrace: =========
/lib64/libc.so.6(+0x81329)[0x7fca0d5c5329]
./test[0x400656]
/lib64/libc.so.6(__libc_start_main+0xf5)[0x7fca0d566555]
./test[0x400549]
======= Memory map: ========
00400000-00401000 r-xp 00000000 fd:01 1320389                            /home/zwy/Test/TestCpp/code01/test
00600000-00601000 r--p 00000000 fd:01 1320389                            /home/zwy/Test/TestCpp/code01/test
00601000-00602000 rw-p 00001000 fd:01 1320389                            /home/zwy/Test/TestCpp/code01/test
01027000-01048000 rw-p 00000000 00:00 0                                  [heap]
7fca08000000-7fca08021000 rw-p 00000000 00:00 0 
7fca08021000-7fca0c000000 ---p 00000000 00:00 0 
7fca0d32e000-7fca0d343000 r-xp 00000000 fd:01 281031                     /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7fca0d343000-7fca0d542000 ---p 00015000 fd:01 281031                     /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7fca0d542000-7fca0d543000 r--p 00014000 fd:01 281031                     /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7fca0d543000-7fca0d544000 rw-p 00015000 fd:01 281031                     /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7fca0d544000-7fca0d708000 r-xp 00000000 fd:01 265442                     /usr/lib64/libc-2.17.so
7fca0d708000-7fca0d907000 ---p 001c4000 fd:01 265442                     /usr/lib64/libc-2.17.so
7fca0d907000-7fca0d90b000 r--p 001c3000 fd:01 265442                     /usr/lib64/libc-2.17.so
7fca0d90b000-7fca0d90d000 rw-p 001c7000 fd:01 265442                     /usr/lib64/libc-2.17.so
7fca0d90d000-7fca0d912000 rw-p 00000000 00:00 0 
7fca0d912000-7fca0d934000 r-xp 00000000 fd:01 281343                     /usr/lib64/ld-2.17.so
7fca0db27000-7fca0db2a000 rw-p 00000000 00:00 0 
7fca0db31000-7fca0db33000 rw-p 00000000 00:00 0 
7fca0db33000-7fca0db34000 r--p 00021000 fd:01 281343                     /usr/lib64/ld-2.17.so
7fca0db34000-7fca0db35000 rw-p 00022000 fd:01 281343                     /usr/lib64/ld-2.17.so
7fca0db35000-7fca0db36000 rw-p 00000000 00:00 0 
7ffc0b5f0000-7ffc0b611000 rw-p 00000000 00:00 0                          [stack]
7ffc0b72d000-7ffc0b72f000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
Aborted

在实际开发中,我们可能会对动态开辟的内存使用free释放后,由于某种原因再次使用free释放, 对同一块动态开辟的内存多次free,会导致程序崩溃


4.5、对动态开辟的内存没有free(内存泄漏)

代码语言:javascript
代码运行次数:0
复制
void Test(){
	int* ptr = (int*)malloc(sizeof(int) * (INT_MAX/10));
	if (ptr == NULL) {
		perror("malloc failed!\n");
	}
}
int main() {
	Test();
	//可能程序运行时间很长,泄露的内存一直无法释放
	return 0;
}

如上代码我们malloc开辟了很大一块内存没有释放,造成了内存泄漏,但是你可能会觉得对我们的程序没有很大影响,那是因为我们的程序很简单,CPU在极短的时间内就能执行完毕,当main()函数调用完,程序结束时,泄露的内存会由操作系统释放。

但是对于长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务器、长时间运行的客户端等等,不断出现内存泄漏会导致可用内存不断变少,各种功能响应越来越慢,最终卡死,导致程序崩溃,带来不可估量的损失。

如果内存泄漏持续发展,最终会耗尽系统的所有可用内存。当系统没有足够的内存来支持新的程序启动或者现有程序的运行时,可能会导致系统崩溃。这种情况在一些关键系统(如服务器、嵌入式系统等)中是非常严重的,可能会导致数据丢失、服务中断等后果!

切记!我们要养成主动释放内存的习惯,当动态开辟的内存使用完毕时,要及时并且正确释放,避免出现内存泄漏造成危害 !


5、柔性数组

柔性数组(Flexible Array Member, FAM)是一种 C99中结构体的特殊特性,允许结构体的最后一个成员被声明为大小未知的数组。这种数组可以在运行时灵活地分配所需的空间,但它本身不占用固定内存空间。

5.1、柔性数组的定义

代码语言:javascript
代码运行次数:0
复制
struct Example {
    int value;         // 一个普通成员
    char data[];      // 柔性数组成员
};
代码语言:javascript
代码运行次数:0
复制
struct Example {
	int value; //普通成员
	const char* str; //普通成员
	double d[]; //柔性数组成员
};

5.2、柔性数组的特点

1、C99标准规定,柔性数组成员必须是结构体中最后一个成员,否则编译器会报错!

错误示例:

代码语言:javascript
代码运行次数:0
复制
struct Example {
	char data[];//柔性数组成员
	int value;//错误的写法,柔性数组成员必须放在最后
};

在gcc上使用C99标准编译时报错信息:

代码语言:javascript
代码运行次数:0
复制
gcc test.c -o test -std=c99
test.c:14:7: error: flexible array member not at end of struct
  char data[];//柔性数组成员
       ^

原因:

柔性数组的内存布局冲突: 柔性数组的目的是允许你在结构体末尾动态分配额外内存来扩展 data 的大小。如果在柔性数组之后还有其他成员,那么这些成员会被柔性数组的动态扩展覆盖,导致内存冲突和不可预测的行为。 动态扩展难以实现: 编译器无法确定 data[] 的实际大小,也无法在结构体中正确定位 value 的位置,导致无法计算结构体的布局和对齐。


2、数组大小未定义,柔性数组的大小在定义时是空的([]),它的实际大小在运行时通过动态内存分配确定

代码语言:javascript
代码运行次数:0
复制
struct Example {
	int value;
	char data[];//未定义大小
};

3、柔性数组成员不占用结构体的固定大小,sizeof的返回结果不包含柔性数组的大小

代码语言:javascript
代码运行次数:0
复制
struct Example {
    int value;
    char data[];
};
int main()
{
    //sizeof结构体时,只计算value的大小
    int size = sizeof(struct Example);
    printf("%d\n", size);
}

4、不支持直接赋值或初始化,柔性数组无法通过直接赋值或初始化语句设置内容

错误示例:

代码语言:javascript
代码运行次数:0
复制
struct Example {
    int value;
    char data[];
};
int main()
{
    // 错误,柔性数组不能直接初始化
    struct Example example = { 10, "Hello" };  
    return 0;
}

在gcc上使用C99标准编译时报错信息:

代码语言:javascript
代码运行次数:0
复制
gcc test.c -o test -std=c99
test.c: In function ‘main’:
test.c:25:12: error: non-static initialization of a flexible array member
     struct Example example = { 10, "Hello" };  
            ^
test.c:25:12: error: (near initialization for ‘example’)

5.3、柔性数组的使用

柔性数组的使用主要用于实现 动态大小的数据结构,通过在结构体末尾定义一个大小未知的数组(柔性数组),实现高效的内存管理和灵活的数据存储。

代码语言:javascript
代码运行次数:0
复制
struct Example {
    int value;
    char data[];
};
typedef struct Example Example;

动态分配内存:

由于柔性数组没有固定大小,通常需要使用 malloc 或 calloc 动态分配内存,分配的总大小需要包含结构体的固定部分和柔性数组部分。

代码语言:javascript
代码运行次数:0
复制
 //柔性数组分配元素的数量
 int size = 10; 
 //动态分配的内存需要包括结构体的固定部分大小加上柔性数组大小
 Example* example = (Example*)malloc(sizeof(Example) + sizeof(char) * size);
 if (example == NULL) {
     perror("malloc failed!\n");
     exit(1);
 }

初始化:

使用结构体指针->分别对普通成员和柔性数组数组成员初始化,要注意柔性数组的类型和初始化类型要匹配!

代码语言:javascript
代码运行次数:0
复制
    example->value = 10;//初始化普通成员
    strcpy(example->data, "HelloWorld"); // 向柔性数组写入数据

使用:

代码语言:javascript
代码运行次数:0
复制
 printf("value->%d\n", example->value);
 printf("data[]->%s\n", example->data);

5.4、指针模拟柔性数组

定义结构体:

代码语言:javascript
代码运行次数:0
复制
struct Example {
    int value;    
    char* data;   // 使用指针替代柔性数组
};

为结构体动态分配内存:

代码语言:javascript
代码运行次数:0
复制
int main() {
    Example* example = (Example*)malloc(sizeof(Example));
    if (example == NULL) {
        perror("malloc failed!\n");
        exit(1);
    }
}

这里只需要malloc开辟结构体的大小就可以,因为指针的大小是固定的,32位平台下4个字节,64位平台下八个字节!

为数组动态分配内存:

代码语言:javascript
代码运行次数:0
复制
 int size = 10;//为数组开辟的元素个数
 example->data = (char*)malloc(sizeof(char) * size);
 if (example->data == NULL) {
     perror("malloc failed!\n");
     exit(1);
 }

初始化和使用:

代码语言:javascript
代码运行次数:0
复制
example->value = 100;
strcpy(example->data, "HelloWorld");
printf("%d\n", example->value);
printf("%s\n", example->data);

修改结构体内容:

使用指针可以直接修改结构体中数组的内容

代码语言:javascript
代码运行次数:0
复制
strcpy(example->data, "Dynamic");
printf("%s\n", example->data);

5.5、柔性数组的优势

1. 动态分配,节省内存 柔性数组允许运行时按需分配内存,仅占用实际需要的空间,避免了普通数组固定大小可能造成的内存浪费,非常适合处理变长数据。 2. 连续存储,访问高效 柔性数组的数据与结构体紧密存储在一起,形成连续的内存布局,充分利用缓存的局部性,减少指针操作带来的间接访问开销。减少内存碎片 3.避免冗余指针操作 内存访问更直接:柔性数组无需指针操作,数组内容直接存储在结构体内。 内存分配更加安全:无需为额外指针单独分配内存,减少了内存分配失败或泄漏的风险。

特性

柔性数组

普通数组

指针模拟实现

大小

动态分配,运行时确定

编译时固定

动态分配,运行时确定

内存布局

数据与结构体连续存储

数据存储在固定区域

数据与结构体分开存储

内存效率

高效,节省空间

固定大小,可能浪费多余空间

高效,但需管理多个内存块

访问效率

连续存储,访问性能高

固定存储,性能高

不连续存储,需间接访问

实现复杂性

简洁,统一管理结构体和数据

简单,大小固定

较复杂,需维护指针关系

内存管理

一次性分配和释放

无需管理

需分别分配和释放


5.6、总结以及注意事项

1. 定义与规则

最后一个成员:柔性数组必须是结构体的最后一个成员,其他成员不能出现在其后。 大小不计入 sizeof:sizeof 仅计算结构体中固定成员的大小,柔性数组的大小需动态分配后单独管理。

2. 内存管理

动态分配:必须通过 malloc 或 calloc 为柔性数组分配内存,并包括结构体本身的大小和柔性数组所需的额外空间。 手动释放:使用 free 时,释放的是整个结构体的起始地址,柔性数组的内存会一并释放,防止内存泄漏。 越界检查:必须确保操作时不超出分配的内存范围,否则会导致未定义行为。

3. 使用限制

不能直接初始化:柔性数组不能通过初始化列表进行赋值,需在动态分配内存后手动填充数据。 无法直接复制:结构体包含柔性数组时,不能使用 = 或 memcpy 进行整体赋值或拷贝,需手动处理柔性数组内容。 不支持静态分配:柔性数组的大小仅在运行时确定,不能为结构体变量直接分配固定大小的内存。

4. 数据对齐

内存对齐问题:柔性数组可能受结构体的对齐规则影响,在动态分配内存时需考虑字节对齐要求,确保性能和正确性。

5. 编译器支持

C99 引入特性:柔性数组是 C99 标准的一部分,需确保使用的编译器支持 C99 或更高版本。如果环境不支持柔性数组,可使用指针替代实现。

6. 适用场景

高效管理变长数据:如动态字符串、动态数组、网络包数据、文件数据块等。 提升内存利用率:按需分配所需的大小,避免固定大小数组带来的内存浪费。 简化代码逻辑:柔性数组与结构体统一存储管理,无需额外维护指针或其他动态结构


分类

注意事项

定义规则

- 必须是结构体的最后一个成员,不能有其他成员在它之后。

- 不计入 sizeof,需要动态分配额外内存来管理柔性数组的大小。

内存管理

- 使用 malloc 或 calloc 为结构体和柔性数组一起分配内存。

- 用 free 释放结构体地址时,柔性数组所占内存一并释放。

- 确保访问柔性数组时不越界,防止未定义行为。

使用限制

- 不能通过初始化列表直接初始化柔性数组,需动态分配后手动赋值。

- 无法直接使用 = 或 memcpy 进行结构体赋值或拷贝。

- 只能动态分配,无法静态定义其大小。

数据对齐

- 动态分配时需注意对齐规则,确保数据访问的正确性和性能优化。

兼容性

- 柔性数组是 C99 标准特性,需支持 C99 或更高版本编译器。

- 对不支持柔性数组的环境,可用指针替代实现类似功能。

适用场景

- 变长数据存储,如动态字符串、网络数据包、文件数据块。

- 动态集合的实现,如队列、栈、变长数组。

- 简化代码管理,将固定和动态数据整合在一个结构体中。


C语言知识总结

C 语言动态内存管理至关重要。常用 malloc 和 calloc 分配内存,realloc 扩展或收缩内存时需关注原地与异地扩容并妥善处理返回值。使用完要用 free 释放内存以防泄漏,同时规避越界访问、多次释放等错误。柔性数组在结构体中,大小动态确定,位置排在最后,在动态数据场景下高效便捷,以此构建动态内存管理知识基本框架,助力在 C 语言探索之路上披荆斩棘,乘风破浪!

如上的讲解只是我的一些拙见,如有不足之处,还望各位大佬不吝在评论区予以斧正,感激不尽!创作不易,还请多多互三支持!你们的支持是我最大的动力!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-12-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、引入
  • 2、内存空间分布
    • 2.1、代码区(Text Segment)
    • 2.2、数据区(Data Segment)
      • 2.2.1、全局初始化数据区(.data)
      • 2.2.2、全局未初始化数据区(.bss)
    • 2.3、堆区(Heap)
    • 2.4、栈区(Stack)
    • 2.5、堆和栈向上向下增长问题
      • 2.5.1、 栈的向下增长
      • 2.5.2、堆的向上增长
    • 2.6、命令行参数区
    • 2.7、内核区(供内核使用)
  • 3、内存管理函数
    • 3.1、malloc
    • 3.2、free
    • 3.3、calloc
    • 3.4、realloc
      • 3.4.1、原地扩容
      • 3.4.2、异地扩容
      • 3.4.3、 realloc的返回值
      • 3.4.4、realloc的特殊情况
  • 4、动态内存管理的常见错误
    • 4.1、动态开辟内存的越界访问
    • 4.2、对非动态开辟的内存使用free
    • 4.3、使用free释放动态开辟内存的一部分
    • 4.4、对动态开辟的内存多次free
    • 4.5、对动态开辟的内存没有free(内存泄漏)
  • 5、柔性数组
    • 5.1、柔性数组的定义
    • 5.2、柔性数组的特点
    • 5.3、柔性数组的使用
    • 5.4、指针模拟柔性数组
    • 5.5、柔性数组的优势
    • 5.6、总结以及注意事项
  • C语言知识总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档