Предупреждение "Разыменование пустого указателя"
Доброго времени суток! Я в процессе написания функции столкнулся с таким предупреждением:
"C6011 Разыменование пустого указателя mostfreqch". На самом деле таких предупреждений 5 штук, так что я просто размещу метки в коде, где именно я получил эти предупреждения.
Функция должна находить самые часто встречаемые символы в строке и возвращать эти символы также в виде одной строки.
И функция работает, но эти предупреждения меня как-то напрягают. Если что, я пишу в Visual Studio 2019.
Если не трудно, объясните, что значат предупреждения и как их можно исправить.
Вот код:
Предупреждение "Невозможно преобразовать аргумент 1 из "тип" в "тот же самый тип""
Добрый день. Я столкнулся с проблемой, об которую, к сожалению, сломалась моя логика, и я вынужден.
Создать запись "Двигатель", которая содержит элементы "Название", "Мощность", "Скорость", "Цена"
Создать запись "Двигатель", которая содержит элементы "Название", "Мощность", "Скорость".
Почему для литералов не требуется разыменование указателя?
#include <stdio.h> char _literal_Vasya = "Vasia"; char *name = _literal_Vasya; int main().
"Выражение должно иметь тип указателя на объект"
Добрый вечер! При написании кода возникло ряд проблем , как только не пытался исправить , но всё.
Разыменование nullptr
Это значит, что компилятор имеет право предполагать, что эта ситуация никогда не случится, и обрабатывать её как ему вздумается. Отсюда следует, что если в вашей программе есть разыменование nullptr , то компилятор имеет право произвести любой код, никаких обязательств перед вами или гарантий нет.
В лучшем случае ваша программа просто крешнется. В худшем — будет вести себя необъяснимым странным образом.
Вот пример, упрощённый из реального критического бага в ядре Линукса [GNU/Linux, как говорит RMS]. (Украдено отсюда.)
В этом примере кажется, что код проверяет указатель на nullptr . Если оптимизатор сначала проведёт удаление мёртвого кода, а затем удаление бессмысленных проверок, наш код превратится вот во что:
Но если оптимизатор реализован по-другому, и выполняет оптимизации в другом порядке, мы получаем следующее:
Что произошло на первом этапе? Для компилятора разыменование nullptr есть undefined behaviour, он имеет право предполагать, что этого не происходит. Значит, видя строчку int dead = *P; , он имеет право предполагать, что P не nullptr . Поэтому проверку на nullptr он может выкинуть как бессмысленную.
Для многих нормальных программистов удаление проверки на nullptr из этой функции выглядит очень странно (и они наверное даже отправят баг разработчикам компилятора). Тем не менее, оба варианта компиляции соответствуют стандарту на 100%, и каждая из приведённых оптимизаций очень важна для производительности.
Несмотря на то, что этот пример кажется слишком простым и надуманным, подобные вещи часто случаются неявно в результате подстановки inline-функций: подстановка даёт возможность большей оптимизации. Это означает, что если оптимизатор решает включить функцию в другую функцию, большое количество локальных оптимизаций тут же включаются в игру, и это может поменять поведение кода. Это тоже разрешено по стандарту, и очень важно для хорошей производительности скомпилированного кода.
Мораль этой истории — компилятор C больше не является «высокоуровневым ассемблером», и выполняемый код может быть очень далёк от буквального, построчного выполнения того, что вы написали.
Разыменовывание нулевого указателя приводит к неопределённому поведению
Ненароком я породил большую дискуссию, касающуюся того, допустимо ли использовать в Си/Си++ выражение &P->m_foo, если P является нулевым указателем. Программисты разделились на два лагеря. Одни уверенно доказывали, что так писать нельзя, другие столь же уверенно утверждали, что можно. Приводились различные аргументы и ссылки. И я понял, что нужно внести окончательную ясность в этот вопрос. Для этого я обратился к экспертам Microsoft MVP и разработчикам Visual C++, общающимся через закрытый список рассылки. Они помогли подготовить эту статью, и я представляю её всем желающим. Для нетерпеливых: этот код не корректен.
Напомню историю обсуждений
Все началось со статьи о проверке ядра Linux с помощью анализатора PVS-Studio. Но сама проверка ядра тут ни причём. Дело в том, что в статье я привёл следующий фрагмент из кода Linux:
Я назвал этот код опасным, так как посчитал, что здесь имеет место неопределённое поведение.
По этому поводу я получил много возражений от читателей и даже одно время был готов поддаться на их убедительные речи в письмах и комментариях. Например, в качестве доказательства корректности кода приводили устройство макроса offsetof, который часто реализован так:
Здесь имеет место разыменование нулевого указателя, но код успешно работает. Были и другие письма с рассуждениями того, что раз нет доступа по нулевому указателю, то нет и проблемы.
Хотя я и доверчивый, но стараюсь проверять информацию. Я начал разбираться с этой темой и в результате написал небольшую статью: «Размышления над разыменованием нулевого указателя».
По всему выходило, что я был прав. Так писать нельзя. Однако я не смог окончательно обосновать свою позицию и привести нужные ссылки на стандарт.
После статьи вновь последовали письма с возражениями, и я понял, что надо разобраться с данной темой окончательно. Я обратился с вопросом к экспертам, чтобы узнать их мнение. Эта статья является их обобщенным ответом.
О языке Си
Выражение ‘&podhd->line6’ является неопределенным поведением в языке C в том случае, если ‘podhd’ — нулевой указатель.
Вот что говорится про оператор взятия адреса ‘&’ в стандарте C99 (Раздел 6.5.3.2 «Операторы взятия адреса и разыменовывания»):
Операнд унарного оператора & должен быть либо указателем функции, либо результатом оператора [] или унарного оператора *, либо lvalue-выражением, указывающим на объект, который не является битовым полем и не содержит в объявлении спецификатора регистрового класса памяти.
Выражение ‘podhd->line6’ однозначно не является указателем функции, результатом оператора [] или *. Это как раз lvalue-выражение. Однако, когда указатель ‘podhd’ равен нулю, выражение не указывает на объект, поскольку в Разделе 6.3.2.3 «Указатели» сказано следующее:
Если константа нулевого указателя приводится к типу указателей, то результирующий указатель, называемый нулевым, гарантированно будет не равен указателю на любой объект или функцию.
Если «lvalue-выражение не указывает на объект при своем вычислении, возникает неопределенное поведение» (Стандарт C99, Раздел 6.3.2.1 «Lvalue-выражения, массивы и указатели функций»):
lvalue — это выражение объектного типа или неполного типа, отличного от void; если lvalue-выражение не указывает на объект при своем вычислении, возникает неопределенное поведение.
Когда оператор -> был применен к указателю, его результатом стало lvalue-выражение, для которого не существует объекта, и в результате мы имеем дело с неопределенным поведением.
О языке Си++
В языке С++ всё обстоит точно также. Выражение ‘&podhd->line6’ является неопределенным поведением в языке C++ в том случае, если ‘podhd’ — нулевой указатель.
С толку немного сбивает дискуссия на WG21 (232. Is indirection through a null pointer undefined behavior?), на которую я ссылался в предыдущей статье. Там настаивают, будто бы такое выражение не является неопределенным поведением. Однако никто так и не нашел никаких правил в стандартах C++, которые разрешали бы использовать «poldh->line6», когда «polhd» — нулевой указатель.
Указатель «polhd» нарушает основное ограничение (Раздел 5.2.5/4, второй пункт в списке) о том, что он должен указывать на объект. Ни один объект в C++ не может иметь адреса nullptr.
Итого
Этот код является некорректным в языке Си и Си++, если указатель podhd равен 0. Если указатель равен 0, то возникает неопределённое поведение.
То, что программа может работать, является везением. Неопределённое поведение может проявить себя, как угодно. В том числе, программа может работать так, как хотел программист. Это один из частных случаев, но не более того.
Что значит разыменование пустого указателя в си
Указатели в языке Си поддерживают ряд операций: присваивание, получение адреса указателя, получение значения по указателю, некоторые арифметические операции и операции сравнения.
Присваивание
Указателю можно присвоить либо адрес объекта того же типа, либо значение другого указателя или константу NULL .
Присвоение указателю адреса уже рассматривалось в прошлой теме. Для получения адреса объекта используется операция & :
Причем указатель и переменная должны иметь тот же тип, в данном случае int.
Присвоение указателю другого указателя:
Когда указателю присваивается другой указатель, то фактически первый указатель начинает также указывать на тот же адрес, на который указывает второй указатель.
Если мы не хотим, чтобы указатель указывал на какой-то конкретный адрес, то можно присвоить ему условное нулевое значение с помощью константы NULL , которая определена в заголовочном файле stdio.h:
Разыменование указателя
Операция разыменования указателя в виде *имя_указателя , позволяет получить объект по адресу, который хранится в указателе.
Через выражение *pa мы можем получить значение по адресу, который хранится в указателе pa , а через выражение типа *pa = значение вложить по этому адресу новое значение.
И так как в данном случае указатель pa указывает на переменную a , то при изменении значения по адресу, на который указывает указатель, также изменится и значение переменной a .
Указатель на void
Указатели указывают на данные определенных типов. Например, указатель типа int* указывает на значение типа int , но не может указывать на данные других типов, скажем, на объект типа float . Однако можно также определять указатели типа void* , которые могут указывать на данные любого типа. И неявно указатели любых можно преобразовать в указатель типа void* :
Следует учитывать, что к void-указателю мы НЕ можем применить операцию разыменования и тем самым получить значение под адресу, который хранится в этом указателе. Поэтому для получения значения надо приводить к указателю соответствующего типа:
Одно из распространенных применений void-указателя — это вывод адреса на консоль:
Если мы хотим получить адрес из указателя другого типа, то, в соответствии со стандартами, его сначала надо преобразовать к типу void* .
Адрес указателя
Указатель хранит адрес переменной, и по этому адресу мы можем получить значение этой переменной. Но кроме того, указатель, как и любая переменная, сам имеет адрес, по которому он располагается в памяти. Этот адрес можно получить также через операцию & :
Операции сравнения
К указателям могут применяться операции сравнения > , >= , < , <= , == , != . Операции сравнения применяются только к указателям одного типа и константе NULL . Для сравнения используются номера адресов:
Консольный вывод в моем случае:
Приведение типов
Иногда требуется присвоить указателю одного типа значение указателя другого типа. В этом случае следует выполнить операцию приведения типов: