„Int * nums = {5, 2, 1, 4}” powoduje błąd segmentacji

81

powoduje segfault, podczas gdy

nie. Teraz:

wydruki 5.

Opierając się na tym, przypuszczałem, że notacja inicjalizacji tablicy {} ślepo ładuje te dane do dowolnej zmiennej po lewej stronie. Gdy jest to int [], tablica jest wypełniana zgodnie z potrzebami. Kiedy jest to int *, wskaźnik jest zapełniany przez 5, a lokalizacje pamięci po miejscu, w którym jest przechowywany wskaźnik, są wypełniane o 2, 1 i 4. So nums [0] próbuje usunąć 5, powodując segfault.

Jeśli się mylę, popraw mnie. A jeśli mam rację, proszę o wyjaśnienie, ponieważ nie rozumiem, dlaczego inicjatory tablic działają tak, jak działają.

user1299784
źródło
3
Skompiluj z włączonymi wszystkimi ostrzeżeniami, a Twój kompilator powinien powiedzieć Ci, co się stanie.
Jabberwocky
1
@GSerg To nie jest w pobliżu duplikatu. W tym pytaniu nie ma wskaźnika tablicy. Mimo że niektóre odpowiedzi w tym poście są podobne do tych tutaj.
Lundin,
2
@Lundin Byłem pewien w 30%, więc nie głosowałem za zamknięciem, tylko zamieściłem link.
GSerg
3
Nabierz nawyku uruchamiania GCC z -pedantic-errorsflagą i obserwuj diagnostykę. int *nums = {5, 2, 1, 4};nie jest ważny C.
AnT

Odpowiedzi:

113

W języku C istnieje (głupia) reguła mówiąca, że ​​każda zwykła zmienna może być zainicjowana listą inicjalizującą w nawiasach klamrowych, tak jakby była tablicą.

Na przykład możesz napisać int x = {0};, co jest całkowicie równoważne z int x = 0;.

Więc kiedy piszesz int *nums = {5, 2, 1, 4};, w rzeczywistości podajesz listę inicjalizującą pojedynczej zmiennej wskaźnikowej. Jednak jest to tylko jedna zmienna, więc zostanie przypisana tylko pierwsza wartość 5, reszta listy jest ignorowana (właściwie nie sądzę, aby kod z nadmiarem inicjalizatorów powinien się nawet kompilować za pomocą ścisłego kompilatora) - tak nie jest w ogóle zostać zapisane w pamięci. Kod jest równoważny z int *nums = 5;. Co oznacza, że numspowinien wskazywać na adres 5 .

W tym momencie powinieneś już otrzymać dwa ostrzeżenia / błędy kompilatora:

  • Przypisywanie liczby całkowitej do wskaźnika bez rzutowania.
  • Nadmiar elementów na liście inicjalizacyjnej.

A potem oczywiście kod ulegnie awarii i wypali się, ponieważ 5najprawdopodobniej nie jest to prawidłowy adres, do którego można się odwołać nums[0].

Na marginesie, powinieneś printfwskazać adresy ze %pspecyfikatorem lub w inny sposób wywołać niezdefiniowane zachowanie.


Nie jestem do końca pewien, co próbujesz tutaj zrobić, ale jeśli chcesz ustawić wskaźnik tak, aby wskazywał na tablicę, powinieneś zrobić:

Lub jeśli chcesz utworzyć tablicę wskaźników:


EDYTOWAĆ

Po kilku badaniach mogę powiedzieć, że „lista inicjalizująca nadmiarowe elementy” rzeczywiście nie jest poprawna. C - jest to rozszerzenie GCC .

Standard 6.7.9 Inicjalizacja mówi (moje podkreślenie):

2 Żaden inicjator nie może próbować podać wartości dla obiektu nie zawartego w inicjowanej jednostce.

/ - /

11 Inicjator dla skalara powinien być pojedynczym wyrażeniem, opcjonalnie zamkniętym w nawiasy klamrowe. Początkową wartością obiektu jest wartość wyrażenia (po konwersji); obowiązują te same ograniczenia typu i konwersje, co w przypadku prostego przypisania, przyjmując typ skalara jako niekwalifikowaną wersję zadeklarowanego typu.

„Typ skalarny” jest standardowym terminem odnoszącym się do pojedynczych zmiennych, które nie są typu tablicowego, strukturalnego lub sumarycznego (są to nazywane „typami agregującymi”).

Tak więc w prostym języku angielskim standard mówi: „kiedy inicjalizujesz zmienną, możesz dodać kilka dodatkowych nawiasów klamrowych wokół wyrażenia inicjującego, tylko dlatego, że możesz”.

Lundin
źródło
11
Nie ma nic "głupiego" w możliwości zainicjowania obiektu skalarnego z pojedynczą wartością zamkniętą w {}. Wręcz przeciwnie, ułatwia jeden z najważniejszych i wygodnych idiomów języka C - { 0 }jako uniwersalny inicjator zera. Wszystko w C może być inicjalizowane przez zero = { 0 }. Jest to bardzo ważne przy pisaniu kodu niezależnego od typu.
AnT
3
@AnT Nie ma czegoś takiego jak „uniwersalny inicjator zerowy”. W przypadku agregatów {0}oznacza jedynie inicjalizację pierwszego obiektu do zera i inicjalizację pozostałych obiektów tak, jakby miały statyczny czas trwania. Powiedziałbym, że jest to raczej przypadek niż celowy projekt języka jakiegoś "uniwersalnego inicjatora", ponieważ {1}nie inicjalizuje wszystkich obiektów na 1.
Lundin
3
@Lundin C11 6.5.16.1/1 obejmuje p = 5;(żaden z wymienionych przypadków nie jest spełniony dla przypisania liczby całkowitej do wskaźnika); a 6.7.9 / 11 mówi, że ograniczenia przypisania są również używane do inicjalizacji.
MM,
4
@Lundin: Tak, jest. Nie ma znaczenia, jaki mechanizm inicjuje jaką część obiektu. Nie ma też żadnego znaczenia, czy {}w tym celu dopuszcza się inicjalizację skalarów. Jedyną rzeczą, która ma znaczenie, jest to, że = { 0 }inicjalizator gwarantuje zerową inicjalizację całego obiektu , co właśnie uczyniło go klasycznym i jednym z najbardziej eleganckich idiomów języka C.
AnT
2
@Lundin: Nie jest też dla mnie jasne, co twoja uwaga {1}ma wspólnego z tematem. Nikt nigdy nie twierdził, że {0}interpretuje to 0jako wielokrotną inicjalizację dla każdego członka agregatu.
AnT
28

SCENARIUSZ 1

Dlaczego ten się rozbija?

Zadeklarowałeś numsjako wskaźnik do int - numsto ma zawierać adres one liczby całkowitej w pamięci.

Następnie próbowałeś zainicjować numstablicę wielu wartości. Tak więc bez zagłębiania się w wiele szczegółów jest to koncepcyjnie niepoprawne - nie ma sensu przypisywanie wielu wartości zmiennej, która ma mieć jedną wartość. Pod tym względem zobaczysz dokładnie ten sam efekt, jeśli zrobisz to:

W każdym przypadku (przypisz wiele wartości do wskaźnika lub zmiennej int), wtedy zmienna otrzyma pierwszą wartość, która jest 5, a pozostałe wartości są ignorowane. Ten kod jest zgodny, ale otrzymasz ostrzeżenia dla każdej dodatkowej wartości, która nie powinna znajdować się w przypisaniu:

warning: excess elements in scalar initializer.

W przypadku przypisywania wielu wartości do zmiennej wskaźnikowej, program segfaults podczas uzyskiwania dostępu nums[0], co oznacza, że dosłownie odrzucasz wszystko, co jest zapisane pod adresem 5 . W tym przypadku nie przydzieliłeś żadnej prawidłowej pamięci dla wskaźnika nums.

Warto zauważyć, że nie ma segfaulta w przypadku przypisania wielu wartości do zmiennej int (nie wyłuskujemy tutaj żadnego nieprawidłowego wskaźnika).


SCENARIUSZ 2

Ten nie powoduje segfaultów, ponieważ zgodnie z prawem przydzielasz tablicę 4 int w stosie.


SCENARIUSZ 3

Ten nie działa zgodnie z oczekiwaniami, ponieważ drukujesz wartość samego wskaźnika - NIE tego, co wyłuskuje (co jest nieprawidłowym dostępem do pamięci).


Inni

Prawie zawsze jest skazane na segfault za każdym razem, gdy zakodujesz wartość takiego wskaźnika (ponieważ zadaniem systemu operacyjnego jest określenie, który proces może uzyskać dostęp do jakiej lokalizacji pamięci).

Tak więc ogólną zasadą jest, aby zawsze inicjalizować wskaźnik do adresu jakiejś przydzielonej zmiennej, takiej jak:

lub,

artm
źródło
2
+1 To dobra rada, ale „nigdy” jest naprawdę zbyt mocne, biorąc pod uwagę magiczne adresy na wielu platformach. (Używanie stałych tabel dla tych ustalonych adresów nie wskazuje na istniejące zmienne, a więc narusza twoją zasadę, jak stwierdzono). Niskopoziomowe sprawy, takie jak rozwój sterowników, zajmują się tego rodzaju rzeczami dość często.
The Nate
3
„To jest poprawne” - ignorowanie nadmiarowych inicjatorów jest rozszerzeniem GCC; w standardzie C, które jest niedozwolone
MM
1
@TheNate - tak, masz rację. Zredagowałem na podstawie twojego komentarza - dzięki.
artm
@MM - dziękuję za zwrócenie uwagi. Edytowałem, żeby to usunąć.
artm,
25

int *nums = {5, 2, 1, 4};jest źle sformułowanym kodem. Istnieje rozszerzenie GCC, które traktuje ten kod tak samo, jak:

próba utworzenia wskaźnika do adresu pamięci 5. (Nie wydaje mi się to przydatne rozszerzenie, ale wydaje mi się, że programiści tego chcą).

Aby uniknąć tego zachowania (lub przynajmniej otrzymać ostrzeżenie), możesz skompilować w trybie standardowym, np -std=c11 -pedantic .

Alternatywną formą prawidłowego kodu byłoby:

co wskazuje na zmienny literał o takim samym czasie przechowywania jak nums. Jednak int nums[]wersja jest ogólnie lepsza, ponieważ zajmuje mniej miejsca i można jej użyć sizeofdo wykrycia długości tablicy.

MM
źródło
Czy macierz w postaci literału złożonego miałaby gwarantowany okres przechowywania co najmniej tak długi, jak ten nums?
supercat
@supercat tak, jest to automatyczne, jeśli nums jest automatyczne, i statyczne, jeśli nums jest statyczne
MM
@MM: Czy miałoby to zastosowanie, nawet jeśli numsjest to zmienna statyczna zadeklarowana w funkcji, czy też kompilator byłby uprawniony do ograniczenia czasu życia tablicy do czasu otaczającego bloku, nawet jeśli byłby przypisany do zmiennej statycznej?
supercat
@supercat tak (pierwszy bit). Druga opcja oznaczałaby UB przy drugim wywołaniu funkcji (ponieważ zmienne statyczne są inicjowane tylko przy pierwszym wywołaniu)
MM
12

numsjest wskaźnikiem typu int. Więc powinieneś wskazać na jakąś prawidłową lokalizację pamięci.num[0]próbujesz wyłuskać jakąś losową lokalizację pamięci i stąd błąd segmentacji.

Tak, wskaźnik ma wartość 5 i próbujesz usunąć z niej odwołanie, co jest niezdefiniowanym zachowaniem w Twoim systemie. (Wygląda jak5 to, że nie jest to prawidłowa lokalizacja pamięci w twoim systemie)

Natomiast

jest prawidłową deklaracją, w której mówisz, że numsjest tablicą typu, inta pamięć jest przydzielana na podstawie liczby elementów przekazanych podczas inicjalizacji.

Gopi
źródło
1
„Tak, wskaźnik ma wartość 5 i próbujesz usunąć z niej odwołanie, co jest niezdefiniowanym zachowaniem”. Wcale nie, jest to doskonale dobre i dobrze zdefiniowane zachowanie. Ale w systemie używanym przez OP nie jest to prawidłowy adres pamięci, stąd awaria.
Lundin
@Lundin Zgadzam się. Ale myślę, że OP nigdy nie wiedział, że 5 jest prawidłową lokalizacją pamięci, więc mówiłem w tych wierszach. Mam nadzieję, że edycja pomoże
Gopi
Powinno tak być? int *nums = (int[]){5, 2, 1, 4};
Islam Azab
10

Przypisując {5, 2, 1, 4}

przypisujesz 5 do nums(po niejawnym rzutowaniu z int na wskaźnik do int). Po usunięciu wywołania wywołanie dostępu do lokalizacji pamięci pod adresem 0x5. To może nie być dozwolone dla twojego programu.

Próbować

Fahad Siddiqui
źródło