Undefined behavior
Стандарт языка C точно определяет наблюдаемое поведение программ на языке C, за исключением следующих категорий:
- undefined behavior - на поведение программы не накладывается никаких ограничений. Примерами неопределенного поведения являются: обращение к памяти за пределами границ массива, переполнение знакового целого числа, разыменование нулевого указателя, изменение одного и того же скаляра более одного раза в выражении без точек следования, доступ к объекту через указатель другого типа и т.д. От компиляторов не требуется диагностировать неопределенное поведение (хотя многие простые ситуации диагностируются), и скомпилированная программа не обязана делать что-либо осмысленное.
- неуточнённое поведение - допускаются два или более вариантов поведения, и реализация не обязана документировать эффекты каждого варианта. Например, порядок вычислений , являются ли идентичные строковые литералы различными и т.д. Каждое неуточнённое поведение приводит к одному из множества допустимых результатов и может давать разные результаты при повторении в той же программе.
- implementation-defined behavior - неопределённое поведение, при котором каждая реализация документирует, как делается выбор. Например, количество битов в байте или является ли сдвиг вправо знаковых целых чисел арифметическим или логическим.
- поведение, зависящее от локали - определяемое реализацией поведение, которое зависит от текущей выбранной локали . Например, возвращает ли islower true для любых символов, кроме 26 строчных латинских букв.
(Примечание: Строго соответствующие программы не зависят от любого неуказанного, неопределённого или определяемого реализацией поведения)
Компиляторы обязаны выдавать диагностические сообщения (либо ошибки, либо предупреждения) для любых программ, нарушающих синтаксические правила или семантические ограничения C++, даже если их поведение определено как неопределенное или определяемое реализацией, или если компилятор предоставляет языковое расширение, позволяющее принять такую программу. Диагностика для неопределенного поведения в остальных случаях не требуется.
Содержание |
НО и оптимизация
Поскольку корректные программы на C++ не содержат неопределённого поведения, компиляторы могут выдавать неожиданные результаты при компиляции программы с фактическим UB с включённой оптимизацией:
Например,
Переполнение знаковых чисел
int foo(int x) { return x + 1 > x; // либо истина, либо неопределенное поведение из-за переполнения знакового типа }
может быть скомпилирован как ( demo )
foo: mov eax, 1 ret
Выход за границы доступа
int table[4] = {0}; int exists_in_table(int v) { // возвращает 1 в одной из первых 4 итераций или неопределённое поведение из-за выхода за границы массива for (int i = 0; i <= 4; i++) if (table[i] == v) return 1; return 0; }
Может быть скомпилировано как ( демо )
exists_in_table: mov eax, 1 ret
Неинициализированная скалярная переменная
Может выдать следующий вывод (наблюдалось в старой версии gcc):
p — истина p — ложь
Может быть скомпилировано как ( демо )
f: mov eax, 42 ret
Недопустимый скаляр
int f(void) { _Bool b = 0; unsigned char* p = (unsigned char*)&b; *p = 10; // чтение из b теперь является неопределённым поведением return b == 0; }
Может быть скомпилировано как ( демо )
f: mov eax, 11 ret
Разыменование нулевого указателя
int foo(int* p) { int x = *p; if (!p) return x; // Либо неопределенное поведение выше, либо эта ветвь никогда не выполняется else return 0; } int bar() { int* p = NULL; return *p; // Безусловное неопределенное поведение }
может быть скомпилирован как ( demo )
foo: xor eax, eax ret bar: ret
Доступ к указателю, переданному в realloc
Выберите clang для просмотра показанного вывода
Возможный вывод:
12
Бесконечный цикл без побочных эффектов
Выберите clang для просмотра показанного вывода
#include <stdio.h> int fermat() { const int MAX = 1000; // Endless loop with no side effects is UB for (int a = 1, b = 1, c = 1; 1;) { if (((a * a * a) == ((b * b * b) + (c * c * c)))) return 1; ++a; if (a > MAX) { a = 1; ++b; } if (b > MAX) { b = 1; ++c; } if (c > MAX) c = 1; } return 0; } int main(void) { if (fermat()) puts("Fermat's Last Theorem has been disproved."); else puts("Fermat's Last Theorem has not been disproved."); }
Возможный вывод:
Fermat's Last Theorem has been disproved.
Ссылки
- Стандарт C23 (ISO/IEC 9899:2024):
-
- 3.4 Поведение (стр.: TBD)
-
- 4 Соответствие стандарту (стр.: TBD)
- Стандарт C17 (ISO/IEC 9899:2018):
-
- 3.4 Поведение (стр: 3-4)
-
- 4 Соответствие стандарту (стр: 8)
- Стандарт C11 (ISO/IEC 9899:2011):
-
- 3.4 Поведение (стр: 3-4)
-
- 4/2 Неопределенное поведение (стр: 8)
- Стандарт C99 (ISO/IEC 9899:1999):
-
- 3.4 Поведение (стр. 3-4)
-
- 4/2 Неопределенное поведение (стр. 7)
- Стандарт C89/C90 (ISO/IEC 9899:1990):
-
- 1.6 ОПРЕДЕЛЕНИЯ ТЕРМИНОВ
Смотрите также
|
C++ documentation
for
Undefined behavior
|
|
C++ documentation
для
Undefined behavior
|
Внешние ссылки
| 1. | Что каждый программист на C должен знать о неопределенном поведении #1/3 |
| 2. | Что каждый программист на C должен знать о неопределенном поведении #2/3 |
| 3. | Что каждый программист на C должен знать о неопределенном поведении #3/3 |
| 4. | Неопределенное поведение может привести к путешествиям во времени (и другим вещам, но путешествия во времени самые интересные) |
| 5. | Понимание целочисленного переполнения в C/C++ |
| 6. | Неопределенное поведение и Великая теорема Ферма |
| 7. | Забавы с NULL указателями, часть 1 (локальная уязвимость в Linux 2.6.30, вызванная UB из-за разыменования нулевого указателя) |