当前位置:   article > 正文

C语言——内存管理

C语言——内存管理

目录

前言

一、内存分类

1. 栈区(Stack)

2. 堆区(Heap)

3. 数据段(Data Segment)

4. 代码段(Code Segment)

二、内存分配方式

1、静态内存分配

2、栈内分配

3、动态内存分配

(1)如何使用malloc 函数:

 (2)内存释放

 (3)内存泄漏

三、常见内存错误——野指针

1、未初始化的指针

2、 指针释放后未置空

3、局部变量指针逃逸

4、指针运算错误

完结



前言

        C语言中,内存管理需要对静态和动态内存分配,静态分配在编译时确定,而动态分配(如malloc, calloc, realloc)则在运行时进行,需程手动管理,包括适时释放(用free)以避免内存泄漏。同时,了解栈、堆、数据区、代码区等内存区域的特性和用途,对于有效管理内存至关重要。


一、内存分类

        可将内存简单分为:栈区、堆区、静态区,其中静态区包含数据段、代码段的内容,主要存储常量、字符串常量等只读数据、已初始化的全局变量和静态变量以及未初始化的全局变量和静态变量。

        堆栈主要指的是栈,而不是堆。

1. 栈区(Stack

定义:栈区用于存储函数的局部变量、函数参数和返回地址。栈区内存由系统自动分配和释放,具有后进先出(LIFO)的特性。

特点:1、分配效率高,但空间有限;

           2、编译器自动管理,无需程序员手动释放。

用于保存局部变量。栈上的内容只在函数的范围内存在,当函数运行结束,这些内容 也会自动被销毁。

2. 堆区(Heap)

定义:堆区用于动态分配内存,即程序运行时使用malloc、new等函数分配的内存。

特点:1、可以动态分配和释放大块内存。

           2、内存由程序员手动管理,需要手动释放(使用free函数)。

           3、内存分配效率相对较低,但空间较大。

其生命周期由 free 或 delete 决定。 在没有释放之前一直存在,直到程序结束。

3. 数据段(Data Segment)

数据段又可以进一步细分为:

1、已初始化数据段:存储已初始化的全局变量和静态变量(static)。这些变量在程序加载时分配内存,并在程序结束时释放。

2、未初始化数据段(BSS段):存储未初始化的全局变量和静态变量。BSS段在程序启动时自动初始化为零或空指针,但它在程序加载时并不占用磁盘空间(因为其内容默认为零)。

4. 代码段(Code Segment)

定义:代码段也称为文本段,存储程序的机器指令。

特点:1、通常是只读的,以防止程序意外修改指令。

           2、包含常量字符串等只读数据。

函数定义和字符串常量存储在代码段。

二、内存分配方式

1、静态内存分配

        静态内存分配是在程序编译时进行的,它将内存分配给全局变量和静态变量,存在周期最长。

优点:内存分配和释放的效率高。

缺点:内存使用不灵活,无法根据需要动态调整内存大小。

  1. char UART_RxBUF[100] = {0};//已初始化全局变量
  2. char flag;//未初始化全局变量
  3. static int count;//静态全局变量,只能在本文件内使用
  4. void fun()
  5. {
  6. static int i;//静态变量
  7. //fun主体
  8. }

2、栈内分配

        栈内存分配是在程序运行时进行的,它将内存分配给函数内部的局部变量。

优点:内存管理简单,不需要程序员手动释放。

缺点:内存空间有限,不适合分配大内存,且存在栈溢出的风险。

  1. void fun()
  2. {
  3. int i = 0;
  4. char buf[100] = {0};//函数运行结束,就会释放
  5. //fun主体
  6. }

3、动态内存分配

        动态内存分配是在程序运行时根据需要进行的内存分配。

优点:内存使用灵活,可以根据需要动态调整内存大小。

缺点:内存管理复杂,需要程序员手动分配和释放,容易出现内存泄漏等问题。

        常用的动态内存分配函数包括malloc()、calloc()realloc(),分别用于分配内存、分配并初始化为0的内存、以及重新调整已分配内存的大小。

(1)如何使用malloc 函数:

        其原型为:

(void *)malloc(int size)

 malloc 函数的返回值是一个 void 类型的指针,参数为 int 类型数据,即申请分配的内存 大小,单位是 byte。内存分配成功之后,malloc 函数返回这块内存的首地址。你需要一个指 针来接收这个地址。但是由于函数的返回值是 void *类型的,所以必须强制转换成你所接收 的类型。也就是说,这块内存将要用来存储什么类型的数据。比如:

  1. int *ptr = (int*)malloc(10 * sizeof(int));
  2. //分配了10个整数(int)的内存空间

 在堆上分配了 10 个int类型的内存(若在编译器中int占四个字节,那么分配内大小为40个字节),返回这块内存的首地址,把地址强制转换成 int *类型后赋给 int *类型的指针变量 ptr。同时告诉我们这块内存将用来存储 int类型的数据。也就是说只能通过指针变量 ptr 来操作这块内存。这块内存本身并没有名字,对它的访问是匿名访问。

        上面就是使用 malloc 函数成功分配一块内存的过程。但是,每次你都能分配成功吗? 不一定使用 malloc 函数同样要注意:如果所申请的内存块大于目前堆上剩余内存块,则内存分配会失败,函数返回 NULL。

注意:这里说的“堆上剩余内存块”不是所有剩余内存块之和,因 为 malloc 函数申请的是连续的一块内存,所以只要堆上剩余的、连续的内存块没有能满足所申请的内存大小就会申请失败。

既然 malloc 函数申请内存有不成功的可能,那我们在使用指向这块内存的指针时,必须用 if(NULL != p)语句来验证内存确实分配成功了。

  1. int *ptr = (int*)malloc(10 * sizeof(int));
  2. //分配了10个整数(int)的内存空间
  3. if(ptr != NULL)
  4. {
  5. printf("内存申请成功!\n");
  6. }

        如果用 malloc 函数申请 0 字节内存时,函数并 不返回 NULL,而是返回一个正常的内存地址。但是你却无法使用这块大小为 0 的内存。这时候 if(NULL != p)语句校验将不起作用。

 (2)内存释放

        既然有分配,那就必须有释放。在C语言中,使用动态内存分配后,程序员需要负责在适当的时候释放这些内存,不然的话,有限的内存总会用光,而没有释放的内存却在空闲,导致内存泄漏。与 malloc 对应的就是 free 函数了。free 函数只有一个参数,就是所要释放的内存块的首地址,该函数只能用于释放通过malloc()、calloc()或realloc()等函数分配的内存。比如上例:

  1. int *ptr = (int*)malloc(10 * sizeof(int));
  2. if(ptr != NULL)
  3. {
  4. printf("内存申请成功!\n");
  5. }
  6. free(ptr);
  7. ptr = NULL;

         切记,申请次数和释放次数要对应。malloc 两次只 free 一次会内存泄漏;malloc 一次 free 两次肯定会出错。也就是说,在程序 中 malloc 的使用次数一定要和 free 相等,否则必有错误。虽然使用 free 函数释放了内存,但指针变量 p 本身保存的地址并没有改变,就需要重新把 p 的值变为 NULL:否则这个指针就成为了“野指针”,也有书叫“悬 垂指针”。这是很危险的,而且也是经常出错的地方。所以一定要记住一条:free 完之后, 一定要给指针置 NULL。

 (3)内存泄漏

        内存泄漏(Memory Leak)是指程序中已分配的内存由于某种原因未被释放或无法释放,导致该内存块持续被占用,无法再被程序或系统用于其他目的。随着内存泄漏的不断积累,可用内存逐渐减少,最终可能导致程序运行速度变慢、响应迟缓,甚至崩溃。在程序中动态分配的内存(如使用 malloc、new 等函数或操作符)在不再需要时,应当通过相应的释放函数(如 free、delete)进行释放。如果忘记释放,就会导致内存泄漏。

三、常见内存错误——野指针

        野指针是一个指针变量,所指向的地址是未知的、随机的、不正确的或没有明确限制的,以及那些已经被释放的内存地址。这种指针在尝试访问或修改其所指向的内存时,会导致不可预测的行为,甚至程序崩溃。野指针的存在是编程中的一个严重问题,因为它可能导致程序崩溃、数据损坏或其他不可预期的行为。在严重的情况下,野指针还可能被恶意利用,造成安全漏洞。

1、未初始化的指针

        在C或C++中,声明一个指针变量时,如果没有立即为其分配内存或初始化为NULL,它将包含一个随机的地址。尝试访问这个随机地址会导致不可预知的后果。如:

  1. #include <stdio.h>
  2. struct student
  3. {
  4. char *name;
  5. int score;
  6. }stu;
  7. int main()
  8. {
  9. strcpy(stu.name,"Jimy");
  10. return 0;
  11. }
  12. /*这里定义了结构体变量 stu,但是他没想到这个结构体内部 char *name
  13. 这成员在定义结构体变量 stu 时,只是给 name 这个指针变量本身分配了
  14. 4 个字节。name 指针并没有指向一个合法的地址,这时候其内部存的只是
  15. 一些乱码。所以在调用 strcpy 函数时,会将字符串"Jimy"往乱码所指的内
  16. 存上拷贝,而这块内存 name 指针根本就无权访问,导致出错。解决的办法
  17. 是为 name 指针 malloc 一块空间。*/

2、 指针释放后未置空

        使用free()delete等函数释放了指针指向的内存后,如果不将指针置为NULL,那么这个指针就变成了野指针。在后续的代码中,如果错误地尝试通过这个指针访问内存,将会导致不可预测的行为。

  1. int *ptr = (int*)malloc(10 * sizeof(int));
  2. if(ptr != NULL) printf("内存申请成功!\n");
  3. free(ptr);
  4. // ptr = NULL;
  5. *ptr = 100;
  6. *(ptr+1) = 200;
  7. //ptr已经被释放,错误操作

3、局部变量指针逃逸

        当函数返回时,其栈上的局部变量将不再有效。如果指针仍然指向这些局部变量,它们将成为野指针。如:

  1. #include <stdio.h>
  2. int* fun()
  3. {
  4. int value = 10; // 局部变量
  5. return &value; // 返回局部变量的地址
  6. }
  7. int main()
  8. {
  9. int* ptr = fun(); // ptr 指向了 fun函数中的局部变量 value 的地址
  10. printf("%d\n", *ptr); // 这里可能打印出 10,但这是未定义行为
  11. // 当 fun函数执行完毕后,value 所占用的内存已经被释放
  12. // 此时 ptr 是一个野指针,访问它会导致未定义行为
  13. // 但在某些情况下(比如没有立即重用该内存区域),它可能看起来还在工作
  14. return 0;
  15. }

 正确方式:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. // 函数声明,返回一个指向int的指针
  4. int* createInteger(int value) {
  5. // 在堆上分配内存来存储一个int值
  6. int* ptr = (int*)malloc(sizeof(int));
  7. if (ptr == NULL) {
  8. // 如果malloc失败,返回NULL
  9. return NULL;
  10. }
  11. *ptr = value; // 初始化内存中的值
  12. return ptr; // 返回指向新分配内存的指针
  13. }
  14. // 注意:在C语言中,我们通常不编写专门的函数来释放内存,因为这通常是调用者的责任
  15. int main() {
  16. int* myInteger = createInteger(42); // 调用函数,分配内存并初始化
  17. if (myInteger != NULL) {
  18. // 检查指针是否为NULL,以避免解引用空指针
  19. printf("The value is: %d\n", *myInteger); // 使用指针访问值
  20. // ... 在这里可以使用myInteger做一些事情 ...
  21. // 释放之前分配的内存
  22. free(myInteger);
  23. // 将指针置为NULL,避免成为野指针(这是一个好习惯)
  24. myInteger = NULL;
  25. } else {
  26. // 处理内存分配失败的情况
  27. printf("Memory allocation failed!\n");
  28. }
  29. return 0;
  30. }

4、指针运算错误

        对指针进行算术运算时,如果运算后的指针指向了未知或无效的内存区域,也会形成野指针。如:

  1. #include <stdio.h>
  2. int main() {
  3. int arr[5] = {1, 2, 3, 4, 5}; // 定义一个有5个元素的数组
  4. int *ptr = arr; // 指针ptr指向数组的第一个元素
  5. // 正确访问数组元素
  6. for (int i = 0; i < 5; i++) {
  7. printf("%d ", *(ptr + i)); // 输出1 2 3 4 5
  8. }
  9. // 指针运算错误:尝试访问数组之外的内存
  10. ptr += 5; // ptr现在指向arr[5],但实际上arr[5]不存在,这是越界
  11. printf("%d\n", *ptr); // 尝试解引用ptr,这是未定义行为
  12. // 在某些情况下,上面的代码可能不会立即崩溃,但会导致不可预测的结果
  13. // 因为ptr现在指向了一个未定义的内存位置
  14. return 0;
  15. }

完结

有误之处望指正!!!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/煮酒与君饮/article/detail/876698
推荐阅读
相关标签
  

闽ICP备14008679号