网站建设不赚钱,腾达企业交换机管理网站,软文发布网站,怎么给公司做网站#x1f493;博客主页#xff1a;江池俊的博客⏩收录专栏#xff1a;C语言进阶之路#x1f449;专栏推荐#xff1a;✅C语言初阶之路 ✅数据结构探索#x1f4bb;代码仓库#xff1a;江池俊的代码仓库#x1f389;欢迎大家点赞#x1f44d;评论#x1f4dd;收藏⭐ 文…
博客主页江池俊的博客⏩收录专栏C语言进阶之路专栏推荐✅C语言初阶之路 ✅数据结构探索代码仓库江池俊的代码仓库欢迎大家点赞评论收藏⭐ 文章目录 1. 为什么存在动态内存分配2. 动态内存函数的介绍malloc、calloc、realloc、free2.1 malloc 和 free2.2 calloc2.3 realloc 3. 常见的动态内存错误3.1 对NULL指针的解引用操作3.2 对动态开辟空间的越界访问3.3 对非动态开辟内存使用free释放3.4 使用free释放一块动态开辟内存的一部分3.5 对同一块动态内存多次释放3.6 动态开辟内存忘记释放内存泄漏【总结】 4. C/C程序的内存开辟5. 柔性数组5.1 柔性数组的特点5.2 柔性数组的使用5.3 柔性数组的优势 6. 经典笔试题6.1 题目16.2 题目26.3 题目36.4 题目4 前言 动态内存管理是计算机科学中的一个重要概念它允许程序在运行时请求和释放内存。这种灵活性使得程序能够根据需要调整其内存使用量从而提高了资源利用率和程序性能。然而动态内存管理也带来了一定的复杂性如内存泄漏、悬空指针等问题。因此对动态内存管理的理解和应用能力对于编程人员来说至关要。本文将介绍动态内存管理的基本概念、原理和方法包括静态内存分配和动态内存分配的区别、内存分配的几种方式如 malloc、calloc、realloc 和 free 函数、内存泄漏的检测和避免方法。希望通过本文的学习读者能够掌握动态内存管理的基本原理和技巧在实际编程过程中避免出现相关问题提高程序的稳定性和可维护性。 首先我们先来了一下C语言的内存区域的划分情况
在C语言中内存可以分为以下几个部分 栈区Stack存放函数的局部变量和函数调用时的参数。其生命周期由编译器自动管理函数调用结束后会自动清空。 堆区Heap也称为动态内存分配区用于存放通过 malloc、calloc、realloc 等函数动态分配的内存。该区域的内存需要程序员自行管理使用完毕后需要手动释放否则会导致内存泄漏。 静态存储区Static Storage存放全局变量和静态变量。这些变量的生命周期是整个程序运行期间直到程序结束时才被释放。 代码区Code Segment存放程序的可执行代码。这部分内存是只读的不能写入数据。 数据区Data Segment存放已初始化的全局变量和静态变量。这些变量的值可以在程序运行时被修改。
需要注意的是在不同的操作系统和编译器下内存分区的具体实现可能会有所不同。但是以上几个区域是C语言中常见的内存分区。
这里我们讲的动态内存分配都是在堆区上开辟申请空间的且是由程序员来控制的。 1. 为什么存在动态内存分配
在前面我们已经掌握的内存开辟方式有
int val 20;//在栈空间上开辟四个字节
char arr[10] {0};//在栈空间上开辟10个字节的连续空间但是上述的开辟空间的方式有两个特点
空间开辟大小是固定的。数组在申明的时候必须指定数组的长度它所需要的内存在编译时分配。
但是对于空间的需求不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道 那数组的编译时开辟空间的方式就不能满足了。 这时候就只能试试动态存开辟了。 2. 动态内存函数的介绍malloc、calloc、realloc、free
头文件均为 stdlib.h
2.1 malloc 和 free
C语言提供了一个动态内存开辟的函数
声明
void *malloc(size_t size);参数 size – 内存块的大小以字节为单位。 返回值 该函数返回一个指针 指向已分配大小的内存。如果请求失败则返回 NULL。 【注意】
这个函数向内存申请一块连续可用的空间并返回指向这块空间的指针。如果开辟成功则返回一个指向开辟好空间的指针。如果开辟失败则返回一个NULL指针因此malloc的返回值一定要做检查。返回值的类型是 void* 所以malloc函数并不知道开辟空间的类型具体在使用的时候使用者自己来决定。如果参数 size 为0malloc的行为是标准是未定义的取决于编译器。
C语言提供了另外一个函数free专门是用来做动态内存的释放和回收的
声明
void free(void *ptr);参数 ptr - - - 指针指向一个要释放内存的内存块该内存块之前是通过调用 malloc、calloc 或 realloc 进行分配内存的。如果传递的参数是一个空指针则不会执行任何动作。 返回值 该函数不返回任何值。 【注意】
如果参数 ptr 指向的空间不是动态开辟的那 free 函数的行为是未定义的。如果参数 ptr 是NULL指针则函数什么事都不做。
malloc 和 free 都声明在 stdlib.h 头文件中。
【示例】
#include stdio.h
#includestdlib.h
int main()
{//代码1int num 0;scanf(%d, num);int arr[num] { 0 };//代码2int* ptr NULL;ptr (int*)malloc(num * sizeof(int));if (NULL ! ptr)//判断ptr指针是否为空{int i 0;for (i 0; i num; i){*(ptr i) 0;}}free(ptr);//释放ptr所指向的动态内存ptr NULL;//是否有必要return 0;
}很明显代码1会报错这就说明在编译前我们不能使用静态内存分配给定数组一个通过变量来确定的大小的空间所以为了在编译前创建一个给定的大小空间就需要使用到动态内存分配即代码2 2.2 calloc
C语言还提供了一个函数叫 calloc calloc 函数也用来动态内存分配。
声明
void *calloc(size_t num, size_t size);参数 num - - - 要被分配的元素个数。size - - - 元素的大小。 返回值 该函数返回一个指针指向已分配的内存。如果请求失败则返回 NULL。 【注意】
函数的功能是为 num 个大小为 size 的元素开辟一块空间并且把空间的每个字节初始化为 0。与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全 0。如果不需要初始化可以使用 malloc() 函数代替。另外使用 calloc() 函数时需要注意如果分配的内存块过大可能会导致内存不足的问题。
【示例】
#include stdio.h
#include stdlib.h
int main()
{int* p (int*)calloc(10, sizeof(int));if (NULL ! p){//使用空间}free(p);p NULL;return 0;
}所以如果我们对申请的内存空间的内容要求初始化那么可以很方便的使用calloc函数来完成任务。
2.3 realloc
realloc函数的出现让动态内存管理更加灵活。有时会我们发现过去申请的空间太小了有时候我们又会觉得申请的空间过大了那为了合理的时候内存我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
声明
void *realloc(void *ptr, size_t size);参数 ptr - - - 指针指向一个要重新分配内存的内存块该内存块之前是通过调用 malloc、calloc 或 realloc 进行分配内存的。如果为空指针则会分配一个新的内存块且函数返回一个指向它的指针。size - - - 内存块的新的大小以字节为单位。如果大小为 0且 ptr 指向一个已存在的内存块则 ptr 所指向的内存块会被释放并返回一个空指针。 返回值 该函数返回一个指针 指向重新分配大小的内存。如果请求失败则返回 NULL。 【注意】
这个函数调整原内存空间大小的基础上还会将原来内存中的数据移动到 新 的空间。realloc在调整内存空间的是存在两种情况 1. 情况1原有空间之后有足够大的空间 2. 情况2原有空间之后没有足够大的空间
情况1原地扩容 当是情况1 的时候要扩展内存就直接原有内存之后直接追加空间原来空间的数据不发生变化。 情况2异地扩容 当是情况2 的时候原有空间之后没有足够多的空间时扩展的方法是在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
由于上述的两种情况realloc函数的使用就要注意一些。
【示例】
#include stdio.h
#includestdlib.hint main()
{int* ptr (int*)malloc(100);if (ptr ! NULL){//业务处理}else{exit(EXIT_FAILURE); //异常退出程序}//扩展容量//代码1//ptr (int*)realloc(ptr, 1000);//不建议这样写如果内存申请失败则ptr会指向NULL,即再也找不到原先的那块地址会导致内存泄漏的问题//代码2int* p NULL;//p (int*)realloc(ptr, 110); //原地扩容p (int*)realloc(ptr, 1000); //异地扩容if (p ! NULL){ptr p;}//业务处理free(ptr);return 0;
}【运行结果】
1. 原地扩容 2. 异地扩容 【ps】EXIT_FAILURE EXIT_FAILURE是C语言头文件库中定义的一个符号常量在vc6.0下头文件stdlib.h中定义如下 #define EXIT_FAILURE 1EXIT_FAILURE 可以作为exit()的参数来使用表示没有成功地执行一个程序。EXIT_SUCCESS 作为exit()的参数来使用表示成功地执行一个程序。 3. 常见的动态内存错误
3.1 对NULL指针的解引用操作
这是一种非常常见的编程错误发生在尝试访问空指针所指向的内存时。例如
int *p NULL;
*p 10; // 对NULL指针进行解引用操作会导致程序崩溃为了避免这种错误我们应该始终检查指针是否为空然后再进行解引用操作。例如
if (p ! NULL)
{*p 10; // 只有在p非空时才进行解引用操作
}3.2 对动态开辟空间的越界访问
这种错误通常发生在程序试图访问超出已分配内存区域的内存时。例如
int *arr (int *)malloc(10 * sizeof(int)); // 动态分配一个包含10个整数的数组
for (int i 0; i 10; i) // 循环访问数组的第11个元素导致越界访问
{ arr[i] i; // 访问超出已分配内存区域的内存会导致未定义的行为
} //当i是10的时候越界访问为了避免这种错误我们应该确保不会访问超出已分配内存区域的内存。可以使用数组下标来访问数组元素并确保下标在有效范围内。例如
for (int i 0; i 10; i) // 循环访问数组的前10个元素不会导致越界访问
{ arr[i] i; // 访问有效的内存区域不会出现问题
}3.3 对非动态开辟内存使用free释放
这会导致未定义的行为因为 free 函数只能用于释放通过malloc、calloc或realloc 函数动态分配的内存。 例如
int arr[10]; // 静态数组不是通过malloc等函数动态分配的内存
free(arr); // 试图释放非动态开辟的内存会导致未定义的行为为了避免这种错误我们应该只使用 free 函数来释放通过 malloc、calloc或realloc 函数动态分配的内存。对于静态数组和其他非动态分配的内存应该使用适当的方法来释放它们。例如
int arr[10]; // 静态数组不是通过malloc等函数动态分配的内存
// 不需要使用free函数来释放静态数组的内存因为它会在程序结束时自动释放3.4 使用free释放一块动态开辟内存的一部分
当我们使用malloc或calloc函数动态分配内存时我们可以使用free函数来释放这块内存。然而如果我们只释放动态内存的一部分而不是整个内存块就会导致未定义的行为。这是因为free函数只会释放它所接收到的指针所指向的内存块而不会检查是否还有其他未被释放的部分。
例如假设我们有一个包含10个整数的数组但我们只释放了其中5个整数的空间
int* arr (int*)malloc(10 * sizeof(int));
// ... 对数组进行操作
free(arr 5); // 只释放了前5个整数的空间这样做会导致未定义的行为因为我们没有释放剩余的5个整数的空间。这可能会导致内存泄漏或者访问非法内存的问题。因此我们应该始终释放整个动态分配的内存块而不是只释放其中的一部分。
3.5 对同一块动态内存多次释放
当我们释放一个动态分配的内存块后它的地址不会发生变化但是这块内存的所有权和可访问性会发生变化。再次释放同一个内存块会导致未定义的行为因为它可能已经被操作系统回收或者分配给其他变量。
例如
int *ptr (int *)malloc(sizeof(int) * 10);
free(ptr); // 释放内存块
free(ptr); // 再次释放同一个内存块这是不安全的为了避免这种情况我们应该确保每个内存块只被释放一次。如果需要重新分配内存可以使用realloc函数来调整已有内存块的大小。
3.6 动态开辟内存忘记释放内存泄漏
这是另一个常见的错误被称为内存泄漏。当我们动态分配了内存但没有释放它会导致程序消耗的内存持续增长可能最终耗尽所有可用内存。这通常发生在循环中或者在函数内部我们分配了内存但忘记在合适的时候释放它。
例如
int *ptr malloc(10 * sizeof(int)); // 被分配的内存
// 一些操作...
// 但在之后的代码中忘记释放内存避免这个问题的最佳方法是养成良好的编程习惯确保每次分配内存后在不再需要时立即释放它。我们可以在适当的位置添加 free 语句确保内存被正确地释放。此外我们还可以使用工具如 Valgrind 等来检测内存泄漏。
【总结】 忘记释放不再使用的动态开辟的空间会造成内存泄漏。切记 动态开辟的空间一定要释放并且正确释放 。 只有当我们完全不再需要动态分配的内存时才应该释放它而且不要试图释放不属于我们的内存。 4. C/C程序的内存开辟 C/C程序内存分配的几个区域
栈区stack在执行函数时函数内局部变量的存储单元都可以在栈上创建函数执行结 束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中效率很高但是 分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返 回地址等。堆区heap一般由程序员分配释放 若程序员不释放程序结束时可能由OS回收 。分 配方式类似于链表。数据段静态区static存放全局变量、静态数据。程序结束后由系统释放。代码段存放函数体类成员函数和全局函数的二进制代码。
有了这幅图我们就可以更好的理解在《C语言初识》中讲的static关键字修饰局部变量的例子了。
实际上普通的局部变量是在栈区分配空间的栈区的特点是在上面创建的变量出了作用域就销毁。但是被 static 修饰的变量存放在数据段静态区数据段的特点是在上面创建的变量直到程序 结束才销毁所以生命周期变长。 5. 柔性数组 也许你从来没有听说过柔性数组flexible array这个概念但是它确实是存在的。 C99 中结构中的最后一个元素允许是未知大小的数组这就叫做 『柔性数组』 成员。 例如
typedef struct st_type
{int i;int a[0];//柔性数组成员
}type_a;有些编译器会报错无法编译可以改成
typedef struct st_type
{int i;int a[];//柔性数组成员
}type_a;在这个示例中我们定义了一个名为 type_a 的结构体其中包含一个名为 a 的柔性数组成员。 注意柔性数组成员的类型后面没有指定大小这是因为其大小需要在运行时确定。
5.1 柔性数组的特点
结构中的柔性数组成员前面必须 至少一个其他成员 。sizeof 返回的这种结构大小不包括柔性数组的内存。包含柔性数组成员的结构用 malloc () 函数进行内存的动态分配并且分配的内存应该大于结构的大小以适应柔性数组的预期大小。
例如
//代码1
typedef struct st_type
{int i;int a[];//柔性数组成员
}type_a;
printf(%d\n, sizeof(type_a));//输出的是45.2 柔性数组的使用
//代码1
int i 0;
type_a* p (type_a*)malloc(sizeof(type_a) 100 * sizeof(int));
//业务处理
p-i 100;
for (i 0; i 100; i)
{p-a[i] i;
}
free(p);这样柔性数组成员 a相当于获得了 100 个整型元素的连续空间。
5.3 柔性数组的优势
上述的 type_a 结构也可以设计为
//代码2
typedef struct st_type
{int i;int* p_a;
}type_a;int mian()
{type_a* p (type_a*)malloc(sizeof(type_a));p-i 100;p-p_a (int*)malloc(p-i * sizeof(int));//业务处理for (i 0; i 100; i){p-p_a[i] i;}//释放空间free(p-p_a);p-p_a NULL;free(p);p NULL;return 0;
}上述 代码1 和 代码2 可以完成同样的功能但是 方法1 的实现有两个好处
方便内存释放 如果我们的代码是在一个给别人用的函数中你在里面做了二次内存分配并把整个结构体返回给用户。用户调用 free 可以释放结构体但是用户并不知道这个结构体内的成员也需要 free所以你不能指望用户来发现这个事。所以如果我们把结构体的内存以及其成员要的内存一次性分配好了并返回给用户一个结构体指针用户做一次 free 就可以把所有的内存也给释放掉。 这样有利于访问速度. 连续的内存有益于提高访问速度也有益于减少内存碎片。 6. 经典笔试题
6.1 题目1 void GetMemory(char* p)
{p (char*)malloc(100);
}
void Test(void)
{char* str NULL;GetMemory(str);strcpy(str, hello world);printf(str);
}int main()
{Test();return 0;
}【解析】
传入 GetMemory 的指针 str 为实参指针 p 为形参而形参p只是实参str的一份临时拷贝即使在函数内部申请了一块内存空间当函数运行结束形参即销毁因此 str 仍为 NULL。给形参 p 分配的内存空间没有free释放当函数运行结束后形参 p 是临时变量会销毁且形参p的改变不会影响实参 str即分配的这份内存空间无法找到了后续也无法对分配的这个空间进行释放就会发生内存泄漏的问题。str 为 NULL当其传入 strcpy 函数内部时strcpy 函数对其进行解引用操作即是对 NULL 进行解引用造成非法访问因此程序将会崩溃。
【下面来看修改后的代码】
void GetMemory(char** p)
{*p (char*)malloc(100);
}
void Test()
{char* str NULL;GetMemory(str);strcpy(str, hello world);printf(str);free(str);str NULL;
}
int main()
{Test();return 0;
}在这个修改后的代码中我们做了以下修改 将GetMemory函数的参数类型改为char**这样我们可以传递一个指向字符指针的指针。这样就能通过解引用操作修改 str 指向的地址了在GetMemory函数中使用*p (char*)malloc(100);为p指向的内存分配空间。在Test函数中使用GetMemory(str);为str分配内存。在Test函数中使用free(str);释放str指向的内存。并将 str 置为 NULL。 6.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;
}【解析】 在GetMemory函数中char p[] hello world;定义了一个局部数组而不是动态分配的内存。这意味着当函数返回时数组将被销毁因此返回的指针将指向无效的内存地址。 在Test函数中char* str NULL;定义了一个空指针然后将其赋值为GetMemory()的返回值。然而由于GetMemory函数返回的是局部数组的地址所以这个赋值操作实际上是将指针指向了无效的内存地址。 在printf(str);语句中尝试使用printf函数打印一个字符串即 hello world 。由于str是一个空指针这将导致未定义的行为所以会打印乱码。
6.3 题目3 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;
}【解析】
因为将 str 指针的地址传入 GetMemory 函数中即传址于是函数内部的形参 p 解引用后便与 str 在内存中指向同一块空间使得函数内部的变量与外部的变量建立的联系。在 GetMemeroy 函数malloc 申请了一块空间之后赋值给了 *p即赋值给了 str因此str 此时指向了内存当中一块连续的空间。然而这段代码还是存在着内存泄漏的问题需要 free 释放 str 指向的空间并把 str 置为 NULL。
6.4 题目4 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;
}【解析】
函数 malloc 申请了一块空间并让指针 str 指向了这块空间strcpy 函数将字符串 hello ,拷贝到了这块空间中之后对 str 指向的空间进行释放但没有对 str 进行置空操作。因为 str 不为空仍指向内存空间中的一块随机区域在进入到 if 语句内部之后strcpy 将字符串world 拷贝到了 str 指向的内存空间中。这段代码似乎没有什么问题但我们还是要说这已经造成了非法访问原因在于 free 把 str 指向的空间释放后尽管 str 仍然保留着原来空间的地址但是我们对这块空间不再具有访问权限 此时再进行strcpy 操作就属于非法访问。 小结
动态内存管理是指在程序运行过程中根据需要动态地分配和释放内存空间。在C语言中可以使用 malloc、calloc、realloc和free 等函数来实现动态内存管理。柔性数组Flexible Array Member是C99标准引入的一个特性它允许结构体中最后一个元素的大小可以不指定从而使得结构体的长度可以是可变的。柔性数组成员必须是结构体中的最后一个成员且其前面必须有一个固定大小的整数类型成员。
今天的分享就到这里了后续我也会继续给大家带来自己学习编程路上的各种知识点如果觉得我的文章有帮助的话欢迎大家点赞关注支持