Nieporozumienia dotyczące inicjalizacji macierzy w C

102

W języku C, jeśli zainicjuj tablicę w ten sposób:

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

wtedy wszystkie elementy tablicy, które nie zostały zainicjowane jawnie, zostaną zainicjowane niejawnie zerami.

Ale jeśli zainicjalizuję tablicę w ten sposób:

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

printf("%d %d %d %d %d\n", a[0], a[1],a[2], a[3], a[4]);

wynik:

1 0 1 0 0

Nie rozumiem, dlaczego a[0]drukuje 1zamiast 0? Czy jest to niezdefiniowane zachowanie?

Uwaga: to pytanie zostało zadane w wywiadzie.

msc
źródło
35
Wyrażenie a[2]=1zwraca 1.
tkausl
14
Bardzo głębokie pytanie. Zastanawiam się, czy ankieter sam zna odpowiedź. Ja nie. Rzeczywiście, pozornie wartość wyrażenia a[2] = 1jest taka 1, ale nie jestem pewien, czy wolno ci przyjąć wynik wyznaczonego wyrażenia inicjalizującego jako wartość pierwszego elementu. Fakt, że dodałeś tag prawnik, oznacza, że ​​myślę, że potrzebujemy odpowiedzi powołującej się na standard.
Batszeba,
15
Cóż, jeśli to ich ulubione pytanie, prawdopodobnie uniknąłeś kuli. Osobiście wolę pisemne ćwiczenie programistyczne (z dostępem do kompilatora i debuggera), które zajmuje kilka godzin niż pytania w stylu „asa”, takie jak powyższe. Mógłbym przewidzieć odpowiedź, ale nie sądzę, aby miała ona jakąkolwiek prawdziwą podstawę.
Batszeba,
1
@Bathsheba Zrobiłbym odwrotnie, ponieważ odpowiedź tutaj odpowiada teraz na oba pytania.
Do widzenia SE
1
@Bathsheba byłaby najlepsza. Mimo to uhonorowałbym pytanie OP, ponieważ wymyślił temat. Ale nie do mnie należy decyzja, co uważam za „właściwe”.
Do widzenia SE

Odpowiedzi:

95

TL; DR: Nie sądzę, że zachowanie int a[5]={a[2]=1};jest dobrze zdefiniowane, przynajmniej w C99.

Zabawne jest to, że jedynym bitem, który ma dla mnie sens, jest część, o którą pytasz: a[0]jest ustawiona na, 1ponieważ operator przypisania zwraca przypisaną wartość. To wszystko inne jest niejasne.

Gdyby był kod int a[5] = { [2] = 1 }, wszystko byłoby łatwe: to jest wyznaczone ustawienie inicjalizatora a[2]dla 1i wszystko inne do 0. Ale { a[2] = 1 }mamy niewyznaczony inicjator zawierający wyrażenie przypisania i wpadamy do króliczej nory.


Oto, co do tej pory znalazłem:

  • a musi być zmienną lokalną.

    6.7.8 Inicjalizacja

    1. Wszystkie wyrażenia w inicjatorze dla obiektu, który ma statyczny czas trwania, powinny być wyrażeniami stałymi lub literałami łańcuchowymi.

    a[2] = 1nie jest wyrażeniem stałym, więc amusi mieć automatyczne przechowywanie.

  • a jest objęty zakresem własnej inicjalizacji.

    6.2.1 Zakresy identyfikatorów

    1. Tagi struktury, unii i wyliczenia mają zakres, który zaczyna się tuż po pojawieniu się tagu w specyfikatorze typu, który deklaruje tag. Każda stała wyliczenia ma zakres, który zaczyna się zaraz po pojawieniu się jej definiującego modułu wyliczającego na liście modułów wyliczających. Każdy inny identyfikator ma zakres, który zaczyna się zaraz po zakończeniu jego deklaratora.

    Deklarator jest a[5], więc zmienne znajdują się w zakresie w ich własnej inicjalizacji.

  • a żyje w swojej własnej inicjalizacji.

    6.2.4 Czas przechowywania obiektów

    1. Obiekt, którego identyfikator jest zadeklarowany bez powiązania i bez specyfikatora klasy pamięci,static ma automatyczny czas trwania .

    2. W przypadku takiego obiektu, który nie ma typu tablicy o zmiennej długości, jego czas życia rozciąga się od wejścia do bloku, z którym jest skojarzony, aż do zakończenia wykonywania tego bloku w jakikolwiek sposób. (Wprowadzenie bloku zamkniętego lub wywołanie funkcji wstrzymuje wykonanie bieżącego bloku, ale go nie kończy). Jeśli blok jest wprowadzany rekurencyjnie, za każdym razem tworzona jest nowa instancja obiektu. Początkowa wartość obiektu jest nieokreślona. Jeśli dla obiektu określono inicjalizację, jest ona wykonywana za każdym razem, gdy deklaracja zostanie osiągnięta podczas wykonywania bloku; w przeciwnym razie wartość staje się nieokreślona za każdym razem, gdy deklaracja zostanie osiągnięta.

  • Następuje punkt sekwencji a[2]=1.

    6.8 Instrukcje i bloki

    1. Pełne wyrażenie jest wyrażeniem, które nie jest częścią innego wyrazu lub z declarator. Każde z poniższych jest pełnym wyrażeniem: inicjalizator ; wyrażenie w wyrażeniu wyrażenia; kontrolujące wyrażenie instrukcji wyboru ( iflub switch); kontrolne wyrażenie a whilelub doinstrukcja; każde z (opcjonalnych) wyrażeń forinstrukcji; (opcjonalne) wyrażenie w returninstrukcji. Koniec pełnego wyrażenia to punkt sekwencji.

    Należy pamiętać, że na przykład w int foo[] = { 1, 2, 3 }tej { 1, 2, 3 }części znajduje się lista zamkniętych klamra z inicjalizatorów, z których każdy ma temperaturę sekwencji po niej.

  • Inicjalizacja jest wykonywana w kolejności na liście inicjatorów.

    6.7.8 Inicjalizacja

    1. Każda lista inicjatorów w nawiasach klamrowych ma skojarzony bieżący obiekt . Gdy nie ma żadnych oznaczeń, podobiekty bieżącego obiektu są inicjowane w kolejności zgodnej z typem bieżącego obiektu: elementy tablicy w rosnącej kolejności indeksów, elementy struktury w kolejności deklaracji i pierwszy nazwany element członkowski unii. […]

     

    1. Inicjalizacja powinna nastąpić w kolejności listy inicjalizatorów, przy czym każdy inicjator dostarczony dla określonego podobiektu przesłania każdy poprzednio wymieniony inicjator dla tego samego podobiektu; wszystkie podobiekty, które nie zostały zainicjowane jawnie, zostaną zainicjowane niejawnie w taki sam sposób, jak obiekty o statycznym czasie trwania.
  • Jednak wyrażenia inicjatora niekoniecznie są oceniane w kolejności.

    6.7.8 Inicjalizacja

    1. Kolejność występowania efektów ubocznych w wyrażeniach listy inicjalizacji jest nieokreślona.

Jednak to wciąż pozostawia kilka pytań bez odpowiedzi:

  • Czy punkty sekwencji są w ogóle istotne? Podstawowa zasada to:

    6.5 Wyrażenia

    1. Pomiędzy poprzednim a następnym punktem sekwencji obiekt będzie miał swoją przechowywaną wartość zmodyfikowaną co najwyżej raz przez ocenę wyrażenia . Ponadto poprzednia wartość jest tylko do odczytu w celu określenia wartości, która ma być przechowywana.

    a[2] = 1 jest wyrażeniem, ale inicjalizacja nie.

    Jest to nieco sprzeczne z załącznikiem J:

    J.2 Niezdefiniowane zachowanie

    • Pomiędzy dwoma punktami sekwencji obiekt jest modyfikowany więcej niż jeden raz lub jest modyfikowany i odczytywana jest poprzednia wartość, inaczej niż w celu określenia wartości do zapisania (6.5).

    Załącznik J mówi, że liczą się wszelkie modyfikacje, a nie tylko modyfikacje wyrażeniami. Biorąc jednak pod uwagę, że załączniki są nienormatywne, prawdopodobnie możemy to zignorować.

  • W jaki sposób inicjalizacje podobiektów są sekwencjonowane w odniesieniu do wyrażeń inicjatora? Czy wszystkie inicjatory są oceniane jako pierwsze (w jakiejś kolejności), a następnie podobiekty są inicjowane z wynikami (w kolejności na liście inicjatorów)? Czy można je przeplatać?


Myślę, że int a[5] = { a[2] = 1 }jest wykonywany w następujący sposób:

  1. Pamięć dla ajest przydzielana po wprowadzeniu zawierającego ją bloku. W tym momencie zawartość jest nieokreślona.
  2. (Jedyny) inicjator jest wykonywany ( a[2] = 1), po którym następuje punkt sekwencji. Zapisuje 1w a[2]i powroty 1.
  3. Który 1służy do zainicjowania a[0](pierwszy inicjator inicjuje pierwszy podobiekt).

Ale tu robi się rozmyty, ponieważ pozostałe elementy ( a[1], a[2], a[3], a[4]) mają być inicjowane 0, ale nie jest jasne, gdy: zdarza się, zanim a[2] = 1jest oceniany? Jeśli tak, a[2] = 1czy "wygra" i nadpisze a[2], ale czy to przypisanie będzie miało niezdefiniowane zachowanie, ponieważ nie ma punktu sekwencji między zerową inicjalizacją a wyrażeniem przypisania? Czy punkty sekwencji są w ogóle istotne (patrz wyżej)? A może zerowa inicjalizacja ma miejsce po ocenie wszystkich inicjatorów? Jeśli tak, a[2]powinno to skończyć 0.

Ponieważ standard C nie definiuje jasno, co się tutaj dzieje, uważam, że zachowanie jest nieokreślone (przez pominięcie).

melpomene
źródło
1
Zamiast niezdefiniowanej argumentowałbym, że jest ona nieokreślona , co pozostawia kwestię otwartą do interpretacji przez implementacje.
Jakiś programista,
1
„wpadamy do króliczej nory” LOL! Nigdy nie słyszałem tego dla UB lub nieokreślonych rzeczy.
BЈовић
2
@Someprogrammerdude Nie sądzę, że może to być nieokreślone („ zachowanie, w którym niniejsza Norma Międzynarodowa zapewnia dwie lub więcej możliwości i nie nakłada żadnych dalszych wymagań, które są wybrane w każdym przypadku ”), ponieważ norma tak naprawdę nie zapewnia żadnych możliwości, spośród których wybierać. Po prostu nie mówi, co się dzieje, co moim zdaniem mieści się w kategorii „ Nieokreślone zachowanie jest [...] wskazane w niniejszej Normie Międzynarodowej [...] przez pominięcie jakiejkolwiek wyraźnej definicji zachowania.
melpomene
2
@ BЈовић To także bardzo ładny opis nie tylko niezdefiniowanego zachowania, ale także zdefiniowanego zachowania, którego wyjaśnienie wymaga wątku takiego jak ten.
gnasher729
1
@JohnBollinger Różnica polega na tym, że w rzeczywistości nie można zainicjować a[0]podobiektu przed oceną jego inicjalizatora, a ocena dowolnego inicjalizatora obejmuje punkt sekwencji (ponieważ jest to „pełne wyrażenie”). Dlatego uważam, że modyfikowanie inicjowanego podobiektu jest uczciwą grą.
melpomene
22

Nie rozumiem, dlaczego a[0]drukuje 1zamiast 0?

Przypuszczalnie a[2]=1inicjalizuje się jako a[2]pierwsza, a wynik wyrażenia jest używany do inicjalizacji a[0].

Od N2176 (projekt C17):

6.7.9 Inicjalizacja

  1. Oceny wyrażeń listy inicjalizacyjnej są sekwencjonowane w nieokreślony sposób względem siebie, a zatem kolejność, w jakiej występują jakiekolwiek skutki uboczne, jest nieokreślona. 154)

Wydawałoby się więc, że produkcja 1 0 0 0 0również byłaby możliwa.

Wniosek: nie pisz inicjatorów, które modyfikują zainicjowaną zmienną w locie.

user694733
źródło
1
Ta część nie ma zastosowania: jest tutaj tylko jedno wyrażenie inicjalizujące, więc nie musi być z niczym sekwencjonowane.
melpomene
@melpomene Jest {...} ekspresji, który inicjuje a[2]się 0i a[2]=1sub-ekspresji, który inicjuje a[2]się 1.
user694733
1
{...}jest listą inicjalizacyjną ze wzmocnieniem. To nie jest wyrażenie.
melpomene
@melpomene Ok, możesz tam być. Ale nadal twierdziłbym, że nadal istnieją 2 konkurujące ze sobą skutki uboczne, więc akapit pozostaje niezmieniony.
user694733
@melpomene są dwie rzeczy do zsekwencjonowania: pierwszy inicjator i ustawienie innych elementów na 0
MM
6

Myślę, że standard C11 obejmuje to zachowanie i mówi, że wynik jest nieokreślony i nie sądzę, aby C18 wprowadził jakiekolwiek istotne zmiany w tym obszarze.

Język standardowy nie jest łatwy do przeanalizowania. Odpowiednią sekcją normy jest §6.7.9 Inicjalizacja . Składnia jest udokumentowana jako:

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

Zwróć uwagę, że jednym z terminów jest wyrażenie przypisania , a ponieważ a[2] = 1jest to niewątpliwie wyrażenie przypisania, jest dozwolone wewnątrz inicjatorów dla tablic o niestatycznym czasie trwania:

§4 Wszystkie wyrażenia w inicjatorze dla obiektu, który ma statyczny lub wątkowy czas trwania, powinny być wyrażeniami stałymi lub literałami łańcuchowymi.

Jednym z kluczowych akapitów jest:

§19 Inicjalizacja powinna odbywać się w kolejności listy inicjalizatorów, przy czym każdy inicjalizator przewidziany dla określonego podobiektu przesłania wszelkie poprzednio wymienione inicjatory dla tego samego podobiektu; 151) wszystkie podobiekty, które nie zostały zainicjowane jawnie, zostaną zainicjowane niejawnie tak samo, jak obiekty o statycznym czasie trwania.

151) Każdy inicjator dla podobiektu, który jest przesłonięty i nie jest używany do zainicjowania tego podobiektu, może w ogóle nie zostać oceniony.

Kolejny kluczowy akapit to:

§23 Oceny wyrażeń listy inicjalizacyjnej są nieokreślone względem siebie, a zatem kolejność, w jakiej występują jakiekolwiek skutki uboczne, jest nieokreślona. 152)

152) W szczególności kolejność oceny nie musi być taka sama jak kolejność inicjalizacji podobiektu.

Jestem prawie pewien, że paragraf 23 wskazuje, że zapis w pytaniu:

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

prowadzi do nieokreślonego zachowania. Przypisanie do a[2]jest efektem ubocznym, a kolejność oceny wyrażeń jest nieokreślona względem siebie. W związku z tym nie sądzę, aby można było odwołać się do standardu i twierdzić, że konkretny kompilator obsługuje to poprawnie lub nieprawidłowo.

Jonathan Leffler
źródło
Jest tylko jedno wyrażenie listy inicjalizacyjnej, więc §23 nie ma zastosowania.
melpomene
2

My Understanding a[2]=1zwraca wartość 1 więc kod staje się

int a[5]={a[2]=1} --> int a[5]={1}

int a[5]={1}przypisz wartość dla [0] = 1

Stąd wypisuje 1 dla [0]

Na przykład

char str[10]={‘H’,‘a’,‘i’};


char str[0] = H’;
char str[1] = a’;
char str[2] = i;
Karthika
źródło
2
To jest pytanie [prawnika językowego], ale nie jest to odpowiedź zgodna ze standardem, co czyni ją nieistotną. Ponadto dostępne są jeszcze 2 bardziej szczegółowe odpowiedzi, a twoja odpowiedź nie wydaje się niczego dodawać.
Do widzenia SE
Mam wątpliwości, czy koncepcja, którą zamieściłem, jest niewłaściwa? Czy możesz mi to wyjaśnić?
Karthika,
1
Po prostu spekulujesz z powodów, podczas gdy istnieje już bardzo dobra odpowiedź z odpowiednimi częściami normy. Samo powiedzenie, jak to się mogło stać, nie jest tym, o co chodzi. Chodzi o to, co według normy powinno się wydarzyć.
Do widzenia SE
Ale osoba, która zamieściła powyższe pytanie, spytała o przyczynę i dlaczego tak się dzieje? Więc tylko ja upuściłem tę odpowiedź, ale koncepcja jest poprawna.
Karthika,
OP zapytał „ Czy jest to niezdefiniowane zachowanie? ”. Twoja odpowiedź nie mówi.
melpomene
1

Na zagadkę staram się udzielić krótkiej i prostej odpowiedzi: int a[5] = { a[2] = 1 };

  1. Pierwszy a[2] = 1jest ustawiony. Oznacza to, że tablica mówi:0 0 1 0 0
  2. Ale oto, biorąc pod uwagę, że zrobiłeś to w { }nawiasach, które są używane do inicjalizacji tablicy w kolejności, przyjmuje pierwszą wartość (czyli 1) i ustawia ją na a[0]. To tak, jakby zostało int a[5] = { a[2] };tam, gdzie już dotarliśmy a[2] = 1. Wynikowa tablica to teraz:1 0 1 0 0

Inny przykład: int a[6] = { a[3] = 1, a[4] = 2, a[5] = 3 };- Mimo że kolejność jest nieco arbitralna, zakładając, że jest przesuwana od lewej do prawej, będzie przebiegać w tych 6 krokach:

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
Bitwa
źródło
1
A = B = C = 5nie jest deklaracją (ani inicjalizacją). Jest to normalne wyrażenie, które analizuje się jako, A = (B = (C = 5))ponieważ =operator jest prawoskrętny. To naprawdę nie pomaga w wyjaśnieniu, jak działa inicjalizacja. Tablica faktycznie zaczyna istnieć w momencie wprowadzenia bloku, w którym została zdefiniowana, co może nastąpić na długo przed wykonaniem właściwej definicji.
melpomene
1
Od lewej do prawej, każdy zaczyna się od deklaracji wewnętrznej ” jest niepoprawne. Standard C wyraźnie mówi: „ Kolejność, w jakiej występują efekty uboczne wśród wyrażeń listy inicjalizacji, jest nieokreślona.
melpomene
1
Testujesz kod z mojego przykładu dostatecznie dużo razy i sprawdzasz, czy wyniki są spójne. ” Nie tak to działa. Wydaje się, że nie rozumiesz, czym jest niezdefiniowane zachowanie. Wszystko w C ma domyślnie niezdefiniowane zachowanie; po prostu niektóre części mają zachowanie zdefiniowane przez standard. Aby udowodnić, że coś ma określone zachowanie, musisz przytoczyć standard i pokazać, gdzie definiuje, co powinno się wydarzyć. W przypadku braku takiej definicji zachowanie jest nieokreślone.
melpomene
1
Twierdzenie w punkcie (1) jest ogromnym skokiem w stosunku do kluczowego pytania tutaj: czy niejawna inicjalizacja elementu a [2] do 0 występuje przed zastosowaniem efektu ubocznego a[2] = 1wyrażenia inicjalizującego? Zaobserwowany wynik jest taki, jakby był, ale norma nie wydaje się określać, że tak powinno być. To jest centrum kontrowersji, a ta odpowiedź całkowicie go pomija.
John Bollinger,
1
„Niezdefiniowane zachowanie” to termin techniczny o wąskim znaczeniu. Nie oznacza to „zachowania, którego nie jesteśmy pewni”. Kluczowym wnioskiem jest tutaj to, że żaden test, bez kompilatora, nie może nigdy wykazać, że dany program zachowuje się lub nie zachowuje się dobrze zgodnie ze standardem , ponieważ jeśli program ma niezdefiniowane zachowanie, kompilator może zrobić wszystko - w tym działać w doskonale przewidywalny i rozsądny sposób. Nie chodzi po prostu o problem z jakością implementacji, gdy autorzy kompilatora dokumentują pewne rzeczy - jest to nieokreślone lub zdefiniowane w implementacji zachowanie.
Jeroen Mostert,
0

Przypisanie a[2]= 1jest wyrażeniem, które ma wartość 1i zasadniczo zostało napisane int a[5]= { 1 };(z a[2]przypisanym efektem ubocznym 1).

Yves Daoust
źródło
Ale nie jest jasne, kiedy oceniany jest efekt uboczny, a zachowanie może się zmienić w zależności od kompilatora. Również norma wydaje się stwierdzać, że jest to niezdefiniowane zachowanie, przez co wyjaśnienia dotyczące realizacji specyficznych dla kompilatora nie są pomocne.
Do widzenia SE
@KamiKaze: jasne, wartość 1 wylądowała tam przez przypadek.
Yves Daoust
0

Myślę, że int a[5]={ a[2]=1 };to dobry przykład dla programisty strzelającego sobie w stopę.

Mógłbym ulec pokusie, by pomyśleć, że chodziło Ci o to, int a[5]={ [2]=1 };że będzie to inicjator wyznaczony przez C99 ustawiający element 2 na 1, a resztę na zero.

W rzadkich przypadkach, o których naprawdę miałeś na myśli int a[5]={ 1 }; a[2]=1;, to byłby zabawny sposób pisania. W każdym razie do tego sprowadza się twój kod, mimo że niektórzy tutaj zauważyli, że nie jest dobrze zdefiniowany, kiedy zapis a[2]jest faktycznie wykonywany. Pułapka polega na tym, że a[2]=1nie jest to wyznaczony inicjator, ale proste przypisanie, które samo w sobie ma wartość 1.

Sven
źródło
wygląda na to, że ten temat językowo-prawniczy polega na pytaniu o referencje ze standardowych projektów. Dlatego jesteś negatywnie oceniony (nie zrobiłem tego, jak widzisz, jestem odrzucony z tego samego powodu). Myślę, że to, co napisałeś, jest w porządku, ale wygląda na to, że wszyscy ci prawnicy językowi tutaj są albo z komitetu, albo z czegoś podobnego. Więc wcale nie proszą o pomoc, próbują sprawdzić, czy projekt obejmuje sprawę, czy nie, a większość facetów tutaj jest wyzwalanych, jeśli udzielisz odpowiedzi, tak jak ty im pomagasz. Chyba źle skasuję moją odpowiedź :) Gdyby ten temat był jasno określony, to byłoby pomocne
Abdurrahim