Un argomento free(void *)
(introdotto in Unix V7) ha un altro grande vantaggio rispetto ai due argomenti precedenti mfree(void *, size_t)
che non ho visto menzionato qui: un argomento free
semplifica notevolmente ogni altra API che funziona con la memoria heap. Ad esempio, se fosse free
necessaria la dimensione del blocco di memoria, in strdup
qualche modo dovrebbe restituire due valori (puntatore + dimensione) invece di uno (puntatore) e C rende i rendimenti a più valori molto più ingombranti dei rendimenti a valore singolo. Invece di char *strdup(char *)
dovremmo scrivere char *strdup(char *, size_t *)
o altro struct CharPWithSize { char *val; size_t size}; CharPWithSize strdup(char *)
. (Al giorno d'oggi questa seconda opzione sembra piuttosto allettante, perché sappiamo che le stringhe con terminazione NUL sono "il bug di progettazione più catastrofico nella storia dell'informatica", ma questo è il senno di poi. Negli anni '70, la capacità del C di gestire le stringhe come un semplice char *
era in realtà considerata un vantaggio decisivo rispetto a concorrenti come Pascal e Algol .) Inoltre, non è solo strdup
che soffre di questo problema: colpisce ogni sistema o definito dall'utente funzione che alloca memoria heap.
I primi progettisti di Unix erano persone molto intelligenti e ci sono molte ragioni per cui free
è meglio di mfree
così fondamentalmente penso che la risposta alla domanda sia che hanno notato questo e hanno progettato il loro sistema di conseguenza. Dubito che troverai una registrazione diretta di ciò che stava accadendo nelle loro teste nel momento in cui hanno preso quella decisione. Ma possiamo immaginare.
Immagina di scrivere applicazioni in C da eseguire su Unix V6, con i suoi due argomenti mfree
. Finora sei riuscito bene, ma tenere traccia di queste dimensioni dei puntatori sta diventando sempre più una seccatura poiché i tuoi programmi diventano più ambiziosi e richiedono un uso sempre maggiore di variabili allocate nell'heap. Ma poi hai un'idea brillante: invece di copiarli size_t
tutto il tempo, puoi semplicemente scrivere alcune funzioni di utilità, che nascondono la dimensione direttamente all'interno della memoria allocata:
void *my_alloc(size_t size) {
void *block = malloc(sizeof(size) + size);
*(size_t *)block = size;
return (void *) ((size_t *)block + 1);
}
void my_free(void *block) {
block = (size_t *)block - 1;
mfree(block, *(size_t *)block);
}
E più codice scrivi usando queste nuove funzioni, più sembrano fantastiche. Non solo rendono il tuo codice più facile da scrivere, ma rendono anche il tuo codice più veloce - due cose che spesso non vanno insieme! Prima di passare questi messaggi size_t
dappertutto, il che aggiungeva il sovraccarico della CPU per la copia e significava che dovevi spargere i registri più spesso (specialmente per gli argomenti della funzione extra) e sprecare memoria (poiché le chiamate di funzione nidificate spesso risulteranno in più copie della size_t
memorizzazione in diversi stack frame). Nel nuovo sistema, devi ancora spendere la memoria per archiviare il filesize_t
, ma solo una volta e non viene mai copiato da nessuna parte. Questi possono sembrare piccoli vantaggi, ma tieni presente che stiamo parlando di macchine di fascia alta con 256 KiB di RAM.
Questo ti rende felice! Quindi condividi il tuo fantastico trucco con gli uomini barbuti che stanno lavorando alla prossima versione di Unix, ma non li rende felici, li rende tristi. Vedi, stavano solo aggiungendo un mucchio di nuove funzioni di utilità come strdup
, e si rendono conto che le persone che usano il tuo fantastico trucco non saranno in grado di usare le loro nuove funzioni, perché le loro nuove funzioni usano tutte l'ingombrante puntatore + dimensione API. E poi questo ti rattrista anche tu, perché ti rendi conto che dovrai riscrivere la buona strdup(char *)
funzione da solo in ogni programma che scrivi, invece di poter usare la versione di sistema.
Ma aspetta! Siamo nel 1977 e la retrocompatibilità non verrà inventata per altri 5 anni! E inoltre, nessuno serio usa questa oscura cosa "Unix" con il suo nome off-color. La prima edizione di K&R è in arrivo per l'editore ora, ma non c'è problema - dice proprio sulla prima pagina che "C non fornisce operazioni per trattare direttamente oggetti compositi come stringhe di caratteri ... non c'è mucchio ... ". A questo punto della storia, string.h
e malloc
sono estensioni del fornitore (!). Quindi, suggerisce Bearded Man # 1, possiamo cambiarli come preferiamo; perché non dichiariamo semplicemente che il tuo difficile allocatore è l' allocatore ufficiale ?
Pochi giorni dopo, Bearded Man # 2 vede la nuova API e dice ehi, aspetta, questo è meglio di prima, ma sta ancora spendendo un'intera parola per allocazione per memorizzare le dimensioni. La considera la cosa successiva alla blasfemia. Tutti gli altri lo guardano come se fosse pazzo, perché cos'altro puoi fare? Quella notte rimane in ritardo e inventa un nuovo allocatore che non memorizza affatto la dimensione, ma invece la deduce al volo eseguendo bitshift di magia nera sul valore del puntatore e lo scambia mantenendo la nuova API in posizione. La nuova API significa che nessuno si accorge del passaggio, ma notano che la mattina dopo il compilatore utilizza il 10% in meno di RAM.
E ora tutti sono felici: ottieni il tuo codice più facile da scrivere e più veloce, Bearded Man # 1 può scrivere un bel semplice strdup
che le persone useranno davvero e Bearded Man # 2 - fiducioso di essersi guadagnato il suo mantenimento per un po '- - torna a scherzare con i quines . Spediscilo!
O almeno, è così che sarebbe potuto accadere.