Il linguaggio di programmazione C è vivo e vegeto dal 1972 e rappresenta ancora uno degli elementi fondamentali del nostro mondo costellato di software. Ma che dire delle dozzine di linguaggi più recenti che sono emersi negli ultimi decenni? Alcuni sono stati esplicitamente progettati per sfidare il dominio di C, ma superarlo a livello prestazioni, compatibilità bare metal e ubiquità si è rivelata un’impresa tutt’altro che semplice per tutti questi sfidanti. Tuttavia, vale la pena capire come il linguaggio C si ponga nei confronti degli altri linguaggi di programmazione più moderni.

Confronto tra C e C++

C viene spesso paragonato a C++, il linguaggio che, come indica il nome, è stato creato come estensione di C. Pur essendo ancora simile a C nella sua sintassi e nel suo approccio, C++ offre molte funzionalità veramente utili che non sono disponibili in modo nativo in C: namespace, template, eccezioni, gestione automatica della memoria e così via. I progetti che richiedono prestazioni di alto livello, come database e sistemi di machine learning, sono spesso scritti in C++, utilizzando tali funzionalità per eliminare ogni calo di prestazioni dal sistema.

Inoltre, C++ continua ad espandersi in modo molto più aggressivo di C. L’imminente C++ 23 offre ancora di più, inclusi moduli, coroutine e una libreria standard modularizzata per una compilazione più rapida. Al contrario, la prossima versione pianificata dello standard C, C2x, aggiunge poco e si concentra sul mantenimento della compatibilità con le versioni precedenti.

Il fatto è che tutti i vantaggi in C ++ possono anche funzionare come svantaggi. Più funzionalità di C++ vengono utilizzate, maggiore è la complessità introdotta e più difficile diventa “domare” i risultati. Gli sviluppatori che si limitano a un sottoinsieme di C++ possono evitare molte delle sue peggiori insidie. Ma alcuni team vogliono proteggersi del tutto da questa complessità. Il team di sviluppo del kernel Linux, ad esempio, evita C++ e se al momento sta guardando con interesse a Rust come un linguaggio per future aggiunte al kernel, la maggior parte di Linux sarà ancora scritta in C.

Scegliere C su C++ è un modo per gli sviluppatori di optare per il minimalismo forzato ed evitare di complicarsi la vita con gli eccessi di C++. Naturalmente, C++ ha un ricco set di funzionalità di alto livello per una buona ragione. Ma se questo minimalismo è più adatto per i progetti attuali e futuri e per i team di progetto, allora C ha più senso.

Confronto tra C e Java

Dopo decenni, Java rimane un punto fermo dello sviluppo di software aziendale e un punto fermo dello sviluppo in generale. La sintassi Java prende in prestito molto da C e C++. A differenza di C, però, Java non compila per impostazione predefinita in codice nativo. Il compilatore JIT (just-in-time) di Java compila infatti il codice Java per l’esecuzione nell’ambiente di destinazione. Il motore JIT ottimizza le routine in fase di esecuzione in base al comportamento del programma, consentendo molte classi di ottimizzazione che non sono possibili con C compilato in anticipo. Nelle giuste circostanze, il codice Java compilato con JIT può avvicinarsi o addirittura superare le prestazioni di C.

E mentre il runtime Java automatizza la gestione della memoria, è possibile aggirare il problema. Ad esempio, Apache Spark ottimizza in parte l’elaborazione in memoria utilizzando parti “non sicure” del runtime Java per allocare e gestire direttamente la memoria ed evitare il sovraccarico del sistema di raccolta spazzatura della JVM.

La filosofia Java “scrivi una volta, esegui ovunque” consente anche l’esecuzione di programmi Java con relativamente poche modifiche per un’architettura di destinazione. Al contrario, sebbene C sia stato portato su un gran numero di architetture, qualsiasi programma C può ancora richiedere la personalizzazione per funzionare correttamente, ad esempio, su Windows rispetto a Linux.

linguaggio di programmazione C

Credit immagine: Depositphotos

Questa combinazione di portabilità e prestazioni elevate, insieme a un enorme ecosistema di librerie e framework software, rende Java un linguaggio e un runtime di riferimento per la creazione di applicazioni aziendali. Dove non è all’altezza di C, è un’area in cui il linguaggio non è mai stato pensato per competere: lavorare direttamente con l’hardware.

Il codice C viene compilato in codice macchina, che viene eseguito direttamente dal processo. Java viene compilato in bytecode, un codice intermedio che l’interprete JVM converte in codice macchina. Inoltre, sebbene la gestione automatica della memoria di Java sia una benedizione nella maggior parte delle circostanze, C è più adatto per i programmi che devono fare un uso ottimale delle risorse di memoria limitate, a causa del suo piccolo ingombro iniziale.

Confronto tra C, C# e .NET

Quasi due decenni dopo la loro introduzione, C# e .NET rimangono parti importanti del mondo del software aziendale. Come Java (e in una certa misura Python), anche .NET offre portabilità su una varietà di piattaforme e un vasto ecosistema di software integrato. Questi non sono piccoli vantaggi dato quanto sviluppo orientato alle aziende avviene nel mondo .NET. Quando si sviluppa un programma in C# o in qualsiasi altro linguaggio .NET, è possibile attingere a tantissimi strumenti e librerie scritti per il runtime .NET.

Un altro vantaggio di .NET simile a Java è l’ottimizzazione JIT. I programmi in C# e .NET possono essere compilati in anticipo come per C, ma sono principalmente just-in-time compilati dal runtime .NET e ottimizzati con le informazioni di runtime. La compilazione JIT consente tutti i tipi di ottimizzazioni sul posto per un programma .NET in esecuzione che non può essere eseguito in C.

Come C (e Java, in una certa misura), C# e .NET forniscono vari meccanismi per accedere direttamente alla memoria. Heap, stack e memoria di sistema non gestita sono tutti accessibili tramite API e oggetti .NET. Inoltre, gli sviluppatori possono utilizzare la modalità non sicura in .NET per ottenere prestazioni ancora maggiori.

Niente di tutto questo però è privo di lati negativi. Gli oggetti gestiti e gli oggetti non sicuri non possono essere scambiati arbitrariamente e il marshalling di interoperabilità tra di essi comporta un costo per le prestazioni. Pertanto, massimizzare le prestazioni delle applicazioni .NET significa ridurre al minimo lo spostamento tra oggetti gestiti e non gestiti.

Quando non potete permettervi di pagare la penalità per la memoria gestita rispetto a quella non gestita, o quando il runtime .NET è una scelta sbagliata per l’ambiente di destinazione (ad esempio, lo spazio del kernel) o potrebbe non essere disponibile affatto, allora C è ciò di cui avete bisogno. E, a differenza di C# e .NET, C sblocca l’accesso diretto alla memoria per impostazione predefinita.

Confronto tra C e Go

La sintassi Go deve molto a C: le parentesi graffe come delimitatori e le istruzioni terminate con punto e virgola sono solo due esempi. Gli sviluppatori esperti in C possono in genere passare direttamente in Go senza troppe difficoltà, anche tenendo conto delle nuove funzionalità di Go come i namespace e la gestione dei pacchetti.

Il codice leggibile era uno degli obiettivi di progettazione iniziali di Go: rendere più facile per gli sviluppatori aggiornarsi con qualsiasi progetto Go e diventare competenti con la base di codice in breve tempo. Le basi di codice C possono essere difficili da “ammaestrare”, in quanto sono inclini a trasformarsi in un nido di macro specifiche sia per un progetto, sia per un determinato team. La sintassi di Go e i suoi strumenti integrati di formattazione del codice e di gestione dei progetti hanno proprio lo scopo di tenere a bada questo tipo di problemi.

Go offre anche extra come goroutines e channels, strumenti a livello di linguaggio per la gestione del passaggio dei messaggi tra i componenti che con C andrebbero compilati a mano o forniti da una libreria esterna, mentre Go le fornisce immediatamente, rendendo molto più facile costruire software che ne abbia bisogno.

Dove Go differisce di più da C è nella gestione della memoria. Gli oggetti Go vengono gestiti automaticamente per impostazione predefinita. Per la maggior parte dei lavori di programmazione, ciò è estremamente conveniente, ma significa anche che qualsiasi programma che richieda una gestione deterministica della memoria sarà più difficile da scrivere.

Go è adatto per la creazione di programmi come utility da riga di comando e servizi di rete, che raramente hanno bisogno di manipolazioni così granulari. Ma i driver di periferica di basso livello, i componenti del sistema operativo dello spazio del kernel e altre attività che richiedono un controllo rigoroso sul layout e sulla gestione della memoria sono creati al meglio in C.

Infrastructure as code

Confronto tra C e Rust

In un certo senso, Rust è una risposta ai problemi di gestione della memoria creati da C e C++ e a molte altre carenze di questi linguaggi. Rust compila in codice macchina nativo, quindi è considerato alla pari con C per quanto riguarda le prestazioni. La sicurezza della memoria per impostazione predefinita, tuttavia, è il principale punto di forza di Rust.

La sintassi e le regole di compilazione di Rust aiutano gli sviluppatori a evitare errori comuni di gestione della memoria. Se un programma ha un problema di gestione della memoria all’interno della sintassi di Rust, semplicemente non verrà compilato. Chi è alle prime armi con Rust (specialmente chi viene da un linguaggio come C che offre molto spazio per tali bug), trascorre la prima fase della sua formazione su Rust imparando come “domare” il compilatore. Ma i sostenitori di Rust sostengono che questo scoglio iniziale ha un ritorno a lungo termine: un codice più sicuro che non sacrifica la velocità.

Anche gli strumenti di Rust sono migliori rispettano a quelli di C. La gestione dei progetti e dei componenti fa parte della toolchain di default fornita con Rust, che è la stessa di Go. Esiste un modo predefinito consigliato per gestire i pacchetti, organizzare le cartelle di progetto e gestire molte altre cose che in C sono al massimo ad-hoc, con ogni progetto e team che li gestisce in modo diverso.

Tuttavia, ciò che viene pubblicizzato come un vantaggio in Rust potrebbe diventare uno svantaggio per uno sviluppatore C. Le funzionalità di sicurezza in fase di compilazione di Rust non possono essere disabilitate, quindi anche il più banale programma Rust deve essere conforme alle restrizioni di sicurezza della memoria di Rust. C può essere meno sicuro per impostazione predefinita, ma è molto più flessibile e indulgente quando necessario.

Un altro possibile inconveniente è la dimensione del linguaggio Rust. C ha relativamente poche funzionalità, anche se si tiene conto della libreria standard. Il set di funzionalità Rust è invece tentacolare e continua a crescere. Come per C++, il set di funzionalità più ampio significa più potenza, ma anche più complessità. C è un linguaggio più piccolo, ma molto più facile da modellare mentalmente e quindi più adatto a progetti in cui Rust sarebbe quasi sprecato.

Confronto tra C e Python

In questi giorni, ogni volta che si parla di sviluppo software, Python sembra sempre al centro della conversazione. Dopotutto, Python è considerato da molti il secondo miglior linguaggio di programmazione ed è indiscutibilmente uno dei più versatili, con migliaia di librerie di terze parti disponibili.

Ciò che ha fatto la fortuna di Python, nonché il punto in cui differisce maggiormente da C, è il favorire la velocità di sviluppo rispetto alla velocità di esecuzione. Un programma che potrebbe richiedere un’ora per essere assemblato in un altro linguaggio come C, potrebbe essere sviluppato in Python in pochi minuti. Il rovescio della medaglia? Quello stesso programma potrebbe richiedere secondi per l’esecuzione in C, ma un minuto per l’esecuzione in Python. Ma per molti lavori su hardware moderno Python è abbastanza veloce e questa è stata la chiave per la sua ampia adozione.

Un’altra grande differenza è la gestione della memoria. I programmi Python sono completamente gestiti dalla memoria dal runtime Python, quindi gli sviluppatori non devono preoccuparsi di allocare e liberare la memoria. Ma anche in questo caso, la facilità degli sviluppatori ha un costo di prestazioni runtime. Scrivere programmi in C richiede un’attenzione scrupolosa alla gestione della memoria, ma i programmi risultanti sono spesso lo standard se si guarda alla pura velocità della macchina.

Tuttavia, Python e C condividono una connessione profonda Il runtime Python di riferimento è scritto in C e ciò consente ai programmi Python di eseguire il wrapping delle librerie scritte in C e C++. Pezzi significativi dell’ecosistema Python di librerie di terze parti, ad esempio per il machine learning, hanno il codice C al centro. In molti casi, non è una questione di C contro Python, ma più una questione di quali parti della vostra applicazione dovrebbero essere scritte in C e quali in Python.

Se la velocità di sviluppo è più importante della velocità di esecuzione e se la maggior parte delle parti performanti del programma può essere isolata in componenti standalone (invece di essere diffusa in tutto il codice), Python puro (o un mix di librerie Python e C) rappresenta una scelta migliore rispetto al solo C. Altrimenti, C è ancora la scelta ideale.

Confronto tra C e Carbon

Un altro possibile contendente sia per C, sia per C++ è Carbon, un nuovo linguaggio che è attualmente in fase di forte sviluppo. L’obiettivo di Carbon è quello di essere un’alternativa moderna a C e C ++, con una sintassi semplice, strumenti moderni e tecniche di organizzazione del codice e soluzioni ai problemi che i programmatori C e C ++ hanno a lungo affrontato. Ha anche lo scopo di fornire l’interoperabilità con le basi di codice C++, in modo che il codice esistente possa essere migrato in modo incrementale. Tutto ciò rappresenta un pacchetto più che gradito agli occhi degli sviluppatori, dal momento che C e C++ hanno storicamente avuto strumenti e processi primitivi rispetto ai linguaggi sviluppati più di recente.

Quindi, qual è il rovescio della medaglia? In questo momento Carbon è un progetto sperimentale e non ancora pronto per l’uso in produzione. Non c’è nemmeno un compilatore funzionante; solo un esploratore di codice online. Ci vorrà un po’ prima che Carbon diventi un’alternativa pratica a C o C ++, se mai lo farà.