Archivio per la categoria c++

Gestione degli errori

La gestione degli errori è un elemento molto importante per la buona riuscita di un software di qualità. A nessuno piace ricevere messaggi di errore incomprensibili, con conseguenti comportamenti inattesi e magari anche la possibilità di perdere del lavoro già fatto.

La presenza di bug logici, che non vengono segnalati in fase di compilazione, è quasi inevitabile, o comunque molto difficile da evitare, ed è quindi molto importante prepararsi a gestire eventuali errori logici.

Quando il nostro programma esegue delle istruzioni che generano dei problemi, o non sono consentite o supportate, o comunque vanno in errore, viene generata una “eccezione”. Una eccezione è appunto un “evento” che notifica un problema accaduto in una qualsiasi funzione, il quale viene propagato a ritroso alla funzione che ha chiamato quella funzione, e cosi via fino alla funzione in testa a tutte (ad esempio il main).

In c++, come in molti altri linguaggi, esiste uno strumento per intercettare queste eccezioni, che è il costrutto try-catch.

Se abbiamo un blocco di codice di cui vogliamo intercettare eventuali eccezioni, possiamo quindi inserirlo in un try-catch in questo modo:

try

{

// istruzioni

}

catch(exception ex)

{

//codice in risposta dell’eccezione

}

In questo modo, se nel blocco di codice racchiuso nel try viene generata un’eccezione, l’esecuzione salta direttamente al blocco di codice contenuto nel “catch”, mettendo a disposizione un oggetto che contiene informazioni riguardo all’errore, come ad esempio il messaggio, un eventuale id, informazioni sullo stato ecc.

Potremmo ad esempio inserire del codice che mostri un messaggio user friendly tipo ” si è verificato un errore, contattare l’assistenza per ottenere un supporto”, piuttosto che un errore incomprensibile per l’utente tipo “Fatal error 0x03432 in system32.dll”

I tipi di eccezione sono tanti, tutti derivati dalla classe “exception”, ed ognuno è specifico al tipo di errore verificatosi. Esistono ad esempio eccezioni per deferenziazioni a puntatori nulli (il famoso null reference pointer”), o argomenti nulli passati a una funzione, o a errori di accesso in lettura o scrittura agli stream, e cosi via.

Può verificarsi anche il caso in cui uno stesso blocco di codice possa generare più tipi di eccezioni, e che noi vogliamo definire dei comportamenti diversi a seconda dell’eccezione generata.  Per farlo possiamo inserire più blocchi catch, specificando per ciascuno il tipo di eccezione che vogliamo intercettare.

E’ importante fare attenzione all’ordine in cui si inseriscono i blocchi catch, perchè essendo l’esecuzione del codice sequenziale, alcuni blocchi catch potrebbero essere esclusi. Vediamo un esempio:

try

{
//istruzioni
}

catch(exception ex)

{ // istruzioni di gestione dell’errore generico }

catch(overflow_error ex)

{//codice eseguito in seguito ad errore di tipo overflow_error }

 

In questo blocco try-catch abbiamo due blocchi catch, uno per gestire un’eccezione generica (exception) e uno per gestire un’eccezione di tipo overflow_error.

Se viene generata un’eccezione generica, viene eseguito il codice corrispondente al blocco catch(exception ex), e tutto va come previsto. Se invece si solleva un’eccezione di tipo overflow_error, siccome il blocco catch che intercetta le eccezioni generiche si trova più in alto, e siccome overflow_error è in effetti una classe ereditata da exception, ed è quindi anch’essa assimilabile al tipo exception, il codice eseguito è di nuovo quello del primo catch.

Invertendo l’ordine dei catch, ovvero mettendo l’eccezione più specifica in alto e quella più generica in basso, ciascun catch intercetterà soltanto le eccezioni del tipo specifico indicato, o di tipi ereditati da esso.

try

{
//istruzioni
}

catch(overflow_error ex)

{//codice eseguito in seguito ad errore di tipo overflow_error }

catch(exception ex)

{ // istruzioni di gestione dell’errore generico }

 

Possiamo sollevare noi stessi eccezioni in particolari situazioni, in seguito a dei controlli tali per cui riteniamo che l’esecuzione non possa continuare. Se ad esempio stiamo scrivendo una funzione che divide due numeri, dovremmo controllare che il divisore non sia zero. In tal caso, possiamo sollevare un’eccezione, del tipo che riteniamo più opportuno:

float Dividi(float a, float b)

{

if(b==0)

{ throw exception(“divisione per zero non consentita”); }

return a/b;

}

con throw tipoeccezione(parametri) generiamo un’eccezione del tipo specificato, usando uno dei costruttori messi a disposizione dal tipo specifico di eccezione. Essa verrà propagata a ritroso nella pila di chiamate a funzione fino alla prima, a meno che in una delle funzioni chiamate a cascata ci sia un blocco try-catch del tipo uguale all’eccezione da noi sollevata, o di un tipo da cui essa eredita.

Se invece ci accorgessimo che non esiste un tipo di eccezione che coincide esattamente con le nostre esigenze, possiamo crearne una, ereditando dal tipo più generico exception o da un’eccezione simile a quella che ci serve, estendendola a nostra discrezione.

E’ nostra cura specificare i tipi di eccezione sollevati dai metodi che scriviamo. Per il caso precedente, nell’header della nostra classe scriveremo ad esempio:

float Dividi(float a, float b) throw(exception, miaccezione)

in questo caso chi utilizzerà la nostra funzione, saprà che il metodo può generare due tipi di eccezioni, ovvero excepion e miaeccezione, e adeguerà il suo codice con eventuali blocchi try-catch, se lo riterrà necessario.

 

Lascia un commento

Overloading di operatori

L’overloading di un operatore consiste nella ridefinizione del funzionamento di un operatore esistente. L’overload di un operatore è possibile a patto che i parametri dell’operatore siano diversi dalla definizione standard.

Non si possono ridefinire operatori di tipi primitivi, come gli int, char, float, ecc.

Questa caratteristica ci consente di definire degli operatori per le classi scritte da noi nel nostro progetto, in modo da scrivere codice compatto e pulito.

Se ad esempio abbiamo una classe Warrior, che rappresenta un guerriero di un gioco, il quale ha un membro di tipo int che contiene un ID univoco, potremmo voler avere un operatore che ci consenta di sapere se due variabili di tipo Warrior siano effettivamente lo stesso warrior, ovvero entrambe abbiano lo stesso ID.

Se facessimo qualcosa di questo tipo :

Warrior w1;

w1.ID = 10;

Warrior w2;

w2.ID = 10;

return (w1 == w2) ;

otterremmo un false come ritorno, perchè l’implementazione di default dell’operatore == non prevede il confronto che ci aspettiamo e che ci serve. Chiaramente potremmo fare direttamente:

return(w1.ID == w2.ID)

ma ci sono decine di motivazioni per spiegare i vantaggi di scrivere questo controllo in un posto solo, tra cui i vantaggi dell’incapsulamento, della riusabilità, della manutenibilità, il debugging, e tanti altri.

Detto questo, vediamo come ridefinire l’operatore ==:

bool Warrior::operator==(const Warrior &other) const {

… return this->ID == other->ID;

}

L’operatore di uguaglianza da come output un valore booleano vero o falso, per cui come valore di ritorno della funzione di overload useremo un bool. Warrior:: sta ad indicare che l’operatore è sovraccaricato nella classe che ci interessa (e lo scriveremo quindi nel file .cpp della nostra classe Warrior).  La keywork “operator” indica che stiamo appunto effettuando l’overload di un operatore, e il simbolo == indica quale operatore stiamo ridefinendo.

Come parametro di ingresso, l’operatore == vuole un riferimento costante a un tipo, e visto che dobbiamo comparare due Warrior, indicheremo come parametro un “const Warrior &other”, ovvero un riferimento a un oggetto di tipo Warrior, ma costante. Viene passato un riferimento costante per due motivi: 1) il riferimento implicitamente si traduce in un passaggio di un puntatore, che è molto più efficiente rispetto a un passaggio per valore 2) il fatto che sia costante è garanzia del fatto che all’interno dell’operatore l’oggetto da comparare non venga modificato.

A ulteriore indicazione che il metodo non modificherà nessun dato, è aggiunta la keyword const anche al corpo del metodo, che impedisce appunto la modifica di dati al suo interno.

La logica dell’operatore è banale, basta ritornare infatti un semplice confronto tra i membri ID dei nostri Warrior, dove this è un puntatore all’oggetto su cui è chiamato l’operatore, mentre other è l’oggetto di confronto. In pratica, se facciamo

a==b

stiamo chiamando l ‘operatore == sull’oggetto a, il che significa che sarà chiamata l’implementazione dell’operatore == definita per quel tipo, dove this è a, e il parametro passato alla funzione è il riferimento a b

quindi, avendo ridefinito l’operatore == per il nostro Warrior, rieseguendo il codice:

return w1 == w2; 

dove w1 e w2 sono due oggetti di tipo Warrior, verrà richiamata la funzione definita da noi nella nostra classe Warrior, che eseguirà il confronto tra gli ID.

Allo stesso modo possiamo effettuare l’overload di un operatore a livello globale, ovvero non dentro una classe ma in una funzione globale, in questo modo:

si può ridefinire un operatore a livello globale:
bool operator ==(const Warrior& w1, const Warrior& w2)
{
return w1->ID == w2->ID;
}

la differenza rispetto all’overload nella nostra classe è che l’operatore, essendo globale, non ha nessun puntatore implicito a se stesso, ovvero nessun “this”. Ha bisogno per cui che gli passiamo sia il primo che il secondo oggetto da confrontare.

Lascia un commento

STL e iteratori

Gli iteratori sono degli oggetti che ci permettono di accedere agli elementi di un container di prima classe (quindi non quelli che al loro interno contengono a loro volta un container, come lo stack o la coda), in modo causale (tramite indice) o in avanti o indietro. La caratteristica del container è che le modalità con cui li utilizziamo su container diversi sono sempre le stesse, anche se poi per ciascun container esiste un iterator specifico i cui metodi implementano logiche specifiche e ottimizzate per ciascun container.

Un iterator in sostanza si comporta come un puntatore, mantenendo un riferimento a un certo elemento del container, e possiamo quindi accedere al valore a cui fa riferimento con l’operatore *, o avanzare all’elemento successivo con l’operatore ++, o a quello precedente con l’operatore –, e cosi via.

le diverse implementazioni degli iteratori sono in realtà delle classi innestate nelle classi del rispettivo contenitore; questo significa che nel container “vector” c’è una classe vector::iterator che definisce metodi e membri del relativo iterator e il comportamento che deve avere.

I container hanno due metodi fondamentali, begin() e end(), che restituiscono rispettivamente un iteratore che punta al primo elemento del container, e un iteratore che punta all’indirizzo di memoria successivo all’ultimo elemento, quindi a una POSIZIONE VUOTA.

Vediamo come utilizzare un iterator:

vector v; // dichiaro un container di tipo vector di int
v.push_back(1);// inserisco valori
v.push_back(2);// inserisco valori
v.push_back(3);// inserisco valori
v.push_back(4);// inserisco valori

vector::iterator it = v.begin(); // dichiaro una variabile di tipo iteratore di vector, e la inizializzo chiamando il metodo begin() del vettore, che restituisce un iteratore alla prima posizione

count << *it; // stampo il valore referenziato dall’iteratore, che è al primo posto del container

it++; // incremento l’iteratore di una posizione, a prescindere da come è fatto il container. Al suo interno l’iteratore ha un suo overload dell’operatore ++ ottimizzato per la struttura dati del vector. Se fosse stata una list, avrei sempre utilizzato it++, ma al suo interno l’iteratore avrebbe avuto un overload diverso dell’operatore ++, basato sulla struttura dati della lista.

Oppure possiamo fare un ciclo dal primo all’ultimo elemento in questo modo:

for(vector::iterator it = v.begin(); it < v.end(); ++it)
{
count << *it;
}

la prima parte del for dichiara una variabile di tipo iteratore di vector e la inizializza con l’iteratore alla prima posizione del mio vector.
La seconda parte controlla che l’iteratore sia sempre ad una posizione precedente a quella restituita da end() (ovvero la posizione dopo l’ultimo elemento).
La terza parte incrementa l’iteratore per spostarlo alla posizione successiva.

Esistono inoltre diversi tipi di iteratori, come ad esempio il reverse_iterator, che ridefiniscono l’effetto di alcuni operatori per consentire di utilizzare la stessa logica avendo comportamenti diversi.
Ad esempio il reverse_iterator si comporta in modo opposto all’iterator, infatti usando l’operatore ++, otteniamo uno spostamento indietro dell’operatore, mentre usando il — otteniamo uno spostamento in avanti.
La sintassi del ciclo cambierà solo per quanto riguarda il tipo di iteratore istanziato, mentre il resto rimane invariato.
Per ciclare un vector dalla fine all’inizio, possiamo ad esempio fare cosi:

vector::reverse_iterator revIt = v.end();
while (revIt != v.begin()) {
std::cout << ‘ ‘ << *revIt;
++revIt;
}

in questo caso l’istruzione ++revIt decrementa l’iteratore, invece di incrementarlo, proprio perchè il reverse_iterator ha un’implementazione diversa dall’operatore ++ rispetto all’iterator normale.

Come visto per i costruttori dei container, si può istanziare un container specificando due iteratori come indirizzi di inizio e fine memoria da cui copiare, e lo si può fare passando degli iteratori, in questo modo:

vector<int> v2(v.begin(), v.end());

in questo modo indico a v2 di copiarsi e aggiungere al suo interno degli elementi presi dall’iteratore restituito da v.begin(), e iterato fino a v.end(). Ovviamente v.end() non contiene elementi, per cui è escluso dai limiti della copia.

 

Lascia un commento

STL e Container

STL sta per Standard Template Library, ovvero una libreria di classi basate su template. Particolarmente utili sono i containers che STL mette a disposizione, ovvero dei contenitori di oggetti, ognuno con caratteristiche diverse, dei pro e dei contro.

E’ molto importante sapere quale container utilizzare in ogni situazione, perchè sbagliando il container ne potremmo perdere molto in prestazioni, magari vanificando gli sforzi fatti per ottenere perfomance in altri punti del nostro programma.

I container non sono altro che contenitori dinamici di oggetti, ovvero si ridimensionano a nostro piacimento quando aggiungiamo o togliamo elementi, togliendoci l’onere di gestione che avevamo con gli array statici, a cui dobbiamo dare una dimensione fissa al momento della dichiarazione. la parola “template” indica che quando si crea un container bisogna specificare il tipo di elementi che dovrà contenere, in modo che il container sappia come trattarli. Questo li rende molto flessibili perchè con un ristretto numero di tipi di container abbiamo un numero grandissimo di possibilità.

I container sono di due tipi: associativi o sequenziali. Quelli sequenziali si basano sul concetto che ciascun elemento abbia una posizione specifica rispetto agli altri (è il primo, è l’ultimo, è il quinto), mentre quelli associativi si preoccupano di associare un elemento a una chiave, prescindendo dall’ordine effettivo in cui gli elementi si trovino nel container.

Queste due particolarità sono già il primo criterio di scelta di un container: sceglieremo un container sequenziale quando ad esempio cicleremo molto spesso il container (esempio: ogni tot secondi devo rigenerare dei punti vita a un’armata di soldati), useremo un container associativo quando dobbiamo accedere frequentemente a degli elementi specifici del container, in quanto i container associativi sono organizzati secondo un albero binario, per cui la ricerca di un elemento al loro interno ha una complessità O(log n) che è minore di una ricerca sequenzale, che ha complessità O(n). Per approfondire gli alberi binari, Albero binario da Wikipedia

Altri aspetti importanti da tenere presente:

1. aggiungendo un elemento ad un container, viene fatta una copia dell’oggetto e salvata nel container.

2. il container non si occupa di eliminare gli oggetti, per cui prima di eliminare il container, vanno distrutti a mano gli oggetti al suo interno.

Vediamo ora i container più comuni:

1. Vector

Il vector è un container sequenziale, ed ha lo stesso comportamento di un normale array, salvo che non dobbiamo decidere a priori la sua lunghezza. Sarà infatti il container ad auto adattarsi in base al numero di elementi che inseriamo al suo interno.

Dietro le quinte, il vector utilizza in effetti un array statico. Quando inseriamo un elemento in più di quelli che l’array può contenere, il vector si occupa di creare un array più grande, di una dimensione proporzionale al numero di elementi contenuti, e copiare tutti gli elementi dal vecchio al nuovo array.

Basandosi su un singolo array, gli elementi allocati in memoria sono contigui, e questo rende molto veloce l’iterazione degli elementi e l’accesso tramite indice agli elementi (accesso casuale), e consente inoltre di utilizzare l’aritmetica dei puntatori (dato un puntatore p, posso fare p++, p–, ecc)

Quando rimuoviamo elementi, la memoria già allocata rimane tale, come vale per un normale array, per cui in termini di risorse utilizzate, più aggiungiamo elementi e più la memoria occupata aumenta, senza mai diminuire fino alla distruzione del vector stesso.

Quando inseriamo un elemento in testa o in mezzo a due altri elementi, il vector è meno performante di altri container perchè gli elementi successivi a quello inserito devono essere tutti spostati di un posto verso la coda, e questo richiede tempo. La stessa cosa vale per la cancellazione di elementi in testa o in mezzo, che comporta lo spostamento di un posto verso la testa di ciascun elemento a partire da quello eliminato.

vediamo i costruttori e i metodi del vector, ma più in generale della maggior parte dei container, a parte casi specifici di alcuni container.

il vector ha una size , ovvero il numero di elementi realmente contenuti al suo interno, e una capacity, ovvero il numero massimo di elementi che si possono inserire prima che il vector si ridimensioni  (di norma del doppio dell’attuale capacità).

costruttori di un vector:

vector<int> v; // costruttore senza parametri, istanzia un vector di interi (<int>) di size = 0 e capacity = 0

vector<int> v[5];  // istanzia un vector in cui vengono inseriti 5 elementi inizializzati chiamando il loro costruttore di default  in base al tipo specificato

vector<int> v[5,2]; // istanzia un vettore di 5 elementi interi di valore 2

vector<int> v[begin,end]; // istanzia un vettore copiando elementi compresi tra due puntatori o due posizioni di iteratore (equivalenti di fatto a puntatori) che fanno da indirizzi di inizio e fine da cui copiare gli elementi. Da notare che l’indice indicato come inizio è compreso nella copia degli elementi, mentre l’indice di fine è escluso

metodi di un vector:

.size() // numero di elementi contenuti

.capacity // numero di elementi che può contenere senza doversi ridimensionare

.push_back() // aggiunge un elemento in coda

.back() // restituisce l’ultimo elemento

.front() //restituisce il primo elemento

.reserve(int) // imposta la capacity, utile per controllare manualmente la memoria allocata dal vector

.pop_back() // toglie un elemento in coda

.remove() // rimuove un elemento, ma la memoria rimane allocata. se l’elemento è in mezzo, vengono spostati gli elementi dalla coda alla testa per riempire i vuoti, e gli slot in coda rimangono allocati

.erase(index) // cancella effettivamente l’elemento, liberando anche la memoria.

.clear() elimina tutti gli elementi del vettore, la capacity rimane ovviamente uguale

.at[index] restituisce l’elemento presente alla posizione indicata con index, lancia un’eccezione se l’indice non è compreso tra i limiti del vector, ovvero tra 0 e .size()-1

2.Deque

Deque sta per double-ended queue, ovvero coda a due estremi. Anch’esso sequenziale, è ottimizzato per inserire e rimuovere elementi sia in testa che in coda. Quando un deque è pieno, viene allocata ulteriore memoria sia in testa che in coda, di cui viene tenuta traccia con un array di puntatori, per passare da un blocco all’altro di memoria nelle operazioni di lettura e scrittura.
Un vantaggio del deque rispetto al vector è negli inserimenti in testa, perchè il deque non deve spostare tutti gli elementi verso la coda perchè li salva in un nuovo blocco di memoria, salvandosi anche il percorso che deve fare per saltare da un blocco all’altro in modo ordinato.
Un altro vantaggio rispetto al vector è che rimuovendo elementi dal deque, quando un intero blocco di memoria si libera, viene deallocato e quindi rilasciato dalla memoria, a differenza del vector che non si restringe mai una volta espanso.

Il deque aggiunge principalmente due metodi a quelli già visti nel vector:

.push_front() // inserisce un elemento in testa

.pop_front() // rimuove un elemento in testa

3.List

La list è un insieme sequenziale di elementi, ciascuno dei quali ha un puntatore al precedente e al successivo. Questo comporta la possibilità di poter salvare gli elementi non necessariamente in posizioni di memoria contigue, perchè in qualsiasi posto si trovino, ogni elemento sa dove si trova il suo prev e il suo next.
Il vantaggio di questa gestione della memoria è che l’inserimento in mezzo a due elementi è il più efficiente tra i container sequenziali, in quanto non serve nessuno spostamento degli elementi successivi, né in inserimento né in cancellazione, ma si vanno a modificare solo i puntatori a prev e next degli elementi coinvolti.
Lo svantaggio è che ogni elemento occupa, a parte la memoria necessaria per se stesso, anche lo spazio per i puntatori al next e al prev.
Non è inoltre possibile accedere tramite indice a un elemento.

Per quanto riguarda i metodi, i fondamentali della list sono quelli già visti per il vector e il deque.

4.Map

La map è un container associativo, ed è paragonabile ad un dizionario di coppie chiave/valore. In pratica ogni elemento è formato da un oggetto pair, che ha come membri “first” che contiene la chiave di ricerca e “second” che contiene l’elemento vero e proprio da conservare. Lo scopo di una map è quello di rendere molto efficiente l’accesso a un elemento a partire dalla sua chiave. Gli elementi di una map vengono ordinati in base ad un albero binario, che ad ogni inserimento o rimozione viene bilanciato per minimizzarne la complessità durante le ricerche. In una map non è possibile aggiungere elementi con la stessa chiave. E’ possibile invece farlo con la multimap, che è identica alla map ma accetta chiavi duplicate.

Questo vuol dire che mentre una map è molto efficiente per accedere frequentemente ad un elemento, è molto meno efficiente l’inserimento e la rimozione. Ne consegue che se si prevede di inserire e cancellare elementi di frequente, probabilmente la map non è la scelta giusta.

E’ stimato che la map sia più efficiente di un vector soltanto superati i 10 elementi, per cui anche questo è da tener presente scegliendo il tipo di container da usare.

A differenza degli altri container, per istanziare una map bisogna indicare due tipi invece che uno, che saranno il tipo della chiave e il tipo dell’elemento. Ad esempio per fare una map che come chiave abbia un numero e come elemento una stringa, possiamo scrivere map<int, string>.

Per costruire l’albero binario, del valore che verrà passato come chiave verrà calcolato l’hash, la cui efficienza è tanto maggiore quanto più semplice è la chiave. Specificando ad esempio come chiave un intero, avremo che l’hash di un intero è l’intero stesso, ed è quindi molto efficiente. Se specifichiamo invece un tipo complesso, il calcolo dell’hash sarà più lento. Abbiamo detto comunque che la map ha senso se ho un set di elementi che cambiano pochissimo nel tempo o non cambiano affatto, per cui l’eventuale hash iniziale per il calcolo dell’albero binario potrebbero non incidere cosi tanto sulle performance finali.

Il modo più esplicito per inserire valori in una map è quello di fare un pair, assegnare chiave e valore ai membri first e second del pair, e aggiungerlo quindi alla map, come segue:

map<int, string> m;

pair<first k, second v> v;

v.first = key

v.second = value;

m.insert(v);

// oppure

pair<int, int> p = make_pair(3,5);

m.insert(p);

5. Set

Il set è simile alla map, con la differenza che la chiave e il valore coincidono. ne consegue che, essendo anch’esso ordinato in modo binario, gli elementi sono automaticamente ordinati ad ogni inserimento e cancellazione in seguito al bilanciamento dell’albero binario.

dichiarando un set, oltre a specificare il tipo come tutti gli altri container, possiamo indicare un ordinamento diverso da quello di default, che è crescente.

Altra particolarità del set è che non accetta valori duplicati, in quanto essi stessi fanno da chiave di ricerca. E’ possibile inserire valori duplicati utilizzando il multiset al posto del set, che ha le stesse caratteristiche del set, salvo accettare anche valori duplicati.

6. Stack

Lo stack non è considerato propriamente un container puro, ma viene detto adattatore, perchè in effetti nella pratica non implementa un modo diverso di salvare dati, ma sfrutto un container esistente per ridefinire delle logiche specifiche di lettura e scrittura dati.

Uno stack è una PILA, ovvero un modo di immagazzinare dati in modalità LIFO (Last in, First Out). In una pila, come in una coda, gli elementi possono essere inseriti solo da un lato, e rimossi solo dallo stesso lato da cui vengono inseriti, da questo deriva che il primo elemento che entra nella pila sia per forza di cose l’ultimo ad uscirne, perchè dovendo uscire dallo stesso lato da cui è entrato, non può uscire finchè tutti quelli inseriti dopo di lui siano usciti.

Per default, lo stack utilizza un deque come base per il salvataggio dei dati, ma possiamo specificare noi che container usare in fase di dichiarazione, ad esempio con stack<int, vector<int>> dichiariamo uno stack di interi, indicando che al posto di deque vogliamo utilizzare un vector.

Gli unici metodi implementati per lo stack sono:

push() // inserisce un elemento in testa

pop() // rimuove un elemento dalla testa, senza restituire nessun esito o valore rimosso

top() // legge l’ultimo valore inserito, ovvero quello in testa in quel dato momento.

7.Queue

La queue implementa il concetto di coda, o di fila, per cui il primo elemento che entra nella coda è anche il primo ad uscirne, ovvero l’opposto dello stack. Anche la queue è un adattatore, e sfrutta di default un deque, ma possiamo indicare in fase di dichiarazione quale container usare, come per lo stack. 

I metodi implementati per la queue sono:

front() // restituisce il primo elemento entrato (quello in testa)

back()  // restituisce l’ultimo elemento entrato (quello in coda)

push_back() // inserisce un elemento in coda

pop_front() // rimuove in testa

 

Lascia un commento

Best Practices c++

Un po di best practices e consigli da seguire per del codice ben fatto:

1. se non c’è nel vostro progetto, create un file header chiamato “stdafx.h”, in cui vanno messe per convenzione tutte le inclusioni di header di sistema o di librerie di terze parti, ovvero gli #include <…>, e tutte le direttive “using namespace …”

NON vanno inclusi assolutamente gli header scritti da voi.

2. Quando creiamo un header, è molto importante aggiungere subito la direttiva #pragma once, che impedisce che l’header venga incluso più di una volta.

3. Se all’interno della nostra classe facciamo riferimento ad altre classi, possiamo evitare di fare l’include del rispettivo header, utilizzando la forward declaration, ovvero “class NomeClasse;”. Il compilatore accetta questa dichiarazione come un tipo e ci consente di compilare.

Gli unici #include che andrebbero fatti sono quelli nel file .cpp relativo all’omonimo file .h e quelli relativi alle classi che stiamo eventualmente estendendo/ereditando.

4. Usare una convenzione standard per i nomi di membri, variabili e metodi è la base per lavorare in gruppo senza perdere tempo a interpretare geroglifici. Quella suggerita prevede che le variabili membro siano chiamate m_+iniziale tipo+nome esplicativo con notazione camel case. Ad esempio:

m_vWarrior = variabile membro di una classe, di un tipo generico v), che rappresenta molto probabilmente un’istanza di una classe Warrior

m_pWarrior = variabile membro di una classe, di tipo puntatore (p), che rappresenta molto probabilmente un’istanza di una classe Warrior

m_iWarEnergy = variabile membro di una classe, di tipo intero, che conterrà l’energia di un Warrior

5. Potreste avere degli errori riguardo a delle intestazioni precompilate non trovate in fase di compilazione. Per evitarlo, andate nelle proprietà del progetto, alla voce Proprietà di configurazione -> C/C++ -> Intestazioni Precompilate  e impostate la voce “Intestazione Precompilata” a “Senza Intestazioni Precompilate”

6. Nella definizione di metodi virtual, quindi ridefinibili in una classe derivata, è bene tener presente che un metodo virtual in fase di esecuzione va ad occupare un indirizzo di memoria (4 byte) nella “virtual table”, che si occupa di capire quale metodo deve richiamare quando ad esempio viene richiamato un metodo presente sia nella classe base che nella classe ereditata. Per cui se non è necessario e sapete che non ci sarà mai l’esigenza di sovrascrivere, non fate metodi virtual.

 

 

 

Lascia un commento