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

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

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

#include <stdio.h>

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

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


person user541686    schedule 31.10.2011    source источник
comment
почему значение v имеет значение?   -  person Jim Rhodes    schedule 01.11.2011
comment
@JimRhodes: потому что, если v равно нулю, ошибочный фрагмент кода не выполняется.   -  person Matteo Italia    schedule 01.11.2011
comment
@MatteoItalia: я знаю это, но код плохой, так что какая разница, если v равно 0   -  person Jim Rhodes    schedule 01.11.2011
comment
@JimRhodes: дело не в том, хороший код или плохой, а в том, что в стандарте он демонстрирует неопределенное поведение независимо от значения, вставленного пользователем.   -  person Matteo Italia    schedule 01.11.2011
comment
@MatteoItalia: в чем смысл этого вопроса? Представленный код имеет путь, который (думаю, с этим никто не спорит) является недопустимым/неправильным. Споры о том, действительна ли программа в обстоятельствах, не соответствующих этому пути, ничего вам не дадут. Эти недопустимые пути существуют. Программа в целом недействительна.   -  person Mat    schedule 01.11.2011
comment
@Mat: как и во всех вопросах языкового юриста, речь идет о стандартных придирках, а не о полезности. Кроме того, int a, b, c; scanf("%d", "%d", &a, &b); c=a+b;. Этот код действителен? Вы бы так сказали, но в определенных обстоятельствах (когда a+b переполняется) это демонстрирует неопределенное поведение. Означает ли это, что программа в целом недействительна ни при каких обстоятельствах?   -  person Matteo Italia    schedule 01.11.2011
comment
Поведение программы не определено только тогда, когда выполняется оператор, вызывающий неопределенное поведение. Да, это плохая практика программирования - иметь достижимые пути кода, которые вызывают UB, но UB не вызывается, пока нулевые или нечисловые данные считываются со стандартного ввода. Имейте в виду, что многие реальные программы имеют аналогичные случаи условного вызова UB из-за невозможности проверить возвращаемое значение malloc или аналогичные проблемы (UB вызывается при разыменовании указателя только если malloc вернул 0).   -  person R.. GitHub STOP HELPING ICE    schedule 01.11.2011
comment
@R..: поведение становится неопределенным, как только состояние выполнения таково, что компилятор имеет право предположить, что будет вызвано неопределенное поведение. Если цикл не имеет побочных эффектов, компилятору разрешается распространять эффекты Undefined Behavior, которые были бы неизбежны после завершения цикла, для кодирования перед циклом без необходимости показывать, что сам цикл завершается.   -  person supercat    schedule 24.04.2015


Ответы (8)


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

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

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

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


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

person Matteo Italia    schedule 01.11.2011
comment
Указатель все еще может быть выделен, в зависимости от компилятора. Однако это не проблема, так как да, он не будет разыменован. - person chacham15; 01.11.2011
comment
Это только мне кажется, или мне кажется противоречащим сказать, что его UB только в том случае, если пользователь вводит 0. Если пользователь ввел 0, оскорбительный код не будет запущен, и условия для UB не будут выполнены - person Nick Rolando; 01.11.2011
comment
Я на секунду запутался в вашем примере, но теперь я понимаю, что вы имеете в виду, спасибо. +1 - person user541686; 01.11.2011
comment
Еще один отличный пример: любая программа, которая вызывает gets, когда не находится в EOF в потоке с неизвестным содержимым. В зависимости от ввода эта программа может иметь неопределенное поведение. - person R.. GitHub STOP HELPING ICE; 01.11.2011
comment
Я думаю, что это правильно в том смысле, что сама программа хорошо определена, когда пользователь вводит 0. Однако секция с неопределенным поведением может по-прежнему вызывать неожиданные вещи во время компиляции, например, компилятор может заметить, что v != 0 ведет к UB и удалить весь if в предположении, что поэтому всегда v == 0. - person Arkku; 17.09.2018

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

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

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

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

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

or as

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

person Keith Thompson    schedule 01.11.2011
comment
Вау, я никогда этого не знал. Так чем же int main() отличается от int main(...)? - person user541686; 01.11.2011
comment
@Mehrdad: int main(...) — синтаксическая ошибка; вариационная функция должна иметь по крайней мере один именованный параметр. В более общем случае функции с переменным числом аргументов (такие как printf) можно законно вызывать с переменными числами и типами аргументов. Функции с непрототипными объявлениями в старом стиле должны вызываться с точным числом и типом (типами) аргументов, указанными в определении, но компилятор не будет диагностировать вызовы с неверными аргументами. Вот почему в язык были добавлены прототипы. - person Keith Thompson; 01.11.2011
comment
Ха, интересно; +1 от меня. - person user541686; 01.11.2011
comment
int main() эквивалентен int main(void) за исключением того, что первый не предоставляет прототип. Оба объявляют функцию, не принимающую аргументов, и int main() вполне допустимо или эквивалентно. - person R.. GitHub STOP HELPING ICE; 01.11.2011
comment
@R..: ... за исключением того, что первый не предоставляет прототип. Тогда они не эквивалентны. Посмотрите примеры, которые я только что добавил в свой ответ. Почему вы предполагаете, что фраза или ее эквивалент допускают эту разницу? - person Keith Thompson; 01.11.2011
comment
Если вы вызываете main(42), используемая версия определяет, является ли это нарушением ограничений или просто UB, но в любом случае это ошибочная программа. Определение main с помощью int main() по-прежнему вполне допустимо и эквивалентно, поскольку в любом случае main определяется как функция, не принимающая аргументов и возвращающая int. - person R.. GitHub STOP HELPING ICE; 01.11.2011
comment
@R..: Две программы, которые отличаются только тем, имеет ли main объявление параметра void, не эквивалентны; один нарушает ограничение, другой нет. Эти два определения похожи в том, что они оба определяют функцию, не принимающую аргументов и возвращающую целое число, но у них есть и другие отличия. Откуда вы знаете, что фраза или ее эквивалент относится только к аргументам и возвращаемому типу? Является ли int foo(void) эквивалентом int main(void)? (Я обновил примеры.) - person Keith Thompson; 01.11.2011
comment
Хорошо, давайте посмотрим на фактический язык: main должен быть определен с возвращаемым типом int и без параметров... или.... Сноска (Таким образом, int можно заменить именем typedef, определенным как int, или типом argv может быть записано как char ** argv и т. д.) для эквивалента указывает, что это не имеет отношения к нашему обсуждению. Если я определяю main как int main() {...}, я, как того требует стандарт, определяю main как функцию, возвращающую int и не принимающую никаких параметров. - person R.. GitHub STOP HELPING ICE; 01.11.2011
comment
Кстати, в качестве мысленного эксперимента, что бы вы сказали о int main(void); int main() { }? - person R.. GitHub STOP HELPING ICE; 01.11.2011
comment
@R..: Вы игнорируете int main(void) { /* ... */ } По тому же аргументу вы можете игнорировать int main(int argc, char *argv[]) { /* ... */ } и определить int main(char *argc, double argv) { /* ... */ }. Показанные определения — это не просто примеры, это требования. Что касается вашего мысленного эксперимента, main не определено ни одним из двух явно разрешенных способов. Сочетание объявления и определения, насколько я могу судить, эквивалентно правильному определению, но определение само по себе таковым не является, и именно об этом говорится в стандарте. - person Keith Thompson; 01.11.2011
comment
Единственная разница между int main() {...} и int main(void) {...} заключается в предоставляемых ими объявлениях. Как определения они идентичны. Если вы хотите продвинуть это дальше, побеспокоите кого-нибудь в комитете. Я считаю вопрос закрытым. - person R.. GitHub STOP HELPING ICE; 02.11.2011
comment
@R..: Я допускаю, что стандарт может интерпретироваться так, как вы описываете; вы не единственный, кто это делает. Я считаю, что моя интерпретация более соответствует фактической формулировке и, по крайней мере, не противоречит ей. Что касается намерения, я подозреваю, что комитет просто не думал о определениях в старом стиле, когда писал этот раздел; если бы они это сделали, они, вероятно, подробно осветили бы этот случай. - person Keith Thompson; 02.11.2011
comment
Более 3 лет назад я писал: Две программы, отличающиеся только тем, имеет ли main объявление параметра void, не эквивалентны; один нарушает ограничение, другой нет. Это было неясно. Думаю, я имел в виду, что две такие программы с таким вызовом, как main(42), отличаются тем, что одна нарушает ограничение, а другая нет. - person Keith Thompson; 19.11.2014
comment
Чтобы быть более придирчивым, ваша придирка на самом деле неверна. Различие между () и (void) применяется только в деклараторе функции, который не является частью определения этой функции (C99, раздел 6.7.5.3, параграф 14). В данном случае это является частью определения функции, поэтому int main() { ... } и int main(void) { ... } идентичны. - person Chris Dodd; 18.09.2016
comment
@ChrisDodd: Нет, это не так. Они идентичны как определения, но определение также предоставляет объявление, а объявления различаются. Это: int main(void) { return main(42); } является нарушением ограничения. Это: int main() { return main(42); } не является, но имеет неопределенное поведение. - person Keith Thompson; 19.09.2016
comment
@KeithThompson: второе также является нарушением ограничения, поскольку () в объявлении, которое также является определением, не указывает аргументов, как и (void). Он только указывает неопределенное количество аргументов в объявлении, которое также не является определением - см. 6.7.5.3. - person Chris Dodd; 19.09.2016
comment
@ChrisDodd: я думаю, вы имеете в виду C99 6.7.5.3p14 (C11/N1570) 6.7.6.3p14). Да, он указывает, что функция не имеет параметров, но, как и отдельное объявление int foo(), не является прототипом и не указывает, что функция не ожидает аргументов в вызов. И gcc -std=c11 -pedantic не диагностирует int main() { return main(42); }, но диагностирует int main(void) { return main(42); }; аналогично для лязга. Судя по всему, авторы gcc и clang разделяют мою интерпретацию. - person Keith Thompson; 19.09.2016

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

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

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

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

person Nemo    schedule 01.11.2011
comment
Я не понимаю, как ваш пример поддерживает точку зрения, которую вы пытаетесь сделать. Ваш пример выполняется безоговорочно. Мой код выполняется условно. Как они связаны? - person user541686; 01.11.2011
comment
Мой printf пример более точен. Неопределенная конструкция может повлиять на поведение программы еще до ее выполнения. (В этом случае это может эффективно предотвратить выполнение printf вообще.) Я не думаю, что это слишком большая натяжка от того, прежде чем он даже выполнится, до того, даже если он не будет выполнен. Хотя я не думаю, что кто-то дал исчерпывающий ответ на ваш вопрос со ссылкой на спецификацию. - person Nemo; 01.11.2011
comment
@Nemo: Это натяжка. Почти каждая программа, когда-либо написанная на C (например, каждая, использующая указатели), имеет недостижимые пути кода, вызывающие UB. Но поведение программы четко определено до тех пор, пока эти пути никогда не используются. Единственное, что сбивает с толку в этом вопросе, это то, что путь зависит от ввода. Это делает программу небезопасной, но UB по-прежнему зависит от получения неверного ввода. - person R.. GitHub STOP HELPING ICE; 01.11.2011
comment
@Nemo: компилятор может создать код для выполнения всего, что он хочет, как только программа получит комбинацию входных данных, которая неизбежно вызовет неопределенное поведение. Что касается стандарта C, код может даже построить машину времени и стереть память каждого о том, что программа сделала что-то разумное до того, как такой ввод был получен. Тем не менее, если существует какая-либо комбинация входных значений, которая будет давать определенное поведение, сгенерированный компилятором код должен давать четко определенное поведение, если эти входные значения действительно предоставлены. - person supercat; 05.11.2011
comment
Будет ли поведение такой программы неопределенным, если ее выходные данные будут отправлены в сломанный канал, а сброс будет выполнен после printf, что приведет к завершению программы до выполнения UB? - person supercat; 24.04.2015
comment
Necropost, но @supercat, я полагаю, что компилятору все равно будет разрешено изменить порядок печати (и сброса), чтобы он выполнялся после UB, поскольку в отсутствие UB (что может предположить компилятор) разница не будет обнаружена. Сначала может быть выполнено переупорядочение, а затем обнаружен UB, например, вызывающий удаление всего тела if. Теперь я не совсем уверен, что если бы у вас был int *p в области global, то теоретически вы могли бы назначить действительный адрес извне (например, отладчик, который делает это, увидев приветствие на выходе), но я думаю, что это все еще может быть UB. По крайней мере без volatile. - person Arkku; 17.09.2018
comment
@Arkku: я думаю, законность переупорядочивания будет определяться тем, признают ли определенные реализацией аспекты того, как выполняется ввод-вывод, возможность ввода-вывода, вызывающего синхронный сигнал, или иное нарушение выполнения программы. Учитывая sig_atomic_t flag; в глобальной области видимости, компилятору с данным printf("Raises SIGPIPE"); flag=1; будет разрешено переупорядочивать запись в flag перед printf, если он не распознает возможность синхронного сигнала от printf, но если он распознает эту возможность, и сигнал от printf происходит... - person supercat; 17.09.2018
comment
@supercat На самом деле кажется, что по стандарту 5.1.2.3.2 вызов функции, которая изменяет файл или обращается к объекту volatile, является побочным эффектом, поэтому, если printf делает то же самое (и stdout, вероятно, считается файлом, так что это так) , изменение порядка технически не разрешено даже в вашем предыдущем случае. (На практике я думаю, что компиляторы могут позволить себе вольности, особенно со стандартной библиотечной функцией, такой как printf.) - person Arkku; 17.09.2018
comment
... и приводит к тому, что exit(1); никогда не возвращается, то все, что следует за printf, следует рассматривать как код, следующий за if(hardToRecognizeCondition) exit(1) - как код, который может потребоваться выполнить, но может быть не разрешено выполнять. К сожалению, переупорядочивание операций между действиями, которые могут генерировать синхронные сигналы, определяемые реализацией, попадает в категорию поведения, которое некоторые разработчики компиляторов сочли бы достаточно глупым, чтобы они не чувствовали необходимости явно говорить, что они воздерживаются от такой глупости, но которые другие авторы компиляторов рассматривают как полезная оптимизация. - person supercat; 17.09.2018
comment
@Arkku: стандарт, как правило, не требует, чтобы реализации признавали возможность того, что доступ с квалификацией volatile или операция ввода-вывода могут повлиять на любые аспекты состояния абстрактной машины, включая повышение синхронного сигнала, потому что такое требование без необходимости ухудшит оптимизацию. в тех случаях, когда такие эффекты на самом деле не могут возникнуть, и ожидается, что составители компиляторов должны быть достаточно знакомы со своими целевыми платформами и областями приложений, чтобы знать, будет ли такое признание уместным, независимо от того, предписывается ли это Стандартом или нет. - person supercat; 17.09.2018
comment
@Arkku: Иными словами, если бы операции энергозависимой записи должны были записывать адреса и данные на какой-либо носитель, содержимое которого не может быть просмотрено работающей программой (например, распечатка), Стандарт требовал бы, чтобы оптимизации не влияли на последовательность создаваемых записей, но не требует, чтобы реализации распознавали какие-либо побочные эффекты помимо этого. - person supercat; 17.09.2018
comment
@supercat Да, я думаю, вы правы, и поэтому просто иметь энергозависимую операцию перед UB было бы недостаточно, если сам UB является энергонезависимым и, таким образом, может быть заказан раньше. Но что, если бы это был глобальный изменчивый указатель (с назначенным в программе только NULL), которому предшествовала некоторая изменчивая операция, которая вызвала бы внешнюю программу для изменения значения указателя на действительный адрес? знак равно - person Arkku; 17.09.2018
comment
@Arkku: Насколько я могу судить, если оптимизация полностью не отключена, ни gcc, ни clang не допустят возможности того, что операция над объектом с квалификацией volatile может взаимодействовать со значениями объектов, которые были к которым обращались ранее или к которым будут обращаться позже, даже если они нацелены на аппаратные платформы, которые определяют механизмы, с помощью которых это может произойти. ИМХО, качественная реализация, подходящая для низкоуровневого программирования на одноядерной машине, не должна требовать ничего, кроме volatile, для реализации мьютекса чередующегося доступа, защищающего неквалифицированные объекты, но... - person supercat; 17.09.2018
comment
... gcc и clang не поддерживают такую ​​семантику, за исключением случаев, когда оптимизация отключена. - person supercat; 17.09.2018
comment
@supercat: Вы немного упускаете мою мысль. В этом примере компилятору разрешено предположить, что v равно нулю... Потому что в противном случае он увидит, что вы вызовете UB, что, как он может предположить, никогда не происходит. Это не имеет ничего общего с изменением порядка операций. - person Nemo; 18.09.2018
comment
@Nemo: реализация, которая соответствует требованиям, но не предназначена для целей, связанных с низкоуровневым программированием, ведет себя таким образом, что делает ее непригодной для низкоуровневого программирования. Реализация хорошего качества, предназначенная для низкоуровневого программирования, не может этого сделать [поскольку это сделало бы ее непригодной для этой цели]. Если действие вызывает синхронный сигнал, а обработчик сигнала вызывает exit или longjmp, то код, следующий за действием, вызвавшим сигнал, не будет выполняться и, таким образом, не может повлиять на какой-либо аспект поведения программы. Реализация, которая распознает... - person supercat; 18.09.2018
comment
... возможность того, что действие может вызвать синхронный сигнал, не может слепо предполагать, что такого сигнала не будет. Реализация может соответствовать требованиям, не требуя возможности синхронных сигналов, но это не означает, что компилятор может быть высококачественной реализацией, подходящей для низкоуровневого программирования на платформе, где такие сигналы могут возникать без такого распознавания. - person supercat; 18.09.2018

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

person Peter    schedule 01.11.2011

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

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

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

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

person ludesign    schedule 01.11.2011
comment
Дело не в том, как на самом деле компилятор реализует указатели и прочее, здесь речь идет об абстрактной машине стандарта. Кстати, в любой современной ОС с изоляцией памяти между процессами случайный адрес не будет занят другим процессом, потому что каждый процесс имеет свое виртуальное адресное пространство, и указатели могут ссылаться только на него. - person Matteo Italia; 01.11.2011
comment
Что вы подразумеваете под (обычно int)? - person Keith Thompson; 01.11.2011
comment
Извините, я хотел сказать, что все переменные на самом деле являются указателями, все указатели на самом деле являются целочисленными переменными, содержащими адрес их указателя; Тем не менее, я должен перестать просматривать stackoverflow поздно ночью, пытаясь выразить себя так, как я не могу. :) - person ludesign; 01.11.2011

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

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

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

person Shahbaz    schedule 01.11.2011
comment
Я не уверен, что это обязательно правда. Как насчет if (input==9) {int foo[1000000000]; foo[0]=1;} Это может привести к неопределенному поведению независимо от значения 'input'. - person supercat; 05.11.2011

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

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

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

person Pubby    schedule 01.11.2011
comment
вся программа не определена Это ничего не значит. Выполнение программы может иметь неопределенное поведение. Это означает только то, что поведение выполнения этой программы не определено стандартом. включая изменение частей, не связанных с ним. Чистая чепуха. - person curiousguy; 01.11.2011

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

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

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

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

person Adam Hawes    schedule 01.11.2011
comment
неинициализированные переменные не имеют известного значения Неинициализированные переменные не имеют значения вообще. - person curiousguy; 01.11.2011
comment
В этом вы ошибаетесь. В C все переменные имеют значение. Неинициализированные имеют случайные (или неизвестные) значения. Рассмотрим этот фрагмент: int i; printf(%d\n, я);. Скомпилируйте его с отключенными предупреждениями и посмотрите, что он напечатает! - person Adam Hawes; 14.11.2011
comment
Нет, вы ошибаетесь. В C и C++ переменные, в которые не была произведена запись, не могут быть прочитаны. Поведение при чтении неинициализированной переменной не определено. В C все переменные имеют значение. Каково значение неинициализированной переменной? Неинициализированная переменная не имеет значения. Нельзя сказать, что ее значение равно нулю или отлично от нуля, потому что она не имеет значения. - person curiousguy; 19.11.2011