Как undefined работает undefined?

Я не уверен, что понимаю, насколько поведение undefined может поставить под угрозу программу.

Скажем, у меня есть этот код:

#include <stdio.h>

int main()
{
    int v = 0;
    scanf("%d", &v);
    if (v != 0)
    {
        int *p;
        *p = v;  // Oops
    }
    return v;
}

Является ли поведение этой программы undefined только для тех случаев, когда v отличное от нуля или оно undefined, даже если v равно нулю?

Ответы

Ответ 1

Я бы сказал, что поведение undefined только в том случае, если пользователи вставляют любое число, отличное от 0. В конце концов, если код нарушения кода не выполняется, условия для UB не выполняются (т. инициализированный указатель не создается ни разыменованным).

Смысл этого можно найти в стандарте, в 3.4.3:

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

Это, по-видимому, означает, что, если бы такие "ошибочные данные" были правильны, поведение было бы идеально определено, что, похоже, в значительной степени применимо к нашему делу.


Дополнительный пример: целочисленное переполнение. Любая программа, которая дополняет данные, предоставленные пользователем, без проведения обширной проверки на них, подчиняется такому типу поведения undefined, но добавление является UB только тогда, когда пользователь предоставляет такие конкретные данные.

Ответ 2

Поскольку это имеет language-lawyer, У меня есть чрезвычайно аргумент о том, что поведение программы undefined вне зависимости от пользовательского ввода, но не по причинам, которые вы могли бы ожидать, хотя он может быть четко определен (когда v==0) в зависимости от реализации.

Программа определяет main как

int main()
{
    /* ... */
}

C99 5.1.2.2.1 говорит, что функция main должна быть определена либо как

int main(void) { /* ... */ }

или

int main(int argc, char *argv[]) { /* ... */ }

или эквивалент; или каким-либо другим способом реализации.

int main() не эквивалентен int main(void). Первый, как декларация, говорит, что main принимает фиксированное, но неуказанное число и тип аргументов; последний говорит, что он не принимает никаких аргументов. Разница в том, что рекурсивный вызов main, такой как

main(42);

является нарушением ограничения, если вы используете int main(void), но не используете int main().

Например, эти две программы:

int main() {
    if (0) main(42); /* not a constraint violation */
}


int main(void) {
    if (0) main(42); /* constraint violation, requires a diagnostic */
}

не эквивалентны.

Если документы реализации, которые он принимает int main() как расширение, то это не относится к этой реализации.

Это очень важная точка (о которой не все согласны), и ее легко избежать, объявив int main(void) (что вы должны делать в любом случае: все функции должны иметь прототипы, а не декларации/определения старого стиля).

На практике каждый компилятор, который я видел, принимает int main() без жалоб.

Чтобы ответить на вопрос, который был предназначен:

После того, как это изменение сделано, поведение программы хорошо определено, если v==0 и undefined, если v!=0. Да, определенность поведения программы зависит от пользовательского ввода. В этом нет ничего необычного.

Ответ 3

Позвольте мне привести аргумент, почему я думаю, что это все еще undefined.

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

#include <stdio.h>

int
main()
{
    int v;
    scanf("%d", &v);
    if (v != 0)
    {
        printf("Hello\n");
        int *p;
        *p = v;  // Oops
    }
    return v;
}

Что делает эта программа, если вы предоставляете "1" в качестве входных данных? Если вы отвечаете: "Он печатает Hello, а затем сбой", вы ошибаетесь. "Undefined поведение" не означает, что поведение какого-либо конкретного оператора undefined; это означает, что поведение всей программы undefined. Компилятору разрешено предположить, что вы не вступаете в поведение undefined, поэтому в этом случае он может предположить, что v отличен от нуля и просто не испускает какой-либо из заключенного в скобки кода вообще, включая printf.

Если вы думаете, что это маловероятно, подумайте еще раз. GCC может не выполнять этот анализ точно, но он выполняет очень похожие. Мой любимый пример, который на самом деле иллюстрирует реальность:

int test(int x) { return x+1 > x; }

Попробуйте написать небольшую тестовую программу для печати INT_MAX, INT_MAX+1 и test(INT_MAX). (Обязательно включите оптимизацию.) Типичная реализация может показать, что INT_MAX - 2147483647, INT_MAX+1 - -2147483648, а test(INT_MAX) - 1.

Фактически GCC компилирует эту функцию для возврата константы 1. Почему? Поскольку целочисленное переполнение - это поведение undefined, поэтому компилятор может предположить, что вы этого не делаете, поэтому x не может равняться INT_MAX, поэтому x+1 больше, чем x, поэтому эта функция может безоговорочно возвращаться.

Undefined поведение может и приводит к переменным, которые не равны сами по себе, отрицательным числам, которые сравнивают больше положительных чисел (см. пример выше) и другим странным поведением. Чем умнее компилятор, тем более странным поведением.

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

И в реальной жизни, если код мертв, вы должны его удалить; если он не мертв, вы не должны вызывать поведение undefined. Так что это мой ответ на ваш вопрос.

Ответ 4

Если v равно 0, ваше случайное назначение указателя никогда не будет выполнено, и функция вернет нуль, поэтому это не поведение undefined

Ответ 5

Когда вы объявляете переменные (особенно явные указатели), выделяется часть памяти (обычно это int). Этот мир памяти помечен как free для системы, но старое значение, хранящееся там, не очищается (это зависит от распределения памяти, которое реализуется компилятором, оно может заполнить место нулями), поэтому ваш int *p будет имеют случайное значение (хлам), которое оно должно интерпретировать как integer. Результатом является место в памяти, где p указывает на (p pointee). Когда вы пытаетесь dereference (ака. Получить доступ к этой части памяти), он будет (почти каждый раз) занят другим процессом/программой, поэтому попытка изменить/изменить некоторую другую память приведет к проблемам access violation memory manager.

Итак, в этом примере любое другое значение, равное 0, приведет к поведению undefined, потому что никто не знает, к чему в данный момент укажет *p.

Я надеюсь, что это объяснение поможет.

Редактировать: Ах, извините, еще несколько ответов впереди меня:)

Ответ 6

Это просто. Если кусок кода не выполняется, он не имеет поведения!!!, независимо от того, задано оно или нет.

Если ввод равен 0, код внутри if не запускается, поэтому он зависит от остальной части программы, чтобы определить, определено ли поведение (в этом случае оно определено).

Если вход не равен 0, вы выполняете код, который, как мы все знаем, является примером поведения undefined.

Ответ 7

Я бы сказал, что он делает всю программу undefined.

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

Например, компилятор может добавить в программу сообщение "эта программа может быть опасной", если она обнаруживает поведение undefined. Это изменит результат, независимо от того, равен или нет v.

Ответ 8

Ваша программа очень хорошо определена. Если v == 0, то он возвращает ноль. Если v!= 0, то он брызгает по некоторой случайной точке в памяти.

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

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

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