Qual è la logica delle stringhe con terminazione null?


281

Per quanto adoro il C e il C ++, non posso fare a meno di grattarmi la testa nella scelta di stringhe con terminazione null:

  • Le stringhe con prefisso di lunghezza (cioè Pascal) esistevano prima di C
  • Le stringhe con prefisso di lunghezza rendono più veloci diversi algoritmi consentendo una ricerca della lunghezza temporale costante.
  • Le stringhe con prefisso di lunghezza rendono più difficile causare errori di sovraccarico del buffer.
  • Anche su una macchina a 32 bit, se si consente alla stringa di avere la dimensione della memoria disponibile, una stringa con prefisso di lunghezza è solo tre byte più larga di una stringa con terminazione nulla. Su macchine a 16 bit questo è un singolo byte. Su macchine a 64 bit, 4 GB è un limite di lunghezza della stringa ragionevole, ma anche se si desidera espanderlo alla dimensione della parola macchina, le macchine a 64 bit di solito hanno una memoria ampia che rende i sette byte extra una sorta di argomento null. So che lo standard C originale è stato scritto per macchine follemente povere (in termini di memoria), ma l'argomento dell'efficienza non mi vende qui.
  • Praticamente ogni altra lingua (ad es. Perl, Pascal, Python, Java, C #, ecc.) Usa stringhe con prefisso di lunghezza. Queste lingue di solito battono C nei benchmark di manipolazione delle stringhe perché sono più efficienti con le stringhe.
  • Il C ++ ha corretto un po 'questo con il std::basic_stringmodello, ma le matrici di caratteri semplici che prevedono stringhe con terminazione null sono ancora pervasive. Anche questo è imperfetto perché richiede l'allocazione dell'heap.
  • Le stringhe con terminazione nulla devono riservare un carattere (vale a dire null), che non può esistere nella stringa, mentre le stringhe con prefisso di lunghezza possono contenere null incorporati.

Molte di queste cose sono venute alla luce più recentemente di C, quindi avrebbe senso che C non le conoscesse. Tuttavia, molti erano chiaramente ben prima che C diventasse. Perché sarebbero state scelte le stringhe nulle invece del prefisso ovviamente di lunghezza superiore?

EDIT : Poiché alcuni hanno chiesto fatti (e non mi sono piaciuti quelli che ho già fornito) sul mio punto di efficienza sopra, derivano da alcune cose:

  • Concat utilizzando stringhe con terminazione null richiede O (n + m) complessità temporale. Il prefisso di lunghezza richiede spesso solo O (m).
  • La lunghezza usando stringhe con terminazione null richiede O (n) complessità temporale. Il prefisso di lunghezza è O (1).
  • La lunghezza e il concat sono le operazioni di stringa di gran lunga più comuni. Esistono diversi casi in cui le stringhe con terminazione null possono essere più efficienti, ma si verificano molto meno spesso.

Dalle risposte di seguito, questi sono alcuni casi in cui le stringhe con terminazione null sono più efficienti:

  • Quando è necessario tagliare l'inizio di una stringa e passarlo a un metodo. Non puoi davvero farlo in tempo costante con il prefisso di lunghezza anche se ti è permesso distruggere la stringa originale, perché probabilmente il prefisso di lunghezza deve seguire le regole di allineamento.
  • In alcuni casi in cui stai semplicemente eseguendo il looping della stringa carattere per carattere, potresti essere in grado di salvare un registro CPU. Nota che questo funziona solo nel caso in cui non hai allocato dinamicamente la stringa (Perché allora dovresti liberarla, necessitando di usare quel registro CPU che hai salvato per contenere il puntatore che originariamente avevi da malloc e dagli amici).

Nessuna delle precedenti è quasi comune quanto la lunghezza e il concat.

Ce n'è un altro affermato nelle risposte di seguito:

  • Devi tagliare l'estremità della stringa

ma questo non è corretto - è la stessa quantità di tempo per stringhe con terminazione null e lunghezza prefissata. (Le stringhe con terminazione Null si limitano a incollare un valore nullo nel punto in cui si desidera che si trovi la nuova estremità, i prefissi di lunghezza vengono sottratti dal prefisso.)


110
Ho sempre pensato che fosse un rito di passaggio per tutti i programmatori C ++ scrivere la propria libreria di stringhe.
Juliet,

31
Cosa significa aspettarsi spiegazioni razionali adesso. Suppongo che vorrai ascoltare una logica per x86 o DOS dopo? Per quanto mi riguarda, vince la tecnologia peggiore. Ogni volta. E la peggior rappresentazione delle stringhe.
jalf

4
Perché ritieni che le stringhe con prefisso di lunghezza siano superiori? Dopotutto, C divenne popolare perché utilizzava stringhe con terminazione null, che lo distinguevano dalle altre lingue.
Daniel C. Sobral,

44
@Daniel: C è diventato popolare perché è una rappresentazione semplice, efficiente e portatile di programmi eseguibili su macchine Von Neumann e perché è stato utilizzato per Unix. Certamente non è perché ha deciso di utilizzare stringhe con terminazione null. Se fosse stata una buona decisione di progettazione, la gente l'avrebbe copiata, e non l'ha fatto. Hanno sicuramente copiato praticamente tutto il resto da C.
Billy ONeal

4
Concat è solo O (m) con prefisso di lunghezza se distruggi una delle stringhe. Altrimenti, stessa velocità. Le stringhe C (storicamente) più comuni sono state la stampa e la scansione. In entrambi i casi, la terminazione null è più veloce perché salva un registro.
Daniel C. Sobral,

Risposte:


195

Dalla bocca del cavallo

Nessuno di BCPL, B o C supporta fortemente i dati dei caratteri nella lingua; ognuna tratta stringhe come vettori di numeri interi e integra le regole generali con alcune convenzioni. Sia in BCPL che in B una stringa letterale indica l'indirizzo di un'area statica inizializzata con i caratteri della stringa, racchiusa in celle. In BCPL, il primo byte compresso contiene il numero di caratteri nella stringa; in B, non vi è alcun conteggio e le stringhe sono terminate da un carattere speciale, che B ha scritto *e. Questa modifica è stata apportata in parte per evitare la limitazione della lunghezza di una stringa causata dal mantenimento del conteggio in uno slot a 8 o 9 bit, e in parte perché il mantenimento del conteggio sembrava, nella nostra esperienza, meno conveniente dell'uso di un terminatore.

Dennis M Ritchie, Sviluppo del linguaggio C.


12
Un'altra citazione pertinente: "... la semantica delle stringhe è pienamente inclusa nelle regole più generali che governano tutti gli array, e di conseguenza il linguaggio è più semplice da descrivere ..."
AShelly

151

C non ha una stringa come parte del linguaggio. Una "stringa" in C è solo un puntatore a char. Quindi forse stai facendo la domanda sbagliata.

"Qual è la logica per tralasciare un tipo di stringa" potrebbe essere più pertinente. A tal proposito, vorrei sottolineare che C non è un linguaggio orientato agli oggetti e ha solo tipi di valore di base. Una stringa è un concetto di livello superiore che deve essere implementato combinando in qualche modo valori di altri tipi. C è ad un livello inferiore di astrazione.

alla luce dello squall infuriato qui sotto:

Voglio solo sottolineare che non sto cercando di dire che questa è una domanda stupida o cattiva, o che il modo C di rappresentare le stringhe è la scelta migliore. Sto cercando di chiarire che la domanda sarebbe più concisa se si tiene conto del fatto che C non ha alcun meccanismo per differenziare una stringa come tipo di dati da un array di byte. È questa la scelta migliore alla luce della potenza di elaborazione e memoria dei computer di oggi? Probabilmente no. Ma il senno di poi è sempre il 20/20 e tutto il resto :)


29
char *temp = "foo bar";è una dichiarazione valida in C ... ehi! non è una stringa? non è null terminato?
Yanick Rochon,

56
@Yanick: questo è solo un modo conveniente per dire al compilatore di creare un array di caratteri con un null alla fine. non è una "stringa"
Robert S Ciaccio l'

28
@calavera: Ma avrebbe potuto significare semplicemente "Creare un buffer di memoria con questo contenuto di stringa e un prefisso di due byte di lunghezza",
Billy ONeal,

14
@Billy: beh, dato che una 'stringa' è in realtà solo un puntatore a char, che equivale a un puntatore a byte, come faresti a sapere che il buffer con cui hai a che fare è davvero inteso come una 'stringa'? avresti bisogno di un nuovo tipo diverso da char / byte * per denotarlo. forse una struttura?
Robert S Ciaccio,

27
Penso che @calavera abbia ragione, C non ha un tipo di dati per le stringhe. Ok, puoi considerare una matrice di caratteri come una stringa, ma questo non significa che sia sempre una stringa (per stringa intendo una sequenza di caratteri con un significato definito). Un file binario è un array di caratteri, ma quei caratteri non significano nulla per un essere umano.
BlackBear l'

106

La domanda viene posta come una cosa Length Prefixed Strings (LPS)vs zero terminated strings (SZ), ma espone principalmente i vantaggi delle stringhe con prefisso di lunghezza. Ciò può sembrare schiacciante, ma ad essere sinceri dovremmo anche considerare gli svantaggi di LPS e i vantaggi di SZ.

A quanto ho capito, la domanda può anche essere intesa come un modo parziale di chiedere "quali sono i vantaggi delle stringhe con terminazione zero?".

Vantaggi (vedo) delle stringhe con terminazione zero:

  • molto semplice, non è necessario introdurre nuovi concetti nel linguaggio, possono fare matrici di caratteri / puntatori di caratteri.
  • il linguaggio principale include solo uno zucchero sintattico minimo per convertire qualcosa tra virgolette doppie in un mucchio di caratteri (in realtà un mucchio di byte). In alcuni casi può essere usato per inizializzare cose completamente estranee al testo. Ad esempio il formato di file immagine xpm è una sorgente C valida che contiene dati immagine codificati come una stringa.
  • tra l'altro, si può mettere uno zero in un letterale stringa, il compilatore sarà solo anche aggiungere un altro alla fine del letterale: "this\0is\0valid\0C". È una stringa? o quattro corde? O un mucchio di byte ...
  • implementazione piatta, nessuna indiretta nascosta, nessun numero intero nascosto.
  • nessuna allocazione di memoria nascosta coinvolta (beh, alcune famigerate funzioni non standard come strdup eseguono l'allocazione, ma questo è principalmente una fonte di problemi).
  • nessun problema specifico per hardware piccolo o grande (immagina l'onere di gestire la lunghezza del prefisso a 32 bit su microcontrollori a 8 bit o le restrizioni di limitare la dimensione della stringa a meno di 256 byte, che era un problema che avevo effettivamente con Turbo Pascal eoni fa).
  • l'implementazione della manipolazione di stringhe è solo una manciata di funzioni di libreria molto semplici
  • efficiente per l'uso principale delle stringhe: testo costante letto in sequenza da un inizio noto (principalmente messaggi all'utente).
  • lo zero finale non è nemmeno obbligatorio, sono disponibili tutti gli strumenti necessari per manipolare i caratteri come un mucchio di byte. Quando si esegue l'inizializzazione dell'array in C, è anche possibile evitare il terminatore NUL. Basta impostare la giusta dimensione. char a[3] = "foo";è C valido (non C ++) e non inserirà uno zero finale in a.
  • coerente con il punto di vista unix "tutto è file", inclusi "file" che non hanno una lunghezza intrinseca come stdin, stdout. Dovresti ricordare che le primitive open read e write sono implementate a un livello molto basso. Non sono chiamate in libreria, ma chiamate di sistema. E la stessa API viene utilizzata per file binari o di testo. Le primitive di lettura dei file ottengono un indirizzo di buffer e una dimensione e restituiscono la nuova dimensione. E puoi usare stringhe come buffer per scrivere. L'uso di un altro tipo di rappresentazione di stringa implicherebbe che non è possibile utilizzare facilmente una stringa letterale come buffer per l'output, oppure si dovrebbe fare in modo che abbia un comportamento molto strano quando si esegue il cast char*. Vale a dire non restituire l'indirizzo della stringa, ma invece restituire i dati effettivi.
  • molto facile da manipolare i dati di testo letti da un file sul posto, senza inutili copie del buffer, basta inserire gli zero nei punti giusti (beh, non proprio con la moderna C poiché le stringhe tra virgolette sono matrici di caratteri costanti al giorno d'oggi normalmente conservate in dati non modificabili segmento).
  • anteporre alcuni valori int di qualsiasi dimensione implicherebbe problemi di allineamento. La lunghezza iniziale dovrebbe essere allineata, ma non c'è motivo di farlo per i dati dei caratteri (e, di nuovo, forzare l'allineamento delle stringhe implicherebbe problemi nel trattarle come un mucchio di byte).
  • la lunghezza è nota al momento della compilazione per stringhe letterali costanti (sizeof). Quindi perché qualcuno dovrebbe voler memorizzarlo in memoria anteporlo ai dati effettivi?
  • in un modo che C sta facendo come (quasi) tutti gli altri, le stringhe sono viste come matrici di caratteri. Poiché la lunghezza dell'array non è gestita da C, la lunghezza logica non è gestita neanche per le stringhe. L'unica cosa sorprendente è che l'elemento 0 è stato aggiunto alla fine, ma è solo a livello di linguaggio principale quando si digita una stringa tra virgolette doppie. Gli utenti possono chiamare perfettamente le funzioni di manipolazione delle stringhe che passano la lunghezza, o persino usare semplicemente la semplice copia. SZ sono solo una struttura. Nella maggior parte delle altre lingue la lunghezza dell'array è gestita, è logico che è lo stesso per le stringhe.
  • in tempi moderni, tuttavia, i set di caratteri a 1 byte non sono sufficienti e spesso è necessario gestire stringhe unicode codificate in cui il numero di caratteri è molto diverso dal numero di byte. Implica che gli utenti probabilmente vorranno più di "solo le dimensioni", ma anche altre informazioni. Mantenere la lunghezza non serve a nulla (in particolare nessun luogo naturale per conservarli) per quanto riguarda queste altre informazioni utili.

Detto questo, non c'è bisogno di lamentarsi nel raro caso in cui le stringhe C standard sono effettivamente inefficienti. Le librerie sono disponibili. Se seguissi questa tendenza, dovrei lamentarmi del fatto che lo standard C non include alcuna funzione di supporto regex ... ma davvero tutti sanno che non è un vero problema in quanto ci sono librerie disponibili a tale scopo. Quindi, quando si desidera l'efficienza di manipolazione delle stringhe, perché non utilizzare una libreria come bstring ? O persino stringhe C ++?

EDIT : Recentemente ho avuto uno sguardo alle corde D . È abbastanza interessante vedere che la soluzione scelta non è né un prefisso di dimensione, né una terminazione zero. Come in C, le stringhe letterali racchiuse tra virgolette doppie sono solo una scorciatoia per array di caratteri immutabili, e il linguaggio ha anche una parola chiave stringa che significa che (array di caratteri immutabili).

Ma le matrici D sono molto più ricche delle matrici C. Nel caso di matrici statiche la lunghezza è nota in fase di esecuzione, quindi non è necessario memorizzare la lunghezza. Il compilatore ce l'ha al momento della compilazione. Nel caso di matrici dinamiche, la lunghezza è disponibile ma la documentazione D non indica dove è conservata. Per quanto ne sappiamo, il compilatore potrebbe scegliere di tenerlo in un registro o in una variabile memorizzata lontano dai dati dei caratteri.

Su normali array di caratteri o stringhe non letterali non c'è zero finale, quindi il programmatore deve metterlo da solo se vuole chiamare una funzione C da D. Nel caso particolare delle stringhe letterali, tuttavia il compilatore D mette ancora uno zero al fine di ogni stringa (per consentire un facile cast su stringhe C per rendere più semplice la chiamata della funzione C?), ma questo zero non fa parte della stringa (D non la conta nella dimensione della stringa).

L'unica cosa che mi ha deluso un po 'è che le stringhe dovrebbero essere utf-8, ma apparentemente la lunghezza restituisce ancora un numero di byte (almeno è vero sul mio compilatore gdc) anche quando si usano caratteri multi-byte. Non mi è chiaro se si tratta di un bug del compilatore o di uno scopo. (OK, probabilmente ho scoperto cosa è successo. Per dire al compilatore D che il tuo sorgente usa utf-8, all'inizio devi mettere un segno di ordine byte stupido. Scrivo stupido perché so di non farlo con l'editor, specialmente per UTF- 8 che dovrebbe essere compatibile ASCII).


7
... Continua ... Penso che molti dei tuoi punti siano semplicemente sbagliati, vale a dire l'argomento "tutto è un file". I file sono ad accesso sequenziale, le stringhe C no. Il prefisso di lunghezza può anche essere fatto con uno zucchero sintattico minimo. L'unico argomento ragionevole qui è il tentativo di gestire i prefissi a 32 bit su hardware di piccole dimensioni (ovvero 8 bit); Penso che potrebbe essere semplicemente risolto dicendo che la dimensione della lunghezza è determinata dall'implementazione. Dopotutto, ecco cosa std::basic_stringfa.
Billy ONeal,

3
@Billy ONeal: ci sono davvero due parti diverse nella mia risposta. Uno riguarda ciò che fa parte del "linguaggio C di base", l'altro riguarda ciò che le librerie standard dovrebbero fornire. Per quanto riguarda il supporto delle stringhe, esiste un solo elemento dal linguaggio principale: il significato di una doppia virgoletta racchiusa in un mucchio di byte. Non sono davvero più felice di te con il comportamento C. Mi sento magicamente aggiungere che zero alla fine di ogni doppia chiusura racchiusa di byte è abbastanza male. Preferirei ed esplicire \0alla fine quando i programmatori lo vogliono invece di quello implicito. Preparare la lunghezza è molto peggio.
Kriss,

2
@Billy ONeal: questo non è vero, gli usi si preoccupano di ciò che è core e di cosa sono le librerie. Il punto più importante è quando C viene utilizzato per implementare il sistema operativo. A quel livello non sono disponibili librerie. C viene spesso utilizzato anche in contesti incorporati o per programmare dispositivi in ​​cui si hanno spesso le stesse restrizioni. In molti casi probabilmente Joes non dovrebbe usare C al giorno d'oggi: "OK, lo vuoi sulla console? Hai una console? No? Peccato ..."
Kriss

5
@Billy "Bene, per lo 0,01% dei programmatori C che implementano sistemi operativi, va bene." Gli altri programmatori possono fare un'escursione. C è stato creato per scrivere un sistema operativo.
Daniel C. Sobral,

5
Perché? Perché dice che è un linguaggio generico? Dice cosa stavano facendo le persone che l'hanno scritto quando è stato creato? A cosa serviva per i primi anni della sua vita? Quindi, che cosa dice che non è d'accordo con me? È un linguaggio generico creato per scrivere un sistema operativo . Lo nega?
Daniel C. Sobral,

61

Penso che abbia ragioni storiche e l'ho trovato su Wikipedia :

Al tempo in cui C (e le lingue da cui era derivato) furono sviluppati, la memoria era estremamente limitata, quindi usare solo un byte di overhead per memorizzare la lunghezza di una stringa era attraente. L'unica alternativa popolare a quel tempo, di solito chiamata "stringa Pascal" (anche se utilizzata anche dalle prime versioni di BASIC), utilizzava un byte iniziale per memorizzare la lunghezza della stringa. Ciò consente alla stringa di contenere NUL e di trovare la lunghezza richiede solo un accesso alla memoria (O (1) (costante) tempo). Ma un byte limita la lunghezza a 255. Questa limitazione della lunghezza era molto più restrittiva dei problemi con la stringa C, quindi la stringa C in generale ha vinto.


2
@muntoo Hmm ... compatibilità?
Khachik,

19
@muntoo: Perché ciò spezzerebbe enormi quantità di codice C e C ++ esistente.
Billy ONeal,

10
@muntoo: i paradigmi vanno e vengono, ma il codice legacy è per sempre. Qualsiasi versione futura di C dovrebbe continuare a supportare stringhe con terminazione 0, altrimenti dovrebbe essere riscritto un codice legacy di oltre 30 anni (cosa che non accadrà). E fintanto che il vecchio modo è disponibile, questo è ciò che le persone continueranno a usare, poiché è quello che hanno familiarità.
John Bode,

8
@muntoo: credimi, a volte vorrei poterlo fare. Preferirei comunque le stringhe con terminazione 0 rispetto alle stringhe Pascal.
John Bode,

2
Parlare di legacy ... Le stringhe C ++ sono ora obbligate a terminare con NUL.
Jim Balter,

32

Calavera è giusto , ma come persone non sembrano per ottenere il suo punto, ti fornisce alcuni esempi di codice.

Innanzitutto, consideriamo cos'è C: un linguaggio semplice, in cui tutto il codice ha una traduzione piuttosto diretta in linguaggio automatico. Tutti i tipi si adattano ai registri e allo stack e non richiedono un sistema operativo o una libreria di runtime di grandi dimensioni per l'esecuzione, poiché doveva scrivere queste cose (un'attività alla quale si adatta perfettamente, considerando che non è nemmeno un probabile concorrente fino ad oggi).

Se C avesse un stringtipo, come intochar , sarebbe un tipo che non rientrava in un registro o nello stack e richiederebbe che l'allocazione di memoria (con tutta la sua infrastruttura di supporto) sia gestita in alcun modo. Tutto ciò va contro i principi di base di C.

Quindi, una stringa in C è:

char s*;

Quindi, supponiamo quindi che questo fosse prefissato in lunghezza. Scriviamo il codice per concatenare due stringhe:

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;
}

Un'altra alternativa sarebbe usare una struttura per definire una stringa:

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

A questo punto, tutta la manipolazione delle stringhe richiederebbe due allocazioni, il che, in pratica, significa che dovresti passare attraverso una libreria per gestirla.

La cosa divertente è che ... esistono strutture del genere in C! Non vengono semplicemente utilizzati per la visualizzazione quotidiana dei messaggi per la gestione dell'utente.

Quindi, ecco il punto sollevato da Calavera: non esiste un tipo di stringa in C . Per fare qualsiasi cosa con esso, dovresti prendere un puntatore e decodificarlo come puntatore a due tipi diversi, e quindi diventa molto rilevante quale sia la dimensione di una stringa e non può essere lasciato come "implementazione definita".

Ora, C può gestire la memoria in ogni caso, e le memfunzioni nella libreria (in <string.h>, anche!) Forniscono tutti gli strumenti necessari per gestire la memoria come una coppia di puntatori e dimensioni. Le cosiddette "stringhe" in C sono state create per un solo scopo: mostrare i messaggi nel contesto della scrittura di un sistema operativo destinato ai terminali di testo. E, per questo, è sufficiente la nullità.


2
1. +1. 2. Ovviamente se il comportamento predefinito della lingua sarebbe stato reso usando prefissi di lunghezza, ci sarebbero state altre cose per renderlo più semplice. Ad esempio, tutti i tuoi cast lì sarebbero stati nascosti dalle chiamate strlene dagli amici. Per quanto riguarda il problema con "lasciarlo all'implementazione", si potrebbe dire che il prefisso è quello che si shorttrova sulla casella di destinazione. Quindi tutto il tuo casting continuerebbe a funzionare. 3. Posso escogitare scenari inventati per tutto il giorno che fanno sembrare l'uno o l'altro sistema cattivo.
Billy ONeal,

5
@Billy La cosa della libreria è abbastanza vera, a parte il fatto che C è stato progettato per un uso minimo o nullo della libreria. L'uso dei prototipi, ad esempio, non era comune all'inizio. Dire che il prefisso shortlimita effettivamente la dimensione della stringa, che sembra essere una cosa a cui non erano interessati. Io stesso, avendo lavorato con stringhe BASIC e Pascal a 8 bit, stringhe COBOL di dimensioni fisse e cose simili, siamo diventati rapidamente un grande fan di stringhe C di dimensioni illimitate. Al giorno d'oggi, una dimensione di 32 bit gestirà qualsiasi stringa pratica, ma aggiungere quei byte all'inizio era problematico.
Daniel C. Sobral,

1
@Billy: Innanzitutto, grazie Daniel ... sembra che tu capisca a cosa sto arrivando. Secondo, Billy, penso che ti stia ancora perdendo il punto che viene sollevato qui. Io per primo non sto discutendo i pro e i contro del prefisso tipi di dati stringa con la loro lunghezza. Quello che sto dicendo, e quello che Daniel ha sottolineato molto chiaramente, è che non v'è stata una decisione presa nella realizzazione di C per non gestire tale argomento a tutti . Le stringhe non esistono per quanto riguarda la lingua di base. La decisione su come gestire le stringhe è lasciata al programmatore ... e la terminazione nulla è diventata popolare.
Robert S Ciaccio,

1
+1 da parte mia. Un'altra cosa che vorrei aggiungere; una struttura come si propone manca un passo importante verso un stringtipo reale : non è consapevole dei personaggi. È un array di "char" (un "char" nel gergo macchina è un personaggio tanto quanto una "parola" è ciò che gli umani chiamerebbero una parola in una frase). Una stringa di caratteri è un concetto di livello superiore che potrebbe essere implementato in cima a una matrice di charse si introducesse la nozione di codifica.
Frerich Raabe,

2
@ DanielC.Sobral: Inoltre, la struttura citata non richiederebbe due allocazioni. Usalo come lo hai nello stack (quindi bufrichiede solo un'allocazione) o usa struct string {int len; char buf[]};e alloca il tutto con una allocazione come membro flessibile dell'array e passalo come string*. (O probabilmente, struct string {int capacity; int len; char buf[]};per ovvi motivi di performance)
Mooing Duck,

20

Ovviamente per prestazioni e sicurezza, ti consigliamo di mantenere la lunghezza di una corda mentre lavori con essa piuttosto che eseguire ripetutamente strleno l'equivalente su di essa. Tuttavia, memorizzare la lunghezza in una posizione fissa appena prima del contenuto della stringa è un progetto incredibilmente male. Come sottolineato da Jörgen nei commenti sulla risposta di Sanjit, impedisce di considerare la coda di una stringa come una stringa, il che ad esempio rende molte operazioni comuni come path_to_filenameo filename_to_extensionimpossibili senza allocare nuova memoria (e comportando la possibilità di errori e gestione degli errori) . E poi ovviamente c'è il problema che nessuno può essere d'accordo su quanti byte il campo di lunghezza della stringa dovrebbe occupare (un sacco di "stringa di Pascal" errata

Il design di C di consentire al programmatore di scegliere se / dove / come memorizzare la lunghezza è molto più flessibile e potente. Ma ovviamente il programmatore deve essere intelligente. C punisce la stupidità con programmi che si bloccano, si fermano o fanno sradicare i nemici.


+1. Sarebbe bello avere un posto standard per archiviare la lunghezza in modo che quelli di noi che vogliono qualcosa come il prefisso di lunghezza non debbano scrivere tonnellate di "codice colla" ovunque.
Billy ONeal,

2
Non esiste un posto standard possibile rispetto ai dati delle stringhe, ma puoi ovviamente usare una variabile locale separata (ricalcolarla anziché passarla quando quest'ultima non è conveniente e la prima non è troppo dispendiosa) o una struttura con un puntatore alla stringa (e ancora meglio, un flag che indica se la struttura "possiede" il puntatore ai fini dell'allocazione o se si tratta di un riferimento a una stringa di proprietà altrove. E ovviamente è possibile includere un membro di array flessibile nella struttura per la flessibilità di allocare la stringa con la struttura che fa per te
R .. GitHub FERMA AIUTANDO GHIACCIO

13

Pigrizia, registro di frugalità e portabilità considerando l'intestino dell'assemblaggio di qualsiasi linguaggio, in particolare C, che è un gradino sopra l'assemblaggio (ereditando così molto codice legacy dell'assembly). Accetteresti che un carattere nullo sarebbe inutile in quei giorni ASCII, (e probabilmente buono come un carattere di controllo EOF).

vediamo in pseudo codice

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

totale 1 utilizzo del registro

caso 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

totale 2 registri utilizzati

Ciò potrebbe sembrare miope in quel momento, ma considerando la frugalità nel codice e nel registro (che erano PREMIUM a quel tempo, il momento in cui sai, usano la scheda perforata). Così essendo più veloce (quando la velocità del processore può essere contata in kHz), questo "Hack" è stato dannatamente buono e portatile per registrare con facilità un processore senza registrazione.

Per l'argomento implementerò 2 operazioni di stringa comuni

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

complessità O (n) dove nella maggior parte dei casi la stringa PASCAL è O (1) perché la lunghezza della stringa è anticipata alla struttura della stringa (ciò significherebbe anche che questa operazione dovrebbe essere eseguita in una fase precedente).

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

la complessità O (n) e anteporre la lunghezza della stringa non cambierebbe la complessità dell'operazione, mentre ammetto che ci vorrebbe 3 volte in meno.

D'altra parte, se usi la stringa PASCAL dovresti ridisegnare l'API per tenere conto della lunghezza e dell'endianità del registro account, la stringa PASCAL ha la limitazione ben nota di 255 caratteri (0xFF) perché la lunghezza è stata memorizzata in 1 byte (8 bit ), e se volevi una stringa più lunga (16 bit-> qualsiasi cosa) dovresti prendere in considerazione l'architettura in un livello del tuo codice, ciò significherebbe nella maggior parte dei casi API di stringa incompatibili se desideri una stringa più lunga.

Esempio:

Un file è stato scritto con l'API di stringa anteposta su un computer a 8 bit e quindi dovrebbe essere letto su un computer a 32 bit, cosa farebbe il programma pigro considerando che i tuoi 4byte sono la lunghezza della stringa e allocare così tanta memoria quindi tenta di leggere tanti byte. Un altro caso potrebbe essere la lettura di una stringa PPC a 32 byte (little endian) su un x86 (big endian), ovviamente se non sai che uno è scritto dall'altro ci sarebbero problemi. La lunghezza di 1 byte (0x00000001) diventerebbe 16777216 (0x0100000) che è 16 MB per la lettura di una stringa da 1 byte. Ovviamente diresti che le persone dovrebbero essere d'accordo su uno standard ma anche un unicode a 16 bit ha ottenuto un piccolo e grande endianness.

Naturalmente anche C avrebbe i suoi problemi, ma sarebbe molto poco influenzato dai problemi sollevati qui.


2
@deemoowoor: Concat: O(m+n)con stringhe nullterm, O(n)tipiche ovunque. Lunghezza O(n)con stringhe nullterm, O(1)ovunque. Join: O(n^2)con stringhe nullterm, O(n)ovunque. Ci sono alcuni casi in cui le stringhe con terminazione null sono più efficienti (cioè basta aggiungerne una al caso puntatore), ma il concat e la lunghezza sono di gran lunga le operazioni più comuni (almeno la lunghezza è necessaria per la formattazione, l'output del file, la visualizzazione della console, ecc.) . Se si memorizza nella cache la lunghezza per ammortizzare, O(n)è sufficiente sottolineare che la lunghezza deve essere memorizzata con la stringa.
Billy ONeal,

1
Sono d'accordo che nel codice di oggi questo tipo di stringa è inefficiente e soggetto a errori, ma ad esempio il display della console non deve davvero conoscere la lunghezza della stringa per visualizzarla in modo efficiente, l'output del file non ha davvero bisogno di conoscere la stringa lunghezza (allocando solo il cluster in movimento), e la formattazione della stringa in questo momento è stata eseguita su una lunghezza fissa della stringa nella maggior parte dei casi. Comunque devi scrivere un codice errato se concateni in C ha una complessità O (n ^ 2), sono abbastanza sicuro di poterne scrivere una in complessità O (n)
dvhh

1
@dvhh: non ho detto n ^ 2 - ho detto m + n - è ancora lineare, ma è necessario cercare la fine della stringa originale per eseguire la concatenazione, mentre con un prefisso di lunghezza non cercare è obbligatorio. (Questa è davvero solo un'altra conseguenza della lunghezza che richiede tempo lineare)
Billy ONeal

1
@Billy ONeal: per pura curiosità ho fatto un colpo grosso sul mio attuale progetto C (circa 50000 righe di codice) per chiamate di funzione di manipolazione di stringhe. strlen 101, strcpy e varianti (strncpy, strlcpy): 85 (ho anche diverse centinaia di stringhe letterali usate per messaggi, copie implicite), strcmp: 56, strcat: 13 (e 6 sono concatenazioni a stringa di lunghezza zero per chiamare strncat) . Sono d'accordo che un prefisso di lunghezza velocizzerà le chiamate a strlen, ma non a strcpy o strcmp (forse se l'API strcmp non utilizza il prefisso comune). La cosa più interessante riguardo ai commenti sopra è che strcat è molto raro.
Kriss,

1
@supercat: non proprio, guarda alcune implementazioni. Le stringhe brevi utilizzano un buffer basato su stack corto (nessuna allocazione di heap) e usano l'heap solo quando diventano più grandi. Ma sentiti libero di fornire una reale implementazione della tua idea come libreria. Di solito i problemi si presentano solo quando arriviamo ai dettagli, non nel design generale.
Kriss,

9

In molti modi, C era primitivo. E l'ho adorato.

È stato un passo avanti rispetto al linguaggio assembly, offrendoti quasi le stesse prestazioni con un linguaggio che era molto più facile da scrivere e mantenere.

Il terminatore null è semplice e non richiede alcun supporto speciale da parte della lingua.

Guardando indietro, non sembra così conveniente. Ma ho usato il linguaggio degli assemblaggi negli anni '80 e all'epoca mi sembrava molto conveniente. Penso solo che il software sia in continua evoluzione e che le piattaforme e gli strumenti diventino sempre più sofisticati.


Non vedo cosa c'è di più primitivo sulle stringhe con terminazione null di ogni altra cosa. Pascal è precedente a C e utilizza il prefisso di lunghezza. Certo, era limitato a 256 caratteri per stringa, ma il semplice utilizzo di un campo a 16 bit avrebbe risolto il problema nella stragrande maggioranza dei casi.
Billy ONeal,

Il fatto che abbia limitato il numero di personaggi è esattamente il tipo di problemi a cui devi pensare quando fai qualcosa del genere. Sì, potresti allungarlo, ma allora contavano i byte. E un campo a 16 bit sarà abbastanza lungo per tutti i casi? Dai, devi ammettere che un null-terminate è concettualmente primitivo.
Jonathan Wood,

10
O si limita la lunghezza della stringa o si limita il contenuto (senza caratteri null) o si accetta l'overhead aggiuntivo di un conteggio compreso tra 4 e 8 byte. Non c'è pranzo libero. Al momento dell'inizio la stringa con terminazione nulla aveva perfettamente senso. In assemblea a volte ho usato la parte superiore di un carattere per segnare la fine di una stringa, salvando anche un altro byte!
Mark Ransom,

Esatto, Mark: non c'è pranzo gratis. È sempre un compromesso. In questi giorni, non è necessario scendere a compromessi con lo stesso tipo. Ma allora, questo approccio sembrava buono come qualsiasi altro.
Jonathan Wood,

8

Supponendo per un momento che C abbia implementato le stringhe nel modo Pascal, prefissandole per lunghezza: una stringa lunga 7 caratteri è lo stesso TIPO DATI di una stringa a 3 caratteri? Se la risposta è sì, quale tipo di codice dovrebbe generare il compilatore quando assegno il primo al secondo? La stringa deve essere troncata o ridimensionata automaticamente? Se ridimensionato, quell'operazione dovrebbe essere protetta da un lucchetto per renderlo sicuro? Il lato approccio C ha intensificato tutti questi problemi, piaccia o no :)


2
Err .. no, non è così. L'approccio C non consente affatto di assegnare la stringa lunga 7 caratteri alla stringa lunga 3 caratteri.
Billy ONeal,

@Billy ONeal: perché no? Per quanto ho capito in questo caso, tutte le stringhe sono dello stesso tipo di dati (char *), quindi la lunghezza non ha importanza. A differenza di Pascal. Ma quello era un limite di Pascal, piuttosto che un problema con stringhe con prefisso di lunghezza.
Oliver Mason,

4
@Billy: Penso che tu abbia appena ribadito il punto di Cristian. C affronta questi problemi non affrontandoli affatto. Stai ancora pensando in termini di C che in realtà contiene una nozione di stringa. È solo un puntatore, quindi puoi assegnarlo a quello che vuoi.
Robert S Ciaccio,

2
È come ** la matrice: "non c'è stringa".
Robert S Ciaccio,

1
@calavera: non vedo come questo provi qualcosa. Puoi risolverlo allo stesso modo con il prefisso di lunghezza ... cioè non consentire affatto l'assegnazione.
Billy ONeal,

8

In qualche modo ho capito che la domanda implica che non c'è supporto del compilatore per stringhe con prefisso di lunghezza in C. L'esempio seguente mostra, almeno puoi avviare la tua libreria di stringhe C, dove le lunghezze delle stringhe vengono contate al momento della compilazione, con un costrutto come questo:

#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;
}

Questo, tuttavia, non comporta alcun problema in quanto è necessario fare attenzione quando liberare specificamente quel puntatore di stringa e quando è allocato staticamente ( chararray letterale ).

Modifica: come risposta più diretta alla domanda, la mia opinione è che questo fosse il modo in cui C poteva supportare sia la lunghezza della stringa disponibile (come costante di tempo di compilazione), se necessario, ma senza sovraccarico di memoria se si desidera utilizzare solo puntatori e terminazione zero.

Ovviamente sembra che lavorare con stringhe a terminazione zero sia stata la pratica consigliata, dal momento che la libreria standard in generale non prende le lunghezze delle stringhe come argomenti, e poiché l'estrazione della lunghezza non è così semplice come char * s = "abc", come mostra il mio esempio.


Il problema è che le biblioteche non conoscono l'esistenza della tua struttura e gestiscono comunque in modo errato cose come null incorporati. Inoltre, questo non risponde davvero alla domanda che ho posto.
Billy ONeal,

1
È vero. Quindi il problema più grande è che non esiste un modo standard migliore per fornire interfacce con parametri di stringa rispetto alle semplici stringhe con terminazione zero. Direi ancora, ci sono librerie che supportano l'alimentazione in coppie lunghezza-puntatore (beh, almeno puoi costruire una stringa std :: string C ++ con loro).
Pyry Jahkola,

2
Anche se memorizzi una lunghezza, non dovresti mai consentire stringhe con valori nulli incorporati. Questo è il buon senso di base. Se i tuoi dati potrebbero contenere valori nulli, non dovresti mai usarli con funzioni che prevedono stringhe.
R .. GitHub smette di aiutare ICE il

1
@supercat: dal punto di vista della sicurezza, accolgo con favore la ridondanza. Altrimenti i programmatori ignoranti (o privati ​​del sonno) finiscono per concatenare dati binari e stringhe e passarli a cose che si aspettano stringhe [null-terminated] ...
R .. GitHub STOP HELPING ICE

1
@R ..: Mentre i metodi che prevedono stringhe con terminazione null generalmente prevedono a char*, molti metodi che non prevedono la terminazione null prevedono anche a char*. Un vantaggio più significativo della separazione dei tipi riguarderebbe il comportamento Unicode. Può essere utile che un'implementazione di una stringa mantenga i flag per sapere se le stringhe contengono determinati tipi di caratteri o se non sono noti per contenerle [ad esempio, trovare il punto di codice 999.990 in una stringa di milioni di caratteri che non è noto contenere tutti i personaggi oltre il piano multilingue di base saranno ordini di grandezza più veloci ...
supercat

6

"Anche su una macchina a 32 bit, se si consente alla stringa di avere la dimensione della memoria disponibile, una stringa con prefisso di lunghezza è solo tre byte più larga di una stringa con terminazione nulla."

Innanzitutto, 3 byte extra possono essere un notevole sovraccarico per stringhe brevi. In particolare, una stringa di lunghezza zero ora occupa 4 volte più memoria. Alcuni di noi utilizzano macchine a 64 bit, quindi abbiamo bisogno di 8 byte per memorizzare una stringa di lunghezza zero, oppure il formato della stringa non può far fronte alle stringhe più lunghe supportate dalla piattaforma.

Potrebbero inoltre esserci problemi di allineamento da affrontare. Supponiamo di avere un blocco di memoria contenente 7 stringhe, come "solo \ 0second \ 0 \ 0four \ 0five \ 0 \ 0seventh". La seconda stringa inizia dall'offset 5. L'hardware potrebbe richiedere che gli interi a 32 bit siano allineati a un indirizzo che è un multiplo di 4, quindi è necessario aggiungere il riempimento, aumentando ulteriormente l'overhead. La rappresentazione C è molto efficiente in termini di memoria in confronto. (L'efficienza della memoria è buona; ad esempio, aiuta le prestazioni della cache.)


Credo di aver affrontato tutto questo nella domanda. Sì, su piattaforme x64 un prefisso a 32 bit non può adattarsi a tutte le stringhe possibili. D'altra parte, non vuoi mai una stringa così grande come una stringa terminata nulla, perché per fare qualsiasi cosa devi esaminare tutti i 4 miliardi di byte per trovare la fine di quasi tutte le operazioni che potresti voler fare. Inoltre, non sto dicendo che le stringhe con terminazione nulla siano sempre malvagie: se stai costruendo una di queste strutture a blocchi e la tua specifica applicazione viene accelerata da quel tipo di costruzione, provaci. Vorrei solo che il comportamento predefinito della lingua non lo facesse.
Billy ONeal,

2
Ho citato quella parte della tua domanda perché, a mio avviso, ha sottovalutato il problema dell'efficienza. Raddoppiare o quadruplicare i requisiti di memoria (rispettivamente su 16 e 32 bit) può essere un costo elevato per le prestazioni. Le stringhe lunghe possono essere lente, ma almeno sono supportate e funzionano ancora. L'altro mio punto, sull'allineamento, non me ne accenni affatto.
Brangdon,

L'allineamento può essere gestito specificando che i valori oltre UCHAR_MAX devono comportarsi come se fossero impacchettati e spacchettati usando gli accessi ai byte e lo spostamento dei bit. Un tipo di stringa progettato in modo adeguato potrebbe offrire efficienza di archiviazione sostanzialmente paragonabile alle stringhe con terminazione zero, consentendo al contempo il controllo dei limiti sui buffer senza sovraccarico di memoria aggiuntivo (utilizzare un bit nel prefisso per dire se un buffer è "pieno"; se non è e l'ultimo byte è diverso da zero, quel byte rappresenterebbe lo spazio rimanente. Se il buffer non è pieno e l'ultimo byte è zero, gli ultimi 256 byte sarebbero inutilizzati, quindi ...
supercat

... si potrebbe memorizzare in quello spazio il numero esatto di byte inutilizzati, con zero costi di memoria aggiuntivi). Il costo di lavorare con i prefissi sarebbe compensato dalla possibilità di usare metodi come fgets () senza dover passare la lunghezza della stringa (poiché i buffer saprebbero quanto fossero grandi).
supercat

4

La terminazione nulla consente operazioni veloci basate su puntatori.


5
Eh? Quali "operazioni di puntatore rapido" non funzionano con il prefisso di lunghezza? Ancora più importante, altre lingue che usano il prefisso di lunghezza sono più veloci della manipolazione di stringhe C wrt.
Billy ONeal,

12
@billy: con le stringhe con prefisso di lunghezza, non puoi semplicemente prendere un puntatore di stringa e aggiungere 4 ad esso, e aspettarti che sia ancora una stringa valida, perché non ha un prefisso di lunghezza (non valido comunque).
Jörgen Sigvardsson,

3
@j_random_hacker: la concatenazione è molto peggio per le stringhe di asciiz (O (m + n) anziché potenzialmente O (n)) e il concat è molto più comune di qualsiasi altra operazione elencata qui.
Billy ONeal,

3
c'è una sola operazione tiiny poco che diventa più costoso con le stringhe null-terminated: strlen. Direi che è un po 'un inconveniente.
jalf

10
@Billy ONeal: anche tutti gli altri supportano regex. E allora ? Usa le librerie per cui sono fatte. C riguarda l'efficienza e il minimalismo massimi, non le batterie incluse. Gli strumenti C consentono inoltre di implementare la stringa con prefisso lunghezza usando le strutture molto facilmente. E nulla ti proibisce di implementare i programmi di manipolazione delle stringhe attraverso la gestione della propria lunghezza e buffer di caratteri. Di solito è quello che faccio quando voglio efficienza e uso C, non chiamare una manciata di funzioni che si aspettano uno zero alla fine di un buffer di caratteri non è un problema.
Kriss,

4

Un punto non ancora menzionato: quando C è stato progettato, c'erano molte macchine in cui un 'char' non era di otto bit (anche oggi ci sono piattaforme DSP dove non lo è). Se si decide che le stringhe devono essere precedute dalla lunghezza, quanti prefissi di lunghezza devono essere usati? L'uso di due imporrebbe un limite artificiale alla lunghezza della stringa per macchine con carattere a 8 bit e spazio di indirizzamento a 32 bit, mentre si sprecare spazio su macchine con carattere a 16 bit e spazio di indirizzamento a 16 bit.

Se si volesse consentire l'archiviazione efficiente di stringhe di lunghezza arbitraria e se 'char' fosse sempre a 8 bit, si potrebbe - per qualche spesa in termini di velocità e dimensioni del codice - definire uno schema con una stringa preceduta da un numero pari N sarebbe lungo N / 2 byte, una stringa preceduta da un valore dispari N e un valore pari M (lettura all'indietro) potrebbe essere ((N-1) + M * char_max) / 2, ecc. E richiedere quel buffer che pretende di offrire una certa quantità di spazio per contenere una stringa deve consentire un numero sufficiente di byte che precedono quello spazio per gestire la lunghezza massima. Il fatto che 'char' non sia sempre 8 bit, tuttavia, complicherebbe un tale schema, poiché il numero di 'char' richiesto per contenere la lunghezza di una stringa varierebbe a seconda dell'architettura della CPU.


Il prefisso potrebbe essere di dimensioni definite dall'implementazione, così come è sizeof(char).
Billy ONeal,

@BillyONeal: sizeof(char)è uno. Sempre. Si potrebbe avere il prefisso come una dimensione definita dall'implementazione, ma sarebbe imbarazzante. Inoltre, non esiste un modo reale di sapere quale dovrebbe essere la dimensione "giusta". Se uno contiene molte stringhe di 4 caratteri, il padding zero imporrebbe un overhead del 25%, mentre un prefisso di quattro byte imporrebbe un overhead del 100%. Inoltre, il tempo impiegato per impacchettare e decomprimere i prefissi di lunghezza di quattro byte potrebbe superare il costo della scansione di stringhe di 4 byte per il byte zero.
supercat

1
Ah sì. Hai ragione. Il prefisso potrebbe facilmente essere qualcosa di diverso da char. Tutto ciò che farebbe funzionare i requisiti di allineamento sulla piattaforma di destinazione andrebbe bene. Non ci andrò comunque, ho già discusso a morte.
Billy ONeal,

Supponendo che le stringhe avessero il prefisso di lunghezza, probabilmente la cosa più sana da fare sarebbe un size_tprefisso (lo spreco di memoria sarebbe dannato, sarebbe il più sano --- consentire stringhe di qualsiasi lunghezza possibile che potrebbero adattarsi alla memoria). In effetti, è una specie di cosa fa D; le matrici sono struct { size_t length; T* ptr; }e le stringhe sono solo matrici di immutable(char).
Tim Čas,

@ TimČas: a meno che le stringhe non debbano essere allineate a parole, il costo di lavorare con stringhe brevi su molte piattaforme sarebbe dominato dall'obbligo di impacchettare e spacchettare la lunghezza; Davvero non lo vedo come pratico. Se si desidera che le stringhe siano array di byte di dimensioni arbitrarie indipendenti dal contenuto, penso che sarebbe meglio mantenere la lunghezza separata dal puntatore ai dati dei caratteri e avere un linguaggio che consenta di ottenere entrambe le informazioni per stringhe letterali .
supercat,

2

Molte decisioni di progettazione che circondano C derivano dal fatto che quando fu implementato originariamente, il passaggio dei parametri era piuttosto costoso. Una scelta tra ad es

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);
}

contro

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);
}

quest'ultimo sarebbe stato leggermente più economico (e quindi preferito) poiché richiedeva solo il passaggio di un parametro anziché di due. Se il metodo chiamato non avesse bisogno di conoscere l'indirizzo di base dell'array né l'indice al suo interno, passare un singolo puntatore combinando i due sarebbe più economico che passare i valori separatamente.

Mentre ci sono molti modi ragionevoli in cui C avrebbe potuto codificare le lunghezze delle stringhe, gli approcci inventati fino a quel momento avrebbero avuto tutte le funzioni necessarie che dovrebbero essere in grado di lavorare con parte di una stringa per accettare l'indirizzo di base della stringa e l'indice desiderato come due parametri separati. L'uso della terminazione a zero byte ha permesso di evitare tale requisito. Sebbene altri approcci sarebbero migliori con le macchine di oggi (i compilatori moderni spesso passano i parametri nei registri e memcpy può essere ottimizzato in modo strcpy () - gli equivalenti non possono) un codice di produzione sufficiente utilizza stringhe terminate a zero byte che è difficile passare a nient'altro.

PS - In cambio di una leggera penalità di velocità su alcune operazioni e di un piccolo sovraccarico extra su stringhe più lunghe, sarebbe stato possibile avere metodi che funzionano con le stringhe accettare i puntatori direttamente su stringhe, buffer di stringhe controllati da limiti o strutture dati che identificano sottostringhe di un'altra stringa. Una funzione come "strcat" sarebbe assomigliata a [sintassi moderna]

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);
  }
}

Un po 'più grande del metodo K&R strcat, ma supporterebbe il controllo dei limiti, cosa che il metodo K&R non ha. Inoltre, a differenza del metodo attuale, sarebbe possibile concatenare facilmente una sottostringa arbitraria, ad es

/* 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);
}

Nota che la durata della stringa restituita da temp_substring sarebbe limitata da quella di se src, che è sempre stata più breve (motivo per cui il metodo richiede infdi essere passato - se fosse locale, morirebbe quando il metodo restituito).

In termini di costo della memoria, stringhe e buffer fino a 64 byte avrebbero un byte di sovraccarico (uguale alle stringhe con terminazione zero); stringhe più lunghe avrebbero leggermente più (se si consentissero quantità di overhead tra due byte e il massimo richiesto sarebbe un compromesso tempo / spazio). Un valore speciale del byte lunghezza / modalità verrebbe utilizzato per indicare che a una funzione stringa è stata assegnata una struttura contenente un byte flag, un puntatore e una lunghezza del buffer (che potrebbe quindi indicizzarsi arbitrariamente in qualsiasi altra stringa).

Naturalmente, K&R non ha implementato nulla del genere, ma è molto probabile perché non volevano spendere molto per la gestione delle stringhe, un'area in cui anche oggi molte lingue sembrano piuttosto anemiche.


Non c'è nulla che avrebbe impedito char* arrdi puntare a una struttura del modulo struct { int length; char characters[ANYSIZE_ARRAY] };o simile che sarebbe comunque passabile come singolo parametro.
Billy ONeal

@BillyONeal: Due problemi con questo approccio: (1) Consentirebbe solo di passare la stringa nel suo insieme, mentre l'attuale approccio consente anche di passare la coda di una stringa; (2) sprecherà spazio significativo se utilizzato con stringhe di piccole dimensioni. Se K&R avesse voluto passare un po 'di tempo sugli archi, avrebbero potuto rendere le cose molto più robuste, ma non penso che intendessero che il loro nuovo linguaggio sarebbe stato utilizzato dieci anni dopo, molto meno quaranta.
supercat

1
Questo pezzettino sulla convention di chiamata è una storia così-così senza relazione con la realtà ... non è stata una considerazione nel design. E le convenzioni di chiamata basate sul registro erano già state "inventate". Inoltre, approcci come due puntatori non erano un'opzione perché le strutture non erano di prima classe ... solo le primitive erano assegnabili o passabili; la copia di struct non è arrivata fino a UNIX V7. Avere bisogno di memcpy (che non esisteva) solo per copiare un puntatore di stringa è uno scherzo. Prova a scrivere un programma completo, non solo funzioni isolate, se stai facendo finta di progettare il linguaggio.
Jim Balter

1
"è molto probabile perché non volevano spendere molto per la gestione delle stringhe" - sciocchezze; l'intero dominio dell'applicazione dei primi UNIX era la gestione delle stringhe. Se non fosse stato per quello, non ne avremmo mai sentito parlare.
Jim Balter

1
"Non credo che" il buffer dei caratteri inizi con un int che contiene la lunghezza "è più magico", è se farai str[n]riferimento al carattere giusto. Queste sono le cose a cui la gente che discute di questo non pensa .
Jim Balter,

2

Secondo Joel Spolsky in questo post del blog ,

È perché il microprocessore PDP-7, su cui sono stati inventati UNIX e il linguaggio di programmazione C, aveva un tipo di stringa ASCIZ. ASCIZ significava "ASCII con una Z (zero) alla fine".

Dopo aver visto tutte le altre risposte qui, sono convinto che anche se questo è vero, è solo una parte del motivo per cui C ha "stringhe" con terminazione nulla. Quel post è abbastanza illuminante su come cose semplici come le stringhe possano effettivamente essere piuttosto difficili.


2
Senti, rispetto Joel per molte cose; ma questo è qualcosa su cui sta speculando. La risposta di Hans Passant arriva direttamente dagli inventori di C.
Billy ONeal,

1
Sì, ma se ciò che dice Spolsky è vero, allora sarebbe stato parte della "convenienza" a cui si riferivano. Questo è in parte il motivo per cui ho incluso questa risposta.
BenK,

AFAIK .ASCIZera solo un'istruzione assembler per costruire una sequenza di byte, seguita da 0. Significa solo che la stringa con terminazione zero era un concetto ben definito in quel momento. Ciò non significa che le stringhe con terminazione zero fossero qualcosa correlato all'architettura di un PDP- *, tranne per il fatto che si potevano scrivere loop stretti costituiti da MOVB(copia di un byte) e BNE(branch se l'ultimo byte copiato non era zero).
Adrian W,

Suppone di mostrare che C è un linguaggio vecchio, flaccido, decrepito.
Purec,

2

Non necessariamente una logica ma un contrappunto alla lunghezza codificata

  1. Alcune forme di codifica di lunghezza dinamica sono superiori alla codifica di lunghezza statica per quanto riguarda la memoria, tutto dipende dall'uso. Basta guardare UTF-8 per la prova. È essenzialmente un array di caratteri estensibile per codificare un singolo carattere. Questo utilizza un singolo bit per ogni byte esteso. La terminazione NUL utilizza 8 bit. Penso che il prefisso di lunghezza possa essere ragionevolmente definito lunghezza infinita anche usando 64 bit. Quante volte colpisci il caso dei tuoi bit extra è il fattore decisivo. Solo 1 stringa estremamente grande? A chi importa se stai usando 8 o 64 bit? Molte stringhe piccole (ovvero stringhe di parole inglesi)? Quindi i costi del prefisso sono una grande percentuale.

  2. Le stringhe con prefisso di lunghezza che consentono di risparmiare tempo non sono una cosa reale . Se è necessario fornire la lunghezza dei dati forniti, stai contando in fase di compilazione o ti vengono effettivamente forniti dati dinamici che devi codificare come stringa. Queste dimensioni sono calcolate ad un certo punto dell'algoritmo. È possibile fornire una variabile separata per memorizzare la dimensione di una stringa terminata nulla . Il che rende il confronto discutibile sul risparmio di tempo. Uno ha solo un NUL extra alla fine ... ma se la codifica della lunghezza non include quel NUL, non c'è letteralmente alcuna differenza tra i due. Non è richiesto alcun cambiamento algoritmico. Solo un pre-pass devi progettare manualmente te stesso invece che un compilatore / runtime lo faccia per te. C si occupa principalmente di fare le cose manualmente.

  3. Il prefisso di lunghezza essendo facoltativo è un punto di forza. Non ho sempre bisogno di quelle informazioni extra per un algoritmo, quindi essere obbligato a farlo per ogni stringa rende il mio pre-calcolo + tempo di calcolo mai in grado di scendere sotto O (n). (Vale a dire il generatore di numeri casuali hardware 1-128. Posso estrarre da una "stringa infinita". Diciamo che genera caratteri così velocemente. Quindi la lunghezza della nostra stringa cambia continuamente. Ma probabilmente il mio uso dei dati non mi interessa come ho molti byte casuali. Vuole solo il prossimo byte non utilizzato disponibile non appena può ottenerlo dopo una richiesta. Potrei essere in attesa sul dispositivo. Ma potrei anche avere un buffer di caratteri pre-letti. Un confronto di lunghezza è inutile spreco di calcolo. Un controllo nullo è più efficiente.)

  4. Il prefisso lunghezza è una buona protezione contro l'overflow del buffer? Lo stesso è un uso razionale delle funzioni e dell'implementazione delle librerie. Cosa succede se invio dati non validi? Il mio buffer è lungo 2 byte ma dico alla funzione che è 7! Esempio: se get () doveva essere utilizzato su dati noti, avrebbe potuto avere un controllo del buffer interno che testava i buffer compilati e malloc ()chiama e continua a seguire le specifiche. Se doveva essere usato come pipe per STDIN sconosciuto per arrivare a buffer sconosciuto, allora chiaramente non si può sapere circa la dimensione del buffer, il che significa che una lunghezza arg è inutile, qui è necessario qualcos'altro come un controllo canarino. Del resto, non è possibile aggiungere un prefisso di lunghezza ad alcuni stream e input, semplicemente non è possibile. Ciò significa che il controllo della lunghezza deve essere integrato nell'algoritmo e non una parte magica del sistema di digitazione. TL; DR NUL-terminato non ha mai dovuto essere pericoloso, ma è finito in quel modo per uso improprio.

  5. counter-counter point: la terminazione NUL è fastidiosa per il binario. O è necessario fare qui il prefisso di lunghezza o trasformare i byte NUL in qualche modo: codici di escape, rimappatura di intervallo, ecc ... che ovviamente significa più utilizzo della memoria / informazioni ridotte / più operazioni per byte. Il prefisso di lunghezza vince principalmente la guerra qui. L'unico lato positivo di una trasformazione è che non è necessario scrivere funzioni aggiuntive per coprire le stringhe del prefisso lunghezza. Ciò significa che nelle routine sub-O (n) più ottimizzate puoi farle agire automaticamente come equivalenti O (n) senza aggiungere altro codice. Il rovescio della medaglia è, ovviamente, il tempo / memoria / spreco di compressione se utilizzato su stringhe pesanti NUL.A seconda della quantità della tua libreria che finisci per duplicare per operare su dati binari, può avere senso lavorare esclusivamente con stringhe con prefisso di lunghezza. Detto questo, si potrebbe fare lo stesso anche con le stringhe con prefisso di lunghezza ... -1 lunghezza potrebbe significare NUL-terminato e si potrebbero usare le stringhe con terminazione NUL all'interno della terminazione-lunghezza.

  6. Concat: "O (n + m) vs O (m)" Suppongo che ti riferirai a m come lunghezza totale della stringa dopo il concatenamento perché entrambi devono avere quel numero minimo di operazioni (non puoi semplicemente virare -sulla stringa 1, cosa succede se è necessario riallocare?). E suppongo che n sia una quantità mitica di operazioni che non devi più fare a causa di un pre-calcolo. In tal caso, la risposta è semplice: pre-calcolo. Sestai insistendo che avrai sempre abbastanza memoria per non dover riallocare e questa è la base della notazione big-O quindi la risposta è ancora più semplice: fai una ricerca binaria sulla memoria allocata per la fine della stringa 1, chiaramente c'è un grande campione di zero infiniti dopo la stringa 1 per non preoccuparci di realloc. Lì, ho ottenuto n facilmente per accedere (n) e ci ho provato a malapena. Che se si richiama log (n) è essenzialmente sempre solo 64 su un computer reale, che è essenzialmente come dire O (64 + m), che è essenzialmente O (m). (E sì, quella logica è stata usata nell'analisi di runtime di strutture dati reali in uso oggi. Non è una cazzata fuori dalla testa.)

  7. Concat () / Len () di nuovo : Memorizza i risultati. Facile. Trasforma tutti i calcoli in pre-calcoli, se possibile / necessario. Questa è una decisione algoritmica. Non è un vincolo obbligatorio della lingua.

  8. Il passaggio del suffisso della stringa è più semplice / possibile con la terminazione NUL. A seconda di come viene implementato il prefisso length, può essere distruttivo sulla stringa originale e talvolta non è nemmeno possibile. Richiede una copia e passa O (n) invece di O (1).

  9. Il passaggio dell'argomento / de-referencing è minore per il prefisso NUL rispetto al prefisso di lunghezza. Ovviamente perché stai passando meno informazioni. Se non hai bisogno di lunghezza, questo consente di risparmiare molta impronta e consente ottimizzazioni.

  10. Puoi imbrogliare. È davvero solo un puntatore. Chi dice che devi leggerlo come una stringa? Cosa succede se si desidera leggerlo come un singolo personaggio o un float? Cosa succede se si desidera fare il contrario e leggere un float come una stringa? Se stai attento puoi farlo con NUL-termination. Non puoi farlo con il prefisso lunghezza, in genere è un tipo di dati nettamente diverso da un puntatore. Molto probabilmente dovresti costruire una stringa byte per byte e ottenere la lunghezza. Ovviamente se volevi qualcosa come un intero float (probabilmente ha un NUL al suo interno) dovresti leggere comunque byte per byte, ma i dettagli sono lasciati a te per decidere.

TL; DR Stai utilizzando i dati binari? In caso negativo, la terminazione NUL consente una maggiore libertà algoritmica. Se sì, allora la quantità di codice rispetto a velocità / memoria / compressione è la tua preoccupazione principale. Una combinazione di due approcci o memoization potrebbe essere la migliore.


9 era un po 'off-base / mal rappresentato. La lunghezza pre-correzione non presenta questo problema. Passa l'undicesimo passaggio come variabile separata. Stavamo parlando di pre-fiix ma mi sono lasciato trasportare. Ancora una buona cosa a cui pensare, quindi lo lascerò lì. : d
Nero,

1

Non compro la risposta "C non ha stringhe". È vero, C non supporta tipi di livello superiore incorporati, ma puoi comunque rappresentare strutture di dati in C ed è quello che è una stringa. Il fatto che una stringa sia solo un puntatore in C non significa che i primi N byte non possano assumere un significato speciale come lunghezza.

Gli sviluppatori Windows / COM avranno molta familiarità con il BSTRtipo esattamente simile a questo: una stringa C con prefisso di lunghezza in cui i dati dei caratteri effettivi non iniziano al byte 0.

Quindi sembra che la decisione di usare la terminazione nulla sia semplicemente ciò che le persone preferiscono, non una necessità della lingua.


-3

gcc accetta i seguenti codici:

char s [4] = "abcd";

ed è ok se trattiamo è una matrice di caratteri ma non una stringa. Cioè, possiamo accedervi con s [0], s [1], s [2] e s [3], o anche con memcpy (dest, s, 4). Ma avremo personaggi disordinati quando proveremo con put (s), o peggio con strcpy (dest, s).


@Adrian W. Questo è valido. Le stringhe di lunghezza esatta sono in un involucro speciale e NUL è omesso per esse. Questa è generalmente una pratica poco saggia ma può essere utile in casi come il popolamento di strutture di intestazione che usano "stringhe" di FourCC.
Kevin Thibedeau,

Hai ragione. Questo è C valido, compilerà e si comporterà come descritto nella kkaaii. La ragione del downvotes (non mio ...) è probabilmente piuttosto che questa risposta non risponde in alcun modo alla domanda di OP.
Adrian W,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.