"Pointer to pointer to int issue"

"Pointer to pointer to int issue"

今天我试图解决 Here 中的一个测验,当我到达问题 3 时,有以下代码:

#include <stdlib.h>

int main(void){
    int *pInt;
    int **ppInt1;
    int **ppInt2;

    pInt = (int*)malloc(sizeof(int));
    ppInt1 = (int**)malloc(10*sizeof(int*));
    ppInt2 = (int**)malloc(10*sizeof(int*));

    free( pInt );
    free( ppInt1 );
    free( *ppInt2 );
}

问题是:

Choose the correct statement w.r.t. above C program:

A - malloc() for ppInt1 and ppInt2 isn’t correct. It’ll give compile time error.
B - free(*ppInt2) is not correct. It’ll give compile time error.
C - free(*ppInt2) is not correct. It’ll give run time error.
D - No issue with any of the malloc() and free() i.e. no compile/run time error

因为这一行:

free(*ppInt2);

根据我的理解,不会有编译或 运行 时间错误,我决定

free(*ppInt2)

不正确。

但是因为这里没有 compile/run 时间错误,使得答案 BC 错误。

作者说接受的答案是:

D - No issue with any of the malloc() and free() i.e. no compile/run time error.

现在这是我的问题,为什么没有问题,因为这样做:

free( *ppInt2 );

Valgrind 报告:

==9468== Memcheck, a memory error detector
==9468== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==9468== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==9468== Command: ./program
==9468== 
==9468== Conditional jump or move depends on uninitialised value(s)
==9468==    at 0x4C30CF1: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==9468==    by 0x1086C1: main (program.c:14)
==9468==  Uninitialised value was created by a heap allocation
==9468==    at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==9468==    by 0x108696: main (program.c:10)
==9468== 
==9468== 
==9468== HEAP SUMMARY:
==9468==     in use at exit: 80 bytes in 1 blocks
==9468==   total heap usage: 3 allocs, 2 frees, 164 bytes allocated
==9468== 
==9468== 80 bytes in 1 blocks are definitely lost in loss record 1 of 1
==9468==    at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==9468==    by 0x108696: main (program.c:10)
==9468== 
==9468== LEAK SUMMARY:
==9468==    definitely lost: 80 bytes in 1 blocks
==9468==    indirectly lost: 0 bytes in 0 blocks
==9468==      possibly lost: 0 bytes in 0 blocks
==9468==    still reachable: 0 bytes in 0 blocks
==9468==         suppressed: 0 bytes in 0 blocks
==9468== 
==9468== For counts of detected and suppressed errors, rerun with: -v
==9468== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

我认为正确的 free 调用应该是:

free( ppInt2 );

Linux mint 19GCC-8valgrind-3.13.0

上测试

答案C最接近正确。行

free( *ppInt2 );

绝对不正确。该错误不是编译器可以检测到的错误。但它很可能会导致 运行-time 错误。 (但不保证会导致 运行 次错误。更多内容请见下文。)

mallocfree 的规则非常简单:您传递给 free 的每个指针必须与您从上一次调用 malloc 收到的指针完全相同(或 calloc,或 realloc)。在代码中,mallocfree 调用 pIntppInt1 正确遵循此规则。但是对于ppInt2来说,malloc返回的指针赋值给了ppInt2,但是交给free的指针是*ppInt2,[=指向的值22=]。但是由于 *ppInt2 —— 即 ppInt2 指向的值 —— 没有以任何方式初始化,它是一个垃圾值, free 可能会崩溃。最终结果或多或少就像你说的那样

int main()
{
    int *p;
    free(p);     /* WRONG */
}

但是,同样,不能保证一定会崩溃。因此,更正确的答案将被表述为

C' - free(*ppInt2) is not correct. It’ll likely give a run time error.

恐怕说答案 D 正确的人可能并不真正知道他们在说什么。我建议不要继续进行此测验 -- 谁知道它包含多少其他错误或误导性答案?

总是很难理解未定义的行为,因为未定义的行为意味着任何事情都可能发生,包括什么都没有。当有人说 "I heard that doing X was undefined, but I tried it, and it worked fine" 时,就像说 "I heard that running across a busy street was dangerous, but I tried it, and it worked fine."

关于未定义行为的另一件事是你必须仔细考虑并理解它。几乎根据定义,没有语言翻译工具——没有 C 编译器或其他工具——保证会警告您。 必须知道什么是未定义的,因此要避免什么。你 不能 说 "Well, my program compiles without errors or warnings, and it seems to work, so it must be correct." 换句话说,你不能试图将 "correct vs. incorrect" 的决定强加到机器上——你必须拥有这种区别.


但也许你知道这一切。也许真正的问题很简单,"If answer C is correct, how can the program not fail with a run-time error, in fact how can it repeatedly not fail?"这个问题有两个答案:

  1. 如前所述,未定义的行为意味着 任何事情 都可能发生,包括什么都没有(即没有错误),包括在多个连续的 运行 上什么也没有。

  2. 在许多系统上,第一次 malloc 给你一个指向一些全新内存的指针,它总是全位 0(也就是说,或多或少就像你'称为 calloc)。这绝对是 由 C 标准保证——你应该从不依赖它——但在那些系统上,它很可能也可能是保证。此外,在几乎所有系统上,全位为 0 的指针值都是空指针。因此,再次仅在那些特定系统上,并且仅在第一次 malloc 为您提供指向全新内存的指针时,代码 ppInt2 = malloc(10 * sizeof(int*)) 将为您提供 10 个空指针。由于 free 被定义为如果您向它传递空指针则什么也不做,在这种特定情况下,free(*ppInt2) 永远不会失败,即使在 运行 时也不会。 (也许这就是设置测验的人的想法,因为 如果 你做了这些额外的假设,那么写的答案 C 基本上是不正确的,而答案 D 基本上是——我讨厌承认这一点——或多或少是准确的。)

回到前面的类比,如果有人做出这些额外的假设,并注意到代码永远不会失败,并试图声称答案 D 是正确的,这基本上就像在说 "I heard that running across the street was dangerous, but I tried it in the middle of the night, and it worked fine. I even ran back and forth ten times. I never got hit by a car, not even once"。而且,不幸的是,那里有一些遵循类似逻辑的程序员,他们将编写程序来执行 C 编程等价于街对面的 运行ning。然后这些程序员抱怨,好像这不是他们的错,当他们的运气不可避免地 运行 结束并且出现可怕的致命崩溃时。

我们来解决这个问题:

  1. 这里没有编译时错误
  2. 这里也没有(ISO C 标准指定的)运行时错误,因为在 C 中只有 none 个运行时错误。本质上,唯一的运行时错误是标准库函数的错误(返回) .
  3. free(*ppInt2)未定义的行为。任何事情都可能发生。编译器可能会删除它,甚至可能会删除整个 main(),或者更糟。如果它只是保持原样,free() 函数本身也可以做任何事情——忽略、崩溃、报告错误、通过尝试释放给定指针来搞乱它的簿记...
  4. 这是一个编码错误。不幸的是,像 C 中的许多语言一样,它没有被语言编译器或它的 runtime/standard 库捕获。

Valgrind 捕捉到这一点的事实是该工具的一个很好的卖点,但是,它不是 C 语言的一部分。