Неопределённое поведение
Стандарт языка C точно определяет наблюдаемое поведение программ на языке C, за исключением следующих категорий:
- неопределённое поведение - нет ограничений на поведение программы. Примерами неопределённого поведения являются обращения к памяти за пределами массива, переполнение знакового целого числа, обращение к нулевому указателю, модификация одного и того же скалярного значения более одного раза в выражении без точек последовательности, доступ к объекту через указатель другого типа и т.д. Компиляторы не обязаны диагностировать неопределённое поведение (хотя многие простые ситуации диагностируются), и скомпилированная программа не обязана делать что-либо осмысленное.
- неопределённое поведение - допускается два или более поведения, и реализация не обязана документировать последствия каждого поведения. Например, порядок вычисления, являются ли идентичные строковые литералы различными и т.д. Каждое неопределённое поведение приводит к одному из набора допустимых результатов и может давать разный результат при повторном выполнении в одной и той же программе.
- поведение, определяемое реализацией - неопределённое поведение, где каждая реализация документирует, как сделан выбор. Например, количество бит в байте или то, является ли правое сдвиг знакового целого числа арифметическим или логическим.
- поведение, зависящее от локали - поведение, определяемое реализацией, которое зависит от текущей выбранной локали. Например, возвращает ли
islowerзначение true для любого символа, отличного от 26 строчных латинских букв.
(Примечание: Строго соответствующие программы не зависят от неопределённого, не определённого или определяемого реализацией поведения).
Компиляторы обязаны выдавать диагностические сообщения (ошибки или предупреждения) для любых программ, нарушающих правила синтаксиса C или семантические ограничения, даже если её поведение определено как неопределённое или определяемое реализацией, или если компилятор предоставляет расширение языка, которое позволяет ему принимать такую программу. Диагностика неопределённого поведения не требуется в других случаях.
Неопределённое поведение и оптимизация
Поскольку корректные программы на C свободны от неопределённого поведения, компиляторы могут генерировать неожиданные результаты при компиляции программы, которая фактически имеет неопределённое поведение при включённой оптимизации:
Например,
Переполнение знакового целого числа
int foo(int x)
{
return x + 1 > x; // either true or UB due to signed overflow
}может быть скомпилировано как (пример)
foo:
mov eax, 1
retОбращение за пределы массива
int table[4] = {0};
int exists_in_table(int v)
{
// return 1 in one of the first 4 iterations or UB due to out-of-bounds access
for (int i = 0; i <= 4; i++)
if (table[i] == v)
return 1;
return 0;
}Может быть скомпилировано как (пример)
exists_in_table:
mov eax, 1
retНеинициализированный скаляр
_Bool p; // uninitialized local variable
if (p) // UB access to uninitialized scalar
puts("p is true");
if (!p) // UB access to uninitialized scalar
puts("p is false");Может вывести следующий результат (наблюдалось со старой версией gcc):
p is true p is false
size_t f(int x)
{
size_t a;
if (x) // either x nonzero or UB
a = 42;
return a;
}Может быть скомпилировано как (пример)
f:
mov eax, 42
retНекорректный скаляр
int f(void)
{
_Bool b = 0;
unsigned char* p = (unsigned char*)&b;
*p = 10;
// reading from b is now UB
return b == 0;
}Может быть скомпилировано как (пример)
f:
mov eax, 11
retОбращение к нулевому указателю
int foo(int* p)
{
int x = *p;
if (!p)
return x; // Either UB above or this branch is never taken
else
return 0;
}
int bar()
{
int* p = NULL;
return *p; // Unconditional UB
}может быть скомпилировано как (пример)
foo:
xor eax, eax
ret
bar:
ret Доступ к указателю, переданному в realloc
Выберите clang для наблюдения за отображаемым выводом
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *p = (int*)malloc(sizeof(int));
int *q = (int*)realloc(p, sizeof(int));
*p = 1; // UB access to a pointer that was passed to realloc
*q = 2;
if (p == q) // UB access to a pointer that was passed to realloc
printf("%d%d\n", *p, *q);
}Возможный вывод:
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:2023):
- 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 ОПРЕДЕЛЕНИЯ ТЕРМИНОВ
Внешние ссылки
| 1. | Что каждый программист на C должен знать о неопределённом поведении #1/3 |
| 2. | Что каждый программист на C должен знать о неопределённом поведении #2/3 |
| 3. | Что каждый программист на C должен знать о неопределённом поведении #3/3 |
| 4. | Неопределённое поведение может привести к путешествию во времени (в частности, но путешествие во времени самое интересное) |
| 5. | Понимание переполнения целых чисел в C/C++ |
| 6. | Неопределённое поведение и последняя теорема Ферма |
| 7. | Развлечения с указателями NULL, часть 1 (локальная уязвимость в Linux 2.6.30, вызванная неопределённым поведением из-за обращения к нулевому указателю) |
См. также
| Документация C++ по Неопределённому поведению |
© cppreference.com
Licensed under the Creative Commons Attribution-ShareAlike Unported License v3.0.
https://en.cppreference.com/w/c/language/behavior