Какие из перечисленных вызовов приводят к неопределенному поведению программы

Достаточно сложной темой для программистов на С++ является undefined behavior. Даже опытные разработчики зачастую не могут четко сформулировать причины его возникновения. Статья призвана внести чуть больше ясности в этот вопрос.

Статья является ПЕРЕВОДОМ нескольких статей и выдержек из Стандарта по данной теме.

Что такое «точки следования»?


Стандарте сказано:

Точки следования (sequence points)– такие точки в процессе выполнения программы, в которых все побочные эффекты уже выполненного кода закончили свое действие, а побочные эффекты кода, подлежащего исполнению, еще не начали действовать. (§1.9/7)

Побочные эффекты? А что такое «побочные эффекты»?

Побочный эффект (side effect) (согласно Стандарту) – результат доступа к volatile объекту, изменения объекта, вызова функции из библиотеки I/O или же вызова функции, включающей в себя какие-то из этих действий. Побочный эффект является изменением состояния среды выполнения.

Дмитрий Кашицын, Дракон в мешке: от LLVM к C++ и проблемам неопределенного поведения

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

int x = y++; // «y» тоже int

В дополнение к операции инициализации переменной «x» значение переменной «y» изменилось из-за побочного эффекта оператора ++.
Что ж, с этим понятно. Далее к точкам следования. Альтернативное определение понятия «точка следования» дано Стивом Саммитом (автор книг «Язык C в вопросах и ответах», блога «comp.lang.c»):
Точка следования – момент времени, когда «пыль улеглась», и все встреченные побочные эффекты гарантированно завершены и остались позади.

Какие точки следования описаны в Стандарте C++?

В стандарте описаны следующие точки следования:

    в конце вычисления полного выражения (§1.9/16). Под «полным выражением» (full-expression) подразумевается выражение, не являющееся подвыражением (subexpression) — частью другого выражения (прим: вычисление полного выражения может включать в себя вычисление подвыражения, лексически не являющегося его частью. Например, подвыражения, участвующие в вычислении аргумента по умолчанию, считаются частью выражения, которое вызвало функцию, а не выражения, определяющего этот аргумент).

int a = 5; // «;» — точка следования в данном контексте

1. a b (§5.14)
2. a || b (§5.15)
3. a? b: c (§5.16)
4. a, b (§5.18)

Вычисление этих выражений идет слева направо. После вычисления левого подвыражения все побочные эффекты этого вычисления прекращают действие. Если после вычисления левого подвыражения значение полного выражения известно, то правая часть не вычисляется. В последнем случае имеется в виду оператор запятая. В функции func(a, a++) запятая – не оператор, а просто разделитель между аргументами.

Что такое «неопределенное поведение»?


Стандарт дает определение словосочетанию «неопределенное поведение» в §1.3.12:

Александр Ганюхин — Контрактное программирование в C++

Неопределенное поведение (undefined behavior)– поведение, которое может возникать в результате использования ошибочных программных конструкций или некорректных данных, на которые Международный Стандарт не налагает никаких требований. Неопределенное поведение также может возникать в ситуациях, не описанных в Стандарте явно.

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

Какая связь между неопределенным поведением и точками следования?

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

Неуточняемое поведение (unspecified behavior) (согласно Стандарту) – поведение, для которого Стандарт предлагает два или более возможных вариантов и не налагает четких требований на то, какой из них должен быть выбран в определенной ситуации.

  • аргументы в вызове функции
  • операнды операторов (напр. +, -, =, *, /), за исключением:
    1. операторов бинарной логики ( и ||)
    2. оператора условия (?:)
    3. оператора запятой.
    4. (прим.: за исключением именно тех операторов, которые содержат точку следования)

    Поведение, определяемое реализацией (implementation-defined behavior) (согласно Стандарту) – поведение правильно сформированной программной конструкции с правильными данными, которое зависит от реализации (должно быть документировано для каждой реализации).

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

    int x = 5, y = 6; int z = x++ + y++; // не уточнено, будет вычислен первым x++ или y++

    Еще один пример:

    int Hello() < return printf(«Hello»); >int World() < return printf(«World !»); >int main() < int a = Hello() + World(); /**может вывести «Hello World!» или «World! Hello» ^ | Функции могут быть вызваны в любом порядке **/ return 0; >

    В §5/4 Стандарт говорит:

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

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

    i++ * ++i; // i = ++i; // ++i = 2; // i изменено более 1 раза i = ++i +1 // i = (i,++i,++i); // нет точки следования между правым `++i` и присвоением `i` (`i` изменяется более 1 раза между 2мя точками следования)

    Но в то же время:

    i = (i, ++i, 1) + 1; // определено i = (++i,i++,i) // определено int j = i; j = (++i, i++, j*i); // определено

    Кроме того (по Стандарту) – старое значение выражения (до вычисления) должно быть доступно только для определения хранимого значения.
    Это значит, что некорректными являются те выражения, в которых доступ к значению может предшествовать его модификации.

    std::printf(«%d %d», i,++i); // неизвестно, что произойдет раньше – вычисление (++i) или доступ к нему.

    Еще один пример:

    a[i] = i++ ; // либо a[++i] = i , либо a[i++] = ++i и т.д.

    Я слышал, что в C++0x нет никаких Точек Следования, это правда?

    Да, это правда.
    Понятие «точка следования» было заменено комитетом ISO C++ на уточненное и дополненное понятие Отношения Следования [ДОПОСЛЕ].

    Что такое Отношение Следования[ДО]?

    • ассиметрично
    • транзитивно
    • возникает между парами вычислений и формирующее из них частично упорядоченное множество (partially ordered set)

    Формально, это означает, что при двух данных выражениях А и B, если А [следует ДО] B, то выполнение А должно предшествовать выполнению В. Если же А не [следует ДО] В, тогда выполнение А и В является неупорядоченным (unsequenced) (выполнение неупорядоченных вычислений может пересекаться).
    Вычисление A и В являются неопределенно упорядоченным (indeterminantly sequenced), когда либо А [следует ДО] В, либо В [следует ДО] А, но что именно – не уточнено. Неопределенно упорядоченные вычисления не могут пересекаться, но любое из них может выполнятся первым.

    Что означает слово «вычисление» в контексте C++0x ?
    • подсчет (computation) значения (включая определение положения объекта в памяти для вычисления значения gvalue-выражения и получение значения по ссылке для вычисления prvalue-выражения)
    • инициирование побочных эффектов

    Стандарт говорит нам (§1.9/14):

    Каждый подсчет значения и побочный эффект, связанные с полным выражением, [следуют ДО] подсчета значения и побочного эффекта, связанных со следующим полным выражением, которое будет вычислено.

    int x; x = 10; ++x;

    В данном примере подсчет значения и побочный эффект, связанный с выражением (++x), [следует ПОСЛЕ] подсчета значения и побочного эффекта (x=10).

    Ведь между вещами, описанными выше, и неопределенным поведением должна быть какая-то связь, да?

    Конечно, связь есть.

    В §1.9/15 упоминается, что:

    Вычисление операндов конкретного оператора или подвыражений конкретного выражения неупорядоченно, помимо случаев, которые были описаны ранее.

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

    int main() < int num = 19 ; num = (num > 3) ; >

    1) Вычисление операндов оператора «+» неупорядоченно.
    2) Вычисление операндов операторов «>» неупорядоченно.

    §1.9/15 подсчет значения операндов конкретного оператора [следует ДО] подсчета значения результата работы оператора.

    Это означает, что в выражении x + y подсчет значений «х» и «у» [следует ДО] подсчета x+y.

    Теперь к более важному:

    • возникновению другого побочного эффекта этого же объекта
    • подсчету значения с использованием значения этого объект

    f(i = -1, i = -1);

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

    Предположим, что компилятор решил, что оптимальнее всего присвоить «-1» будет обнулив переменную и сделав ее декремент.
    Инструкции могут сформироваться так (команды условны):

    clear i decr i clear i decr i
    clear i clear i decr i decr i

    после чего в i будет хранится значение -2.

    При вызове функции каждый подсчет значения и побочный эффект, связанный с выражением аргумента этой функции, или с выражением, вызывающим функцию, [следует ДО] выполнения любого выражения или оператора в теле вызываемой функции.
    Подсчет значения и побочные эффекты, связанные с разными аргументами, неупорядоченны.

    Поток выполнения программы

    Оперируя терминами, расшифрованными ранее, поток выполнения программы можно представить графически. В следующих далее диаграммах обозначим вычисление выражения (или подвыражения) как E(x), точку следования — %, побочный эффект «k» для объекта «e» обозначим S(k,e). Если для вычисления необходимо считать значение из именованного объекта (пусть «x» — имя), вычисление будем обозначать V(x), в остальных случаях – так же, как договаривались ранее, E(x). Побочные эффекты запишем справа и слева от выражений. Граница между двумя выражениями обозначает, что верхнее выражение вычисляется до нижнего выражения (зачастую потому что нижнее выражение зависит от prvalue или lvalue верхнего выражения).
    Для двух выражений i++; i++; диаграмма будет иметь вид:

    E(i++) -> < S(increment, i) >| % | E(i++) -> < S(increment, i) >| %

    Как видите, в данном случае мы имеем две точки следования, одна из которых разделяет два изменения «i».
    Вызовы функций также представляют интерес, несмотря на то, что диаграмму для них мы опустим:

    int c = 0; int d = 0; void f(int a, int b) < assert((a == c — 1) (b == d — 1)); >int main()

    Этот код корректный, потому что к тому времени, как начнет выполняться тело функции f, все побочные эффекты, порожденные вычислением аргументов, гарантированно закончатся: «с» и «d» будут увеличены на 1.
    Теперь рассмотрим выражение i++ * j++;

    < S(increment, i) > < S(increment, j) > / +—+—+ | E(i++ * j++) | %

    Откуда же появилось две ветки? Напомним, что точки следования завершают вычисления, проводившиеся ДО их наступления.

    Все подвыражения умножения вычисляются до самого умножения, больше в этом выражении нет точки следования, следовательно, нам нужно принять во внимание теоретическую «параллельность» вычисления операндов, чтобы предположить, где может произойти конкурентное изменение одного и того же объекта. Говоря более формально, эти две ветви неупорядочены. Точки следования – это отношение, которое упорядочивает некоторые вычисления и не упорядочивает другие. Т.о. точки следования, как и говорилось выше, являются частичным упорядочиванием (partial order).

    Конфликтующие побочные эффекты.

    Чтобы обеспечить компилятору свободу в генерации и оптимизации машинного кода, в случаях, подобных рассмотренному выше умножению, не устанавливается порядок вычисления подвыражений и не разделяются побочные эффекты, порожденные ими (за исключением описанных ранее случаев).
    Это может вести к конфликтам, поэтому Стандарт называет неопределенным поведение программы, если она пытается модифицировать один и тот же объект без участия точек следования. Это относится к скалярным объектам, потому что остальные объекты являются либо неизменяемыми (array) или попросту не подпадают под это правило (class objects). Неопределенное поведение также возникает, если в выражении присутствуют обращение к предыдущему значению объекта и его модификация, как например в i * i++

    // Ведет к неопределенному поведению! // Не факт, что из левого ‘i’ будет считано «новое» значение: V(i) E(i++) -> < S(increment, i) >) / +—+—+ | E(i * i++) | %

    В качестве исключения позволено считывать значение объекта, если оно необходимо для подсчета нового значения. Пример контекста: i = i+1

    V(i) E(1) / +—+—+ | E(i) E(i + 1) / +——-+——-+ | E(i = i + 1) -> < S(assign, i) >| %

    Здесь мы видим обращение к «i» в правой части; после вычисления обеих частей совершается присваивание. Т.о. побочный эффект и обращение к «i» происходят, не пересекая точку следования, но обращались к «i» мы только для определения хранимого значения, поэтому разногласий не будет.
    Иногда, значение считывается после модификации. Для случая
    a = (b = 0);
    справедливо, что происходит запись в «b», а потом чтение из «b» без пересечения точки следования. Тем не менее, это нормально, потому что считывается уже новое значение «b», а обращения к старому не происходит. В этом случае побочные эффекты присвоения «b» закончат свое действие не только до следующей точки следования, но и перед чтением значения «b», требуемого для присвоения «а».

    Стандарт явно говорит: «результатом операции присваивания является значение, хранимое в левом операнде, после того, как присваивание выполнено (результат — lvalue)». Почему не используется понятие точки следования? Потому что это понятие содержит ненужное в данной ситуации требование, чтобы все побочные эффекты левого и правого операнда были завершены, вместо того чтобы рассматривать только побочные эффекты присвоения, возвращающего lvalue, с помощью которого происходит считывание.

    • Актуальная версия стандарта
    • stackoverflow.com/questions/4176328/undefined-behavior-and-sequence-points?lq=1
    • stackoverflow.com/questions/2397984/undefined-unspecified-and-implementation-defined-behavior
    • undefined behavior
    • sequence points

    Источник: habr.com

    Неопределенное поведение

    В компьютерном программировании, неопределенное поведение (UB) является результатом выполнения программы, поведение предписывается как непредсказуемое в спецификации языка, которой придерживается компьютерный код. Это отличается от неопределенного поведения, для которого спецификация языка не предписывает результат и поведение, определяемое реализацией, которое относится к документации другого компонента платформы платформы (например, ABI или документацию по переводчику ).

    В сообществе C неопределенное поведение может быть юмористически названо «назальные демоны » после сообщения comp.std.c в котором поведение undefined объясняется тем, что компилятор может делать все, что он захочет, даже «заставить демонов вылетать из вашего носа».

    Обзор

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

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

    Однако прогрессивная стандартизация платформ сделала это меньшим преимуществом, особенно в новых версиях C.Теперь случаи неопределенного поведения обычно представляют собой однозначные ошибки в коде для пример индексации массива за его пределами. По определению среда выполнения может предполагать, что неопределенное поведение никогда не происходит; следовательно, нет необходимости проверять некоторые недопустимые условия. Для компилятора это также означает, что различные программные преобразования становятся действительными или упрощаются их доказательства правильности; это допускает различные виды преждевременной оптимизации и, которые приводят к некорректному поведению, если состояние программы соответствует любому из таких условий. Компилятор может также удалить явные проверки, которые могли быть в исходном коде, без уведомления программиста; например, обнаружение неопределенного поведения путем проверки того, произошло ли оно, по определению не гарантирует работы. Это делает трудным или невозможным программирование переносимого отказоустойчивого варианта (для некоторых конструкций возможны непереносимые решения).

    Текущая разработка компилятора обычно оценивает и сравнивает производительность компилятора с тестами, разработанными для микрооптимизации, даже на платформах, которые в основном используются на рынке настольных компьютеров и портативных компьютеров общего назначения (например, amd64). Следовательно, неопределенное поведение предоставляет достаточно возможностей для повышения производительности компилятора, поскольку исходный код для конкретного оператора исходного кода может быть сопоставлен с чем угодно во время выполнения.

    Для C и C ++ компилятору разрешено давать диагностику времени компиляции в этих случаях, но это не обязательно: реализация будет считаться правильной независимо от того, что она делает в таких случаях, аналогично безразличные термины в цифровой логике. Программист несет ответственность за написание кода, который никогда не вызывает неопределенное поведение, хотя реализациям компилятора разрешено выдавать диагностику, когда это происходит. В настоящее время у компиляторов есть флаги, которые включают такую ​​диагностику, например, -fsanitize включает «дезинфицирующее средство неопределенного поведения» () в gcc 4.9 и в clang. Однако этот флаг не установлен по умолчанию, и его включение — это выбор того, кто собирает код.

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

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

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

    Преимущества

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

    Пример для языка C:

    int foo (unsigned char x)

    Значение x не может быть отрицательным, и, учитывая, что знаковое целочисленное переполнение является неопределенным поведением в C, компилятор может предположить, что value всегда будет ложным. Таким образом, оператор if , включая вызов функции bar , может игнорироваться компилятором, поскольку тестовое выражение в if не имеет побочных эффектов, и его условие никогда не будет выполнено. Таким образом, код семантически эквивалентен:

    int foo (unsigned char x)

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

    Такие оптимизации становится трудно обнаружить людям, когда код более сложен и другие оптимизации, такие как встраивание, например, другая функция может вызывать указанную выше функцию:

    void run_tasks (unsigned char * ptrx) 60) >

    Компилятор может оптимизировать цикл while здесь, применив анализ диапазона значений : проверяя foo () , он знает, что начальное значение, на которое указывает ptrx , не может превышать 47 (поскольку любое другое значение вызовет неопределенное поведение в foo () ), поэтому начальная проверка * ptrx>60 всегда будет ложным в соответствующей программе. Идя дальше, поскольку результат z теперь никогда не используется и foo () не имеет побочных эффектов, компилятор может оптимизировать run_tasks () как пустую функцию. это немедленно возвращается. Исчезновение цикла while может быть особенно неожиданным, если foo () определен в отдельно скомпилированном объектном файле.

    . Еще одно преимущество от разрешения подписанного целочисленного переполнения. undefined заключается в том, что он позволяет хранить и управлять значением переменной в регистре процессора , которое больше размера переменной в исходном коде. Например, если тип переменной, как указано в исходном коде, уже, чем ширина собственного регистра (например, «int » на 64-битной машине, распространенный сценарий), то компилятор может безопасно использовать 64-битное целое число со знаком для переменной в создаваемом им машинном коде без изменения определенного поведения кода. Если программа зависела от поведения 32-битного целочисленного переполнения, то компилятор должен был бы вставить дополнительную логику при компиляции для 64-битной машины, потому что поведение переполнения большинства машинных инструкций зависит от ширины регистра.

    Неопределенное поведение также позволяет выполнять больше проверок во время компиляции обоими компиляторами и статический анализ программы.

    Риски

    Стандарты C и C ++ имеют несколько форм неопределенного поведения повсюду, что обеспечивает повышенную свободу в компиляторе реализации и проверки во время компиляции за счет неопределенного поведения во время выполнения, если оно присутствует. В частности, в стандарте ISO для C есть приложение, в котором перечислены общие источники неопределенного поведения.

    Более того, компиляторы не обязаны диагностировать код, основанный на неопределенном поведении. Следовательно, программисты, даже опытные, часто полагаются на неопределенное поведение либо по ошибке, либо просто потому, что они плохо разбираются в правилах языка, который может охватывать сотни страниц. Это может привести к ошибкам, которые обнаруживаются при использовании другого компилятора или других настроек. Тестирование или фаззинг с включенными динамическими проверками неопределенного поведения, например, дезинфицирующие средства Clang, могут помочь выявить неопределенное поведение, не диагностируемое компилятором или статическими анализаторами.

    Не определено. поведение может привести к уязвимостям безопасности в программном обеспечении. Например, переполнение буфера и другие уязвимости безопасности в основных веб-браузерах происходят из-за неопределенного поведения. Проблема 2038 года — еще один пример из-за переполнения целого числа signed . Когда разработчики GCC изменили свой компилятор в 2008 году так, что он пропустил определенные проверки переполнения, основанные на неопределенном поведении, CERT выдал предупреждение против более новых версий компилятора. Linux Weekly News указывает, что такое же поведение наблюдается в PathScale C, Microsoft Visual C ++ 2005 и нескольких других компиляторах; предупреждение было позже изменено, чтобы предупредить о различных компиляторах.

    Примеры в C и C ++

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

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

    Попытка изменить строковый литерал вызывает неопределенное поведение:

    char * p = «wikipedia»; // допустимый C, не рекомендуется в C ++ 98 / C ++ 03, плохо сформирован в C ++ 11 p [0] = ‘W’; // неопределенное поведение

    целое число деление на ноль приводит к неопределенному поведению:

    int x = 1; вернуть x / 0; // неопределенное поведение

    Некоторые операции с указателями могут привести к неопределенному поведению:

    int arr [4] = ; int * p = arr + 5; // неопределенное поведение для индексации за пределами p = 0; int a = * p; // неопределенное поведение для разыменования нулевого указателя

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

    int main (void)

    Достижение конца функции, возвращающей значение (кроме main () ) без оператора return, приводит к неопределенному поведению, если значение вызова функции используется вызывающим:

    int f () <> / * неопределенное поведение, если используется значение вызова функции * /

    Изменение объекта между двумя точками последовательности более одного раза приводит к неопределенному поведению. В C ++ 11 произошли значительные изменения в причинах неопределенного поведения по отношению к точкам последовательности. Однако следующий пример приведет к неопределенному поведению в C ++ и C.

    i = i ++ + 1; // неопределенное поведение

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

    a [i] = i ++; // неопределенное поведение printf («% d% d n», ++ n, power (2, n)); // также неопределенное поведение

    В C / C ++ побитовое смещение значения на количество битов, которое является либо отрицательным числом, либо больше или равно общему количеству битов в этом значении, приводит к неопределенное поведение. Самый безопасный способ (независимо от производителя компилятора) — всегда сохранять количество бит для сдвига (правый операнд побитовых операторов и >> ) в диапазоне: 0, sizeof (значение) * CHAR_BIT — 1 >(где значение — левый операнд).

    int num = -1; unsigned int val = 1

    См. также

    • Компилятор
    • Halt and Catch Fire
    • Неопределенное поведение

    Ссылки

    Дополнительная литература

    • Питер ван дер Линден, эксперт C Программирование. ISBN 0-13-177429-8
    • UB Canaries (апрель 2015 г.), Джон Регер (Университет штата Юта, США)
    • Неопределенное поведение в 2017 г. (июль 2017 г.) Паскаль Куок (TrustInSoft, Франция) и Джон Регер (Университет Юты, США)

    Внешние ссылки

    • Исправленная версия стандарта C99. См. Раздел 6.10.6 для #pragma

    Источник: alphapedia.ru

    Какие из перечисленных вызовов приводят к неопределенному поведению программы

    Скачай курс
    в приложении

    Перейти в приложение
    Открыть мобильную версию сайта

    Наши условия использования и конфиденциальности

    Get it on Google Play

    Public user contributions licensed under cc-wiki license with attribution required

    Источник: stepik.org

    Рейтинг
    ( Пока оценок нет )
    Загрузка ...
    EFT-Soft.ru