Подтвердить что ты не робот

C проверяет, не указана ли указатель без разыменования указателя?

У меня был этот аргумент с некоторыми людьми, говорящими, что C-ориентированные указатели вызывают поведение undefined, даже если они не разыменовываются. Пример:

int a;
int *p = &a;
p = p - 1;

третья строка здесь приведет к поведению undefined, даже если p никогда не разыменовывается (*p никогда не используется).

По-моему, звучит нелогично, что C проверяет, не указана ли указатель без используемого указателя (как будто кто-то будет проверять людей на улице, чтобы увидеть, если они несут оружие, если они введут его дом, где идеальная вещь - осмотреть людей, когда они собираются войти в дом). Я думаю, что если C проверит это, тогда произойдет много сбоев во время выполнения.

Кроме того, если C действительно проверяет наличие указателей OOB, то почему это не вызовет UB:

int *p; // uninitialized thus pointing to a random adress

в этом случае, почему ничего не происходит, даже если вероятность p, указывающая на адрес OOB, высока.

ADD:

int a;
int *p = &a;
p = p - 1;

say &a равно 1000. Будет ли значение p после оценки третьей строки:

  • 996, но все еще undefined, потому что p может быть разыменован где-то еще и вызвать реальную проблему.
  • undefined значение и что поведение undefined.

потому что я думаю, что "третья строка была вызвана как поведение undefined", в первую очередь, из-за потенциального будущего использования этого указателя OOB (разыменование) и людей со временем принимала его как undefined поведение в нем собственное. Теперь значение p будет 100% 996, и что все еще undefined поведение или его значение будет undefined?

4b9b3361

Ответ 1

C не проверяет, не указана ли указатель. Но основное оборудование может вести себя странным образом, когда вычисляется адрес, который выходит за границы объекта, указывая сразу после окончания объекта, являющегося исключением. Стандарт C явно описывает это как причину поведения undefined.

В большинстве современных сред вышеуказанный код не представляет проблемы, но подобные ситуации могут привести к ошибкам сегментации в 16-разрядном защищенном режиме x86, около 25 лет назад.

На языке Стандарта такое значение может быть значением ловушки, с которым нельзя манипулировать без вызова поведения undefined.

Соответствующий раздел стандарта C11:

6.5.6 Аддитивные операторы

  1. Когда выражение, которое имеет целочисленный тип, добавляется или вычитается из указателя, результат имеет тип операнда указателя. Если операнд указателя указывает на элемент объекта массива, и массив достаточно велик, результат указывает на смещение элемента от исходного элемента, так что разность индексов результирующих и исходных элементов массива равна целочисленному выражению. [...] Если оба операнда указателя и результат указывают на элементы одного и того же объекта массива или один за последним элементом объекта массива, оценка не должна приводить к переполнению; в противном случае поведение undefined. Если результат указывает один за последний элемент объекта массива, он не должен использоваться как операнд унарного оператора *, который оценивается.

Аналогичным примером поведения undefined является следующее:

char *p;
char *q = p;

Простое скачивание значения неинициализированного указателя p вызывает поведение undefined, даже если оно никогда не разыменовывается.

EDIT:, это спорный вопрос, пытающийся спорить об этом. Стандарт говорит, что вычисление такого адреса вызывает поведение undefined, так оно и есть. Тот факт, что некоторые реализации могут просто вычислить некоторое значение и сохранить его или нет, не имеет значения. Не полагайтесь на какие-либо предположения относительно поведения undefined: компилятор может воспользоваться своей непредсказуемой природой для выполнения оптимизаций, которые вы не можете себе представить.

Например, этот цикл:

for (int i = 1; i != 0; i++) {
    ...
}

может компилироваться в бесконечный цикл без какого-либо теста: i++ вызывает поведение undefined, если i равно INT_MAX, поэтому анализ компилятора таков:

  • начальное значение i равно > 0.
  • для любого положительного значения i < INT_MAX, i++ все еще > 0
  • для i = INT_MAX, i++ вызывает поведение undefined, поэтому мы можем предположить i > 0, потому что мы можем предположить все, что угодно.

Поэтому i всегда > 0, и тестовый код можно удалить.

Ответ 2

В самом деле, поведение программы C - это undefined, если он пытается вычислить значение через арифметику указателя, которая не приводит к указателю на элемент или один за концом одного и того же элемента массива. Из C11 6.5.6/8:

Если указатель операнд и результат указывают на элементы одного и того же объекта массива или один за последним элемент объекта массива, оценка не должна приводить к переполнению; в противном случае поведение undefined.

(Для целей этого описания адрес объекта типа T может рассматриваться как адрес первого элемента массива T[1].)

Ответ 3

Чтобы уточнить, "Undefined Поведение" означает, что результат рассматриваемого кода не определен в стандартах, регулирующих язык. Фактический результат зависит от способа реализации компилятора и может варьироваться от нуля до полного сбоя и всего между ними.

В стандартах не указывается, что должна выполняться любая проверка диапазона указателей. Но в отношении вашего конкретного примера это то, что они говорят:

Когда выражение, которое имеет целочисленный тип, добавляется или вычитается из указателя... Если и операнд указателя, и результат указывают на элементы одного и того же объекта массива или один за последним элементом объект массива, оценка не должна приводить к переполнению; в противном случае, поведение undefined. Если результат указывает на прошлое элемент объекта массива, он не должен использоваться в качестве операнда унарный *, который оценивается.

Вышеприведенная цитата из C99 §6.5.6 Параграф 8 (самая новая версия, которую я имею под рукой).

Обратите внимание, что это также относится к указателям без массива, поскольку в предыдущем разделе говорится:

Для целей этих операторов указатель на объект, который является не элемент массива ведет себя так же, как указатель на первый элемент массива длиной один с типом объекта как его тип элемента.

Итак, если вы выполняете арифметику указателя, и результат либо находится в пределах границ, либо указывает на один конец объекта, тогда вы получите действительный результат, иначе вы получите поведение undefined. Это может быть так, что вы попадаете в блуждающий указатель, но это может быть что-то другое.

Ответ 5

Когда спецификации говорят что-то undefined, это может быть довольно запутанным.

Это означает, что в этом случае реализация спецификации может делать все, что она хочет. В некоторых случаях он будет делать что-то, что появляется, интуитивно, правильно. В других случаях это не будет.

Для спецификаций адресных границ я знаю, что моя интуиция исходит из моих предположений о плоской модели однородной памяти. Но есть и другие модели памяти.

Слово "undefined" никогда не появляется в завершенной спецификации непреднамеренно. Комитеты по стандартизации обычно решают использовать это слово, когда им известны разные реализации стандарта, которые нужно делать разными вещами. Во многих случаях причиной разных вещей является производительность. Итак: появление слова в спецификации является предупреждением красного флага об этом для нас, простых смертных, пользователей спецификации, что наша интуиция может быть неправильной.

Такая спецификация "что бы она ни пожелала" лихорадочно раздражала rms несколько лет назад. Таким образом, он сделал некоторые версии своей сборника компиляторов Gnu (gcc), чтобы попытаться сыграть в компьютерную игру, когда столкнулся с чем-то undefined.

IBM использовала слово непредсказуемое в своих спецификациях в течение 360/370 дней. Это лучшее слово. Это делает звук более случайным и опасным. В рамках "непредсказуемого" поведения возникают такие проблемные результаты, как "остановить и уловить" .

Вот и все. "Случайный" - плохой способ описать такое непредсказуемое поведение, потому что "случайное" подразумевает, что система может делать что-то другое каждый раз, когда сталкивается с проблемой. Если он каждый раз делает что-то новое, у вас есть шанс поймать проблему в тесте. В мире "undefined" / "непредсказуемое" поведение система делает то же самое каждый раз, , пока это не будет. И вы знаете, что время не будет спустя годы после того, как вы подумаете, что закончили тестирование своих вещей.

Итак, когда спецификация говорит, что что-то есть undefined,, не делайте этого. Если вы не друг Murphy. OK?

Ответ 6

"Undefined поведение" означает "все может случиться". Общие значения "ничего": "ничего плохого вообще не происходит" и "ваш код падает". Другими распространенными значениями "ничего" являются "плохие вещи, возникающие при включении оптимизации", или "плохие вещи происходят, когда вы не запускаете код в разработке, но клиент его запускает", а все остальные значения - "ваш код" делает что-то неожиданное "и" ваш код делает то, что он не может сделать ".

Итак, если вы говорите "звучит нелогично, что C проверяет, не указана ли указатель без использования указателя", вы находитесь на очень, очень и очень опасной территории. Возьмите этот код:

int a = 0;
int b [2] = { 1, 2 };
int* p = &a; p - 1;
printf ("%d\n", *p);

Компилятор может предположить, что поведение undefined отсутствует. p - 1. Компилятор заключает (юридически), что либо p = & a [1], p = & b [1], либо p = & b [2], так как во всех других случаях существует поведение undefined либо при оценке p или при оценке p-1. Затем компилятор предполагает, что * p не является undefined поведением, поэтому он завершает (юридически), что p = & b [1] и печатает значение 2. Вы этого не ожидали, не так ли?

Это законно, и так бывает. Итак, урок: НЕ ПРИНИМАЙТЕ поведение undefined.

Ответ 7

Некоторые платформы рассматривают указатели как целые числа и арифметику указателя процесса так же, как и целочисленную арифметику, но с определенными значениями, масштабированными вверх или вниз в соответствии с размерами объектов. На таких платформах это эффективно определит "естественный" результат всех арифметических операций указателя, кроме вычитания указателей, разность которых не кратная размеру целевого типа указателя.

Другие платформы могут представлять указатели другими способами, и сложение или вычитание определенных комбинаций указателей может привести к непредсказуемым результатам.

Авторы стандарта C не хотели проявлять фаворитизм по отношению к любой из платформ, поэтому он не предъявляет никаких требований к тому, что может произойти, если указатели манипулируют способами, которые могут вызвать проблемы на некоторых платформах. Перед C-стандартом и в течение нескольких лет после этого программисты могли разумно ожидать, что реализации общего назначения для платформ, которые рассматривали арифметику указателя, такую ​​как масштабированная целочисленная арифметика, сами по себе обрабатывали арифметику указателя аналогично, но реализации для платформ, которые по-разному обрабатывали арифметику указателя скорее всего, будут относиться к нему по-разному.

В последнее десятилетие или около того, однако, в стремлении к "оптимизации", составители компилятора решили бросить в окно "Принцип наименьшего удивления". Даже в тех случаях, когда программист знал бы, какому эффекту определенных операций указателя будут предоставлены представления естественного указателя платформы, нет никакой гарантии, что компиляторы будут генерировать код, который ведет себя так, как будут вести себя представления естественных указателей. Тот факт, что стандарт говорит о поведении undefined, интерпретируется как приглашение для компиляторов наложить "оптимизации", которые заставляют программистов писать код, который медленнее и clunkier, чем он должен был быть на реализациях, которые просто ведут себя в соответствии с поведение документа в базовой среде (одно из трех методов обработки, которое авторы C89 явно отметили как обычные).

Таким образом, если не известно, что кто-то использует компилятор, который не имеет каких-либо глупых "оптимизаций", то тот факт, что промежуточный шаг в последовательности вычислений указателей вызывает undefined Поведение, делает невозможным вообще рассуждать об этом, независимо от того, насколько сильно здравый смысл будет означать, что качественные реализации для конкретной платформы должны вести себя определенным образом.

Ответ 8

Часть вопроса, касающаяся поведения undefined, очень понятна, ответ: "Ну, да, конечно, это поведение undefined".

Я буду интерпретировать формулировку "Проверяет ли C..." как следующие две:

  • Проверяет ли компилятор C...?
  • Проверка моей скомпилированной программы...?

(сам C - это спецификация языка, он не проверяет и не делает ничего)

Ответ на первый вопрос: Да, но не надежно, а не так, как вы пожелаете. Современные компиляторы довольно умны, иногда умнее, чем вам хотелось бы. В некоторых случаях компилятор сможет диагностировать ваше незаконное использование указателей. Поскольку это для каждого определения вызывает поведение undefined, и поэтому язык больше не требует от компилятора делать что-либо в частности, компилятор будет часто оптимизировать непредсказуемым образом. Это может привести к тому, что код сильно отличается от того, что вы изначально планировали. Не удивляйтесь, если весь охват или даже полная функция будет снята. Это справедливо для многих нежелательных "сюрпризов" в отношении поведения undefined.
Обязательно читайте: Что должен знать каждый программист C о undefined Поведение.

Ответ на второй вопрос: Нет, кроме случаев, когда вы используете компилятор, который поддерживает проверки границ, и если вы скомпилируете с проверками границ времени выполнения, что подразумевает довольно нетривиальные накладные расходы во время выполнения. На практике это означает, что если ваша программа "выжила", компилятор оптимизировал поведение undefined, тогда он просто упрямо сделает то, что вы ему сказали, с непредсказуемыми результатами - обычно либо считываемые значения мусора, либо ваша программа вызывает ошибка сегментации.

Ответ 9

Но что такое undefined поведение? Это просто означает, что никто не хочет говорить, что произойдет.

Я - старая мэйнфреймовая собака годами, и мне нравится фраза IBM для одного и того же: результаты непредсказуемы.

BTW: Мне нравится идея НЕ проверять границы массива. Например, если у меня есть указатель на строку, и я хочу видеть, что перед тем, как указывать байт, я могу использовать:

pointer[-1]

чтобы посмотреть на него.