Was ist der Grund für nullterminierte Zeichenfolgen?

281

So sehr ich C und C ++ liebe, ich kann nicht anders, als mir bei der Auswahl der nullterminierten Zeichenfolgen den Kopf zu kratzen:

  • Vor C existierende Zeichenfolgen mit Längenpräfix (dh Pascal)
  • Zeichenfolgen mit Längenpräfix beschleunigen mehrere Algorithmen, indem sie eine Suche mit konstanter Zeitlänge ermöglichen.
  • Zeichenfolgen mit Längenpräfix machen es schwieriger, Pufferüberlauffehler zu verursachen.
  • Selbst auf einem 32-Bit-Computer ist eine Zeichenfolge mit Präfixlänge nur drei Byte breiter als eine Zeichenfolge mit Nullterminierung, wenn Sie zulassen, dass die Zeichenfolge die Größe des verfügbaren Speichers hat. Auf 16-Bit-Computern ist dies ein einzelnes Byte. Auf 64-Bit-Computern sind 4 GB eine angemessene Beschränkung der Zeichenfolgenlänge. Selbst wenn Sie es auf die Größe des Maschinenworts erweitern möchten, verfügen 64-Bit-Computer normalerweise über ausreichend Speicher, sodass die zusätzlichen sieben Bytes eine Art Nullargument darstellen. Ich weiß, dass der ursprüngliche C-Standard für wahnsinnig schlechte Maschinen (in Bezug auf den Speicher) geschrieben wurde, aber das Argument der Effizienz verkauft mich hier nicht.
  • Nahezu jede andere Sprache (z. B. Perl, Pascal, Python, Java, C # usw.) verwendet Zeichenfolgen mit Längenpräfix. Diese Sprachen schlagen normalerweise C in Benchmarks zur Manipulation von Zeichenfolgen, da sie mit Zeichenfolgen effizienter sind.
  • C ++ hat dies mit dem etwas korrigiert std::basic_string Vorlage , aber einfache Zeichenarrays, die nullterminierte Zeichenfolgen erwarten, sind immer noch weit verbreitet. Dies ist auch nicht perfekt, da eine Heap-Zuweisung erforderlich ist.
  • Null-terminierte Zeichenfolgen müssen ein Zeichen (nämlich null) reservieren, das in der Zeichenfolge nicht vorhanden sein kann, während Zeichenfolgen mit Längenpräfix eingebettete Nullen enthalten können.

Einige dieser Dinge sind in jüngerer Zeit ans Licht gekommen als C, daher wäre es sinnvoll, wenn C nichts von ihnen gewusst hätte. Einige waren jedoch deutlich, lange bevor C entstand. Warum wurden nullterminierte Zeichenfolgen anstelle des offensichtlich überlegenen Längenpräfixes gewählt?

BEARBEITEN : Da einige nach Fakten gefragt haben (und die, die ich bereits zur Verfügung gestellt habe) zu meinem Effizienzpunkt oben nicht mochten, sind sie auf einige Dinge zurückzuführen:

  • Concat mit nullterminierten Zeichenfolgen erfordert eine zeitliche Komplexität von O (n + m). Längenpräfixe erfordern oft nur O (m).
  • Die Länge mit nullterminierten Zeichenfolgen erfordert eine Komplexität von O (n). Das Längenpräfix ist O (1).
  • Länge und Concat sind bei weitem die häufigsten Zeichenfolgenoperationen. Es gibt mehrere Fälle, in denen nullterminierte Zeichenfolgen effizienter sein können, diese treten jedoch viel seltener auf.

In den folgenden Antworten sind einige Fälle aufgeführt, in denen nullterminierte Zeichenfolgen effizienter sind:

  • Wenn Sie den Anfang eines Strings abschneiden und an eine Methode übergeben müssen. Sie können dies nicht wirklich in konstanter Zeit mit Längenpräfix tun, selbst wenn Sie die ursprüngliche Zeichenfolge zerstören dürfen, da das Längenpräfix wahrscheinlich den Ausrichtungsregeln entsprechen muss.
  • In einigen Fällen, in denen Sie die Zeichenfolge nur zeichenweise durchlaufen, können Sie möglicherweise ein CPU-Register speichern. Beachten Sie, dass dies nur funktioniert, wenn Sie die Zeichenfolge nicht dynamisch zugewiesen haben (da Sie sie dann freigeben müssten und das gespeicherte CPU-Register verwenden müssten, um den Zeiger zu speichern, den Sie ursprünglich von malloc und Freunden erhalten haben).

Keines der oben genannten ist fast so häufig wie Länge und Concat.

In den folgenden Antworten wird noch eines behauptet:

  • Sie müssen das Ende der Zeichenfolge abschneiden

Dies ist jedoch falsch - es ist dieselbe Zeitspanne für nullterminierte und längenpräfixierte Zeichenfolgen. (Nullterminierte Zeichenfolgen setzen einfach eine Null an die Stelle, an der das neue Ende sein soll. Längenpräfixe werden nur vom Präfix subtrahiert.)

Billy ONeal
quelle
110
Ich dachte immer, es sei ein Übergangsritus für alle C ++ - Programmierer, ihre eigene String-Bibliothek zu schreiben.
Julia
31
Was bedeutet es, jetzt rationale Erklärungen zu erwarten? Ich nehme an, Sie möchten als nächstes eine Begründung für x86 oder DOS hören? Für mich gewinnt die schlechteste Technologie. Jedes Mal. Und die schlechteste Zeichenfolgendarstellung.
Jalf
4
Warum behaupten Sie, dass Zeichenfolgen mit Längenpräfix überlegen sind? Immerhin wurde C populär, weil es nullterminierte Zeichenfolgen verwendete, die es von den anderen Sprachen unterschieden.
Daniel C. Sobral
44
@Daniel: C wurde populär, weil es eine einfache, effiziente und portable Darstellung von Programmen ist, die auf Von Neumann-Computern ausgeführt werden können, und weil es für Unix verwendet wurde. Es liegt sicherlich nicht daran, dass entschieden wurde, nullterminierte Zeichenfolgen zu verwenden. Wenn es eine gute Designentscheidung gewesen wäre, hätten die Leute sie kopiert, und sie haben es nicht getan. Sie haben sicherlich so ziemlich alles andere von C. kopiert
Billy ONeal
4
Concat ist nur O (m) mit Längenpräfix, wenn Sie eine der Zeichenfolgen zerstören. Ansonsten gleiche Geschwindigkeit. Die am häufigsten verwendeten C-Zeichenfolgen (historisch) waren Drucken und Scannen. In beiden Fällen ist die Nullterminierung schneller, da ein Register gespeichert wird.
Daniel C. Sobral

Antworten:

195

Aus dem Maul des Pferdes

Keine von BCPL, B oder C unterstützt Zeichendaten stark in der Sprache; Jedes behandelt Zeichenfolgen ähnlich wie Vektoren von ganzen Zahlen und ergänzt allgemeine Regeln durch einige Konventionen. Sowohl in BCPL als auch in B bezeichnet ein Zeichenfolgenliteral die Adresse eines statischen Bereichs, der mit den Zeichen der Zeichenfolge initialisiert und in Zellen gepackt ist. In BCPL enthält das erste gepackte Byte die Anzahl der Zeichen in der Zeichenfolge. In B gibt es keine Zählung und Zeichenfolgen werden durch ein Sonderzeichen abgeschlossen, das B buchstabiert *e . Diese Änderung wurde teilweise vorgenommen, um die Begrenzung der Länge eines Strings zu vermeiden, die durch das Halten der Zählung in einem 8- oder 9-Bit-Slot verursacht wird, und teilweise, weil das Aufrechterhalten der Zählung unserer Erfahrung nach weniger bequem erschien als die Verwendung eines Terminators.

Dennis M Ritchie, Entwicklung der C-Sprache

Hans Passant
quelle
12
Ein weiteres relevantes Zitat: "... die Semantik von Strings wird vollständig durch allgemeinere Regeln für alle Arrays subsumiert,
weshalb
151

C hat keine Zeichenfolge als Teil der Sprache. Ein 'String' in C ist nur ein Zeiger auf char. Vielleicht stellen Sie die falsche Frage.

"Was ist der Grund für das Weglassen eines Zeichenfolgentyps?" Könnte relevanter sein. Dazu möchte ich darauf hinweisen, dass C keine objektorientierte Sprache ist und nur grundlegende Werttypen hat. Ein String ist ein übergeordnetes Konzept, das implementiert werden muss, indem Werte anderer Typen auf irgendeine Weise kombiniert werden. C befindet sich auf einer niedrigeren Abstraktionsebene.

im Lichte des tobenden Gewitters unten:

Ich möchte nur darauf hinweisen, dass ich nicht versuche zu sagen, dass dies eine dumme oder schlechte Frage ist oder dass die C-Darstellung von Strings die beste Wahl ist. Ich versuche zu verdeutlichen, dass die Frage prägnanter gestellt wird, wenn Sie die Tatsache berücksichtigen, dass C keinen Mechanismus zur Unterscheidung einer Zeichenfolge als Datentyp von einem Byte-Array hat. Ist dies angesichts der Verarbeitungs- und Speicherleistung heutiger Computer die beste Wahl? Wahrscheinlich nicht. Aber im Nachhinein ist immer 20/20 und das alles :)

Robert S. Ciaccio
quelle
29
char *temp = "foo bar";ist eine gültige Aussage in C ... hey! Ist das nicht eine Schnur? ist es nicht null beendet?
Yanick Rochon
56
@Yanick: Dies ist nur eine bequeme Möglichkeit, den Compiler anzuweisen, ein Array von Zeichen mit einer Null am Ende zu erstellen. Es ist keine "Saite"
Robert S Ciaccio
28
@calavera: Aber es hätte genauso gut bedeuten können "Erstellen Sie einen Speicherpuffer mit diesem String-Inhalt und einem Präfix von zwei Byte Länge",
Billy ONeal
14
@Billy: Nun, da ein 'String' wirklich nur ein Zeiger auf char ist, was einem Zeiger auf Byte entspricht, woher wissen Sie, dass der Puffer, mit dem Sie es zu tun haben, wirklich ein 'String' sein soll? Sie würden einen anderen neuen Typ als char / byte * benötigen, um dies zu kennzeichnen. vielleicht eine Struktur?
Robert S Ciaccio
27
Ich denke, @calavera ist richtig, C hat keinen Datentyp für Zeichenfolgen. Ok, Sie können ein Array von Zeichen wie eine Zeichenfolge betrachten, aber dies bedeutet nicht, dass es immer eine Zeichenfolge ist (für Zeichenfolge meine ich eine Folge von Zeichen mit einer bestimmten Bedeutung). Eine Binärdatei ist ein Array von Zeichen, aber diese Zeichen bedeuten für einen Menschen nichts.
BlackBear
106

Die Frage wird als Length Prefixed Strings (LPS)vs- zero terminated strings (SZ)Sache gestellt, zeigt aber meistens die Vorteile von Zeichenfolgen mit Längenpräfix auf. Das mag überwältigend erscheinen, aber um ehrlich zu sein, sollten wir auch die Nachteile von LPS und die Vorteile von SZ berücksichtigen.

Nach meinem Verständnis kann die Frage sogar als voreingenommene Frage verstanden werden: "Was sind die Vorteile von Zero Terminated Strings?".

Vorteile (ich sehe) von Zero Terminated Strings:

  • Sehr einfach, keine Notwendigkeit, neue Konzepte in die Sprache einzuführen, können Char-Arrays / Char-Zeiger.
  • Die Kernsprache enthält nur minimalen syntaktischen Zucker, um etwas zwischen doppelten Anführungszeichen in eine Reihe von Zeichen (wirklich eine Reihe von Bytes) umzuwandeln. In einigen Fällen kann es verwendet werden, um Dinge zu initialisieren, die nichts mit Text zu tun haben. Beispielsweise ist das xpm-Bilddateiformat eine gültige C-Quelle, die als Zeichenfolge codierte Bilddaten enthält.
  • Übrigens können Sie eine Null in ein String-Literal einfügen. Der Compiler fügt am Ende des Literals einfach eine weitere hinzu : "this\0is\0valid\0C". Ist es eine Schnur? oder vier Saiten? Oder ein paar Bytes ...
  • flache Implementierung, keine versteckte Indirektion, keine versteckte Ganzzahl.
  • Keine verborgene Speicherzuweisung erforderlich (einige berüchtigte nicht standardmäßige Funktionen wie strdup führen die Zuweisung durch, aber das ist meistens eine Problemquelle).
  • Kein spezielles Problem für kleine oder große Hardware (stellen Sie sich die Belastung durch die Verwaltung der 32-Bit-Präfixlänge auf 8-Bit-Mikrocontrollern oder die Einschränkungen der Begrenzung der Zeichenfolgengröße auf weniger als 256 Byte vor, das war ein Problem, das ich vor Äonen tatsächlich mit Turbo Pascal hatte).
  • Die Implementierung der String-Manipulation ist nur eine Handvoll sehr einfacher Bibliotheksfunktionen
  • Effizient für die Hauptverwendung von Zeichenfolgen: Konstanter Text, der von einem bekannten Start an nacheinander gelesen wird (meistens Nachrichten an den Benutzer).
  • Die abschließende Null ist nicht einmal obligatorisch. Es sind alle erforderlichen Tools verfügbar, um Zeichen wie eine Reihe von Bytes zu bearbeiten. Wenn Sie eine Array-Initialisierung in C durchführen, können Sie sogar den NUL-Terminator vermeiden. Stellen Sie einfach die richtige Größe ein. char a[3] = "foo";ist gültiges C (nicht C ++) und setzt keine endgültige Null in a.
  • kohärent mit dem Unix-Standpunkt "Alles ist Datei", einschließlich "Dateien", die keine intrinsische Länge haben, wie stdin, stdout. Sie sollten sich daran erinnern, dass offene Lese- und Schreibprimitive auf einer sehr niedrigen Ebene implementiert sind. Es handelt sich nicht um Bibliotheksaufrufe, sondern um Systemaufrufe. Dieselbe API wird für Binär- oder Textdateien verwendet. Grundelemente zum Lesen von Dateien erhalten eine Pufferadresse und eine Größe und geben die neue Größe zurück. Und Sie können Zeichenfolgen als Puffer zum Schreiben verwenden. Die Verwendung einer anderen Art der Zeichenfolgendarstellung würde bedeuten, dass Sie eine Literalzeichenfolge nicht einfach als Puffer für die Ausgabe verwenden können, oder Sie müssten dafür sorgen, dass sie sich beim Umwandeln in ein sehr seltsames Verhalten verhält char*. Das heißt, nicht die Adresse der Zeichenfolge zurückzugeben, sondern die tatsächlichen Daten zurückzugeben.
  • Sehr einfach zu manipulierende Textdaten, die aus einer Datei direkt gelesen wurden, ohne nutzlose Kopie des Puffers. Fügen Sie einfach Nullen an den richtigen Stellen ein (naja, nicht wirklich mit modernem C, da Zeichenfolgen in doppelten Anführungszeichen konstante Zeichenarrays sind, die heutzutage normalerweise in nicht modifizierbaren Daten gespeichert werden Segment).
  • Das Voranstellen einiger int-Werte jeder Größe würde Ausrichtungsprobleme mit sich bringen. Die anfängliche Länge sollte ausgerichtet sein, aber es gibt keinen Grund, dies für die Zeichendaten zu tun (und das Erzwingen der Ausrichtung von Zeichenfolgen würde wiederum Probleme mit sich bringen, wenn sie als ein Bündel von Bytes behandelt werden).
  • Die Länge ist zur Kompilierungszeit für konstante Literalzeichenfolgen (sizeof) bekannt. Warum sollte jemand es im Speicher speichern wollen, bevor es den tatsächlichen Daten vorangestellt wird?
  • In gewisser Weise wie C (fast) alle anderen werden Strings als Arrays von Zeichen angesehen. Da die Array-Länge nicht von C verwaltet wird, wird die logische Länge auch für Zeichenfolgen nicht verwaltet. Das einzig Überraschende ist, dass am Ende 0 Elemente hinzugefügt wurden, dies jedoch nur auf der Ebene der Kernsprache, wenn eine Zeichenfolge zwischen doppelten Anführungszeichen eingegeben wird. Benutzer können Funktionen zur Manipulation von Zeichenfolgen, die die Länge überschreiten, perfekt aufrufen oder stattdessen sogar einfache Memcopy verwenden. SZ sind nur eine Einrichtung. In den meisten anderen Sprachen wird die Arraylänge verwaltet. Es ist logisch, dass dies für Zeichenfolgen gleich ist.
  • In der heutigen Zeit reichen 1-Byte-Zeichensätze ohnehin nicht aus, und Sie müssen sich häufig mit codierten Unicode-Zeichenfolgen befassen, bei denen die Anzahl der Zeichen stark von der Anzahl der Bytes abweicht. Dies bedeutet, dass Benutzer wahrscheinlich mehr als "nur die Größe", sondern auch andere Informationen wünschen. Wenn Sie die Länge einhalten, verwenden Sie nichts (insbesondere keinen natürlichen Aufbewahrungsort) für diese anderen nützlichen Informationen.

In dem seltenen Fall, in dem Standard-C-Zeichenfolgen tatsächlich ineffizient sind, müssen Sie sich jedoch nicht beschweren. Bibliotheken sind verfügbar. Wenn ich diesem Trend folgte, sollte ich mich beschweren, dass Standard C keine Regex-Unterstützungsfunktionen enthält ... aber wirklich jeder weiß, dass dies kein wirkliches Problem ist, da für diesen Zweck Bibliotheken verfügbar sind. Wenn also die Effizienz der String-Manipulation gewünscht wird, warum nicht eine Bibliothek wie bstring verwenden ? Oder sogar C ++ - Strings?

EDIT : Ich habe mir kürzlich D-Saiten angesehen . Es ist interessant genug zu sehen, dass die gewählte Lösung weder ein Größenpräfix noch eine Nullterminierung ist. Wie in C sind in doppelte Anführungszeichen eingeschlossene Literalzeichenfolgen nur eine Abkürzung für unveränderliche Zeichenarrays, und die Sprache hat auch ein Zeichenfolgenschlüsselwort, das dies bedeutet (unveränderliches Zeichenarray).

D-Arrays sind jedoch viel umfangreicher als C-Arrays. Bei statischen Arrays ist die Länge zur Laufzeit bekannt, sodass die Länge nicht gespeichert werden muss. Der Compiler hat es zur Kompilierungszeit. Bei dynamischen Arrays ist die Länge verfügbar, in der D-Dokumentation ist jedoch nicht angegeben, wo sie aufbewahrt wird. Nach allem, was wir wissen, kann der Compiler wählen, ob er es in einem Register oder in einer Variablen aufbewahrt, die weit entfernt von den Zeichendaten gespeichert ist.

Bei normalen Zeichen-Arrays oder nicht-wörtlichen Zeichenfolgen gibt es keine endgültige Null, daher muss der Programmierer diese selbst setzen, wenn er eine C-Funktion von D aufrufen möchte. Im speziellen Fall von Literal-Zeichenfolgen setzt der D-Compiler jedoch immer noch eine Null an die Ende jeder Zeichenfolge (um eine einfache Umwandlung in C-Zeichenfolgen zu ermöglichen, um das Aufrufen der C-Funktion zu vereinfachen?), aber diese Null ist nicht Teil der Zeichenfolge (D zählt sie nicht in der Zeichenfolgengröße).

Das einzige, was mich etwas enttäuscht hat, ist, dass Strings utf-8 sein sollen, aber die Länge gibt anscheinend immer noch eine Anzahl von Bytes zurück (zumindest auf meinem Compiler gdc), selbst wenn Mehrbyte-Zeichen verwendet werden. Es ist mir unklar, ob es sich um einen Compiler-Fehler handelt oder nicht. (OK, ich habe wahrscheinlich herausgefunden, was passiert ist. Um dem D-Compiler zu sagen, dass Ihre Quelle utf-8 verwendet, müssen Sie am Anfang ein dummes Byte-Ordnungszeichen setzen. Ich schreibe dumm, weil ich weiß, dass kein Editor dies tut, insbesondere für UTF- 8, die ASCII-kompatibel sein soll).

kriss
quelle
7
... Fortsetzung ... Einige Ihrer Punkte sind meiner Meinung nach einfach falsch, dh das Argument "Alles ist eine Datei". Dateien haben sequentiellen Zugriff, C-Strings nicht. Das Längenpräfix kann auch mit minimalem syntaktischem Zucker erfolgen. Das einzig vernünftige Argument ist der Versuch, 32-Bit-Präfixe auf kleiner (dh 8-Bit-) Hardware zu verwalten. Ich denke, das könnte einfach gelöst werden, indem man sagt, dass die Größe der Länge von der Implementierung bestimmt wird. Das ist std::basic_stringes doch.
Billy ONeal
3
@ Billy ONeal: In meiner Antwort gibt es wirklich zwei verschiedene Teile. Zum einen geht es darum, was Teil der 'Kern-C-Sprache' ist, zum anderen geht es darum, was Standardbibliotheken liefern sollen. In Bezug auf die Zeichenfolgenunterstützung gibt es nur ein Element aus der Kernsprache: die Bedeutung eines doppelten Anführungszeichens von Bytes. Ich bin nicht wirklich glücklicher als Sie mit C-Verhalten. Ich fühle mich magisch, wenn ich hinzufüge, dass Null am Ende jedes Doppelschlusses eingeschlossene Bytes schlimm genug ist. Ich würde es vorziehen und \0am Ende explizit angeben, wenn Programmierer dies anstelle des impliziten wünschen. Das Voranstellen der Länge ist viel schlimmer.
kriss
2
@ Billy ONeal: Das ist einfach nicht wahr, die Verwendung kümmert sich darum, was Kern und was Bibliotheken sind. Der größte Punkt ist, wenn C zum Implementieren des Betriebssystems verwendet wird. Auf dieser Ebene sind keine Bibliotheken verfügbar. C wird auch häufig in eingebetteten Kontexten oder zum Programmieren von Geräten verwendet, bei denen Sie häufig die gleichen Einschränkungen haben. In vielen Fällen sollte Joes's heutzutage wahrscheinlich überhaupt kein C verwenden: "OK, du willst es auf der Konsole? Hast du eine Konsole? Nein? Schade ..."
kriss
5
@ Billy "Nun, für die 0,01% der C-Programmierer, die Betriebssysteme implementieren, ist das in Ordnung." Die anderen Programmierer können eine Wanderung machen. C wurde erstellt, um ein Betriebssystem zu schreiben.
Daniel C. Sobral
5
Warum? Weil es heißt, es ist eine Allzwecksprache? Sagt es, was die Leute, die es geschrieben haben, getan haben, als es geschaffen wurde? Wofür wurde es in den ersten Jahren seines Lebens verwendet? Also, was sagt es, was mit mir nicht übereinstimmt? Es ist eine Allzwecksprache, die zum Schreiben eines Betriebssystems erstellt wurde . Leugnet es das?
Daniel C. Sobral
61

Ich denke, es hat historische Gründe und fand dies in Wikipedia :

Zum Zeitpunkt der Entwicklung von C (und der Sprachen, aus denen es abgeleitet wurde) war der Speicher äußerst begrenzt, sodass es attraktiv war, nur ein Byte Overhead zum Speichern der Länge eines Strings zu verwenden. Die einzige beliebte Alternative zu dieser Zeit, die normalerweise als "Pascal-Zeichenfolge" bezeichnet wird (obwohl sie auch von früheren Versionen von BASIC verwendet wird), verwendete ein führendes Byte, um die Länge der Zeichenfolge zu speichern. Dadurch kann die Zeichenfolge NUL enthalten, und das Ermitteln der Länge erfordert nur einen Speicherzugriff (O (1) (konstante) Zeit). Ein Byte begrenzt die Länge jedoch auf 255. Diese Längenbeschränkung war weitaus restriktiver als die Probleme mit der C-Zeichenfolge, sodass die C-Zeichenfolge im Allgemeinen siegte.

Khachik
quelle
2
@muntoo Hmm ... Kompatibilität?
Khachik
19
@muntoo: Weil das monumentale Mengen an vorhandenem C- und C ++ - Code zerstören würde.
Billy ONeal
10
@muntoo: Paradigmen kommen und gehen, aber Legacy-Code ist für immer. Jede zukünftige Version von C müsste weiterhin Zeichenfolgen mit 0-Terminierung unterstützen, andernfalls müsste Legacy-Code für mehr als 30 Jahre neu geschrieben werden (was nicht passieren wird). Und solange der alte Weg verfügbar ist, werden die Leute ihn weiterhin benutzen, da sie damit vertraut sind.
John Bode
8
@muntoo: Glaub mir, manchmal wünschte ich, ich könnte. Aber ich würde immer noch 0-terminierte Strings Pascal-Strings vorziehen.
John Bode
2
Sprechen Sie über Legacy ... C ++ - Zeichenfolgen müssen jetzt NUL-terminiert werden.
Jim Balter
32

Calavera hat recht , aber da die Leute seinen Standpunkt nicht zu verstehen scheinen, werde ich einige Codebeispiele bereitstellen.

Betrachten wir zunächst, was C ist: eine einfache Sprache, in der der gesamte Code ziemlich direkt in die Maschinensprache übersetzt wird. Alle Typen passen in die Register und auf dem Stapel, und es nicht ein Betriebssystem oder eine große Laufzeitbibliothek zu laufen benötigen, da sie gedacht waren , schreiben diese Dinge (eine Aufgabe, der sich hervorragend gut geeignet ist, wenn man bedenkt es ist bis heute nicht einmal ein wahrscheinlicher Konkurrent).

Wenn C einen stringTyp hätte, wie intoderchar , wäre dies ein Typ, der nicht in ein Register oder in den Stapel passt und eine Speicherzuweisung (mit der gesamten unterstützenden Infrastruktur) in irgendeiner Weise erfordern würde. All dies widerspricht den Grundsätzen von C.

Eine Zeichenfolge in C lautet also:

char s*;

Nehmen wir also an, dass dies ein Längenpräfix war. Schreiben wir den Code, um zwei Zeichenfolgen zu verketten:

char* concat(char* s1, char* s2)
{
    /* What? What is the type of the length of the string? */
    int l1 = *(int*) s1;
    /* How much? How much must I skip? */
    char *s1s = s1 + sizeof(int);
    int l2 = *(int*) s2;
    char *s2s = s2 + sizeof(int);
    int l3 = l1 + l2;
    char *s3 = (char*) malloc(l3 + sizeof(int));
    char *s3s = s3 + sizeof(int);
    memcpy(s3s, s1s, l1);
    memcpy(s3s + l1, s2s, l2);
    *(int*) s3 = l3;
    return s3;
}

Eine andere Alternative wäre die Verwendung einer Struktur zum Definieren einer Zeichenfolge:

struct {
  int len; /* cannot be left implementation-defined */
  char* buf;
}

Zu diesem Zeitpunkt müssten für jede Zeichenfolgenmanipulation zwei Zuweisungen vorgenommen werden. In der Praxis bedeutet dies, dass Sie eine Bibliothek durchsuchen müssen, um sie zu verarbeiten.

Das Komische ist , ... structs wie das tun in C existieren! Sie werden einfach nicht für die tägliche Anzeige von Nachrichten an den Benutzer verwendet.

Hier ist also der Punkt, den Calavera macht: In C gibt es keinen Zeichenfolgentyp . Um etwas damit zu tun, müssten Sie einen Zeiger nehmen und ihn als Zeiger auf zwei verschiedene Typen dekodieren. Dann wird es sehr relevant, wie groß eine Zeichenfolge ist, und kann nicht einfach als "Implementierung definiert" belassen werden.

Nun, C kann Speicher in irgendeiner Weise handhaben , und die memFunktionen in der Bibliothek (in <string.h>, auch!) Stellen Sie alle Werkzeuge die Sie als Paar Zeiger und Größe Greifen Speicher benötigen. Die sogenannten "Strings" in C wurden nur für einen Zweck erstellt: Anzeigen von Nachrichten im Zusammenhang mit dem Schreiben eines Betriebssystems für Textterminals. Und dafür reicht eine Nullbeendigung aus.

Daniel C. Sobral
quelle
2
1. +1. 2. Wenn das Standardverhalten der Sprache mithilfe von Längenpräfixen festgelegt worden wäre, hätte es natürlich andere Möglichkeiten gegeben, dies zu vereinfachen. Zum Beispiel wären alle Ihre Casts dort strlenstattdessen durch Anrufe an und Freunde versteckt worden . Was das Problem betrifft, "es der Implementierung zu überlassen", könnte man sagen, dass das Präfix das ist, was auch immer a shortauf der Zielbox ist. Dann würde dein ganzes Casting immer noch funktionieren. 3. Ich kann mir den ganzen Tag über erfundene Szenarien ausdenken, die das eine oder andere System schlecht aussehen lassen.
Billy ONeal
5
@ Billy Die Bibliothekssache ist wahr genug, abgesehen von der Tatsache, dass C für minimale oder keine Bibliotheksnutzung entwickelt wurde. Der Einsatz von Prototypen war beispielsweise schon früh nicht üblich. Wenn Sie sagen, dass das Präfix shorteffektiv ist, wird die Größe der Zeichenfolge begrenzt, was eine Sache zu sein scheint, an der sie nicht interessiert waren. Nachdem ich mit 8-Bit-BASIC- und Pascal-Strings, COBOL-Strings mit fester Größe und ähnlichen Dingen gearbeitet hatte, wurde ich schnell ein großer Fan von C-Strings mit unbegrenzter Größe. Heutzutage kann eine Größe von 32 Bit jede praktische Zeichenfolge verarbeiten, aber das frühzeitige Hinzufügen dieser Bytes war problematisch.
Daniel C. Sobral
1
@ Billy: Zuerst danke Daniel ... du scheinst zu verstehen, worauf ich hinaus will. Zweitens, Billy, ich denke, Sie vermissen immer noch den Punkt, der hier gemacht wird. Ich jedenfalls diskutiere nicht die Vor- und Nachteile, wenn man String -Datentypen mit ihrer Länge voranstellt . Was ich sage und was Daniel sehr deutlich betont hat, ist, dass bei der Implementierung von C eine Entscheidung getroffen wurde, dieses Argument überhaupt nicht zu behandeln . Strings existieren in Bezug auf die Basissprache nicht. Die Entscheidung über den Umgang mit Strings bleibt dem Programmierer überlassen ... und die Nullterminierung wurde populär.
Robert S Ciaccio
1
+1 von mir. Eine weitere Sache möchte ich hinzufügen; Eine Struktur, wie Sie sie vorschlagen, übersieht einen wichtigen Schritt in Richtung eines realen stringTyps: Sie kennt keine Zeichen. Es ist eine Reihe von "Zeichen" (ein "Zeichen" in der Maschinensprache ist ebenso ein Zeichen wie ein "Wort", wie Menschen ein Wort in einem Satz nennen würden). Eine Zeichenfolge ist ein übergeordnetes Konzept, das über einem Array von implementiert charwerden kann, wenn Sie den Begriff der Codierung eingeführt haben.
Frerich Raabe
2
@ DanielC.Sobral: Außerdem würde die von Ihnen erwähnte Struktur keine zwei Zuordnungen erfordern. Verwenden Sie es entweder so, wie Sie es auf dem Stapel haben (es ist also nur bufeine Zuordnung erforderlich), oder verwenden struct string {int len; char buf[]};und ordnen Sie das Ganze mit einer Zuordnung als flexibles Array-Mitglied zu und geben Sie es als string*. (Oder wohl struct string {int capacity; int len; char buf[]};aus offensichtlichen Leistungsgründen)
Mooing Duck
20

Aus Gründen der Leistung und Sicherheit sollten Sie die Länge eines Strings beibehalten, während Sie damit arbeiten, anstatt wiederholt strlenoder gleichwertig daran zu arbeiten. Das Speichern der Länge an einem festen Ort kurz vor dem Inhalt der Zeichenfolge ist jedoch ein unglaublich schlechtes Design. Wie Jörgen in den Kommentaren zu Sanjits Antwort hervorhob, schließt es aus, das Ende eines Strings als String zu behandeln, was zum Beispiel viele gängige Operationen wie path_to_filenameoder filename_to_extensionunmöglich macht, ohne neuen Speicher zuzuweisen (und die Möglichkeit von Fehlern und Fehlerbehandlung zu verursachen). . Und dann gibt es natürlich das Problem, dass niemand zustimmen kann, wie viele Bytes das Feld für die Zeichenfolgenlänge belegen soll (viele schlechte "Pascal-Zeichenfolgen").

Cs Design, dem Programmierer die Wahl zu lassen, ob / wo / wie die Länge gespeichert werden soll, ist viel flexibler und leistungsfähiger. Aber natürlich muss der Programmierer klug sein. C bestraft Dummheit mit Programmen, die abstürzen, zum Stillstand kommen oder Ihren Feinden Wurzeln schlagen.

R .. GitHub HÖREN SIE AUF, EIS ZU HELFEN
quelle
+1. Es wäre jedoch schön, einen Standardplatz zum Speichern der Länge zu haben, damit diejenigen von uns, die so etwas wie ein Längenpräfix möchten, nicht überall Tonnen von "Klebercode" schreiben müssen.
Billy ONeal
2
Es gibt keine mögliche Standardposition in Bezug auf die Zeichenfolgendaten, aber Sie können natürlich eine separate lokale Variable (die neu berechnet wird, anstatt sie zu übergeben, wenn die letztere nicht bequem und die erstere nicht zu verschwenderisch ist) oder eine Struktur mit einem Zeiger verwenden auf die Zeichenfolge (und noch besser auf ein Flag, das angibt, ob die Struktur den Zeiger für Zuweisungszwecke "besitzt" oder ob es sich um eine Referenz auf eine Zeichenfolge handelt, die an anderer Stelle gehört. Und natürlich können Sie ein flexibles Array-Mitglied in die Struktur aufnehmen, damit die Flexibilität zugewiesen werden kann die Zeichenfolge mit der Struktur, wenn es Ihnen passt.
R .. GitHub STOP HELPING ICE
13

Faulheit, Genügsamkeit und Portabilität des Registers unter Berücksichtigung des Assembler-Darms jeder Sprache, insbesondere C, das einen Schritt über dem Assembler liegt (wodurch viel Assembler-Legacy-Code geerbt wird). Sie würden zustimmen, dass ein Nullzeichen in diesen ASCII-Tagen nutzlos wäre (und wahrscheinlich so gut wie ein EOF-Kontrollzeichen).

Mal im Pseudocode sehen

function readString(string) // 1 parameter: 1 register or 1 stact entries
    pointer=addressOf(string) 
    while(string[pointer]!=CONTROL_CHAR) do
        read(string[pointer])
        increment pointer

Insgesamt 1 Register verwenden

Fall 2

 function readString(length,string) // 2 parameters: 2 register used or 2 stack entries
     pointer=addressOf(string) 
     while(length>0) do 
         read(string[pointer])
         increment pointer
         decrement length

Insgesamt 2 Register verwendet

Das mag zu dieser Zeit kurzsichtig erscheinen, aber angesichts der Genügsamkeit in Code und Register (die zu dieser Zeit PREMIUM waren, zu der Zeit, als Sie wissen, verwenden sie Lochkarten). Da dieser "Hack" schneller war (wenn die Prozessorgeschwindigkeit in kHz gezählt werden konnte), war er verdammt gut und mit Leichtigkeit für registrierungslose Prozessoren tragbar.

Aus Gründen der Argumentation werde ich zwei allgemeine Zeichenfolgenoperationen implementieren

stringLength(string)
     pointer=addressOf(string)
     while(string[pointer]!=CONTROL_CHAR) do
         increment pointer
     return pointer-addressOf(string)

Komplexität O (n), wobei in den meisten Fällen die PASCAL-Zeichenfolge O (1) ist, da die Länge der Zeichenfolge der Zeichenfolgenstruktur vorangestellt ist (dies würde auch bedeuten, dass diese Operation in einem früheren Stadium ausgeführt werden müsste).

concatString(string1,string2)
     length1=stringLength(string1)
     length2=stringLength(string2)
     string3=allocate(string1+string2)
     pointer1=addressOf(string1)
     pointer3=addressOf(string3)
     while(string1[pointer1]!=CONTROL_CHAR) do
         string3[pointer3]=string1[pointer1]
         increment pointer3
         increment pointer1
     pointer2=addressOf(string2)
     while(string2[pointer2]!=CONTROL_CHAR) do
         string3[pointer3]=string2[pointer2]
         increment pointer3
         increment pointer1
     return string3

Die Komplexität O (n) und das Voranstellen der Zeichenfolgenlänge würden die Komplexität der Operation nicht ändern, während ich zugeben würde, dass dies dreimal weniger Zeit in Anspruch nehmen würde.

Wenn Sie andererseits eine PASCAL-Zeichenfolge verwenden, müssten Sie Ihre API neu gestalten, um die Registerlänge und die Bitendianität zu berücksichtigen. Die PASCAL-Zeichenfolge hat die bekannte Beschränkung von 255 Zeichen (0xFF), da die Länge in 1 Byte (8 Bit) gespeichert wurde ), und wenn Sie eine längere Zeichenfolge (16 Bit -> alles) möchten, müssen Sie die Architektur in einer Ebene Ihres Codes berücksichtigen. Dies würde in den meisten Fällen inkompatible Zeichenfolgen-APIs bedeuten, wenn Sie eine längere Zeichenfolge wünschen.

Beispiel:

Eine Datei wurde mit Ihrer vorangestellten Zeichenfolgen-API auf einem 8-Bit-Computer geschrieben und musste dann beispielsweise auf einem 32-Bit-Computer gelesen werden. Was würde das Lazy-Programm tun, wenn Ihre 4 Bytes die Länge der Zeichenfolge sind und dann so viel Speicher zuweisen Versuchen Sie dann, so viele Bytes zu lesen. Ein anderer Fall wäre das Lesen eines PPC-32-Byte-Strings (Little Endian) auf einem x86 (Big Endian). Wenn Sie nicht wissen, dass einer vom anderen geschrieben wird, gibt es natürlich Probleme. 1 Byte Länge (0x00000001) würde 16777216 (0x0100000) werden, was 16 MB zum Lesen einer 1-Byte-Zeichenfolge entspricht. Natürlich würde man sagen, dass sich die Leute auf einen Standard einigen sollten, aber selbst 16-Bit-Unicode hat wenig und große Endianness.

Natürlich hätte C auch seine Probleme, wäre aber von den hier angesprochenen Problemen sehr wenig betroffen.

dvhh
quelle
2
@deemoowoor: Concat: O(m+n)mit nullterm Strings, O(n)typisch überall sonst. Länge O(n)mit Nullterm-Strings, O(1)überall sonst. Join: O(n^2)mit nullterm Strings, O(n)überall sonst. Es gibt einige Fälle, in denen nullterminierte Zeichenfolgen effizienter sind (dh nur eine zum Zeiger hinzufügen), aber concat und length sind bei weitem die häufigsten Operationen (Länge ist mindestens für Formatierung, Dateiausgabe, Konsolenanzeige usw. erforderlich). . Wenn Sie die Länge zwischenspeichern, um sie zu amortisieren, haben O(n)Sie lediglich darauf hingewiesen, dass die Länge mit der Zeichenfolge gespeichert werden soll.
Billy ONeal
1
Ich bin damit einverstanden, dass diese Art von Zeichenfolge im heutigen Code ineffizient und fehleranfällig ist, aber zum Beispiel muss die Konsolenanzeige die Länge der Zeichenfolge nicht wirklich kennen, um sie effizient anzuzeigen. Die Dateiausgabe musste nicht wirklich über die Zeichenfolge Bescheid wissen Länge (nur Cluster wird unterwegs zugewiesen), und die Zeichenfolgenformatierung wurde zu diesem Zeitpunkt in den meisten Fällen mit einer festen Zeichenfolgenlänge durchgeführt. Wie auch immer, Sie müssen schlechten Code schreiben, wenn Sie in C eine O (n ^ 2) -Komplexität haben. Ich bin mir ziemlich sicher, dass ich einen in O (n)
-Komplexität
1
@dvhh: Ich habe nicht n ^ 2 gesagt - ich habe m + n gesagt - es ist immer noch linear, aber Sie müssen bis zum Ende der ursprünglichen Zeichenfolge suchen, um die Verkettung durchzuführen, während bei einem Längenpräfix keine Suche erfolgt Wird benötigt. (Dies ist wirklich nur eine weitere Folge der Länge, die eine lineare Zeit erfordert)
Billy ONeal
1
@ Billy ONeal: Aus reiner Neugier habe ich mein aktuelles C-Projekt (ca. 50000 Codezeilen) für Funktionsaufrufe zur Manipulation von Zeichenfolgen überprüft. strlen 101, strcpy und Varianten (strncpy, strlcpy): 85 (ich habe auch mehrere hundert Literalzeichenfolgen für Nachrichten, implizite Kopien), strcmp: 56, strcat: 13 (und 6 sind Verkettungen zu Zeichenfolgen mit der Länge Null, um strncat aufzurufen) . Ich bin damit einverstanden, dass eine vorangestellte Länge die Aufrufe von strlen beschleunigt, nicht jedoch von strcpy oder strcmp (möglicherweise, wenn die strcmp-API kein allgemeines Präfix verwendet). Das Interessanteste an den obigen Kommentaren ist, dass strcat sehr selten ist.
kriss
1
@supercat: nicht wirklich, schauen Sie sich einige Implementierungen an. Kurze Zeichenfolgen verwenden einen Short-Stack-basierten Puffer (keine Heap-Zuordnung) und verwenden Heap nur, wenn sie größer werden. Sie können jedoch gerne eine tatsächliche Umsetzung Ihrer Idee als Bibliothek bereitstellen. Normalerweise treten die Probleme nur auf, wenn wir zu den Details kommen, nicht im Gesamtdesign.
kriss
9

In vielerlei Hinsicht war C primitiv. Und ich liebte es.

Es war ein Schritt über der Assemblersprache und bot Ihnen fast die gleiche Leistung mit einer Sprache, die viel einfacher zu schreiben und zu warten war.

Der Null-Terminator ist einfach und erfordert keine besondere Unterstützung durch die Sprache.

Rückblickend scheint es nicht so bequem zu sein. Aber ich habe in den 80ern Assemblersprache verwendet und es schien zu dieser Zeit sehr praktisch zu sein. Ich denke nur, dass sich die Software ständig weiterentwickelt und die Plattformen und Tools immer ausgefeilter werden.

Jonathan Wood
quelle
Ich sehe nicht, was an nullterminierten Zeichenfolgen primitiver ist als alles andere. Pascal ist älter als C und verwendet ein Längenpräfix. Sicher, es war auf 256 Zeichen pro Zeichenfolge beschränkt, aber die einfache Verwendung eines 16-Bit-Felds hätte das Problem in den allermeisten Fällen gelöst.
Billy ONeal
Die Tatsache, dass die Anzahl der Zeichen begrenzt ist, ist genau die Art von Problemen, über die Sie nachdenken müssen, wenn Sie so etwas tun. Ja, Sie könnten es länger machen, aber damals waren Bytes wichtig. Und wird ein 16-Bit-Feld für alle Fälle lang genug sein? Komm schon, du musst zugeben, dass eine Null-Terminierung konzeptionell primitiv ist.
Jonathan Wood
10
Entweder Sie begrenzen die Länge der Zeichenfolge oder Sie begrenzen den Inhalt (keine Nullzeichen) oder Sie akzeptieren den zusätzlichen Overhead einer Anzahl von 4 bis 8 Bytes. Es gibt kein kostenloses Mittagessen. Zum Zeitpunkt des Beginns war die nullterminierte Zeichenfolge absolut sinnvoll. In der Assembly habe ich manchmal das oberste Bit eines Zeichens verwendet, um das Ende einer Zeichenfolge zu markieren und sogar ein weiteres Byte zu sparen!
Mark Ransom
Genau, Mark: Es gibt kein kostenloses Mittagessen. Es ist immer ein Kompromiss. Heutzutage müssen wir nicht die gleichen Kompromisse eingehen. Aber damals schien dieser Ansatz so gut wie jeder andere.
Jonathan Wood
8

Nehmen wir für einen Moment an, dass C Zeichenfolgen nach Pascal-Art implementiert hat, indem Sie ihnen die Länge voranstellen: Ist eine Zeichenfolge mit 7 Zeichen derselbe DATENTYP wie eine Zeichenfolge mit 3 Zeichen? Wenn die Antwort ja lautet, welche Art von Code sollte der Compiler dann generieren, wenn ich den ersteren dem letzteren zuweise? Sollte die Zeichenfolge abgeschnitten oder automatisch in der Größe geändert werden? Sollte diese Operation bei einer Größenänderung durch ein Schloss geschützt werden, um die Thread-Sicherheit zu gewährleisten? Die C-Ansatz-Seite hat all diese Probleme gelöst, ob es Ihnen gefällt oder nicht :)

Cristian
quelle
2
Ähm ... nein, hat es nicht. Der C-Ansatz erlaubt es überhaupt nicht, die 7 Zeichen lange Zeichenfolge der 3 Zeichen langen Zeichenfolge zuzuweisen.
Billy ONeal
@ Billy ONeal: warum nicht? Soweit ich es in diesem Fall verstehe, haben alle Zeichenfolgen den gleichen Datentyp (char *), daher spielt die Länge keine Rolle. Im Gegensatz zu Pascal. Dies war jedoch eher eine Einschränkung von Pascal als ein Problem mit Zeichenfolgen mit Längenpräfix.
Oliver Mason
4
@ Billy: Ich denke, Sie haben gerade Cristians Punkt wiederholt. C befasst sich mit diesen Problemen, indem es sie überhaupt nicht behandelt. Sie denken immer noch an C, das tatsächlich den Begriff einer Zeichenfolge enthält. Es ist nur ein Zeiger, sodass Sie ihn beliebig zuweisen können.
Robert S Ciaccio
2
Es ist wie ** die Matrix: "Es gibt keine Zeichenfolge".
Robert S Ciaccio
1
@calavera: Ich sehe nicht, wie das etwas beweist. Sie können es auf die gleiche Weise mit Längenpräfixen lösen ... dh die Zuweisung überhaupt nicht zulassen.
Billy ONeal
8

Irgendwie habe ich die Frage so verstanden, dass es in C keine Compiler-Unterstützung für Zeichenfolgen mit Längenpräfix gibt. Das folgende Beispiel zeigt, dass Sie zumindest eine eigene C-Zeichenfolgenbibliothek starten können, in der die Zeichenfolgenlängen zur Kompilierungszeit mit einem Konstrukt wie diesem gezählt werden:

#define PREFIX_STR(s) ((prefix_str_t){ sizeof(s)-1, (s) })

typedef struct { int n; char * p; } prefix_str_t;

int main() {
    prefix_str_t string1, string2;

    string1 = PREFIX_STR("Hello!");
    string2 = PREFIX_STR("Allows \0 chars (even if printf directly doesn't)");

    printf("%d %s\n", string1.n, string1.p); /* prints: "6 Hello!" */
    printf("%d %s\n", string2.n, string2.p); /* prints: "48 Allows " */

    return 0;
}

Dies ist jedoch nicht ohne Probleme, da Sie vorsichtig sein müssen, wann Sie diesen Zeichenfolgenzeiger speziell freigeben und wann er statisch zugewiesen ist (Literal- charArray).

Bearbeiten: Als direktere Antwort auf die Frage ist meine Ansicht, dass C auf diese Weise beide unterstützen kann, wenn die Zeichenfolgenlänge verfügbar ist (als Kompilierungszeitkonstante), falls Sie sie benötigen, aber immer noch ohne Speicheraufwand, wenn Sie sie verwenden möchten nur Zeiger und Nullterminierung.

Natürlich scheint es die empfohlene Vorgehensweise zu sein, mit nullterminierten Zeichenfolgen zu arbeiten, da die Standardbibliothek im Allgemeinen keine Zeichenfolgenlängen als Argumente verwendet und das Extrahieren der Länge nicht so einfach char * s = "abc"ist wie in meinem Beispiel.

Pyry Jahkola
quelle
Das Problem ist, dass Bibliotheken die Existenz Ihrer Struktur nicht kennen und dennoch Dinge wie eingebettete Nullen falsch behandeln. Dies beantwortet auch nicht wirklich die Frage, die ich gestellt habe.
Billy ONeal
1
Das ist richtig. Das größere Problem ist also, dass es keinen besseren Standard gibt, um Schnittstellen mit Zeichenfolgenparametern bereitzustellen, als einfache alte Zeichenfolgen mit Nullterminierung. Ich würde immer noch behaupten, dass es Bibliotheken gibt, die das Einspeisen von Zeigerlängenpaaren unterstützen (zumindest können Sie damit einen C ++ std :: string erstellen).
Pyry Jahkola
2
Selbst wenn Sie eine Länge speichern, sollten Sie niemals Zeichenfolgen mit eingebetteten Nullen zulassen. Dies ist der gesunde Menschenverstand. Wenn Ihre Daten möglicherweise Nullen enthalten, sollten Sie sie niemals mit Funktionen verwenden, die Zeichenfolgen erwarten.
R .. GitHub STOP HELPING ICE
1
@supercat: Aus Sicherheitsgründen würde ich diese Redundanz begrüßen. Andernfalls verketten ignorante (oder schlafentzugene) Programmierer binäre Daten und Zeichenfolgen und geben sie an Dinge weiter, die [nullterminierte] Zeichenfolgen erwarten ...
R .. GitHub STOP HELPING ICE
1
@R ..: Während Methoden, die nullterminierte Zeichenfolgen erwarten char*, im Allgemeinen a erwarten , erwarten viele Methoden, die keine nullterminierte Zeichenfolge erwarten , auch a char*. Ein bedeutenderer Vorteil der Trennung der Typen würde sich auf das Unicode-Verhalten beziehen. Es kann für eine Zeichenfolgenimplementierung sinnvoll sein, Flags beizubehalten, um festzustellen, ob Zeichenfolgen bestimmte Arten von Zeichen enthalten oder nicht [z. B. den 999.990. Codepunkt in einer Zeichenfolge mit einer Million Zeichen zu finden, von der bekannt ist, dass sie keine enthält
Alle
6

"Selbst auf einem 32-Bit-Computer ist eine Zeichenfolge mit Präfixlänge nur drei Byte breiter als eine Zeichenfolge mit Nullterminierung, wenn Sie zulassen, dass die Zeichenfolge die Größe des verfügbaren Speichers hat."

Erstens können zusätzliche 3 Bytes für kurze Zeichenfolgen einen erheblichen Overhead bedeuten. Insbesondere eine Zeichenfolge mit der Länge Null benötigt jetzt viermal so viel Speicher. Einige von uns verwenden 64-Bit-Maschinen, daher benötigen wir entweder 8 Byte, um eine Zeichenfolge mit der Länge Null zu speichern, oder das Zeichenfolgenformat kann die längsten von der Plattform unterstützten Zeichenfolgen nicht verarbeiten.

Möglicherweise müssen auch Ausrichtungsprobleme behoben werden. Angenommen, ich habe einen Speicherblock mit 7 Zeichenfolgen, z. B. "solo \ 0second \ 0 \ 0four \ 0five \ 0 \ 0seventh". Die zweite Zeichenfolge beginnt bei Offset 5. Die Hardware erfordert möglicherweise, dass 32-Bit-Ganzzahlen an einer Adresse ausgerichtet sind, die ein Vielfaches von 4 ist. Sie müssen also eine Auffüllung hinzufügen, um den Overhead noch weiter zu erhöhen. Die C-Darstellung ist im Vergleich sehr speichereffizient. (Die Speichereffizienz ist gut; sie hilft beispielsweise bei der Cache-Leistung.)

Brangdon
quelle
Ich glaube, ich habe all dies in der Frage angesprochen. Ja, auf x64-Plattformen kann ein 32-Bit-Präfix nicht auf alle möglichen Zeichenfolgen passen. Auf der anderen Seite möchten Sie niemals eine Zeichenfolge, die so groß ist wie eine nullterminierte Zeichenfolge, denn um etwas zu tun, müssen Sie alle 4 Milliarden Bytes untersuchen, um das Ende für fast jede Operation zu finden, die Sie möglicherweise ausführen möchten. Ich sage auch nicht, dass nullterminierte Zeichenfolgen immer böse sind - wenn Sie eine dieser Blockstrukturen erstellen und Ihre spezifische Anwendung durch diese Art von Konstruktion beschleunigt wird, versuchen Sie es. Ich wünschte nur, das Standardverhalten der Sprache hätte das nicht getan.
Billy ONeal
2
Ich habe diesen Teil Ihrer Frage zitiert, weil er meiner Ansicht nach das Effizienzproblem unterschätzt hat. Das Verdoppeln oder Vervierfachen des Speicherbedarfs (bei 16-Bit- bzw. 32-Bit-Speicher) kann hohe Leistungskosten verursachen. Lange Saiten mögen langsam sein, aber zumindest werden sie unterstützt und funktionieren immer noch. Mein anderer Punkt, über die Ausrichtung, erwähnen Sie überhaupt nicht.
Brangdon
Die Ausrichtung kann behandelt werden, indem angegeben wird, dass sich Werte jenseits von UCHAR_MAX so verhalten sollen, als ob sie mithilfe von Bytezugriffen und Bitverschiebung gepackt und entpackt würden. Ein entsprechend gestalteter String-Typ könnte eine Speichereffizienz bieten, die im Wesentlichen mit nullterminierten Strings vergleichbar ist, und gleichzeitig die Überprüfung der Grenzen von Puffern ohne zusätzlichen Speicheraufwand ermöglichen (verwenden Sie ein Bit im Präfix, um festzustellen, ob ein Puffer "voll" ist; ist nicht und das letzte Byte ist nicht Null, dieses Byte würde den verbleibenden Speicherplatz darstellen. Wenn der Puffer nicht voll ist und das letzte Byte Null ist, dann würden die letzten 256 Bytes nicht verwendet, also ...
supercat
... man könnte in diesem Raum die genaue Anzahl nicht verwendeter Bytes ohne zusätzliche Speicherkosten speichern). Die Kosten für die Arbeit mit den Präfixen würden durch die Möglichkeit ausgeglichen, Methoden wie fgets () zu verwenden, ohne die Zeichenfolgenlänge übergeben zu müssen (da die Puffer wissen würden, wie groß sie sind).
Supercat
4

Die Nullterminierung ermöglicht schnelle zeigerbasierte Operationen.

Sanjit Saluja
quelle
5
Huh? Welche "schnellen Zeigeroperationen" funktionieren nicht mit Längenpräfixen? Noch wichtiger ist, dass andere Sprachen, die Längenpräfixe verwenden, schneller sind als die Manipulation von C wrt-Zeichenfolgen.
Billy ONeal
12
@billy: Bei Zeichenfolgen mit Längenpräfix können Sie nicht einfach einen Zeichenfolgenzeiger nehmen und 4 hinzufügen und erwarten, dass es sich weiterhin um eine gültige Zeichenfolge handelt, da sie kein Längenpräfix hat (ohnehin nicht gültig).
Jörgen Sigvardsson
3
@j_random_hacker: Die Verkettung ist für ASCIZ-Zeichenfolgen (O (m + n) anstelle von möglicherweise O (n)) viel schlechter, und Concat ist viel häufiger als alle anderen hier aufgeführten Operationen.
Billy ONeal
3
Es gibt eine kleine Operation, die mit nullterminierten Zeichenfolgen teurer wird : strlen. Ich würde sagen, das ist ein kleiner Nachteil.
Jalf
10
@ Billy ONeal: Alle anderen unterstützen auch Regex. Na und ? Verwenden Sie Bibliotheken, für die sie gemacht sind. Bei C geht es um maximale Effizienz und Minimalismus, nicht um Batterien. Mit C-Tools können Sie auch sehr einfach Zeichenfolgen mit Längenpräfix mithilfe von Strukturen implementieren. Und nichts verbietet Ihnen, die String-Manipulationsprogramme durch Verwalten Ihrer eigenen Längen- und Zeichenpuffer zu implementieren. Das ist normalerweise das, was ich mache, wenn ich Effizienz will und C verwende. Es ist kein Problem, nicht eine Handvoll Funktionen aufzurufen, die eine Null am Ende eines Zeichenpuffers erwarten.
kriss
4

Ein Punkt, der noch nicht erwähnt wurde: Als C entworfen wurde, gab es viele Maschinen, auf denen ein 'char' nicht acht Bit betrug (selbst heute gibt es DSP-Plattformen, auf denen dies nicht der Fall ist). Wenn man entscheidet, dass Zeichenfolgen mit einem Längenpräfix versehen werden sollen, wie viele Zeichen mit einem Längenpräfix sollte man verwenden? Die Verwendung von zwei würde eine künstliche Begrenzung der Zeichenfolgenlänge für Computer mit 8-Bit-Zeichen und 32-Bit-Adressierungsraum auferlegen, während Speicherplatz für Computer mit 16-Bit-Zeichen und 16-Bit-Adressierungsraum verschwendet wird.

Wenn man zulassen möchte, dass Zeichenfolgen beliebiger Länge effizient gespeichert werden, und wenn 'char' immer 8-Bit ist, kann man - aus Kosten- und Codekostengründen - ein Schema definieren, bei dem eine Zeichenfolge mit einer geraden Zahl vorangestellt wird N wäre N / 2 Bytes lang, eine Zeichenfolge, der ein ungerader Wert N und ein gerader Wert M (Rückwärtslesen) vorangestellt sind, könnte ((N-1) + M * char_max) / 2 usw. sein und erfordert jeden Puffer, der Ansprüche, eine bestimmte Menge an Speicherplatz für eine Zeichenfolge anzubieten, müssen genügend Bytes vor diesem Speicherplatz zulassen, um die maximale Länge zu verarbeiten. Die Tatsache, dass 'char' nicht immer 8 Bit ist, würde ein solches Schema jedoch komplizieren, da die Anzahl von 'char', die erforderlich ist, um die Länge eines Strings zu halten, abhängig von der CPU-Architektur variieren würde.

Superkatze
quelle
Das Präfix könnte leicht eine implementierungsdefinierte Größe haben, so wie es ist sizeof(char).
Billy ONeal
@ BillyONeal: sizeof(char)ist einer. Immer. Man könnte das Präfix eine implementierungsdefinierte Größe haben, aber es wäre umständlich. Außerdem gibt es keine wirkliche Möglichkeit, die "richtige" Größe zu ermitteln. Wenn man viele Zeichenfolgen mit 4 Zeichen hält, würde das Auffüllen mit Nullen 25% Overhead verursachen, während ein Präfix mit einer Länge von 4 Bytes 100% Overhead verursachen würde. Ferner könnte die Zeit, die zum Packen und Entpacken von Präfixen mit einer Länge von vier Bytes aufgewendet wird, die Kosten für das Scannen von 4-Byte-Zeichenfolgen nach dem Nullbyte überschreiten.
Supercat
1
Ah ja. Du hast recht. Das Präfix könnte jedoch leicht etwas anderes als char sein. Alles, was die Ausrichtungsanforderungen auf der Zielplattform erfüllen würde, wäre in Ordnung. Ich werde aber nicht dorthin gehen - ich habe dies bereits zu Tode argumentiert.
Billy ONeal
Unter der Annahme, dass Zeichenfolgen mit einem Längenpräfix versehen sind, wäre es wahrscheinlich am sinnvollsten, ein size_tPräfix zu verwenden (Speicherverschwendung wäre verdammt, es wäre das vernünftigste - Zeichenfolgen mit einer möglichen Länge zuzulassen, die möglicherweise in den Speicher passen könnten). In der Tat ist die Art , was D tut; Arrays sind struct { size_t length; T* ptr; }und Strings sind nur Arrays von immutable(char).
Tim
@ TimČas: Wenn Zeichenfolgen nicht wortausgerichtet sein müssen, werden die Kosten für die Arbeit mit kurzen Zeichenfolgen auf vielen Plattformen von der Anforderung dominiert, die Länge zu packen und zu entpacken. Ich sehe das wirklich nicht als praktisch an. Wenn Zeichenfolgen inhaltsunabhängige Byte-Arrays beliebiger Größe sein sollen, ist es meiner Meinung nach besser, die Länge vom Zeiger auf die Zeichendaten getrennt zu halten und eine Sprache zu haben, mit der beide Informationen für eine wörtliche Zeichenfolge abgerufen werden können .
Supercat
2

Viele Entwurfsentscheidungen in Bezug auf C beruhen auf der Tatsache, dass die Parameterübergabe bei der ursprünglichen Implementierung etwas teuer war. Bei einer Wahl zwischen z

void add_element_to_next(arr, offset)
  char[] arr;
  int offset;
{
  arr[offset] += arr[offset+1];
}

char array[40];

void test()
{
  for (i=0; i<39; i++)
    add_element_to_next(array, i);
}

gegen

void add_element_to_next(ptr)
  char *p;
{
  p[0]+=p[1];
}

char array[40];

void test()
{
  int i;
  for (i=0; i<39; i++)
    add_element_to_next(arr+i);
}

Letzteres wäre etwas billiger (und daher bevorzugt) gewesen, da nur ein Parameter anstelle von zwei übergeben werden musste. Wenn die aufgerufene Methode weder die Basisadresse des Arrays noch den darin enthaltenen Index kennen müsste, wäre die Übergabe eines einzelnen Zeigers, der beide kombiniert, billiger als die Übergabe der Werte separat.

Während es viele vernünftige Möglichkeiten gibt, wie C Zeichenfolgenlängen codieren könnte, hätten die bis zu diesem Zeitpunkt erfundenen Ansätze alle erforderlichen Funktionen, die in der Lage sein sollten, mit einem Teil einer Zeichenfolge zu arbeiten, um die Basisadresse der Zeichenfolge und zu akzeptieren der gewünschte Index als zwei separate Parameter. Durch die Verwendung der Null-Byte-Terminierung konnte diese Anforderung vermieden werden. Obwohl andere Ansätze mit heutigen Maschinen besser wären (moderne Compiler übergeben häufig Parameter in Registern, und memcpy kann auf eine Weise optimiert werden, die strcpy () - Äquivalente können nicht), verwendet genug Produktionscode nullbyte-terminierte Zeichenfolgen, die nur schwer in andere geändert werden können.

PS - Als Gegenleistung für eine leichte Geschwindigkeitsstrafe bei einigen Operationen und einen kleinen zusätzlichen Aufwand bei längeren Zeichenfolgen wäre es möglich gewesen, dass Methoden, die mit Zeichenfolgen arbeiten, Zeiger direkt auf Zeichenfolgen, Zeichenfolgenpuffer mit eingeschränkten Grenzen oder akzeptieren Datenstrukturen, die Teilzeichenfolgen einer anderen Zeichenfolge identifizieren. Eine Funktion wie "strcat" hätte ungefähr so ​​ausgesehen wie [moderne Syntax]

void strcat(unsigned char *dest, unsigned char *src)
{
  struct STRING_INFO d,s;
  str_size_t copy_length;

  get_string_info(&d, dest);
  get_string_info(&s, src);
  if (d.si_buff_size > d.si_length) // Destination is resizable buffer
  {
    copy_length = d.si_buff_size - d.si_length;
    if (s.src_length < copy_length)
      copy_length = s.src_length;
    memcpy(d.buff + d.si_length, s.buff, copy_length);
    d.si_length += copy_length;
    update_string_length(&d);
  }
}

Etwas größer als die K & R-Strcat-Methode, würde jedoch die Überprüfung von Grenzen unterstützen, was bei der K & R-Methode nicht der Fall ist. Ferner wäre es im Gegensatz zum gegenwärtigen Verfahren möglich, einen beliebigen Teilstring leicht zu verketten, z

/* Concatenate 10th through 24th characters from src to dest */

void catpart(unsigned char *dest, unsigned char *src)
{
  struct SUBSTRING_INFO *inf;
  src = temp_substring(&inf, src, 10, 24);
  strcat(dest, src);
}

Beachten Sie, dass die Lebensdauer des von temp_substring zurückgegebenen Strings durch die von sund begrenzt ist src, je nachdem , welcher Wert kürzer war (weshalb die Methode dies erfordertinf - wenn sie lokal ist, stirbt sie bei der Rückgabe der Methode).

In Bezug auf die Speicherkosten würden Zeichenfolgen und Puffer mit bis zu 64 Byte ein Byte Overhead haben (wie Zeichenfolgen mit Nullterminierung). Längere Zeichenfolgen hätten etwas mehr (ob man einen Overhead zwischen zwei Bytes zulässt und das maximal erforderliche ein Zeit / Raum-Kompromiss wäre). Ein spezieller Wert des Längen- / Modusbytes würde verwendet, um anzuzeigen, dass eine Zeichenfolgenfunktion eine Struktur erhalten hat, die ein Flagbyte, einen Zeiger und eine Pufferlänge enthält (die dann willkürlich in eine andere Zeichenfolge indiziert werden kann).

Natürlich hat K & R so etwas nicht implementiert, aber das liegt höchstwahrscheinlich daran, dass sie nicht viel Aufwand für die Handhabung von Strings betreiben wollten - ein Bereich, in dem viele Sprachen auch heute noch eher anämisch erscheinen.

Superkatze
quelle
Es gibt nichts, was daran gehindert hätte char* arr, auf eine Struktur des Formulars struct { int length; char characters[ANYSIZE_ARRAY] };oder ähnliches zu verweisen, die als einzelner Parameter noch passierbar wäre.
Billy ONeal
@BillyONeal: Zwei Probleme mit diesem Ansatz: (1) Es würde nur das Übergeben des Strings als Ganzes erlauben, während der vorliegende Ansatz auch das Übergeben des Endes eines Strings erlaubt; (2) Bei Verwendung mit kleinen Saiten wird viel Platz verschwendet. Wenn K & R etwas Zeit mit Streichern verbringen wollte, hätten sie die Dinge viel robuster machen können, aber ich glaube nicht, dass sie beabsichtigten, dass ihre neue Sprache zehn Jahre später verwendet wird, geschweige denn vierzig.
Supercat
1
Dieses Stück über die Calling Convention ist eine einfache Geschichte ohne Bezug zur Realität ... es war keine Überlegung im Design. Und registergestützte Anrufkonventionen wurden bereits "erfunden". Auch Ansätze wie zwei Zeiger waren keine Option, da Strukturen nicht erstklassig waren ... nur Grundelemente waren zuweisbar oder passierbar; Das Kopieren von Strukturen kam erst unter UNIX V7 an. Es ist ein Witz, memcpy (das es auch nicht gab) zu benötigen, um einen Zeichenfolgenzeiger zu kopieren. Versuchen Sie, ein vollständiges Programm zu schreiben, nicht nur isolierte Funktionen, wenn Sie das Sprachdesign vortäuschen.
Jim Balter
1
"Das liegt höchstwahrscheinlich daran, dass sie nicht viel Aufwand für die Handhabung von Saiten betreiben wollten" - Unsinn; Die gesamte Anwendungsdomäne des frühen UNIX war die Zeichenfolgenbehandlung. Wenn das nicht gewesen wäre, hätten wir nie davon gehört.
Jim Balter
1
"Ich denke nicht, dass" der Zeichenpuffer mit einem Int beginnt, das die Länge enthält "magischer ist" - es ist, wenn Sie str[n]sich auf das richtige Zeichen beziehen wollen. Dies sind die Dinge, über die die Leute, die darüber diskutieren , nicht nachdenken .
Jim Balter
2

Laut Joel Spolsky in diesem Blog-Beitrag ,

Dies liegt daran, dass der PDP-7-Mikroprozessor, auf dem UNIX und die Programmiersprache C erfunden wurden, einen ASCIZ-Zeichenfolgentyp hatte. ASCIZ bedeutete "ASCII mit einem Z (Null) am Ende".

Nachdem ich alle anderen Antworten hier gesehen habe, bin ich überzeugt, dass selbst wenn dies wahr ist, dies nur ein Teil des Grundes dafür ist, dass C nullterminierte "Strings" hat. Dieser Beitrag ist ziemlich aufschlussreich, wie einfach Dinge wie Saiten tatsächlich ziemlich schwierig sein können.

BenK
quelle
2
Schau, ich respektiere Joel für viele Dinge; Aber hier spekuliert er. Die Antwort von Hans Passant stammt direkt von den Erfindern von C.
Billy ONeal
1
Ja, aber wenn das, was Spolsky sagt, überhaupt wahr ist, dann wäre es Teil der "Bequemlichkeit" gewesen, auf die sie sich bezogen. Das ist teilweise der Grund, warum ich diese Antwort aufgenommen habe.
BenK
AFAIK .ASCIZwar nur eine Assembler-Anweisung zum Erstellen einer Folge von Bytes, gefolgt von 0. Es bedeutet nur, dass eine nullterminierte Zeichenfolge zu dieser Zeit ein etabliertes Konzept war. Dies bedeutet nicht , dass nullterminierte Zeichenfolgen etwas mit der Architektur eines PDP- * zu tun haben, außer dass Sie enge Schleifen schreiben können, die aus MOVB(Kopieren eines Bytes) und BNE(Verzweigen, wenn das zuletzt kopierte Byte nicht Null war) bestehen.
Adrian W
Es soll zeigen, dass C eine alte, schlaffe, heruntergekommene Sprache ist.
Purec
2

Nicht unbedingt eine Begründung , sondern ein Kontrapunkt zur Längencodierung

  1. Bestimmte Formen der dynamischen Längencodierung sind der statischen Längencodierung in Bezug auf den Speicher überlegen. Dies hängt alles von der Verwendung ab. Schauen Sie sich UTF-8 zum Beweis an. Es ist im Wesentlichen ein erweiterbares Zeichenarray zum Codieren eines einzelnen Zeichens. Dies verwendet ein einzelnes Bit für jedes erweiterte Byte. Die NUL-Terminierung verwendet 8 Bit. Das Längenpräfix kann meiner Meinung nach auch mit 64 Bit als unendliche Länge bezeichnet werden. Wie oft Sie den Fall Ihrer zusätzlichen Bits treffen, ist der entscheidende Faktor. Nur 1 extrem große Saite? Wen interessiert es, wenn Sie 8 oder 64 Bit verwenden? Viele kleine Zeichenketten (dh Zeichenketten mit englischen Wörtern)? Dann sind Ihre Präfixkosten ein großer Prozentsatz.

  2. Zeichenfolgen mit Längenpräfix, die Zeit sparen, sind keine echte Sache . Unabhängig davon, ob für die angegebenen Daten eine Länge angegeben werden muss, Sie zur Kompilierungszeit zählen oder tatsächlich dynamische Daten bereitgestellt werden, die Sie als Zeichenfolge codieren müssen. Diese Größen werden zu einem bestimmten Zeitpunkt im Algorithmus berechnet. Eine separate Variable der Größe einer Null - terminierte Zeichenkette zu speichern , kann vorgesehen werden. Das macht den Vergleich zur Zeitersparnis umstritten. Man hat nur eine zusätzliche NUL am Ende ... aber wenn die Längencodierung diese NUL nicht enthält, gibt es buchstäblich keinen Unterschied zwischen den beiden. Es ist überhaupt keine algorithmische Änderung erforderlich. Nur ein Pre-Pass, den Sie manuell entwerfen müssen, anstatt dass ein Compiler / eine Laufzeit dies für Sie erledigt. Bei C geht es hauptsächlich darum, Dinge manuell zu erledigen.

  3. Das optionale Längenpräfix ist ein Verkaufsargument. Ich brauche diese zusätzlichen Informationen nicht immer für einen Algorithmus. Wenn ich sie also für jede Zeichenfolge ausführen muss, kann meine Vorberechnungs- + Rechenzeit niemals unter O (n) fallen. (Dh Hardware-Zufallszahlengenerator 1-128. Ich kann aus einer "unendlichen Zeichenfolge" ziehen. Nehmen wir an, sie generiert nur so schnell Zeichen. Unsere Zeichenfolgenlänge ändert sich also ständig. Aber meine Verwendung der Daten ist wahrscheinlich egal, wie Ich habe nur viele zufällige Bytes. Es möchte nur das nächste verfügbare nicht verwendete Byte, sobald es nach einer Anfrage abgerufen werden kann. Ich könnte auf dem Gerät warten. Aber ich könnte auch einen Puffer mit vorgelesenen Zeichen haben. Ein Längenvergleich ist eine unnötige Rechenverschwendung. Eine Nullprüfung ist effizienter.)

  4. Längenpräfix ist ein guter Schutz gegen Pufferüberlauf? Dies gilt auch für die Verwendung von Bibliotheksfunktionen und deren Implementierung. Was ist, wenn ich fehlerhafte Daten weitergebe? Mein Puffer ist 2 Bytes lang, aber ich sage der Funktion, dass es 7 ist! Beispiel: Wenn get () für bekannte Daten verwendet werden sollte, hätte es eine interne Pufferprüfung geben können, bei der kompilierte Puffer und malloc () getestet wurden.Anrufe und folgen immer noch spec. Wenn es als Pipe für unbekannte STDIN verwendet werden sollte, um zu einem unbekannten Puffer zu gelangen, kann man die Puffergröße eindeutig nicht kennen, was bedeutet, dass ein Argument arg sinnlos ist. Sie benötigen hier etwas anderes wie eine Kanarienvogelprüfung. Im Übrigen können Sie einigen Streams und Eingaben kein Längenpräfix voranstellen, Sie können es einfach nicht. Dies bedeutet, dass die Längenprüfung in den Algorithmus integriert werden muss und kein magischer Teil des Typisierungssystems ist. TL; DR NUL-terminiert musste nie unsicher sein, es endete nur so durch Missbrauch.

  5. Zähler-Zähler-Punkt: NUL-Terminierung ist bei Binärdateien ärgerlich. Sie müssen hier entweder ein Längenpräfix eingeben oder NUL-Bytes auf irgendeine Weise transformieren: Escape-Codes, Bereichs-Neuzuordnung usw., was natürlich mehr Speicherauslastung / reduzierte Informationen / mehr Operationen pro Byte bedeutet. Längenpräfix gewinnt hier meistens den Krieg. Der einzige Vorteil einer Transformation besteht darin, dass keine zusätzlichen Funktionen geschrieben werden müssen, um die Längenpräfix-Zeichenfolgen abzudecken. Dies bedeutet, dass Sie Ihre optimierten Sub-O (n) -Routinen automatisch als O (n) -Äquivalente verwenden können, ohne weiteren Code hinzuzufügen. Nachteil ist natürlich Zeit- / Speicher- / Komprimierungsverschwendung bei Verwendung auf schweren NUL-Saiten. Abhängig davon, wie viel von Ihrer Bibliothek Sie duplizieren, um Binärdaten zu verarbeiten, kann es sinnvoll sein, nur mit Zeichenfolgen mit Längenpräfix zu arbeiten. Das heißt, man könnte dasselbe auch mit Längenpräfix-Zeichenfolgen tun ... -1 Länge könnte NUL-terminiert bedeuten und Sie könnten NUL-terminierte Zeichenfolgen innerhalb von Längen-terminierten Zeichenfolgen verwenden.

  6. Concat: "O (n + m) vs O (m)" Ich gehe davon aus, dass Sie m als Gesamtlänge der Zeichenfolge nach der Verkettung bezeichnen, da beide mindestens diese Anzahl von Operationen haben müssen (Sie können nicht einfach anheften -auf zu String 1, was ist, wenn Sie neu zuordnen müssen?). Und ich gehe davon aus, dass n eine mythische Menge von Operationen ist, die Sie aufgrund einer Vorberechnung nicht mehr ausführen müssen. Wenn ja, dann ist die Antwort einfach: Vorberechnung. WennSie bestehen darauf, dass Sie immer genug Speicher haben, um keine Neuzuweisung vornehmen zu müssen, und das ist die Grundlage für die Big-O-Notation. Dann ist die Antwort noch einfacher: Führen Sie eine binäre Suche im zugewiesenen Speicher für das Ende von Zeichenfolge 1 durch Swatch von unendlichen Nullen nach String 1, damit wir uns nicht um Realloc kümmern müssen. Dort bekam ich leicht n zu loggen (n) und ich versuchte es kaum. Wenn Sie sich an log (n) erinnern, ist dies auf einem realen Computer im Wesentlichen immer nur 64, was im Wesentlichen so ist, als würde man O (64 + m) sagen, was im Wesentlichen O (m) ist. (Und ja, diese Logik wurde bei der Laufzeitanalyse der heute verwendeten realen Datenstrukturen verwendet. Es ist kein Blödsinn von meinem Kopf.)

  7. Concat () / Len () wieder : Memoize Ergebnisse. Einfach. Verwandelt alle Berechnungen nach Möglichkeit in Vorberechnungen. Dies ist eine algorithmische Entscheidung. Es ist keine erzwungene Einschränkung der Sprache.

  8. Das Übergeben von String-Suffixen ist mit der NUL-Terminierung einfacher / möglich. Abhängig davon, wie das Längenpräfix implementiert ist, kann es die ursprüngliche Zeichenfolge zerstören und manchmal sogar nicht möglich sein. Benötigen Sie eine Kopie und übergeben Sie O (n) anstelle von O (1).

  9. Das Übergeben / De-Referenzieren von Argumenten ist für NUL-terminierte im Vergleich zum Längenpräfix geringer. Offensichtlich, weil Sie weniger Informationen weitergeben. Wenn Sie keine Länge benötigen, spart dies viel Platz und ermöglicht Optimierungen.

  10. Du kannst schummeln. Es ist wirklich nur ein Zeiger. Wer sagt, dass Sie es als Zeichenfolge lesen müssen? Was ist, wenn Sie es als einzelnes Zeichen oder als Float lesen möchten? Was ist, wenn Sie das Gegenteil tun und einen Float als String lesen möchten? Wenn Sie vorsichtig sind, können Sie dies mit NUL-Terminierung tun. Sie können dies nicht mit dem Längenpräfix tun, es ist ein Datentyp, der sich normalerweise deutlich von einem Zeiger unterscheidet. Sie müssten höchstwahrscheinlich Byte für Byte einen String erstellen und die Länge ermitteln. Wenn Sie so etwas wie einen ganzen Float haben möchten (wahrscheinlich mit einem NUL), müssen Sie natürlich ohnehin Byte für Byte lesen, aber die Details bleiben Ihnen überlassen, um zu entscheiden.

TL; DR Verwenden Sie Binärdaten? Wenn nein, ermöglicht die NUL-Terminierung mehr algorithmische Freiheit. Wenn ja, ist die Codemenge im Verhältnis zur Geschwindigkeit / Speicher / Komprimierung Ihr Hauptanliegen. Eine Mischung der beiden Ansätze oder Memoisierung könnte am besten sein.

Schwarz
quelle
9 war irgendwie off-base / falsch dargestellt. Längenvorfix hat dieses Problem nicht. Lenth Passing als separate Variable tut. Wir sprachen über Pre-Fiix, aber ich wurde mitgerissen. Es ist immer noch eine gute Sache, darüber nachzudenken, also lasse ich es dort. : d
Schwarz
1

Ich kaufe nicht die Antwort "C hat keine Zeichenfolge". Zwar unterstützt C keine integrierten übergeordneten Typen, aber Sie können trotzdem Datenstrukturen in C darstellen, und genau das ist eine Zeichenfolge. Die Tatsache, dass ein String nur ein Zeiger in C ist, bedeutet nicht, dass die ersten N Bytes als Länge keine besondere Bedeutung annehmen können.

Windows / COM-Entwickler kennen den BSTRTyp genau so - eine C-Zeichenfolge mit Längenpräfix, bei der die tatsächlichen Zeichendaten nicht bei Byte 0 beginnen.

Es scheint also, dass die Entscheidung, die Nullterminierung zu verwenden, einfach das ist, was die Leute bevorzugen, nicht eine Notwendigkeit der Sprache.

Mr. Boy
quelle
-3

gcc akzeptiert die folgenden Codes:

char s [4] = "abcd";

und es ist in Ordnung, wenn wir es als ein Array von Zeichen behandeln, aber nicht als Zeichenfolge. Das heißt, wir können mit s [0], s [1], s [2] und s [3] oder sogar mit memcpy (dest, s, 4) darauf zugreifen. Aber wir werden unordentliche Charaktere bekommen, wenn wir es mit Puts versuchen, oder schlimmer noch mit strcpy (dest, s).

kkaaii
quelle
@Adrian W. Dies ist gültig C. Zeichenfolgen mit exakter Länge werden speziell ummantelt und NUL wird für sie weggelassen. Dies ist im Allgemeinen eine unkluge Vorgehensweise, kann jedoch in Fällen wie dem Auffüllen von Header-Strukturen nützlich sein, die FourCC- "Strings" verwenden.
Kevin Thibedeau
Du hast recht. Dies ist gültig C, wird kompiliert und verhält sich wie kkaaii beschrieben. Der Grund für die Abstimmungen (nicht meine ...) ist wahrscheinlich eher, dass diese Antwort die Frage von OP in keiner Weise beantwortet.
Adrian W