31、指针进阶3:指针与内存管理

厨子大约 5 分钟C语言基础程序程序厨

在C语言中,指针是访问内存的重要工具,而内存管理则是确保程序稳定运行和高效利用资源的关键。

动态内存分配与释放

在C语言中,除了使用静态分配的数组和变量外(数据段),还可以通过动态内存分配函数(如malloccallocreallocfree)在程序运行时根据需要分配和释放内存(堆区)。

动态内存分配函数

  • malloc(size_t size): 分配指定字节数的内存,并返回一个指向该内存的指针。如果分配失败,返回NULL
  • calloc(size_t num, size_t size): 分配一个包含num个元素的数组,每个元素的大小为size字节,并自动将分配的内存初始化为零。如果分配失败,返回NULL
  • realloc(void *ptr, size_t size): 调整之前调用malloccalloc分配的内存块的大小。ptr是指向要调整大小的内存块的指针,size是新的大小。如果调整成功,返回指向新内存块的指针(可能与ptr相同,也可能不同)。如果失败,返回NULL,并且原内存块保持不变。
  • free(void *ptr): 释放之前调用malloccallocrealloc分配的内存。ptr是指向要释放的内存块的指针。

为什么malloc的时候传递字节数,而free的时候却不需要传递字节数呢?会不会释放多了或者释放少了?

示例代码

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

int main() {
    int *arr = (int *)malloc(5 * sizeof(int)); // 分配一个包含5个整数的数组
    if (arr == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        return 1;
    }

    // 使用数组
    for (int i = 0; i < 5; i++) {
        arr[i] = i * 2;
    }

    // 打印数组内容
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    // 释放内存
    free(arr);

    return 0;
}

推荐阅读:内存管理open in new window

内存泄漏

内存****泄漏是指程序在动态分配内存后,没有正确地释放这些内存,导致这些内存无法被其他程序或后续操作使用。内存泄漏会导致系统性能下降,甚至可能导致系统崩溃。

推荐几个调试内存泄漏的方法:

  • 使用工具:如Asan或者Valgrind(在Linux上)等内存检查工具可以帮助识别内存泄漏和其他内存问题。
  • 手动检查:在代码中添加适当的free调用,确保每个malloccallocrealloc都有对应的free
  • 编写代码时养成良好的习惯:在分配内存后立即检查返回值是否为NULL,并在使用完内存后立即释放,特别是if-else分支的处理上,尤其需要注意要释放内存。(后续你使用C++时,可以使用更方便的内存管理编码技巧

悬挂指针与野指针问题

悬挂指针是指指向已经被释放的内存的指针。悬挂指针通常发生在释放内存后仍然保留该内存地址的指针变量上。

危害:如果尝试通过悬挂指针访问内存,将导致未定义行为,可能导致程序崩溃或数据损坏。

野指针是指未初始化或未正确赋值的指针。野指针可能指向任意内存地址,包括无效的内存区域或受保护的内存区域。

识别与防范

  • 初始化指针:在声明指针时立即将其初始化为NULL或指向有效的内存地址。
  • 避免悬挂指针:在释放内存后将指针设置为NULL
  • 检查指针的有效性:在访问指针指向的内存之前,检查指针是否为NULL

示例代码(避免悬挂指针)

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

int main() {
    int *ptr = (int *)malloc(sizeof(int));
    if (ptr == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        return 1;
    }

    *ptr = 42;
    printf("Value: %d\n", *ptr);

    // 释放内存并将指针设置为NULL
    free(ptr);
    ptr = NULL; // 如果不设置NULL,就会出现悬挂指针

    // 避免使用悬挂指针
    // if (ptr != NULL) { // 这行代码是多余的,因为ptr已经被设置为NULL
    //     printf("Avoiding dangling pointer\n");
    // }

    // 尝试访问悬挂指针将导致未定义行为(已注释掉)
    // printf("Value after free: %d\n", *ptr); // 不要这样做!

    return 0;
}

内存对齐与结构体布局

内存对齐是指编译器在分配结构体成员时,为了优化内存访问速度和数据一致性,将成员按一定的规则对齐到特定的内存地址上。

原因:某些硬件平台在访问未对齐的内存时会导致性能下降或引发异常。

结构体布局的优化与调整

我们可以通过以下策略对结构体布局进行优化和调整:

  • 使用#pragma pack指令指定结构体以几字节对齐。
  • 通过调整成员变量的顺序来优化结构体布局,以减少内存填充(空洞)(padding)。
  • 使用sizeof运算符和offsetof宏(在stddef.h中)来检查结构体的大小和成员偏移量。

示例代码(检查结构体布局)

#include <stdio.h>
#include <stddef.h>

struct A {
    char a;
    int b;
    short c;
};

struct B {
    char a;
    short c;
    int b;
};

int main() {
    printf("Size of struct A: %zu\n", sizeof(struct A));
    printf("Offset of a in struct A: %zu\n", offsetof(struct A, a));
    printf("Offset of b in struct A: %zu\n", offsetof(struct A, b));
    printf("Offset of c in struct A: %zu\n", offsetof(struct A, c));

    printf("Size of struct B: %zu\n", sizeof(struct B));
    printf("Offset of a in struct B: %zu\n", offsetof(struct B, a));
    printf("Offset of c in struct B: %zu\n", offsetof(struct B, c));
    printf("Offset of b in struct B: %zu\n", offsetof(struct B, b));

    return 0;
}

输出结果(https://godbolt.org/z/zc4TeEnab):open in new window

Size of struct A: 12
Offset of a in struct A: 0
Offset of b in struct A: 4
Offset of c in struct A: 8
Size of struct B: 8
Offset of a in struct B: 0
Offset of c in struct B: 2
Offset of b in struct B: 4

通过比较struct Astruct B的大小和成员偏移量,可以看到不同的成员顺序会导致不同的内存布局和填充。在实际编程中,应根据需要优化结构体布局以减少内存占用和提高性能(很重要)