diff --git a/21_compendio_C_Cpp.md b/21_compendio_C_Cpp.md new file mode 100644 index 0000000..2bd640d --- /dev/null +++ b/21_compendio_C_Cpp.md @@ -0,0 +1,81 @@ +# Compendio C/C++ + +Non è generalmente possibile utilizzare un linguaggio di programmazione "as is" + +Sono utili le librerie per evitare di reinventare la ruota e semplificare il processo di astrazione + +## Libreria standard +Sia in C che in Cpp è presente una *libreria standard* + +La stdlib del C è un sottoset della stdlib del Cpp + +Entrambe sono suddivise in sezioni che svolgono compiti diversi + +Esistono molte altre librerie, come le librerie `boost` del Cpp + +## Moduli +In C++ la libreria standard è suddivisa in moduli. + +In generale il nome di una libreria in Cpp si ottiene a partire dal nome della libreria C, rimuovendo il suffisso ".h" ed aggiungendo "c" come prima lettera + +## I/O formattato in C +All'interno della `stdio.h` sono presenti `printf(const char format[], ...)` e `scanf(const char format[], )` + +Queste due funzioni hanno un numero di parametri variabile (funzioni *variadiche*) + +Le funzioni variadiche si possono essere usate sia in C che in Cpp. Si utilizza un'estensione della sintassi vista fin'ora. Per recuperare gli args si utilizzano funzioni presenti nella libreria standard. + +### Confronto con cin e cout +Rispetto al C, la stampa e l'input formattato in C++ permette di non specificare il tipo di dato da stampare o prendere in input, perchè il compilatore deduce automaticamente qual'è il tipo di dato utilizzato (delle variabili di destinazione/origine) + +### Corruzione della memoria +=> molto frequente in C, in quanto se si sbaglia a passare l'indirizzo della variabile a scanf -> comporta la scrittura in una locazione non adeguatamente grande, oppure la scrittura sull'area di memoria di un puntatore + +### Performance +scanf, printf, << e >> hanno la stessa velocità di lettura/scrittura perchè gli operatori << e >> vengono sostituiti a tempo di compilazione + +## Direttiva #define +Negli ultimi standard del C è stato introdotto il qualificatore `cosnt`, prima si utilizzava la direttiva al precompilatore `#define` + +-> a tempo di compilazione viene sostituito simbolo per simbolo con il valore associato ad esso dal define + +## typedef +In C è necessario dichiarare un oggetto di tipo struct nel seguente modo: +``` +struct ; +``` + +Quindi per evitare di scrivere tutto per esteso si utilizza: +``` +typedef struct persona persona_t; +``` + +La stessa cosa vale per gli enum e per tipi di dato frequentemente utilizzati. +``` +typedef unsigned long int uint32; +``` + +typedef permette di migliorare la leggibilità. + +## Allocazione memoria dinamica +In C si usa `malloc()` (al posto di `new`) presente nella stdlib + +``` +* = (*) malloc(); +``` + +Differenze: +- `new` casta automaticamente l'indirizzo allocato al tipo di dato dichiarato +- `new` calcola la dimensione dell'area di memoria al posto di utilizzare sizeof e moltiplicare per la lunghezza dell'array + +E per liberare la memoria (al posto di `delete[]`): +``` +free(); +``` + +## Compilazione +``` +gcc program.c +``` + +L'estensione è differente, ma questo non ha importanza (il compilatore potrebbe arrabbiarsi) diff --git a/21_io_non_formattato.md b/21_io_non_formattato.md new file mode 100644 index 0000000..4c07af0 --- /dev/null +++ b/21_io_non_formattato.md @@ -0,0 +1,118 @@ +# Input Output non formattato +Fino ad ora abbiamo visto I/O formattato => gli operatori `<<` e `>>` stampano o prendono in input su uno stream sequenze di caratteri + +Esistono anche operazioni di i/o non formattate che operano su sequenze di byte -> trasferimento di byte da uno stream alla memoria o dalla memoria allo stream + +Ogni volta che faccio lettura o scrittura in modalità non formattata agisco in modo sequenziale sulla memoria + +Le funzioni di lettura e scrittura non formattate sono *funzioni membro* degli stream -> per invocarle si usa l'operatore punto + +## Buffer +Un buffer è un array di byte utilizzato per operazioni di I/O + +Il tipo char ha esattamente la dimensioni di un byte -> tipo di dato utilizzato per operazioni di I/O non formattato + +## Funzioni membro degli istream +- get() - ritorna il valore del prossimo byte nello stream di ingresso (intero). + ``` + int i; + while ( (i = cin.get()) != EOF ) { ... } + ``` +- get(char &c) - preleva un byte dallo stdin e lo assegna alla variabile (tipo carattere) passata in ingresso. Nel caso in cui venisse incontrato EOF `c` rimane invariata e il metodo riorna false. + ``` + char c; + while (cin.get(c)) { ... } + ``` +- get(char \*buf, int n, [char delimitatore]) - Legge una sequenza di byte dallo stdin e la copia in buf. Aggiunge automaticamente '\0' alla fine. La lettura va avanti finchè non si sono letti n byte OPPURE finchè non si incontra '\n' (delimitatore di default). L'istream viene messo in stato di errore se la riga è vuota oppure incontra il '\n' da solo (vedi dopo). +- peek() - ritorna il valore del prossimo byte presente su un istream senza consumare il carattere -> può essere utile per evitare lo stato di errore dell'istream + ``` + char *riga; + while (cin.peek() == '\n') ; // pulisco righe vuote + cin.get(riga, MAXLEN); + cin.get(); // butto via '\n' + ``` +- read(char \*buf, int n) - funzione di più basso livello rispetto a get(). Non aggiunge delimitatore e non va in stato di errore. Il delimitatore non esiste. read() cerca di leggere esattamente n bytes, e ritorna il valore effettivamente letto. + +// TODO: link file_conta_linee.cpp + +## Funzioni membro degli ostream +- put(char c) - equivalente della get +- write(char \*buf, int n) - equivalente di read() + +## File di testo e binari +I file binari memorizzano i dati "grezzi" provenienti dalla memoria -> sono una sequenza di byte (numeri) + +Un file è una sequenza di byte a cui è associato un nome + +## Puntatori ed array +Sappiamo che ` *` memorizza un puntatore. Si può passare un array al posto di ` *` il nome di un array, perchè esso corrisponde all'indirizzo del primo elemento. + +Il tipo char * memorizza indirizzi di qualunque tipo di variabile -> qualunque tipo di dato è convertibile in un array di caratteri + +``` +reinterpreter_cast(); // reinterpreta l'oggetto come se fosse un array di caratteri +``` + +Una sequenza di byte equivale ad un array di byte. Un array di byte è interpretabile come un array di caratteri. Quindi ogni tipo di dato può essere considerato un array di caratteri. + +Rimane da capire come determinare la dimensione di un oggetto: uso `sizeof()` +Oppure nel caso in cui sia un array: `sizeof()*` + +``` +int a[] = {1, 2, 3}; +of.write(reinterpreter_cast(a), sizeof(a[0])*3); +``` + +La dimensione finale del file sarà di esattamente 12 byte + +### Numeri reali e memoria +Memorizzando un numero reale con questo metodo posso mantenere tutti i byte salvati in memoria evitando troncamenti + +// TODO: link scrivi_leggi_array.cpp +// TODO: link studente_su_file.cpp + +## File di testo +Per leggere e scrivere file di testo si usano tipicamente le scritture formattate perchè ogni byte equivale ad un char + +## Efficienza +Le letture e scritture formattate possono essere effettuate mediante cicli for, perchè gli stream memorizzano un i dati in un buffer prima di eseguire la scrittura + +Le letture e scrutture non formattate sono inefficienti se effettuate mediante cicli, perchè non vene interposto un buffer tra la scruttura e la memoria. + +## Puntatori e alias +W: puntatori e alias ad una variabile sono estremamente diversi. Quando utilizzo un alias il valore e l'indirizzo di alias e variabile rimangono uguali. + +## Dereferenziazione +Per ottenere l'indirizzo di una variabilie che non sia un array si utilizza l'operatore `&`, che dereferenzia (prende l'indirizzo) della variabile alla quale è applicato + +### Sintassi +``` +& +``` + +### Semantica +Ritorna l'indirizzo dell'oggetto + +// TODO: vedi es per casa (x2) + +## Accesso sequenziale e casuale +Quando opero sui file posso accedere in modo casuale al file (non sequenziale). Questo non è possibile con gli stream. +Quando accedo in modo "randomico" ad un file posso decidere arbitrariamente dove leggere o scrivere. +Sugli stream è possibile solamente l'accesso sequenziale. + +Il byte a partire da quale avverrà la prossima operazione è dato da un contatore che parte da 0. Il suo valore può essere modificato con queste funzioni: + +``` +seekg(nuovo_valore); // sposto la testina per un file di input (g sta per get) +seekp(nuovo_valore); // sposto la testina per un file di output (p sta per put) +``` + +Le due funzioni possono essere invocate con due argomenti: +``` +seekg(offset, origine); +seekp(offset, origine); +``` + +Dove `origine` può essere: +- `ios::beg` (argomento default del secondo parametro) +- `ios::end` (parto dalla fine andando all'indietro) diff --git a/22_algoritmi_programmi.md b/22_algoritmi_programmi.md new file mode 100644 index 0000000..239662b --- /dev/null +++ b/22_algoritmi_programmi.md @@ -0,0 +1,38 @@ +# Algoritmi e programmi +formulazione di un problema -(metodo risolutivo "astratto")-> individuzione algo -(codifica in un linguaggio di programmazione)-> programma + +Un programma mandato in esecuzione diventa un processo, in grado di prendere in input dei dati e di fornire i risultati in uscita + +## Proprietà di un algoritmo +- eseguibile in un tempo finito +- non ambiguo +- + +## Costo computazionale +Il costo computazione di un algoritmo dipende dal numero di passi elementari che sono necessari per arrivare ad una soluzione + +Questo valore dipende da: +- dall'insieme di valori in ingresso +- dall'insieme delle azioni elementari che posso eseguire sui dati. Eg: in base a come è implementato ogni passo elementare a basso livello dipende il costo computazionale dell'istruzione ad alto livello + +Necessaria una notazine che renda il significato di costo computazione indipendente da questi due fattori. + +Eg: N passi per lavorare su N elementi (solitamente sempre presente perchè i dati li devo almeno prendere in input) +Eg: Due cicli innestati di N valori diventa N^2 -> non importa l'efficienza di ogni singolo passo, perchè comunque l'algoritmo con costo pari a N è sempre più ottimizzato + +Importa la *pendenza* del costo dell'algoritmo -> ovvero la derivata prima del costo di un algoritmo + +### Ordine di costo +Un algo ha un ordine O(N) se esiste una costante _c_ tale che il numero di operazioni la variare dell'input cresce al più _c\*N_ + +### Ordini di costo notevoli +- costo lineare O(N) +- costo quadratico O(N^2) eg: algo di insertion sort +- costo polinomiale O(N^i) +- costo esponenziale O(a^N) l'algo non può essere completato in un tempo ragionevole +- costo costante O(1) permette di eseguire le operazioni in un tempo che non dipende dalla dimensione dell'input (eg: hashmap) + +### Confronto costo algoritmi +Non sempre un algoritmi con ordine di costo maggiore è il migliore per risolvere un problema che coinvolge un piccolo set di dati (perchè la notazione O-grande mi indica il costo per N che tende all'infinito) + +Verifica sempre i "costi di basso livello" diff --git a/22_memoria_rda_indirizzamento.md b/22_memoria_rda_indirizzamento.md new file mode 100644 index 0000000..93becf6 --- /dev/null +++ b/22_memoria_rda_indirizzamento.md @@ -0,0 +1,70 @@ +# Classi di memorizzazione +- automatica +- statica +- dinamica + +## Oggetti statici +- oggetti globali +- oggetti dichiarati con l'attributo static + +Esistono per tutto il tempo di vita del programma + +Se non inizializzati hanno valore 0 + +## Oggetti dinamici +Hanno un valore casuale (bullshits) + +Il tempo di vita va dal momento dell'allocazione a quello della deallocazione dell'area di memoria + +## RDA +Record di attivazione: struttura dati che rappresenta l'ambiente della funzione e contiene: +- parametri formali inizializzati con i parametri attuali +- variabili e costanti locali +- indirizzo del record di attivazione del chiamante (da riattivare una volta finita l'esecuzione della funzione) +- indirizzo di ritorno (dal quale riprendere l'esecuzione dopo aver ritornato) + +## Base ed offset +L'indirizzo a cui inizia il record di attivazione di una funzione viene chiamato indirizzo *base* +Ogni oggetto è identificato da un *offset* calcolato a partire dall'indirizzo base + +Ogni oggetto locale è identificato da un indirizzo calcolato come base+offset + +## Prologo +=> codice aggiuntivo inserito dal compliatore per creare il RDA, inizializzare il base pointer ed eseguire la prima istruzione della funzione + +## Dimensione +=> dipende dal numero di variabili/costanti locali e dai parametri formali cha abbiamo dichiarato + +Gli RDA sono memorizzati nello _stack_ (pila) -> struttura dati LIFO + +L'RDA viene deallocato solo al termine dell'esecuzione + +Gli RDA sono innestati: vengono allocati secondo l'ordine delle chiamate e deallocati in ordine inverso + +Il C e C++ sono linguaggi imperativi e funzionali + +### Catena dinamica +=> rappresenta la storia di come sono state chiamate le funzioni una dopo l'altra + +## Valore di ritorno +È diponibile una sola area di memoria come valore di ritorno -> per questo ho un solo valore possibile, che può essere al massimo un puntatore + +### Epilogo +Pezzo di codice inserito dal compilatore che si occupa di distruggere e deallocare l'RDA di una funzione + +## Spazio di indirizzamento +Ad un processo, quando viene lanciato, viene associata un'area di memoria chiamata _spazio di indirizzamento_ (assegnata dal SO) così suddivisa: + +testo e istruzioni +costanti +oggetti statici inizializzati +oggetti statici non inizializzati +heap (memoria dinamica) +area libera (che può ridursi dall'alto o dal basso) +stack + +Eg: spectre e meltdown permettevano di leggere i valori rimasti sulla cache della CPU a causa di meccanismi della branch prediction mal gestiti + +Gli oggetti statici sono automaticamente inizializzati a zero + +Lo stack cresce verso l'alto, l'heap verso il basso. Quando lo spazio libero viene esaurito le due aree di memoria non sono ulteriormente incrementabili diff --git a/22_traduttori_e_compilatori.md b/22_traduttori_e_compilatori.md new file mode 100644 index 0000000..7510cac --- /dev/null +++ b/22_traduttori_e_compilatori.md @@ -0,0 +1,87 @@ +# Traduttori e compilatori +Un algoritmo è utile se esiste un _esecutore_ in grado di eseguirilo. + +Un esecutore viene chiamato _automa esecutore_ -> deve essere in grado di eseguire una codifica dell'algoritmo + +Gli automi venivano realizzati con congegni meccanici (Pascal - pascalina, Babbage) o implementati attraverso un modello matematico (modello funzionale - Hilbert, modello ... - Turing) + +## Macchina virtuale +=> automa esecutore in grado di eseguire un programma C/C++ (o un qualsiasi altro linguaggio di programmazione) senza che esista una macchina reale +=> gira sulla macchina reale (processore) -> linguaggio macchina: sequenza di 0 e 1 -> operazioni elementari + +## Linguaggio assembly +Linguaggio di basso livello in grado di essere tradotto 1:1 in linguaggio macchina + +Non esiste il concetto di memoria/variabili, ma sono utilizzabili solamente i registri (cella di memoria interna al processore) + +Le istruzioni sono parole mnemoniche che identificano una precisa sequenza di 0 e 1 intepretabile dalla CPU + +## Problema della portabilittà +Il linguaggio macchina è il linguaggio di un processore, strettamente dipendente dall'instruction set della CPU -> il codice assembly non è portabile + +Eg: Istruction Set x64 risolve parzialmente questo problema, perchè permette di utilizzare lo stesso codice per una famiglia di CPU +Ma se utilizzo una istruzione specifica di una CPU (oppure disponibile da una certa versione in poi) il programma non è più portabile + +## Scelta del linguaggio macchina +È una buona scelta codificare un algoritmo in linguaggio macchina? + - sì se devo utilizzare il programma su una sola architettura + - no se devo rendere il codice portabile + +## Livello di astrazione +C/C++ sono linguaggi di alto livello perchè si basano su un elaboratore ipotetico in grado di eseguire delle istruzioni che non appartengono ad un processore reale + +Un linguaggio di alto livello è più vicino al linguaggio naturale, quindi mette a disposizione istruzioni e strutture dati più vicine al dominio del problema + +L'esistenza di svariati linguaggi di programmazione + +## Processo di traduzione +I traduttori trasformano il linguaggio sorgente in linguaggio macchina + +2 tipi di traduttori: +- compilatori: genera un file eseguibile contenente tutto il programma. Non esegue il programma. Si dice che il compilatore *traduce* ogni istruzione. +- interpreti: esegue direttamente il codice sorgente senza generare un file eseguibile. Esegue una istruzione alla volta. Si dice che l'interprete *valuta* ogni istruzione. + +## Programmi intepretati e compilati +Tipicamente i programmi compilati girano più velocemente dei programmi interpretati + +I programma interpretati sono più portabili dei programmi scritti con un linguaggio compilato + +Questa differenza si sta appiattendo sempre di più + +## Programmi compilati +Gli header file spesso contengono solo delle dichiarazione + +Linking del codice macchina delle funzioni messe a disposizione dalla libreria già compilata. Vengono messi a disposizione gli headerfile contenenti solamente le dichiarazioni e i prototipi delle funzioni + +### Link delle librerie +A volte il complilatore non trova il percorso di una libreria: bisogna usare il flag `-lm` + +### Fasi compilazione +- preprocessing -> esecuzione delle direttive al precompilatore (eg: gli include copiano e incollano pari pari gli headerfile importati, i define vengono risolti sostituendo il valore associato al simbolo) +- traduzione -> genera un file oggetto (file contenente il linguaggio macchina) +- linking -> si unisce il file oggetto (tipicamente _.o_) con il programma appena compilato. Si ottiene un file eseguibile. + +## Fasi di sviluppo +1. progettazione +2. scrittura (codifica) +3. compilazione +4. esecuzione +5. collaudo (testing) +6. ricerca ed eliminazione degli errorri (debugging) + +### Abiente di programmazione +ambiente di programmazione o di sviluppo => Linux stesso è un ambiente di sviluppo. Elementi di un ambiente di sviluppo: +- editor +- traduttore +- linker +- interprete +- debugger + +### Tipi di ambienti di sviluppo +1. ambienti che mettono a disposizione singoli strumenti +2. i singoli strumenti di sviluppo sono raggruppati sotto un'unica interfaccia grafica (IDE - Integrated Development Environment -) + +### Confronto tra gli ambienti di sviluppo +Il vantaggio degli IDE è la semplicità e facilità d'uso + +Il vantaggio dei sistemi non IDE (tipo UNIX) è il poter distinguere le varie fasi dello sviluppo e l'indipendenza tra i vari strumenti diff --git a/esercitazioni/23_intro_alle_liste.md b/esercitazioni/23_intro_alle_liste.md new file mode 100644 index 0000000..4ec7108 --- /dev/null +++ b/esercitazioni/23_intro_alle_liste.md @@ -0,0 +1,57 @@ +# Introduzione alle liste +N.B.: questo capitolo non è presente all'esame + +Liste: struttura dati dinamica => cambiano dimensione a tempo di esecuzione del programma + +Le strutture dati dinamiche permettono di risolvere problemi efficacemente mediante alcuni algoritmi. + +Esempio: memorizzare una sequenza di valori la cui lunghezza non è conosciuta a priori. + +Possibile soluzione: utilizzare un array dinamico riallocato ogni volta che si renda necessario. Ogni riallocazione costa O(N), ma questa operazione non la faccio tutte le volte, quindi l'algoritmo ha _costo ammortizzato_ O(1) -> costo costante. Ma l'estrazione costa O(N) tutte le volte perchè devo ricompattare la sequenza di dati. + +Altra soluzione: allocare 1 elemento alla volta con l'operatore `new`. Memorizzo una variabile puntatore che punta alla prima locazione che contiene il primo elemento. Quando diventa necessario memorizzare il primo elemento alloco la memoria necessaria e memorizzo l'indirizzo nel puntatore. Al prossimo elemento avrò bisogno di un'area di memoria per l'elemento e un'altra variabile puntatore che punta all'elemento successivo. + +## Struttura dati +Per ogni elemento della lista memorizzo il suo valore e un puntatore all'elemento successivo + +``` +struct { + int v; // valore dell'elemento corrente + int *p; // puntatore al prossimo elemento +} +``` + +Per segnalare l'ultimo elemento si memorizza `NULL` (equivalente a 0) nella variabile puntatore dell'ultimo elemento. + +## Terminologia + +Abbiamo appena creato una *linked list* + +Diremo che ciascun elemento contiene un: + - campo informazione (oppure più informazioni) + - campo puntatore + +Il primo elemento della lista è chiamato *head* (testa). L'ultimo elemento è chiamato *tail* (coda). + +Esistono due tipi di liste: + - single linked list + - double linked list (ogni elemento contiene un riferimento all'elemento precedente e successivo) + +## Implementazioni +Esistono molte librerie che implementano le liste. + +Nella libreria std del Cpp è presente il tipo di dato `` + +## Confronto array-liste +Data una sequenza di N oggetti... + +...se uso un array occupo meno memoria (quasi la metà rispetto alla lista), il costo di inserimento è O(1) finchè non serve la riallocare la lista, altrimenti O(N). + +...se uso una lista occupo quasi il doppio della memoria, posso inserire e rimuovere elementi, sia in testa che in coda in O(1) + +## Fine corso +Consigli per il futuro: + - http://www.topcoder.com/ + - http://www.hackerrank.com/ + + diff --git a/esercitazioni/data.dat b/esercitazioni/data.dat new file mode 100644 index 0000000..ea06d71 Binary files /dev/null and b/esercitazioni/data.dat differ diff --git a/esercitazioni/data.txt b/esercitazioni/data.txt new file mode 100644 index 0000000..8fd9695 --- /dev/null +++ b/esercitazioni/data.txt @@ -0,0 +1,5 @@ +1 +0.2 +3.33333 +2.33333 +0 diff --git a/esercitazioni/file_conta_linee.cpp b/esercitazioni/file_conta_linee.cpp new file mode 100644 index 0000000..545f60f --- /dev/null +++ b/esercitazioni/file_conta_linee.cpp @@ -0,0 +1,36 @@ +#include +#include + +using namespace std; + +int main() { + const char FILENAME[] = "testo.txt"; + ofstream f(FILENAME); + if (!f) { + cerr << "Errore nell'apertura del file in scrittura" << endl; + return 1; + } + + cout << "Inserisci il contenuto del file un carattere alla volta e termina con EOF :" << endl; + char c; + while (cin.get(c)) { + f << c; + } + f.close(); + + ifstream f2(FILENAME); + if (!f2) { + cerr << "Errore nell'apertura del file in lettura" << endl; + } + + cout << "Il file contiene: " << endl; + int rows = 0; + while (f2.get(c)) { + cout << c; + if (c == '\n') + rows++; + } + cout << endl << "Numero di righe: " << rows << endl; + + return 0; +} diff --git a/esercitazioni/scrivi_leggi_array.cpp b/esercitazioni/scrivi_leggi_array.cpp new file mode 100644 index 0000000..4389c40 --- /dev/null +++ b/esercitazioni/scrivi_leggi_array.cpp @@ -0,0 +1,50 @@ +#include +#include +#include + +using namespace std; + +int main() { + const char *BIN_FILENAME = "data.dat"; + const char *TXT_FILENAME = "data.txt"; + const int LEN = 5; + const double a[LEN] = {1., .2, 3.33333333, 7.0/3.0, 0.0}; + + // Scrittura + ofstream f_bin(BIN_FILENAME), f_txt(TXT_FILENAME); + if (!f_bin or !f_txt) { + cerr << "Errore apertura file in scrittura" << endl; + return 1; + } + + for (int i=0; i(a), sizeof(a[0])*LEN); + + f_bin.close(); + f_txt.close(); + + // Lettura + ifstream if_bin(BIN_FILENAME), if_txt(TXT_FILENAME); + double d; + cout << "File testuale" << endl; + for (int i=LEN; i --> 0;) { + if_txt >> d; + cout << d << endl; + } + + double a2[LEN]; + if_bin.read(reinterpret_cast(a2), sizeof(a2)); + cout << endl << "File binario" << endl; + cout << setprecision(9); + for (int i=0; i