Плутанина щодо ініціалізації масиву в 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? Це невизначена поведінка?

Примітка. Це питання було задано в інтерв'ю.


35
Вираз a[2]=1оцінюється на 1.
tkausl

14
Дуже глибоке запитання. Цікаво, чи знає співбесідник відповідь самі. Я не. Справді, нібито значення виразу a[2] = 1є 1, але я не впевнений, чи дозволяють ви отримувати результат визначеного ініціалізаторного виразу як значення першого елемента. Той факт, що ви додали тег адвоката, означає, що нам потрібна відповідь із посиланням на стандарт.
Вірсавія

15
Ну, якщо це їх улюблене питання, можливо, ви цілком ухилилися від кулі. Особисто я вважаю за краще письмове вправлення програмування (з доступом до компілятора та налагоджувача), яке потрібно проводити протягом декількох годин, а не питання "у стилі асо", як описано вище. Я міг би припустити відповідь, але я не думаю, що це матиме реальну фактичну основу.
Вірсавія

1
@Bathsheba Я би зробив навпаки, оскільки відповідь тут відповідає на обидва запитання.
Прощання SE

1
@Bathsheba була б найкращою. І все-таки я віддав би заслугу за питання до ОП, коли він придумав цю тему. Але це не для мене, щоб вирішити саме те, що я вважаю, було б "правильною справою".
Прощай SE SE

Відповіді:


95

TL; DR: Я не думаю, що поведінка int a[5]={a[2]=1};чітко визначена, принаймні у С99.

Найсмішніша частина полягає в тому, що єдиний біт, який має для мене сенс, - це частина, про яку ви питаєте: 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); керуючий вираз a 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 чітко не визначає, що тут відбувається, я вважаю, що поведінка не визначена (за пропуском).


1
Замість невизначеного я б стверджував, що це не визначено , що залишає речі відкритими для інтерпретації реалізаціями.
Якийсь програміст чувак

1
"ми потрапляємо в кролячу нору" LOL! Ніколи не чув про те, що стосується UB або не визначених речей.
BЈоviћ

2
@Someprogrammerdude Я не думаю, що це не може бути визначено (" поведінка, коли цей Міжнародний стандарт надає дві або більше можливостей і не пред'являє жодних додаткових вимог, які вибираються в будь-якому випадку "), оскільки стандарт насправді не забезпечує жодних можливостей, серед яких вибрати. Він просто не говорить про те, що відбувається, і я вважаю, що під " Невизначена поведінка [...] зазначено в цьому Міжнародному стандарті [...] упущенням явного визначення поведінки ".
Мельпомена

2
@ BЈовић Це також дуже приємний опис не тільки для невизначеної поведінки, але і для визначеної поведінки, для пояснення якої потрібна така нитка, як ця.
gnasher729

1
@JohnBollinger Різниця полягає в тому, що ви не можете насправді ініціалізувати a[0]субект перед оцінкою його ініціалізатора, а оцінка будь-якого ініціалізатора включає в себе точку послідовності (оскільки це "повний вираз"). Тому я вважаю, що зміна подобекта, який ми ініціалізуємо, - це чесна гра.
мельпомена

22

Я не розумію, чому a[0]друкується 1замість 0?

Імовірно спочатку a[2]=1ініціалізується a[2], а результат вираження використовується для ініціалізації a[0].

Від N2176 (проект C17):

6.7.9 Ініціалізація

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

Отже, здавалося б, вихід 1 0 0 0 0також був би можливим.

Висновок: Не пишіть ініціалізатори, які змінюють ініціалізовану змінну на льоту.


1
Ця частина не поширюється: Тут є лише один вираз ініціалізатора, тому його не потрібно ні з чим послідувати.
мельпомена

@melpomene Існує {...}вислів , яке ініціалізує a[2]до 0і a[2]=1суб-вираз , яке ініціалізує a[2]до 1.
користувач694733

1
{...}являє собою список скоплених ініціалізаторами. Це не вираз.
мельпомена

@melpomene Гаразд, ви можете бути там. Але я б все-таки стверджував, що є ще два конкуруючих побічні ефекти, так що абзац стоїть.
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 };

призводить до невказаної поведінки. Присвоєння to a[2]є побічним ефектом, і порядок оцінки виразів невизначено послідовно послідовно взаємопов'язаний один з одним. Отже, я не думаю, що існує спосіб звернутися до стандарту і стверджувати, що певний компілятор обробляє це правильно чи неправильно.


Є лише один вираз списку ініціалізації, тому §23 не має значення.
Мельпомена

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

У мене є сумніви. Чи поняття, яке я розмістив невірно? Не могли б ви мені прояснити це?
Картика

1
Ви просто міркуєте з причин, хоча вже є дуже хороша відповідь у відповідних частинах стандарту. Сказати, як це могло статися, - це не питання. Йдеться про те, що стандарт каже, що має відбутися.
Прощай SE SE

Але людина, яка розмістила вище запитання, запитала причину і чому це відбувається? Так що я лише відмовився від цієї відповіді. Але концепція правильна. Право?
Картика

ОП запитала " Чи не визначена поведінка? ". Ваша відповідь не говорить.
мельпомена

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))тому що =оператор є правильним асоціативним. Це не дуже допомагає пояснити, як працює ініціалізація. Масив фактично починає існувати, коли вводиться блок, який він визначений, що може бути задовго до виконання фактичного визначення.
мельпомена

1
" Це йде зліва направо, кожне починаючи з внутрішньої декларації " є невірним. Стандарт C чітко говорить: " Порядок, коли виникають будь-які побічні ефекти серед виразів списку ініціалізації, не визначено ".
Мельпомена

1
" Ви перевіряєте код з мого прикладу достатньо разів і бачите, чи результати відповідають. " Це не так, як це працює. Ви, здається, не розумієте, що таке невизначена поведінка. Усе в C має невизначене поведінку за замовчуванням; це просто те, що деякі частини мають поведінку, яку визначає стандарт. Щоб довести, що щось має певну поведінку, потрібно навести стандарт і показати, де він визначає, що має відбуватися. За відсутності такого визначення поведінка не визначена.
мельпомена

1
Твердження в пункті (1) - це величезний стрибок над ключовим питанням: чи відбувається неявна ініціалізація елемента a [2] до 0 до того, як буде застосовано побічний ефект a[2] = 1вираження ініціалізатора? Результат, який спостерігається, як би він був, але стандарт не вказує, що це має бути так. Це центр суперечки, і ця відповідь цілком її оминає.
Джон Боллінгер

1
"Невизначена поведінка" - це технічний термін із вузьким значенням. Це не означає "поведінку, в чому ми не дуже впевнені". Ключове розуміння тут полягає в тому, що жоден тест, без компілятора, ніколи не може показати, що певна програма не відповідає або не відповідає нормам , тому що якщо програма має не визначене поведінку, компілятору дозволяється робити що завгодно - в тому числі працювати у абсолютно передбачуваному та розумному вигляді. Це не просто питання якості впровадження, де автори-компілятори документують речі - це не визначене чи визначене реалізацією поведінка.
Jeroen Mostert

0

Призначення a[2]= 1- це вираження, яке має значення 1, і ви, по суті, написали int a[5]= { 1 };(з побічним ефектом, який a[2]також призначається 1).


Але незрозуміло, коли оцінюється побічний ефект і поведінка може змінюватися залежно від компілятора. Також стандарт здається, що це не визначене поведінка, пояснення для конкретних реалізацій компілятора не є корисними.
Прощайте SE SE

@KamiKaze: звичайно, значення 1 приземлилося там випадково.
Ів Дауст

0

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

Я міг би спокуситись думати, що це ви мали на увазі int a[5]={ [2]=1 }; що було б позначений С99 елемент налаштування ініціалізатора 2 до 1, а решта - до нуля.

У рідкісному випадку, який ви насправді мали на увазі int a[5]={ 1 }; a[2]=1;, тоді це був би смішний спосіб його написання. Так чи інакше, до цього зводиться ваш код, навіть хоча деякі з них вказували, що він недостатньо чітко визначений, коли записування в a[2]насправді виконується. Проблема тут полягає в тому, що a[2]=1це не призначений ініціалізатор, а проста задача, яка сама має значення 1.


схоже, що ця мовно-юристська тема запитує посилання на стандартні проекти. Ось чому ви прихильнені (я цього не робив, як ви бачите, що я зв'язаний з тієї ж причини). Я думаю, що те, що ви написали, цілком чудово, але схоже, що всі ці адвокати з мовної мови є або від комітетів, або щось подібне. Тому вони взагалі не просять допомоги, вони намагаються перевірити, чи висвітлює проект справи чи ні, і більшість хлопців тут спрацьовує, якщо ви відповіли так, як ви їм допомагаєте. Я думаю, погано видаляю свою відповідь :) Якщо в цій темі правила чітко викладені, це було б корисно
Абдуррахім
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.