Насколько неопределенным является неопределенное поведение? - PullRequest
34 голосов
/ 01 ноября 2011

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

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

#include <stdio.h>

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

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

Ответы [ 8 ]

15 голосов
/ 01 ноября 2011

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

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

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

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

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

12 голосов
/ 01 ноября 2011

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

9 голосов
/ 01 ноября 2011

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

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

#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 просто вызывает ошибку сегмента» более ошибочно, чем они могут себе представить, и они ошибаются с каждым поколением компилятора.

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

2 голосов
/ 01 ноября 2011

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

1 голос
/ 01 ноября 2011

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

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

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

1 голос
/ 01 ноября 2011

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

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

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

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

0 голосов
/ 01 ноября 2011

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

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

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

0 голосов
/ 01 ноября 2011

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

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

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

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

...