La più importante caratteristica del C++, che lo rende un linguaggio per la programmazione orientata agli oggetti, è la possibilità offerta al programmatore di definire un nuovo tipo attraverso un meccanismo chiamato Classi
Mentre la dichiarazione di un tipo predefinito è chiamata variabile, la dichiarazione (o istanza) di una classe è chiamata Oggetto
Prima di descrivere com'è fatta una classe, si consideri il modo in cui si crea un nuovo tipo di dato in C.
Creazione di un nuovo tipo di dato in C
Ad esempio, si ha bisogno di scrivere un programma C che fa un intenso uso di date. Si potrebbe creare un nuovo
tipo di dato in grado di rappresentare le date, usando la seguente struttura:
struct Data {
int giorno;
int mese;
int anno;
};
I campi membro di questa struttura sono mese, giorno, e anno. Per memorizzare una data si devono assegnare i valori corretti ai campi della struttura:
struct Data unaData;
unaData.giorno = 23;
unaData.mese = 1;
unaData.anno = 1985;
Non si può stampare una tale data passando la struttura a printf. Bisogna stampare ciascun campo della struttura separatamente, oppure si deve scrivere una funzione che riceve la data come parametro e la stampa, come nel seguente esempio:
void mostraData( struct Data *dt ) {
static char *nomeMese[] = {
"zero", "Gennaio", "Febbraio", "Marzo",
"Aprile", "Maggio", "Giugno", "Luglio", "Agosto",
"Settembre","Ottobre","Novembre","Dicembre"
};
printf( "%d-%s-%d", dt->giorno, nomeMese[dt->mese], dt->anno );
}
Questa funzione stampa il contenuto di una struttura "data", trasformando il numero del mese nel nome e stampando i campi della data.
Per eseguire operazioni sulle date, come ad esempio il confronto tra due di esse, si dovrebbero confrontare i singoli campi della struttura, oppure si potrebbe scrivere una funzione che riceva le due strutture come parametri e effettui il confronto.
Quando si definisce una struttura in C si crea un nuovo tipo. Quando si scrivono funzioni che operano su quella struttura si creano le operazioni ammesse su quel tipo di dato.
Questa tecnica per gestire le date ha dei difetti:
Non garantisce che una variabile di tipo "data" contenga una data valida. Ad esempio si potrebbe, involontariamente, rappresentare una data come 31 Febbraio 2000, oppure i campi potrebbero non essere stati inizializzati e quindi trovarsi a rappresentare valori casuali come il 158-mo giorno dell'ottavo mese di un certo anno. Quando si usa una variabile con tali valori si avranno risultati privi di significato.
Una volta che si è deciso di usare una tale struttura, in seguito sarà difficile apportare modifiche. Ad esempio si potrebbe desiderare di codificare il giorno e il mese in modo da occupare meno spazio di memoria, oppure si potrebbe rappresentare solo il numero del giorno dell'anno, come un numero da 1 a 365. Per fare queste modifiche ogni programma che usa questa struttura deve essere riscritto per adeguarsi alla nuova organizzazine dei dati. Anche le eventuali funzioni che accedono ai singoli campi giorno e mese devono essere riscritte.
Un programmatore esperto riesce a prevenire questi inconvenienti, perchè sicuramente ha già vissuto le conseguenze derivanti dalla necessità di apportare modifiche ad un programma.
Creazione di un nuovo tipo di dato in C++
In C++ si definiscono sia i dati che le operazioni su di essi dichiarandoli in una classe.
Dichiarazione della classe
La dichiarazione di una classe assomiglia alla dichiarazione di una struttura, con la differenza che, la classe, contiene
anche le funzioni. La seguente è una versione della classe Data:
// La classe Data
class Data {
public:
Data( int g, int m, int a ); // Costruttore
void mostra(); // funzione per stampare la data
~Data(); // Distruttore
private:
int giorno, mese, anno; // campi privati
// alcune funzioni utili:
inline int max( int a, int b) {
if( a > b ) return a;
return b;
}
inline int min( int a, int b) {
if( a < b ) return a;
return b;
}
};
Questa dichiarazione di classe corrisponde alla combinazione di una dichiarazione di struttura più un insieme di prototipi di funzione. Essa dichiara quanto segue:
Il contenuto di ogni istanza di Data: gli interi giorno, mese ed anno. Questi sono i campi membro della classe, anche detti attributi o proprietà
I prototipi di tre funzioni che si possono usare con gli oggetti (o istanze di classe Data):
Data,
~Data,
mostra.
Queste sono le funzioni membro della classe, anche dette metodi della classe. Le definizioni delle funzioni membro
si forniscono di seguito alla dichiarazione della classe.
Ecco le definizioni delle funzioni membro:
// - - - - - - il costruttore
Data::Data( int g, int m, int a ) {
static int lung[] = { 0, 31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31 };
mese = max( 1, m );
mese = min( mese, 12 );
giorno = max( 1, g );
giorno = min( giorno, lung[mese] );
anno = max( 1, a );
}
// - - - - - - Funzioni membro per stampare date
void Data::mostra() {
static char *nomeMese[] = {
"zero", "Gennaio", "Febbraio", "Marzo",
"Aprile", "Maggio", "Giugno", "Luglio", "Agosto",
"Settembre","Ottobre","Novembre","Dicembre"
};
cout << giorno << ' ' << nomeMese[mese] << ' ' << anno;
}
// - - - - Il distruttore
Data::~Data() {
// non fa nessuna operazione
}
La funzione mostra è familiare, ma le funzioni Data() e ~Data() sono nuove. Sono chiamate il Costruttore e il Distruttore e servono per creare e distruggere oggetti.
Queste non sono tutte le funzioni membro che una classe Data potrebbe richiedere, ma sono sufficienti per mostrare la sintassi di base per scrivere una classe.
Ecco un programma che usa la classe Data:
void main() {
Data unaData( 3, 12, 1985 ); // Dichiara una data
Data altraData( 23, 259, 1966 ); // Dichiara una data impossibile
unaData.mostra();
cout << endl;
altraData.mostra();
cout << endl;
}
Usare la Classe
Dopo aver definito una classe, si possono dichiarare una o più istanze di quella classe, esattamente
come si fa con i tipi predefiniti.
Nell'esempio precedente. la funzione main dichiara due istanze della classe Data chiamate unaData e altraData. Questi sono oggetti, e ciascuno di essi contiene i valori di giorno, mese ed anno.
Nella dichiarazione di un oggetto potebbe esserci un elenco di parametri che servono per assegnare i valori iniziali alle proprietà della classe. Ad esempio, nella dichiarazione degli oggetti unaData e altraData sono presenti i valori iniziali, che verranno passati al costruttore, allo scopo di assegnarli ai campi membro. Si noti la sintassi per stampare il contenuto degli oggetti di classe Data.
In C si sarebbe dovuto passare la struttura come parametro:
mostraData( &unaData );
mostraData( &altraData );
In C++ si richiama la funzione membro, o metodo, usando l'operatore di accesso: ".", come nell'esempio seguente:
unaData.mostra();
altraData.mostra();
Queste espressioni evidenziano la stretta relazione tra il tipo Data e le funzioni che operano su di esso. Fa capire che l'operazione mostra() è parte della classe Data.
Comunque questo legame tra la classe e le sue funzioni compare solo nella sintassi. I singoli oggetti di classe Data non contengono una propria copia delle funzioni, essi contengono solo i campi membro.
Membri della classe (Attributi)
Si osservi la dichiarazione della classe Data:
class Data {
public:
Data( int g, int m, int a ); // Costruttore
void mostra(); // funzione per stampare la data
~Data(); // Distruttore
private:
int giorno, mese, anno; // campi privati
}
Una dichiarazione di classe differisce dalla dichiarazione di una struttura nei seguenti modi:
Ha i termini public e private
dichiara funzioni
include il costruttore e il distruttore
Di seguito vengono esaminate queste tre differenze.
Visibilità dei campi membro
I termini private e public all'interno della definizione della classe specificano la
visibilità dei membri che seguono. Tutti i campi che seguono un termine hanno la visibilità specificata,
fino a quando si incontra un termine diverso, oppure fino alla fine della definizione della classe.
I campi privati sono accessibili solo dalle funzioni membro della classe. Questi definiscono lo stato interno
della classe.
I campi pubblici sono accessibili sia dalle funzioni membro della classe sia da tutte le altre funzioni del programma che hanno un'istanza della classe nella loro regione di visibilità. I campi pubblici rappresentano le funzionalità che la classe mostra al resto del programma, in altri termini si dice che essi sono l'interfaccia della classe.
La classe Data dichiara che i suoi tre campi membro sono privati, ovvero sono visibili solo alle funzioni membro della classe. Se un'altra funzione tenta di accedere a uno di questi campi privati il compilatore segnala un errore. Per esempio, si supponga di accedere a un campo privato di un oggetto di classe Data:
void main() {
int i;
Data unaData( 3, 12, 1985 );
i = unaData.mese; // Errore: non è ammesso leggere un campo privato
unaData.giorno = 1; // Errore: non è ammesso scrivere in un campo privato
}
Al contrario, la funzione mostra() è pubblica, quindi è visibile alle funzioni esterne alla classe.
Sebbene sia ammesso usare, anche ripetendo, i termini public e private dovunque all'interno della classe, conviene raggruppare tutti i campi pubblici e tutti i campi privati e poi tutti metodi pubblici e tutti i metodi privati. Se manca la specifica, si assume che i campi siano privati. Ma è preferibile specificarlo ugualmente.
La classe Data mostra una regola importante da rispettare nella programmazione orientata agli oggetti: L'interfaccia pubblica contiene solo funzioni. Un campo membro privato deve essere letto o modificato solo chiamando una apposita funzione membro pubblica.
Funzioni membro
La classe Data ha una funzine membro denominata mostra(), che serve a stampare i campi membro.
Si noti che la funzione è dichiarata e anche definita. Il prototipo della funzione appare all'interno della
dichiarazione della classe Data e quando la funzione è definita è indicata con:
Data::mostra( ). Questa notazione indica che la funzione è membro della classe e che il suo nome
rientra nella regione di visibilità della classe. L'operatore "::" è detto scope resolution,
cioè indicatore della regione di visibilià.
Le funzioni membro possono essere ridefinite, purchè restino distinguibili in base ai parametri.
La funzione membro mostra() non riceve parametri, perchè ha libero accesso ai campi membro dell'oggetto. Per esempio:
unaData.mostra();
altraData.mostra();
Nella prima istruzione la funzione mostra() accede ai campi dell'oggetto unaData, nella seconda istruzione la funzione mostra() accede ai campi dell'oggetto altraData. La funzione usa i campi dell'oggetto a cui appartiene.
Una funzione membro può essere chiamata anche tramite un puntatore a un oggetto, usando l'operatore ->. Per esempio:
Data unaData( 3, 12, 1985 );
Data *ptrData = &unaData;
ptrData->mostra();
In questo segmento di programma, viene dichiarato un puntatore ad un oggetto Data e poi si richiama la funzione mostra() tramite quel puntatore.
Una funzione membro può essere chiamata tramite il riferimento ad un oggetto. Per esempio:
Data unaData( 3, 12, 1985 );
Data &altraData = unaData;
altraData.mostra();
Qui la funzione mostra() viene chiamata tramite il riferimento altraData. Siccome altraData è un sinonimo di unaData, la funzione mostra() stampa il contenuto di unaData.
Queste tecniche descritte per chiamare una funzione valgono solo se la funzione è pubblica. Se una funzione è dichiarata private, può essere chiamata solo dalle funzioni della stessa classe. Per esempio:
class Data {
public:
void mostra(); // funzioni Pubbliche
// ...
private:
int giorniTrascorsi(); // funzioni Private
// ...
};
// - - - - - - - mostra la data nella forma "ggg AAAA"
void Data::mostra() {
cout << giorniTrascorsi() << " " << anno; // chiama una funzione privata
}
// - - - - Calcola il numero di giorni trascorsi
int Data::giorniTrascorsi() {
int totale = 0;
static int lung[] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
for( int i= 1; i<= mese; i++ )
totale += lung[i];
totale += giorno;
return totale;
}
Notare che la funzione mostra() chiama direttamente la funzione giorniTrascorsi(), senza specificare l'oggetto, perchè una funzione membro può usare campi e funzioni dell'oggetto a cui appartiene.
Una lista concatenata offre alcuni vantaggi rispetto agli array
Una lista non ha una dimensione massima, quindi non si ha spreco di spazio inutilizzato o, viceversa, non succede che un elemento non trova posto perché si è esaurito lo spazio. Inoltre in una lista gli elementi possono essere inseriti o eliminati senza alterare la posizione degli altri elementi.
Una lista si definisce concatenata perché ogni elemento contiene il puntatore al successivo elemento. Si supponga che gli elementi contenuti nella lista siano di tipo intero. I metodi di una classe Lista sono:
Metodo | Operazione | Parametro ricevuto | Parametro restituito |
Lista () | costruttore senza parametri | void | void |
Lista | costruttore copia | Lista | void |
~Lista | distruttore | void | void |
Inserisci() | Inserisce un elemento in testa alla lista | Tipo dell'elemento di lista | |
NumeroElementi() | Restituisce il numero degli elementi contenuti nella lista | void | int |
Svuota() | Elimina tutti gli elementi contenuti nella lista | void | void |
Elimina() | Elimina il primo elemento della lista, se contenuto, che è uguale a quello specificato | Tipo dell'elemento di lista | void |
Stampa() | Stampa tutti gli elementi contenuti nella lista | void | void |
Ricerca() | Indica se un dato elemento è contenuto nella lista | Tipo dell'elemento di lista | bool |
Nr. linea | Commento |
4-7 | Si definisce la struttura Record: contiene un campo dato e un campo puntatore al successivo. |
8, 9, 10 | Il costruttore assegna i valori iniziali ai campi della classe. |
Nr. linea | Commento |
12 | Lista(const Lista& lst) Il costruttore Copia riceve il riferimento ad una lista, che non viene modificata (const): L'oggetto lst ricevuto come parametro: ![]() |
13 | if ((this != &lst) && primo) Se il riferimento ricevuto (lst) non punta alla stessa lista e la lista lst contiene almeno un elemento (primo diverso da 0) si procede con la copia della lista lst in questa lista. |
14 | primo = new Record Si crea un nuovo record e si raccoglie il suo puntatore nel campo primo della classe ![]() |
15 | primo->dato = lst.primo->dato Nella sezione dato del nuovo record si copia il valore contenuto nel corrispondente elemento della lista da copiare ![]() |
16 | PRec lp = lst.primo Si crea un nuovo puntatore a Record e lo si inizializza con il puntatore al primo Record della lista da copiare. ![]() |
17 | PRec p = primo p è un puntatore a Record inizializzato con il puntatore al primo Record della lista in cui copiare gli elementi. ![]() |
18 | while (lp->successivo) Si avanza nella lista da copiare, fintanto che non si raggiunge il puntatore 0 |
19 | p->successivo = new Record Si aggiunge un nuovo record alla lista in cui copiare il nodo dalla lista sorgente lst. ![]() Il puntatore al nuovo nodo viene scritto nel campo successivo del record puntato da p |
20 | p = p->successivo Si passa al nodo successivo. ![]() |
21, 22 | lp = lp->successivo p->dato = lp->dato Si avanza nella lista sorgente e si copia il campo dalla lista sorgente alla lista destinazione. ![]() |
24 | p->successivo = 0 Quando si raggiunge la fine della lista sorgente si assegna il valore 0 anche al puntatore contenuto nell'ultimo record della lista, per indicare la fine della lista. |
Nr. linea | Commento |
30 | Inserisci(const Elemento& el) Il metodo Inserisci riceve il riferimento ad un intero, che non viene modificato (const): |
31 | PRec p = new Record Viene creato un nuovo nodo e il puntatore viene mamorizzato in p |
32 | p->dato = el nel canpo dato del nuovo nodo viene memorizzato il valore ricevuto come parametro. |
Nr. linea | Commento |
38 | PRec p = primo Si dichiara un puntatore locale alla struttura Record e lo si inizializza con il puntatore alla testa della Lista. |
39 | while (p) { si entra in un ciclo che si ripete se p è diverso da 0. |
40 | cout << p->dato << " - " si stampa il valore contenuto nel campo dato del nodo. |
41 | p = p->successivo si avanza nella lista, leggendo il puntatore al nodo successivo e assegnandone il valore al puntatore p. |
42 | si ritorna al test di ripetizione del ciclo while |
Nr. linea | Commento |
45 | Il metodo NumeroElementi restituisce il numero di elementi contenuti nella Lista. |
Nr. linea | Commento |
48 | PRec cancella si dichiara un puntatore a Record. |
49 | while (primo) { si entra in un ciclo a condizione che il puntatore al primo elemento della lista sia diverso da 0. |
50 | cancella = primo si effettua una copia del puntatore al primo elemento della lista. |
51 | primo = primo->successivo il successivo elemento diventa il nuovo elemento di testa della lista, eliminando così dalla lista il primo elemento che è puntato dal puntatore cancella. |
52 | delete cancella si elimina lo spazio occupato dal nodo estratto dalla lista. |
54 | conteggio = 0 Al termine del ciclo il campo membro conteggio viene posto a 0. |
Nr. linea | Commento |
56 | Elimina(const Elemento& el) { il metodo riceve come parametro il riferimento a un intero, che rappresenta il valore che si vuole eliminare dalla lista. |
57 | if(primo) { se la lista non è vuota. |
58 | if (primo->dato == el) { se il primo elemento della lista contiene il valore ricevuto come parametro. |
59 | PRec cancella = primo si salva il puntatore al primo elemento della lista. |
60 | primo->successivo si passa al successivo elemento. |
61 | delete cancella si elimina lo spazio occupato dal nodo. |
62 | conteggio-- si conta un elemento in meno. |
63 | altrimenti si entra in un ciclo di scansione della lista, alla ricerca del nodo il cui campo dato sia uguale al parametro el |
Nr. linea | Commento |
79 | bool Ricerca(const Elemento& el) { il metodo riceve come parametro il riferimento a un intero, che rappresenta il valore che si vuole eliminare dalla lista e restituisce un valore booleano. |
80 | PRec p = primo Si accede alla lista. |
81 | bool trovato = false si crea una variabile booleana. |
82 | while ((p) && (!trovato)) { Si scandisce la lista, a condizione che l'elemento non sia stato trovato e non si sia raggiunta la fine della lista. |
83, 84 | if (p->dato ==el) trovato = true Se il nodo raggiunto contiene il valore cercato si ritorna true. |
86 | p = p->successivo altrimenti si avanza nella lista. |
Nr. linea | Commento |
5 | La funzione StampaMenu()verrà richiamato
per proporre le possibili operazioni ammesse sulla lista. |
Nr. linea | Commento |
19, 20 | vengono dichiarate due variabili |
21 | Lista lista viene dichiarata l'istanza lista della classeLista |
21 | |
22 | do { Si entra in un ciclo |
23 | StampaMenu() viene richiamata la funzione per mostrare il menu |
24 | cin >> i la scelta avviene introducendo un numero da tastiera e premendo invio. Il tasto premuto viene acquisito in formato carattere |
25 | switch(c) { a seconda del carattere il flusso del programma subisce una diramazione verso l'operazione scelta |
56, 61 | Si deve notare che (linea 57) viene creata una seconda istanza della classe Lista richiamando il costruttore copia (quello con un parametro di ingresso). Le operazioni sono inserite in un blocco in modo che al termine dell'uso della lista copiata venga richiamato il distruttore. |
L'albero che si propone nel seguente esercizio memorizza, in ogni nodo, numeri interi.
Per ogni nodo, a sinistra ci sono i valori minori e a destra i valori maggiori del valore contenuto nel nodo.
Esempio:
Nr. linea | Commento |
3 | questa dichiarazione serve ad informare il compilatore che l'identificatore Nodo è una struttura. |
4, 5 | l'identificatore pNodo è di tipo "puntatore a Nodo". (il compilatore ha appreso nella linea precedente il tipo dell'identificatore Nodo) e Elemento è un nuovo tipo di variabile intera. |
6 | Alberobinario è una classe. |
8 | radice è di tipo pNodo (come definito alla linea 4). |
9, 15 | Sezione dei metodi privati |
17, 25 | Sezione dei metodi pubblici |
Nr. linea | Commento |
4 | Nodo è una struttura. |
7 | il campo che contiene l'informazione è costituito da un intero. |
8, 9 | Ogni nodo dell'albero ha due puntatori ai nodi successori. |
Nr. linea | Commento |
2 | Il costruttore assegna il valore iniziale zero al puntatore alla radice dell'albero, per indicare che l'albero è vuoto. |
5 | Il distruttore richiama il metodo privato Svuota per rilasciare la memoria occupata dai nodi creati dinamicamente. |
Nr. linea | Commento |
1 | Il metodo AggiungiNodo riceve il riferimento ad un valore da memorizzare nell'albero. |
2 | Richiama il metodo AggiungiElem passando il riferimento alla radice dell'albero e il riferimento al valore da inserire. |
5 | la prima volta che la funzione viene richiamata la radice vale zero.
Quando l'albero contiene almeno un elemento il riferimento al nodo è diverso da zero quindi verrà eseguito il ramo else . |
6 - 9 | viene creato un nuovo nodo, viene inizializzato con il dato ricevuto e con i due puntatori a zero. Si deve notare che il parametro n è dichiarato come riferimento quindi, la prima volta, alla linea 6 viene modificato anche il valore di radice, o comunque del puntatore contenuto nel nodo genitore. |
11 | si deve decidere se il nodo deve essere inserito nel sotto albero di destra o di sinistra. |
12 - 14 | Avviene una chiamata ricorsiva alla funzione, accedendo ad uno dei due sottoalberi del nodo. |
Nr. linea | Commento |
1 | riceve, come parametro, un valore e restituisce un valore logico per indicare se il valore è presente nell'albero. |
5 | se si raggiunge un nodo terminale significa che il valore non è stato trovato. |
7, 8 | se il valore cercato corrisponde al valore contenuto nel nodo si restituisce true |
10, 12 | altrimenti si richiama ricorsivamente la funzione passando il puntatore al sottoalbero appropriato. |
Nr. linea | Commento |
1 | il metodo Sostituisci riceve il puntatore n alla radice di un albero e il puntatore p ad un nodo da sostituire |
2 | q è il puntatore a un nodo |
3 | se il sottoalbero destro è vuoto |
4 | copia il puntatore al nodo corrente |
5 | con n avanza nel sottoalbero sinistro |
6 | nel puntatore a sinistra del sottoalbero corrente copia il puntatore a sinistra contenuto nel nodo da sostituire |
7 | nel puntatore a destra del sottoalbero corrente copia il puntatore a destra contenuto nel nodo da sostituire |
8 | il puntatore al nodo da sostituire viene aggiornato con il puntatore al nodo corrente. |
10 | altrimenti (se il sottoalbero destro non è vuoto) richiama ricorsivamente la funzione passando come parametri il sottoalbero destro e il nodo da sostituire. La condizione di arresto della chiamata ricorsiva è che il sottoalbero destro sia vuoto. |
Nr. linea | Commento |
1 | il metodo Elimina riceve il puntatore alla radice dell'albero e il valore dell'elemento da eliminare |
2 | se non si è raggiunta una foglia |
3 | e se il campo dato è uguale al valore cercato |
4 | copia il puntatore al nodo corrente |
5 | se il sottoalbero sinistro del nodo da eliminare è vuoto |
6 | sostituisci il nodo con il sottoalbero destro |
8 | altrimenti se il sottoalbero destro del nodo da eliminare è vuoto |
9 | sostituisci il nodo con il sottoalbero sinistro |
10 | il nodo da eliminare ha entrambi i sottoalberi |
11 | richiama la funzione Sostituisci |
12 | cancella il nodo |
13 | se il campo dato è diverso dal valore da eliminare |
14 | se il valore da eliminare è maggiore del valore contenuto nel campo dato |
15 | richiama ricorsivamente la funzione accedendo al sottoalbero destro |
17 | altrimenti richiama ricorsivamente la funzione accedendo al sottoalbero sinistro |
Nr. linea | Commento |
2 | Se il sottoalbero non è vuoto |
3, 4 | richiama ricorsivamente la funzione svuota passando il puntatore a uno dei due sottoalberi |
5 | al ritorno dalla funzione elimina il nodo |
Nr. linea | Commento |
2 | se non si è raggiunta una foglia |
3 | stampa il dato |
4 | avanza a sinistra |
5 | quando si è esaurito il sottoalbero sinistro avanza nel sottoalbero destro |
Nr. linea | Commento |
2 | se non si è raggiunta una foglia |
3 | avanza nel sottoalbero sinistro |
4 | quando si è esaurito il sottoalbero sinistro avanza nel sottoalbero destro |
5 | mostra il dato |
5 |
linea