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

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

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

Ответ 1

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. Теги Structure, union и enumeration имеют область, которая начинается сразу после появления тега в спецификаторе типа, который объявляет тег. Каждая константа перечисления имеет область действия, которая начинается сразу после появления его определяющего перечислителя в списке перечислителей. Любой другой идентификатор имеет область действия, которая начинается сразу после завершения его декларатора.

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

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

    6.2.4 Длительность хранения объектов

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

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

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

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

    1. Полное выражение - это выражение, которое не является частью другого выражения или декларатора. Каждое из следующего является полным выражением: инициализатор; выражение в выражении выражения; управляющее выражение оператора выбора (if или switch); контролирующее выражение while или 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 не дает четкого определения того, что происходит здесь, я считаю, что поведение не определено (путем упущения).

Ответ 2

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

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

От N2176 (проект C17):

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

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

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

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

Ответ 3

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

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

initializer:
assignment-expression
{ initializer-list }
{ initializer-list, }
initializer-list:
designation optinitializer
initializer-list, designation optinitializer
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] является побочным эффектом, а порядок оценки выражений неопределенно секвенирован относительно друг друга. Следовательно, я не думаю, что есть способ обратиться к стандарту и утверждать, что конкретный компилятор обрабатывает это правильно или неправильно.

Ответ 4

Мое понимание - 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;

Ответ 5

Я пытаюсь дать короткий и простой ответ на загадку: 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

Ответ 6

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

Ответ 7

следить за этим

int a[3] = {1}; // [1,0,0]

затем проверьте это

int x=5;
int a[3] = {x}; // its like a[3]={5} which results [5,0,0]

затем проверьте это

int x=5;
int a[3] = {x = 6}; // its like a[3]={6} which results [6,0,0]

то вы должны понять, как это становится

int a[3] = {a[0]++}; // [0,0,0]

Чтобы понять это, вам нужно идти шаг за шагом.

  • Сначала он запустит [0] ++, ваш массив станет [1 0 0]
  • a [0] ++ - выражение, которое будет иметь значение 0, потому что его первое значение vas 0
  • последнее ваше выражение станет как [3] = {0}, которое перепишет ваш массив и превратит его в [0 0 0]

то вы должны понять, почему это должно быть

int a[3] = {++a[0]}; // [1,0,0]

он запускает оператор ++ на [0], а затем записывает 1, являющийся результатом [0] ++, на [0]

Надеюсь, тайна будет решена. Позвольте мне дать вам еще одну загадку

int a[3] = {a[1] = ++a[2] + ++a[2]}; // [3,3,2]
  • сначала мы выделяем 3 целых числа, с которыми ваш массив начинается с [0 0 0]
  • если вы проверите это, сначала запустите ++a [2], чтобы ваш массив стал [0 0 1]
  • то он подталкивает 1 к стеку, что является первым результатом ++a [2]
  • затем он запускает второй ++a [2], что является вторым аргументом плюса, массив становится [0 0 2]
  • то он толкает результат второго ++a [2], который равен 2
  • он работает +, который равен 1 + 2, и сохраняет результат 3
  • он запускает второй оператор равенства, который является [1] = результатом массива sum = 3, становится [0 3 2]. Остерегайтесь результата по-прежнему 3: int x = a = 3, как int x = (a = 3); и (a = 3) является выражением со значением 3.
  • Наконец, ваши выражения становятся такими же, как [3] = {3}, что это просто, просто назначит ваше последнее, и вы получите [3 3 2]

Кстати, это может отличаться от компилятора к компилятору или даже между версиями компилятора! Вот троллейная часть: от этого мы ничего не узнали! Не делай ничего подобного

Ответ 8

Я считаю, что int a[5]={ a[2]=1 }; хороший пример для программиста, стреляющего себе в ногу.

У меня может возникнуть соблазн думать, что вы имели в виду int a[5]={ [2]=1 }; это будет обозначенный C99 элемент настройки инициализатора от 2 до 1, а остальные до нуля.

В редком случае, который вы действительно имели в виду int a[5]={ 1 }; a[2]=1; int a[5]={ 1 }; a[2]=1; тогда это был бы забавный способ написать это. В любом случае, это то, к чему сводится ваш код, хотя некоторые здесь отметили, что он не очень хорошо определен, когда запись в a[2] фактически выполняется. Подводным камнем здесь является то, что a[2]=1 является не назначенным инициализатором, а простым присваиванием, которое само по себе имеет значение 1.

Ответ 9

Порядок операций.

Во-первых, выполняется присвоение, и присваивание оценивается следующим образом:

int a[5] = {1}, что дает следующее:

1, 0, 0, 0, 0. Мы получаем {1} потому что a[2]=1 оценивается как true и неявно приводится к 1 в серии назначений.

Во-вторых, выполняется выражение в фигурных скобках, что приводит к фактическому присвоению 1 массиву с индексом 2.

Вы можете поэкспериментировать, компилируя int a[5]={true}; а также int a[5]={a[3]=3}; и наблюдение за результатами.

edit: Я ошибался в отношении результата назначения в списке инициализации, что приводит к целому числу, которое совпадает с назначенным.