printf() 是不是在 C 中分配内存?

Posted

技术标签:

【中文标题】printf() 是不是在 C 中分配内存?【英文标题】:Does printf() allocate memory in C?printf() 是否在 C 中分配内存? 【发布时间】:2017-02-13 04:07:54 【问题描述】:

这个简单的方法只是创建一个动态大小为 n 的数组,并用值 0 ... n-1 对其进行初始化。它包含一个错误,malloc() 只分配 n 而不是 sizeof(int) * n 字节:

int *make_array(size_t n) 
    int *result = malloc(n);

    for (int i = 0; i < n; ++i) 
        //printf("%d", i);
        result[i] = i;
    

    return result;


int main() 
    int *result = make_array(8);

    for (int i = 0; i < 8; ++i) 
        printf("%d ", result[i]);
    

    free(result);

当您检查输出时,您会看到它会按预期打印一些数字,但最后一个是乱码。但是,一旦我将 printf() 插入循环中,输出就奇怪地正确,即使分配仍然是错误的! 是否存在与 printf() 相关的某种内存分配?

【问题讨论】:

通常,printf() — 或许多其他 &lt;stdio.h&gt; 函数 — 将在第一次需要缓冲区时而不是在创建文件流时分配与 FILE * 关联的缓冲区。因此,标题问题的简洁答案是“是”。 我猜想一开始就调用未定义行为的恶魔,以后会遇到更多未定义行为,您应该不会感到惊讶。 “一旦我将 printf() 插入循环中......”。你到底在哪里插入了额外的printf malloc(8) 返回 8 个字节的内存或返回 NULL,您尝试在其中存储 8 个整数,每个整数(取决于系统)占用 4 个字节。因此 C 不能保证最后 6 个整数会发生什么,因此是未定义的行为。 第二个printf 你提到//printf("%d", i); 你只是在打印i,而不是缓冲区,所以这将按预期工作。 【参考方案1】:

您为数组分配了 8 个字节,但您存储了 8 个int,每个至少有 2 个字节(可能是 4 个),因此您正在写入超过分配内存的末尾。这样做会调用未定义的行为。

当您调用未定义的行为时,任何事情都可能发生。您的程序可能会崩溃,它可能会显示意外的结果,或者它可能看起来正常工作。一个看似不相关的变化可以改变上述哪些动作发生。

修复内存分配,您的代码将按预期工作。

int *result = malloc(sizeof(int) * n);

【讨论】:

However, once I inserted the printf() inside the loop, the output was strangely correct 你应该提到循环中的printf 只是打印i 这是明确定义的行为。 当 CHAR_BIT 至少为 16 时,int 只能是一个字节。 @12431234123412341234123 事实核查:int 由标准保证至少为 16 位(2 字节)。 int 不能是一个字节。如果是,则允许它的编译器不符合标准,不应被视为 C 编译器。 @Braden Best : int 可以是一个字节。 ANSI-C、C99 和 C11 禁止 int 只能是一个字节。 (正如我已经写过的)。 CHAR_BIT 可以是 16,在这种情况下,一个字节是 16 位长,一个 int 只需要一个单字节。 @BradenBest 一个字节被定义为最小的可寻址内存块。在某些架构上,它可能是 9 位或 16 位。如果这就是硬件设置的方式,那么从这个意义上说,你真的不能帮助“浪费”。【参考方案2】:

未指定printf() 在执行其工作的过程中是否分配任何内存。如果任何给定的实现都这样做了,这并不奇怪,但没有理由假设它确实如此。此外,如果一个实现确实如此,那并不能说明不同的实现是否如此。

printf() 位于循环内时,您会看到不同的行为并不会告诉您任何事情。该程序通过超出已分配对象的边界表现出未定义的行为。一旦这样做,所有后续行为都是未定义的。你不能推理未定义的行为,至少不能在 C 语义方面。一旦未定义的行为开始,程序没有 C 语义。这就是“未定义”的意思。

【讨论】:

【参考方案3】:

严格来说,要回答标题中的问题,答案是它取决于实现。一些实现可能会分配内存,而另一些可能不会。

虽然您的代码中存在其他固有问题,我将在下面详细说明。


注意:这原本是我针对这个问题做的一系列cmets。我认为评论太多了,因此将他们移至此答案。


当您检查输出时,您会看到它会按预期打印一些数字,但最后一个是乱码。

我相信在使用分段内存模型的系统上,分配会“四舍五入”到某个大小。 IE。如果您分配 X 字节,您的程序确实将拥有这些 X 字节,但是,在 CPU 注意到您违反边界并发送 SIGSEGV 之前,您也可以(错误地)运行一段时间超过这些 X 字节。

这很可能是您的程序在您的特定配置中没有崩溃的原因。请注意,您分配的 8 个字节将仅涵盖 sizeof (int) 为 4 的系统上的两个整数。其他 6 个整数所需的其他 24 个字节不属于您的数组,因此任何内容都可以写入该空间,并且当您读取从那个空间,如果你的程序没有首先崩溃,你会得到垃圾。

数字 6 很重要。以后记住!

神奇的部分是生成的数组将包含正确的数字,printf 实际上只是再次打印每个数字。但这确实改变了数组。

注意:以下是推测,我还假设您在 64 位系统上使用 glibc。我将添加此内容,因为我觉得它可能会帮助您了解某些可能看似正常工作但实际上不正确的可能原因。

它“神奇地正确”的原因很可能与printf 通过 va_args 接收这些数字有关。 printf 可能正在填充刚刚超过数组物理边界的内存区域(因为 vprintf 正在分配内存来执行打印i 所需的“itoa”操作)。换句话说,那些“正确”的结果实际上只是“看起来正确”的垃圾,但实际上,这正是 RAM 中发生的事情。如果您尝试将int 更改为long,同时保持8 字节分配,您的程序将更有可能崩溃,因为longint 长。

malloc 的 glibc 实现有一个优化,它每次用完堆时都会从内核分配整个页面。这使它更快,因为不是在每次分配时都要求内核提供更多内存,它可以从“池”中获取可用内存,并在第一个“池”填满时创建另一个“池”。

也就是说,就像堆栈一样,malloc 的堆指针来自内存池,往往是连续的(或至少非常接近)。这意味着 printf 对 malloc 的调用可能会出现在您为 int 数组分配的 8 个字节之后。但是,无论它如何工作,关键是无论结果看起来多么“正确”,它们实际上只是垃圾,并且您正在调用未定义的行为,因此无法知道会发生什么,或者是否程序会在不同的情况下执行其他操作,例如崩溃或产生意外行为。


所以我尝试在有和没有 printf 的情况下运行你的程序,但结果都是错误的。

# without printf
$ ./a.out 
0 1 2 3 4 5 1041 0 

无论出于何种原因,没有任何东西干扰了持有 2..5 的内存。但是,有些东西干扰了持有67 的内存。我的猜测是这是 vprintf 的缓冲区,用于创建数字的字符串表示形式。 1041 是文本,0 是空终止符 '\0'。即使它不是 vprintf 的结果,something 正在写入人口和数组打印之间的那个地址。

# with printf
$ ./a.out
*** Error in `./a.out': free(): invalid next size (fast): 0x0000000000be4010 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x77725)[0x7f9e5a720725]
/lib/x86_64-linux-gnu/libc.so.6(+0x7ff4a)[0x7f9e5a728f4a]
/lib/x86_64-linux-gnu/libc.so.6(cfree+0x4c)[0x7f9e5a72cabc]
./a.out[0x400679]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f9e5a6c9830]
./a.out[0x4004e9]
======= Memory map: ========
00400000-00401000 r-xp 00000000 08:02 1573060                            /tmp/a.out
00600000-00601000 r--p 00000000 08:02 1573060                            /tmp/a.out
00601000-00602000 rw-p 00001000 08:02 1573060                            /tmp/a.out
00be4000-00c05000 rw-p 00000000 00:00 0                                  [heap]
7f9e54000000-7f9e54021000 rw-p 00000000 00:00 0 
7f9e54021000-7f9e58000000 ---p 00000000 00:00 0 
7f9e5a493000-7f9e5a4a9000 r-xp 00000000 08:02 7995396                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7f9e5a4a9000-7f9e5a6a8000 ---p 00016000 08:02 7995396                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7f9e5a6a8000-7f9e5a6a9000 rw-p 00015000 08:02 7995396                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7f9e5a6a9000-7f9e5a869000 r-xp 00000000 08:02 7999934                    /lib/x86_64-linux-gnu/libc-2.23.so
7f9e5a869000-7f9e5aa68000 ---p 001c0000 08:02 7999934                    /lib/x86_64-linux-gnu/libc-2.23.so
7f9e5aa68000-7f9e5aa6c000 r--p 001bf000 08:02 7999934                    /lib/x86_64-linux-gnu/libc-2.23.so
7f9e5aa6c000-7f9e5aa6e000 rw-p 001c3000 08:02 7999934                    /lib/x86_64-linux-gnu/libc-2.23.so
7f9e5aa6e000-7f9e5aa72000 rw-p 00000000 00:00 0 
7f9e5aa72000-7f9e5aa98000 r-xp 00000000 08:02 7999123                    /lib/x86_64-linux-gnu/ld-2.23.so
7f9e5ac5e000-7f9e5ac61000 rw-p 00000000 00:00 0 
7f9e5ac94000-7f9e5ac97000 rw-p 00000000 00:00 0 
7f9e5ac97000-7f9e5ac98000 r--p 00025000 08:02 7999123                    /lib/x86_64-linux-gnu/ld-2.23.so
7f9e5ac98000-7f9e5ac99000 rw-p 00026000 08:02 7999123                    /lib/x86_64-linux-gnu/ld-2.23.so
7f9e5ac99000-7f9e5ac9a000 rw-p 00000000 00:00 0 
7ffc30384000-7ffc303a5000 rw-p 00000000 00:00 0                          [stack]
7ffc303c9000-7ffc303cb000 r--p 00000000 00:00 0                          [vvar]
7ffc303cb000-7ffc303cd000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
012345670 1 2 3 4 5 6 7 Aborted

这是有趣的部分。你没有在你的问题中提到你的程序是否崩溃了。但是当我运行它时,它崩溃了。 困难

如果你有可用的 valgrind,检查一下也是一个好主意。 Valgrind 是一个有用的程序,它报告你如何使用你的记忆。这是 valgrind 的输出:

$ valgrind ./a.out
==5991== Memcheck, a memory error detector
==5991== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==5991== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==5991== Command: ./a.out
==5991== 
==5991== Invalid write of size 4
==5991==    at 0x4005F2: make_array (in /tmp/a.out)
==5991==    by 0x40061A: main (in /tmp/a.out)
==5991==  Address 0x5203048 is 0 bytes after a block of size 8 alloc'd
==5991==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==5991==    by 0x4005CD: make_array (in /tmp/a.out)
==5991==    by 0x40061A: main (in /tmp/a.out)
==5991== 
==5991== Invalid read of size 4
==5991==    at 0x40063C: main (in /tmp/a.out)
==5991==  Address 0x5203048 is 0 bytes after a block of size 8 alloc'd
==5991==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==5991==    by 0x4005CD: make_array (in /tmp/a.out)
==5991==    by 0x40061A: main (in /tmp/a.out)
==5991== 
0 1 2 3 4 5 6 7 ==5991== 
==5991== HEAP SUMMARY:
==5991==     in use at exit: 0 bytes in 0 blocks
==5991==   total heap usage: 2 allocs, 2 frees, 1,032 bytes allocated
==5991== 
==5991== All heap blocks were freed -- no leaks are possible
==5991== 
==5991== For counts of detected and suppressed errors, rerun with: -v
==5991== ERROR SUMMARY: 12 errors from 2 contexts (suppressed: 0 from 0)

如您所见,valgrind 报告您有一个invalid write of size 4 和一个invalid read of size 4(4 个字节是我系统上一个 int 的大小)。它还提到您正在读取大小为 0 的块,该块位于大小为 8 的块(您 malloc 的块)之后。这告诉您,您将经过阵列并进入垃圾场。您可能会注意到的另一件事是它从 2 个上下文中生成了 12 个错误。具体来说,在写作环境中有 6 个错误,在阅读环境中有 6 个错误。正是我之前提到的未分配空间量。

这是更正后的代码:

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

int *make_array(size_t n) 
    int *result = malloc(n * sizeof (int)); // Notice the sizeof (int)

    for (int i = 0; i < n; ++i)
        result[i] = i;

    return result;


int main() 
    int *result = make_array(8);

    for (int i = 0; i < 8; ++i)
        printf("%d ", result[i]);

    free(result);
    return 0;

这是 valgrind 的输出:

$ valgrind ./a.out
==9931== Memcheck, a memory error detector
==9931== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==9931== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==9931== Command: ./a.out
==9931== 
0 1 2 3 4 5 6 7 ==9931== 
==9931== HEAP SUMMARY:
==9931==     in use at exit: 0 bytes in 0 blocks
==9931==   total heap usage: 2 allocs, 2 frees, 1,056 bytes allocated
==9931== 
==9931== All heap blocks were freed -- no leaks are possible
==9931== 
==9931== For counts of detected and suppressed errors, rerun with: -v
==9931== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

请注意它没有报告错误并且结果是正确的。

【讨论】:

以上是关于printf() 是不是在 C 中分配内存?的主要内容,如果未能解决你的问题,请参考以下文章

如何在C中的堆栈中分配超过所需的内存?

在函数 C 中分配内存二维数组

如何在 C 中分配 32 字节对齐的内存

如何在 C 中分配和释放对齐的内存

在函数C中分配内存2d数组

C - 在函数中分配矩阵