目录
[1 C 程序内存区域划分](#1 C 程序内存区域划分)
[1.1 代码区 (Code Section)](#1.1 代码区 (Code Section))
[1.2 全局/静态区 (Global/Static Section)](#1.2 全局/静态区 (Global/Static Section))
[1.3 栈区 (Stack Section)](#1.3 栈区 (Stack Section))
[1.4 堆区 (Heap Section)](#1.4 堆区 (Heap Section))
[1.5 动态内存分配](#1.5 动态内存分配)
[2 void 指针(无类型指针)](#2 void 指针(无类型指针))
[2.1 void 指针介绍](#2.1 void 指针介绍)
[2.2 void 指针的作用](#2.2 void 指针的作用)
[2.3 void 指针的特点](#2.3 void 指针的特点)
[2.4 void 指针类型转换注意事项](#2.4 void 指针类型转换注意事项)
[2.4.1 其他类型指针赋给 void 指针](#2.4.1 其他类型指针赋给 void 指针)
[2.4.2 void 指针赋给其他类型指针](#2.4.2 void 指针赋给其他类型指针)
[3 malloc() 函数](#3 malloc() 函数)
[3.1 函数原型](#3.1 函数原型)
[3.2 使用步骤](#3.2 使用步骤)
[3.3 动态分配整型数据的空间](#3.3 动态分配整型数据的空间)
[3.4 动态分配数组空间](#3.4 动态分配数组空间)
[4 calloc() 函数](#4 calloc() 函数)
[4.1 函数原型](#4.1 函数原型)
[4.2 使用步骤](#4.2 使用步骤)
[4.3 案例演示](#4.3 案例演示)
[5 realloc() 与 _msize 函数](#5 realloc() 与 _msize 函数)
[5.1 函数原型](#5.1 函数原型)
[5.2 使用步骤](#5.2 使用步骤)
[5.3 案例演示](#5.3 案例演示)
[6 内存泄漏与 free() 函数](#6 内存泄漏与 free() 函数)
[6.1 内存泄漏](#6.1 内存泄漏)
[6.2 free() 函数](#6.2 free() 函数)
[6.2.1 函数原型](#6.2.1 函数原型)
[6.2.2 使用步骤](#6.2.2 使用步骤)
[6.2.3 注意事项](#6.2.3 注意事项)
[6.2.4 案例演示](#6.2.4 案例演示)
[7 内存分配的基本原则](#7 内存分配的基本原则)
[7.1 避免分配大量的小内存块](#7.1 避免分配大量的小内存块)
[7.2 仅在需要时分配内存](#7.2 仅在需要时分配内存)
[7.3 总是确保释放已分配的内存](#7.3 总是确保释放已分配的内存)
[8 综合案例](#8 综合案例)
1 C 程序内存区域划分
在 C 语言中,内存可以分为几个不同的区域,每个区域都有其特定的作用。
1.1 代码区 (Code Section)
也称为文本区,是只读的 ,用于存放程序的机器指令。
这个区域的内容是在程序编译时确定的,并且在程序运行期间不会改变。
1.2 全局/静态区 (Global/Static Section)
这个区域用于存储全局变量和静态变量。
全局变量是在所有函数外部定义的变量,它们在整个程序的生命周期内都存在。
静态变量可以在全局或局部范围内定义,但无论在哪里定义,它们都会在这个区域分配空间,并且在整个程序执行过程中保持存在。
初始化的全局变量和静态变量会被分配到已初始化的数据段(如 .data 段),而未初始化的全局变量和静态变量则会被分配到未初始化的数据段(如 .bss 段)。
1.3 栈区 (Stack Section)
栈区用于存储函数的局部变量和函数调用信息(如返回地址)。
当一个函数被调用时,一个新的栈帧(stack frame)会被创建并压入栈顶,其中包含了该函数所有局部变量。
函数执行完毕后,这个栈帧会被弹出栈,释放了该函数使用的内存。
栈区的操作是自动化的,由编译器管理,不需要程序员手动干预。
1.4 堆区 (Heap Section)
堆区是一个动态分配内存的区域,通常通过 malloc()、calloc()、realloc() 和 free() 等函数进行管理。
动态内存分配允许程序在运行时请求任意大小的内存块,这对于处理未知大小的数据集非常有用。
一旦不再需要这块内存,应该调用 free() 函数释放它,以避免内存泄漏。
1.5 动态内存分配
动态内存分配是指在程序运行时根据需要从堆区请求内存的行为。
由于动态分配的内存没有与任何变量名关联,因此必须使用指针来访问这些内存。
使用动态内存分配时,程序员需要负责确保正确地分配和释放内存,以防止内存泄漏或其他错误。
2 void 指针(无类型指针)
2.1 void 指针介绍
在 C 语言中,void 指针是一种特殊的指针类型,它可以指向任何类型的数据。C99 标准允许定义一个类型为 void 的指针变量,这种指针在编译时没有具体的类型信息,因此可以灵活地用于各种场景。
2.2 void 指针的作用
灵活性 :指针变量必须有类型,以便编译器知道如何解释内存块中的二进制数据。然而,在某些情况下,当向系统请求内存时,可能还不确定会有什么类型的数据写入内存。此时,可先使用void指针获取内存块(仅含地址信息,无类型信息),待后续使用时再明确数据类型。
通用性 :void 指针在函数参数传递和通用数据处理中非常有用,尤其是在实现泛型编程时,可以避免重复编写针对不同数据类型的代码。
2.3 void 指针的特点
类型转换 :void 指针与其他所有类型的指针之间可以互相转换。任一类型的指针都可以转换为 void 指针,而 void 指针也可以转换为任一类型的指针。
解引用限制 :由于 void 指针没有具体的类型信息,因此不能直接使用 * 运算符(解引用)来访问它所指向的值 。如果需要访问 void 指针指向的数据,必须先将其转换为适当的类型指针。
cpp
#include <stdio.h>
int main()
{
int num = 42;
double pi = 3.14159;
// 将 int 指针隐式转换为 void 指针
void *viPtr = #
// 将 double 指针隐式转换为 void 指针
void *vdPtr = π
// 将 void 指针转换为 int 指针并解引用
// int *intPtr = viPtr; // 隐式类型转换
int *intPtr = (int *)viPtr; // 显示类型转换
printf("整数值:%d\n", *intPtr); // 42
// 将 void 指针转换为 double 指针并解引用
// double *doublePtr = vdPtr; // 隐式类型转换
double *doublePtr = (double *)vdPtr; // 显示类型转换
printf("浮点数:%f\n", *doublePtr); // 3.141590
// void 指针不能直接解引用,会导致编译错误
// 下面的代码会报错
// printf("%d\n", *viPtr);
// printf("%f\n", *vdPtr);
// 如果需要访问 void 指针指向的数据,必须先将其转换为适当的类型指针
printf("%d\n", *(int *)viPtr); // 42
printf("%f\n", *(double *)vdPtr); // 3.141590
return 0;
}
2.4 void 指针类型转换注意事项
2.4.1 其他类型指针赋给 void 指针
将其他类型指针赋给 void 指针时,可以使用隐式转换,因为 void 指针不包含指向的数据类型的信息,通常是安全的。
cpp
int num = 42;
double pi = 3.14159;
// 将 int 指针隐式转换为 void 指针
void *viPtr = #
// 将 double 指针隐式转换为 void 指针
void *vdPtr = π
2.4.2 void 指针赋给其他类型指针
将 void 指针赋给其他类型指针时,建议使用显式类型转换,这样更加安全。如果使用隐式类型转换,有些编译器会触发警告。
cpp
// 将 void 指针显式转换为 int 指针并解引用
int *intPtr = (int *)viPtr;
printf("整数值:%d\n", *intPtr);
// 将 void 指针显式转换为 double 指针并解引用
double *doublePtr = (double *)vdPtr;
printf("浮点数:%f\n", *doublePtr);
3 malloc() 函数
3.1 函数原型
malloc() 函数用于在程序运行时动态分配一块连续的内存空间。这是 C 语言中常用的动态内存分配函数之一,通常与 free() 函数一起使用,以确保内存的正确管理和释放。
cpp
#include <stdlib.h>
void *malloc(size_t size);
- size :要分配的内存块的大小,以字节为单位。
- 如果内存分配成功,返回一个 void 指针,指向新分配内存块的起始地址。
- 如果内存分配失败(例如内存不足),返回一个空指针 NULL。
3.2 使用步骤
- 分配内存:调用 malloc() 函数,传入所需的内存大小。
- 检查返回值:检查返回的指针是否为 NULL,以确保内存分配成功。
- 使用内存:将返回的 void 指针转换为适当的类型指针,并使用该指针访问分配的内存。
- 释放内存:使用 free() 函数释放分配的内存,以避免内存泄漏。
3.3 动态分配整型数据的空间
cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
// 在栈区直接创建局部变量
int num = 120;
int *p = NULL;
// 动态分配整型数据的空间
// malloc(sizeof(int)) 请求分配一个 int 类型大小的内存块
// (int *) 是显式类型转换,将 void 指针转换为 int 指针
// p 指向新分配内存块的起始地址
p = (int *)malloc(sizeof(int));
// 检查内存是否分配成功
if (p == NULL)
{
printf("内存分配失败\n");
return 1; // 退出程序
}
// p = # 不要这样操作,这相当于修改了指针 p 的指向,就没有用到上面动态分配的空间
// 使用解引用赋值并输出
*p = num;
printf("p指向的地址(堆区):%p\n", (void *)p);
printf("局部变量num的地址(栈区):%p\n", (void *)&num);
printf("p指向的值:%d\n", *p); // 120
// 释放分配的内存,避免内存泄漏
free(p);
p = NULL; // 释放后将指针设为 NULL,避免悬挂指针
return 0;
}
输出结果如下所示:
错误做法:
3.4 动态分配数组空间
在 C 语言中,malloc() 函数不仅可用于分配单个变量的内存,还可以用于动态分配数组的内存(指针的偏移)。以下是一个示例,展示了如何使用 malloc() 函数动态分配整型数组的内存,并对其进行操作。
cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p = NULL; // 定义整型指针
int n = 5; // 定义数组长度
// int array[n]; 错误,表达式必须含有常量值
// 动态分配内存,将地址赋给指针 p
// malloc(n * sizeof(int)) 请求分配一个大小为 n * sizeof(int) 的内存块,即 n 个 int 类型的内存
// (int *) 是显式类型转换,将 void 指针转换为 int 指针
// p 指向新分配内存块的起始地址
p = (int *)malloc(n * sizeof(int));
// 判断是否分配成功
if (p == NULL)
{
printf("内存分配失败\n");
return 1; // 退出程序
}
// 给数组元素赋值
for (int i = 0; i < n; i++)
{
// p[i] = i * 10; // p[i] 等价于 *(p + i)
*(p + i) = i * 10;
}
// 输出数组的元素
for (int i = 0; i < n; i++)
{
// printf("p[%d] = %d\n", i, p[i]); // p[i] 等价于 *(p + i)
printf("p[%d] = %d\n", i, *(p + i));
}
// 释放分配的内存,避免内存泄漏
free(p);
p = NULL; // 释放后将指针设为 NULL,避免悬挂指针
return 0;
}
输出结果如下所示:
4 calloc() 函数
4.1 函数原型
calloc() 函数用于在程序运行时动态分配内存,并将分配的内存初始化为零。这是 C 语言中常用的动态内存分配函数之一,通常与 free() 函数一起使用,以确保内存的正确管理和释放。
cpp
#include <stdlib.h>
void *calloc(size_t numElements, size_t sizeOfElement);
- numElements :要分配的元素的数量。
- sizeOfElement :每个元素的大小(以字节为单位)。
- 如果内存分配成功,返回一个 void 指针,指向新分配内存块的起始地址。
- 如果内存分配失败(例如内存不足),返回一个空指针 NULL。
4.2 使用步骤
- 分配内存:调用 calloc() 函数,传入所需的元素数量和每个元素的大小。
- 检查返回值:检查返回的指针是否为 NULL,以确保内存分配成功。
- 使用内存:将返回的 void 指针转换为适当的类型指针,并使用该指针访问分配的内存。
- 释放内存:使用 free() 函数释放分配的内存,以避免内存泄漏。
4.3 案例演示
以下是一个示例代码,展示了如何使用 calloc() 函数动态分配整型数组的内存,并将其初始化为零:
cpp
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p = NULL; // 定义整型指针
int n = 5; // 定义数组长度
// 动态分配内存并初始化为零,将地址赋给指针 p
// calloc(n, sizeof(int)) 请求分配一个大小为 n * sizeof(int) 的内存块,并将每个字节初始化为零
// (int *) 是显式类型转换,将 void 指针转换为 int 指针
p = (int *)calloc(n, sizeof(int));
// 判断是否分配成功
if (p == NULL)
{
printf("内存分配失败\n");
return 1; // 退出程序
}
// 输出数组的元素的值
for (int i = 0; i < n; i++)
{
printf("p[%d] = %d\n", i, p[i]); // 全是 0
}
// 给数组元素赋值
for (int i = 0; i < n; i++)
{
p[i] = i * 10;
}
// 输出数组的元素
for (int i = 0; i < n; i++)
{
printf("p[%d] = %d\n", i, p[i]); // 0 10 20 30 40
}
// 释放分配的内存,避免内存泄漏
// free(p); // 简单处理
// 推荐处理
if (p != NULL)
{
free(p);
p = NULL; // 释放后将指针设为 NULL,避免悬挂指针
}
return 0;
}
输出结果如下所示:
5 realloc() 与 _msize 函数
5.1 函数原型
realloc() 函数用于重新分配 malloc() 或 calloc() 函数所获得的内存块的大小。这在需要动态调整内存大小时非常有用。
cpp
#include <stdlib.h>
void *realloc(void *ptr, size_t size);
- ptr :要重新分配的内存块的指针。
- size :新的内存块的大小(以字节为单位)。
- 返回一个指向重新分配内存块的指针。如果内存重新分配成功,返回的指针可能与原始指针相同,也可能不同。
- 如果内存分配失败,返回一个空指针 NULL。
- 如果在原内存块上进行缩减,通常返回的地址与原来的地址相同。
5.2 使用步骤
- 分配内存:使用 malloc() 或 calloc() 函数分配初始内存。
- 重新分配内存:调用 realloc() 函数,传入当前指针和新的内存大小。
- 检查返回值:检查返回的指针是否为 NULL,以确保内存重新分配成功。
- 使用新的内存:使用返回的新指针访问重新分配的内存。
- 释放内存:使用 free() 函数释放分配的内存,以避免内存泄漏。
5.3 案例演示
以下是一个示例代码,展示了如何使用 realloc() 函数动态调整内存大小,并使用 _msize() 函数获取指定内存块的大小:
_msize() 函数用于获取指定内存块的大小,但请注意,这个函数不是标准 C 库的一部分,而是特定于某些平台(如 Windows)。在其他平台上,可能需要使用其他方法来获取内存块的大小。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
int main()
{
// 声明指针
int *p = NULL;
// 分配内存
// 使用 malloc() 函数分配初始内存,大小为 100 * sizeof(int)
p = (int *)malloc(sizeof(int) * 100);
if (p == NULL)
{
printf("初始内存分配失败\n");
return 1;
}
// 使用 _msize() 函数获取分配的内存大小,并输出指针地址和内存大小
printf("p=%p, size:%zu 字节\n", p, _msize(p)); // 400
// 调整内存大小
p = (int *)realloc(p, sizeof(int) * 2000);
if (p == NULL)
{
printf("内存重新分配失败\n");
return 1;
}
// 使用 _msize() 函数获取分配的内存大小,并输出指针地址和内存大小
printf("p=%p, size:%zu 字节\n", p, _msize(p)); // 8000
// 再次调整内存大小
// 如果在原内存块上进行缩减,通常返回的地址与原来的地址相同
p = (int *)realloc(p, sizeof(int) * 200);
if (p == NULL)
{
printf("内存重新分配失败\n");
return 1;
}
// 使用 _msize() 函数获取分配的内存大小,并输出指针地址和内存大小
printf("p=%p, size:%zu 字节\n", p, _msize(p)); // 800
// 释放分配的内存,避免内存泄漏
// free(p); // 简单处理
// 推荐处理
if (p != NULL)
{
free(p);
p = NULL; // 释放后将指针设为 NULL,避免悬挂指针
}
return 0;
}
输出结果如下所示:
6 内存泄漏与 free() 函数
6.1 内存泄漏
内存泄漏是指在程序运行过程中,动态分配的内存空间没有被正确释放,导致系统中的可用内存逐渐减少,直到耗尽系统可用的内存资源。内存泄漏不仅会影响程序的性能,还可能导致程序崩溃或系统不稳定。
6.2 free() 函数
6.2.1 函数原型
free() 函数用于释放动态分配的内存,以便将内存返回给操作系统,防止内存泄漏。
cpp
#include <stdlib.h>
void free(void *ptr);
- ptr :指向要释放的内存块的指针。ptr 必须是通过 malloc()、calloc() 或 realloc() 动态分配的内存块地址。
- free() 函数没有返回值。
6.2.2 使用步骤
- 分配内存:使用 malloc()、calloc() 或 realloc() 函数动态分配内存。
- 使用内存:在程序中使用分配的内存。
- 释放内存:使用 free() 函数释放分配的内存,确保内存返回给操作系统。
6.2.3 注意事项
1. 避免双重释放:
释放的内存块一旦被 free() 释放,就不应该再次操作已经释放的地址,也不应该再次使用 free() 对该地址释放第二次。这会导致未定义行为,可能会导致程序崩溃。
cpp
int *p = (int *)malloc(sizeof(int));
free(p); // 第一次释放
free(p); // 错误:第二次释放
2. 避免内存泄漏:
如果忘记调用 free() 函数,会导致无法访问未回收的内存块,构成内存泄漏。
cpp
int *p = (int *)malloc(sizeof(int));
// 忘记释放内存
// free(p); // 应该在这里释放内存
3. 检查指针是否为 NULL:
在释放内存之前,最好检查指针是否为 NULL。释放 NULL 指针是安全的,不会导致错误,但这是一个良好的编程习惯。
释放内存后,最好将指针设为 NULL,以避免悬挂指针(即指向已释放内存的指针)。
cpp
int *p = (int *)malloc(sizeof(int));
if (p != NULL) {
free(p);
p = NULL; // 释放后将指针设为 NULL,避免悬挂指针
}
6.2.4 案例演示
以下是一个示例代码,展示了如何正确使用 malloc() 和 free() 函数,避免内存泄漏和双重释放:
cpp
#include <stdio.h>
#include <stdlib.h>
int main() {
int *p = NULL; // 定义整型指针
// 动态分配内存
p = (int *)malloc(sizeof(int));
if (p == NULL) {
printf("内存分配失败\n");
return 1;
}
// 使用分配的内存
*p = 120;
printf("p指向的地址:%p\n", (void *)p);
printf("p指向的值:%d\n", *p);
// 释放分配的内存
if (p != NULL) {
free(p);
p = NULL; // 释放后将指针设为 NULL
}
return 0;
}
7 内存分配的基本原则
在 C 语言中,动态内存分配是一项重要的任务,合理的内存管理可以提高程序的性能和稳定性。以下是一些内存分配的基本原则:
7.1 避免分配大量的小内存块
原因 :分配堆上的内存有一些系统开销,包括分配和释放内存时的管理开销。分配许多小的内存块会增加这些开销,从而影响程序的性能。
建议 :尽量合并多个小内存块的分配,使用较大的内存块来减少系统开销。例如,可以预先分配一个较大的缓冲区,然后在需要时从中划分出所需的小内存块。
7.2 仅在需要时分配内存
原因 :动态分配的内存会占用系统的资源,如果分配了不必要的内存,不仅浪费资源,还可能导致内存泄漏。
建议 :在实际需要使用内存时再进行分配,并且在使用完内存后及时释放。避免过早分配内存或分配过多的内存。
7.3 总是确保释放已分配的内存
原因 :未释放的内存会导致内存泄漏,随着时间的推移,内存泄漏会逐渐消耗系统资源,最终可能导致程序崩溃或系统不稳定。
建议 :在编写分配内存的代码时,就要确定好在代码的什么地方释放内存。使用 free() 函数释放不再需要的内存,并确保不会对同一个内存块多次释放。
cpp
#include <stdio.h>
#include <stdlib.h>
// 分配和释放内存的辅助函数
void allocate_and_use_memory() {
int *p = NULL;
// 仅在需要时分配内存
p = (int *)malloc(sizeof(int) * 1000);
if (p == NULL) {
printf("内存分配失败\n");
return;
}
// 使用分配的内存
for (int i = 0; i < 1000; i++) {
p[i] = i * 10;
}
// 输出部分元素
for (int i = 0; i < 10; i++) {
printf("p[%d] = %d\n", i, p[i]);
}
// 及时释放内存
if (p != NULL)
{
free(p);
p = NULL; // 释放后将指针设为 NULL,避免悬挂指针
}
}
int main() {
// 调用内存管理函数
allocate_and_use_memory();
return 0;
}
8 综合案例
动态创建数组,输入 5 个学生的成绩,再定义一个函数检测成绩低于 60 分的,输出不合格的成绩。
cpp
#include <stdlib.h>
#include <stdio.h>
// 函数原型声明
void check(int *);
int main()
{
int *p = NULL;
// 在堆区开辟一个 5 * 4 的空间,用于存储 5 个整数
p = (int *)malloc(5 * sizeof(int));
// 检查内存是否分配成功
if (p == NULL)
{
printf("内存分配失败\n");
return 1; // 退出程序
}
printf("请输入5个成绩(整数):");
// 从用户输入读取 5 个整数,存储到动态分配的内存中
for (int i = 0; i < 5; i++)
{
// scanf("%d", p + i); // 使用指针算术,将输入的整数存储到 p[i] 中
// p + i 是指针算术,表示将指针 p 向后移动 i 个元素的位置
scanf("%d", &p[i]); // 使用数组形式,将输入的整数存储到 p[i] 中
// &p[i] 是取地址操作,表示 p 指向的数组中第 i 个元素的地址
}
// 调用 check 函数,检查不及格的成绩
check(p);
// 释放动态分配的内存,避免内存泄漏
free(p);
p = NULL; // 避免悬挂指针
return 0;
}
// 函数定义
void check(int *p)
{
printf("不及格的成绩有: ");
// 遍历动态分配的内存中的 5 个整数
for (int i = 0; i < 5; i++)
{
// 如果成绩小于 60,输出该成绩
if (p[i] < 60)
{
printf(" %d ", p[i]);
}
}
}
输出结果如下所示: