[C] perchè usare unsigned

studente_studente
Buonasera,
sapete dirmi per quale motivo dovrei preferire, ad esempio unsigned int al posto del solito int?
Io so che con unsigned non si presenta il problema del segno ma cambia qualcosa in memoria in fatto di bit?

Inoltre: Devo usare %u sulla printf e sulla scanf?
Allora se io ho due tipi, ad esempio:
unsigned int a;
unsigned float b;
unsigned double c;
 
scanf("%u %u %u", &a, &b, &c);
printf("%u %u %u", a, b, c);

Può essere mai che hanno tutti lo stesso %u ?! :evil:

E se, in una possibile acquisizione, l'utente scrivesse -1 al posto di 1? Che succede? Salva solo 1 o da problemi?
(Lo avrei già provato ma non so se usare %u oppure normalmente %d/f)

Nel libro c'è scritto che lo specificatore di conversione u permette di leggere/stampare un intero decimale senza segno quindi non posso fare unsigned float? E allora se voglio salvare un numero con la virgola positivo? Intendo dire, posso pure fare un controllo e richiedere all'utente di inserire un dato fino a quando esso non è positivo ma pensavo si potesse fare... c'è un altro modo?

P.S. nel libro fa:

int a;

scanf("%u", &a);

E allora mi chiedo, di nuovo, perché dovrei dichiarare un tipo apposito unsigned int?

Grazie in anticipo e per la pazienza!

Risposte
Raptorista1
Un [inline]int[/inline] e un [inline]unsigned int[/inline] hanno la stessa dimensione, solo (come dicevi) il bit di segno viene usato per memorizzare l'intero e non il segno. Ammettiamo che entrambi occupino 4 bit, allora un [inline]int[/inline] può memorizzare tutti i numeri da \(-2^3+1\) a \(2^3\), più o meno [magari non sono questi i numeri giusti, però il concetto c'è] mentre un [inline]unsigned int[/inline] può memorizzare numeri tra \(0\) e \(2^4\). Questo è un primo fattore che potrebbe aiutarti ad evitare un overflow.
Ci potrebbero essere anche ragioni di qualità del codice: se una grandezza deve essere positiva, è bene che questo vincolo sia forzato nel codice. In questo modo se provassi ad utilizzarlo in maniera non consona il compilatore dovrebbe avvisarti [se non sbaglio, GCC ha un warning se fai il test [inline]a < b[/inline] dove uno è con segno e uno no].

Questi mi vengono in mente, potrebbero essercene altri.

claudio862
In C solo i tipi interi si distinguono in con / senza segno. Float, double e simili no.

Come detto da Raptorista, nei tipi con segno uno dei bit viene usato per rappresentare il segno, quindi si perde un ordine di grandezza nei possibili valori.

Un'altra differenza tra tipi con / senza segno è che per i secondi sono definiti overflow e underflow (secondo l'aritmetica modulare), ma per i primi no:

unsigned int a = UINT_MAX; // Probabilmente 4294967295
a += 1;
// Ora a == 0


signed int a = INT_MAX; // Probabilmente 2147483647
a += 1;
// Undefined behaviour, lo stato del programma è indefinito
// (Anche se su un normale PC probabilmente a == INT_MIN)


"studente-studente":
Inoltre: Devo usare %u sulla printf e sulla scanf?

Sì. %u per variabili "unsigned int", %ul per variabili "unsigned long int" e %ull per variabili "unsigned long long int".

"studente-studente":
P.S. nel libro fa:

int a;

scanf("%u", &a);

E allora mi chiedo, di nuovo, perché dovrei dichiarare un tipo apposito unsigned int?

A occhio questo codice è sbagliato e dovrebbe risultare in undefined beaviour, dato che %u non corrisponde a signed int, ma non sono completamente sicuro. Nella pratica, scanf (e printf e simili) funziona così:

- Scorre la stringa del formato.
- Quando incontra uno "specificatore" (chiamiamoli così, quelli che iniziano con %) si aspetta una variabile di una certa dimensione (tot byte) tra gli argomenti.
- Prende questa variabile, la interpreta secondo lo specificatore, e avanza di tot byte nella lista degli argomenti.
- Continua a scorrere la stringa del formato.

Dato che signed int e unsigned int hanno la stessa dimensione, quello che succede è scanf che interpreta &a come puntatore a unsigned int, legge un valore senza segno dall'utente e lo memorizza nel formato relativo (che, a memoria, per tipi con segno è (di solito) in complemento a due). Se provi a inserire un numero minore di INT_MAX tutto funziona, ma se inserisci un numero più grande, che andrebbe bene per unsigned int, ti ritroverai un valore negativo.
Problemi più gravi accadono quando usi uno specificatore per un tipo più grande / piccolo di quello che passi.

char a = 1;
scand("%lf", & a);


Qui scanf prende l'indirizzo di a e ci scrive dentro un double, che di norma è molto più grande (8 bit vs 64 bit). Quindi va a sovrascrivere altre variabili.

Questo poi è complicato dal fatto che lo standard C è molto lasco nel definire dimensioni dei vari tipi. Su un PC probabilmente avrai char da 8 bit, short da 16, int da 32 e long da... Boh... Dipende da sistema operativo e compilatore, a volte 32, a volte 64.
Essendo così lasco permette di scrivere un compilatore per macchine relativamente strane (dove per esempio un char è grande 32 bit, oppure 9). In generale non si dovrebbero fare ipotesi sulla dimensione delle variabili, ma usare i valori in limits.h.

Tornando alla domanda iniziale, usare o meno unsigned porta diverse soluzioni e diversi problemi. Ecco due pareri contrastanti:
http://www.soundsoftware.ac.uk/c-pitfall-unsigned
http://blog.robertelder.org/signed-or-unsigned/

Questa invece è una spiegazione su cosa significhi undefined behaviour. Anche se potrebbe essere un po' troppo complicata per un principiante è comunque una lettura interessante.
http://blog.regehr.org/archives/213

apatriarca
Quando il C è stato progettato esistevano tantissimi sistemi diversi per rappresentare i numeri con segno (e anche char, float, double, puntatori..). Invece di forzare quindi le implementazioni a emulare delle rappresentazioni non native, ha deciso di lasciare molta libertà. Questa libertà ha da una parte favorito le performance del C, ma ha certamente portato ad una enorme serie di problemi di portabilità nascosti da operazioni con un comportamento non definito o che dipendono dalla implementazione. Spesso i programmatori tendono a non preoccuparsi più di tanto di queste problematiche finché qualcosa cambia nel loro sistema e tutto smette di funzionare con i bug più diversi.

I numeri con virgola sono solitamente rappresentati con una delle seguenti 4 implementazioni:

1. Segno + mantissa (i numeri a virgola mobile fanno uso di una rappresentazione simile a questa). Un bit viene riservato per il segno e il resto è riservato al numero senza virgola.
2. Complemento a 1. I numeri negativi sono rappresentati con tutti i bit invertiti.
3. Complemento a 2 (quello più usato in assoluto). I numeri negativi sono rappresentati come il complemento del numero in riferimento a \(2^N\) dove \(N\) è il numero di bit. In altre parole sottrai il numero senza segno da \(2^N\). Il vantaggio di questa rappresentazione risiede nel fatto che somma e sottrazione sono immediate. Inoltre ha un solo zero (al contrario di 1 e 2 che hanno sia +0 che -0).
4. Bias. Scegli l'intervallo che vuoi rappresentare e sottrai a tutti i numeri il minimo di tale rappresentazione. Non è usata più di tanto per la rappresentazione di numeri interi nei computer. E' tuttavia presente nella rappresentazione floating point per l'esponente.

Per fare un esempio. Supponiamo di avere 4 bit e di voler rappresentare i numeri 4, 0 e -2. Nelle varie rappresentazioni abbiamo:

1. 4: 0100. 0: 0000 e 1000. -2: 1010.
2. 4: 0100. 0: 0000 e 1111. -2: 1101.
3. 4: 0100. 0: 0000. -2: 1110.
4. Dipende dal valore rappresentabile minore scelto. Supponiamo si voglia usare -7. 4: 1011. 0: 0111. -2: 0101.

E' quindi evidente la ragione per cui non fosse all'epoca possibile stabilire come si dovesse comportare un numero negativo quando superava il suo massimo o diventava più piccolo del suo minimo. In pratica ormai si usa praticamente solo il complemento a 2 e molti linguaggio più moderni danno per scontato che sia così.

Per quanto riguarda l'uso di unsigned o int la mia opinione è la seguente:
1. Per memorizzare i valori sapendo di volere solo numeri positivi: usare unsigned.
2. Se si vuole sfruttare il comportamento di overflow/underflow dei numeri unsigned: usare unsigned.
3. Per i calcoli (in situazioni in cui l'overflow e l'underflow sono evitabili) usare variabili signed.
4. Se si vogliono memorizzare numeri con segno non c'è ovviamente scelta e vanno usati i numeri con segno.
5. In funzioni pubbliche in cui si vuole forzare il fatto che un valore deve essere positivo allora è meglio usare unsigned.
6. Usare SEMPRE la libreria stdint.h e i tipi ivi definiti per avere tipi interi con una particolare dimensione.

studente_studente
"Raptorista":
Ci potrebbero essere anche ragioni di qualità del codice: se una grandezza deve essere positiva, è bene che questo vincolo sia forzato nel codice.


"claudio86":
In C solo i tipi interi si distinguono in con / senza segno. Float, double e simili no.


Ma allora se avessi bisogno di un tipo non intero necessariamente positivo come dovrei fare allora?
C'è un modo per salvare un numero reale, float o double, positivo?


"claudio86":
Se provi a inserire un numero minore di INT_MAX tutto funziona, ma se inserisci un numero più grande, che andrebbe bene per unsigned int, ti ritroverai un valore negativo.


In effetti avevo letto, al suo tempo, questa particolarità.. gli darò di nuovo una lettura approfondita; ti ringrazio per la puntualizzazione. E' una cosa parecchio bizzarra, chissà perché si ha questo "fenomeno".

Ringrazio anche apatriarca per il suo prezioso commento, non sapevo nemmeno che esistessero il complemento a 1 e bias!
Provvederò a colmare questa lacuna e leggere bene la "storia" e i progressi del linguaggio di programmazione C..anche se temo di aver bisogno ancora qualche nozione prima di buttarmi in questa impresa eheh

P.S. cercando online ho scoperto che unsigned e signed si usano pure per i char! Chi lo avrebbe mai immaginato

Raptorista1
"studente-studente":

Ma allora se avessi bisogno di un tipo non intero necessariamente positivo come dovrei fare allora?
C'è un modo per salvare un numero reale, float o double, positivo?

Non è offerto dal linguaggio, ma puoi implementare tu la cosa con delle struct e dei metodi apposta.
Potrebbe non essere il modo migliore di fare, ma il primo che mi viene in mente è di definire una [inline]struct positive_real[/inline] che contiene solo un float e poi funzioni per le varie operazioni come [inline]positive_real addPositiveReals(positive_real x, positive_real y)[/inline] che contengono tutti gli [inline]if[/inline] del caso.
Un modo diverso potrebbe essere di fare overloading di [inline]operator+[/inline], [inline]operator*[/inline] e così via, ma qui mi si mischiano C e C++ e non so quanto si può fare in puro C.

"studente-studente":

P.S. cercando online ho scoperto che unsigned e signed si usano pure per i char! Chi lo avrebbe mai immaginato

[inline]char[/inline] altro non è che un intero.

studente_studente
"Raptorista":
[inline]char[/inline] altro non è che un intero.


Effettivamente guardando la tabella ascii e' abbastanza chiaro, è come se scrivendo char ci sia una conversione automatica no?

Ora che ci penso, esiste pure size_t.. ma e' solo un typedef di unsigned int quindi è uguale usare l'uno o l'altro

Raptorista1
Non è una conversione, [inline]char[/inline] e [inline]int[/inline] sono tipi di dati interi diversi, come [inline]float[/inline] e [inline]double[/inline] sono tipi di dato in virgola mobile diversi.

[inline]size_t[/inline] è un'altra storia, e credo che sia un tipo di dato definito nello standard C++, non C, però qui altri ti sapranno aiutare meglio. Si trova anche molto online sul perché [inline]size_t[/inline] è meglio di [inline]unsigned int[/inline], e il primo è diventato più popolare recentemente.

apatriarca
size_t è definito in C da sempre. Rappresenta un intero senza segno grande abbastanza da contenere la dimensione di qualsiasi blocco di memoria allocabile sul sistema. Nel caso di una macchina a 64-bit sarà insomma un intero senza segno da 64 bit. Altri esempi di questo tipo di numeri interi sono clock_t, ptrdiff_t, ..

Siccome i tipi nel C sono pensati per essere in diretta corrispondenza (o quasi) con i tipi di dato su cui la CPU è in grado di lavorare, non esistono tipi come "floating point solo positivi". Ma in generale, la vera utilità dei tipi unsigned risiede nella rappresentazione binaria ben definita e non ne fatto di supportare solo interi non negativi. Questo vantaggio non esiste per i floating point.

Raptorista1
Grazie per la correzione, apatriarca :smt023

apatriarca
Considera comunque che non tutti i linguaggi hanno tipi interi senza segno. Java e Python sono esempi di tali linguaggi. Questi linguaggi hanno tuttavia spesso bisogno di tipi appositi per lavorare direttamente con sequenze di byte.

Rispondi
Per rispondere a questa discussione devi prima effettuare il login.