La presentazione è in caricamento. Aspetta per favore

La presentazione è in caricamento. Aspetta per favore

Introduzione alla programmazione

Presentazioni simili


Presentazione sul tema: "Introduzione alla programmazione"— Transcript della presentazione:

1 Introduzione alla programmazione
Dott. Ing. Leonardo Rigutini Dipartimento Ingegneria dell’Informazione Università di Siena Via Roma 56 – – SIENA Uff

2 Algoritmo

3 Che cos’è l’informatica
L’informatica è lo studio sistematico degli algoritmi che descrivono e trasformano l’informazione: la loro teoria, analisi, progetto, efficienza, realizzazione (ACM  Association for Computing Machinery) Nota: È possibile svolgere un’attività concettualmente di tipo informatico senza l’ausilio del calcolatore, per esempio nel progettare ed applicare regole precise per svolgere operazioni aritmetiche con carta e penna; l’elaboratore, tuttavia, è uno strumento di calcolo potente, che permette la gestione di quantità di informazioni altrimenti intrattabili

4 Algoritmo Algoritmo: sequenza di istruzioni attraverso le quali un operatore umano è capace di risolvere ogni problema di una data classe; non è direttamente eseguibile dall’elaboratore Programma: sequenza di operazioni atte a predisporre l’elaboratore alla soluzione di una determinata classe di problemi Il programma è la descrizione di un algoritmo in una forma comprensibile all’elaboratore L’elaboratore è una macchina universale: cambiando il programma residente in memoria, è in grado di risolvere problemi di natura diversa (una classe di problemi per ogni programma)

5 Algoritmi nella realtà
Come abbiamo già detto un algoritmo può essere descritto in maniera informale come “una sequenza di passi, definiti con precisione, che portano alla realizzazione di un compito” Es: Le istruzioni di montaggio di un elettrodomestico Il calcolo del MCD (massimo comune divisore) Il prelievo di denaro dal bancomat: Inserimento tessera Inserimento codice segreto .ecc… Molte attività umane sono “algoritmi”

6 Algoritmo Un algoritmo deve essere:
comprensibile Altrimenti l’esecutore non può capirlo non-ambiguo: Altrimenti vi potrebbero essere diverse valide interpretazioni dello stesso algoritmo corretto Ovviamente dovrebbe effettivamente fare quello per cui è stato pensato efficiente (utile ma non vincolante) Dovrebbe eseguire il suo compito nel modo più veloce possibile

7 Codifica di un algoritmo
Quindi per risolvere un problema è necessario esprimerlo sotto forma di algoritmo: Una volta descritto in termini di passi più o meno elementari è possibile codificare questi passi utilizzando il linguaggio di programmazione prescelto Ricordiamo anche che per un dato problema possono esistere molteplici algoritmi che lo risolvono e che possono distinguersi per la migliore o peggiore complessità L’uso di un linguaggio di alto livello permette di utilizzare nomi per le variabili e per le funzioni molto vicini alla loro funzione: tassoInteresse per una variabile che indica il tasso d’interesse, ecc…

8 Schema dell’algoritmo
Solitamente la codifica di un problema in algoritmo avviene utilizzando i “diagrammi di flusso”, ovvero diagrammi grafici che permettono di visualizzare il “flusso” delle operazioni da compiere per arrivare alla soluzione Un diagramma di flusso schematizza l’ordine delle operazioni più o meno semplici richieste per risolvere un problema Ogni operazione di assegnamento è racchiusa in un rettangolo mentre le operazioni di verifica sono visualizzate in rombi

9 Diagrammi di flusso Esempio di codifica di un banale algoritmo con un diagramma di flusso: Rappresentare un grosso programma tramite un unico diagramma di flusso diventa comunque impossibile: Solitamente un algoritmo viene scomposto in unità funzionali distinte (funzioni, procedure o moduli) Ogni unità dovrebbe essere così semplice da poter essere facilmente schematizzabile tramite un diagramma di flusso leggi x e memorizzalo in a a > 100 Fai qualcosa Stampa “errore” false true

10 Linguaggi Il linguaggio utilizzato per descrivere l’algoritmo deve essere interpretabile dall’esecutore dell’algoritmo: Se abbiamo le istruzioni di montaggio di un elettrodomestico in cinese, non siamo in grado di seguire i passi necessari a terminare il lavoro (a meno che non si conosca il cinese!!) In informatica: I calcolatori sono “esecutori di algoritmi” Gli algoritmi sono descritti tramite programmi, cioè sequenze di istruzioni scritte in un opportuno linguaggio, comprensibile al calcolatore Compito dell’esperto informatico: Produrre algoritmi per un dato problema Codificarli in programmi (renderli comprensibili al calcolatore)

11 Precisione Il calcolatore esegue un programma passo-passo, in modo preciso, veloce e potente, senza deviare dal flusso delle istruzioni anche se esso è palesemente sbagliato: Questo perché il calcolatore è privo di “intelligenza”, non può decidere se una istruzione è sbagliata, se c’è, la esegue. Questo richiede quindi che le istruzioni siano puntuali e che l’algoritmo sia chiaro e puntuale: “per fare la torta di mele occorrono 3 Kg di mele, 3 uova e 0,5 Kg di farina”  è sufficientemente precisa “legate l’arrosto e salatelo”  non è sufficientemente precisa poiché per esempio non specifica la quantità di sale da utilizzare

12 Correttezza Un algoritmo è corretto se esso perviene alla soluzione del compito per cui è stato sviluppato, senza difettare di alcun passo. Se nell’algoritmo di montaggio di un mobile viene omessa l’istruzione di stringere una vite, il risultato finale può non essere “corretto” perché il mobile molto probabilmente non starà in piedi (dipende dalla importanza della vite!!) Sarà necessario individuare l’errore e ripararlo per avere il risultato corretto Un algoritmo non deve tralasciare nessun passo per giungere alla soluzione, altrimenti il suo risultato potrebbe essere non corretto.

13 Efficienza Un algoritmo dovrebbe raggiungere la soluzione nel minor tempo possibile e/o utilizzando la minima quantità di risorse, compatibilmente con la sua correttezza. efficienza in tempo e spazio (risorse) Es. Non è efficiente un algoritmo in cui prima viene regolato un dispositivo e poi, nel passo successivo, tale impostazione viene modificata, se poi deve essere riportata a come era prima Un buon informatico cercherà di ottimizzare un algoritmo il più possibile per cercare di arrivare alla soluzione il più veloce possibile e con l’utilizzo di minor risorse possibili

14 Complessità

15 Complessità La bontà di un programma viene valutata analizzando due fattori: Quanto è veloce il programma (complessità in tempo) Quante risorse richiede il programma (complessità in spazio) Solitamente, a meno di richieste esagerate, la complessità spaziale non è un problema, quindi si “guarda” molto più a quanto tempo ci vuole per eseguire un compito

16 Complessità La complessità temporale di un programma però non è di semplice valutazione Tempo di esecuzione: dipende dalla potenza della macchina su cui è eseguito dipende dalla dimensione dell’input (molti dati  molto più tempo) La soluzione è stata di assegnare un costo alle operazioni e calcolare quindi la complessità totale come costo complessivo: L’operazione di confronto ha costo uno Tutte le altre hanno costo nullo (non è vero, ma hanno un costo irrisorio rispetto al confronto)

17 Complessità Inoltre non è possibile dare una misura puntuale del costo di un algoritmo, poiché molte volte esso dipende dallo spazio dell’input: Si calcola quindi il costo in funzione della dimensione dell’input costo = f(n) , dove n è la dimensione dei dati su cui andiamo a lavorare Infine è necessario calcolare il costo nel caso migliore e nel caso peggiore : o(f(n)) per la complessità nel caso migliore (costo minimo) O(f(n)) per la complessità nel caso peggiore Il limite inferiore (o() ) non ha molto significato

18 Complessità Es. Problema: “Dati n numeri, trovare il più grande”
Algoritmo: MAXVAL = primo numero Per ogni numero seguente, se è maggiore di quello in MAXVAL, memorizzalo in MAXVAL Complessità: nel caso peggiore, il valore massimo si trova in ultima posizione; in questo caso sono necessari n-1 confronti. Quindi la complessità dell’algoritmo è O(n). Tale funzione (f(n)=n) è detta anche “lineare”, dato che cresce linearmente con n.

19 Complessità In realtà, l’esempio precedente, avrebbe complessità O(n-1), ma poiché per n >>1  O(n-1)~O(n), viene utilizzato O(n) come valore di complessità Questo vale per tutte le funzioni f(n) indicanti la complessità di un algoritmo: O(2n+4) , per n>>1  O(n) O(2n2+n+4), per n>>1  O(n2) Ecc.. cioè viene individuata la funzione maggiorante

20 Complessità La complessità dà un’idea della “fattibilità” dell’algoritmo. Supponiamo il costo di una operazione pari ad 1 secondo, il tempo necessario per un algoritmo con le varie complessità è il seguente: Come si vede, un problema con complessità polinomiale diventa molto velocemente “intrattabile” complessità n Logaritmica O(log(n)) Lineare O(n) Quadratic aO(n2) Polinomiale O(2n) 10 3 sec 10 sec 1,5 min 17 min 20 4-5 sec 20 sec 7 min 291 giorni 30 5 sec 30 sec 15 min 191 anni

21 Esempio: un algoritmo più complicato
Gestione di una biblioteca: Ogni libro si trova su uno scaffale e può essere preso in prestito ed in seguito restituito. La biblioteca è dotata di uno schedario ed ogni scheda contiene: Nome,cognome dell’autore (o autori) Titolo Data di pubblicazione Numero scaffale in cui si trova Numero d’ordine della posizione Le schede sono disposte in ordine alfabetico in base all’autore ed al titolo.

22 La biblioteca 1.1 Lettura della prima scheda dello schedario
Algoritmo di ricerca di un libro in biblioteca: Ricerca della scheda nello schedario Lettura del numero di scaffale e posizione del libro Ricerca dello scaffale indicato Prelievo del libro e scrittura sulla scheda delle informazioni sul prestito: data,nome ecc.. Ogni passo può (deve) essere descritto più dettagliatamente: sotto- algoritmo Passo1: 1.1 Lettura della prima scheda dello schedario 1.2 Se il nome dell’autore ed il titolo coincidono, ricerca conclusa 1.3 Si ripete il passo 2 fino trovare la scheda cercata o a terminare le schede 1.4 Se non viene trovata la scheda, la ricerca termina in modo infruttuoso

23 La biblioteca Il sotto-algoritmo per il passo 1, è corretto ma non efficiente: Ha una “complessità” lineare con il numero di schede, ossia nel caso peggiore tutte le schede nello schedario verranno analizzate E’ possibile definire un altro algoritmo per il passo 1 più efficiente: 1.1 Se lo schedario è vuoto ricerca infruttuosa 1.2 Lettura scheda centrale 1.3 Se è quella cercata, ricerca conclusa con successo 1.4 Se il nome dell’autore e/o titolo è precedente, si ripete l’algoritmo sulla prima metà dello schedario 1.5 Se il nome dell’autore e/o titolo è successivo, si ripete l’algoritmo sulla seconda metà dello schedario Nel caso peggiore, questo algoritmo analizza log2(n) schede: 16000 schede  alg1 = confronti 16000 schede  alg2 = 14 confronti

24 La biblioteca Nel secondo algoritmo presentato per automatizzare il passo 1, l’algoritmo richiama se stesso su un differente set di dati fino ad una terminazione positiva o negativa Un algoritmo di questo tipo si dice “ricorsivo”: ovvero il problema viene scomposto in sottoproblemi uguali ma su set di dati più piccoli, fino ad arrivare o alla soluzione o alla impossibilità di proseguire condizioni di stop: Il sottoinsieme su cui viene richiamato l’algoritmo è vuoto (1.1) La ricerca è andata a buon fine (1.3) Vedremo più avanti in dettaglio quali tipi di algoritmi esistono

25 Livelli di astrazione e storia dei linguaggi di programmazione

26 Programmazione di basso livello
Al livello più basso le istruzioni di un computer sono estremamente elementari. Il processore esegue le istruzioni macchina: Sequenze di bytes i codici delle operazioni e le locazioni in memoria Le CPU di fornitori diversi (Intel, SUN, Mac) hanno insiemi differenti di istruzioni macchina: Instruction set

27 Linguaggio binario Scrivere programmi direttamente in linguaggio macchina però risulta essere molto complicato: Sequenze di numeri con poca espressività Es Carica il contenuto della posizione di memoria 40 Carica il valore Se il primo valore è maggiore del secondo continua con l’istruzione in posizione 240

28 Assembler Il primo passo fu di assegnare nomi abbreviati ai comandi:
iLoad  “carica un numero intero” bipush  “inserisci una costante numerica” if_icmpgt  “se il numero intero è maggiore” L’esempio può essere riscritto così: iLoad 40 bipush 100 if_icmpgt 240 Similmente in seguito furono assegnati dei nomi anche alle locazioni di memoria (variabili): iLoad intRate if_icmpgt intError ASSEMBLER

29 Assemblatore Tale metodo di programmazione richiedeva quindi un software che traducesse il codice dal linguaggio ASSEMBLER al linguaggio macchina: ASSEMBLATORE La nuova forma di programmazione continua ad avere corrispondenza uno-a-uno con il linguaggio macchina Ad ogni istruzione ASSEMBLER corrisponde una istruzione del Instruction Set Rimane però molto difficile sviluppare grandi programmi utilizzando istruzioni di così basso livello: If_cmpgt, iLoad, ecc… lavorano su indirizzi in memoria, su registri della CPU ed anche un semplice confronto tra due variabili richiede una sequenza di alcune istruzioni assembler

30 Linguaggi di alto livello
Furono teorizzati allora linguaggi di programmazione che astraevano dalla architettura del calcolatore (indipendenza dall’ Instruction Set) e permettevano costrutti molto più vicini al “pensiero umano”: “se EXPR allora FAI QUESTO, altrimenti FAI QUEST’ALTRO” “ripeti L1 fino a che NON ACCADE QUESTO” Questi costrutti permettevano di racchiudere una sequenza di istruzioni macchina in una “descrizione” molto più vicina all’essere umano

31 La compilazione Questi linguaggi richiedono l’uso di un compilatore che traduca il codice di alto livello in codice macchina: Ogni linguaggio ha quindi il suo compilatore (o più di uno) Inoltre, dato che ciò che il compilatore genera è il codice macchina e quest’ultimo è strettamente dipendente dalla macchina (Hardware/SO) su cui gira, la compilazione traduce il codice da un linguaggio astratto in una forma dedicata e strettamente dipendente dalla macchina che si sta adoperando

32 La compilazione Quindi:
È il compilatore che si occupa di tradurre un dato linguaggio (es. C) nel codice macchina adatto alla macchina su cui dovrà essere eseguito Se un programma C viene compilato su ambiente Windows, sarà creato un programma che “girerà” solo su ambienti windows; similarmente, se lo stesso codice viene compilato su Linux, sarà possibile eseguire l’applicazione solo su una macchina Linux Un linguaggio di programmazione di alto livello deve rispettare RIGOROSE convenzioni affinché sia interpretabile dal compilatore

33 Linguaggi alto livello
se il tasso di interesse è maggiore di 100 scrivi a video un messaggio di errore Sviluppo If (intRate>100) printf(“Error”) C++ Compilazione

34 La compilazione Il processo di compilazione però “peggiora” le prestazioni rispetto ad un programma scritto direttamente in linguaggio macchina: Il tempo richiesto dal traduttore per generare il codice macchina Il codice oggetto generato dal compilatore tipicamente è meno “ottimizzato” rispetto a quello generato direttamente dall’essere umano In realtà: Anche considerando il tempo di traduzione del compilatore, il tempo necessario allo sviluppo del programma rimane comunque decisamente inferiore a quello necessario a sviluppare direttamente in codice oggetto Il codice generato dal compilatore è sì difficile che sia “ottimizzato”, ma comunque la perdita di prestazione risulta essere accettabile: Oggi inoltre tutti i compilatori hanno la possibilità di ottimizzare il codice E comunque non è detto che un essere umano riuscirebbe a generare codice ottimizzato per programmi molto complicati come quelli che si possono scrivere utilizzando linguaggi di alto livello

35 Un po’ di storia dei linguaggi di alto livello
A seguito di queste teorie nacquero i primi linguaggi di alto livello: FORTRAN (FORmula TRANslator): il primo linguaggio di alto livello sviluppato per descrivere formule matematiche COBOL (Common Business Oriented Language): il primo linguaggio orientato alle applicazioni gestionali Per molto tempo questi due linguaggi rimasero i linguaggi più diffusi fino a quando non fu studiata una categoria di linguaggi più rigorosamente basati su uno studio dei principi della programmazione: Il capostipite di questa famiglia fu ALGOL60 (ALGOrithmic Language) che pur non essendo mai stato utilizzato nella pratica, è famoso per questo motivo.

36 Un po’ di storia dei linguaggi di alto livello
Dopo l’ALGOL60, furono sviluppati molti nuovi linguaggi di programmazione “orientati” ad una programmazione generale : PASCAL, oggi molto diffuso per la didattica C ,tra i più potenti linguaggi di programmazione, divenuto molto popolare grazie al fatto che il sistema Unix fu interamente sviluppato utilizzando questo linguaggi di programmazione ADA, Basic, Perl, Phyton, ecc…. Anche questi però ad un certo punto furono superati da un nuovo paradigma: la programmazione ad oggetti C++, C# e Java derivati dal C Eiffel e Delphi derivati dal Pascal VisualBasic ecc…

37 Programmazione Procedurale

38 Linguaggio procedurale
La programmazione “classica” segue il paradigma di programmazione procedurale o imperativo Cosa significa: Il problema è scomposto in molti sottoproblemi che possono essere risolti in maniera semi-indipendente per poi essere aggregati per ottenere il risultato finale Ogni sotto-problema viene assegnato ad un sottoprogramma che si occupa di eseguire le operazioni per restituire il risultato (o effettuare il compito per cui è stato creato) L’applicazione stessa è vista come un sottoproblema: Assemblare i sotto-problemi individuati in maniera da ottenere il risultato

39 Linguaggio procedurale
Quindi seguendo questa idea, il programma è costituito da diverse funzioni (procedure o subroutines) ognuna disegnata per uno scopo Il corpo del programma è esso stesso una funzione, con la particolarità però di avere una forma (prototipo) standard: la funzione main() Quando un programma procedurale è compilato ed avviato, ciò che viene lanciato è la procedura main: Tutto ciò che deve essere eseguito dal programma deve essere implementato nella procedura main, ricorrendo a tutte le chiamate di funzione e dati necessari

40 Linguaggio procedurale
Dato A funzione1 Dato B funzione2 int main(int,int) Dato N funzioneM avvio dell’applicazione

41 Variabili e tipi di dato

42 Variabili Una variabile rappresenta un’area di memoria riservata dal compilatore per memorizzare dati Quest’area di memoria è riferita tramite un nome univoco: Questo nome permette di accedere alla memoria per leggere/scrivere informazioni In fase di programmazione, quando viene definita ed utilizzata la variabile non ha un valore, ma rappresenta tutti i valori che ciò che essa rappresenta potrà assumere Il valore è assegnato a run-time

43 Nomi di variabili Ogni variabile (e funzione che vedremo in seguito) è riferita tramite un nome Un nome in un linguaggio di programmazione è inteso come una etichetta che rispetta alcune regole fissate dal linguaggio stesso: Deve essere costituita da un singolo token. Etichette formate da più parole separate non sono accettate (il compilatore non può sapere che le due parole vanno interpretate insieme!!) Non può essere una delle parole riservate del linguaggio (if, else, while, return ecc…) Deve contenere solo caratteri alfa-numerici più il carattere ‘_’: questa condizione implica la prima poiché il carattere spazio ‘ ‘ non è permesso Non può comunque iniziare con un carattere numerico

44 Dichiarazione di variabili
Dichiarare una variabile significa avvertire il compilatore che riservi un’area di memoria perché in futuro essa verrà utilizzata con quel nome: Processo di allocazione della memoria Ogni variabile, prima di essere utilizzata, deve essere dichiarata: In caso contrario il compilatore genera un errore, non trovando la corrispondenza del nome con un area di memoria nella tabella simboli Quando una variabile viene dichiarata, deve perciò essere specificato sia il nome della variabile, sia il tipo di dato che tale variabile potrà assumere: Se una variabile viene dichiarata di tipo T, necessariamente essa potrà memorizzare valori di tipo T

45 Tipi di dato I tipi di dato si distinguono in tipi semplici e tipi strutturati: tipi di dato semplici – sono tipi di dato a cui può essere associato un singolo valore (numerico o stringa) ed un riferimento alla variabile è un riferimento al contenuto tipi di dato strutturati – sono tipi di dato composti da più “campi”, da cioè uno o più altri tipi di dato a loro volta semplici o strutturati Ogni linguaggio di programmazione mette a disposizione una serie di tipi di dati predefiniti: Tipi di dato semplici  bool, int, char, double, ecc… Tipi di dato strutturati  array

46 Tipi semplici predefiniti
Tutti i linguaggi di programmazione mettono a disposizione un numero di tipi di dato semplice “built-in”: bool Valori logici: true e false char Interi su 8 bit utilizzati per rappresentare numeri e caratteri (ASCII): signed/unsigned int Interi su 16 bit: signed/unsigned long Interi su 32 bit: signed/unsigned float Numeri in virgola mobile a 32 bit double Numeri in virgola mobile a 64 bit

47 Operatori Per questi tipi di dato semplici sono forniti dal linguaggio anche gli operatori: Assegnamento (=) Addizione (+) Sottrazione (–) Moltiplicazione (*) Divisione (/) Modulo (%) , ritorna il resto della divisione. Es. 5%3 = 2 Uguaglianza (==) e disuguaglianza (!=), ritornano vero o falso Minore o maggiore (<, <=, >, >=) , ritornano vero o falso

48 Il tipo char Il tipo char è un tipo semplice particolare:
Il nome deriva dall’abbreviazione di “character” ed infatti è utilizzato per rappresentare l’insieme finito dei caratteri (la tabella ASCII) Ogni carattere ha un codice numerico compreso tra 0 e 127 (7 bit), più un “extended set” compreso tra 128 e 255 (8° bit)

49 Il tipo char e le sequenze di escape
Alcuni codici sono riservati per i caratteri di controllo e solitamente sono indicati utilizzando il carattere “\” come carattere di escape: Molte volte è necessario utilizzare particolari sequenze di caratteri per rappresentare caratteri che altrimenti non sarebbe possibile visualizzare Es: Le stringhe sono sequenze di caratteri delimitate da “. Se vogliamo inserire un carattere “ all’interno di una stringa dobbiamo utilizzare la sequenza di escape \” per informare il compilatore che “ non delimita la fine della stringa Di solito le sequenze di escape sono utilizzate per i caratteri di controllo: “new line” \n, “tab”  \t, ecc….

50 Caratteri di controllo
escape ASCII codice Descrizione NULL Carattere nullo \b 8 BS Back Space \t 9 HT Horizontal Tab \n 10 LF Line Feed \r 13 CR Carriage Return La rappresentazione dei caratteri tramite un codice numerico permette un ordinamento tra gli stessi caratteri e quindi la possibilità di operazioni

51 Valori esadecimali È inoltre possibile assegnare valori in base 16 utilizzando la convenzione: 0xNNNN dove NNNN è il numero in base 16 Es int y=0x0A;  assegna 10 alla variabile y

52 Definizione di nuovi tipi semplici
Oltre ai tipi di dato semplici predefiniti, è possibile definirne di nuovi : Ridefinizione  typedef : il nuovo tipo di dato è solamente un alias di uno già esistente. Per esempio, l’istruzione C typedef int T1; definisce un tipo di dato T1 che altro non è che un altro nome per un intero (int) Enumerazione  enum : il nuovo tipo di dato può assumere solamente uno dei valori (numerici o meno) contenuti in una lista di enumerazione. Per esempio, l’istruzione C enum{“A”,”B”,”C”} D1; definisce una variabile D1 che può assumere solamente i valori “A”,”B”, o “C”. In questo esempio il tipo di dato è enum{….} ; per avere un tipo di dato T1 è necessario utilizzare il costrutto typedef: typedef enum{“A”,”B”,”C”} T1;  tipo di dato T1 T1 D1;  crea una variabile D1 di tipo T1

53 Tipi strutturati predefiniti
I linguaggi di programmazione forniscono anche dei tipi strutturati predefiniti: array  questo tipo di dato corrisponde ad una sequenza di celle consecutive di memoria di lunghezza fissata (e fornita al momento della dichiarazione della variabile di quel tipo). Tutte le celle devono necessariamente contenere lo stesso tipo di dato. Per esempio, in C, l’istruzione “int list[20];” dichiara la variabile list come una sequenza di 20 interi I recenti linguaggi di programmazione forniscono altri dati strutturati predefiniti per esempio: date, record, ecc…

54 Creazione di nuovi tipi strutturati
Oltre ai tipi “built-in” è possibile create nuovi tipi di dato strutturati tramite appositi costrutti del linguaggio: Es. in C (o C++) struct { int giorno; int mese; int anno; } D1; L’esempio mostra una situazione tipica in cui è preferibile utilizzare un dato strutturato: La data infatti può essere vista come una tripla di interi, ma gestire tre variabili intere separate per tutto il programma può essere contro- intuitivo per il programmatore. Risulta migliore, come si può immaginare, creare un dato D1 come aggregazione dei tre interi. Anche in questo esempio per definire un tipo di dato T1 si può utilizzare la funzione typedef

55 Dichiarazione ed inizializzazione
Una volta dichiarata, la variabile può essere utilizzata nel programma: La dichiarazione crea una istanza di quel tipo di dato con il suo relativo nome (normalmente si parla di variabile istanziata) Notare che la dichiarazione non inizializza la variabile ad un valore, ma semplicemente riserva uno spazio in memoria per quella variabile: un successivo accesso alla variabile non inizializzata, può restituire un valore non valido L’inizializzazione di una variabile è fatta con un istruzione di assegnamento: Può essere fatta al momento della dichiarazione Può essere fatta in un secondo momento

56 Cast Se memorizziamo un dato in una variabile di tipo diverso, è solitamente avviene un’operazione di “casting”, ovvero il compilatore traduce automaticamente il dato nella forma richiesta dal tipo della variabile: int  double ( 54  54,00) – non si ha perdita di precisione double  int (54,23451  54) – si perde la parte decimale signed char  unsigned char (-1  255) – vengono utilizzati tutti i 28 valori positivi generabili con 8 bit. Infatti: signed char [-27,27-1], di cui 1xxxxxxx sono i valori negativi [-128,-1]  [ , ] se non consideriamo il segno,  129 e  255 quindi -1  255 nel passaggio da signed ad unsigned (e viceversa)

57 Cast Il cast avviene in maniera implicita se è previsto dal linguaggio di programmazione (int  char, double  int, ecc…) altrimenti è necessario farlo in maniera esplicita definendo un’operatore di casting Per i tipi di dato semplici predefiniti il casting è quasi sempre fornito dal linguaggio di programmazione stesso

58 Dichiarazione delle costanti
Una dichiarazione di costante associa permanentemente un valore ad un identificatore La dichiarazione di una costante è praticamente la dichiarazione con assegnamento di una variabile con l’aggiunta della parola riservata const davanti, a specificare il fatto che quella cella di memoria conterrà il valore assegnato senza possibilità di modifica ES: const int numAlunni = 25;  numALunni conterrà 25 per tutto il suo ciclo di vita const char nome[16]=“leonardo”  nome conterrà “leonardo” senza possibilità di modifiche const date data = {5,10,2005}  data conterrà 5/10/2005

59 Istruzioni e controllo del flusso

60 Istruzioni Le istruzioni sono frasi proprie del linguaggio di programmazione delimitate dal simbolo “;” Es. read(x); è una istruzione – senza il simbolo terminatore, il compilatore non capirebbe dove termina l’istruzione Molte volte è necessario che sequenze di istruzioni vengano eseguite sequenzialmente: Corpo del programma, corpo di una funzione, corpo di un ciclo Una lista di istruzioni è delimitata dai simboli “{“ e “}”: “{“ inizia una lista di istruzioni “}” termina la lista E’ possibile innestare più liste di comandi

61 Struttura di un semplice programma
Un programma è organizzato in maniera sequenziale (non è più tanto vero) La prima parte è l’intestazione, ovvero una etichetta seguita da una coppia di parentesi tonde () Il significato di queste parentesi sarà spiegato in seguito A seguire inizia una lista di istruzioni, delimitate quindi da { e } Ma che tipo di istruzioni prevede un linguaggio?

62 Istruzione di assegnamento
Questa istruzione fondamentale viene utilizzata per assegnare a una variabile il valore di una espressione: Consiste nel simbolo = preceduto dal nome della variabile a cui deve essere assegnato il valore Alla destra del simbolo = viene specificate l’espressione che genera il risultato da assegnare alla variabile L’espressione può essere costituita da un valore costante (numero) , identificatori di altre variabili, espressioni aritmetico/logiche di combinazioni di valori e variabili e funzioni: x =23; w=‘a’; y =z; alfa = x + y; r3=(alfa * 43 –xgg)*(delta-32ijj); x = x + 1;

63 Istruzione di assegnamento
L’esecuzione di una istruzione di assegnamento comporta la valutazione della espressione a destra del simbolo = e la sostituzione del valore nella cella di memoria individuata dal nome della variabile a sinistra del simbolo = Si noti che un valore cotante può essre un numero ma anche un carattere: Nell’esempio w=‘a’, assegna il carattere ‘a’ alla variabile w

64 Controllo del flusso Il flusso delle istruzioni può cambiare a seconda della valutazione di una espressione: Può essere necessario per esempio effettuare una lista di operazioni L1 se una condizione è vera, una lista L2 altrimenti Oppure può essere necessario ripetere una lista di istruzioni fino a che non è verificata una condizione Le istruzioni che permettono queste scelte sono dette di controllo del flusso e come si evince dai due esempi qui sopra sono divise in due gruppi: Istruzioni condizionali Istruzioni iterative

65 Istruzione condizionale
Consente di valutare una espressione e di eseguire due sequenze distinte di operazioni a seconda del risultato ritornato dalla espressione Sintassi: if (E1) {L1} else {L2} Se E1 è vera verrà eseguita la lista di istruzioni L1, altrimenti sarà eseguita la lista L2 Il cosiddetto “ramo else” non è obbligatorio e può essere omesso nel caso in cui non vi sia alcuna lista L2 Se L1 e L2 sono composta da un’unica istruzione anche le parentesi graffe possono essere omesse: Es. if (E1) I1; else I2;

66 Castello di if Se è necessario verificare più espressioni in maniera innescata, è necessario fare attenzione a quale if fa riferimento ogni else if (E1) { if (E2) { L2; } else { if (E3) { L3; } }  chiude l’else di E2 L1; }  chiude l’else di E1

67 Istruzione iterativa while
Consente di ripetere la lista di istruzioni (iterare) più volte fino a che non risulta vera una condizione Sintassi: while (E1) {L1} L’espressione E1 viene valutata e se è vera, viene eseguito il corpo del ciclo. Terminato il corpo, viene di nuovo valutata E1, e se continua ad essere vera, il corpo è di nuovo eseguito. Questo è ripetuto fino a che la valutazione di E1 non ritorna false È possibile utilizzare all’interno del ciclo while l’istruzione break che interrompe incondizionatamente il ciclo, come se la condizione E1 fosse verificata

68 Istruzioni derivate: strutture di controllo
Teorema di Bohem-Jacopini: “Le istruzioni di controllo viste finora “if … else …” e “while” sono equivalenti alle strutture di controllo del linguaggio assemblatore basate sulla diretta e condizionale manipolazione del registro contatore di programma: tramite esse è possibile esprimere ogni qualsiasi altra struttura di controllo di ogni qualsiasi altro linguaggio di programmazione” Questo teorema ci dice che sebbene sia possibile “pensare” a molte altre possibili strutture di controllo di alto livello, per ognuna di esse si può sempre trovare una forma equivalente in termini di “if…else…” o di ciclo “while”: La specifica di nuove strutture di controllo rimane comunque utile per fornire ai programmatori strumenti più vicini ai casi concreti

69 Istruzioni condizionali derivate: else if
Quando sono necessarie valutazioni di più espressioni su di una variabile è possibile utilizzare il costrutto else if: if (E1) { L1 } else if (E2) { L2 } …. else if (En-1) { Ln-1 else { Ln In alcuni linguaggi “else if” è stato condensato in un comando unico “elseif”

70 Istruzioni condizionali derivate: switch…case
Molti linguaggi per evitare enormi castelli di if, mettono a disposizione un costrutto derivato: switch … case Sintassi: switch (E1) { case A: LA; break; case B: Lb; …. default: Ldefault; }

71 Istruzioni condizionali derivate: switch…case
L’istruzione switch-case, valuta l’espressione E1 e suppone che siano possibili vari casi Per ognuno dei casi viene fornita una lista di istruzioni, seguita dal comando break Nel caso si verifichi una condizione non specificata da alcun case , viene fornita una scelta di default Il comando break non è obbligatorio, serve solamente per evitare che nel caso A, il programma prosegua effettuando anche le istruzioni relative al caso B, caso C, e così via fino al caso di default. Se all’interno di un particolare algoritmo è richiesto un tale comportamento, il comando break può essere rimosso NB: il costrutto switch-case lavora solamente su valori numerici La valutazione di E1 deve ritornare quindi un valore numerico

72 Istruzioni iterative derivate: for
Consente di ripetere la lista di istruzioni (iterare) un numero prefissato di volte Sintassi: for (START; EXPR; STEP) {L1;} START è la condizione iniziale da cui partire; EXPR è la espressione che viene valutata ad ogni iterazione per decidere se interrompere o meno il ciclo; STEP è la modifica che viene effettuata ad ogni passo Es. for (i=0;i<N;i++) {L1;} per i che va da 0 a N, con passo 1, fai L1. Questo ciclo esegue N volte la lista di istruzioni L1.

73 Istruzioni iterative derivate: for
L’istruzione for è in realtà un modo diverso per scrivere la forma while. Essa può essere infatti sostituita con: i=0; while (i<N) { L1; i=i+1; } dove N è conosciuto. Oramai comunque è diventata una forma di istruzione “standard” in tutti i linguaggi di programmazione.

74 Istruzione iterative derivate: do…while
Così come l’istruzione for può essere vista come un caso particolare del più generale ciclo while, così molte altre istruzioni condizionali sono state proposte nei vari linguaggi di programmazione do … while do {L1;} while (E1) – ripete la lista di istruzioni fino a che E1 non è verificata; a quel punto esce dal ciclo La differenza importante dal ciclo while è il punto in cui E1 viene valutata: - nel ciclo while viene valutata prima di eseguire L1, - nel ciclo do-while è valuata dopo aver eseguito almeno una volta il corpo del ciclo

75 Istruzione iterative derivate: goto
Molti linguaggi hanno mantenuto tra le proprie strutture di controllo “istruzioni di salto” direttamente ispirate alle istruzioni macchina dei processori: Istruzione goto Sintassi: goto LABEL; LABEL: L1 L’istruzione goto permette di saltare l’esecuzione del programma alla linea di codice individuata dalla etichetta LABEL. Sono molto poco utilizzate ed anzi i teorici della programmazione sostengono che “se siamo costretti ad utilizzare un istruzione goto, abbiamo sicuramente sbagliato qualcosa nell’impostare l’algoritmo”

76 Funzioni e procedure

77 Sottoprogrammi Nella creazione di un programma può accadere che problemi simili appaiano in mole parti diverse La soluzione più ovvia è quella di replicare il codice in ogni parte dove è necessario, ma questa soluzione non è certamente la più “friendly”: Se il codice ha un errore, infatti, esso sarà replicato in tutte le parti in cui quel codice è stato copiato Se quella parte di codice viene aggiornata, sarà necessario aggiornare tutte le parti in cui è stato copiato Molto poco flessibile e manutenibile: uso di sottoprogrammi

78 Sottoprogrammi Sottoprogrammi: parti di codice separate dal flusso principale del programma che possono essere richiamate dove necessario senza replicare il codice ogni volta Un sottoprogramma deve essere innanzitutto definito, cioè deve essere dichiarato da qualche parte ciò che esso esegue, su cosa lo esegue e cosa restituisce (se restituisce qualcosa) Una volta definito, esso deve essere implementato, cioè scritto il corpo del sottoprogramma, con le variabili e le istruzioni che permettono di eseguire il compito per cui è stato progettato Infine, una volta dichiarato e implementato, esso può essere richiamato ogni qualvolta è richiesta una operazione di quel tipo

79 Funzioni e procedure Ma cosa sono di preciso i sottoprogrammi?
Ogni sottoprogramma è un blocco più o meno lungo di codice che riceve in ingresso alcuni parametri (anche nessuno) e può restituire un valore: I parametri che riceve in ingresso sono i valori che il chiamante passa alla procedura: sono quindi variabili in cui a run-time vengono memorizzati i valori attuali dei parametri Il sottoprogramma utilizza questi parametri per eseguire il suo compito ed “eventualmente” restituisce a sua volta al chiamante un valore

80 Funzioni e procedure Formalmente due tipologie di sottoprogrammi sono state individuate: Le funzioni, che in analogia con le funzioni matematiche, ricevono alcuni valori in ingresso e restituiscono un valore in uscita Le procedure, che ricevuti alcuni dati in ingresso, eseguono un compito che non può concettualmente essere associato ad una funzione (es. stampa, aggiornamento ecc…) Diciamo subito che la distinzione è abbastanza formale e che chiamare entrambi funzioni o procedure non comporta alcun problema Le procedure, in efetti possono essere assimilate a funzioni che non restituiscono alcun valore, ma eseguono semplicemente un compito

81 Funzioni e procedure I parametri che la funzione utilizza per “ricevere dati dall’esterno” sono variabili locali: Ovvero esistono localmente alla funzione Come tali devono avere un tipo ed un nome (univoco all’interno della funzione) Anche il valore di ritorno della funzione è un dato di un certo tipo, quindi nella descrizione della funzione sarà necessario specificare che tipo di dato essa ritorna

82 Le funzioni Una funzione è una parte di programma individuato da una etichetta che riceve in ingresso dei parametri e ritorna un risultato, terminando sempre il suo compito I parametri d’ingresso sono chiamati semplicemente parametri della funzione Il valore di uscita è detto valore di ritorno della funzione Come accennato in precedenza una funzione deve essere prima definita, ovvero deve venire specificato la struttura della funzione stessa: il nome univoco della funzione i parametri che riceve in ingresso il tipo di dato che essa restituisce

83 Dichiarazione di funzione
Prima di implementare una funzione è necessario dichiarare la sua “interfaccia” con l’esterno, il modo cioè con cui essa è invocata ed il tipo di risultato che ritorna: La dichiarazione di una funzione specifica il prototipo della funzione Es: double potenza( int x, int y)  è una funzione che riceve due parametri di tipo int (x e y) e restituisce un double Come la dichiarazione di una variabile non inizializzava la variabile, così la dichiarazione di una funzione non realizza la funzione: In questo modo per ora è stato dichiarata solamente la interfaccia della funzione

84 Dichiarazione di funzione
La dichiarazione di una funzione (prototipo) è utile per far capire al compilatore che la funzione esiste e che forma ha: La funzione poi, potrebbe essere implementata in qualsiasi altro file del progetto e compilata “dopo” il file principale, ma l’importante è che nel file principale sia riportato il prototipo della funzione che viene utilizzata altrimenti il compilatore non può continuare Questo metodo permette di scomporre il progetto in più file in cui implementare le varie procedure, organizzandole in maniera più intelligente: E’ importante che quando una funzione viene utilizzata in qualche file, sia specificato in quel file il prototipo di quella funzione Il linker si occupa di “collegare” il corpo della funzione nei punti in cui la funzione è chiamata

85 Dichiarazione di funzione
La sintassi comune di una dichiarazione di funzione è la seguente: [tipo_di_dato] [nome_della_funzione] ([lista di parametri]) Dove [lista di parametri] = lista di dichiarazioni di variabili visibili dall’interno e dall’esterno della funzione Es. double potenza( int x, int y); Questa riga di codice dice che da qualche parte esiste una funzione potenza che riceve in ingresso due interi, esegue qualcosa e ritorna un double.

86 Dichiarazione di funzione
Se una funzione (o procedura) non ritorna alcun valore, si utilizza un tipo di dato apposito void : void stampa(String file); È possibile infine avere funzioni che non richiedono parametri, poiché quello che devono fare è indifferente da valori esterni: void stampa(); In molti linguaggi (C++) è possibile specificare valori di default ai parametri della funzione: Valori che se non forniti alla funzione

87 Valori di default Se un parametro normalmente assume un certo valore (=K) e solo poche volte varia, è possibile inserire il valore K come valore di default: se quel parametro non è fornito, la funzione utilizza il valore di default double potenza( int x, int y); Ad esempio: double ris = potenza(4,2)  16 NB: se un parametro ha un valore di default, allora tutti i parametri seguenti devono avere un valore di default, altrimenti durante la chiamata con valore di default il compilatore non sa più a quale parametro fare riferimento: double potenza( int x=2, int y);  se chiamiamo int r=potenza(4); il compilatore non sa se associare 4 ad x o a y.

88 Implementazione della funzione
La funzione deve comunque essere implementata (cioè realizzata) da qualche parte nel progetto: La definizione del prototipo infatti non specifica cosa deve fare la funzione L’implementazione di una funzione avviene dichiarando il prototipo della funzione stessa ed iniziando immediatamente dopo un blocco di istruzioni: double potenza( int x, int y) { Lista di istruzioni; } Il blocco nell’esempio qui sopra è delimitato dalle parentesi graffe, come avviene nel C, C++ e JAVA.

89 Implementazione della funzione
All’interno del corpo di una funzione, possono essere dichiarate nuove variabili, utilizzando anche nomi di variabili utilizzati in altre funzioni: Ogni variabile interna ad una funzione ha infatti una visibilità limitata a quella funzione ed una volta usciti dalla funzione le variabili vengono deallocate Le varibili allocate all’interno di una funzione sono chiamate variabili locali, per distinguerle dalle variabili globali che sono variabili dichiarate fuori da ogni altra funzione, compresa la principale e sono quindi visibili sempre ed ovunque: Se all’interno di una funzione viene utilizzato un nome di variabile globale, ogni riferimento a quel nome viene reindirizzato alla variabile locale

90 il valore viene restituito tramite l’istruzione return
Quando la funzione deve ritornare un valore è necessario al suo interno contenga una istruzione di ritorno valore che termina la funzione e restituisce un dato conforma al prototipo: il valore viene restituito tramite l’istruzione return Sintassi: return [VALORE o VARIABILE]; Il tipo di valore restituito deve coincidere con il tipo di valore specificato nella dichiarazione della funzione, altrimenti il compilatore ritorna errore.

91 Istruzione return Il valore di ritorno può essere una valore costante oppure il contenuto di una variabile: Nel secondo caso la variabile dovrà essere dello stesso tipo del dato da ritornare ES double potenza( int x, int y) { double ris=1; for (int i=0;i<y;i++) { ris=ris*x; } return ris; ris=0;

92 Istruzione return Quando la funzione non deve ritornare alcun valore l’istruzione return non è richiesta: è però possibile inserirlo senza specificare il dato ritornato: void stampa(String file) { L1; return; }

93 Istruzione return L’istruzione return può essere utilizzata anche per interrompere l’esecuzione della funzione senza attendere la fine della lista di istruzioni: ad esempio può accadere che se una condizione è verificata, la funzione ritorna un valore X, altrimenti esegue altre operazioni e ritorna un valore Y In questo caso avremo due istruzioni return, ma eseguite in maniera esclusiva (o l’una o l’altra) ES. double potenza( int x, int y) { if (y==0) { return 1; } double r=(calcola la potenza xy); return r;

94 Chiamate alle funzioni
Una volta dichiarata ed implementata, una funzione può essere richiamata laddove è necessario la chiamata di una funzione è semplicemente il nome della funzione seguito dai parametri da passare: Es. double r=potenza( 2, 4) in questo caso r assume il valore ritornato dalla funzione potenza con i parametri x=2 e y=4 È possibile passare anche variabili invece che valori numerici costanti: Es. int a=2; int b=4; double r=potenza(a,b);

95 Chiamate alle funzioni
Il prototipo deve necessariamente essere rispettato: double potenza( int x, int y)  double potenza( 2, 3) se viene omesso qualche parametro, il compilatore dà errore: il compilatore cerca una funzione con prototipo diverso e non la trova

96 Passaggio di parametri

97 Passaggio di parametri
Cosa succede quando si passa una variabile come parametro ad una funzione? Ossia, se all’interno della funzione modifico il valore di quel parametro, il valore della variabile originaria viene modificato ? La risposta è: dipende !! Esistono due modi per il passaggio dei parametri: Per valore Per riferimento

98 Passaggio di parametri per valore
Quando è richiesto che i valori delle variabili non siano modificati dal corpo della funzione, quello che avviene è che il valore della variabile viene copiato nella variabile-parametro della funzione. In questo modo: ogni modifica alla variabile locale non si riflette sulla variabile esterna ogni modifica fatta alle variabili locali, però, va persa all’uscita dalla funzione e solo il risultato ritornato è visibile dall’esterno della funzione Questo metodo di passaggio dei parametri va sotto il nome di passaggio per copia o per valore

99 Passaggio di parametri per riferimento
Quando invece è richiesto che le variabili interne alla funzione siano non una copia di quelle esterne, ma la stessa cosa, si parla di passaggio per riferimento: In questo caso, la variabile locale assume il riferimento in memoria alla variabile esterna Quindi ogni modifica ad essa è una modifica alla cella di memoria originale e permane anche alla chiusura dalla funzione Normalmente questa situazione è richiesta quando si ha un sottoprogramma che deve effettuare delle operazioni su un dato e non ritorna alcun valore: ES. aggiornamento del fatturato totale, ecc… in questo caso il fatturato totale, una volta aggiornato deve rimanere tale, quindi è necessario operare l’aggiornamento sulla variabile originale e non su una copia del valore

100 Passaggio di parametri
Notiamo che il passaggio per valore richiede la copia della variabile nella nuova variabile locale, mentre il riferimento ha un costo costante, viene copiato comunque un indirizzo di memoria Se stiamo passando dati strutturati particolarmente complessi, array di grandi dimensioni, ecc… il passaggio per valore può essere molto più lento del passaggio per riferimento E’ anche vero però che il passaggio per valore è molto più safety rispetto all’altro, dato che qualsiasi modifica, anche quella non voluta ed erronea non si riflette sul dato originario

101 Passaggio di parametri
Il metodo utilizzato per il passaggio dei parametri cambia da linguaggio a linguaggio: Il C e C++ utilizzano il passaggio per valore, ma forniscono dei tipi di dato che permettono di realizzare il passaggio per riferimento (puntatori e riferimenti) Il JAVA utilizza il passaggio per riferimento. Il passaggio per valore può comunque essere effettuato costruendo ogni volta una copia del parametro e passare quella alla funzione.

102 Funzioni e procedure La differenza sul metodo di passaggio dei parametri stabilisce la principale differenza tra una funzione ed una procedura: Funzione: lo scopo principale è quello di utilizzare alcuni valori in ingresso per produrre un valore di uscita, senza modificare null’altro  passaggio per valore Procedura: lo scopo principale è quello di operare delle operazioni sui parametri in ingresso che abbiano una semantica tale da poterle racchiudere in un corpo unico  passaggio per riferimento Comunque la differenza è solamente formale: In C++ o in JAVA non esistono metodi per dichiarare una funzione piuttosto che una procedura. In realtà il primo prevede solamente funzioni, il secondo procedure.

103 Passaggio di parametri per valore
Spazio di memoria del chiamato Spazio di memoria del chiamante copia par1 int a modifica locale Programma chiamante chiamata di funzione Funzione chiamata valore di ritorno

104 Passaggio di parametri per riferimento
Spazio di memoria del chiamato Spazio di memoria del chiamante riferimento par1 int a modifica modifica locale Programma chiamante chiamata di funzione Funzione chiamata valore di ritorno

105 La funzione main

106 Il corpo del programma Adesso che abbiamo acquisito il concetto di funzione è possibile descrivere la struttura completa di un programma (procedurale) Un programma procedurale è composto da: Dichiarazioni di variabili Dichiarazioni di funzioni (prototipi) Implementazioni delle funzioni (anche su più file) Corpo del programma Ma cosa s’intende per “corpo del programma” ? Il corpo del programma è a sua volta una funzione globale particolare che utilizzando tutte le altre funzioni ed i dati a disposizione, assembla il tutto per eseguire il compito ell’algoritmo

107 La funzione main In definitiva, un programma non è altro che una funzione dal prototipo particolare e fissato: int main(int argc,char[] argv) Questa funzione accetta due parametri in ingresso e restituisce un valore intero in uscita: I due parametri in ingresso permettono di leggere eventuali parametri passati da linea di comando all’applicazione Es. il comando copy in windows accetta due parametri copy file_a file_b  argc=2, argv=[file_a,file_b] Il valore di ritorno permette di stabilire se l’esecuzione del programma è andata a buon fine o meno. Solitamente un programma ritorna 0 se esce correttamente, 1 se vi sono stati errori C e C++

108 Variabili globali e locali
La dichiarazione di una variabile normalmente può avvenire in qualsiasi punto del programma, sempre però prima che la variabile sia utilizzata per la prima volta In particolare, la variabile può essere dichiarata “fuori” da qualsiasi funzione oppure all’interno di una funzione (o procedura : Nel primo caso, ”ambito globale”, la variabile esiste in ogni parte del programma (con le debite eccezioni) , a partire dal momento in cui il compilatore legge la riga di dichiarazione e la istanzia Nel secondo caso, “ambito locale”, invece, la variabile esiste solamente all’interno della funzione in cui è stata dichiarata e sarà “deallocata” all’uscita della funzione stessa

109 Visibilità di variabili (scope)
Ma cosa accade se una variabile locale ha lo stesso nome di una variabile globale? Quando la funzione è chiamata, con quel nome a quale variabile ci riferiamo, quella globale o quella locale? Risposta: la variabile più “vicina”, è quella a cui viene fatto riferimento con il nome (“nesting rule”) Il campo di visibilità di una variabile (scope) è sempre quello ultimo in cui la variabile è stata dichiarata, anche se due variabili portano lo stesso nome Quindi durante l’esecuzione di tutta la procedura, ogni riferimento alla variabile A sarà risolto con un riferimento alla variabile A locale alla procedura. Tale riferimento cessa non appena la procedura e lo scope relativo ad essa terminano

110 Lo spazio di memoria globale
Per tutto quello che si è detto, anche le variabili dichiarate nella funzione principale sono variabili locali alla funzione main: Un programma allora è composto da più spazi di memoria distinti, di cui solamente uno, lo spazio di memoria globale è condiviso in tutto il progetto Lo spazio di memoria globale è composto dalle variabili dichiarate fuori da ogni funzione, compresa la funzione principale (il main())

111 Schema di un programma procedurale
Spazio di memoria globale Spazio di memoria delle funzioni Spazio di memoria del main f1 int main(int,int) f2 fn Chiamate di funzione Accessi alla memoria globale

112 Le funzioni nella CPU

113 Cambio di contesto Ma cosa succede a basso livello quando viene chiamata una funzione ? La chiamata di una funzione genera un cambio di contesto (context switch) nella CPU: Lo stato attuale della CPU deve essere salvato da qualche parte affinchè una volta eseguita la procedura sia possibile riprendere correttamente l’esecuzione delle istruzioni Anche i registri devono essere salvati dato che le istruzioni della procedura non devono assolutamente modificare i valori “eterni” alla procedura stessa Lo stato del processo viene allora salvato sullo stack di sistema

114 Lo stack Una parte di memoria centrale viene gestita come stack di sistema (pila) Alla chiamata di una procedura: il contesto attuale viene inserito in cima allo stack: operazione push Prepara la CPU ad eseguire la nuova procedura: Setta l’istruzione da eseguire, il registro dati e quello indirizzi con i parametri passati alla procedura La procedura esegue le istruzioni Quando la procedura termina, il contesto in cima allo stack viene ripristinato: Operazione pop Lo stack memorizza le chiamate a procedura e le richiama in ordine inverso di memorizzazione

115 Stack – 1 Alla chiamata della funzione f, il contesto della funzione in esecuzione attualmente viene memorizzato in cima allo stack main() f1 f2 f4 CPU stack f3 funzione in esecuzione push

116 Stack – 2 Quando poi la funzione termina, viene ripristinato l’ultimo contesto inserito nello stack, la funzione f(t-1): main() f1 f2 stack CPU f f3 Funzioni chiamate e terminate funzione in esecuzione pop

117 Stack – 3 Quando anche la funzione f(t-1) termina, viene ripristinato il contesto di f(t-2), e così via… f3 main() f1 f2 stack CPU f Funzioni chiamate e terminate funzione in esecuzione pop

118 Stack – 4 L’ultimo contesto nello stack sarà quello relativo alla funzione main(). Quando questo termina, l’applicazione termina. f3 main() f1 f2 stack CPU f Funzioni chiamate e terminate funzione in esecuzione pop empty

119 La ricorsione

120 La ricorsione Si parla di ricorsione quando all’interno di una funzione P, viene richiamata P stessa, normalmente su un diverso set di dati La chiamata ricorsiva può essere sia esplicita che implicita: Esplicita – la funzione P è richiamata esplicitamente all’interno del corpo di P stesso Implicita – la funzione P è richiamata attraverso la chiamata ad un’altra funzione Q che contiene la chiamata a P A prima vista la cosa può apparire sorprendente dato che una funzione (o procedura) è dedita a risolvere un sottoproblema: La ricorsione è come dire che per risolvere il sottoproblema P è necessario risolvere il sottoproblema P stesso !!

121 La ricorsione In realtà la programmazione ricorsiva a profonde radici concettuali derivanti dal fatto che “per molti problemi la soluzione per un caso generico può essere ricavata sulla base della soluzione di un caso più semplice dello stesso problema”: L’ordinamento di un array, il calcolo del fattoriale, ecc… sono tipici problemi di questo tipo

122 Es. somma come ricorsione
La ricorsione è nascosta in moltissime forme del ragionamento umano. Per esempio la somma tra due numeri può essere vista come ricorsione dell’operatore successore(): successore(x)=numero successivo ad x Algoritmo ricorsivo per l’addizione x+0 = x; x+y = x+successore(y-1) = successore(x+y-1) 1+3 = successore(1+2) = successore(sucessore(1+1)) = = successore(successore(successore(1+0))) = = successore(successore(successore(1))) = = successore(successore(2)) = successore(3) = 4

123 Es. 1 – Pesate Problema: date un gruppo di palline di forma identica e tutte di peso identico tranne una di peso maggiore delle altre, trovare la pallina più pesante utilizzando una bilancia ed il minor numero di pesate (supponiamo di avere un numero di palline multiplo di 3) Algoritmo: Dividi il gruppo in tre gruppi di egual numero di palline e pesa due di questi gruppi Se i due gruppi hanno lo stesso peso scartarli entrambi, altrimenti scartare il gruppo non pesato e quello sulla bilancia con il peso minore (la pallina cercata si troverà nel gruppo con peso maggiore) Dividi il gruppo rimasto in tre sottogruppi e ripeti il procedimento finché i sottogruppi non contengano una sola palline: in tal caso l’ultima pesata indica quale delle tre palline rimaste è quella cercata

124 Es. 1 – Pesate Notiamo come l’ultimo punto sia la ripetizione dell’algoritmo su un set ridotto di dati fino a che non ci troviamo di fronte ad un caso notevole, di cui possiamo dare una immediata soluzione: tre gruppi di una pallina ciascuno Per avere una visione più informatica dell’algoritmo: Se il gruppo di palline consiste di una sola pallina, la pallina è la soluzione cercata, altrimenti … Dividi il gruppo di palline in tre sottogruppi e poni due sottogruppi sulla bilancia Se i due gruppi hanno peso uguale scartarli entrambi, altrimenti scartare il gruppo non pesato e quello di peso minore Applica l’algoritmo pesate al sottogruppo individuato

125 Es. 2 – Successione di Fibonacci
La successione di numeri di Fibonacci fu definita dal matematico come modello di crescita dei conigli in allevamento: f0=0 f1=1 fn=fn-1 + fn-2 , per n> 1 Es. calcolando f per i primi 6 numeri: f2=f1+f0=1+0=1 f3=f2+f1=1+1=2 f4=f3+f2=2+1=3 f5=f4+f3=3+2=5

126 Es. 3 – Fattoriale Il fattoriale di un numero è la produttoria di tutti i numeri fino al numero stesso, escluso lo 0: 5! = 5 x 4 x 3 x 2 x 1 = 120 Il fattoriale può essere espresso in forma ricorsiva: 0! = 1 n! = n x (n-1)! Infatti: 5!=5 x (4 x 3 x 2 x 1) = 5 x 4! Anche il fattoriale è un tipico esempio di algoritmo ricorsivo

127 Es. 4 – ricerca in un vettore
Un esempio tipico di algoritmo di ricerca dati in un vettore ordinato è il seguente: Se il vettore è composto da un solo elemento, confronta l’elemento con D e se è uguale ritorna D altrimenti ritorna “non trovato” Seleziona l’elemento centrale P del vettore Confronta il dato D da cercare con l’elemento centrale P Se D<P, ripeti l’algoritmo sulla prima metà del vettore, altrimenti ripetilo sulla seconda

128 Es. 5 – Sommatoria La sommatoria di una sequenza di n numeri può essere vista come la somma numero n-esimo alla sommatoria dei rimanenti (n-1) numeri: La sommatoria comunque può essere risolta semplicemente con un ciclo, risparmiando così il carico aggiuntivo della ricorsione

129 Caso generale Si può generalizzare il concetto di ricorsione. Affinché un algoritmo sia scrivibile in forma ricorsiva, è necessario che: l’algoritmo abbia una soluzione banale, f(0)=0 l’algoritmo al passo n possa essere espresso come funzione del valore al passo (n-1) La condizione banale è necessarie per evitare una ricorsione infinita: Se infatti non esiste una condizione in cui siamo in grado di dare la soluzione, le iterazioni non terminano mai In questo caso, solitamente si verifica un errore di stack overflow, ovvero pila piena

130 Ricorsione e stack La ricorsione viene realizzata mantenendo nello stack la sequenza delle chiamate alla procedura che non possono essere risolte fino al caso banale Una volta trovata la soluzione banale questa viene passata a ritroso a tutte le istanze dell’algoritmo sospese nello stack fino alla istanza finale che restituisce il risultato finale

131 Successione di Fibonacci
Scriviamo l’algoritmo ricorsivo per la successione di Fibonacci: int fibonacci(int n) { int ris=0; if (n==0) return 0; else if (n==1) return 1; else ris = fibonacci(n-1) + fibonacci(n-2); }

132 Fattoriale e sommatoria
Scriviamo l’algoritmo ricorsivo per il calcolo del fattoriale: int fatt(int n) { int ris=0; if (n==0) return 1; else ris = n * fatt(n-1); return ris; } Scriviamo l’algoritmo ricorsivo per la sommatoria: int sum(int a[]) { if (vuota(a)==true) return 0; else ris = testa(a) + coda(a);

133 Fattoriale – 1 y=fatt(5) CPU fatt(2) push fatt(3) fatt(1) fatt(4)
stack fatt(2) funzione in esecuzione push

134 Fattoriale – 2 y=fatt(5) CPU pop fatt(2) fatt(1)=1
stack CPU fatt(1)=1 fatt(2) Funzioni chiamate e terminate funzione in esecuzione pop

135 Fattoriale – 3 y=fatt(5) CPU pop fatt(5) fatt(4)=24
stack CPU fatt(1)=1 Funzioni chiamate e terminate funzione in esecuzione pop empty

136 Programmazione ad oggetti

137 Paradigma ad oggetti Come detto, i linguaggi di programmazione procedurali sono stati quasi tutti sostituiti dalle rispettive versioni ad oggetti (tranne nei casi in cui la programmazione ad oggetti non è “consigliata”, ad esempio programmazione di basso livello, S.O., programmazione PLC ecc…) Ma cos’è questo diverso tipo di programmazione? In cosa differisce dalla programmazione “classica”?

138 Object Oriented Programming (OOP)
La maggior parte dei programmi utili non manipola soltanto numeri e stringhe, ma gestisce dati più complessi e più aderenti alla realtà: Conti bancari, informazioni sugli impiegati, forme grafiche… L’idea della programmazione ad oggetti è di rappresentare questi dati così come sono anche nel linguaggio di programmazione: gli oggetti o classi La programmazione ad oggetti estende al massimo l’idea di dato strutturato presente nella programmazione classica: una classe (o oggetto) è un tipo di dato strutturato a cui sono associate variabili e funzioni

139 Object Oriented Programming (OOP)
Il programmatore individua degli oggetti nella applicazione che deve progettare: Conto corrente, client e server, documento, ecc… Ogni oggetto ha delle proprietà e delle capacità (o funzioni) L’applicazione è poi costruita facendo interagire correttamente gli oggetti esistenti nell’ “ambiente” che si va a progettare Ma la funzione main() che fine ha fatto ? Se non si usano più le procedure/funzioni globali, come si rende eseguibile un programma? Cioè qual è il corpo principale dell’applicazione? Due filosofie differenti per rispondere a queste domande: il C++ ed il JAVA

140 Object Oriented Programming(OOP)
Il C++ ha mantenuto comunque una certa compatibilità con i linguaggi procedurali: in C++ è ancora possibile infatti definire funzioni globali, variabili globali e quindi la funzione main() rimane una funzione definita globalmente e “cercata” dal compilatore per rendere eseguibile un programma Il JAVA ha seguito un altro approccio: In JAVA non esistono funzioni globali, cioè esterne da qualsiasi oggetto, ma ogni funzione o variabile deve appartenere ad un oggetto (o meglio classe): è possibile però definire una funzione main() per ogni classe e “rendere eseguibile” la classe stessa In compilazione, vengono creati degli oggetti eseguibili (non un unico programma). La scelta di cosa far partire viene fatta dall’utente selezionando l’oggetto eseguibile desiderato.

141 Object Oriented Programming(OOP)
Es di oggetti JAVA Normalmente quello che accade in JAVA è che viene creato un oggetto che rappresenta l’applicazione e che contiene la funzione main(): Eseguire l’applicazione significa lanciare il main() di tale oggetto ObjectB: datoX funzione1 funzione2 funzione3 main() ObjectA: datoY funzione1 funzione2 main()


Scaricare ppt "Introduzione alla programmazione"

Presentazioni simili


Annunci Google