Путаница с инициализацией массива в C

102

На языке 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? Это неопределенное поведение?

Примечание: этот вопрос был задан в интервью.

msc
источник
35
Выражение a[2]=1оценивается как 1.
tkausl
14
Очень глубокий вопрос. Интересно, знает ли ответ интервьюер. Я не. Действительно, якобы значение выражения a[2] = 1равно 1, но я не уверен, разрешено ли вам принимать результат назначенного выражения инициализатора в качестве значения первого элемента. Тот факт, что вы добавили тег «адвокат», означает, что я думаю, что нам нужен ответ со ссылкой на стандарт.
Вирсавия
15
Что ж, если это их любимый вопрос, возможно, вы избежали пули. Лично я предпочитаю, чтобы письменное упражнение по программированию (с доступом к компилятору и отладчику) занимало несколько часов, а не вопросы в стиле «туз», подобные приведенным выше. Я мог бы предположить ответ, но не думаю, что он будет иметь какую-либо реальную фактическую основу.
Вирсавия
1
@Bathsheba Я бы сделал наоборот, поскольку ответ здесь теперь отвечает на оба вопроса.
Прощай SE
1
@Bathsheba будет лучшим. Тем не менее, я бы отдал должное за вопрос OP, поскольку он придумал эту тему. Но не мне решать, что, по моему мнению, будет «правильным».
Прощай SE

Ответы:

95

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 Инициализация

    1. Все выражения в инициализаторе для объекта, который имеет статическую продолжительность хранения, должны быть постоянными выражениями или строковыми литералами.

    a[2] = 1не является постоянным выражением, поэтому aдолжно иметь автоматическое сохранение.

  • a входит в область его собственной инициализации.

    6.2.1 Области действия идентификаторов

    1. Теги структуры, объединения и перечисления имеют область видимости, которая начинается сразу после появления тега в спецификаторе типа, который объявляет тег. Каждая константа перечисления имеет область действия, которая начинается сразу после появления определяющего ее перечислителя в списке перечислителей. Любой другой идентификатор имеет область, которая начинается сразу после завершения его декларатора.

    Декларатор есть a[5], поэтому переменные находятся в области видимости при их собственной инициализации.

  • a жив при собственной инициализации.

    6.2.4 Срок хранения объектов

    1. Объект, идентификатор которого объявлен без связи и без спецификатора класса хранения,static имеет автоматическую продолжительность хранения .

    2. Для такого объекта, который не имеет типа массива переменной длины, его время жизни простирается от входа в блок, с которым он связан, до тех пор, пока выполнение этого блока не завершится каким-либо образом. (Вход в закрытый блок или вызов функции приостанавливает, но не завершает выполнение текущего блока.) Если блок вводится рекурсивно, каждый раз создается новый экземпляр объекта. Начальное значение объекта неопределенно. Если для объекта указана инициализация, она выполняется каждый раз, когда достигается объявление при выполнении блока; в противном случае значение становится неопределенным каждый раз, когда достигается объявление.

  • После идет точка последовательности a[2]=1.

    6.8 Заявления и блоки

    1. Полное выражение является выражением , которое не является частью другого выражения или из описателя. Каждое из следующего является полным выражением: инициализатор ; выражение в выражении оператора; управляющее выражение оператора выбора ( ifили switch); управляющее выражение оператора whileor do; каждое из (необязательных) выражений forутверждения; (необязательное) выражение в returnинструкции. Конец полного выражения - это точка следования.

    Следует отметить , что , например , в int foo[] = { 1, 2, 3 }в { 1, 2, 3 }части является скобка огороженный список инициализаторов, каждый из которых имеет точку последовательности после него.

  • Инициализация выполняется в порядке списка инициализаторов.

    6.7.8 Инициализация

    1. Каждый список инициализаторов, заключенный в фигурные скобки, имеет связанный текущий объект . Если обозначений нет, подобъекты текущего объекта инициализируются в порядке, соответствующем типу текущего объекта: элементы массива в порядке возрастания индекса, элементы структуры в порядке объявления и первый именованный член объединения. [...]

     

    1. Инициализация должна происходить в порядке списка инициализаторов, каждый инициализатор, предусмотренный для конкретного подобъекта, отменяет любой ранее указанный инициализатор для того же подобъекта; все подобъекты, которые не инициализированы явно, должны быть инициализированы неявно так же, как объекты, которые имеют статическую продолжительность хранения.
  • Однако выражения инициализатора не обязательно вычисляются по порядку.

    6.7.8 Инициализация

    1. Порядок, в котором возникают какие-либо побочные эффекты среди выражений списка инициализации, не указан.

Однако некоторые вопросы остаются без ответа:

  • Имеются ли вообще точки последовательности? Основное правило:

    6.5 Выражения

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

    a[2] = 1 это выражение, а инициализация - нет.

    Этому немного противоречит Приложение J:

    J.2 Неопределенное поведение

    • Между двумя точками последовательности объект модифицируется более одного раза или модифицируется, и предыдущее значение считывается иначе, чем для определения значения, которое будет сохранено (6.5).

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

  • Каким образом инициализации подобъекта упорядочиваются относительно выражений инициализатора? Все ли инициализаторы сначала оцениваются (в некотором порядке), затем подобъекты инициализируются результатами (в порядке списка инициализаторов)? Или их можно чередовать?


Думаю int a[5] = { a[2] = 1 }выполняется следующим образом:

  1. Хранилище для aвыделяется при вводе содержащего его блока. На данный момент содержание не определено.
  2. Выполняется (единственный) инициализатор ( a[2] = 1), за которым следует точка последовательности. Это сохраняет 1в a[2]и возвращается 1.
  3. Это 1используется для инициализации a[0](первый инициализатор инициализирует первый подобъект).

Но здесь вещи становятся нечеткими , так как остальные элементы ( a[1], a[2], a[3], a[4]) должны быть инициализированы 0, но не ясно , когда: Имеют ли это произойдет до a[2] = 1оцениваемого? Если да, то a[2] = 1будет ли "победить" и перезаписать a[2], но будет ли это присвоение иметь неопределенное поведение, потому что между нулевой инициализацией и выражением присваивания нет точки последовательности? Имеют ли значение точки последовательности (см. Выше)? Или нулевая инициализация происходит после оценки всех инициализаторов? Если так,a[2] должно быть 0.

Поскольку стандарт C четко не определяет, что здесь происходит, я считаю, что поведение не определено (по пропуску).

мельпомена
источник
1
Вместо undefined я бы сказал, что он не указан , что оставляет вещи открытыми для интерпретации реализациями.
Какой-то чувак-программист
1
"мы попадаем в кроличью нору" LOL! Никогда не слышал этого для UB или неуказанного материала.
BЈовић
2
@Someprogrammerdude Я не думаю, что это может быть неопределенным (« поведение, при котором этот международный стандарт предоставляет две или более возможностей и не налагает дополнительных требований, которые выбираются в любом случае »), потому что стандарт на самом деле не предоставляет никаких возможностей, среди которых выберите. Он просто не говорит , что происходит, что я считаю , подпадает под « Неопределенное поведение [...] указано в настоящем стандарте [...] упущение любого явного определения поведения. »
Мельпомена
2
@ BЈовић Это также очень хорошее описание не только для неопределенного поведения, но и для определенного поведения, для объяснения которого нужен поток, подобный этому.
gnasher729
1
@JohnBollinger Разница в том, что вы не можете фактически инициализировать a[0]подобъект до оценки его инициализатора, а оценка любого инициализатора включает точку последовательности (потому что это «полное выражение»). Поэтому я считаю, что изменение подобъекта, который мы инициализируем, - это честная игра.
Melpomene
22

Я не понимаю, почему a[0]печать 1вместо 0?

Предположительно сначала a[2]=1инициализируется a[2], а результат выражения используется для инициализации a[0].

Из N2176 (проект C17):

6.7.9 Инициализация

  1. Вычисления выражений списка инициализации неопределенно упорядочены по отношению друг к другу, и поэтому порядок, в котором возникают какие-либо побочные эффекты, не указан. 154)

Казалось бы, выход 1 0 0 0 0тоже был возможен.

Вывод: не пишите инициализаторы, которые изменяют инициализированную переменную на лету.

пользователь694733
источник
1
Эта часть не применяется: здесь есть только одно выражение инициализатора, поэтому его не нужно ни с чем связывать.
melpomene
@melpomene Существует {...}выражение , которое инициализирует a[2]к 0и a[2]=1суб-выражение , которое инициализирует a[2]к 1.
user694733
1
{...}- это список инициализаторов в фигурных скобках. Это не выражение.
melpomene
@melpomene Хорошо, ты можешь быть здесь. Но я все равно считаю, что есть еще 2 конкурирующих побочных эффекта, так что абзац остается в силе.
user694733
@melpomene нужно упорядочить две вещи: первый инициализатор и установка других элементов на 0
MM
6

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

Стандартный язык разобрать непросто. Соответствующий раздел стандарта - это §6.7.9 Инициализация . Синтаксис задокументирован как:

initializer:
                assignment-expression
                { initializer-list }
                { initializer-list , }
initializer-list:
                designationopt initializer
                initializer-list , designationopt initializer
designation:
                designator-list =
designator-list:
                designator
                designator-list designator
designator:
                [ constant-expression ]
                . identifier

Обратите внимание, что одним из терминов является выражение-присваивание , и поскольку a[2] = 1это, несомненно, выражение присваивания, оно разрешено внутри инициализаторов для массивов с нестатической продолжительностью:

§4 Все выражения в инициализаторе для объекта, который имеет статическую продолжительность или продолжительность хранения потока, должны быть постоянными выражениями или строковыми литералами.

Один из ключевых абзацев:

§19 Инициализация должна происходить в порядке списка инициализаторов, каждый инициализатор, предусмотренный для конкретного подобъекта, отменяет любой ранее перечисленный инициализатор для того же подобъекта; 151) все подобъекты, которые не инициализированы явно, должны быть инициализированы неявно так же, как объекты, которые имеют статическую продолжительность хранения.

151) Любой инициализатор для подобъекта, который переопределяется и поэтому не используется для инициализации этого подобъекта, может вообще не оцениваться.

И еще один ключевой абзац:

§23 Вычисления выражений списка инициализации неопределенно упорядочены относительно друг друга, и поэтому порядок, в котором возникают какие-либо побочные эффекты, не определен. 152)

152) В частности, порядок оценки может не совпадать с порядком инициализации подобъекта.

Я почти уверен, что параграф §23 указывает на то, что обозначения в вопросе:

int a[5] = { a[2] = 1 };

приводит к неопределенному поведению. Присваивание a[2]является побочным эффектом, и порядок оценки выражений неопределенно упорядочен по отношению друг к другу. Следовательно, я не думаю, что есть способ апеллировать к стандарту и утверждать, что конкретный компилятор обрабатывает это правильно или неправильно.

Джонатан Леффлер
источник
Существует только одно выражение списка инициализации, поэтому §23 не имеет значения.
melpomene 05
2

Мое понимание 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
Это вопрос [языкового юриста], но это не тот ответ, который работает со стандартом, поэтому он неуместен. Кроме того, есть еще 2 более подробных ответа, и ваш ответ, похоже, ничего не добавляет.
Прощай SE
Я сомневаюсь, что концепция, которую я опубликовал, неверна? Не могли бы вы пояснить мне это?
Karthika
1
Вы просто размышляете о причинах, в то время как в соответствующих частях стандарта уже дан очень хороший ответ. Вопрос не в том, как это могло произойти. Речь идет о том, что должно происходить в стандарте.
До свидания SE
Но человек, который разместил выше вопрос, спросил причину и почему это происходит? Так что только я отказался от этого ответа, но концепция верна.
Karthika
ОП спросил: « Это неопределенное поведение? ». Ваш ответ не говорит.
Melpomene
1

Я стараюсь дать короткий и простой ответ на загадку: int a[5] = { a[2] = 1 };

  1. Первый a[2] = 1установлен. Это означает, что в массиве написано:0 0 1 0 0
  2. Но вот, учитывая, что вы сделали это в { }скобках, которые используются для инициализации массива по порядку, он берет первое значение (которое есть 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
Боевой
источник
1
A = B = C = 5не является объявлением (или инициализацией). Это нормальное выражение, которое разбирается как, A = (B = (C = 5))потому что =оператор правильно ассоциативен. На самом деле это не помогает объяснить, как работает инициализация. Фактически массив начинает существовать, когда вводится блок, в котором он определен, что может быть задолго до выполнения фактического определения.
Melpomene
1
« Идет слева направо, каждое начинается с внутреннего объявления » неверно. В стандарте C прямо говорится: « Порядок, в котором возникают побочные эффекты среди выражений списка инициализации, не
указан
1
« Вы проверяете код из моего примера достаточное количество раз и смотрите, согласуются ли результаты». Это не так, как это работает. Кажется, вы не понимаете, что такое неопределенное поведение. Все в C по умолчанию имеет неопределенное поведение; просто некоторые части имеют поведение, определенное стандартом. Чтобы доказать, что что-то определило поведение, вы должны процитировать стандарт и показать, где он определяет, что должно происходить. В отсутствие такого определения поведение не определено.
Melpomene
1
Утверждение в пункте (1) - огромный шаг вперед по сравнению с ключевым вопросом здесь: происходит ли неявная инициализация элемента a [2] значением 0 до того, как будет применен побочный эффект a[2] = 1выражения инициализатора? Наблюдаемый результат такой же, как если бы он был, но стандарт, кажется, не указывает, что это должно быть так. Это центр спора, и этот ответ полностью игнорирует его.
Джон Боллинджер
1
«Неопределенное поведение» - это технический термин с узким значением. Это не означает «поведение, в котором мы не уверены». Ключевым моментом здесь является то, что ни один тест без компилятора никогда не может показать, что конкретная программа ведет себя или не ведет себя надлежащим образом в соответствии со стандартом , потому что, если программа имеет неопределенное поведение, компилятору разрешено делать что угодно, включая работу. совершенно предсказуемым и разумным образом. Это не просто проблема качества реализации, когда авторы компилятора документируют вещи - это неуказанное или определяемое реализацией поведение.
Jeroen Mostert
0

Присваивание a[2]= 1- это выражение, имеющее значение 1, и вы, по сути, написали int a[5]= { 1 };a[2]присваиваемым побочным эффектом 1).

Ив Дауст
источник
Но неясно, когда оценивается побочный эффект, и поведение может измениться в зависимости от компилятора. Также в стандарте, по-видимому, указано, что это неопределенное поведение, поэтому объяснения для конкретных реализаций компилятора бесполезны.
Прощай SE
@KamiKaze: конечно, значение 1 попало туда случайно.
Ив Дауст
0

Я полагаю, что 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.

Свен
источник
похоже, что эта языковая тема юриста просит ссылки из типовых проектов. Вот почему вас голосуют против (я этого не делал, как вы видите, меня проголосовали против по той же причине). Я думаю, что то, что вы написали, совершенно нормально, но похоже, что все эти языковые юристы здесь либо из комитета, либо что-то в этом роде. Таким образом, они вообще не просят о помощи, они пытаются проверить, охватывает ли черновик дело или нет, и большинство ребят здесь срабатывают, если вы отвечаете, как будто помогаете им. Думаю, я удалю свой ответ :) Если бы правила этой темы четко изложили, это было бы полезно
Абдуррахим