String-Literale vs. Array von char beim Initialisieren eines Zeigers

Lesezeit: 9 Minuten

Benutzer-Avatar
Eugen Sch.

Inspiriert von dieser Frage.

Wir können a initialisieren char Zeiger durch ein String-Literal:

char *p = "ab";

Und es ist vollkommen in Ordnung. Man könnte meinen, dass es mit Folgendem äquivalent ist:

char *p = {'a', 'b', '\0'};

Aber anscheinend ist es nicht der Fall. Und das nicht nur, weil die String-Literale in einem Nur-Lese-Speicher gespeichert sind, sondern es scheint, dass auch das String-Literal einen Typ hat char Array und den Initialisierer {...} hat die Art von char array werden zwei Deklarationen unterschiedlich behandelt, da der Compiler die Warnung ausgibt:

Warnung: Überzählige Elemente im Skalar-Initialisierer

im zweiten Fall. Was ist die Erklärung für ein solches Verhalten?

Aktualisieren:

Außerdem im letzteren Fall der Zeiger p wird den Wert von haben 0x61 (der Wert des ersten Array-Elements 'a') anstelle eines Speicherorts, sodass der Compiler, wie gewarnt, nur das erste Element des Initialisierers nimmt und es zuweist p.

  • ha… wusste nicht, dass es das tut. Ich hätte schwören können, dass es identisch war.

    – Bolov

    29. Mai 2015 um 15:34 Uhr


  • @bolov Ja. Deshalb lese ich gerne Fragen zu SO. Es beweist manchmal, dass es etwas ist, sich selbst als Profi in etwas zu betrachten leicht überschätzen… Indem man ganz einfache Dinge zeigt.

    – Eugen Sch.

    29. Mai 2015 um 15:36 Uhr

  • Ich denke, die String-Initialisierungssyntax funktioniert, weil sie letztendlich durch ihren Speicherort ersetzt und in eine Adresse aufgelöst wird. Andererseits sieht der Compiler ein char *p als Ort zum Speichern eines einzelnen Werts, und die explizite Array-Initialisierung impliziert, dass mehr als ein Wert gespeichert werden muss.

    – David W

    29. Mai 2015 um 15:40 Uhr

  • @DavidW so erkläre ich es mir im Grunde, dass die Initialisierungsausdrücke anders funktionieren als Laufzeitausdrücke.

    – Eugen Sch.

    29. Mai 2015 um 16:01 Uhr

Ich glaube, Sie sind verwirrt, weil char *p = "ab"; und char p[] = "ab"; haben eine ähnliche Semantik, aber unterschiedliche Bedeutungen.

Ich glaube, dass letzterer Fall (char p[] = "ab";) wird am besten als a angesehen Kurzschreibweise zum char p[] = {'a', 'b', '\0'}; (initialisiert ein Array mit der vom Initialisierer bestimmten Größe). Eigentlich könnte man in diesem Fall sagen "ab" wird nicht wirklich als verwendet String-Literal.

Allerdings ist der erstgenannte Fall (char *p = "ab";) unterscheidet sich darin, dass es einfach den Zeiger initialisiert p auf das erste Element des zeigen schreibgeschütztes Zeichenfolgenliteral "ab".

Ich hoffe, Sie sehen den Unterschied. Während char p[] = "ab"; ist als Initialisierung darstellbar, wie Sie es beschrieben haben, char *p = "ab"; ist nicht, da Zeiger, nun ja, keine Arrays sind, und ihre Initialisierung mit einem Array-Initialisierer macht etwas ganz anderes (nämlich ihnen den Wert des ersten Elements zu geben, 0x61 in Ihrem Fall).

Um es kurz zu machen, C-Compiler “ersetzen” nur ein String-Literal durch a char Array-Initialisierer, wenn er dazu geeignet ist, dh er wird verwendet, um a zu initialisieren char Reihe.

  • Das OP versteht das. Er möchte im Grunde wissen, warum die Initialisierungsliste nicht als Literal interpretiert wird.

    – Gopi

    29. Mai 2015 um 16:14 Uhr

  • @Gopi Fast ist es eher so, warum das Literal nicht als Liste interpretiert wird.

    – Eugen Sch.

    29. Mai 2015 um 16:15 Uhr

  • @EugeneSh.: Es tut mir leid. Siehe Aktualisierung. Ich hoffe, was ich sagen wollte, ist jetzt klarer

    Benutzer3079266

    29. Mai 2015 um 16:16 Uhr

  • @EugenSch. Strings, wie wir in C wissen, haben mehrere Varianten, und ich denke, es ist Konvention, Literls in doppelten Anführungszeichen darzustellen. (Der Standard unterstützt meine Theorie) 🙂

    – Gopi

    29. Mai 2015 um 16:20 Uhr

  • Wie ich aus allen Antworten ersehen kann, konvergiert alles zur Syntax. String-Literale scheinen je nach Kontext unterschiedlich behandelt zu werden.

    – Eugen Sch.

    29. Mai 2015 um 16:24 Uhr

Zeichenfolgenliterale haben ein “magisch” Status in C. Sie sind anders als alles andere. Um zu verstehen, warum, ist es hilfreich, dies im Hinblick auf die Speicherverwaltung zu betrachten. Fragen Sie sich zum Beispiel, “Wo wird ein String-Literal im Speicher gespeichert? Wann wird es aus dem Speicher entfernt?” und die Dinge werden anfangen, Sinn zu machen.

Sie sind anders als numerische Literale, die sich leicht in Maschinenanweisungen übersetzen lassen. Für ein vereinfachtes Beispiel etwa so:

int x = 123;

… könnte auf Maschinenebene in etwa so übersetzt werden:

mov ecx, 123

Wenn wir so etwas tun:

const char* str = "hello";

… wir haben jetzt ein Dilemma:

mov ecx, ???

Es gibt nicht unbedingt ein gewisses natives Verständnis der Hardware dafür, was eine Zeichenfolge mit mehreren Bytes und variabler Länge tatsächlich ist. Es kennt sich hauptsächlich mit Bits und Bytes und Zahlen aus und verfügt über Register, die dafür ausgelegt sind, diese Dinge zu speichern, aber eine Zeichenfolge ist ein Speicherblock, der mehrere davon enthält.

Daher müssen Compiler Anweisungen generieren, um den Speicherblock dieser Zeichenfolge irgendwo zu speichern, und daher generieren sie normalerweise Anweisungen, wenn Sie Ihren Code kompilieren, um diese Zeichenfolge irgendwo an einem global zugänglichen Ort zu speichern (normalerweise ein Nur-Lese-Speichersegment oder das Datensegment). Sie können auch mehrere identische Literal-Strings zusammenfügen, um in derselben Speicherregion gespeichert zu werden, um Redundanz zu vermeiden. Jetzt kann es eine generieren mov/load Anweisung zum Laden der Adresse in die Literalzeichenfolge, und Sie können dann indirekt über einen Zeiger damit arbeiten.

Ein weiteres Szenario, auf das wir stoßen könnten, ist dieses:

static const char* some_global_ptr = "blah";

int main()
{
    if (...)
    {
        const char* ptr = "hello";
        ...
        some_global_ptr = ptr;
    }
    printf("%s\n", some_global_ptr);
}

Natürlich ptr außerhalb des Gültigkeitsbereichs, aber wir brauchen den Speicher dieser Literalzeichenfolge, damit dieses Programm ein wohldefiniertes Verhalten hat. Literale Strings werden also nicht nur in Adressen zu global zugänglichen Speicherblöcken übersetzt, sondern sie werden auch nicht freigegeben, solange Ihre Binärdatei/Ihr Programm geladen/ausgeführt wird, sodass Sie sich keine Gedanken über ihre Speicherverwaltung machen müssen. [Edit: excluding potential optimizations: for the C programmer, we never have to worry about the memory management of a literal string, so the effect is like it’s always there].

Nun zu Zeichen-Arrays: Literal-Strings sind per se nicht unbedingt Zeichen-Arrays. Zu keinem Zeitpunkt in der Software können wir sie in einem Array-R-Wert erfassen, der uns die Anzahl der zugewiesenen Bytes mit angeben kann sizeof. Wir können nur durch auf die Erinnerung verweisen char*/const char*

Dieser Code gibt uns tatsächlich ein Handle für ein solches Array, ohne einen Zeiger einzubeziehen:

char str[] = "hello";

Hier passiert etwas Interessantes. Ein Produktionscompiler wird wahrscheinlich alle Arten von Optimierungen anwenden, aber ohne diese kann ein solcher Code auf einer grundlegenden Ebene zwei separate Speicherblöcke erstellen.

Der erste Block bleibt für die Dauer des Programms bestehen und enthält diese wörtliche Zeichenfolge. "hello". Der zweite Block wird dafür sein str Array, und es ist nicht unbedingt persistent. Wenn wir solchen Code in eine Funktion geschrieben haben, wird er Speicher auf dem Stack zuweisen, diesen Literal-String auf den Stack kopieren und den Speicher wann vom Stack freigeben str geht aus dem Rahmen. Die Adresse von str wird nicht mit der wörtlichen Zeichenfolge übereinstimmen, um es anders auszudrücken.

Schließlich, wenn wir so etwas schreiben:

char str[] = {'h', 'e', 'l', 'l', 'o', '\0'};

… es ist nicht unbedingt äquivalent, da hier keine wörtlichen Zeichenfolgen beteiligt sind. Natürlich darf ein Optimierer alle möglichen Dinge tun, aber in diesem Szenario ist es möglich, dass wir einfach einen einzelnen Speicherblock (der auf dem Stack zugewiesen und aus dem Stack freigegeben wird, wenn wir uns in einer Funktion befinden) mit Anweisungen erstellen um alle diese Zahlen (Zeichen), die Sie angegeben haben, auf den Stapel zu verschieben.

Während wir also in Bezug auf die Logik der Software effektiv denselben Effekt wie die vorherige Version erzielen, machen wir tatsächlich etwas subtil anderes, wenn wir keinen wörtlichen String angeben. Auch hier können Optimierer erkennen, dass etwas anderes den gleichen logischen Effekt haben kann, also könnten sie sich hier etwas einfallen lassen und diese beiden in Bezug auf Maschinenbefehle effektiv gleich machen. Aber kurz gesagt, dies ist ein subtil anderer Code, den wir schreiben.

Wenn wir Initialisierer wie {…} verwenden, erwartet der Compiler zu guter Letzt, dass Sie ihn einem aggregierten L-Wert mit Speicher zuweisen, der irgendwann zugewiesen und freigegeben wird, wenn Dinge den Gültigkeitsbereich verlassen. Deshalb erhalten Sie den Fehler, wenn Sie versuchen, so etwas einem Skalar (einem einzelnen Zeiger) zuzuweisen.

  • The first block is going to be persistent for the duration of the program, and will contain that literal string, "hello"… jeder Compiler, der das ohne a machen würde char *otherStr = "hello" an anderer Stelle im Code wird wertvoller Speicher verschwendet.

    Benutzer3079266

    29. Mai 2015 um 16:40 Uhr

  • Ja, um es zu vereinfachen, bin ich nicht zu sehr ins Detail gegangen, wie ein Optimierer damit umgehen würde. Ich wollte hauptsächlich nur den Unterschied zu einem grundlegenden Niveau erklären. Vielleicht sollte ich noch ein paar Vorbehalte hinzufügen.

    Benutzer4842163

    29. Mai 2015 um 16:43 Uhr


  • Wie können wir wissen, ob im Fall von char str zwei Speicherblöcke zugewiesen sind[] = “Hallo”;? Eine für String-Literal und die andere für a[]. Wie kann man das nach der Kompilierung überprüfen?

    – Jon Wheelock

    18. Oktober 2015 um 1:16 Uhr

  • @JonWheelock Kann die resultierende Assembly überprüfen. Eine andere Möglichkeit, die jedoch nicht unbedingt ein vollständiges Ergebnis garantiert, besteht darin, die Adresse von beiden auszugeben (str und &a). Dies könnte jedoch eine entfernte Chance haben, die Optimierung zu stören – der beste Weg ist, immer den Assemblercode zu überprüfen.

    Benutzer4842163

    18. Oktober 2015 um 12:57 Uhr

Benutzer-Avatar
Yu Hao

Das zweite Beispiel ist syntaktisch falsch. In C, {'a', 'b', '\0'} kann verwendet werden, um ein Array zu initialisieren, aber keinen Zeiger.

Stattdessen können Sie ein zusammengesetztes C99-Literal verwenden (auch in einigen Compilern als Erweiterung verfügbar, z. B. GCC) so was:

char *p = (char []){'a', 'b', '\0'};

Beachten Sie, dass es leistungsfähiger ist, da der Initialisierer nicht unbedingt nullterminiert ist.

  • Nitpick: nur oben c99MEINER BESCHEIDENEN MEINUNG NACH.

    – Sourav Ghosh

    29. Mai 2015 um 16:07 Uhr

  • Also, soweit ich verstehe, ist es nur eine Syntaxkonvention?

    – Eugen Sch.

    29. Mai 2015 um 16:09 Uhr

  • @EugenSch. Wenn Sie Syntaxzucker meinen, ist es das nicht.

    – Yu Hao

    29. Mai 2015 um 16:15 Uhr

  • Nicht Zucker. Nur Syntax.

    – Eugen Sch.

    29. Mai 2015 um 16:16 Uhr

  • @EugenSch. Zusammengesetzte Literale sind eine neue Syntax, die in C99 eingeführt wurde, sicher.

    – Yu Hao

    29. Mai 2015 um 16:23 Uhr

Benutzer-Avatar
Gopi

Von C99 haben wir

Ein Zeichenfolgenliteral ist eine Folge von null oder mehr Multibyte-Zeichen, die in doppelte Anführungszeichen eingeschlossen sind

In der zweiten Definition gibt es also kein Zeichenfolgenliteral, da es nicht in doppelten Anführungszeichen steht. Dem Zeiger sollte Speicher zugewiesen werden, bevor etwas darauf geschrieben wird oder wenn Sie dann nach der Initialisierungsliste gehen möchten

char p[] = {'a','b','\0'};

ist was du willst. Grundsätzlich handelt es sich bei beiden um unterschiedliche Deklarationen.

1299030cookie-checkString-Literale vs. Array von char beim Initialisieren eines Zeigers

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy