Il vero motivo si riduce a una differenza fondamentale nell'intento tra C e C ++ da un lato, e Java e C # (solo per un paio di esempi) dall'altro. Per ragioni storiche, gran parte della discussione qui parla di C piuttosto che di C ++, ma (come probabilmente già saprai) C ++ è un discendente abbastanza diretto di C, quindi ciò che dice di C si applica ugualmente a C ++.
Sebbene siano in gran parte dimenticati (e la loro esistenza a volte addirittura negata), le primissime versioni di UNIX sono state scritte in linguaggio assembly. Gran parte (se non esclusivamente) lo scopo originale di C era il port UNIX dal linguaggio assembly a un linguaggio di livello superiore. Parte dell'intento era scrivere il più possibile del sistema operativo in un linguaggio di livello superiore - o guardarlo dall'altra direzione, per ridurre al minimo la quantità che doveva essere scritta in linguaggio assembly.
A tale scopo, C doveva fornire quasi lo stesso livello di accesso all'hardware del linguaggio assembly. Il PDP-11 (per un esempio) ha mappato i registri I / O su indirizzi specifici. Ad esempio, avresti letto una posizione di memoria per verificare se era stato premuto un tasto sulla console di sistema. È stato impostato un bit in quella posizione quando c'erano dati in attesa di essere letti. Quindi leggere un byte da un'altra posizione specificata per recuperare il codice ASCII del tasto che era stato premuto.
Allo stesso modo, se si desidera stampare alcuni dati, è necessario controllare un'altra posizione specificata e quando il dispositivo di output era pronto, scrivere i dati ancora un'altra posizione specificata.
Per supportare la scrittura di driver per tali dispositivi, C ha permesso di specificare una posizione arbitraria utilizzando un tipo intero, convertirlo in un puntatore e leggere o scrivere quella posizione in memoria.
Naturalmente, questo ha un problema piuttosto grave: non tutte le macchine sulla terra hanno la sua memoria identica a un PDP-11 dei primi anni '70. Quindi, quando prendi quel numero intero, lo converti in un puntatore e poi leggi o scrivi tramite quel puntatore, nessuno può fornire alcuna ragionevole garanzia su ciò che otterrai. Solo per un esempio ovvio, la lettura e la scrittura possono essere associate a registri separati nell'hardware, quindi tu (contrariamente alla memoria normale) se scrivi qualcosa, quindi prova a rileggerlo, ciò che leggi potrebbe non corrispondere a ciò che hai scritto.
Vedo alcune possibilità che lascia:
- Definisci un'interfaccia per tutto l'hardware possibile: specifica gli indirizzi assoluti di tutte le posizioni che potresti voler leggere o scrivere per interagire con l'hardware in qualsiasi modo.
- Proibire quel livello di accesso e decretare che chiunque voglia fare tali cose deve usare il linguaggio assembly.
- Consenti alle persone di farlo, ma lascia loro la possibilità di leggere (ad esempio) i manuali per l'hardware a cui sono destinati e di scrivere il codice per adattarlo all'hardware che stanno utilizzando.
Di questi, 1 sembra sufficientemente assurdo da non meritare ulteriori discussioni. 2 sta praticamente eliminando l'intento di base della lingua. Ciò lascia la terza opzione come essenzialmente l'unica che potrebbero ragionevolmente considerare.
Un altro punto che emerge abbastanza frequentemente sono le dimensioni dei tipi interi. C prende la "posizione" che int
dovrebbe essere la dimensione naturale suggerita dall'architettura. Quindi, se sto programmando un VAX a 32 bit, int
probabilmente dovrebbe essere 32 bit, ma se sto programmando un Univac a 36 bit, int
probabilmente dovrebbe essere 36 bit (e così via). Probabilmente non è ragionevole (e potrebbe anche non essere possibile) scrivere un sistema operativo per un computer a 36 bit utilizzando solo tipi di dimensioni garantite di multipli di 8 bit. Forse sono solo superficiale, ma mi sembra che se stavo scrivendo un sistema operativo per una macchina a 36 bit, probabilmente avrei voluto usare un linguaggio che supportasse un tipo a 36 bit.
Da un punto di vista linguistico, questo porta a comportamenti ancora più indefiniti. Se prendo il valore più grande che si adatta a 32 bit, cosa accadrà quando aggiungo 1? Sull'hardware tipico a 32 bit, verrà eseguito il roll over (o eventualmente gettato una sorta di errore hardware). D'altra parte, se è in esecuzione su hardware a 36 bit, semplicemente ... ne aggiungerà uno. Se la lingua supporterà la scrittura di sistemi operativi, non puoi garantire nessuno dei due comportamenti: devi solo consentire che le dimensioni dei tipi e il comportamento dell'overflow possano variare l'uno dall'altro.
Java e C # possono ignorare tutto ciò. Non intendono supportare la scrittura di sistemi operativi. Con loro, hai un paio di scelte. Uno è quello di fare in modo che l'hardware supporti ciò di cui hanno bisogno, poiché richiedono tipi da 8, 16, 32 e 64 bit, basta creare hardware che supporti quelle dimensioni. L'altra ovvia possibilità è che la lingua venga eseguita solo su altri software che forniscono l'ambiente che desiderano, indipendentemente da ciò che l'hardware sottostante potrebbe desiderare.
Nella maggior parte dei casi, questa non è davvero una scelta o / o. Piuttosto, molte implementazioni fanno un po 'di entrambi. Normalmente si esegue Java su una JVM in esecuzione su un sistema operativo. Più spesso, il sistema operativo è scritto in C e la JVM in C ++. Se la JVM è in esecuzione su una CPU ARM, è abbastanza probabile che la CPU includa le estensioni Jazelle di ARM, per adattare l'hardware più vicino alle esigenze di Java, quindi è necessario fare meno nel software e il codice Java funziona più velocemente (o meno lentamente, comunque).
Sommario
C e C ++ hanno un comportamento indefinito, perché nessuno ha definito un'alternativa accettabile che permetta loro di fare ciò che intendono fare. C # e Java adottano un approccio diverso, ma tale approccio si adatta male (se non del tutto) agli obiettivi di C e C ++. In particolare, nessuno dei due sembra fornire un modo ragionevole per scrivere software di sistema (come un sistema operativo) sull'hardware scelto arbitrariamente. Entrambi in genere dipendono dalle funzionalità fornite dal software di sistema esistente (solitamente scritto in C o C ++) per svolgere il proprio lavoro.