Насколько неопределенным является неопределенное поведение?

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

Допустим, у меня есть этот код:

#include <stdio.h>

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

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

8 ответов

Решение

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

Намек на это можно найти в стандарте, в 3.4.3:

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

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


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

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

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

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

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

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и не определено, если v!=0, Да, определенность поведения программы зависит от пользовательского ввода. В этом нет ничего необычного.

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

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

#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, а затем падает", вы ошибаетесь. "Неопределенное поведение" не означает, что поведение некоторого конкретного утверждения не определено; это означает, что поведение всей программы не определено. Компилятору разрешается предполагать, что вы не участвуете в неопределенном поведении, поэтому в этом случае он может предполагать, что 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. Почему? Поскольку целочисленное переполнение является неопределенным поведением, поэтому компилятор может предположить, что вы этого не делаете, поэтому x не может быть равен INT_MAX, следовательно x+1 больше, чем xследовательно, эта функция может возвращать 1 безоговорочно.

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

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

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

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

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

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

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

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

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

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

Если input не равен 0, вы выполняете код, который, как нам всем известно, является случаем неопределенного поведения.

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

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

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

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

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

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

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

Другие вопросы по тегам