C语言学习(五)内存管理

C语言中内存管理的概念:

        内存管理涉及动态分配、使用和释放内存资源的过程,c语言运行程序员编程控制程序的内存使用。

动态内存分配:

        变量和数组都是内存的别名,如何分配这些内存由编译器在编译期间决定;例如定义数组时必须指定数组的长度;

        malloc函数:void* malloc(size_t size)向系统申请分配size字节的内存空间,这段内存是连续的,不带有任何信息。
        free函数:void free(void* pointer)用于将动态内存归还,释放pointer指向的内存块,释放之后,这段内存将不可再被访问。
        calloc函数:void *calloc(size_t num, size_t size) 用于分配内存并且初始化为零。它分配一个包含num个元素的数组,每个元素的大小为size字节。
        realloc函数:void *realloc(void *ptr, size_t size) 用于重新分配之前分配的内存块,可以扩大和缩小内存块的大小,原内存块的地址是ptr指针指向的。
示例:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *arr;
    arr = (int *)malloc(5 * sizeof(int)); // 分配5个整数的内存
    if (arr == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    }

    // 使用分配的内存
    for (int i = 0; i < 5; i++) {
        arr[i] = i + 1;
    }

    // 重新分配内存
    arr = (int *)realloc(arr, 10 * sizeof(int)); // 扩大到10个整数的内存
    if (arr == NULL) {
        printf("Memory reallocation failed\n");
        return 1;
    }

    // 使用重新分配的内存
    for (int i = 5; i < 10; i++) {
        arr[i] = i + 1;
    }

    for (int i = 0; i < 10; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    // 释放内存
    free(arr);

    return 0;
}

calloc函数示例: 

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *arr;
    arr = (int *)calloc(5, sizeof(int)); // 分配并初始化5个整数的内存
    if (arr == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    }

    // 使用分配的内存
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr[i]); // 所有值都应为0
    }
    printf("\n");

    // 释放内存
    free(arr);

    return 0;
}

栈:

        栈是一种特定的内存区域,用于管理函数调用和局部变量。栈是后进后出的数据结构,最后被压入栈的内容会被最先弹出。

栈的优点:

  • 自动内存管理:简化了内存分配和释放过程,减少了内存泄漏的风险。
  • 快速访问:栈的内存访问速度快,因为它是线性存储结构,且由CPU直接支持。
  • 递归支持:方便管理递归函数调用。

栈之于函数:
        当一个函数被调用的时候,系统会在栈中创建一个栈帧来保存该函数调用的信息。这个栈帧通常会包含以下信息:
返回地址:函数调用结束后,程序需要返回到调用该函数的位置,返回地址就是这个位置的地址,这个过程一般称为现场保护;
参数:保存传递给函数的参数;
局部变量:在函数体内部定义创建的变量,称为局部变量;
保存的寄存器:某些情况下,为了恢复调用之前的状态,需要保存一些寄存器的值。

下列是一个函数调用如何使用栈的示例:

#include <stdio.h>

void functionB(int b) {
    int y = b * 2;
    printf("Function B: y = %d\n", y);
}

void functionA(int a) {
    int x = a + 5;
    printf("Function A: x = %d\n", x);
    functionB(x);
}

int main() {
    int n = 10;
    functionA(n);
    return 0;
}
栈操作过程
  1. main函数执行
    • int n = 10;:在栈上为变量n分配内存,值为10。
  2. 调用functionA
    • 将参数n(值为10)压入栈中。
    • functionA的返回地址压入栈中。
    • 跳转到functionA的入口地址,开始执行functionA
  3. functionA的栈帧
    • 在栈上分配x的内存,值为15(10 + 5)。
    • 打印x的值。
    • 调用functionB
  4. 调用functionB
    • 将参数x(值为15)压入栈中。
    • functionB的返回地址压入栈中。
    • 跳转到functionB的入口地址,开始执行functionB
  5. functionB的栈帧
    • 在栈上分配y的内存,值为30(15 * 2)。
    • 打印y的值。
    • 函数返回,销毁functionB的栈帧。
  6. 返回functionA
    • 恢复functionA的返回地址。
    • 函数返回,销毁functionA的栈帧。
  7. 返回main
    • 恢复main的执行,继续执行剩余代码,最终程序结束。


 堆:

        堆是程序中用于动态分配内存的一块区域。堆与栈的不同之处在于,堆上面的内存分配和释放是由程序员手动管理的,结合malloccallocrealloc和free 这些函数来手动分配与释放。而栈的内存的开辟与释放是由系统主导的。
        堆是一块巨大的内存空间,可以由程序自由使用;堆中被程序申请使用的内存在程序主动释放前将一直有效。因此需要手动释放,防止内存泄漏。

重要概念
        内存泄漏:如果动态分配的内存没有被释放,就会导致内存泄漏。内存泄漏会导致程序占用的内存越来越多,最终可能导致程序奔溃或者系统变慢。
        内存碎片:多次分配和释放不同大小的内存块可能导致内存碎片化。碎片化会降低内存分配效率,可能导致分配大块内存失败。
        空指针检查:在使用动态内存分配函数时,应该检查返回的指针是否为空。动态内存分配函数例如malloc函数等,在分配内存时,若分配内存失败,会返回一个空指针NULL,因此在使用动态内存分配函数时,应该坚持返回的指针是否为NULL,以确保内存成功分配。

系统堆对空间的管理方式:
        1、空闲列表管理:空闲列表管理是最常见的一种堆内存管理方式。空闲列表是一种数据结构,用于跟踪所有未使用的内存块。每当程序请求分配内存时,系统会从空闲列表中找到一个合适的内存块并分配出去。释放内存时,系统会将该内存块归还到空闲列表中。
        空闲列表管理的常见管理策略:

  • 首次适配(First Fit):从空闲列表的头部开始搜索,找到第一个大小合适的内存块并分配。
  • 最佳适配(Best Fit):搜索整个空闲列表,找到最小的、但能够满足要求的内存块进行分配。这种方法可以减少内存碎片,但搜索时间较长。
  • 最差适配(Worst Fit):搜索整个空闲列表,找到最大的内存块进行分配。这种方法的目的是尽量保留大的内存块,但容易产生碎片。

静态存储区:

        静态存储区也称为全局数据区或者静态数据区,是系统用来存储静态分配的变量的一块内存区域。主要存储的变量是:全局变量,静态变量,常量。
        静态存储区随着程序的运行而分配空间,直到程序运行结束;在程序的编译阶段,静态存储区的大小就已经确定;静态存储区是用来保护程序中的全局变量和静态变量的。而且静态存储区的信息最终都会保存到可执行程序中。

示例:

#include <stdio.h>

// 全局变量,位于静态存储区
int globalVar = 10;
static int staticGlobalVar = 20;

void function() {
    // 静态局部变量,位于静态存储区
    static int staticLocalVar = 30;
    staticLocalVar++;
    printf("Static Local Var: %d\n", staticLocalVar);
}

int main() {
    // 常量字符串,位于静态存储区
    const char *str = "Hello, World!";
    
    printf("Global Var: %d\n", globalVar);
    printf("Static Global Var: %d\n", staticGlobalVar);
    printf("String Literal: %s\n", str);

    function();
    function();

    return 0;
}
  • globalVar:这是一个全局变量,存储在静态存储区。
  • staticGlobalVar:这是一个静态全局变量,存储在静态存储区,且只在当前文件内可见。
  • staticLocalVar:这是一个静态局部变量,存储在静态存储区,但其作用域仅限于function函数。即使函数调用结束,它的值也会被保留,在下一次调用时可以继续使用。
  • str:这是一个指向常量字符串的指针,常量字符串存储在静态存储区。

总结:栈、堆和静态数据区
1、栈区主要用于函数调用的使用;
2、堆区主要是用于内存的动态申请和归还;
3、静态存储区用于保存全局变量和静态变量。


 程序的内存分布:

        一个典型的程序文件,在内存中的布局一般分为以下几个部分:

  1. 代码段(Text Segment)
  2. 数据段(Data Segment)
  3. BSS段(BSS Segment)
  4. 堆(Heap)
  5. 栈(Stack)

详细布局

1. 代码段(Text Segment)
  • 内容:包含程序的机器指令,即编译后的代码。
  • 特点:通常是只读的,以防止程序在运行时修改自身的指令。这有助于保护程序的完整性和安全性。
  • 用途:存放程序的可执行代码。
2. 数据段(Data Segment)
  • 内容:包含已初始化的全局变量和静态变量。
  • 特点:在程序开始运行时,这些变量已被赋予初始值。
  • 用途:存放程序中需要在整个运行期间保持已初始化状态的数据。
3. BSS段(BSS Segment)
  • 内容:包含未初始化的全局变量和静态变量。
  • 特点:在程序开始运行时,这些变量被自动初始化为零。
  • 用途:存放程序中需要在整个运行期间保持未初始化状态的数据。
4. 堆(Heap)
  • 内容:动态分配的内存区,用于在程序运行时分配和释放内存。
  • 特点:堆的大小不固定,可以在运行时通过函数(如malloccallocreallocfree)进行动态调整。
  • 用途:存放程序在运行时需要动态分配的内存数据。
5. 栈(Stack)
  • 内容:用于存储函数调用的局部变量、参数和返回地址。
  • 特点:栈是后进先出(LIFO)的数据结构,函数调用时会分配栈帧,函数返回时会释放相应的栈帧。
  • 用途:管理函数调用、参数传递和局部变量。

        一个完整代码加载到内存之后,其内存布局示意图如下:


 野指针:

        野指针指的是一个指向无效内存位置的指针。野指针通常是因为指针变量中保存的值不是一个合法的内存地址而造成的。重点区别:野指针不是null指针,是指向不可以内存的指针。这种情况通常发生在指针被释放或未初始化后继续使用的情况下。野指针可能导致程序崩溃、不可预测的行为或安全漏洞。

        在程序中无法判断一个指针是不是野指针。

野指针的产生原因:

        1、未初始化的指针
        指针变量声明后未被初始化,就指向一个位置的内存地址,也就是局部指针变量没有被初始化,有下列示例说明。

int *ptr; // ptr未初始化
*ptr = 10; // 未定义行为

因此指针要初始化,常见的几种初始化方法如下:
a、将指针初始化为NULL,确保在指针指向有效地址之前不使用它。

int *ptr = NULL;
if (ptr != NULL) {
    *ptr = 10; // 只有在ptr指向有效地址时才使用
}

b、使用动态内存分配函数(如malloc)为指针分配内存

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr = (int *)malloc(sizeof(int)); // 为指针分配内存
    if (ptr == NULL) {
        printf("Memory allocation failed\n");
        return 1; // 内存分配失败时退出
    }
    *ptr = 10; // 使用分配的内存
    printf("Value: %d\n", *ptr);

    free(ptr); // 释放内存
    ptr = NULL; // 将指针置为NULL以避免野指针
    return 0;
}

c、将指针指向一个已经存在的变量;

#include <stdio.h>

int main() {
    int value = 10;
    int *ptr = &value; // 将指针指向现有变量
    printf("Value: %d\n", *ptr);
    return 0;
}

        2、指针释放后仍然被使用;动态分配的内存被释放后,指针仍然指向原来的内存位置,但该内存可能已被重新分配或变为无效。
示例:

int *ptr = (int *)malloc(sizeof(int));
*ptr = 10;
free(ptr);
*ptr = 20; // 未定义行为

        避免这种错误的方法:在释放动态分配的内存之后,将指针设置为NULL,以避免使用以释放的内存。示例如下: 

int *ptr = (int *)malloc(sizeof(int));
if (ptr != NULL) {
    *ptr = 10;
    free(ptr);
    ptr = NULL; // 将指针置为NULL
}

        3、超出变量作用域,即指针所指向的变量在指针之前被销毁;常见于 一个指向局部变量的指针在函数返回后继续使用,该局部变量的内存已无效。
示例:

int* getPointer() {
    int localVar = 10;
    return &localVar;
}

int main() {
    int *ptr = getPointer();
    // localVar在getPointer返回后无效
    printf("%d\n", *ptr); // 未定义行为
    return 0;
}

         避免这种错误的方法:在函数中,不能返回局部变量的地址,可以使用动态内存分配的内存或者全局变量。示例:

int* getPointer() {
    int *ptr = (int *)malloc(sizeof(int));
    if (ptr != NULL) {
        *ptr = 10;
    }
    return ptr;
}

int main() {
    int *ptr = getPointer();
    if (ptr != NULL) {
        printf("%d\n", *ptr);
        free(ptr);
    }
    return 0;
}

关于内存管理的编程经验:

        1、用malloc函数申请了内存之后,应该立即检查返回的指针值是否为NULL,防止使用值为NULL的指针,造成错误。

        2、牢记数组的长度,防止数组越界操作,考虑使用柔性数组。

        3、动态申请操作必须和释放操作相匹配,防止内存泄漏和多次释放。而且尽量遵守在哪个函数申请,就在哪个函数释放的原则,避免多次释放。对应的一次申请,一次释放。

        4、每次使用malloc,calloc或者realloc函数分配内存之后,确保在适当的时候调用free释放内存,并且在释放后将指针设置为NULL。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值