导读
这一期将介绍动态内存分配相关的函数,(malloc、calloc、realloc、free),在介绍前,请大家先思考一个问题,为什么要存在动态内存分配?
为什么要存在动态内存分配
- 灵活性和效率:
- 静态内存分配(即在编译时分配的内存,如数组、静态变量等)在大小上是固定的,并且需要在编译时确定。然而,动态内存分配允许程序在运行时根据需要分配任意数量的内存,这使得程序更加灵活。
- 当处理大量数据或不确定数量的数据时,动态内存分配特别有用。例如,当你需要读取一个文件,但不知道文件的大小时,你可以使用动态内存分配来创建一个足够大的缓冲区来存储文件内容。
- 资源管理:
- 动态内存分配允许程序员更精细地控制内存的使用。通过动态分配和释放内存,程序员可以避免浪费(例如,只为需要的数据分配内存),并且可以更有效地管理内存资源。
- 在某些情况下,如嵌入式系统或资源受限的环境中,有效地管理内存使用是至关重要的。
- 数据结构:
- 动态内存分配对于实现某些数据结构(如链表、树、图等)是必需的。这些数据结构的大小在运行时可能会发生变化,因此它们需要能够根据需要分配和释放内存。
- 库和模块:
- 许多C语言库和模块使用动态内存分配来管理其内部状态和资源。例如,字符串处理库可能需要动态分配内存来存储长字符串,或者图形库可能需要动态分配内存来存储图像数据。
- 错误处理:
- 动态内存分配允许程序员在内存分配失败时(例如,由于内存不足)进行错误处理。通过检查
malloc
、calloc
等函数的返回值,程序员可以确定是否成功分配了内存,并据此采取相应的措施。
- 动态内存分配允许程序员在内存分配失败时(例如,由于内存不足)进行错误处理。通过检查
- 可移植性:
- 虽然不同的操作系统和平台可能有不同的内存管理策略,但C语言的动态内存分配函数(如
malloc
和free
)通常在不同的平台上都是可用的,这使得使用动态内存分配的代码更容易在不同的系统上移植。
- 虽然不同的操作系统和平台可能有不同的内存管理策略,但C语言的动态内存分配函数(如
使用动态内存分配的注意
需要注意的是,动态内存分配也带来了一些挑战,如内存泄漏(忘记释放已分配的内存)、野指针(指向已释放或未分配内存的指针)和内存碎片(由于频繁地分配和释放小块内存而导致的内存不连续)等问题。因此,在使用动态内存分配时,程序员需要特别小心并遵循良好的编程实践来避免这些问题。
C语言内存的使用方式
在C语言中,内存的使用通常可以划分为三个主要区域:栈(Stack)、堆(Heap)和静态/全局存储区(Static/Global Storage Area)。每个区域都有其特定的用途和管理方式。
- 栈(Stack):
- 栈是自动分配和释放的,其操作方式类似于数据结构中的栈。
- 栈主要用于存储局部变量、函数参数以及返回地址等。
- 当函数被调用时,会在栈上为其局部变量分配空间。当函数返回时,这些空间会被自动释放。
- 栈的大小通常是有限的,如果使用了过多的栈空间(如递归调用过深),可能会导致栈溢出(Stack Overflow)。
- 堆(Heap):
- 堆是用于动态内存分配的区域,由程序员显式地分配和释放。
- C语言提供了如
malloc
、calloc
、realloc
等函数来从堆中分配内存,以及free
函数来释放内存。 - 堆的大小通常比栈大得多,但是分配和释放堆内存需要更多的开销,并且需要程序员负责管理内存,以避免内存泄漏和野指针等问题。
- 静态/全局存储区(Static/Global Storage Area):
- 这个区域用于存储全局变量和静态变量。
- 全局变量的生命周期是整个程序运行期间,而静态变量的生命周期是从定义它的代码块开始到程序结束。
- 全局变量和静态变量在程序开始执行之前就已经分配了内存,并在程序的整个执行期间都存在。
- 静态/全局存储区还包括常量区,用于存储字符串常量、全局常量等。
除了这三个主要区域之外,C语言程序还可能使用其他内存区域,如代码区(Text/Code Area)用于存储程序的机器代码,以及某些特定的硬件或操作系统相关的内存区域。但是,从C语言程序员的视角来看,栈、堆和静态/全局存储区是最常用的三个内存区域。
需要注意的是,这些内存区域的划分和管理方式是由编译器和操作系统共同决定的,因此可能会因不同的编译器或操作系统而有所差异。但是,上述描述提供了一个对C语言内存使用的基本理解。
变长数组
在C99标准中,C语言是可以创建变长数组的,但直到目前,VS包括很多其它的编译器都是不支持变长数组的创建的。
如
int n = 8;
char arr[n]; //编译器会报错,因为[]里面规定只能是一个常量,因此不能使用变量n。
gcc编译器是支持创建变长数组的。
在Linux系统里输入命令gcc test.c -std=c99便可按C99标准编译test.c文件。
动态内存分配函数介绍
malloc
在C语言中,malloc
函数是用于动态内存分配的重要函数之一。它属于C标准库中的<stdlib.h>
头文件,用于在堆(heap)上分配指定字节数的内存空间。如果分配成功,则返回指向该内存区域的指针;如果分配失败(如堆内存不足),则返回NULL
。
malloc
函数的原型
void *malloc(size_t size); |
size
:指定要分配的内存块的大小,以字节为单位。- 返回值:成功时返回一个指向已分配大小的、类型为
void
的指针。失败时返回NULL
。
使用malloc
函数的注意事项
-
检查返回值:在使用
malloc
分配内存后,一定要检查返回的指针是否为NULL
。如果为NULL
,说明内存分配失败,需要相应地处理错误。 -
类型转换:
malloc
函数返回的是void
指针,因此在赋值给其他类型的指针时需要进行显式类型转换。 -
初始化内存:
malloc
分配的内存不会自动初始化为0。如果需要初始化,可以使用calloc
函数或者手动初始化。 -
内存泄漏:使用
malloc
分配的内存,在不再需要时,必须使用free
函数显式释放,否则会造成内存泄漏。 -
避免越界访问:确保不要访问分配内存区域之外的内存,这可能导致程序崩溃或不可预测的行为。
示例代码
#include <stdio.h> | |
#include <stdlib.h> | |
int main() { | |
int *p; | |
int num_elements = 5; | |
int element_size = sizeof(int); | |
// 使用malloc分配内存 | |
p = (int *)malloc(num_elements * element_size); | |
// 检查内存是否成功分配 | |
if (p == NULL) { | |
printf("Memory allocation failed\n"); | |
return 1; // 返回非零值表示错误 | |
} | |
// 初始化内存(可选) | |
for (int i = 0; i < num_elements; ++i) { | |
p[i] = i; | |
} | |
// 使用内存 | |
for (int i = 0; i < num_elements; ++i) { | |
printf("%d ", p[i]); | |
} | |
// 释放内存 | |
free(p); | |
return 0; | |
} |
在上面的示例中,我们首先计算了需要分配的内存大小(num_elements * element_size
),然后使用malloc
函数分配了内存,并检查是否成功。接着,我们初始化内存,并使用它。最后,我们使用free
函数释放了分配的内存。
free
在C语言中,free
函数是用于释放之前通过malloc
、calloc
或realloc
等函数在堆(heap)上分配的内存的。它属于C标准库中的<stdlib.h>
头文件。
free
函数的原型
void free(void *ptr); |
ptr
:指向要释放的内存块的指针,该指针必须是通过malloc
、calloc
或realloc
等函数返回的。
使用free
函数的注意事项
-
指针置为NULL:在调用
free
释放内存后,通常建议将指针设置为NULL
,以防止对已释放的内存进行误操作(悬垂指针)。 -
不要重复释放:对同一个指针调用
free
多次会导致未定义的行为。 -
不要释放未分配的内存:尝试释放一个未通过
malloc
、calloc
或realloc
等函数分配的内存块(例如,指向栈上变量的指针)也会导致未定义的行为。 -
内存泄漏:尽管
free
用于释放内存,但如果在程序中不正确地管理内存(例如,忘记释放内存或多次分配但只释放一次),则可能会导致内存泄漏。 -
跨函数或线程释放:只要指针是有效的(即它指向由
malloc
、calloc
或realloc
等函数分配的内存),则可以在程序的任何位置(包括其他函数或线程)调用free
来释放该内存。
示例代码
#include <stdio.h> | |
#include <stdlib.h> | |
int main() { | |
int *p; | |
int num_elements = 5; | |
// 使用malloc分配内存 | |
p = (int *)malloc(num_elements * sizeof(int)); | |
// 检查内存是否成功分配 | |
if (p == NULL) { | |
printf("Memory allocation failed\n"); | |
return 1; // 返回非零值表示错误 | |
} | |
// 使用内存(例如,初始化) | |
for (int i = 0; i < num_elements; ++i) { | |
p[i] = i; | |
} | |
// ... 在此处使用p指向的内存 ... | |
// 释放内存,并将指针置为NULL | |
free(p); | |
p = NULL; | |
// 不要再尝试访问p指向的内存,因为它已经被释放了 | |
return 0; | |
} |
在上面的示例中,我们首先使用malloc
分配了一个整数数组的内存,并检查是否成功。然后,我们使用这块内存(例如,初始化它)。当我们不再需要这块内存时,我们使用free
释放它,并将指针p
置为NULL
。
calloc
在C语言中,calloc
函数用于在堆(heap)上动态分配内存,并且会将分配的内存区域初始化为零。这个函数同样属于C标准库中的<stdlib.h>
头文件。
calloc
函数的原型
void *calloc(size_t num, size_t size); |
num
:要分配的元素的数量。size
:每个元素的大小(以字节为单位)。- 返回值:成功时返回一个指向已分配大小的、类型为
void
的指针。失败时返回NULL
。
使用calloc
函数的注意事项
-
检查返回值:与
malloc
一样,使用calloc
分配内存后,一定要检查返回的指针是否为NULL
。 -
类型转换:
calloc
函数返回的是void
指针,因此在赋值给其他类型的指针时需要进行显式类型转换。 -
内存泄漏:使用
calloc
分配的内存,在不再需要时,必须使用free
函数显式释放,否则会造成内存泄漏。 -
避免越界访问:确保不要访问分配内存区域之外的内存,这可能导致程序崩溃或不可预测的行为。
calloc
与malloc
的区别
calloc
除了分配内存外,还会将分配的内存区域初始化为零。而malloc
只是分配内存,并不负责初始化。calloc
接受两个参数,一个是要分配的元素的数量,另一个是每个元素的大小。而malloc
只接受一个参数,即要分配的总字节数。
示例代码
#include <stdio.h> | |
#include <stdlib.h> | |
int main() { | |
int *p; | |
int num_elements = 5; | |
int element_size = sizeof(int); | |
// 使用calloc分配内存并初始化为零 | |
p = (int *)calloc(num_elements, element_size); | |
// 检查内存是否成功分配 | |
if (p == NULL) { | |
printf("Memory allocation failed\n"); | |
return 1; // 返回非零值表示错误 | |
} | |
// 使用内存(此时内存已被初始化为零) | |
// ... 在此处使用p指向的内存 ... | |
// 释放内存 | |
free(p); | |
return 0; | |
} |
在上面的示例中,我们使用calloc
分配了一个整数数组的内存,并且该数组的内存区域被自动初始化为零。然后,我们使用这块内存(虽然在这个例子中我们并没有显示地使用它,因为它已经被初始化为零了)。最后,我们使用free
释放了分配的内存。
realloc
在C语言中,realloc
函数用于改变已分配内存块的大小。如果你已经使用malloc
、calloc
或realloc
分配了一块内存,但后来发现这块内存的大小不够用或太大了,你可以使用realloc
来调整它的大小。realloc
函数同样属于C标准库中的<stdlib.h>
头文件。
realloc
函数的原型
void *realloc(void *ptr, size_t newsize); |
ptr
:指向要调整大小的内存块的指针,该指针必须是通过malloc
、calloc
或realloc
等函数返回的。newsize
:新的内存块大小(以字节为单位)。- 返回值:成功时返回一个指向已调整大小的内存块的指针(可能是一个新的地址),如果内存不足以扩大当前块并且不允许缩小,则返回
NULL
。
使用realloc
函数的注意事项
-
检查返回值:
realloc
可能会因为内存不足而失败,或者因为需要移动内存块到新的位置而返回一个新的地址。因此,每次调用realloc
后,都应该检查返回的指针是否为NULL
,并更新你的指针变量。 -
不要使用原指针:如果
realloc
返回了新的地址(即内存块被移动了),那么原指针ptr
将不再有效,你应该更新你的指针变量以指向新的地址。 -
内存泄漏:如果
realloc
失败并返回NULL
,而你的原指针ptr
指向的内存还没有被释放,那么你需要手动使用free
来释放它,以避免内存泄漏。 -
避免越界访问:确保不要访问分配内存区域之外的内存,这可能导致程序崩溃或不可预测的行为。
示例代码
#include <stdio.h> | |
#include <stdlib.h> | |
int main() { | |
int *p; | |
size_t num_elements = 5; | |
size_t element_size = sizeof(int); | |
// 初始分配内存 | |
p = (int *)malloc(num_elements * element_size); | |
if (p == NULL) { | |
printf("Initial memory allocation failed\n"); | |
return 1; | |
} | |
// ... 在此处使用p指向的内存 ... | |
// 扩大内存块大小 | |
num_elements *= 2; // 翻倍 | |
p = (int *)realloc(p, num_elements * element_size); | |
if (p == NULL) { | |
printf("Memory reallocation failed\n"); | |
// 注意:此时原内存还没有被释放,如果需要可以继续使用或手动释放 | |
// free(original_p); // 如果original_p是原来的指针的话 | |
return 1; | |
} | |
// ... 在此处使用新的p指向的内存 ... | |
// 释放内存 | |
free(p); | |
return 0; | |
} |
在上面的示例中,我们首先使用malloc
分配了一块内存,并使用它。然后,我们使用realloc
将内存块的大小翻倍。如果realloc
成功,我们更新指针p
以指向新的内存地址。最后,我们使用完内存后,使用free
释放了它。