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.)
quelle
Antworten:
Aus dem Maul des Pferdes
Dennis M Ritchie, Entwicklung der C-Sprache
quelle
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 :)
quelle
char *temp = "foo bar";
ist eine gültige Aussage in C ... hey! Ist das nicht eine Schnur? ist es nicht null beendet?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:
"this\0is\0valid\0C"
. Ist es eine Schnur? oder vier Saiten? Oder ein paar Bytes ...char a[3] = "foo";
ist gültiges C (nicht C ++) und setzt keine endgültige Null in a.char*
. Das heißt, nicht die Adresse der Zeichenfolge zurückzugeben, sondern die tatsächlichen Daten zurückzugeben.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).
quelle
std::basic_string
es doch.\0
am Ende explizit angeben, wenn Programmierer dies anstelle des impliziten wünschen. Das Voranstellen der Länge ist viel schlimmer.Ich denke, es hat historische Gründe und fand dies in Wikipedia :
quelle
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
string
Typ hätte, wieint
oderchar
, 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:
Nehmen wir also an, dass dies ein Längenpräfix war. Schreiben wir den Code, um zwei Zeichenfolgen zu verketten:
Eine andere Alternative wäre die Verwendung einer Struktur zum Definieren einer Zeichenfolge:
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
mem
Funktionen 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.quelle
strlen
stattdessen 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 ashort
auf 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.short
effektiv 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.string
Typs: 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 implementiertchar
werden kann, wenn Sie den Begriff der Codierung eingeführt haben.buf
eine Zuordnung erforderlich), oder verwendenstruct string {int len; char buf[]};
und ordnen Sie das Ganze mit einer Zuordnung als flexibles Array-Mitglied zu und geben Sie es alsstring*
. (Oder wohlstruct string {int capacity; int len; char buf[]};
aus offensichtlichen Leistungsgründen)Aus Gründen der Leistung und Sicherheit sollten Sie die Länge eines Strings beibehalten, während Sie damit arbeiten, anstatt wiederholt
strlen
oder 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 wiepath_to_filename
oderfilename_to_extension
unmö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.
quelle
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
Insgesamt 1 Register verwenden
Fall 2
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
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).
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.
quelle
O(m+n)
mit nullterm Strings,O(n)
typisch überall sonst. LängeO(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, habenO(n)
Sie lediglich darauf hingewiesen, dass die Länge mit der Zeichenfolge gespeichert werden soll.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.
quelle
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 :)
quelle
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:
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-
char
Array).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.quelle
char*
, im Allgemeinen a erwarten , erwarten viele Methoden, die keine nullterminierte Zeichenfolge erwarten , auch achar*
. 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ältErstens 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.)
quelle
Die Nullterminierung ermöglicht schnelle zeigerbasierte Operationen.
quelle
strlen
. Ich würde sagen, das ist ein kleiner Nachteil.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.
quelle
sizeof(char)
.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.size_t
Prä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 sindstruct { size_t length; T* ptr; }
und Strings sind nur Arrays vonimmutable(char)
.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
gegen
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]
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
Beachten Sie, dass die Lebensdauer des von temp_substring zurückgegebenen Strings durch die von
s
und begrenzt istsrc
, 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.
quelle
char* arr
, auf eine Struktur des Formularsstruct { int length; char characters[ANYSIZE_ARRAY] };
oder ähnliches zu verweisen, die als einzelner Parameter noch passierbar wäre.str[n]
sich auf das richtige Zeichen beziehen wollen. Dies sind die Dinge, über die die Leute, die darüber diskutieren , nicht nachdenken .Laut Joel Spolsky in diesem Blog-Beitrag ,
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.
quelle
.ASCIZ
war nur eine Assembler-Anweisung zum Erstellen einer Folge von Bytes, gefolgt von0
. 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 ausMOVB
(Kopieren eines Bytes) undBNE
(Verzweigen, wenn das zuletzt kopierte Byte nicht Null war) bestehen.Nicht unbedingt eine Begründung , sondern ein Kontrapunkt zur Längencodierung
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.
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.
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.)
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.
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.
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.)
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.
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).
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.
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.
quelle
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
BSTR
Typ 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.
quelle
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).
quelle