cmallocvalgrindfreegcc8

"Pointer to pointer to int issue"


Today I tried to solve a Quiz from Here and when I reached the Question 3, there was the following code:

#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 );
}

And the Question was:

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

Because of this line:

free(*ppInt2);

Which for what I understand suggests that, there will be no compile or run time error, I decided that

free(*ppInt2)

is not correct.

But because there is no compile/run time errors here, makes Answers B and C wrong.

The Author says that the accepted Answer is:

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

Now here is my Question, why is there no issue, because doing this:

free( *ppInt2 );

Valgrind reports:

==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)

I thought that the right free call should be:

free( ppInt2 );

Tested on Linux mint 19, GCC-8 and valgrind-3.13.0


Solution

  • Answer C is closest to being correct. The line

    free( *ppInt2 );
    

    is definitely incorrect. The error is not one that can be detected by the compiler. But it is quite likely to cause a run-time error. (But it is not guaranteed to cause a run-time error. More on this below.)

    The rule for malloc and free is pretty simple: every pointer you hand to free must be exactly one that you received from a previous call to malloc (or calloc, or realloc). In the code, the malloc and free calls for pInt and ppInt1 correctly follow this rule. But for ppInt2, the pointer returned by malloc is assigned to ppInt2, but the pointer handed to free is *ppInt2, the value pointed to by ppInt2. But since *ppInt2 -- that is, the value pointed to by ppInt2 -- hasn't been initialized in any way, it's a garbage value, which free is likely to crash on. The end result is more or less exactly as if you had said

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

    But, again, a crash is not guaranteed. So the more correct answer would therefore be worded as

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

    I'm afraid that someone who says that answer D is correct may not really know what they are talking about. I would suggest not continuing further with this quiz -- who knows how many other wrong or misleading answers it contains?

    It's always hard to understand undefined behavior, because undefined behavior means that anything can happen, including nothing. When someone says "I heard that doing X was undefined, but I tried it, and it worked fine", it's just like saying "I heard that running across a busy street was dangerous, but I tried it, and it worked fine."

    The other thing about undefined behavior is that You have to think about it, and understand it, carefully. Pretty much by definition, no language translation tool -- no C compiler or other tool -- is guaranteed to warn you about it. You have to know what's undefined, and what to therefore steer clear of. You can't say "Well, my program compiles without errors or warnings, and it seems to work, so it must be correct." In other words, you can't try to foist the "correct vs. incorrect" determination off onto the machine -- you have to own this distinction.


    But perhaps you knew all that. Perhaps the real question is simply, "If answer C is correct, how can the program not fail with a run-time error, in fact how can it repeatedly not fail?" This question has two answers:

    1. As mentioned, undefined behavior means anything can happen, including nothing (i.e. no errors), including nothing on multiple successive runs.

    2. On many systems, the first time malloc gives you a pointer to some brand-new memory, it's always all-bits-0 (that is, more or less as if you'd called calloc). This is absolutely not guaranteed by the C Standards -- you should never depend on it -- but on those systems, it's so likely that it might as well be guaranteed. Furthermore, on virtually all systems, a pointer value that's all-bits-0 is a null pointer. So, and again only on those particular systems, and again only the first time malloc gives you a pointer to brand-new memory, the code ppInt2 = malloc(10 * sizeof(int*)) will give you 10 null pointers. And since free is defined as doing nothing if you pass it a null pointer, in this specific case, free(*ppInt2) will never fail, not even at run time. (Perhaps this is what the person setting the quiz had in mind, because if you make these additional assumptions, answer C as written is basically not correct, and answer D basically is -- I hate to admit this -- more or less accurate.)

    Returning to an earlier analogy, if someone makes these additional assumptions, and notices that the code never fails, and tries to claim that answer D is correct instead, it's basically like saying, "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". And, unfortunately, there are programmers out there who follow similar logic, who will write programs that do the C programming equivalent of running across the street at all times. And these programmers then complain, as if it's not their fault, when inevitably their luck runs out and there's a horrible, fatal crash.