返回

C语言中realloc(ptr, 0)的行为详解与内存泄漏分析

Linux

realloc 传入大小为 0 的参数时,发生了什么?

问题的提出

realloc 函数用于调整之前分配的内存块的大小。 根据 man 手册,realloc 的第一个参数 ptr除非是 NULL,否则必须是之前调用 malloc()calloc()realloc() 返回的。 为什么要这样规定?

看看下面这段代码,用 gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0 编译,不带任何选项(标志):

#include <stdlib.h>

int main () {
    int *p = malloc(0);
    p = realloc(p, 0);
    return 0;
}

这段代码能正常运行。用 valgrind 检查内存,显示如下:

valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./test
==185872== Memcheck, a memory error detector
==185872== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==185872== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==185872== Command: ./test
==185872==
==185872==
==185872== HEAP SUMMARY:
==185872==     in use at exit: 0 bytes in 0 blocks
==185872==   total heap usage: 1 allocs, 1 frees, 0 bytes allocated
==185872==
==185872== All heap blocks were freed -- no leaks are possible
==185872==
==185872== For lists of detected and suppressed errors, rerun with: -s
==185872== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from

没有内存泄漏。

但是,编译这段代码(添加了 -g 标志):

#include <stdlib.h>

int main () {
    int *p = NULL;
    p = realloc(p, 0);
    return 0;
}

valgrind 输出显示错误(内存泄漏):

valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./test
==186749== Memcheck, a memory error detector
==186749== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==186749== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==186749== Command: ./test
==186749==
==186749==
==186749== HEAP SUMMARY:
==186749==     in use at exit: 0 bytes in 1 blocks
==186749==   total heap usage: 1 allocs, 0 frees, 0 bytes allocated
==186749==
==186749== 0 bytes in 1 blocks are definitely lost in loss record 1 of 1
==186749==    at 0x483B723: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==186749==    by 0x483E017: realloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==186749==    by 0x10916D: main (test1.c:5)
==186749==
==186749== LEAK SUMMARY:
==186749==    definitely lost: 0 bytes in 1 blocks
==186749==    indirectly lost: 0 bytes in 0 blocks
==186749==      possibly lost: 0 bytes in 0 blocks
==186749==    still reachable: 0 bytes in 0 blocks
==186749==         suppressed: 0 bytes in 0 blocks
==186749==
==186749== For lists of detected and suppressed errors, rerun with: -s
==186749== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

明明没有分配任何东西,为什么会发生这种情况?

再用 -std=c11 -g 编译这段代码:

#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#define ASSERT_ERROR_PREFIX "Assertion "
#define ASSERT_ERROR_SUFFIX " failed\n"

#define assert(x, num) { \
    if(!(x)) { \
        write(STDOUT_FILENO, ASSERT_ERROR_PREFIX, strlen(ASSERT_ERROR_PREFIX)); \
        write(STDOUT_FILENO, num, sizeof(char)); \
        write(STDOUT_FILENO, ASSERT_ERROR_SUFFIX, strlen(ASSERT_ERROR_SUFFIX)); \
    } \
}

int main () {
    int *p = NULL;
    p = realloc(p, 0);
    *p = '1';
    assert((*p == '1'), "1");
    assert((p == NULL), "2");
    return 0;
}

输出:Assertion 2 failed

valgrind 输出:

valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./test
==190041== Memcheck, a memory error detector
==190041== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==190041== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==190041== Command: ./test
==190041==
==190041== Invalid write of size 4
==190041==    at 0x109196: main (test1.c:19)
==190041==  Address 0x4a5f040 is 0 bytes after a block of size 0 alloc'd
==190041==    at 0x483B723: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==190041==    by 0x483E017: realloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==190041==    by 0x10918D: main (test1.c:18)
==190041==
==190041== Invalid read of size 4
==190041==    at 0x1091A0: main (test1.c:20)
==190041==  Address 0x4a5f040 is 0 bytes after a block of size 0 alloc'd
==190041==    at 0x483B723: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==190041==    by 0x483E017: realloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==190041==    by 0x10918D: main (test1.c:18)
==190041==
Assertion 2 failed
==190041==
==190041== HEAP SUMMARY:
==190041==     in use at exit: 0 bytes in 1 blocks
==190041==   total heap usage: 1 allocs, 0 frees, 0 bytes allocated
==190041==
==190041== 0 bytes in 1 blocks are definitely lost in loss record 1 of 1
==190041==    at 0x483B723: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==190041==    by 0x483E017: realloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==190041==    by 0x10918D: main (test1.c:18)
==190041==
==190041== LEAK SUMMARY:
==190041==    definitely lost: 0 bytes in 1 blocks
==190041==    indirectly lost: 0 bytes in 0 blocks
==190041==      possibly lost: 0 bytes in 0 blocks
==190041==    still reachable: 0 bytes in 0 blocks
==190041==         suppressed: 0 bytes in 0 blocks
==190041==
==190041== For lists of detected and suppressed errors, rerun with: -s
==190041== ERROR SUMMARY: 3 errors from 3 contexts (suppressed: 0 from 0)

问题原因分析

问题的根源在于 realloc(ptr, 0) 的行为,这在 C 标准中有些微妙的区别,不同的C标准和编译器实现可能不同。

  1. realloc(ptr, 0)free(ptr) 的关系

    许多情况下,realloc(ptr, 0) 等同于 free(ptr)。 这似乎很合理,将内存块的大小缩小到 0,不就等于释放它吗? 大部分情况下确实如此,这也是为什么第一个示例代码没有报内存泄漏的原因。 释放了那块内存。

  2. realloc(NULL, 0)malloc(0) 的关系

如果 ptrNULLrealloc(NULL, size) 的行为类似于 malloc(size)。 那么,realloc(NULL, 0) 类似于 malloc(0)malloc(0) 的行为在 C 标准中是实现定义的(implementation-defined)。它可以返回 NULL,也可以返回一个特殊的、不应被解引用的指针。许多实现选择后者。

  1. realloc-g 选项
    -g 选项会加入调试信息。 影响了程序的行为。 主要原因是内联优化被禁用。 没有-g, 编译器可能直接将代码优化没,p=realloc(p,0) 会被直接忽略掉。 加了-g 之后不会内联优化。

  2. 0 字节分配
    上面第三个代码, 使用realloc(p,0) 会申请一个大小为0的块。然后给块赋予值,触发valgrind检测出内存错误。 assert((p == NULL), "2"); 会检测失败, 因为它不是空指针, 它指向一个大小为0的内存块。

解决方案与解释

下面分情况讨论如何处理 realloc 传入 0 大小参数的情况,并提供代码示例和安全建议。

1. 释放已分配内存

如果你想释放 ptr 指向的内存,直接使用 free(ptr)。这是最清晰、最安全的方式。不要依赖 realloc(ptr, 0) 来释放内存,尽管许多情况下它能正常工作,但这并非其设计初衷。

#include <stdlib.h>

int main() {
    int *p = malloc(10 * sizeof(int));
    // ... 使用 p ...
    free(p); // 明确释放内存
    p = NULL; // 好习惯:释放后将指针置为 NULL
    return 0;
}

原理: free(ptr) 告诉操作系统,ptr 指向的内存块不再需要,可以回收利用。

安全建议: 释放内存后,将指针设置为 NULL。 这可以防止后续代码意外地使用悬空指针(dangling pointer,指向已释放内存的指针),从而避免难以调试的错误。

2. 特殊情况:malloc(0)realloc(NULL, 0)

如果你确实需要分配 0 字节的内存块(虽然这很少见),请做好心理准备,它的行为可能因平台而异。

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

int main() {
    int *p = malloc(0);
    if (p == NULL) {
        perror("malloc(0) returned NULL");
        // 处理错误...
    } else {
        printf("malloc(0) returned a non-NULL pointer: %p\n", (void*)p);
        //  p = realloc(p, 0); 不要这么做
        free(p);
    }
     int *q = realloc(NULL, 0);
     if (q == NULL)
     {
        perror("realloc(NULL, 0) returned NULL");
     }
     else{
        printf("realloc(NULL, 0) returned a non-NULL pointer: %p\n", (void*)q);
        free(q);
     }
    return 0;
}

原理:

  • malloc(0)realloc(NULL, 0) 的行为是实现定义的。
  • 可能返回 NULL,表示分配失败。
  • 可能返回一个特殊的、唯一的指针值,但你不应该解引用这个指针(即你不应该试图访问它指向的内存)。

安全建议:

  • 始终检查 malloc(0)realloc(NULL, 0) 的返回值是否为 NULL
  • 如果返回值不为 NULL,不要尝试读写它指向的内存。 应该直接free掉。
  • 不要对realloc函数返回的 0 字节的指针执行任何内存访问操作, 不管传入的第一个参数是不是NULL

3. 正确使用 realloc 缩小内存

如果你确实需要使用 realloc 来缩小内存块,但又不希望缩小到 0,请确保新的大小大于 0。

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

int main() {
    int *p = malloc(100 * sizeof(int));
    // ... 使用 p ...

    // 将内存块缩小到 50 个整数
    int *new_p = realloc(p, 50 * sizeof(int));
    if (new_p == NULL) {
        perror("realloc failed");
        // 处理错误,可能仍然可以使用 p
        // ...
    } else {
        p = new_p; // 更新指针
    }

     // 将内存块缩小, 但是避免缩小到0. 确保大小最小为1
    new_p = realloc(p, 1 * sizeof(int)); //可以尝试, 例如sizeof(char)
    if (new_p == NULL) {
        perror("realloc failed");
    } else {
        p = new_p; // 更新指针
    }

    free(p);
    p= NULL;

    return 0;
}

原理:

  • realloc 会尝试在原地缩小内存块。 如果原地缩小不可能(例如,后面紧跟着其他已分配的内存),它会分配一块新的、更小的内存区域,并将原有数据复制过去,然后释放原来的内存块。
  • 如果 realloc 失败(例如,内存不足),它会返回 NULL,但原来的内存块 仍然有效。 所以要检查返回值。

安全建议:

  • 不要假设缩小一定会成功。 检查返回指针。
  • 始终更新指针: realloc 可能会返回一个与原来不同的指针值。 使用旧的指针会导致错误。

进阶使用技巧: 避免过度分配和频繁的 realloc

频繁调用 realloc 会带来性能开销,特别是在涉及到大量内存复制时。 以下技巧可以减少 realloc 的调用次数:

  • 预估大小: 在分配内存时,尽量预估一个合理的大小,避免频繁调整。
  • 倍增策略: 如果你需要动态增加内存,不要每次只增加一点点。 可以采用“倍增”策略,例如每次将容量扩大一倍。 这样可以减少 realloc 的调用次数。
#include <stdlib.h>
#include <stdio.h>

int main() {
    int capacity = 10; // 初始容量
    int size = 0;      // 当前元素数量
    int *arr = malloc(capacity * sizeof(int));

    if (arr == NULL) {
        perror("malloc failed");
        return 1;
    }

    // 模拟添加元素
    for (int i = 0; i < 100; i++) {
        if (size == capacity) {
            // 容量不足,扩大一倍
            capacity *= 2;
            int *new_arr = realloc(arr, capacity * sizeof(int));
            if (new_arr == NULL) {
                perror("realloc failed");
                // 处理错误... 可以选择退出,或者尝试较小的增量
                free(arr);
                return 1;
            }
            arr = new_arr;
        }
        arr[size++] = i;
    }

    // ... 使用 arr ...

    free(arr);
      arr= NULL;

    return 0;
}

总结

对于 realloc(ptr, 0), 最好直接使用free, 清晰且不容易出错。 记住: malloc(0)realloc(NULL, 0) 是实现定义的。 处理它们要特别小心。 不要假设 realloc 一定成功,检查其返回值。