Enrico Vicario Fondamenti di Programmazione Linguaggio c, strutture dati e algoritmi elementari, c++ Indice Introduzione 1 1 Rappresentazione 1.1 Il linguaggio c . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.1.1 Sintesi . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 4 5 2 Strutture dati e algoritmi elementari 9 3 Estensione dal c al c++ 11 Testi di approfondimento 11 3 Introduzione Questo libro è pensato come introduzione alla programmazione di un calcolatore. È organizzato in due parti e un’appendice. Nella prima parte viene trattato il modo in cui un algoritmo può essere rappresentato ed eseguito su un calcolatore. L’elemento centrale è il linguaggio c, di cui la trattazione mira a fornire una padronanza avanzata basata sulla comprensione analitica e la razionalizzazione di un insieme di regole anziché sulla enumerazione di esempi. La trattazione fa leva su due opposte premesse: da un lato una descrizione operativa e analitica del processo con cui un programma c può essere compilato in un linguaggio assembler, codificato in forma numerica e eseguito su un processore; dall’altro l’introduzione di concetti teorici elementari che permettono di esprimere in modo compatto e non-ambiguo le regole sintattiche e semantiche di un linguaggio. In Appendice la trattazione del c è estesa al c++, con l’intento di abilitare l’accesso ad un testo di progettazione dove l’apprendimento del linguaggio sia sostenuto da adeguati strumenti di astrazione e metodologia. Nella seconda parte, vengono introdotti i concetti di struttura dati e di algoritmo facendo riferimento alla rappresentazione di liste e alberi binari e ai problemi della ricerca e dell’ordinamento. La trattazione fornisce l’opportunità di introdurre concretamente concetti fondamentali quali: la separazione tra logica e implementazione di una struttura dati; gli schemi di implementazione ricorsiva e iterativa; la valutazione della complessità di un algoritmo e di un problema; la verifica della correttezza; il riuso di schemi e soluzioni nella implementazione di un algoritmo. Anche, fornisce un contesto logico ben definito per un uso avanzato dei concetti di programmazione c introdotti nella prima parte del testo. Nel testo sono anche riportati gli svolgimenti di prove di esame che permettono di esercitare e verificare l’apprendimento. Il testo riflette da vicino i contenuti del corso di Fondamenti di Informatica che tengo da diversi anni presso la facoltà di Ingegneria di Firenze, e che molto risente della mia provenienza dall’area dell’ingegneria del software più che da quella degli algoritmi. 1 Negli anni mi è capitato di tenere il corso in diversi formati. Attualmente lo presento in un corso di 80-90 ore per 9 Crediti Formativi Universitari (CFU) al primo semestre del primo anno. Il nucleo caratterizzante è costituito dalle sezioni ?? dove si fornisce un’introduzione intuitiva al linguaggio, ?? dove si introducono gli elementi di grammatica che servono ad una trattazione compatta, 1.1 dove si descrive in modo quasi completo il linguaggio c, e ?? dove se ne dimostra un uso avanzato in riferimento alla rappresentazione delle liste e dove si introducono vari concetti tra cui dato astratto e rappresentazione, iterazione e ricorsione. Tutto questo richiede tipicamente 3+3+18+9 ore di lezione a cui convenientemente se ne aggiungono altre 6-12 di esercitazione teorica e pratica con cui si arriva a un totale di 4-5 CFU. La parte sugli algoritmi nelle sezioni ??, ??, ?? e ?? consolida in qualche misura l’uso del linguaggio ma soprattutto introduce in modo concreto elementi essenziali in un corso di Fondamenti di Informatica, tra cui in particolare i concetti di problema, algoritmo, complessità, correttezza. Tipicamente questo richiede altre 3+3+15+3 ore a cui se ne aggiungono bene altre 3-6 di esercitazione e se ne sottraggono bene 6 rimandando a un corso successivo lo HeapSort e il costo medio del QuickSort. Complessivamente il tutto corrisponde a 2-3 CFU. La sezioni iniziali ?? e ??, sulla rappresentazione dei dati e sulla rappresentazione e esecuzione di istruzioni su un calcolatore, servono a creare una base che motivi limiti e caratteristiche del linguaggio. In un corso al primo semestre del primo anno servono anche a dare un’intuizione concreta sul principio di funzionamento di un calcolatore. Complessivamente richiedono circa 18 ore che corrispondono ad ulteriori 2 CFU, ma sarebbe tranquillamente possibile saltare questa parte e partire direttamente dalla trattazione del linguaggio c. Le sezioni ?? sugli alberi e 3 sulla transizione dal c al c++ da diversi anni ho rinunciato a farle. Complessivamente richiederebbero almeno altre 9+9 ore per altri 2CFU. La parte sugli alberi non è in effetti essenziale per un primo corso di Informatica e trova posto più adeguatamente in un successivo corso su Strutture Dati e Algoritmi. Ho qualche maggiore rimpianto per la transizione da c a c++, che peraltro nel nostro corso di laurea è sviluppata con maggiore ampiezza e profondità, e con ottimi risultati, in un modulo di laboratorio immediatamente successivo. 2 Capitolo 1 Rappresentazione Il problema della rappresentazione consiste nel dare ad un algoritmo una forma che possa essere eseguita su un elaboratore. In questo assume un ruolo centrale l’uso di un linguaggio di programmazione. In questo testo affrontiamo il problema facendo riferimento al caso del linguaggio c. Questo ha un rilievo preminente in una ampia varietà di contesti applicativi, e costituisce comunque la base di c++ e java. Dopo una prima breve descrizione di un frammento del c orientata a fornire un’intuizione circa le caratteristiche del linguaggio, la trattazione è ampliata in due direzioni opposte. Da un lato sono descritti i principi del processo che permette di tradurre un programma c in una sequenza numerica che possa essere caricata nella memoria di un elaboratore ed eseguita da un processore. Questo permette di intuire aspetti del processo di compilazione che determinano larga parte delle regole del linguaggio. Dall’altro sono introdotti alcuni elementi teorici che permettono di descrivere il linguaggio attraverso un nucleo di regole generali, capaci di cogliere in maniera compatta e univoca l’organizzazione sintattica e i contenuti semantici del linguaggio. Facendo leva sui due diversi strumenti di concretezza e astrazione, viene finalmente affrontato in maniera dettagliata la descrizione di sintassi e semantica del linguaggio c. 3 1.1 Il linguaggio c In questo capitolo viene definito il linguaggio c. La trattazione fa leva congiuntamente sui capitoli precedenti, con diversi gradi di dipendenza: la descrizione informale del frammento di linguaggio c nel capitolo ?? aiuta la comprensione fornendo una visione di insieme, ma è completamente coperta da quanto viene ora esposto; la trattazione dei capitoli ?? e ?? circa il modo in cui un programma c può essere compilato in assembler, tradotto in linguaggio macchina ed eseguito su un processore mostra un razionale per alcune caratteristiche del linguaggio, ma può essere anche omessa; gli elementi di teoria dei linguaggi forniti nel capitolo ?? sono invece usati diffusamente e risultano strettamente necessari per la comprensione. 4 1.1.1 Sintesi Ricapitoliamo in questa sezione la sintassi e la semantica generale delle categorie introdotte nella descrizione del linguaggio c. Tipi <type> ::= char|[unsigned][short|long]int|float|double|void <type expr> ::= <type>|struct<identifier>*|<type expr>* <type decl> ::= <type>|struct<identifier> <struct def> ::= struct<identifier>{ { <declaration> } }; Vincoli contestuali: in <type expr> e <type decl>, il nome <identifier> deve comparire in una <struct def> visibile. La semantica di un tipo <type> consiste di un insieme di valori rappresentati e un insieme di operazioni sui valori. La stessa semantica si applica a <type expr> e <type decl>, che costituiscono estensioni diverse strumentali a catturare le limitazioni imposte sul tipo del valore restituito da un’espressione. Dichiarazioni <declaration> ::= <type decl><decl>; <decl> ::= <identifier>|*<decl>|<decl>[<const>]| (<decl>)|<decl>(<formal parameter list>) <formal parameter list> ::= void|<type decl><decl>{,<type><decl>} Vincoli contestuali: il tipo di una funzione deve appartenere a <type expr>. La semantica di una dichiarazione <declaration> consiste nell’associare un nome e un tipo a un’entità referenziabile, che può essere una variabile, un’array, una funzione. Riferimenti a variabile <var>::=<identifier>|(<var>)|*<expr addr>|<expr addr>[<expr int>]| <var s>.<identifier field>|<expr s>-><identifier field> Vincoli contestuali: <identifier> deve essere il nome identificato in una <declaration> visibile; <expr addr> deve restituire un indirizzo; <expr int> deve restituire un intero; <var s> deve essere un riferimento a variabile di tipo strutturato; <expr s> deve restituire l’indirizzo di una variabile di tipo strutturato; <identifier field> deve essere il nome identificato in una <declaration> contenuta in una <struct def> visibile. La semantica di un riferimento a variabile <var> consiste nell’identificare una variabile, ovvero una locazione di memoria e un tipo. 5 Espressioni <expr> ::= <const>|<var>| <var a>=<expr a>|<var a><op2>=<expr a>| ++<var int>|--<var int>|<var int>++|<var int>--| <expr1><op2><expr2>|<op1><expr>| (<expr>)|<expr1>,<expr2>|<expr1>?<expr2>:<expr3>| (<expr type>)<expr1>|&<var>| <expr func>(<actual parameter list>) <op2> ::= + | - | * | / | % | < | <= | == | != | >= | > | && | || | & | | | >> | << <op1> ::= ! | ∼ actual parameter list ::= [<expr>{,<expr>}] Vincoli contestuali: il tipo di <var> deve appartenere a type expr; il tipo di <expr a> deve essere lo stesso di <var a>; il tipo di var int deve essere [unsigned][long|short]int; l’espressione <expr func> deve restituire un indirizzo di funzione. La semantica di un’espressione <expr> consiste in un valore restituito (in un tipo) e una sequenza di side-effects. Istruzioni <statement> ::= <expr>;| <statement1><statement2>|{<statement1>}| if(<expr>)<statement1> [else<statement2>]| for(<expr init>;<expr guard>;<expr inc>) <statement>| while(<expr guard>)<statement>| do<statement>while(<expr guard>);| return[<expr>];|goto<label>;|break;|continue;| switch(<expr>) { {case <const>:<statement>} [default:<statement>] } La semantica di un’istruzione <statement> consiste nell’identificare una sequenza di espressioni eseguite e uno statement successivo a cui trasferire il controllo. 6 Programma Un programma è costituito da una sequenza di direttive, di definizioni di strutture e di tipi, di dichiarazioni di funzioni o variabili, e di definizioni di funzione ciascuna costituita da una sequenza di dichiarazioni e statements. <program> ::= { <directive>| typedef <type> <identifier>;| <declaration>| <function definition> } <function definition> ::= <type><decl>(<formal parameter list>) { {declaration} <statement> } <directive>::=#include<filename.h>|#define<identifier><any>|... 7 8 Capitolo 2 Strutture dati e algoritmi elementari Introduciamo in questo capitolo i concetti di struttura dati e di algoritmo trattando prima le liste e gli alberi binari e poi i problemi della ricerca e dell’ordinamento. La trattazione fornisce l’opportunità per applicare in modo anche raffinato i costrutti del c già introdotti nella prima parte del testo. Permette anche di introdurre in maniera concreta alcuni concetti fondamentali della programmazione: la separazione tra logica e implementazione di una struttura dati; i concetti di ricorsione e iterazione; la valutazione della complessità di un algoritmo e di un problema; la verifica della correttezza; la disciplina nella programmazione e il riuso di schemi e soluzioni. 9 10 Capitolo 3 Estensione dal c al c++ 11 12 Testi di approfondimento Architettura dei calcolatori: • D.A.Patterson, J.L.Hennessy, “Struttura e progetto dei calcolatori,” Zanichelli, Bologna, 1995. • G.Bucci, “Architettura e organizzazione dei calcolatori elettronici - fondamenti ,” McGraw-Hill Italia, Milano, 2001. Linguaggio c: • H.Schildt, “c, the complete reference,” McGraw-Hill, 2000. • E.Yourdon, L.Constantine, “Structured design: fundamentals of a discipline of Computer programming and system design,” Prentice Hall, 1986. Linguaggio c++: • E.Gamma, R.Helm, R.Johnson, J.Vlissides, “Design Patterns: elements of reusable object oriented software,” Addison Wesley, 1995. • M.Fowler, “UML distilled,” Addison Wesley, 2000. • H.Schildt, “c++, the complete reference,” McGraw-Hill, 2002. • J.Arlow, I.Neustadt, “UML e Unified Process,” McGraw Hill, 2003. • K.Beck, “Embracing change with eXtreme Programming,” IEEE Computer, Vol.32, No.10, 1999. Strutture dati, complessità e algoritmi: • T.H.Cormen, C.E.Leiserson, R.L.Rivest, “Introduction to Algorithms,” MIT Press, 1990. • R.Sedgewick, “Algorithms in C” Addison Wesley, 1990. • R.Sedgewick, “Algorithms in C++, parts 1-4” Addison Wesley, 1998. • C.Batini, L.Carlucci Aiello, M.Lenzerini, A.Marchetti Spaccamela, A.Miola, “Fondamenti di Programmazione dei Calcolatori Elettronici,” Franco Angeli Editore Milano, 1993. 13