Introduzione al linguaggio C - Università degli Studi di Messina - PDF
Document Details
Uploaded by DeadCheapPentagon
Università degli Studi di Cagliari
2024
Francesco Longo
Tags
Summary
These are lecture notes on the C programming language for undergraduate students at the University degli Studi di Messina. The notes cover the basics of the C language, including its structure, functions, and libraries. The notes also include an example of a "Hello, World!" program.
Full Transcript
Università degli Studi di Messina Corso di laurea in Ingegneria Elettronica ed Informatica Corso di laurea in Ingegneria Biomedica A.A. 2023/24 Fondamenti di Informatica...
Università degli Studi di Messina Corso di laurea in Ingegneria Elettronica ed Informatica Corso di laurea in Ingegneria Biomedica A.A. 2023/24 Fondamenti di Informatica Il linguaggio C Introduzione al linguaggio C Ing. Francesco Longo Il linguaggio C Il linguaggio C Il C è un linguaggio di programmazione di alto livello che segue il paradigma di Il C fu progettato ed implementato da Dennis Ritchie nei laboratori programmazione imperativa della AT&T Bell Labs tra il 1969 e il 1973 In particolare il C è un linguaggio strutturato e permette di scrivere programmi chiari, dalla correttezza dimostrabile e semplici da modificare La prima definizione precisa si ha nel 1978 con il libro “The C Programming Language” di Brian Kernighan e Dennis Ritchie Benché originariamente implementato su un sistema UNIX, esso non fu scritto (libro noto semplicemente come K&R) per funzionare solo su un particolare sistema operativo e può essere ad oggi utilizzato su praticamente tutti i sistemi operativi La prima versione accettata come standard è stata prodotta nel E’ il linguaggio di alto livello più a contatto con l’hardware ovvero permette al 1983 dall’American National Standards Institute (ANSI) ed è nota programmatore di accedere e gestire direttamente le risorse hardware come C89 o ANSI C dell’elaboratore ma in maniera indipendente dall’architettura Siamo attualmente alla versione standard C11 (del 2011) Permette di scrivere programmi molto efficienti e quindi è utilizzato soprattutto per scrivere software di base ovvero sistemi operativi, compilatori, interpreti, librerie di sistema Dennis Ritchie Funzioni e librerie Dennis MacAlistair Ritchie (Bronxville, Essendo un linguaggio strutturato, il C è basato sul concetto 9 settembre 1941 – Berkeley Heights, 12 ottobre 2011) è stato un informatico di sottoprogramma statunitense. È uno dei pionieri dell'informatica moderna, importante per essere stato In C i sottoprogrammi prendono il nome di funzioni l'inventore del linguaggio C e, insieme al suo storico collega Ken Thompson, per Una funzione è un modulo che svolge un’operazione ben aver scritto il sistema operativo Unix. Nel 1983 riceve il Premio Turing precisa e che può essere richiamato dal programma insieme a Ken Thompson, per il principale contributo dato allo sviluppo della teoria dei sistemi operativi e, in particolare, per l'implementazione di Le funzioni sono generalmente raggruppate da chi le scrive in Unix, il prototipo dei moderni sistemi collezioni chiamate librerie operativi Unix-like. Esiste una ricca collezione di funzioni già esistenti chiamata libreria standard del C Vantaggi dell’utilizzo di librerie Grammatica del C I principali vantaggi nell’utilizzo di librerie di funzioni sono: Su Internet possono essere reperite molte grammatiche del C nelle sue diverse versioni standard ○ Riusabilità del codice: un programma può essere costruito partendo da blocchi già esistenti Tali grammatiche sono scritte in numerosi varianti della BNF è inutile reinventare la ruota, riscrivendo funzioni già disponibili conviene utilizzare quello che già esiste scrivendo solo ciò che viene Una delle più chiare ed aggiornate può essere reperita qui: inventato ex-novo http://www.quut.com/c/ANSI-C-grammar-y.html ○ Efficienza ed affidabilità del codice: utilizzare funzioni di libreria migliora generalmente l’efficienza e l’affidabilità del programma Quando utile utilizzeremo una grammatica del C semplificata e scritta le librerie sono sviluppate da programmatori bravi che riescono ad adhoc per scopi didattici ottimizzarle al massimo le librerie sono di solito molto affidabili (prive di errori o bug) Nota che tale grammatica andrà spesso a semplificare la sintassi del C per proprio perché molto utilizzate e quindi molto testate fornire indicazioni su come scrivere codice facilmente leggibile e modificabile e quindi NON deve essere considerata esaustiva nel descrivere il linguaggio Struttura di un programma C Struttura di un programma C Un programma C può essere composto da uno o più file di codice sorgente Se volessimo riassumere la struttura semplificata di un programma C, scritto su un unico file sorgente, sotto forma di una grammatica BNF In un primo momento, noi lavoreremo con programmi scritti su un unico file potremmo scrivere: sorgente ::= In un programma C, il programmatore è obbligato a definire almeno una {} funzione chiamata main che rappresenterà il punto di partenza del {} programma Prima o dopo il main possono essere dichiarate e/o definite un certo numero {} di: ○ variabili Come si evince dalla grammatica, la definizione della funzione main è ○ tipi di dato definiti dall’utente l’unica parte obbligatoria! ○ altre funzioni Nota che in questa grammatica non sono esplicitamente indicati i E’ inoltre possibile (generalmente come prima cosa) includere delle direttive commenti che possono essere inseriti in qualunque posizione del per il preprocessore programma Struttura del main Struttura del main La funzione main può essere definita in accordo alla seguente grammatica: Con il simbolo non terminale identifichiamo uno dei seguenti costrutti: ::= ○ dichiarazione di variabile int main() { ○ dichiarazione di funzione {} ○ definizione di funzione {} ○ definizione di tipo di dato definito dall’utente } Ovvero: Vedremo in seguito in modo formale come si definisce una funzione ::= Per adesso ci basti ricordare che per definire la funzione main si devono | | utilizzare la parola chiave int seguita da main() e da una coppia di parentesi | graffe aperta { e chiusa } Vedremo in seguito il significato e l’utilizzo di ognuno di questi costrutti Tra la coppia di parentesi graffe scriveremo il nostro programma principale che potrà essere costituito da zero o più definizioni, dichiarazioni o istruzioni Nota che è sconsigliabile definire una funzione all’interno di un’altra funzione! vere e proprie Struttura del main Semplice programma in C Con il simbolo non terminale identifichiamo uno dei Il primo programma che solitamente si scrive quando si impara un nuovo seguenti costrutti: linguaggio di programmazione è il programma “Hello, World!” che ○ assegnazione di variabile semplicemente visualizza tale messaggio a video ○ inizializzazione di variabile (con relativa dichiarazione) ○ espressione In C esso può assumere la forma: ○ invocazione di funzione ○ istruzione di controllo #include Ovvero: int main(){ ::= printf("Hello, World!\n"); | | } | | Analizziamo il programma riga per riga evidenziando che esso rispetta la Vedremo in seguito il significato e l’utilizzo di ognuno di questi costrutti grammatica proposta precedentemente Analisi del programma Commenti I commenti sono porzioni del file sorgente che verranno ignorate dal compilatore #include Consentono al programmatore di inserire frasi in linguaggio naturale all’interno Commento del codice sorgente per appuntare considerazioni, chiarire la semantica delle int main(){ istruzioni e gli aspetti più ostici del programma e in generale per fornire printf("Hello, World!\n"); documentazione } Essi possono essere definiti formalmente dalla seguente grammatica: Direttiva per il ::= preprocessore ::= {} Definizione della ::= {} funzione main Invocazione di funzione Sono quindi sequenze di caratteri racchiuse fra i delimitatori I commenti non possono essere innestati ovvero non è possibile inserire un commento all’interno di un altro commento Funzione printf Funzione printf #include #include int main(){ int main(){ printf("Hello, World!\n"); printf("Hello, World!\n"); } } L’unica istruzione presente nel main è l’invocazione della funzione printf Si nota che all’interno della stringa passata come parametro a printf non è presente solo la frase “Hello, World!” ma è presente anche la sequenza \n La funzione printf è una funzione della libreria standard del C che consente di stampare una stringa a video In C e in altri linguaggi di programmazione esistono tutta una serie di sequenze speciali di caratteri (dette sequenze di controllo) che vengono utilizzate per indicare caratteri La stringa deve essere passata come parametro alla funzione tra le parentesi tonde speciali Ad esempio, la sequenza \n indica il carattere di NewLine che quando viene incontrato In C una stringa è un insieme di caratteri racchiusi tra virgolette (" ") sposta il cursore alla riga successiva ovvero viene utilizzato per andare a capo In C ogni istruzione deve essere terminata con un punto e virgola (;) Direttiva #include Direttive per il preprocessore Non ci interessa entrare nel dettaglio delle direttive per il preprocessore quindi (solo per completezza) diamo alcuni cenni sulle direttive che utilizzeremo più spesso #include Direttive di inclusione (#include ) int main(){ ○ sono necessarie per comunicare al compilatore che verranno utilizzate funzioni di libreria printf("Hello, World!\n"); ○ deve essere indicato il nome dell’header file della libreria tra parentesi angolari } ○ ad esempio la direttiva “#include ” indica il nome dell’header file della libreria standard per l’I/O ovvero stdio.h La direttiva per il preprocessore #include è necessaria per indicare al compilatore Direttive di definizione: (#define identifier expression) che nel programma si utilizzeranno funzioni di libreria ○ vengono utilizzate per definire identificatori di espressioni che potranno poi essere utilizzate nel programma In particolare, l’istruzione utilizzata in questo programma indica al compilatore ○ sono utili quando si vuole definire costanti o macro che utilizzeremo funzioni della libreria stdio ovvero la libreria standard del C che ○ ad esempio la direttiva “#define N 5” definisce la costante N pari a 5 raccoglie funzioni di I/O (C Standard Input and Output Library) ○ mentre la direttiva “#define SQUARE(a) (a)*(a)” definisce la macro SQUARE che eleva al quadrato il parametro che le viene passato Implementazione di un programma C L’implementazione di un programma in linguaggio C richiede varie fasi: ○ scrittura del file sorgente con un editor di testo ○ compilazione del file sorgente in un file eseguibile con un compilatore ○ caricamento del programma in memoria centrale tramite il loader del sistema operativo Dettagli del processo di ○ esecuzione del programma da parte della CPU un’istruzione alla volta implementazione file sorgente file eseguibile Editor Compilatore memoria memoria secondaria secondaria file eseguibile Loader CPU memoria centrale Fasi della compilazione Pre-processamento La fase che abbiamo indicato come compilazione è in realtà solitamente composta da Durante la fase di pre-processamento, il pre-processore: tre sotto-fasi principali: ○ elimina tutti i commenti (ovvero tutto il testo compreso tra ) ○ pre-processamento ○ legge, esegue ed elimina le direttive a lui indirizzate (ovvero tutte le istruzioni ○ compilazione precedute da #) ○ linking Il pre-processore trasforma il codice sorgente che riceve in ingresso in un codice in puro linguaggio C: ○ pulito: perché non sono più presenti commenti e direttive file sorgente file sorgente ○ espanso: perché viene solitamente aggiunto del codice sorgente aggiuntivo con commenti e senza commenti e file oggetto file eseguibile del direttive per il direttive per il del programma programma pre-processore pre-processore principale completo Considerando un file sorgente in cui sono presenti solo direttive #include e #define il (puro C) pre-processore: ○ sostituisce ad ogni direttiva #include il contenuto dell’header file della libreria Pre-processore Compilatore Linker considerata: questa operazione è necessaria affinché il compilatore sappia riconoscere le invocazioni alle funzioni di libreria come istruzioni valide e sappia come interpretarle ○ scorre tutto il file sorgente del programma e sostituisce ogni occorrenza degli identificatori definiti nelle direttive #define con le rispettive espressioni (costanti file oggetto o macro) delle librerie Compilazione Linking Il compilatore propriamente detto, prende in ingresso il Il codice oggetto prodotto dalla fase di compilazione è ancora incompleto e quindi non eseguibile in quanto mancante di tutto il codice macchina relativo alle codice sorgente processato dal pre-processore e lo trasforma funzioni di libreria in codice oggetto ovvero in un programma scritto nel linguaggio macchina del calcolatore considerato Durante la fase di linking, il linker ha il compito di collegare il file oggetto del programma principale ottenuto dalla compilazione con i file oggetto delle librerie utilizzate Mentre il file sorgente scritto in linguaggio C ha solitamente estensione.c, il file oggetto prodotto ha solitamente Si dice che il linker risolve tutti i simboli esterni al codice oggetto creando un file che è effettivamente eseguibile (.exe in ambiente Windows, senza particolari estensione.o in ambienti Unix-like (Linux e Mac) e.obj in estensioni in ambienti Unix-like) ambienti Windows Il linking può avvenire in due modi: ○ linking statico: i file oggetto delle librerie esterne vengono effettivamente combinati con il file oggetto del programma principale ○ linking dinamico: il linker verifica solamente la validità dei simboli esterni che verranno risolti effettivamente solo quando il programma viene mandato in esecuzione Analisi e Sintesi Analisi Il compilatore traduce il codice sorgente in codice oggetto, attraverso due fasi Nel corso della fase di analisi, il compilatore verifica la correttezza lessicale, sintattica e principali: semantica del codice sorgente ○ Analisi: analisi del codice sorgente e generazione del codice intermedio ○ Sintesi: ottimizzazione e produzione del codice oggetto In particolare, esso effettua: ○ Analisi lessicale: verifica che i simboli utilizzati siano legali cioè appartenenti al Nei moderni compilatori le due fasi sono separate in: vocabolario del linguaggio e suddivide il codice sorgente in lemmi o token (identificatori, parole chiave del linguaggio, operatori, ecc.) ○ Front-end: operazioni identiche su tutti i calcolatori ○ Analisi sintattica: verifica che le regole grammaticali del linguaggio siano ○ Back-end: operazioni specializzate per ogni calcolatore rispettate sulla base di una grammatica formale producendo tra l’altro un albero sintattico e una tabella dei simboli Ogni fase include delle sotto-fasi specializzate su specifici compiti: ○ Analisi semantica: verifica i vincoli imposti dal contesto ovvero stabilisce se le frasi ○ Analisi (front-end): presenti nel codice sorgente hanno senso o meno (ad esempio, verifica che gli Analisi lessicale identificatori di variabili utilizzati siano stati effettivamente dichiarati prima Analisi sintattica dell’uso) Analisi semantica Sintesi del codice intermedio Subito dopo, l’albero sintattico viene visitato per generare il codice intermedio che è ○ Sintesi (back-end) solitamente scritto in un linguaggio interno al compilatore e che verrà poi utilizzato per Ottimizzazione del codice intermedio generare il codice oggetto del calcolatore di interesse Sintesi del codice oggetto Sintesi Schema generale Nella fase di sintesi, il compilatore effettua: ○ Ottimizzazione del codice intermedio: vengono effettuate una serie di codice lemmi o albero codice ottimizzazioni per rendere il codice più efficiente sia dal punto di vista del sorgente token sintattico intermedio tempo di esecuzione che dal punto di vista della memoria ○ Generazione del codice oggetto: il codice intermedio viene tradotto in Analisi Analisi Analisi linguaggio assembler del calcolatore in esame e poi in linguaggio macchina lessicale sintattica semantica Alcune operazioni effettuate nella fase di ottimizzazione: ○ allocazione della memoria tabella dei ○ allocazione dei registri simboli e altre tabelle ○ valutazione di sotto-espressioni comuni a più espressioni ○ spostamento di codice statico fuori dai cicli ○ … Generazione Durante tutte le fasi il compilatore utilizza delle strutture dati interne quali la Ottimizzazione del codice tabella dei simboli e altre tabelle che sono necessarie per lo scambio di informazioni tra le varie fasi codice codice intermedio oggetto ottimizzato Installazione GCC (GNU Compiler Collection) è un software rilasciato sotto licenza GPL (General Public License): ○ è quindi un software libero ○ ovvero consente agli utenti finali di utilizzare, condividere e persino modificare il software liberamente e gratuitamente GCC (GNU Compiler Collection) GCC è il compilatore di default sotto qualunque distribuzione Linux ed è quello di riferimento per la compilazione del kernel di Linux In distribuzioni Debian-like come Ubuntu, per installare GCC e tutte gli header file delle librerie standard del C è sufficiente digitare dal prompt di un terminale: $ sudo apt update $ sudo apt upgrade $ sudo apt install build-essential Prima compilazione ed esecuzione Scegliere il nome dell’eseguibile Come editor di testo sotto Linux si consigliano: Se si vuole dare un nome differente all’eseguibile prodotto lo si può specificare ○ gedit (in ambiente grafico GNOME) con il comando: ○ kate (in ambiente grafico KDE) $ gcc hello.c -o hello ○ nano (da terminale) ○ vim (da terminale) Tale comando produrrà il file eseguibile di nome hello che potrà essere mandato in esecuzione con il comando: Creare un nuovo file di testo, incollare il codice sorgente del programma “Hello, World!” e salvare il file con nome hello.c $./hello Per compilare utilizziamo il comando: Nota: se all’atto dell’esecuzione del nostro programma si ottiene un errore del tipo: $ gcc hello.c bash:./hello: Permission denied Tale comando produrrà il file eseguibile di nome a.out che potrà essere mandato in esecuzione con il comando molto probabilmente il file non ha i permessi di esecuzione che gli possono essere dati $./a.out con il comando: $ chmod +x hello Passaggi intermedi di compilazione Passaggi intermedi di compilazione Se si vuole visualizzare il file sorgente come modificato dal pre-processore è possibile Se vogliamo linkare il file oggetto ottenuto con il comando precedente alle librerie per utilizzare il comando: ottenere il file eseguibile finale possiamo utilizzare il comando: $ gcc -E -P hello.c > hello_pre.c $ cat hello_pre.c $ gcc -o hello hello.o Se si vuole ottenere il file oggetto non ancora linkato alle librerie si può utilizzare il Se vogliamo visualizzare il contenuto del file eseguibile (pure codice binario) possiamo comando: utilizzare il comando: $ gcc -c hello.c $ xxd -b hello Tale comando produrrà il file oggetto di nome hello.o che NON può essere mandato in esecuzione! Se infatti proviamo ad eseguirlo con i comandi $ chmod +x hello.o $./hello.o otterremo l’errore: bash:./hello.o: cannot execute binary file: Exec format error Linking esplicito Fase di analisi Tutte le librerie standard del C necessarie alla corretta creazione del file eseguibile Volendo visualizzare l’output della fase di analisi lessicale : di un programma vengono linkate automaticamente da gcc (o più precisamente da ld che è il linker utilizzato da gcc) $ clang -Xclang -dump-tokens hello.c &> hello_tokens.c $ cat hello_tokens.c Nel caso in cui si voglia utilizzare una libreria non standard (o nel caso in cui si voglia utilizzare la libreria matematica libm che pur essendo una libreria standard E’ anche possibile visualizzare l’output delle fasi di analisi sintattica e semantica del C non viene linkata automaticamente per ragioni storiche) dovremo specificare tramite i comandi: la libreria con un apposito flag $ gcc -fdump-tree-all hello.c -o hello Ad esempio, ipotizzando di avere scritto un programma che utilizzi funzioni della $ gcc -fdump-rtl-all hello.c -o hello libreria libm (il cui header file è math.h) dovremo modificare il comando di compilazione come segue: anche se essi producono parecchi file difficili da decifrare $ gcc matrix.c -o matrix -lm L’opzione -lm dice al linker ld di cercare nel sistema e collegare anche la libreria matematica Output di GCC Se rileva problemi o errori nel codice sorgente, GCC produce una serie di messaggi che possono essere classificati in due categorie fondamentali: Messaggi di avvertimento (warning messages): ○ indicano la presenza di parti di codice mal scritte che potrebbero provocare errori durante l’esecuzione del programma ○ non interrompono la compilazione che produce comunque il file eseguibile Messaggi di errore (error messages): ○ indicano la presenza di errori lessicali, sintattici o semantici nel codice sorgente ○ causano l'interruzione della compilazione che non produce il file eseguibile ○ la causa degli errori deve essere necessariamente eliminata E’ fondamentale leggere bene i messaggi di errore di GCC e imparare a interpretarli perché forniscono suggerimenti utilissimi per correggere i problemi riscontrati!