C语言中realloc(ptr, 0)的行为详解与内存泄漏分析
2025-02-26 20:46:55
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标准和编译器实现可能不同。
-
realloc(ptr, 0)
与free(ptr)
的关系许多情况下,
realloc(ptr, 0)
等同于free(ptr)
。 这似乎很合理,将内存块的大小缩小到 0,不就等于释放它吗? 大部分情况下确实如此,这也是为什么第一个示例代码没有报内存泄漏的原因。 释放了那块内存。 -
realloc(NULL, 0)
与malloc(0)
的关系
如果 ptr
为 NULL
,realloc(NULL, size)
的行为类似于 malloc(size)
。 那么,realloc(NULL, 0)
类似于 malloc(0)
。malloc(0)
的行为在 C 标准中是实现定义的(implementation-defined)。它可以返回 NULL
,也可以返回一个特殊的、不应被解引用的指针。许多实现选择后者。
-
realloc
与-g
选项
-g
选项会加入调试信息。 影响了程序的行为。 主要原因是内联优化被禁用。 没有-g
, 编译器可能直接将代码优化没,p=realloc(p,0)
会被直接忽略掉。 加了-g
之后不会内联优化。 -
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 一定成功,检查其返回值。