domenica 10 ottobre 2010

C++ liste e iteratori...

Salve a tutti :)

In questo post volevo scrivere qualcosa sul come navigare una lista usando gli iteratori in C++.
Cominciamo con un piccolo programma di esempio:

#include <iostream>
#include <list>

using namespace std;

int main(int argc, char **argv) {

    list<int> l;

    for(int i = 0; i < 10; i++)
        l.push_back(i);

    list<int>::const_iterator it;
    for(it = l.begin(); it != l.end(); ++it)
        cout << *it << endl;

}


Bene, nelle prime righe ci sono i soliti include, nello specifico iostream serve per poter utilizzare cout; list come si può intuire serve per poter utilizzare il tipo list dell'STL.
Le successive righe del programma non fanno altro che inizializzare la lista e riempirla con un po' di numeri.

Dalla riga 13 in poi si comincia a scorrere la lista.
Per prima cosa viene dichiarato un const_iterator, un iteratore, cioè un oggetto che permette di accedere ai membri di una classe container (un contenitore di oggetti), in questo caso di una lista. Esso può essere visto come un puntatore agli elementi della classe container, nel nostro caso, esso è il puntatore all'elemento corrente della lista.

Successivamente, l'iteratore viene inizializzato. L'istruzione l.begin() restituisce l'iteratore agli elementi della lista, precisamente un iteratore che punta all'elemento begin.
Gli elementi begin e end non sono il primo e l'ultimo elemento della lista, ma sono dei valori sentinella, l'header e il trailer della stessa. Essi sono cioè l'elemento che precede il primo elemento della lista e quello successivo all'ultimo.
Infatti l'iteratore appena inizializzato non viene subito utilizzato per leggere gli elementi, ma la prima cosa che viene fatta è incrementarlo per poter puntare al primo elemento della lista, con l'istruzione.
Fatto ciò esso può essere utilizzato come se fosse un puntatore all'elemento della lista, in questo caso un puntatore a intero.
Ovviamente l'output del programma è l'elenco dei numeri da 0 a 9.

Una cosa da notare è che l'iteratore in questione è un const_iterator, questo significa che può essere utilizzato in sola lettura e non è possibile modificarne gli elementi.
Per poter modificare gli elementi puntati dall'iteratore basta modificare leggermente il programma precedente:

#include <iostream>
#include <list>

using namespace std;

int main(int argc, char **argv) {

    list<int> l;

    for(int i = 0; i < 10; i++)
        l.push_back(i);

    list<int>::iterator it;
    for(it = l.begin(); it != l.end(); ++it) {
        *it *= 2;
        cout << *it << endl;
    }

}

Come mostrato, in questo modo è possibile modificare gli elementi della lista.

Ovviamente in una lista possono essere contenuti diversi tipi di dati, non solo interi.
Si può scorrere tranquillamente una lista usando gli iteratori anche con altri tipi di strutture dati, quali classi o struct.
Vediamo ora un semplice esempio nell'uso di un iteratore per scorrere una lista che contiene una struct, per poi vederne uno in cui la lista contiene degli oggetti che sono istanze di classi:

#include <iostream>
#include <list>

using namespace std;


struct myStruct {
    string something;
};


int main(int argc, char **argv) {

    list<myStruct> l;

    myStruct *s1 = new myStruct();
    myStruct *s2 = new myStruct();
    s1->something = "hello ";
    s2->something = "world";

    l.push_back(*s1);
    l.push_back(*s2);

    list<myStruct>::const_iterator it;
    for(it = l.begin(); it != l.end(); ++it)
        cout << (*it).something;

}


Qui invece di un intero, la lista contiene una semplice struttura che al suo interno contiene solo una stringa.
Come prima cosa si inizializzano due variabili di tipo myStruct e ci si mette qualcosa dentro.
Visto che ogni tutorial quando ci sono delle stringhe è sempre presente "hello world", questo è quello che è stato inserito nelle strutture :-P
Successivamente si inseriscono le due variabili nella lista. Siccome la lista è stata dichiarata list<myStruct> i riferimenti alle strutture devono essere deferenziati prima di poterli inserire. Ovviamente è possibile inserire direttamente i riferimenti, dichiarando la lista come list<*myStruct>.
Come prima, il passo successivo consiste nell'inizializzare l'iteratore.
La cosa da notare qui è come si accede agli elementi della lista. L'iteratore è un puntatore all'elemento corrente della lista, quindi bisogna innanzitutto deferenziarlo per accedere all'elemento contenuto.
L'istruzione (*it) restituisce quindi l'oggetto di tipo myStruct corrente, si può quindi accedere ai suoi membri semplicemente usando l'operatore punto.
Nel caso in cui nella lista fossero stati inseriti i riferimenti alle strutture, l'istruzione (*it) avrebbe restituito un altro puntatore, cioè un puntatore di tipo myStruct *.
In questo caso per accedere all'elemento in esso contenuto si sarebbe dovuto usare l'operatore -> in questo modo: (*it)->something.
Su questo vorrei sottolineare una cosa...se utilizzate Eclipse CDT (io ci sono capitato con Helios), potreste essere tratti in inganno dall'autocompletamento. Se scrivete it->some e poi utilizzate l'autocompletamento, questo vi mostrerà comunque "something" o qualsiasi altro elemento della struttura. E' un bug che mi ha fatto perdere un bel po di tempo :-)

Per concludere vediamo ora come accedere agli elementi di un iteratore quando in essi è contenuto un oggetto di tipo class. Come vedremo, è praticamente uguale al caso precedente.
Ecco il codice di esempio:


#include <iostream>
#include <list>

using namespace std;


class Persona {

private:
    string nome;
    string cognome;

public:

    Persona(string nome, string cognome): nome(nome), cognome(cognome){};

    string getNome() { return nome; }
    string getCognome() { return cognome; };
};


int main(int argc, char **argv) {

    list<Persona> l;

    Persona *p1 = new Persona(string("Ajeje"), string("Brazorf"));
    Persona *p2 = new Persona(string("Maccio"), string("Capatonda"));

    l.push_back(*p1);
    l.push_back(*p2);

    list<Persona>::const_iterator it;
    for(it = l.begin(); it != l.end(); ++it)
        cout << it->getNome() << " " << it->getCognome() << endl;

}

Qui vediamo una semplice classe chiamata Persona. Ha due variabili istanza che vengono inizializzate nel costruttore e due metodi per accedervi.
Il procedimento è molto simile agli esempi precedenti, cambia solo il fatto che questa volta la lista conterrà oggetti di tipo Persona.
Anche l'inizializzazione è uguale all'esempio precedente.
Quello che cambia è che in questo caso non c'è bisogno di deferenziare l'iteratore per accedere ai metodi dell'oggetto. In questo caso l'iteratore può essere considerato esattamente come un puntatore all'oggetto, dunque può essere usato come tale per accedere ai suoi membri pubblici.

Bene..con questo direi che vi ho annoiati abbastanza e posso chiudere il post :-)
Spero che qualcuno trovi utili le cose che ho scritto!
Arrivederci al prossimo post! ^_^

Nessun commento:

Posta un commento