Piccola Guida al C++

image post

Semplice guida al linguaggio C++ scritta durante lo studio dell’esame di Fondamenti di Informatica 1; sebbene sintetica, la guida risulta molto funzionale per un ripasso veloce del linguaggio in oggeto.

Tipo variabili

Char, tipo carattere:

Il tipo “char” ha come valori i caratteri di stampa opportunamente codificati tramite 8 bit (1 byte). La codifica dipende dall’implementazione, e la più comune è quella ASCII. Le operazioni tipiche sui caratteri sono quelle di confronto, tuttavia per essi sono possibili tutte le operazioni definite sugli interi, che agiscono sulle loro codifiche. Il tipo “char”, come detto, è rappresentato su 8 bit anche se è dichiarato come “signed char” (da -128 a +127) o “unsigned char” (da 0 a +255).

Enum, tipo enumerazione:

Un “tipo enumerazione” è un insieme di costanti intere, definite dal programmatore, ciascuna individuata da un identificatore detto enumeratore. I tipi enumerazioni vengono usati per un numero limitato di valori, ed in genere sono associati ad informazioni non numeriche (quali ad es. costanti matematiche). La dichiarazione di un tipo enumerazione ha questa forma:

enum identifier {enumerator-declaration-list}

Agli enumeratori sono associati valori interi consecutivi a partire da 0, perciò il primo elemento ha come valore 0, il secondo 1… e così via. Generalmente le enumerazioni vengono usate in calcoli che coinvolgono operazioni di confronto, ma sono possibili tutte le operazioni definite sugli interi, che agiscono sulle codifiche degli enumeratori.

Conversioni implicite:

intero e reale: l’intero viene convertito in reale e il risultato è un reale. assegnamento di intero o reale: se un’espressione aritmetica viene assegnata ad una variabile di tipo intero o reale, il suo valore viene convertito di conseguenza. Short e Long: con gli interi e i reali gli operandi sono sempre convertiti al tipo di quello con lunghezza maggiore. Un valore “char” o “enum” agisce come un “int”.

Conversioni esplicite:

int i = (int) 3.14; //cast
double f = double(4); //notazione funzionale

Oggetti costanti:

const int kilo = 1024;
const double pi = 3.14159;
const int mille = kilo – 24;
// Gli oggetti del tipo “enum” possono essere considerati
// anche come int costanti.

Operatore SIZEOF

Restituisce il numero di byte (8 bit) con cui viene codificato un determinato tipo di oggetti.

La notazione è la seguente:

sizeof(char);
sizeof ‘c’;
sizeof(int);
sizeof 3;

Definizione e dichiarazione

Definizione: implica allocazione di memoria. Dichiarazione: crea un nuovo elemento.

Espressioni di assegnamento

L’assegnamento si effetua tramite l’operatore “=” (assoc. “d”, priorità 2), da non confondere con l’ “= =” (operatore uguale, assoc. “s”, priorità 9). Gli assegnamenti più comuni sono:

a = b = c = 3; // OK
a = b = 2 * (c = 5); // OK
a = b + 1 = 2 * (c = 5); // ERRORE

Incremento e decremento

Dato un identificatore si possono usare gli operatori di incremento (o decremento) ++ (o –). L’incremento può essere prefisso, in questo caso prima si aumenta di 1 e poi si restituisce il valore:

i = 0;
j = ++i;
cout << i << j << ‘\n’; // 1 1

Nel caso di incremento postfisso prima si restituisce il valore e poi si incrementa di 1:

i = 0;
j = i++;
cout << i << j << ‘\n’; // 1 0

Nota bene: Le stesse proprietà valgono per l’operatore di decremento (–i oppure i–).

Istruzioni Condizionali

Istruzione if

L’istruzione if ha la seguente forma sintattica:

if (expression)
  statement;
else if (expression) // Le righe seguenti potrebbero anche essere omesse
  statement;
else statement;

L’espressione che compare dopo la parola chiave “if” è di tipo logico, per questo bisogna stare attenti quando si fanno operazioni di confronto “a = = b” poiché se per errore si scrive “a = b” questo è un assegnamento ed è sempre vero.

Da notare che se c’è un “else” presente dopo una serie di “if” questo si riferisce a quello più vicino (a meno che lo “statement” non sia racchiuso tra graffe), quindi nella lettura di un programma non è utile tenere in considerazione soltanto l’indentatura che potrebbe essere sbagliata.

Operatore condizionale (e1 ? e2 : e3)

E’ l’unico operatore ternario del C++, la sua associatività è a sinistra e la sua priorità è 3. Si usa nel caso in cui il valore di e1 sia di tipo logico, se è vera si esegue e2, se è falsa si esegue e3. L’espressione condizionale assume il valore dell’espressione (e2 o e3) che viene eseguita.

Istruzione switch e break

L’istruzione “switch” ha la seguente sintassi:

Switch (expression)
{
  case constant-expression: statement;
  break; // opzionale
}
// in genere “default” è l’ultimo valore quindi non ha break

L’istruzione “switch” è molto più chiara dell’ “if”, poiché esamina tutti i casi e funziona solo per confronti di uguaglianza tra discreti. Dopo aver usato un etichetta (case expression:) è consigliabile mettere un “break” a meno che non si voglia produrre un risultato “in cascata”, se così fosse è opportuno commentare adeguatamente il codice; riguardo alle etichette è da ricordare che ci può essere una sola alternativa con l’etichetta “default:”.

Istruzioni di ciclo

Istruzione while

La forma sintattica dell’istruzione “while” è:

while (expression)
{
  statement;
}

L’esecuzione avviene nel seguente modo, viene valutata l’espressione, se il risultato è vero viene eseguita l’istruzione e l’istruzione “while” viene ripetuta, se il risultato è falso l’istruzione “while” termina; il corpo viene eseguito 0 o più volte.

Ci sono tre modi per usare un ciclo “while” che ha passi “n”:

// n conserva il proprio valore, ma utilizzo 2 variabili
while (i < n)
{
  statement;
  i++;
}
// al termine n vale 0 e si rovina il dato di ingresso
while (n > 0)
{
  statement;
  n--;
}
// al termine n vale –1, impostazione consigliata
while (n-- > 0)
{
  statement;
}

Istruzione do

Essa ha la seguente forma sintattica:

do
{
  statement;
}
while(expression);
// non è il REPEAT… UNTIL del Pascal

Inizialmente viene eseguita l’istruzione tra “do” e “while” (corpo del “do”), poi viene valutata l’espressione, se è vera si riesegue l’istruzione “do”, altrimenti (l’espressione è quindi falsa) l’istruzione “do” termina. Il ciclo “do – while” esegue il corpo 1 o più volte.

Istruzione for

La sintassi dell’istruzione “for” è la seguente:

for (init-statement; expressionopt; expressionopt)
{
  statement;
}

L’istruzione di inizializzazione di solito è un assegnamento, che inizializza una “varabile di controllo”; l’espressione che segue l’inizializzazione in genere è di tipo logico, mentre l’ultima espressione agisce sulla “variabile di controllo” modificandone il valore.

Quindi possiamo equiparare un ciclio “for” e un ciclo “while”:

// con il “for”
for (inizializzazione; condizione; incremento)
{
  istruzione;
}
// con il “while”
inizializzazione;
while (condizione)
{
  istruzione;
  incremento;
}

Pertanto l’inizializzazione viene eseguita una sola volta, mentre la condizione viene valutata prima di ciascuna ripetizione del ciclo: se la condizione è vera, vengono eseguiti nell’ordine, l’istruzione e l’incremento, altrimenti il ciclo “for” termina. Il “for” si può sempre usare, ma è preferibile usarlo quando si sa quante volte bisogna ciclare, altrimenti è migliore l’utlizzazione di “while” e “do”. L’istruzione “for” è utilizzabile per creare i “for innestati”, istruzioni for una dentro l’altra che creano dei cicli annidati, ne è un esempio la possibile rappresentazione di una matrice.

Istruzioni di salto

break

esce da un “while” o da uno “switch”.

continue

riprende l’esecuzione quando si valuta la condizione di controllo (non per lo switch).

goto

chiama la funzione definita con un’etichetta.

return

ritorna alla funzione che l’ha chiamato.

Uso di una funzione

Una funzione viene usata quando in un programma avviene spesso di usare la stessa sequenza di operandi, magari applicata a oggetti dello stesso tipo e numero. Una funzione deve essere definita (la definizione è anche una dichiarazione) e dicichiarata. Nella definizione, il programmatore specifica quali sono i dati su cui operare e quali azioni devono essere eseguite. Per dichiarare una funzione esistono sonstazialmente due modi:

int mcd(int alfa, int beta)
int mcd(int, int)

Una funzione può chiamare un’altra funzione (anche se stessa) e per uscire da essa si usa il comando RETURN.

Esistono anche le “Funzioni a Void” (o procedure) che non restituiscono risultati, usate ad esempio per gli stream; in genere queste funzioni posso essere definite senza argomenti.

Ricorsione

Si definisce “Procedimento Ricorsivo” un procedimento definito in termine di se stesso. In C++ per definire un procedimento ricorsivo si usano le funzioni che, all’interno di se stesse, si richiamano creando una nuova istanza, così finchè non si arriva a uno o due casi in cui la funzione ritorna un valore.

Puntatori

Un puntatore equivale in senso stretto ad un’istruzione di indirizzamento a livello macchina, cioè ad un indirizzo di memoria (che può riferire una variabile o un sottoprogramma). L’uso dei puntatori è frequente e viene utilizzato in maniera pesante da tutti quei linguaggi che si basano sull’utilizzo diretto della memoria principale, in primis il C ( e quindi il C++). E’ bene allora prendere alcuni appunti su cosa non fare con i puntatori, in riferimento proprio al C:

  • specificare sempre il tipo di oggetto puntato: essendo un puntatore soltanto una zona di memoria, il compilatore non può effettuare il controllo in fase di compilazione, ma un eventuale errore verra’ fuori solo a tempo di esecuzione;
  • evitare ogni puntatore uttuante; ad esempio, creando un oggetto nelloheap e poi rimuovendolo con la funzione free() (o con l’equivalente delete), è vene mettere sempre il puntatore a 0 per due motivi: utilizzando nuovamente il puntatore come riferimento all’oggetto puntato, in fase di compilazione non ci si accorge che l’oggetto non c’e più; una successiva chiamata a free() potrebbe creare guai seri per il funzionamento del programma (mentre non ha effetti, come la delete, su puntatori nulli);
  • un puntatore se possibile va inizializzato;
  • riferendosi all’oggetto puntato all’interno di una union, c’e il rischio di alterare il dato in essa conservato;
  • il rischio di produrre garbage è elevato con i puntatori se non si ha l’accortezza di usare la funzione free() (o l’operatore delete) per liberare la memoria puntata, a meno che non si abbiano dei processi raffinati di garbage collection 9 (e.g. in Java).

Uso dei puntatori

Un puntatore è un oggetto derivato e ha come valore l’indirizzo di un altro oggetto o funzione. La sintassi di un puntatore è:

“dichiarazione di puntatore”:
“tipo” “specificatore di puntatore”
“specificatore di puntatore”:
* “variabile”
* “specificatore di puntatore”

In genere le operazioni più comuni sui puntatori sono:

int* p = &i;
// gli operatori “&” e “*” hanno assoc. a destra e priorità 15;
*p = 10;
// equivale a i = 10;
*&i = 4;
// equivale a *p = 4;
const int* p = &i;
// non si può aggiornare i con delle istruzioni che usano p;
int* const p = &i;
// non si può modificare il valore di p (es *p = &j //NO);

E’ consigliabile inizializzare sempre esplicitamente i puntatori. Esistono anche i puntatori a void, ad esempio quando bisogna usare il solito puntatore per farlo puntare prima ad un intero ed in seguito ad un carattere; prima di essere deferenziato, un puntatore a void deve essere convertito specificatamente al tipo opportuno (mediante un CAST).

Aliasing

Due variabili sono alias se denotano (condividono) lo stesso oggetto durante l’attivazione di una unità di programma; una modifica del dato sotto il nome di una variabile è automaticamente visibile tramie tutte le variabili che condividono il dato. L’aliasing può sorgere durante l’esecuzione di una procedura quando i parametri sono passati per indirizzo:

void scambio(int &a, int&b)
{
  int t = a;
  a = b;
  b = t;
}

oppure può anche occorrere quando un parametro formale (passato per indirizzo) e una variabile globale individuno lo stesso oggetto o due oggetti sovrapposti:

void scambio(int &a)
{
  int t = a;
  a = globale;
  // globale è un intero con visibilità globale
  globale = t;
}

Ovviamente l’aliasing non sorge se i parametri sono passati per copia; tali parametri giscono da variabili locali all’interno della procedura, e i corrispondenti parametri attuali vengono modificati solo all’uscita della procedura. Una conseguenza dell’aliasing molto spiacevole è che la chiamata a un sottoprogramma potrebbe produrre risultati scorretti e inaspettati, per la lettura. recupero della spazzatura; è una procedura che consiste nell’individuare lo spazio di memoria nello heap davvero necessario al funzionamento del programma e rimuovere il resto; ad esempio è possibile scandire la memoria alla ricerca di celle non puntate da variabili automatiche.

Qual cosa il programmatore è costretto a esaminare non solo la unità incriminata ma tutte le unità che potrebbero chiamare il sottoprogramma. Inoltre l’aliasing riduce le possibilità di generare codice ottimizato; ad esempio:

a = ( x - y * z) + w;
b = ( x - y * z) + u;

Se a è un alias di x, y o z, la sottoespressione x – y * z non può essere calcolata una sola volta, e poi utilizzata nei due assegnamenti. E’ molto probabile dunque che se si profila la possibilità di aliasing, il compilatore non generi nessun tipo di ottimizzazione.

Uso dei riferimenti

I riferimenti permettono al programmatore di riferirsi ad uno stesso oggetto per mezzo di più identificatori, creando dei sinonimi (o alias) di tale oggetto. Non si possono dichiarare riferimenti a riferimenti. Un riferimento ha questa forma:

int& ri = i;

Si può quindi modificare la variabile “i” per assegnamento della variabile “ri” (es: ri = 5; // equivale a i = 5). Si può usare la parola chiave CONST per impedire che l’oggetto venga modificato tramite riferimento:

const int& ri = i;
ri = 1234; // Errore

L’uso dei riferimenti è utile quando si usano le funzioni, poiché permettono di modificare i dati di ingresso.

Gli array

Il seguente esempio mostra la dichiarazione di un array di 5 elementi di tipo int:

int a[5]; // int[5] è il tipo di a;
int b[] =
{
  1, 2, 3, 4, 5
}; //forma inizializzata esplicitamente;

Il nome di un array viene interpretato come l’indirizzo del primo elemento dell’array stesso, indirizzo costante; pertanto *x = x[0] (// o meglio x = &x[0]) quindi si può scrivere per modificare un valore:

a[3] = 7 // assegna 7 al 4° elemento dell’array;
*(x + 3) = 2 // assegna 2 al 4° elemento dell’array;

Questi sono array monodimensionali (o vettori), ma esistono anche array multidimensionali (o matrici) dove il primo array costituisce le righe, il secondo le colonne; l’unico tipo di assegnamento lecito è:

p = mat[0]; // equivale a “p = &mat[0][0];

Gli array nelle funzioni permettono di operare con array dello stesso tipo ma di dimensioni diverse, e poiché gli array vengono passati per indirizzo permette che questi vengano modificati, ma se si utilizza l’attributo const si impedisce alla funzione di modificare il corrispondente argomento attuale.

Strutture

Una struttura ha la seguente sintassi:

struct identificatore
{
  lista di dichiarazioni;
} variabileopt;

Per riferirsi ad elementi di una struttura si usano i “selettori di membro”, “.” e “->”, esempio:

struct persona
{
  char nome[20];
  char cognome[20];
  struct {int giorno, mese, anno} d_nascita;
};
persona andrea;
//inizializza la variabile “andrea” di tipo “persona”;

In questo caso per cambiare il campo del mese della data di nascita si scrive “andrea.d_nascita.mese = 3;”

Unioni

Le “unioni” sono dichiarate e usate con la stessa sintassi delle strutture, eccetto la parola chiave union al posto di struct, ma rappresentano aree di memoria che in tempi diversi possono contenere dati di tipo differente. Esempio:

struct
{
  int i; double d;
} x;
union
{
  int i; double d;
} y;

Nell’ipotesi che i tipi int e double siano rappresentati rispettivamente da 32 e 64 bit, la struttura x occupa 96 bit, mentre l’unione y solo 64, che possono essere dedicati ad un valore intero o ad uno reale. Non sono definite le operazioni di confronto.

Caratteristiche delle funzioni

Puntatori a funzioni

Un puntatore a funzione ha valore un indirizzo di una funzione, e può essere usato per chiamare la funzione stessa: il suo uso permette di scrivere funzioni che accettano altre funzioni come argomento. Esempio:

double sin(double x);
Main( )
{
 double (*fp)(double);
 fp = sin;
 double y = (*fp)(0.5);
}
// equivale a “sin(0.5)”;

Si possono inoltre definire vettori di puntatori a funzione, e strutture o unioni contenenti puntatori a funzione.

Overloading

In C++ si può usare uno stesso identificatore per funzioni diverse, purchè queste abbiano argomenti diversi, in numero e/o tipo. La situazione in cui funzioni diverse hanno lo stesso identificatore si chiama “overloading”. Esempio:

Double radq(int n)
{
 /* versione int */
}
double radq(double x)
{
 /* versione double */
}
main ( )
{
 cout << radq(36) << ‘\n’
 // versione int
 cout << radq(0.789) << ‘\n’
 //versione double
}

Funzioni inline

Il comando “Inline” si usa quando si definisce una funzione ed in pratica dice al compilatore di sostituire, durante la compilazione, il codice della funzione ogni qualvolta viene chiamata la funzione. Il suo contro più grande è quello di allungare il codice. Non si usa MAI con le funzioni ricorsive.

Dichiarazioni typedef

L’operatore typedef permette di creare dei sinonimi di tipo, ad esempio un puntatore a interi si scrive così:

int* p;

Poiché può essere scomodo scrivere tutte le volte il codice si può usare il typedef:

typedef int* intP;

Operatori New e Delete

L’operatore NEW permette di allocare memoria per una variabile. La sintassi è la seguente:

int* h = new int;
// operatore + tipo

Questo operatore va nell’ HEAP (memoria libera) e cerca lo spazio per la variabile e lo riserva. Invece le variabili automatiche vengono allocate nello STACK. Inoltre questo operatore riporta l’indirizzo dell’int.

Invece l’operatore DELETE libera la memoria allocata con NEW e si applica con la seguente sintassi:

delete [ ] p;
// nel caso di array;
delete q;
// distrugge la variabile q;

Effetti collaterali

Esistono alcuni modi per scambiare informazioni tra funzioni, quello classico si ha quando la funzione chiamata è pura, cioè passa i propri risultati alla funzione chiamante soltanto per mezzo dell’istruzione return; un secondo metodo di interazione fra le funzioni si ha quando una delle funzioni può modificare il valore di variabili visibili alle altre. In questo caso, si dice che la funzione ha effetti collaterali. Una funzione può produrre effetti collaterali quando le vengono passati esplicitamente dei puntatori o dei riferimenti, e quando modifica variabili condivise o variabili globali. Nel caso di effetti collaterali possono anche non valere alcune proprietò comuni, ad esempio la proprietò commutativa della somma. L’uso esplicito di puntatori può mettere in evidenza le chiamate di funzione che possono avere effetti collaterali, mentre i riferimenti possono nascondere la presenza di tali effetti; i programmi in cui si usano variabili condivise o variabili globali sono i meno leggibili e quindi più soggetti ad errori.

Overloading di operatori

La ridefinizione di un operatore (tramite “overloading”) ha la forma di una definizione di funzione, il cui identificatore è costituito dalla parola chiave “operator” seguita dall’operatore che si vuole ridefinire, ed ogni occorrenza della funzione equivale ad una chiamata alla funzione.

Funzioni friend

Una funzione è “friend” di una classe se una sua dichiarazione, preceduta dala parola chiave “friend”, appare nella dichiarazione di tale classe. La funzione può accedere ai membri pubblici e privati della classe, usando i selettori di membro. Le funzioni “friend” sono utili anche quando si debbano realizzare delle funzioni che operano su oggetti appartenenti a classi diverse.

Funzioni const

Possiamo dichiarare una funzione membro un modo che non possa modificare l’oggetto a cui è applicata, scrivendo la parola chiave “const” dopo la parentesi che chiude la lista degli argomenti; se un oggetto è stato dichiarato “const”, ad esso si possono applicare solo funzioni “const”. Il meccanismo di overloading distingue due versioni che differiscono solo per la proprietà di essere “const”.

Liste

Una lista è una struttura dati, formata da elementi dello stesso tipo collegati in catena, la cui lunghezza varia dinamicamente. La struttura di una lista è:

Struct elem
{
 int inf;
 elem* pun;
}

Un tipo di lista particolare è “l’ albero” che ha questa struttura:

Struct elem
{
 int inf;
 elem* sin;
 elem* des;
}

Classi

Un tipo “classe” ha una parte privata ed una pubblica. Tipicamente la parte privata contiene le strutture dati che realizzano il tipo, e la parte pubblica contiene le dichiarazioni delle funzioni che definiscono le operazioni fondamentali del tipo e che costituiscono l’interfaccia. Le funzioni dichiarate in una classe sono dette funzioni membro, e sono le uniche che possono accedere alla parte privata della classe. Esempio:

class nome
{
 parte privata;
 public:
 parte pubblica;
};

Per definire od usare una funzione dichiarata nella classe si usa “::” il risolutore di visibilità (assoc. sinistra, priorità 17).

Puntatore this

Una funzione membro può riferirsi esplicitamente all’oggetto a cui viene applicata: per questo scopo utilizza il “puntatore” costante predefinito this, contenente l’indirizzo dell’oggetto. L’uso del puntatore this è necessario quando una funzione membro deve usare esplicitamente l’indirizzo dell’oggetto a cui è applicata, e quando deve restituire l’oggetto stesso.

Costruttori e Distruttori

Un costruttore è una funzione membro il cui nome è il nome della classe: il costruttore viene applicato automaticamente quando si crea un oggetto appartenente alla classe stessa; se definiamo un costruttore non è più legale scrivere una definizione di oggetti senza inizializzatore, a questo scopo si crea un costruttore default con il metodo dell’overloading (funzione();) o degli argomenti default(funzione(int i = 0);).

Un distruttore è una funzione membro che viene invocata automaticamente quando una variabile termina il suo tempo di vita; un distruttore per una classe ha come identificatore il nome della classe preceduto da “ ~ ”.

Oggetti costanti nelle classi

In una classe non si possono dichiarare costanti, tuttavia si possono dichiarare oggetti con l’attributo const, purchè questi vengano inizializzati nel momento in cui viene dichiarato un oggetto appartenente alla classe stessa.

Classi contenenti oggetti classe

Un membro di un oggetto classe può a sua volta essere un oggetto classe.

Array di oggetti classe

Si possono definire o allocare in memoria libera degli array di oggetti classe.

Membri statici

A volte conviene che una classe contenga delle informazioni globali, cioè appartenenti alla classe nel suo complesso e non alle singole istanze; ad esempio per una variabile contatore viene dichiarata come membro della classe e la sua dichiarazione inizia con la parola chiave static.

Classi Stream

Nelle librerie di ingresso/uscita sono definite le classi istream per l’ingresso ed ostream per l’uscita: per usarle bisogna includere il file di intestazione . Lo stream cin è un istanza predefinita della classe istream e rappresenta lo stream standard di ingresso, mentre gli stream cout e cerr sono istanze predefinite della classe ostream e rappresentano gli stream standard di uscita e di errore. Gli operatori di lettura e di scrittura sono definiti per i tipi fondamentali, per i puntatori e per le stringhe (altrimenti vanno ridefiniti con l’overloading). Se occorre leggere anche i caratteri di spaziatura, si usa la funzione membro get(); nella classe ostream è definita la funzione put(), che scrive un carattere nello stream di uscita.

Uso dei file

Nelle librerie di ingresso/uscita del C++ sono definite le classi ifstream, ofstream ed fstream, dichiarate nel file . Il nome del file viene passato al costruttore come stringa, e la classe ios::in serve per l’apertura del in lettura, ios::out per l’apertura in scrittura, ios::in|ios::out per l’apertura in lettura e scrittura, ios::app per l’apertura in append. La modalità ios::nocreate impedisce la creazione del file nel caso questo non esista. Le funzioni membro sono open(), bad(), close() ed i contatori seekg() e seekp().

Ingresso e uscita per i tipi utente

Il meccanismo di overloading permette di ridefinire gli operatori di lettura e scrittura per i tipi definiti dall’utente.

Semplice guida al linguaggio C++ scritta durante lo studio dell’esame di Fondamenti 1;
sebbene sintetica, la guida risulta molto funzionale per un ripasso veloce del linguaggio
in oggeto.

Tagged : , ,