Dopo tutti questi anni, il mondo è ancora alimentato dalla programmazione C

Pubblicato: 2022-03-11

Molti dei progetti C che esistono oggi sono stati avviati decenni fa.

Lo sviluppo del sistema operativo UNIX è iniziato nel 1969 e il suo codice è stato riscritto in C nel 1972. Il linguaggio C è stato effettivamente creato per spostare il codice del kernel UNIX dall'assembly a un linguaggio di livello superiore, che avrebbe svolto le stesse attività con meno righe di codice .

Lo sviluppo del database Oracle è iniziato nel 1977 e il suo codice è stato riscritto da assembly a C nel 1983. È diventato uno dei database più popolari al mondo.

Nel 1985 è stato rilasciato Windows 1.0. Sebbene il codice sorgente di Windows non sia disponibile pubblicamente, è stato affermato che il suo kernel è principalmente scritto in C, con alcune parti in assembly. Lo sviluppo del kernel Linux è iniziato nel 1991 ed è anche scritto in C. L'anno successivo è stato rilasciato sotto licenza GNU ed è stato utilizzato come parte del sistema operativo GNU. Lo stesso sistema operativo GNU è stato avviato utilizzando i linguaggi di programmazione C e Lisp, quindi molti dei suoi componenti sono scritti in C.

Ma la programmazione in C non si limita a progetti iniziati decenni fa, quando non c'erano tanti linguaggi di programmazione come oggi. Molti progetti C sono ancora avviati oggi; ci sono delle buone ragioni per questo.

Come è il mondo alimentato da C?

Nonostante la prevalenza di lingue di livello superiore, C continua a rafforzare il mondo. Di seguito sono riportati alcuni dei sistemi utilizzati da milioni di persone e programmati in linguaggio C.

Microsoft Windows

Il kernel Windows di Microsoft è sviluppato principalmente in C, con alcune parti in linguaggio assembly. Per decenni, il sistema operativo più utilizzato al mondo, con circa il 90% della quota di mercato, è stato alimentato da un kernel scritto in C.

Linux

Anche Linux è scritto principalmente in C, con alcune parti in assembly. Circa il 97% dei 500 supercomputer più potenti del mondo esegue il kernel Linux. Viene utilizzato anche in molti personal computer.

Mac

Anche i computer Mac sono basati su C, poiché il kernel OS X è scritto principalmente in C. Ogni programma e driver in un Mac, come nei computer Windows e Linux, è in esecuzione su un kernel basato su C.

Mobile

Anche i kernel iOS, Android e Windows Phone sono scritti in C. Sono solo adattamenti mobili di kernel Mac OS, Linux e Windows esistenti. Quindi gli smartphone che usi ogni giorno funzionano con un kernel C.

Kernel del sistema operativo scritto in C

Banche dati

I database più popolari al mondo, inclusi Oracle Database, MySQL, MS SQL Server e PostgreSQL, sono codificati in C (i primi tre in realtà sia in C che in C++).

I database sono utilizzati in tutti i tipi di sistemi: finanziari, governativi, media, intrattenimento, telecomunicazioni, salute, istruzione, vendita al dettaglio, social network, web e simili.

Database Powered by C

Film in 3D

I filmati 3D vengono creati con applicazioni generalmente scritte in C e C++. Tali applicazioni devono essere molto efficienti e veloci, poiché gestiscono un'enorme quantità di dati ed eseguono molti calcoli al secondo. Più sono efficienti, meno tempo impiegano gli artisti e gli animatori per generare le riprese del film e più denaro risparmia l'azienda.

Sistemi integrati

Immagina di svegliarti un giorno e andare a fare shopping. La sveglia che ti sveglia è probabilmente programmata in C. Quindi usi il microonde o la macchinetta del caffè per preparare la colazione. Sono anche sistemi embedded e quindi sono probabilmente programmati in C. Accendi la TV o la radio mentre fai colazione. Anche quelli sono sistemi embedded, alimentati da C. Quando apri la porta del tuo garage con il telecomando, stai usando anche un sistema embedded che molto probabilmente è programmato in C.

Poi sali in macchina. Se ha le seguenti caratteristiche, programmate anche in C:

  • trasmissione automatica
  • sistemi di rilevamento della pressione dei pneumatici
  • sensori (ossigeno, temperatura, livello olio, ecc.)
  • memoria per regolazioni sedili e specchietti.
  • visualizzazione del cruscotto
  • freni antibloccaggio
  • controllo automatico della stabilità
  • regolazione automatica della velocità
  • controllo climatico
  • serrature a prova di bambino
  • entrata senza chiave
  • sedili riscaldati
  • controllo dell'airbag

Vai al negozio, parcheggi la macchina e vai a un distributore automatico per prendere una bibita. Che lingua hanno usato per programmare questo distributore automatico? Probabilmente C. Poi compri qualcosa al negozio. In C è programmato anche il registratore di cassa. E quando si paga con la carta di credito? Avete indovinato: il lettore di carte di credito è, ancora, probabilmente programmato in C.

Tutti questi dispositivi sono sistemi embedded. Sono come piccoli computer che hanno un microcontrollore/microprocessore all'interno che esegue un programma, chiamato anche firmware, su dispositivi embedded. Tale programma deve rilevare la pressione dei tasti e agire di conseguenza, nonché visualizzare informazioni all'utente. Ad esempio, la sveglia deve interagire con l'utente, rilevando quale pulsante l'utente sta premendo e, talvolta, per quanto tempo viene premuto, e programmare il dispositivo di conseguenza, il tutto visualizzando all'utente le informazioni rilevanti. Il sistema antibloccaggio dell'auto, ad esempio, deve essere in grado di rilevare il bloccaggio improvviso degli pneumatici e agire per rilasciare la pressione sui freni per un breve periodo di tempo, sbloccandoli, e quindi impedendo lo sbandamento incontrollato. Tutti questi calcoli vengono eseguiti da un sistema integrato programmato.

Sebbene il linguaggio di programmazione utilizzato sui sistemi embedded possa variare da marca a marca, sono più comunemente programmati nel linguaggio C, a causa delle caratteristiche del linguaggio di flessibilità, efficienza, prestazioni e vicinanza all'hardware.

I sistemi incorporati sono spesso scritti in C

Perché il linguaggio di programmazione C è ancora utilizzato?

Ci sono molti linguaggi di programmazione, oggi, che consentono agli sviluppatori di essere più produttivi rispetto al C per diversi tipi di progetti. Esistono linguaggi di livello superiore che forniscono librerie integrate molto più grandi che semplificano il lavoro con JSON, XML, UI, pagine Web, richieste client, connessioni al database, manipolazione dei media e così via.

Ma nonostante ciò, ci sono molte ragioni per credere che la programmazione C rimarrà attiva per molto tempo.

Nei linguaggi di programmazione una taglia non va bene per tutti. Ecco alcuni motivi per cui C è imbattibile e quasi obbligatorio per determinate applicazioni.

Portabilità ed efficienza

C è quasi un linguaggio assembly portatile . È il più vicino possibile alla macchina mentre è quasi universalmente disponibile per le architetture di processori esistenti. C'è almeno un compilatore C per quasi ogni architettura esistente. E al giorno d'oggi, a causa dei binari altamente ottimizzati generati dai moderni compilatori, non è un compito facile migliorare il loro output con l'assembly scritto a mano.

Tale è la sua portabilità ed efficienza che "compilatori, librerie e interpreti di altri linguaggi di programmazione sono spesso implementati in C". Linguaggi interpretati come Python, Ruby e PHP hanno le loro implementazioni primarie scritte in C. Viene persino utilizzato dai compilatori per altri linguaggi per comunicare con la macchina. Ad esempio, C è il linguaggio intermedio alla base di Eiffel e Forth. Ciò significa che, invece di generare codice macchina per ogni architettura da supportare, i compilatori per quei linguaggi generano semplicemente codice C intermedio e il compilatore C gestisce la generazione del codice macchina.

Il C è anche diventato una lingua franca per la comunicazione tra sviluppatori. Come dice Alex Allain, Dropbox Engineering Manager e creatore di Cprogramming.com:

Il C è un ottimo linguaggio per esprimere idee comuni nella programmazione in un modo con cui la maggior parte delle persone si sente a proprio agio. Inoltre, molti dei principi utilizzati in C, ad esempio argc e argv per i parametri della riga di comando, nonché costrutti di ciclo e tipi di variabili, verranno visualizzati in molte altre lingue che imparerai, quindi sarai in grado di parlare alle persone anche se non conoscono C in un modo che è comune a entrambi.

Manipolazione della memoria

L'accesso arbitrario all'indirizzo di memoria e l'aritmetica del puntatore sono una caratteristica importante che rende C una soluzione perfetta per la programmazione di sistema (sistemi operativi e sistemi embedded).

Al confine tra hardware e software, i sistemi informatici ei microcontrollori mappano le loro periferiche e i pin di I/O in indirizzi di memoria. Le applicazioni di sistema devono leggere e scrivere in quelle posizioni di memoria personalizzate per comunicare con il mondo. Quindi la capacità di C di manipolare indirizzi di memoria arbitrari è fondamentale per la programmazione del sistema.

Un microcontrollore potrebbe essere progettato, ad esempio, in modo tale che il byte nell'indirizzo di memoria 0x40008000 venga inviato dal ricevitore/trasmettitore asincrono universale (o UART, un componente hardware comune per la comunicazione con le periferiche) ogni volta che viene impostato il bit numero 4 dell'indirizzo 0x40008001 a 1 e che dopo aver impostato quel bit, verrà automaticamente disinserito dalla periferica.

Questo sarebbe il codice per una funzione C che invia un byte attraverso quell'UART:

 #define UART_BYTE *(char *)0x40008000 #define UART_SEND *(volatile char *)0x40008001 |= 0x08 void send_uart(char byte) { UART_BYTE = byte; // write byte to 0x40008000 address UART_SEND; // set bit number 4 of address 0x40008001 }

La prima riga della funzione verrà ampliata a:

 *(char *)0x40008000 = byte;

Questa riga dice al compilatore di interpretare il valore 0x40008000 come un puntatore a un char , quindi di dereferenziare (dare il valore a cui punta) quel puntatore (con l'operatore * più a sinistra) e infine di assegnare un valore byte a quel puntatore dereferenziato. In altre parole: scrivere il valore della variabile byte nell'indirizzo di memoria 0x40008000 .

La riga successiva sarà ampliata a:

 *(volatile char *)0x40008001 |= 0x08;

In questa riga, eseguiamo un'operazione OR bit per bit sul valore all'indirizzo 0x40008001 e il valore 0x08 ( 00001000 in binario, ovvero un 1 nel bit numero 4) e salviamo il risultato all'indirizzo 0x40008001 . In altre parole: impostiamo il bit 4 del byte che si trova all'indirizzo 0x40008001. Dichiariamo inoltre che il valore all'indirizzo 0x40008001 è volatile . Questo dice al compilatore che questo valore può essere modificato da processi esterni al nostro codice, quindi il compilatore non farà alcuna ipotesi sul valore in quell'indirizzo dopo averci scritto. (In questo caso, questo bit non viene impostato dall'hardware UART subito dopo averlo impostato dal software.) Questa informazione è importante per l'ottimizzatore del compilatore. Se lo facessimo all'interno di un ciclo for , ad esempio, senza specificare che il valore è volatile, il compilatore potrebbe presumere che questo valore non cambi mai dopo essere stato impostato e saltare l'esecuzione del comando dopo il primo ciclo.

Uso deterministico delle risorse

Una caratteristica del linguaggio comune su cui la programmazione di sistema non può fare affidamento è la raccolta dei rifiuti, o anche solo l'allocazione dinamica per alcuni sistemi embedded. Le applicazioni integrate sono molto limitate nel tempo e nelle risorse di memoria. Sono spesso usati per i sistemi in tempo reale, dove non è possibile effettuare una chiamata non deterministica al Garbage Collector. E se l'allocazione dinamica non può essere utilizzata a causa della mancanza di memoria, è molto importante disporre di altri meccanismi di gestione della memoria, come inserire i dati in indirizzi personalizzati, come consentito dai puntatori C. I linguaggi che dipendono fortemente dall'allocazione dinamica e dalla raccolta dei rifiuti non sarebbero adatti ai sistemi con risorse limitate.

Codice Dimensione

C ha un tempo di esecuzione molto piccolo. E l'ingombro di memoria per il suo codice è inferiore rispetto alla maggior parte delle altre lingue.

Se confrontato con C++, ad esempio, un binario generato dal C che va a un dispositivo incorporato è circa la metà delle dimensioni di un binario generato da un codice C++ simile. Una delle cause principali di ciò è il supporto delle eccezioni.

Le eccezioni sono un ottimo strumento aggiunto da C++ al C e, se non attivate e implementate in modo intelligente, non hanno praticamente alcun sovraccarico del tempo di esecuzione (ma a costo di aumentare la dimensione del codice).

Vediamo un esempio in C++:

 // Class A declaration. Methods defined somewhere else; class A { public: A(); // Constructor ~A(); // Destructor (called when the object goes out of scope or is deleted) void myMethod(); // Just a method }; // Class B declaration. Methods defined somewhere else; class B { public: B(); // Constructor ~B(); // Destructor void myMethod(); // Just a method }; // Class C declaration. Methods defined somewhere else; class C { public: C(); // Constructor ~C(); // Destructor void myMethod(); // Just a method }; void myFunction() { A a; // Constructor aA() called. (Checkpoint 1) { B b; // Constructor bB() called. (Checkpoint 2) b.myMethod(); // (Checkpoint 3) } // b.~B() destructor called. (Checkpoint 4) { C c; // Constructor cC() called. (Checkpoint 5) c.myMethod(); // (Checkpoint 6) } // c.~C() destructor called. (Checkpoint 7) a.myMethod(); // (Checkpoint 8) } // a.~A() destructor called. (Checkpoint 9)

I metodi delle classi A , B e C sono definiti da qualche altra parte (ad esempio in altri file). Pertanto il compilatore non può analizzarli e non può sapere se genereranno eccezioni. Quindi deve prepararsi a gestire le eccezioni generate da qualsiasi loro costruttore, distruttore o altre chiamate di metodo. I distruttori non dovrebbero lanciare (pratica molto cattiva), ma l'utente potrebbe lanciare comunque, oppure potrebbero lanciare indirettamente chiamando una funzione o un metodo (esplicitamente o implicitamente) che genera un'eccezione.

Se una qualsiasi delle chiamate in myFunction genera un'eccezione, il meccanismo di rimozione dello stack deve essere in grado di chiamare tutti i distruttori per gli oggetti già costruiti. Un'implementazione per il meccanismo di rimozione dello stack utilizzerà l'indirizzo di ritorno dell'ultima chiamata da questa funzione per verificare il "numero del checkpoint" della chiamata che ha attivato l'eccezione (questa è la semplice spiegazione). Lo fa utilizzando una funzione ausiliaria generata automaticamente (una specie di tabella di ricerca) che verrà utilizzata per lo svolgimento dello stack nel caso in cui venga generata un'eccezione dal corpo di quella funzione, che sarà simile a questa:

 // Possible autogenerated function void autogeneratedStackUnwindingFor_myFunction(int checkpoint) { switch (checkpoint) { // case 1 and 9: do nothing; case 3: b.~B(); goto destroyA; // jumps to location of destroyA label case 6: c.~C(); // also goes to destroyA as that is the next line destroyA: // label case 2: case 4: case 5: case 7: case 8: a.~A(); } }

Se l'eccezione viene generata dai checkpoint 1 e 9, nessun oggetto deve essere distrutto. Per il checkpoint 3, b e a devono essere distrutti. Per il checkpoint 6, c e a devono essere distrutti. In tutti i casi l'ordine di distruzione deve essere rispettato. Per i checkpoint 2, 4, 5, 7 e 8, è necessario distruggere solo l'oggetto a .

Questa funzione ausiliaria aggiunge dimensioni al codice. Questo fa parte del sovraccarico di spazio che C++ aggiunge a C. Molte applicazioni incorporate non possono permettersi questo spazio aggiuntivo. Pertanto, i compilatori C++ per i sistemi incorporati hanno spesso un flag per disabilitare le eccezioni. La disabilitazione delle eccezioni in C++ non è gratuita, perché la libreria di modelli standard si basa molto sulle eccezioni per informare gli errori. L'uso di questo schema modificato, senza eccezioni, richiede più formazione per gli sviluppatori C++ per rilevare possibili problemi o trovare bug.

E stiamo parlando di C++, un linguaggio il cui principio è: "Non paghi per ciò che non usi". Questo aumento della dimensione binaria peggiora per altre lingue che aggiungono ulteriore sovraccarico con altre funzionalità che sono molto utili ma non possono essere offerte dai sistemi embedded. Sebbene C non ti dia l'uso di queste funzionalità extra, consente un footprint del codice molto più compatto rispetto alle altre lingue.

Motivi per imparare C

Il C non è una lingua difficile da imparare, quindi tutti i vantaggi derivanti dall'apprendimento saranno abbastanza economici. Vediamo alcuni di questi vantaggi.

Lingua franca

Come già accennato, C è una lingua franca per gli sviluppatori. Molte implementazioni di nuovi algoritmi nei libri o su Internet vengono rese disponibili per la prima volta (o solo) in C dai loro autori. Ciò offre la massima portabilità possibile per l'implementazione. Ho visto programmatori che lottano su Internet per riscrivere un algoritmo C in altri linguaggi di programmazione perché non conoscevano i concetti di base del C.

Tieni presente che il C è un linguaggio vecchio e diffuso, quindi puoi trovare tutti i tipi di algoritmi scritti in C in giro per il web. Quindi molto probabilmente trarrai vantaggio dalla conoscenza di questa lingua.

Capire la macchina (pensa in C)

Quando discutiamo con i colleghi del comportamento di alcune porzioni di codice, o di alcune caratteristiche di altri linguaggi, finiamo per "parlare in C:" Questa porzione sta passando un "puntatore" all'oggetto o sta copiando l'intero oggetto? Potrebbe esserci un "cast" qui? E così via.

Raramente si discute (o si pensa) alle istruzioni di assembly che una parte di codice sta eseguendo quando si analizza il comportamento di una parte di codice di un linguaggio di alto livello. Invece, quando discutiamo di cosa sta facendo la macchina, parliamo (o pensiamo) abbastanza chiaramente in C.

Inoltre, se non riesci a fermarti a pensare in questo modo a quello che stai facendo, potresti finire per programmare con una sorta di superstizione su come (magicamente) le cose vengono fatte.

Pensa come la macchina con C

Lavora su molti progetti C interessanti

Molti progetti interessanti, da grandi server di database o kernel di sistemi operativi, a piccole applicazioni embedded che puoi fare anche a casa per la tua soddisfazione personale e il tuo divertimento, sono realizzati in C. Non c'è motivo di smettere di fare cose che potresti amare per l'unico motivo che non conosci un linguaggio di programmazione vecchio e piccolo, ma forte e collaudato come C.

Lavora su fantastici progetti con C

Conclusione

Gli Illuminati non governano il mondo. I programmatori C lo fanno.
Twitta

Il linguaggio di programmazione C non sembra avere una data di scadenza. La sua vicinanza all'hardware, la grande portabilità e l'uso deterministico delle risorse lo rendono ideale per lo sviluppo di basso livello per cose come kernel di sistemi operativi e software embedded. La sua versatilità, efficienza e buone prestazioni lo rendono una scelta eccellente per software di manipolazione dei dati ad alta complessità, come database o animazioni 3D. Il fatto che molti linguaggi di programmazione oggi siano migliori del C per l'uso previsto non significa che superino il C in tutte le aree. C è ancora insuperabile quando le prestazioni sono la priorità.

Il mondo gira su dispositivi alimentati a C. Usiamo questi dispositivi ogni giorno, che ce ne rendiamo conto o meno. C è il passato, il presente e, per quanto possiamo vedere, ancora il futuro per molte aree del software.

Correlati: Come imparare i linguaggi C e C++: l'elenco definitivo