Spec-Zone.ru › C

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

Стандарт языка 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: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++ о неопределённом поведении

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

1. Что должен знать каждый программист на C о неопределённом поведении #1/3
2. Что должен знать каждый программист на C о неопределённом поведении #2/3
3. Что должен знать каждый программист на C о неопределённом поведении #3/3
4. Неопределённое поведение может привести к путешествиям во времени (и не только, но путешествия во времени — самое необычное)
5. Понимание переполнения целых чисел в C/C++
6. Неопределённое поведение и последняя теорема Ферма
7. Забавы с нулевыми указателями, часть 1 (локальная уязвимость в Linux 2.6.30, вызванная неопределённым поведением при разыменовании нулевого указателя)

© cppreference.com
Licensed under the Creative Commons Attribution-ShareAlike Unported License v3.0.
https://en.cppreference.com/c/language/behavior

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API