"Int * nums = {5, 2, 1, 4}" verursacht einen Segmentierungsfehler

81
int *nums = {5, 2, 1, 4};
printf("%d\n", nums[0]);

verursacht einen Segfault, während

int nums[] = {5, 2, 1, 4};
printf("%d\n", nums[0]);

nicht. Jetzt:

int *nums = {5, 2, 1, 4};
printf("%d\n", nums);

druckt 5.

Auf dieser Grundlage habe ich vermutet, dass die Array-Initialisierungsnotation {} diese Daten blind in die Variable auf der linken Seite lädt. Wenn es int [] ist, wird das Array wie gewünscht gefüllt. Wenn es int * ist, wird der Zeiger mit 5 gefüllt, und die Speicherstellen, nach denen der Zeiger gespeichert ist, werden mit 2, 1 und 4 gefüllt. Num [0] versucht also, 5 zu deref, was einen Segfault verursacht.

Wenn ich falsch liege, korrigieren Sie mich bitte. Und wenn ich richtig liege, erläutern Sie dies bitte, da ich nicht verstehe, warum Array-Initialisierer so funktionieren, wie sie es tun.

user1299784
quelle
3
Kompilieren Sie mit allen aktivierten Warnungen und Ihr Compiler sollte Ihnen mitteilen, was passiert.
Jabberwocky
1
@GSerg Das ist nicht annähernd ein Duplikat. In dieser Frage gibt es keinen Array-Zeiger. Obwohl einige Antworten in diesem Beitrag denen hier ähnlich sind.
Lundin
2
@Lundin Ich war mir zu 30% sicher, also habe ich nicht für das Schließen gestimmt, sondern nur den Link gepostet.
GSerg
3
Gewöhnen Sie sich an, GCC mit -pedantic-errorsFlag auszuführen, und beobachten Sie die Diagnose. int *nums = {5, 2, 1, 4};ist nicht gültig C.
AnT

Antworten:

113

Es gibt eine (dumme) Regel in C, die besagt, dass jede einfache Variable mit einer in Klammern eingeschlossenen Initialisierungsliste initialisiert werden kann, als wäre es ein Array.

Zum Beispiel können Sie schreiben int x = {0};, was völlig gleichbedeutend ist mit int x = 0;.

Wenn Sie also schreiben, geben int *nums = {5, 2, 1, 4};Sie einer einzelnen Zeigervariablen tatsächlich eine Initialisierungsliste. Da es sich jedoch nur um eine einzelne Variable handelt, wird ihr nur der erste Wert 5 zugewiesen, der Rest der Liste wird ignoriert (eigentlich denke ich nicht, dass Code mit überschüssigen Initialisierern sogar mit einem strengen Compiler kompiliert werden sollte) - dies ist nicht der Fall überhaupt in den Speicher geschrieben werden. Der Code entspricht int *nums = 5;. Was bedeutet, numssollte auf Adresse zeigen 5 .

Zu diesem Zeitpunkt sollten Sie bereits zwei Compiler-Warnungen / Fehler erhalten haben:

  • Zuweisen einer Ganzzahl zum Zeiger ohne Umwandlung.
  • Überschüssige Elemente in der Initialisierungsliste.

Und dann stürzt der Code natürlich ab und brennt, da 5es sich höchstwahrscheinlich nicht um eine gültige Adresse handelt, mit der Sie dereferenzieren dürfen nums[0].

Als Randnotiz sollten Sie printfAdressen mit dem %pBezeichner verweisen oder auf andere Weise undefiniertes Verhalten aufrufen.


Ich bin mir nicht ganz sicher, was Sie hier versuchen, aber wenn Sie einen Zeiger auf ein Array setzen möchten, sollten Sie Folgendes tun:

int nums[] = {5, 2, 1, 4};
int* ptr = nums;

// or equivalent:
int* ptr = (int[]){5, 2, 1, 4};

Oder wenn Sie ein Array von Zeigern erstellen möchten:

int* ptr[] = { /* whatever makes sense here */ };

BEARBEITEN

Nach einigen Recherchen kann ich sagen, dass die "Initialisierungsliste für überschüssige Elemente" tatsächlich nicht gültig ist. C - es handelt sich um eine GCC-Erweiterung .

Der Standard 6.7.9 Initialisierung sagt (Schwerpunkt Mine):

2 Kein Initialisierer darf versuchen, einen Wert für ein Objekt bereitzustellen, das nicht in der zu initialisierenden Entität enthalten ist.

/ - /

11 Der Initialisierer für einen Skalar muss ein einzelner Ausdruck sein, der optional in geschweiften Klammern eingeschlossen ist. Der Anfangswert des Objekts ist der des Ausdrucks (nach der Konvertierung). Es gelten die gleichen Typbeschränkungen und Konvertierungen wie für die einfache Zuweisung, wobei der Typ des Skalars als nicht qualifizierte Version seines deklarierten Typs angesehen wird.

"Skalartyp" ist ein Standardbegriff, der sich auf einzelne Variablen bezieht, die nicht vom Typ Array, Struktur oder Vereinigung sind (diese werden als "Aggregattyp" bezeichnet).

Im Klartext heißt es im Standard: "Wenn Sie eine Variable initialisieren, können Sie einige zusätzliche Klammern um den Initialisiererausdruck setzen, nur weil Sie können."

Lundin
quelle
11
Es ist nichts "Dummes" an der Fähigkeit, ein skalares Objekt mit einem einzelnen Wert zu initialisieren {}. Im Gegenteil, es erleichtert eine der wichtigsten und bequemsten Redewendungen der C-Sprache - { 0 }als universeller Null-Initialisierer. Alles in C kann durch Null initialisiert werden = { 0 }. Dies ist sehr wichtig für das Schreiben von typunabhängigem Code.
Am
3
@AnT Es gibt keinen "universellen Null-Initialisierer". Bei Aggregaten {0}bedeutet dies lediglich, das erste Objekt auf Null zu initialisieren und den Rest der Objekte so zu initialisieren, als ob sie eine statische Speicherdauer hätten. Ich würde sagen, dass dies eher ein Zufall als ein absichtliches Sprachdesign eines "universellen Initialisierers" ist, da {1}nicht alle Objekte auf 1 initialisiert werden.
Lundin
3
@Lundin C11 6.5.16.1/1 deckt ab p = 5;(keiner der aufgelisteten Fälle wird erfüllt, um dem Zeiger eine Ganzzahl zuzuweisen ); und 6.7.9 / 11 besagt, dass die Einschränkungen für die Zuweisung auch für die Initialisierung verwendet werden.
MM
4
@Lundin: Ja, das gibt es. Es ist völlig irrelevant, welcher Mechanismus welchen Teil des Objekts initialisiert. Es ist auch völlig irrelevant, ob die {}Initiierung von Skalaren speziell für diesen Zweck zulässig ist. Das einzige, was zählt, ist, dass der = { 0 }Initialisierer garantiert das gesamte Objekt auf Null initialisiert. Genau das machte es zu einem Klassiker und einer der elegantesten Redewendungen der C-Sprache.
Am
2
@Lundin: Es ist mir auch völlig unklar, was Ihre Bemerkung {1}mit dem Thema zu tun hat. Niemand hat jemals behauptet, {0}dass dies 0als Multi-Initialisierer für jedes einzelne Mitglied des Aggregats interpretiert wird .
Am
28

SZENARIO 1

int *nums = {5, 2, 1, 4};    // <-- assign multiple values to a pointer variable
printf("%d\n", nums[0]);    // segfault

Warum ist dieser eine Fehler?

Sie haben numsals Zeiger auf int deklariert - das numssoll die Adresse einer ganzen Zahl im Speicher enthalten.

Sie haben dann versucht, numsein Array mit mehreren Werten zu initialisieren . Ohne auf viele Details einzugehen, ist dies konzeptionell falsch - es ist nicht sinnvoll, einer Variablen, die einen Wert enthalten soll, mehrere Werte zuzuweisen. In dieser Hinsicht würden Sie genau den gleichen Effekt sehen, wenn Sie dies tun:

int nums = {5, 2, 1, 4};    // <-- assign multiple values to an int variable
printf("%d\n", nums);    // also print 5

In beiden Fällen (einem Zeiger oder einer int-Variablen mehrere Werte zuweisen) erhält die Variable dann den ersten Wert 5, während die verbleibenden Werte ignoriert werden. Dieser Code entspricht, aber Sie erhalten Warnungen für jeden zusätzlichen Wert, der nicht in der Zuweisung enthalten sein soll:

warning: excess elements in scalar initializer.

Wenn Sie der Zeigervariablen mehrere Werte zuweisen, wird das Programm beim Zugriff fehlerhaft ausgeführt. Dies nums[0]bedeutet, dass Sie alles, was in Adresse 5 gespeichert ist, buchstäblich zurückstellen . numsIn diesem Fall haben Sie keinen gültigen Speicher für den Zeiger zugewiesen .

Es ist erwähnenswert, dass es keinen Segfault für den Fall gibt, dass der Variablen int mehrere Werte zugewiesen werden (Sie dereferenzieren hier keinen ungültigen Zeiger).


SZENARIO 2

int nums[] = {5, 2, 1, 4};

Dieser ist kein Segfault, da Sie legal ein Array von 4 Ints im Stapel zuweisen.


SZENARIO 3

int *nums = {5, 2, 1, 4};
printf("%d\n", nums);   // print 5

Dieser Fehler tritt nicht wie erwartet auf, da Sie drucken den Wert des Zeigers selbst - NICHT das, was er dereferenziert (was ein ungültiger Speicherzugriff ist).


Andere

Es ist fast immer zum Segfault verurteilt, wenn Sie den Wert eines Zeigers fest codieren wie diesen fest (da es die Aufgabe des Betriebssystems ist, zu bestimmen, welcher Prozess auf welchen Speicherort zugreifen kann).

int *nums = 5;    // <-- segfault

Als Faustregel gilt also, immer einen Zeiger auf die Adresse einiger zugeordneter zu initialisieren Variablen , z.

int a;
int *nums = &a;

oder,

int a[] = {5, 2, 1, 4};
int *nums = a; 
artm
quelle
2
+1 Das ist ein guter Rat, aber "nie" ist angesichts der magischen Adressen auf vielen Plattformen wirklich zu stark. (Die Verwendung konstanter Tabellen für diese festen Adressen verweist nicht auf vorhandene Variablen und verstößt daher wie angegeben gegen Ihre Regel.) Niedrige Dinge wie die Treiberentwicklung befassen sich ziemlich häufig mit solchen Dingen.
The Nate
3
"Dies ist gültig" - das Ignorieren überschüssiger Initialisierer ist eine GCC-Erweiterung. in Standard C ist das nicht erlaubt
MM
1
@ TheNate - ja du bist richtig. Ich habe die Basis auf Ihren Kommentar hin bearbeitet - danke.
Artm
@MM - danke für den Hinweis. Ich habe bearbeitet, um das zu entfernen.
Artm
25

int *nums = {5, 2, 1, 4};ist schlecht geformter Code. Es gibt eine GCC-Erweiterung, die diesen Code wie folgt behandelt:

int *nums = (int *)5;

Versuch, einen Zeiger auf die Speicheradresse 5 zu bilden. (Dies scheint mir keine nützliche Erweiterung zu sein, aber ich denke, die Entwicklerbasis möchte es).

Um dieses Verhalten zu vermeiden (oder zumindest eine Warnung zu erhalten), können Sie im Standardmodus kompilieren, z -std=c11 -pedantic.

Eine alternative Form des gültigen Codes wäre:

int *nums = (int[]){5, 2, 1, 4};

Dies zeigt auf ein veränderliches Literal mit der gleichen Speicherdauer wie nums. Die int nums[]Version ist jedoch im Allgemeinen besser, da weniger Speicherplatz benötigt wird und Sie feststellen sizeofkönnen, wie lang das Array ist.

MM
quelle
Würde das Array in der zusammengesetzten Literalform eine mindestens so lange Speicherlebensdauer wie die von haben nums?
Supercat
@supercat ja, es ist automatisch, wenn nums automatisch ist, und statisch, wenn nums statisch ist
MM
@MM: Würde dies auch dann gelten, wenn numseine statische Variable innerhalb einer Funktion deklariert ist, oder wäre der Compiler berechtigt, die Lebensdauer des Arrays auf die des umschließenden Blocks zu beschränken, selbst wenn es einer statischen Variablen zugewiesen wurde?
Supercat
@supercat ja (das erste Bit). Die zweite Option würde UB beim zweiten Aufruf der Funktion bedeuten (da statische Variablen nur beim ersten Aufruf initialisiert werden)
MM
12
int *nums = {5, 2, 1, 4};

numsist ein Zeiger vom Typ int. Sie sollten diesen Punkt also auf einen gültigen Speicherort hinweisen.num[0]Sie versuchen, einen zufälligen Speicherort und damit den Segmentierungsfehler zu dereferenzieren.

Ja, der Zeiger hält den Wert 5 und Sie versuchen, ihn zu dereferenzieren, was auf Ihrem System ein undefiniertes Verhalten ist. (Sieht aus wie5 als wäre kein gültiger Speicherort auf Ihrem System vorhanden.)

Wohingegen

int nums[] = {1,2,3,4};

ist eine gültige Deklaration, bei der Sie sagen, dass numses sich um ein Array vom Typ handelt intund der Speicher basierend auf der Anzahl der während der Initialisierung übergebenen Elemente zugewiesen wird.

Gopi
quelle
1
"Ja, der Zeiger hält den Wert 5 und Sie versuchen, ihn zu dereferenzieren, was ein undefiniertes Verhalten ist." Überhaupt nicht, es ist vollkommen feines und klar definiertes Verhalten. Auf dem System, das das OP verwendet, ist es jedoch keine gültige Speicheradresse, daher der Absturz.
Lundin
@ Lundin zustimmen. Aber ich denke, OP wusste nie, dass 5 ein gültiger Speicherort ist, also sprach ich in diesen Zeilen. Hoffe, die Bearbeitung hilft
Gopi
Sollte so sein? int *nums = (int[]){5, 2, 1, 4};
Islam Azab
10

Durch Zuweisung {5, 2, 1, 4}

int *nums = {5, 2, 1, 4};

Sie weisen 5 zu nums(nach einer impliziten Typumwandlung von int zu Zeiger auf int). Wenn Sie es verschieben, rufen Sie den Speicherort unter auf 0x5. Darauf kann Ihr Programm möglicherweise nicht zugreifen.

Versuchen

printf("%p", (void *)nums);
Fahad Siddiqui
quelle