1

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

Michi
  • 5,175
  • 7
  • 33
  • 58
  • 3
    `free( *ppInt2 );` is undefined behavior, there is no concept of run time error in C standard. Your quizz cast return of `malloc` so I give them 0. – Stargateur Jul 06 '18 at 11:04
  • none of the options are correct statements – Sander De Dycker Jul 06 '18 at 11:04
  • @SanderDeDycker That was my Point too. – Michi Jul 06 '18 at 11:06
  • 1
    @Michi : then your analysis is correct :) – Sander De Dycker Jul 06 '18 at 11:07
  • There _could_ be a runtime error from `free(*ppInt2);` since that is one of the possible manifestations of undefined behaviour (specifically, "terminating a translation or execution (with the issuance of a diagnostic message)"). I expect that the quiz is looking for an answer of C. (Oh, except I see they claim D is correct. That's definitely wrong). – davmac Jul 06 '18 at 11:13
  • 1
    @Michi - to spare you another problem: The very next question uses an incorrect format specifier for printing a `sizeof` value. That makes all of *those* answers wrong. That site might not be the best place to learn C. :-) – Bo Persson Jul 06 '18 at 11:40
  • 1
    Again and again "geeksforgeeks" proves to be a trash site, with amateurs writing articles and nobody proof-reading. Just don't use that site at all. – Lundin Jul 06 '18 at 11:41
  • @BoPwrsson Thank you for your suggestion :)). I was not there to learn C, but to play some Quiz. – Michi Jul 06 '18 at 11:43
  • Get a better "quiz" site. Cast to/from `void *` are strongly discouraged. And none of the answer options is correct. – too honest for this site Jul 06 '18 at 13:38

2 Answers2

7

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.

Steve Summit
  • 45,437
  • 7
  • 70
  • 103
  • Well, imagine yourself how many people are making that Quiz :)). – Michi Jul 06 '18 at 11:14
  • `The error is not one that can be detected by the compiler` : Well, the funny thing by GCC is, that it takes its own decision when comes to something like that. Take a look [Here](https://pastebin.com/raw/72JmsALc), when you compile the same program with `-O3`. ==>> `total heap usage: 1 allocs, 0 frees, 80 bytes allocated` instead of ==>>> `total heap usage: 3 allocs, 2 frees, 164 bytes allocated` – Michi Jul 06 '18 at 11:18
  • for completeness : `free` can also be called for a null pointer without invoking undefined behavior. That's the principal reason that in many cases the shown code will not cause a runtime error (if `ppInt2` happens to point to zero initialized memory). – Sander De Dycker Jul 06 '18 at 11:27
  • 1
    @SanderDeDycker Ah, right. Good point. (Which, returning to my analogy, is sort of like saying "I heard that running across a busy street was dangerous, but I tried it in the middle of the night, and it worked fine. I even tried it ten times.") – Steve Summit Jul 06 '18 at 11:34
  • @SanderDeDycker If you add some more information [Like Here](https://ideone.com/lomOlo) you will get those `free's` but still an [invalid free will take place](https://pastebin.com/raw/wDqTxEyG). – Michi Jul 06 '18 at 11:34
  • @SteveSummit : indeed - I wasn't trying to invalidate your answer. Just added the note to pre-empt the naysayers who live along a pedestrian-only street. – Sander De Dycker Jul 06 '18 at 11:40
  • @Michi : that updated code has a double free (`free(ppInt2[0])` is the same as `free(*ppInt2)`) as well as a memory leak (`ppInt2` is never freed). – Sander De Dycker Jul 06 '18 at 11:43
  • @DeDycker I no that. I only changed the free rules here :)) – Michi Jul 06 '18 at 11:45
  • @Michi Perhaps you knew that answer C was or ought to be correct, and your question was simply, "Why does it never fail in practice, making it look like answer D is the right one instead?" If so, I'm sorry for lecturing you on what you already knew, and I've added an additional few paragraphs at the end of this answer addressing that point. – Steve Summit Jul 06 '18 at 13:53
  • @SanderDeDycker No worries, it's a fair point, and worth addressing in the answer. (Well, no, not *really* a "fair" point, but seemingly fair for those parochial naysayers. :-) ) – Steve Summit Jul 06 '18 at 14:15
1

Let's sort this out:

  1. There is no compile time error here
  2. There is no (ISO C standard specified) runtime error here, either, since there are next to none runtime errors in C. Essentially, the only runtime errors are errors (returned) from standard library functions.
  3. The free(*ppInt2) is undefined behaviour. Anything can happen. Compiler may delete it, or it can even delete the whole main(), or much, much worse. If it just leaves it as-is, the free() function itself may also do anything - ignore, crash, report an error, mess up its bookkeeping by trying to free given pointer...
  4. This is a coding error. Unfortunately, like many in C, it is not caught by the language compiler or it's runtime/standard library.

The fact that Valgrind catches this is nice selling point for the tool, but, it's not part of C language.

srdjan.veljkovic
  • 2,468
  • 16
  • 24
  • Actually, I'd say Valgrind is as much a part of the C language as gcc is. – Steve Summit Jul 06 '18 at 14:21
  • @SteveSummit Well, in the sense that you _should_ use Valgrind, if you can, sure, you could (should? :) ) say that. But, it really isn't part of C (the language) and is available on only a handful of platforms - while C is available _everywhere_ and gcc is available for a _lot_ of platforms. – srdjan.veljkovic Jul 07 '18 at 08:01