【C语言_动态内存管理详解】

    • 动态内存管理
      • 1.什么是动态内存
      • 2.动态内存分配函数
        • 2.1、malloc
        • 2.2、calloc
        • 2.3、realloc
      • 3.动态内存管理与结构体
        • 3.1 结构体也可以嵌套指向自身的指针。
        • 3.2 柔性数组
          • 3.2.1.定长包:
          • 3.2.2 指针数据包
          • 3.3.3. 变长数据缓冲区
      • 4、堆区与栈区的区别:

动态内存管理

本文的主要内容:

  1. 什么是动态内存
  2. 动态内存分配函数
  3. 动态内存管理与结构体
  4. 堆区与栈区的区别

1.什么是动态内存

示例:
【C语言_动态内存管理详解】_第1张图片
运行时程序崩溃。这是为什么呢?
分析编译链接过程

【C语言_动态内存管理详解】_第2张图片

栈区: 我们知道栈区在函数被调时分配,用于存放函数的参数值,局部变量等值。在 windows 中栈的默认大小是1M,在 vs 中可以设置栈区的大小。在 Liunx 中栈的默认大小是 10M,在 gcc 编译时可以设置栈区的大小。
堆区: 程序运行时可以在堆区动态地请求一定大小的内存,并在用完之后归还给堆区。在 Liunx 系统中堆区的大小接近 3G。windows 下的大小大家可以自己测一下。

一般情况下我们需要大块内存或程序在运行的过程中才知道所需内存大小,我们就从堆区分配空间。

2.动态内存分配函数

C 语言中动态内存管理的有四个函数:malloc,calloc,realloc,free,都需要引用 stdlib.h 或 malloc.h 文件。

2.1、malloc

malloc 向堆区申请一块指定大小的连续内存空间 。

void * malloc (size_t size); // typedef unsigned int size_t;

Allocate memory block //分配内存块

分配一个大小为size字节的内存块,返回指向该块开头的指针

新分配的内存块的内容未初始化,剩余值不确定。

如果大小为零,则返回值取决于特定的库实现(它可能是一个空指针),但返回的指针不应被取消引用。

参数

大小:内存块的大小,以字节为单位。size_t是一种无符号整数类型

返回值

成功时,指向函数分配的内存块的指针。
此指针的类型始终为void*,可以将其转换为所需的数据指针类型,以便取消引用。
如果函数未能分配请求的内存块,则返回空指针。

free

用来释放从 malloc , realloc, calloc 成功获取到的动态内存分配的空间。

void free (void* ptr); 

Deallocate memory block // 释放内存块

以前通过调用malloc、calloc或realloc分配的内存块被释放,使其再次可用于进一步分配。

如果ptr不指向与上述函数一起分配的内存块,则会导致未定义的行为

如果ptri是空指针,则函数不执行任何操作。

请注意,此函数不会更改ptritself的值,因此它仍然指向相同(现在无效)的位置。

参数

ptr:指向以前分配给malloc的内存块的指针,callocor realloc。

示例 1:
【C语言_动态内存管理详解】_第3张图片

2.2、calloc

void* calloc (size_t num, size_t size);

Allocate and zero-initialize array // 分配并使用零初始化连续内存空间

为num元素数组分配一块内存,每个元素的大小为字节长,并将其所有位初始化为零。

有效的结果是分配(num*size)字节的零初始化内存块。

如果大小为零,则返回值取决于特定的库实现(它可能是空指针,也可能不是空指针),但不应取消对返回指针的引用。

参数

num:要分配的元素数。

size : 每个元素的大小。

size_t是一种无符号整数类型。

返回值

成功时,指向函数分配的内存块的指针。

此指针的类型始终为void*,可以将其转换为所需的数据指针类型,以便取消引用。

如果函数未能分配请求的内存块,则返回空指针。

示例:
【C语言_动态内存管理详解】_第4张图片
使用 memset 函数:将内存单元置为 0 值。

我们使用 malloc 或 calloc 申请了堆空间内存,但是程序在运行的过程中发现申请空间少了或多了如何处理呢?

2.3、realloc

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

Reallocate memory block // 重新分配内存块

更改ptr指向的内存块的大小。

该函数可以将内存块移动到新位置(其地址由函数返回)。

即使将内存块移动到新位置,内存块的内容也会保留到新旧大小中较小的一个。如果新大小更大,则新分配部分的值是不确定的。

如果ptr是空指针,则函数的行为类似于malloc,分配一个新的大小字节块,并将指针返回到其开头。

否则,如果大小为零,则先前在ptr分配的内存将被释放,就像调用了free一样,并返回空指针。

如果函数未能分配请求的内存块,将返回一个空指针,并且参数ptr指向的内存块不会被释放(它仍然有效,且其内容不变)。

参数

Ptr:指向以前使用malloc、calloc或realloc分配的内存块的指针。或者,这可以是一个空指针,在这种情况下,会分配一个新块(就像调用了malloc一样)。

size: 内存块的新大小,以字节为单位。size_t是一种无符号整数类型。

返回值

指向重新分配的内存块的指针,它可以与ptr相同,也可以是新位置。此指针的类型为void*,可以将其转换为所需的数据指针类型,以便取消引用。

空指针表示大小为零(因此ptr被解除分配),或者函数未分配存储(因此ptr指向的块未被修改)。

示例:
【C语言_动态内存管理详解】_第5张图片

realloc 函数调整内存大小分为 3 种情况。

第一种情况: 后续未分配内存空间足够大,可以分配空间。图示如下:
【C语言_动态内存管理详解】_第6张图片
第二种情况: 后续未分配内存空间不足够大,不能分配空间。图示如下:

【C语言_动态内存管理详解】_第7张图片

第三种情况: 堆内存不足,扩展空间失败,realloc 函数返回 NULL。

示例代码:

#include
#include
int main()
{
	 int* p = NULL;
	 int* ip = (int*)malloc(sizeof(int) * 10);
	 if (NULL == ip) exit(EXIT_FAILURE);
	 // 处理程序
	ip = (int*)realloc(ip, sizeof(int) * 1000);
	// realloc 分配失败,返回 NULL ?
	if (NULL == ip) exit(EXIT_FAILURE);
	//正确处理方法
	p = (int*)realloc(ip, sizeof(int) * 1000);
	if (NULL == p)
	{
		// 扩充失败,
		free(ip);
		exit(EXIT_FAILURE);
	}
	else
	{
		ip = p;
		// 处理程序
	}
	free(ip);
	ip = NULL;
	return 0;
}

3.动态内存管理与结构体

结构体变量和内置类型都有局部 ,全局 ,动态生存期。
示例 1:

#include
#include
#include
struct Student
{
	char s_name[20]; //姓名
	int age; //年龄
	float score; //成绩
};

struct Student g_studa; //全局
struct Student g_studb = { "tulun",13,145.5 };
int main()
{
	 struct Student studa; // 局部未初始化
	 struct Student studb = { "tulun",12,140.5 };
	 struct Student* sp = (struct Student*)malloc(sizeof(struct Student)); //?
	
	 struct Student* sp2 = (struct Student*)malloc(sizeof(*sp2)); //?
	 struct Student* sp3 = (struct Student*)malloc(sizeof(sp3)); //?
	 strcpy(sp->s_name, "tulun");
	 sp->age = 11;
	 sp->score = 143.5;
	 free(sp);
	 free(sp2);
	 free(sp3);
	 return 0;
}

3.1 结构体也可以嵌套指向自身的指针。

示例:

struct Student
{
	char s_name[20]; //姓名
	int age; //年龄
	float score; //成绩
	struct Student* next; // 指向 Student 类型的指针变量属性;
};

我们可以一个个的动态申请 struct Student 类型的内存空间,然后用 next 指针把它们链接起来。

3.2 柔性数组

我们先看一个结构体的设计:

#define MAXLEN 1024
typedef struct kd_node
{
	struct kd_node* left;
	struct kd_node* right;
	int dim;
	unsigned long long data[MAXLEN]; // 数据
}kd_node;

在这段代码中,为了存储数据,申请了长度为 1024 的 unsigned long long 型数组。若是数据的长度远远小于MAXLEN,这样的设计,是及其浪费空间的。

C99 标准中给出了新的设计方法。通过柔性数组可以解决这个问题。

示例:

【C语言_动态内存管理详解】_第8张图片

柔性数组是一种数组大小待定的数组。

在 C 语言中,可以使用结构体产生柔性数组,结构体的最后一个元素可以是大小未知的数组。

在 struct sd_node 结构体中 data,仅仅是一个待使用的标识符,不占用存储空间。所以 sizeof(struct sd_data) = 8

  • 用途 : 长度为 0 的数组的主要用途是为了满足长度可变的结构体。
  • 用法 : 在一个结构体的最后, 声明一个长度为 0 的数组, 就可以使得这个结构体是可变长的。对于编译器来说, 此时长度为 0 的数组并不占用空间, 因为数组名本身不占空间, 它只是一个偏移量, 数组名这个符号本身代表了一个不可修改的地址常量。但对于这个数组的大小, 我们可以进行动态分配。
  • 注意 : 如果结构体是通过 calloc、malloc 或 realloc 等动态分配方式生成,在不需要时要释放相应的空间。
  • 优点 : 比起在结构体中声明一个指针变量、再进行动态分配的办法,这种方法效率要高。因为简单。
  • 缺点 : 在结构体中,数组为 0 的数组必须在最后声明,在设计结构体类型有一定限制。

对于编译器而言, 数组名仅仅是一个符号, 它不会占用任何空间, 它在结构体中, 只是代表了一个偏移量, 代表一个不可
修改的地址常量!

示例:
【C语言_动态内存管理详解】_第9张图片

假设我们在网络通信过程中使用的数据缓冲区, 缓冲区包括一个 num, size 和 data 字段, 分别标识数据的块号,长度和传输的数据, 我们设计思路如下:

  • 定长数据缓冲区, 设置一个足够大小 MAXSIZE 的数据缓冲区。
  • 设置一个指向实际数据的指针, 每次使用时, 按照数据的长度动态的开辟数据缓冲区的空间。

我们从实际场景中应用来考虑他们的优劣,主要考虑点有:缓冲区空间的开辟, 释放和访问.

3.2.1.定长包:
#define MAXSIZE 4096 // 4k
struct data_buffer
{
	int num;
	int size;
	char data[MAXSIZE];
};	// 结构体的大小==>sizeof(int)+sizeof(size)+sizeof(char)*MAXSIZE;

使用定长数组作为数据缓冲区,为了避免造成缓冲区溢出,数组设计是大开小用,而实际使用过程中,达到MAXSIZE 长度的数据很少,那么多数情况下,缓冲区的大部分空间都浪费了,也会造成不必要的流量浪费。但是使用过程很简单, 数据空间的开辟和释放简单, 无需程序员考虑额外的操作。

3.2.2 指针数据包

如果你将上面的长度为 MAXSIZE 的定长数组换为指针, 每次使用时动态的开辟 CURSIZE 大小的空间, 那么就避免造成 MAXSIZE - CURSIZE 空间的浪费, 只浪费了一个指针域的空间。

struct data_buffer
{
	 int num;
	 int size;
	 char* data;
	
}
//结构体大小= sizeof(num)+sizeof(size)+sizeof(char*)
//在分配和释放内存时,都必须采用两步。
struct data_buffer* pbuff = (struct data_buffer*)malloc(sizeof(struct data_buffer));
if (NULL == pbuff) exit(EXIT_FAILURE);
pbuff->size = CURSIZE;			// CURSIZE 假设是发送数据的长度。
pbuff->data = (char*)malloc(sizeof(char) * CURSIZE);
if (NULL == pbuff) exit(EXIT_FAILURE);
// 处理程序
free(pbuff->data);
free(pbuff);
pbuff = NULL;

使用指针结果作为缓冲区, 只多使用了一个指针大小的空间, 无需使用 MAXSIZE 长度的数组, 不会造成空间的大量浪费。但那是开辟空间时, 需要额外开辟数据域的空间, 施放时候也需要显示释放数据域的空间, 但是实际使用过程中, 往往在函数中开辟空间, 然后返回给使用者指向 struct point_buffer 的指针, 这时候我们并不能假定使用者了解我们开辟的细节, 并按照约定的操作释放空间, 因此使用起来多有不便, 甚至造成内存泄漏。

3.3.3. 变长数据缓冲区

定长数组使用方便,但是却浪费空间。 指针形式只多使用了一个指针的空间,不会造成大量空间分浪费,但是使用起来需要多次分配,多次释放,那么有没有一种实现方式既不浪费空间, 又使用方便的呢?

柔性数组:

struct data_buffer
{
	int num;
	int size;
	char data[];
};
//结构体大小 sizeof(struct data_buffer) = sizeof(num)+sizeof(size) = 8 字节
//使用的时候, 只需要开辟一次空间即可
int main()
{
	// n 是数据长度
	int n = strlen("tulun hello") + 1;
	struct data_buffer* pbuff = (struct data_buffer*)malloc(sizeof(struct data_buffer) + sizeof(char) * n);
	if (NULL == pbuff) exit(EXIT_FAILURE);
	pbuff->num = 1;
	pbuff->size = n;
	memcpy(pbuff->data, "tulun hello", n);
	// 处理程序
	free(pbuff);
	return 0;
}

4、堆区与栈区的区别:

  1. 管理方式:栈由系统自动管理;堆由程序员控制,使用方便,但易产生内存泄露。
  2. 生长方向:栈向低地址扩展(即”向下生长”),是连续的内存区域;堆向高地址扩展(即”向上生长”),是不连续的内存区域。这是由于堆区管理系统用链表来存储空闲内存地址,自然不连续,而链表从低地址向高地址遍历。
  3. 空间大小:栈顶地址和栈的最大容量由系统预先规定(通常默认 1M 或 10M);堆的大小则受限于计算机系统中有效的虚拟内存,32 位 Linux 系统中堆内存可达 2.9G 空间。
  4. 存储内容:栈在函数调用时,首先压入是函数实参,然后主调函数中下条指令(函数调用语句的下条可执行语句)的地址压入,最后是被调函数的局部变量。本次调用结束后,局部变量先出栈,指令地址出栈,最后栈平衡,程序由该点继续运行下条可执行语句。堆通常在头部用一个字节存放其大小,堆用于存储生存期与函数调用无关的数据,具体内容由程序员安排。
  5. 分配方式:栈可静态分配或动态分配。静态分配由编译器完成,如局部变量的分配。动态分配由 alloca 函数在栈上申请空间,用完后自动释放不需要调动 free 函数。堆只能动态分配且手工释放。
  6. 分配效率:栈由计算机底层提供支持:分配专门的寄存器存放栈地址,压栈出栈由专门的指令执行,因此效率较高。堆由函数库提供,机制复杂,效率比栈低得多。
  7. 分配后系统响应:只要栈剩余空间大于所申请空间,系统将为程序提供内存,否则报告异常提示栈溢出。操作系统为堆维护一个记录空闲内存地址的链表。当系统收到程序的内存分配申请时,会遍历该链表寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点空间分配给程序。若无足够大小的空间(可能由于内存碎片太多),有可能调用系统功能去增加程序数据段的内存间,以便有机会分到足够大小的内存,然后进行返回。大多数系统会在该内存空间首地址处记录本次分配的内存大小,供后续的释放函数(如free/delete)正确释放本内存空间。此外,由于找到的堆结点大小不一定正好等于申请的大小,系统会自动将多余的部分重新放入空闲链表中。
  8. 碎片问题:栈不会存在碎片问题,因为栈是先进后出的队列,内存块弹出栈之前,在其上面的后进的栈内容已弹出。而频繁申请释放操作会造成堆内存空间的不连续,从而造成大量碎片,使程序效率降低。

可见,堆容易造成内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和内核态切换,内存申请的代价更为昂贵。所以栈在程序中应用最广泛,函数调用也利用栈来完成,调用过程中的参数、返回地址、栈基指针和局部变量等都采用栈的方式存放。所以,建议尽量使用栈,仅在分配大量或大块内存空间时使用堆。

最后使用栈和堆时应避免越界发生,否则可能程序崩溃或破坏程序堆、栈结构,产生意想不到的后果。

你可能感兴趣的:(c语言,开发语言,后端)