Scarica Elementi di Programmazione 1 e più Appunti in PDF di Programmazione C solo su Docsity!
Elementi di
Programmazione 1
Gabriel Rovesti
Attenzione Il file non ha alcuna pretesa di correttezza; di fatto, è una riscrittura attenta di appunti, slide, materiale sparso in rete, approfondimenti personali dettagliati al meglio delle mie capacità. Credo comunque che, per scopo didattico e di piacere di imparare (sì, io studio per quello e non solo per l’esame) questo file possa essere utile. Semplice si pone, per davvero ci prova. Thank me sometimes, it won’t kill you that much. Gabriel
Introduzione 3 Viene definito algoritmo un insieme di istruzioni ordinato e finito di istruzioni elementari, chiare e non ambigue, per risolvere un problema. Il concetto di algoritmo è generale (non ci sono riferimenti al calcolatore). Di fatto, si hanno:
- 3 elementi o Un problema da risolvere o Una sequenza di istruzioni o Un esecutore di istruzioni
- 2 attori o Chi crea le istruzioni o Chi esegue le istruzioni La risoluzione di un problema spesso implica la risoluzione di una serie di sottoproblemi e spesso significa seguire le istruzioni in modo chiaro, preciso ed ordinato. Programmare significa scrivere algoritmi per il calcolatore Data la descrizione di un problema da risolvere
- Si descrive l’algoritmo di risoluzione
- Si traduce l’algoritmo nel linguaggio di programmazione
- più adatto oppure in quello imposto dal problema
- Si fornisce prova che il nostro programma fa quello richiesto
- Il tipo di prova dipende dal contesto
- unit tests
- prove di correttezza A parità di correttezza, come si può migliorare un programma?
- Efficienza (tempo e spazio), sia a livello algoritmico che implementativo a. Il secondo aspetto è preponderante nel corso, ma tratteremo entrambi b. Cenni di complessità
- Organizzazione: si codifica (per bene) una volta, poi si riusa il codice a. Si divide il problema in sottoproblemi e si cerca di riusare le soluzioni già implementate in passato
- Stile a. Il codice deve essere comprensibile ai tuoi colleghi e a te stesso tra 3 mesi. Notiamo in particolare le specifiche del Linguaggio Macchina.
Il linguaggio della macchina è un linguaggio di basso livello (il livello di dettaglio tende a far perdere la visione d’insieme), poiché dipendente dall’architettura.
- Se cambia architettura, dobbiamo aggiornare il programma.
- Difficile da ricordare (sequenze di 0 e 1) o Soluzione: Il linguaggio assembler è una versione mnemonica del linguaggio macchina, es. ADD invece della sequenza di bit dell’addizione o l’assemblatore sostituisce le istruzioni con i corrispondenti codici macchina Ecco quindi che si fornisce uno strumento che traduca i nostri programmi nel linguaggio della macchina ospite: il traduttore. Distinguiamo le due seguenti nozioni:
- Interprete : traduce una istruzione di alto livello e la esegue immediatamente o più lenta l’esecuzione del programma o necessita del traduttore per eseguire il programma o se si ha il traduttore ed il codice sorgente, può essere eseguito su ogni computer
- Compilatore : traduce tutte le istruzioni assieme che vengono poi eseguite tutte assieme direttamente in linguaggio macchina (C, C++) o più lenta l’esecuzione del programma o necessita del traduttore per eseguire il programma o se si ha il traduttore ed il codice sorgente, può essere eseguito su ogni computer Possono tuttavia esistere soluzioni intermedie: compilazione in bytecode ed interpretazione (Java). Nel corso viene usato un linguaggio semplice: il C. Esso è linguaggio compilato; insieme ristretto di comandi di base (ci si affida a librerie di funzioni), il compilatore è “facile” da scrivere, quindi portabilità. La fase di traduzione avviene graficamente come segue:
Come in matematica, le espressioni e le condizioni possono essere generalizzate utilizzando simboli (variabili) al posto di alcuni valori. Una variabile ha i seguenti attributi:
- il nome (che definiamo noi)
- l’area di memoria in cui è mantenuto il suo valore (non assegnata dall’utente)
- il tipo: le variabili vengono usate per rappresentare numeri interi, reali, caratteri (in C è definito dall’utente) o Il tipo è un modo coinciso per dire quanta memoria occupa (dipende dall’architettura della macchina), come leggere o scrivere la sequenza di bit e quali operazioni posso fare con quella variabile. Le variabili posseggono due vie di assegnamento (cioè, valore corrente che hanno, distinguendo così): Le variabili, per poter essere usate, vanno dichiarate. Un legame tra una variabile ed un suo attributo si dice statico se è stabilito prima dell’esecuzione e non può essere cambiato in seguito, dinamico altrimenti:
- il valore è un legame dinamico
- In C il tipo è un legame statico (questo implica che il compilatore può identificare i seguenti tipi di errore: int x; x = “Ciao Mondo!”; In C è possibile definire “variabili il cui valore è un legame statico”, quelle che comunemente chiamiamo costanti (es. pi greco)
- const int x = 3; // poiché non possiamo cambiare x, dobbiamo definirne il valore quando dichiariamo la variabile Le variabili possono avere qualsiasi nome; noi usiamo caratteri alfanumerici, ma il nome non deve iniziare con 0-9 e _ (underscore). Il C è case sensitive (cioè, distingue tra maiuscolo e minuscolo), ma evitiamo di avere variabili di nome VAR e var oppure variabili che assomigliano ad un comando o ad un elemento del linguaggio: IF, INT. I nomi dovrebbero indicare la loro funzione ed il loro contesto (evitando nomi troppo lunghi).
Scrivere il seguente codice C: _/*
- Trasformare il valore in gradi fahrenheit della variabile fahr (X) nel
- corrispondente valore celsius (Y) arrotondato all'intero inferiore e stampare
- "X gradi fahrenheit corrispondono a Y gradi celsius"
- Ad esempio se fahr=78 stampa
- 78 gradi fahrenheit corrispondono a 25 gradi celsius
- Si ricorda che C = (5/9)(F-32) */_ Si noti l’utilizzo della funzione floor , che fondamentalmente arrotonda all’interno più basso per difetto. La struttura è composta dalla funzione main (oppure, funzione principale) e uno scanf che acquisisce un numero (%d). In C, la funzione main è la funzione di partenza del programma, che viene eseguita al momento del lancio del programma. Il valore di ritorno di main indica lo stato di uscita del programma: un valore di 0 significa che il programma è terminato correttamente, mentre un valore non zero indica un errore o una condizione di uscita anomala. L'uso di return 0; alla fine del main è una convenzione standard che indica che il programma è terminato correttamente. Gli include , invece, descrivono le librerie incluse → stdio.h per l’input/output standard e math.h , libreria (collezione di variabili, funzioni, etc.) per le funzioni matematiche. Proseguiamo con la presenza di condizionali attraverso il meccanismo if-else (se-altrimenti ).
_* Calcolare la somma dei primi n numeri naturali e stamparla a video
- Ad es. se n=4 stampa
- 10 */_ Un modo efficiente per calcolare la somma dei primi n numeri naturali è utilizzare la formula matematica per calcolare la somma di una progressione aritmetica. La somma dei primi n numeri naturali è data da n * (n + 1) / 2. Ecco un esempio di codice che implementa questa formula: Questo codice legge n dall’input dell’utente e calcola la somma utilizzando la formula e la stampa a video. L'operazione di divisione intera (/) produce un risultato intero, anche se i due operandi sono numeri a virgola mobile. In questo caso, la divisione intera è sufficiente, poiché la somma dei primi n numeri naturali è sempre un numero intero.
Correttezza 10 Nella programmazione ci possono essere varie fonti di errore, dovute alla sintassi (come è scritto il codice, sbagliando a scrivere le parole chiave, istruzioni, etc.) oppure la semantica (significato delle parti di codice). Abbiamo vari strumenti utili:
- Debug: permette di trovare gli errori di implementazione
- Unit testing : può aiutare per individuare errori logici
- Correttezza: aiuta per dimostrare che il nostro programma fa sempre quello che vogliamo Definiamo quindi:
- Sintassi: insieme di regole che descrive come si possano costruire “frasi” (programmi) validi
- Errore di sintassi: il compilatore riesce a tradurre il nostro codice se e solo se rispetta la sintassi del linguaggio Se non ci riesce la compilazione fallisce e il file eseguibile non viene generato.
- Il compilatore prova a darci informazioni sull’errore (dove si è bloccato e che tipo di problema ha riscontrato)
- Di solito sono molto precise ed utili, ma a volte (per esempio in casi particolari quando ci si dimentica una parentesi, non indicano dove sia esattamente l’errore) In aggiunta il compilatore analizza il codice per trovare sequenze di istruzioni possibilmente problematiche anche se corrette sintatticamente, i cosiddetti warning.
- Es. una variabile utilizzata prima di essere assegnata o int x, y; y=x+2; // non si sa quanto valga x!
- Es. int x=3; if (x=2) {printf(“x=5”);} // un assegnamento ha valore di verità vero!
- Es. for(i=0; i<10; i=i+1); { printf("%d\n",i); } o Il blocco viene eseguito una volta stampando 10 Per visualizzare tutti i warning usare:
- gcc – Wall – o file_eseguibile file_sorgente.c Un programma può quindi avere errori semantici (ossia di logica: il programma non fa quello che si voleva) che si evidenziano a run-time (ossia quando si esegue il programma). Il problema può derivare:
- dalla non correttezza dell’algoritmo
- dall’errata implementazione dello stesso L’analisi di un programma può essere effettuata tramite
- l’aggiunta di istruzioni printf in opportuni punti del codice che danno informazioni sulle variabili
- l’uso di un debugger, ossia un programma che
- esegue il codice in modo controllato e permette di:
- eseguire le istruzioni fermandosi dopo ciascuna
- verificare e cambiare il contenuto delle variabili
- fermarsi automa6camente quando si verificano certe condizioni Come possiamo verificare che il nostro programma calcola ciò che ci aspettiamo?
- Unit tests: calcoliamo a mano una serie di input/output e poi verifichiamo che il calcolatore restituisca, per ogni input, lo stesso output. o Un test fallito dimostra che un programma non è corretto o Una serie di test passati non significa che il programma sia corretto per ogni input, ma soltanto per quelli testati o In certi casi potremmo dover fornire evidenza che, per un certo insieme di input, l’output sia corretto. In questo caso si procede quasi come se fosse una dimostrazione matematica.
Per rendere le funzioni più flessibili ed interessanti, si ha la possibilità di passare, all’interno delle parentesi tonde, dei parametri sui quali la funzione possa operare. Nella definizione della funzione, per ogni parametro bisogna indicare il tipo: Gli argomenti che la funzione riceve dal chiamante devono essere memorizzati in opportune variabili locali alla funzione stessa dette parametri. I parametri sono automaticamente inizializzati con i valori degli argomenti. Possiamo distinguere i due tipi di parametri:
- I parametri formali sono i parametri dichiarati nella dichiarazione di una funzione o di una procedura. Questi parametri rappresentano le variabili che verranno utilizzate all'interno della funzione per elaborare i dati.
- I parametri attuali sono i valori effettivamente passati alla funzione quando viene chiamata. Questi valori vengono assegnati ai parametri formali all'interno della funzione, che quindi può elaborare i dati. Ad esempio, consideriamo la seguente funzione: In questo caso, i parametri formali sono x e y, che rappresentano le variabili utilizzate all'interno della funzione per scambiare i valori. Se la funzione viene chiamata con i valori 1 e 2, i parametri attuali sono 1 e
- Questi valori verranno assegnati ai parametri formali x e y, e la funzione effettuerà lo scambio dei valori. Argomenti e parametri devono corrispondere in base alla posizione e al numero (almeno per le funzioni che definiremo noi). I nomi dei parametri sono indipendenti dai nomi delle variabili del Chiamante. Se la funzione non richiede parametri è preferibile indicare void tra le parentesi. In memoria i parametri sono del tutto distinti e indipendenti dagli argomenti, quindi cambiare il valore di un parametro non modifica l’argomento corrispondente.
In merito alla chiamata di funzione :
I numeri interi positivi sono rappresentati all’interno dell’elaboratore utilizzando un multiplo del byte (generalmente 4 o 8 byte). Se si vuole verificare la dimensione di un int, il comando sizeof(int) restituisce il numero di byte (celle di memoria) occupati da un int. Il file limits.h (#include ) riporta una serie di costanti numeriche, tra cui il massimo numero rappresentabile sul computer che si sta usando: INT_MAX = 2147483647. Per quanto riguarda i numeri reali , essi utilizzano la rappresentazione in virgola mobile.
In merito ai caratteri , Lo standard di codifica più diffuso è il codice ASCII, per American Standard Code for Information Interchange. Esso definisce una tabella di corrispondenza fra ciascun simbolo (carattere minuscolo, maiuscolo, cifre) e un codice a 7 bit (128 caratteri). Vi è poi UNICODE (UTF-8 e UTF-16): standard proposto a 8 e 16 bit (65.536 caratteri). Il tipo carattere viene definito con char e normalmente si assegna un carattere con degli apici.
- Esempio: char x=’c’; Gli operatori aritmetici ed i comandi sono definiti tra termini dello stesso tipo • Ma il C effettua automaticamente alcune conversioni tra tipi, le promozioni :
- il tipo con meno capacità espressiva viene promosso al tipo con maggiore capacità espressiva (ovvero presi due elementi nella tabella a fianco, la conversione avviene a quello più in alto
- quando si cambia il tipo di un’espressione (non di una variabile), viene fatta una copia del valore temporanea per effettuare il calcolo Si rischia di perdere informazione, ovvero il C non è type-safe. Array Un array è un gruppo di locazioni di memoria contigue che hanno tutte lo stesso tipo.
Puntatori 18 I puntatori sono uno strumento di basso livello: ci permettono di manipolare altre variabili (altre celle di memoria. Gli indirizzi di memoria (l-valori) sono interi positivi. Un puntatore è una variabile il cui r-valore è un l-valore.
- Es. int x = 3; //l-valore = 1021, r-valore = 3 la variabile con l-valore 1025 potrebbe essere un puntatore (vedi figura) Dichiarazione: tipo *nome;
- Es. int *ptr = NULL; // l-valore = 1026
- ptr è una variabile di tipo puntatore ad una variabile di tipo intero. Per ogni Tipo di variabile, esiste il corrispondente tipo “puntatore a Tipo”
- float *x; char *c; long *l, ...
- Esistono pure puntatori a funzioni
- &x (dove x è necessariamente una variabile) restituisce l’l-valore di x
- *p restituisce la variabile indicata dal r-valore del puntatore p
- & e * o hanno precedenza minore di () [], ma maggiore degli operatori aritmetici o sono associativi da destra a sinistra
- int *xptr = &x; // l-valore di xptr = 1025
- printf(“%p,%d,%p”, xptr, *xptr, &x); // stampa 1021, 3, 1021 Attenzione:
- Non usare una variabile non inizializzata (occorre controllare che il puntatore non sia nullo)
- Controllare che il tipo della variabile sia corretto per l’operazione dove si sta per usare.
- &* sono associativi da destra &p = &((p)).
- Memorizzare alla perfezione il significato degli operatori: per esempio * restituisce la variabile (un oggetto che ha un nome, tipo, l-valore, r-valore) non il suo r-valore.
- è usato con due significati diversi 1) nella dichiarazione e 2) nel corpo di una funzione. Se un puntatore è stato dichiarato ed inizializzato, può essere utilizzato in un’espressione. Si può definire un puntatore ad una variabile puntatore utilizzando più volte il simbolo * nella dichiarazione Distinguiamo i passaggi di parametri in due tipi: per valore e per riferimento.