Laboratorio di trattamento numerico dei dati sperimentali Maurizio Tomasi turno A2 Giovedì 16 Novembre 2017
Avviso Il prof. Carminati vorrebbe comunque fare lezione nella giornata di Venerdì 17 Novembre domani, visto che non sono previsti compitini per gli studenti del II anno. Al momento l'aula prenotata è l'aula A. Consultate il sito questa sera, perché potrebbero esserci cambiamenti dell'ultimo minuto.
Esercizio 6.0 Il punto 4 dell'esercizio «questi metodi devono controllare che l'indice delle componenti richieste sia compatibile con la lunghezza del vettore» richiede di gestire una condizione d'errore particolare, ma non dice in che modo gestirla. Un errore del genere è solitamente imputabile a un bug nel codice, non ad un uso scorretto del programma da parte dell'utente. Non è quindi detto che abbia senso scrivere un messaggio d'errore: questi messaggi dovrebbero aiutare l'utente a capire come usare il programma, ma se c'è un bug, il povero utente può fare ben poco!
Esercizio 6.0 In questi casi la soluzione migliore è quella di far fallire il programma nel modo più clamoroso e catastrofico possibile: in questo modo l'utente capirà che deve contattare lo sviluppatore. Ci sono una serie di possibilità: 1. Usare la funzione abort definita in stdlib.h ; 2. Sollevare un'eccezione. La soluzione migliore sarebbe la 2, ma le eccezioni sono un argomento complesso e non sono trattate in questo corso, quindi il consiglio è di usare abort. Da linea di comando scrivete man abort per avere maggiori dettagli.
Esercizio 6.0 L'esercizio richiede di costruire una classe Vettore, che funziona in modo molto simile alla classe std::vector usabile con #include<vector>. Nei programmi della vita reale, si usa sempre std::vector : lo scopo dell'esercizio è quello di farvi capire quali sono i problemi da affrontare nella costruzione di una classe come questa.
La classe std::vector Il linguaggio C++11 ha introdotto alcune importanti novità che funzionano con la classe std::vector ma non con la classe Vettore che scriverete oggi. Elenchiamo qui alcune delle caratteristiche di std::vector accessibili dal C++11.
Inizializzazione La classe std::vector può essere inizializzata con una sola riga: std::vector<double> values { 1.0, 2.0, 3.0, 4.0 };
Loop sugli elementi Il C++11 introduce una sintassi molto comoda per iterare sugli elementi di un vettore: std::vector<double> values { 1.0, 2.0, 3.0, 4.0 }; for (double x : values) { std::cout << x << "\n"; }
Ciclo di vita delle variabili Quando il ciclo di vita di una variabile finisce in gergo, termina la visibilità della variabile, il C++ si preoccupa di liberare la memoria associata. Di norma non è necessario preoccuparsi dei dettagli, perché quello che fa il C++ è sufficiente. if (verbose) { double ver = get_version_number(); std::cout << "version " << ver << "\n"; } // at this point, "ver" does no longer exist Terminato il codice nell' if, la variabile ver non è più usata e la memoria allocata per essa può essere liberata. Il C++ fa questo in modo automatico.
Distruttori Nel caso in cui si crei una variabile di un tipo complesso es., una classe, può essere però che il C++ non sappia bene quali risorse liberare. Consideriamo le seguenti classi: class PointerTo { int * m_a; // this takes 8 bytes public: PointerTo(int * ptr_a) : m_a(ptr_a) {} }; class NewVar { int * m_a; // this takes 8 bytes public: // but here we are allocating 8 more bytes NewVar(int a) : m_a(new int) { *m_a = a; } };
Distruttori Consideriamo adesso questa funzione: void fn(void) { int a = 123; PointerTo p(&a); NewVar n(a); } /* What should happen here to the data pointed by p.m_a and n.m_a? */ Cosa farà il C++ quando la funzione termina? 1. Dovrà liberare la memoria allocata per la variabile p, incluso quindi il puntatore m_a ma non la variabile puntata da m_a ; 2. Dovrà liberare la memoria allocata per la variabile a, incluso il puntatore m_a : ma in questo caso dovrebbe anche chiamare delete m_a, però non lo fa!
Distruttori Il C++ non sa però che il puntatore PointerTo::m_a punta a una zona di memoria che verrà già liberata è la variabile a = 123, mentre NewVar::m_a è un'area di memoria che deve essere liberata. Di default, il C++ libera m_a in entrambi i casi, ma si disinteressa della variabile a cui punta m_a : per PointerTo non ci sono problemi, ma per NewVar sì! In questi casi è utile definire un distruttore, che liberi esplicitamente la memoria che il C++ non reclamerebbe autonomamente: NewVar::~NewVar() { delete m_a; }
Debug del codice Il codice che usa puntatori ed allocazioni di memoria è molto fragile e facilmente soggetto a crash e malfunzionamenti. Un utile strumento per capire cosa abbia scatenato un crash è lo Gnu Debugger GDB. Per usarlo, modificate il vostro Makefile in modo che abbia la riga CXXFLAGS = g oppure aggiungete il flag g quando eseguite g++ : g++ g c o main.o main.cpp
Debug del codice Una volta compilato il programma con g, invece che eseguirlo direttamente, avviate gdb. Se il vostro programma si chiama esercizio6.0, avviate gdb così: gdb esercizio6.0 A questo punto comparirà il prompt di GDB: (gdb) dal quale si possono digitare comandi. Se il vostro programma richiede argomenti da linea di comando, usate set args : set args argomento1 30 25.0
Debug del codice A questo punto potete eseguire il codice col comando run. Se il codice dovesse andare in crash, comparirà nuovamente il prompt (gdb). Nel caso in cui dovesse accadere ciò, avete a disposizione un'ampia gamma di comandi con cui ispezionare lo stato del codice: 1. backtrace stampa a video un'istantanea delle funzioni in esecuzione al momento del crash; 2. print VAR stampa a video il valore della variabile VAR ; 3. frame NN si sposta alla funzione numero NN, dove il numero è quello mostrato da backtrace. GDB può essere istruito anche perché si fermi ogni volta che si raggiunge una determinata funzione, senza necessariamente un crash. Consultate la documentazione info gdb per dettagli.
Esercizio 6.1 Il consiglio è di fare in modo che il programma salvi autonomamente un file di testo che contenga due colonne: la prima col numero di elementi considerati, la seconda con il tempo impiegato. Per cronometrare il tempo di esecuzione, usate la funzione clock, definita in time.h da terminale digitate info clock per sapere come usarla, attenti all'unità di misura del numero restituito!