На языке C, если инициализировать массив следующим образом:
int a[5] = {1,2};
тогда все элементы массива, которые не инициализированы явно, будут неявно инициализированы нулями.
Но если я инициализирую такой массив:
int a[5] = {a[2]=1};
printf("%d %d %d %d %d\n", a[0], a[1],a[2], a[3], a[4]);
выход:
1 0 1 0 0
Я не понимаю, почему a[0] печатает 1 вместо 0? Это неопределенное поведение?
Примечание: Этот вопрос задавали в интервью.
Выражение a[2]=1 оценивается как 1.
Очень глубокий вопрос. Интересно, знает ли ответ интервьюер. Я не. Действительно, якобы значение выражения a[2] = 1 равно 1, но я не уверен, разрешено ли вам принимать результат назначенного выражения инициализатора в качестве значения первого элемента. Тот факт, что вы добавили тег «адвокат», означает, что я думаю, что нам нужен ответ со ссылкой на стандарт.
Что ж, если это их любимый вопрос, возможно, вы избежали пули. Лично я предпочитаю, чтобы письменное упражнение по программированию (с доступом к компилятору и отладчику) занимало несколько часов, а не вопросы в стиле «туз», подобные приведенным выше. Я мог бы предполагать ответить, но не думаю, что у него будет реальная фактическая основа.
@Bathsheba Особенно, если ответ на побочный вопрос заключает, что это не совсем точно. Тогда на этот вопрос отвечает только конкретный компилятор.
Не могли бы мы закрыть это как дубликат дополнительного вопроса?
@Bathsheba Я бы сделал наоборот, поскольку ответ здесь теперь отвечает на оба вопроса.
@KamiKaze: Я попросил модов объединить их; Хотя нет гарантии, что они это сделают. Мне кажется, что второй вопрос немного более каноничен, если вы понимаете, что я имею в виду. Но посмотрим, что думают начальство!
@Bathsheba будет лучшим. Тем не менее, я бы отдал должное за вопрос OP, поскольку он придумал эту тему. Но не мне решать, что я считаю «правильным».
@Bathsheba Назначенные инициализаторы должны начинаться с [ или .. a[2] = 1 - нормальное выражение, без обозначения.
Возможно ли это в Ruby? просто любопытно !
Может быть связано с Странные значения при инициализации массива с использованием назначенных инициализаторов
Я думаю, что a [0] = 1 зависит от порядка байтов в архитектуре ЦП ....
Я запомню эту конструкцию, если она мне когда-нибудь понадобится для код игры в гольф!
Ох, я действительно хочу, чтобы работодатели перестали использовать кодовые вопросы по гольфу для проверки кандидатов. Так непродуктивно.
Согласился, что это очень плохой вопрос для интервью. Я был бы счастлив, если бы у меня не было этой работы.





I don't understand, why does
a[0]print1instead of0?
Предположительно, a[2]=1 сначала инициализирует a[2], и результат выражения используется для инициализации a[0].
Из N2176 (проект C17):
6.7.9 Initialization
- The evaluations of the initialization list expressions are indeterminately sequenced with respect to one another and thus the order in which any side effects occur is unspecified.154)
Казалось бы, выход 1 0 0 0 0 тоже был возможен.
Вывод: не пишите инициализаторы, которые изменяют инициализированную переменную на лету.
Эта часть не применяется: здесь есть только одно выражение инициализатора, поэтому его не нужно ни с чем связывать.
@melpomene Существует экспрессия {...}, которая инициализирует a[2] в 0, и субэкспрессию a[2]=1, которая инициализирует a[2] в 1.
{...} - это список инициализаторов в фигурных скобках. Это не выражение.
@melpomene Хорошо, ты можешь быть здесь. Но я бы все же сказал, что есть еще 2 конкурирующих побочных эффекта, так что абзац остается в силе.
@melpomene нужно упорядочить две вещи: первый инициализатор и установка других элементов на 0
@ M.M Именно поэтому цитата из 6.7.9 / 23 неприменима: она охватывает только последовательность между выражениями инициализатора, а не между выражением инициализатора и инициализацией подобъектов (к чему сводится этот вопрос).
TL; DR: Я не думаю, что поведение int a[5] = {a[2]=1}; четко определено, по крайней мере, в C99.
Забавно то, что единственный бит, который имеет смысл для меня, - это та часть, о которой вы спрашиваете: a[0] установлен на 1, потому что оператор присваивания возвращает значение, которое было присвоено. Все остальное непонятно.
Если бы код был int a[5] = { [2] = 1 }, все было бы просто: это назначенный инициализатор, устанавливающий a[2] на 1, а все остальное - на 0. Но с { a[2] = 1 } у нас есть неназначенный инициализатор, содержащий выражение присваивания, и мы падаем в кроличью нору.
Вот что я нашел на данный момент:
a должен быть локальной переменной.
6.7.8 Initialization
- All the expressions in an initializer for an object that has static storage duration shall be constant expressions or string literals.
a[2] = 1 не является постоянным выражением, поэтому a должен иметь автоматическое хранение.
a находится в области своей собственной инициализации.
6.2.1 Scopes of identifiers
- Structure, union, and enumeration tags have scope that begins just after the appearance of the tag in a type specifier that declares the tag. Each enumeration constant has scope that begins just after the appearance of its defining enumerator in an enumerator list. Any other identifier has scope that begins just after the completion of its declarator.
Декларатором является a[5], поэтому переменные находятся в области видимости при их собственной инициализации.
a жив при собственной инициализации.
6.2.4 Storage durations of objects
An object whose identifier is declared with no linkage and without the storage-class specifier
statichas automatic storage duration.For such an object that does not have a variable length array type, its lifetime extends from entry into the block with which it is associated until execution of that block ends in any way. (Entering an enclosed block or calling a function suspends, but does not end, execution of the current block.) If the block is entered recursively, a new instance of the object is created each time. The initial value of the object is indeterminate. If an initialization is specified for the object, it is performed each time the declaration is reached in the execution of the block; otherwise, the value becomes indeterminate each time the declaration is reached.
После a[2]=1 есть точка последовательности.
6.8 Statements and blocks
- A full expression is an expression that is not part of another expression or of a declarator. Each of the following is a full expression: an initializer; the expression in an expression statement; the controlling expression of a selection statement (
iforswitch); the controlling expression of awhileordostatement; each of the (optional) expressions of aforstatement; the (optional) expression in areturnstatement. The end of a full expression is a sequence point.
Обратите внимание, что, например, в int foo[] = { 1, 2, 3 } часть { 1, 2, 3 } представляет собой заключенный в фигурные скобки список инициализаторов, каждый из которых имеет точку последовательности после него.
Инициализация выполняется в порядке списка инициализаторов.
6.7.8 Initialization
- Each brace-enclosed initializer list has an associated current object. When no designations are present, subobjects of the current object are initialized in order according to the type of the current object: array elements in increasing subscript order, structure members in declaration order, and the first named member of a union. [...]
- The initialization shall occur in initializer list order, each initializer provided for a particular subobject overriding any previously listed initializer for the same subobject; all subobjects that are not initialized explicitly shall be initialized implicitly the same as objects that have static storage duration.
Однако выражения инициализатора не обязательно вычисляются по порядку.
6.7.8 Initialization
- The order in which any side effects occur among the initialization list expressions is unspecified.
Тем не менее, некоторые вопросы остаются без ответа:
Имеются ли вообще точки последовательности? Основное правило:
6.5 Expressions
- Between the previous and next sequence point an object shall have its stored value modified at most once by the evaluation of an expression. Furthermore, the prior value shall be read only to determine the value to be stored.
a[2] = 1 - это выражение, а инициализация - нет.
Это немного противоречит Приложению J:
J.2 Undefined behavior
- Between two sequence points, an object is modified more than once, or is modified and the prior value is read other than to determine the value to be stored (6.5).
В приложении J говорится, что учитываются любые модификации, а не только модификации выражениями. Но учитывая, что приложения не являются нормативными, мы, вероятно, можем игнорировать это.
Каким образом инициализации подобъекта упорядочиваются по отношению к выражениям инициализатора? Все ли инициализаторы сначала оцениваются (в некотором порядке), затем подобъекты инициализируются результатами (в порядке списка инициализаторов)? Или их можно чередовать?
Думаю, int a[5] = { a[2] = 1 } выполняется следующим образом:
a выделяется при вводе содержащего его блока. На данный момент содержание не определено.a[2] = 1), за которым следует точка последовательности. Это сохраняет 1 в a[2] и возвращает 1.1 используется для инициализации a[0] (первый инициализатор инициализирует первый подобъект).Но здесь все становится нечетким, потому что остальные элементы (a[1], a[2], a[3], a[4]) должны быть инициализированы как 0, но неясно, когда: происходит ли это до оценки a[2] = 1? Если так, a[2] = 1 «выиграет» и перезапишет a[2], но будет ли это присвоение иметь неопределенное поведение, потому что между нулевой инициализацией и выражением присваивания нет точки последовательности? Имеют ли значение точки последовательности (см. Выше)? Или нулевая инициализация происходит после оценки всех инициализаторов? Если это так, a[2] должен в конечном итоге стать 0.
Поскольку стандарт C четко не определяет, что здесь происходит, я считаю, что поведение не определено (по пропуску).
Вместо undefined я бы сказал, что это неопределенные, что оставляет вещи открытыми для интерпретации реализациями.
Я не думаю, что что-либо из того, что было до вашего последнего горизонтального правила, имеет отношение к вопросу; единственная проблема - когда происходит инициализация нуля (что стандарт, похоже, не говорит). Тот же вопрос будет поднят int a[5] = { a[2] = a[3] };.
"мы попадаем в кроличью нору" LOL! Никогда не слышал об этом для UB или неуказанного материала.
Я думаю, что 6.7.8.19 можно читать двумя способами, оба из которых дают «действительные» результаты, но оба являются нежелательными прочтениями. 1) Мы считаем, что a[2] = 1 инициализирует a[2] (хотя он явно не является «инициализатором» для a[2]), потому что он «устанавливает начальное значение» (как указано в скобках в 5.1.2), что делает результат с a[2] == 1 четко определенным ( потому что он был "явно инициализирован" и поэтому не должен быть установлен в 0). Это нежелательно, потому что это незаметно для инициализации без инициализатора, минуя все осторожные слова о том, что порядок инициализаторов не указан ...
... или 2) мы считаем, что a[2] = 1 не инициализируется узкой интерпретацией «инициализировать», когда назначение не «инициализируется», но тогда a[2] должен быть установлен в 0 согласно 6.7.8.19. Это нежелательно, потому что это противоречит наиболее очевидной реализации инициализации (инициализация всего блока до 0, затем инициализаторы процесса), создает чрезмерную нагрузку на компилятор и в любом случае делает все это бесполезным. Я согласен с тем, что, судя по нынешней формулировке, все это, вероятно, следует считать неопределенным, поскольку стандарт недостаточно ясен.
Выше, для «порядка инициализаторов», конечно, прочтите «порядок побочных эффектов инициализаторов». Указан порядок инициализаторов.
Мне кажется, что ключевой вопрос - это порядок неявной инициализации по отношению к инициализации элементов, для которых есть явные инициализаторы. Я не использую 6.7.8 / 19, чтобы указать это, поскольку "порядок списка инициализаторов" не имеет смысла для элементов, у которых нет соответствующего инициализатора. Наблюдаемое поведение следует из неявной инициализации, выполняемой до оценки явных инициализаторов. Я считаю, что это очень разумная - действительно, вероятная - реализация, но я согласен с тем, что стандарт не определяет такой порядок.
@Someprogrammerdude Я не думаю, что он может быть неопределенным ("поведение, при котором настоящий международный стандарт предоставляет две или более возможности и не налагает дополнительных требований, которое выбирается в любом случае"), потому что стандарт на самом деле не предоставляет никаких возможностей для выбора. Здесь просто не говорится о том, что происходит, что, как мне кажется, подпадает под "Неопределенное поведение [...] указывается в этом международном стандарте [...] отсутствием какого-либо явного определения поведения.".
@ BЈовић Это отсылка к Алисе в стране чудес: merriam-webster.com/dictionary/rabbit%20hole
@ BЈовић Это также очень хорошее описание не только для неопределенного поведения, но и для определенного поведения, для объяснения которого нужен поток, подобный этому.
@ M.M OP задал два вопроса: "почему [0] печатает 1 вместо 0? Это неопределенное поведение?" Первая часть моего ответа отвечает на оба: вся конструкция не определена, но значение 1 для a[0] имеет смысл, потому что = возвращает присвоенное значение. Остальная часть моего ответа подробно объясняет, почему я считаю, что поведение не определено.
Могли бы вы тогда сказать, что поведение программы все равно не было бы определено, если бы инициализатором был { a[0] = 1 }?
@JohnBollinger Я думаю, что int a[5] = { a[0] = 1 } определил поведение и должен установить a[0] на 1.
Хм. Я, конечно, исследую вашу логику. Я думаю, что готов согласиться с тем, что оригинал демонстрирует UB, но на данный момент я не готов признать, что предложенная мною альтернатива также не делает этого. Он также имеет два разных побочных эффекта для одного и того же объекта (в данном случае a[0]), относительный порядок которых не определен. То, что они оба производят одинаковый эффект на этот объект, не имеет значения. Рассмотрим, например, оператор a[0] = a[0] = 1;. Думаю понятно, что это УБ выставляет по стандарту.
@JohnBollinger Разница в том, что вы не можете фактически инициализировать подобъект a[0] до оценки его инициализатора, а оценка любого инициализатора включает точку последовательности (потому что это «полное выражение»). Поэтому я считаю, что изменение подобъекта, который мы инициализируем, - это честная игра.
Мое понимание
a[2]=1 возвращает значение 1, поэтому код становится
int a[5] = {a[2]=1} --> int a[5] = {1}
int a[5] = {1} присвоить значение а [0] = 1
Следовательно, он печатает 1 для а [0]
Например
char str[10] = {‘H’,‘a’,‘i’};
char str[0] = ‘H’;
char str[1] = ‘a’;
char str[2] = ‘i;
Это вопрос [языкового юриста], но это не тот ответ, который работает со стандартом, что делает его неактуальным. Кроме того, есть еще 2 более подробных ответа, и ваш ответ, похоже, ничего не добавляет.
Я сомневаюсь, что концепция, которую я опубликовал, неверна? Не могли бы вы пояснить мне это?
Вы просто размышляете о причинах, в то время как в соответствующих частях стандарта уже дан очень хороший ответ. Вопрос не в том, чтобы просто сказать, как это могло произойти. Речь идет о том, что должно происходить в стандарте.
Но человек, который разместил выше вопрос, спросил причину и почему это происходит? Так что только я отказался от этого ответа, но концепция верна.
ОП спросил "Это неопределенное поведение?". Ваш ответ не говорит.
Нет концепции, потому что это неопределенное поведение, предположения о том, как генерируется возможное конечное состояние, имеют меньшее значение, потому что оно может измениться по прихоти. OP спросил, обрабатывается ли это стандартом, но ваш ответ вообще не ссылается на стандарт.
Я пытаюсь дать короткий и простой ответ на загадку: int a[5] = { a[2] = 1 };
a[2] = 1. Это означает, что в массиве написано: 0 0 1 0 0{ }, которые используются для инициализации массива по порядку, он берет первое значение (которым является 1) и устанавливает его на a[0]. Как будто остался бы int a[5] = { a[2] }; там, где у нас уже есть a[2] = 1. Теперь получившийся массив: 1 0 1 0 0.Другой пример: int a[6] = { a[3] = 1, a[4] = 2, a[5] = 3 }; - Несмотря на то, что порядок несколько произвольный, предполагая, что он идет слева направо, он будет состоять из следующих 6 шагов:
0 0 0 1 0 0
1 0 0 1 0 0
1 0 0 1 2 0
1 2 0 1 2 0
1 2 0 1 2 3
1 2 3 1 2 3
A = B = C = 5 не является декларацией (или инициализацией). Это нормальное выражение, которое разбирается как A = (B = (C = 5)), потому что оператор = является правоассоциативным. На самом деле это не помогает объяснить, как работает инициализация. Фактически массив начинает существовать, когда вводится блок, в котором он определен, что может быть задолго до выполнения фактического определения.
Это вопрос [языкового юриста], но это не тот ответ, который работает со стандартом, что делает его неактуальным. Кроме того, есть еще 2 более подробных ответа, и ваш ответ, похоже, ничего не добавляет.
"Он идет слева направо, каждый из которых начинается с внутреннего объявления." неверно. Стандарт C явно говорит "Порядок, в котором возникают какие-либо побочные эффекты среди выражений списка инициализации, не указан."
@melpomene - Так это на самом деле произвольно в C? Хорошо, я еще раз отредактирую. Результат должен быть таким же, хотя шаги могут и не быть. Также я помню темные времена, когда меня заставляли писать на C в курсе ... как я не скучаю по тем дням.
Результатом «не должно быть» ничего конкретного, потому что поведение не определено. См. Мой ответ для обоснования.
@melpomene - даже если он не определен в стандартах, на практике компилятор может обрабатывать его последовательно. Я цитирую вас: «Поскольку стандарт C четко не определяет, что здесь происходит, я считаю, что поведение не определено (по пропуску)». - Учитывая, что это можно проверить, нет необходимости в домыслах. И нет никаких оснований утверждать, что ваши домыслы, основанные на недостатке информации (как вы сказали - «по бездействию»), являются веским доказательством. Есть теория и есть практика. Вы привели аргументы в пользу того, почему теоретически это не (хорошо) определено - хорошо. Но никто из нас не проверял это.
Вы не можете проверить неопределенное поведение. Как это вообще могло бы работать? Любой наблюдаемый результат будет соответствовать неопределенному поведению. Компилятор может обрабатывать это последовательно, но в этом случае это должно быть задокументировано в руководстве компилятора. В противном случае все может незаметно измениться с другим компилятором, или следующей версией компилятора, или другими настройками оптимизации, или тем же кодом в другом контексте.
@melpomene - вы тестируете код из моего примера достаточное количество раз и смотрите, согласуются ли результаты. Вносите вариации там, где, по вашему мнению, возникают несоответствия. Если это непоследовательно, вы можете сказать, что это не окончательно и действительно несколько произвольно. Это было бы твердым доказательством. Иначе что? Отсутствие документации и предположений, основанных на указанной недостающей документации. Что ж, тогда скажите людям, которые работают над компилятором, исправить их документацию. Докажите, что я ошибаюсь, и я с радостью уступлю и удалю или отредактирую свой ответ.
"Вы тестируете код из моего примера достаточное количество раз и смотрите, согласуются ли результаты." Это не так. Кажется, вы не понимаете, что такое неопределенное поведение. Все в C по умолчанию имеет неопределенное поведение; просто некоторые части имеют поведение, определенное стандартом. Чтобы доказать, что что-то определило поведение, вы должны процитировать стандарт и показать, где он определяет, что должно происходить. В отсутствие такого определения поведение не определено.
Утверждение в пункте (1) - это огромный шаг вперед по сравнению с ключевым вопросом здесь: происходит ли неявная инициализация элемента a [2] значением 0 до того, как будет применен побочный эффект выражения инициализатора a[2] = 1? Наблюдаемый результат такой, как если бы он был, но стандарт, похоже, не указывает, что это должно быть так. Что - центр спора, и этот ответ полностью игнорирует его.
«Неопределенное поведение» - технический термин с узким значением. Это не означает «поведение, в котором мы не уверены». Ключевым моментом здесь является то, что ни один тест без компилятора никогда не может показать, что конкретная программа ведет себя или плохо ведет себя по стандарту, потому что, если программа имеет неопределенное поведение, компилятору разрешено выполнять что-нибудь, включая работу в идеальном режиме. предсказуемо и разумно. Это не просто проблема качества реализации, когда авторы компилятора документируют вещи - это неуказанное или определяемое реализацией поведение.
TL; DR: с помощью тщательных тестов вы можете установить (в любой желаемой степени), как какой-либо конкретный компилятор обрабатывает этот код, но это ничего не скажет вам о том, что стандартное говорит должно происходить с этим кодом, и в этом заключается вопрос . Достаточно ясно, как «очевидный» или «разумный» компилятор может обработать этот код, но language-lawyer не в этом. Если бы C был определен в соответствии с тем, как работает один конкретный эталонный компилятор (некоторые языки действительно работают таким образом), это было бы другое дело.
@Jeroen Mostert - Вопрос был такой: «Я не понимаю, почему [0] выводит 1 вместо 0? Это неопределенное поведение?» - Я ответил на первую часть и расширил ее другим примером, оставшись лаконичным. Есть система голосования, я вижу, вы оба проголосовали против, радуйтесь и идите дальше. Подправил то, что нужно, но все. Черт, это не естествознание, это просто о некоторых людях, которые недостаточно хорошо выполняют свою работу (не то чтобы я хотел их критиковать), и что их поведение остается неопределенным. Это все. После исправления (если вообще когда-либо) этот диалог устаревает.
Я думаю, что стандарт C11 охватывает это поведение и говорит, что результат это неопределенные, и я не думаю, что C18 внес какие-либо соответствующие изменения в эта зона.
Стандартный язык нелегко разобрать. Соответствующий раздел стандарта §6.7.9 Инициализация. Синтаксис задокументирован как:
initializer:
assignment-expression
{ initializer-list }
{ initializer-list , }initializer-list:
designationoptinitializer
initializer-list , designationoptinitializerdesignation:
designator-list =designator-list:
designator
designator-list designatordesignator:
[ constant-expression ]
. identifier
Обратите внимание, что одним из терминов является присваивание-выражение, и поскольку a[2] = 1, несомненно, является выражением присваивания, он разрешен внутри
инициализаторы для массивов с нестатической продолжительностью:
§4 All the expressions in an initializer for an object that has static or thread storage duration shall be constant expressions or string literals.
Один из ключевых абзацев:
§19 The initialization shall occur in initializer list order, each initializer provided for a particular subobject overriding any previously listed initializer for the same subobject;151) all subobjects that are not initialized explicitly shall be initialized implicitly the same as objects that have static storage duration.
151) Any initializer for the subobject which is overridden and so not used to initialize that subobject might not be evaluated at all.
И еще один ключевой абзац:
§23 The evaluations of the initialization list expressions are indeterminately sequenced with respect to one another and thus the order in which any side effects occur is unspecified.152)
152) In particular, the evaluation order need not be the same as the order of subobject initialization.
Я почти уверен, что параграф §23 указывает на то, что обозначения в вопрос:
int a[5] = { a[2] = 1 };
приводит к неопределенному поведению.
Присвоение a[2] является побочным эффектом, и порядок оценки
выражения имеют неопределенную последовательность относительно друг друга.
Следовательно, я не думаю, что есть способ апеллировать к стандарту и
утверждают, что конкретный компилятор обрабатывает это правильно или неправильно.
Существует только одно выражение списка инициализации, поэтому §23 не имеет значения.
Назначение a[2]= 1 - это выражение, которое имеет значение 1, и вы, по сути, написали int a[5]= { 1 }; (с побочным эффектом, что a[2] также назначается 1).
Но неясно, когда оценивается побочный эффект, и поведение может измениться в зависимости от компилятора. Также в стандарте, кажется, утверждается, что это неопределенное поведение, поэтому объяснения для конкретных реализаций компилятора бесполезны.
@KamiKaze: конечно, значение 1 попало туда случайно.
Считаю, что int a[5] = { a[2]=1 }; - хороший пример для программиста, стреляющего себе в ногу.
У меня может возникнуть соблазн подумать, что вы имели в виду int a[5] = { [2]=1 };, который был бы назначенным C99 элементом настройки инициализатора 2 равным 1, а остальным - нулем.
В том редком случае, когда вы действительно имели в виду int a[5] = { 1 }; a[2]=1;, это было бы забавным способом его написания. Во всяком случае, это то, к чему сводится ваш код, хотя некоторые здесь указали, что он не очень хорошо определен, когда фактически выполняется запись в a[2]. Проблема здесь в том, что a[2]=1 - это не назначенный инициализатор, а простое присвоение, которое само имеет значение 1.
похоже, что эта языковая тема юриста запрашивает ссылки из типовых проектов. Вот почему вас голосуют против (я этого не делал, как вы видите, меня проголосовали против по той же причине). Я думаю, что то, что вы написали, совершенно нормально, но похоже, что все эти языковые юристы здесь либо из комитета, либо что-то в этом роде. Таким образом, они вообще не просят о помощи, они пытаются проверить, охватывает ли черновик дело или нет, и большинство ребят здесь срабатывают, если вы отвечаете, как будто помогаете им. Думаю, я удалю свой ответ :) Если бы правила этой темы были четко изложены, это было бы полезно
int a [5] = {выражение} не является присвоением [0]?