"Указатель на указатель на выпуск int"
Сегодня я попытался решить викторину отсюда, и когда я дошел до вопроса 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 );
}
И вопрос был:
Выберите правильное утверждение по сравнению с C-программой:
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)
не является правильным.
Но так как здесь нет ошибок компиляции / выполнения, получаются ответы B
а также C
неправильно.
Автор говорит, что принятый ответ:
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 19
, GCC-8
а также valgrind-3.13.0
2 ответа
Ответ С наиболее близок к правильности. Линия
free( *ppInt2 );
определенно неверно. Ошибка не может быть обнаружена компилятором. Но это вполне может вызвать ошибку во время выполнения. (Но это не гарантированно приведет к ошибке во время выполнения. Подробнее об этом ниже.)
Правило для malloc
а также free
довольно просто: каждый указатель free
должно быть именно то, что вы получили от предыдущего звонка malloc
(или же calloc
, или же realloc
). В коде malloc
а также free
призывает для pInt
а также ppInt1
правильно следуйте этому правилу. Но для ppInt2
, указатель возвращается malloc
назначен на ppInt2
, но указатель передан free
является *ppInt2
значение, на которое указывает ppInt2
, Но с тех пор *ppInt2
- то есть значение, на которое указывает ppInt2
- не был инициализирован каким-либо образом, это значение мусора, которое free
может привести к сбою. Конечный результат более или менее точно такой, как если бы вы сказали
int main()
{
int *p;
free(p); /* WRONG */
}
Но, опять же, сбой не гарантируется. Таким образом, более правильный ответ будет сформулирован как
C' -
free(*ppInt2)
не является правильным. Это, вероятно, даст ошибку во время выполнения.
Боюсь, что тот, кто говорит, что ответ D верен, может не знать, о чем он говорит. Я бы предложил не продолжать эту викторину - кто знает, сколько других неверных или вводящих в заблуждение ответов она содержит?
Неопределенное поведение всегда трудно понять, потому что неопределенное поведение означает, что все может случиться, в том числе и ничего. Когда кто-то говорит: "Я слышал, что выполнение X было неопределенным, но я попробовал это, и оно работало нормально", это все равно, что сказать: "Я слышал, что бегать по оживленной улице опасно, но я пытался, и это работало нормально".
Еще одна вещь, связанная с неопределенным поведением, заключается в том, что вы должны тщательно обдумать это и понять. По определению, ни один инструмент языкового перевода - ни компилятор C, ни другой инструмент - не гарантированно предупредит вас об этом. Вы должны знать, что не определено и что следует избегать. Вы не можете сказать: "Ну, моя программа компилируется без ошибок и предупреждений, и, похоже, она работает, поэтому она должна быть правильной". Другими словами, вы не можете пытаться навязать "правильное или неправильное" определение на машине - вы должны владеть этим различием.
Но, возможно, ты знал все это. Возможно, реальный вопрос заключается в следующем: "Если ответ С верен, как программа может не потерпеть неудачу с ошибкой во время выполнения, а на самом деле, как она может многократно не потерпеть неудачу?" На этот вопрос есть два ответа:
Как уже упоминалось, неопределенное поведение означает, что все может произойти, в том числе ничего (то есть без ошибок), в том числе ничего при нескольких последовательных запусках.
На многих системах первый раз
malloc
дает вам указатель на какую-то совершенно новую память, это всегда все-бит-0 (то есть, более или менее, как если бы вы позвонилиcalloc
). Это абсолютноне гарантируется стандартами C - вы никогда не должны зависеть от этого - но для этих систем вполне вероятно, что это может быть гарантировано. Кроме того, практически во всех системах значение указателя all-bits-0 является нулевым указателем. Итак, и снова только на этих конкретных системах, и снова только в первый разmalloc
дает вам указатель на совершенно новую память, кодppInt2 = malloc(10 * sizeof(int*))
даст вам 10 нулевых указателей. И с тех порfree
определяется как ничего не делать, если вы передаете ему нулевой указатель, в данном конкретном случае,free(*ppInt2)
никогда не подведет, даже во время выполнения. (Возможно, именно это и имел в виду человек, проводящий тест, потому что, если вы сделаете эти дополнительные предположения, ответ C в том виде, в котором он написан, в основном неверен, а ответ D в основном - я не хочу это признавать - более или менее точный.)
Возвращаясь к более ранней аналогии, если кто-то делает эти дополнительные предположения и замечает, что код никогда не завершается с ошибкой, и пытается утверждать, что ответ D вместо этого правильный, то это, в основном, все равно что сказать: "Я слышал, что бегать по улице было опасно, но я попробовал это среди ночи, и это работало отлично. Я даже бегал туда-сюда десять раз. Я никогда не попадал под машину, даже ни разу ". И, к сожалению, есть программисты, которые следуют схожей логике и пишут программы, которые делают программирование на C эквивалентным работе через дорогу в любое время. И эти программисты потом жалуются, как будто это не их вина, когда неизбежно заканчивается их удача и происходит ужасный фатальный сбой.
Давайте разберемся с этим:
- Здесь нет ошибки времени компиляции
- Здесь также нет никаких ошибок времени выполнения (указано в стандарте ISO C), поскольку в C. практически нет ошибок времени выполнения. По сути, единственными ошибками времени выполнения являются ошибки (возвращаемые) из функций стандартной библиотеки.
free(*ppInt2)
является неопределенным поведением. Все может случиться. Компилятор может удалить его, или он может даже удалить весьmain()
или намного, намного хуже. Если он просто оставляет все как есть,free()
Сама функция также может делать что угодно - игнорировать, аварийно завершать работу, сообщать об ошибке, портить бухгалтерию, пытаясь освободить указанный указатель...- Это ошибка кодирования. К сожалению, как и многие в C, он не перехватывается языковым компилятором или библиотекой времени выполнения / стандартной.
Тот факт, что Valgrind уловил это, является хорошим аргументом в пользу этого инструмента, но он не является частью языка Си.