Gli operatori 

Come tutti i linguaggi di programmazione, il C dispone di un insieme di operatori, cioè di simboli che rappresentano particolari operazioni sul valore di un dato (che viene comunemente detto operando). 

Alcuni operatori C sono perfettamente equivalenti a quelli omologhi di altri linguaggi, altri sono peculiari; tuttavia, prima di esaminarne le principali caratteristiche, è bene chiarire il significato di due concetti: precedenza e associatività

Quando un operatore agisce su più operandi o in un'espressione sono definite più operazioni, tali concetti assumono notevole importanza, perché consentono di interpretare correttamente l'espressione stessa, stabilendo quali operazioni devono essere effettuate prima delle altre. Consideriamo, quale esempio, una somma: 
    a = b + c;
Nell'espressione sono presenti due operatori: l'uguale (operatore di assegnamento) ed il "più" (operatore di somma). E' facile comprendere che l'espressione ha significato solo se viene dapprima calcolata la somma dei valori contenuti in b e c, e solo successivamente il risultato è assegnato ad a. Possiamo dire che la precedenza dell'operatore di assegnamento è minore di quella dell'operatore di somma. 

Consideriamo ora una serie di assegnamenti: 
    a = b = c = d;
Il compilatore C la esegue assegnando il valore di d a c; poi il valore di c a b; infine, il valore di b ad a. Il risultato è che il valore di d è assegnato in cascata alle altre variabili; in pratica, che l'espressione è stata valutata da destra a sinistra, cioè che l'operatore di assegnamento gode di associatività da destra a sinistra. 

In altre parole, la precedenza si riferisce all'ordine in cui il compilatore valuta gli operatori, mentre l'associatività concerne l'ordine in cui sono valutati operatori aventi la stessa precedenza (non è detto che l'ordine sia sempre da destra a sinistra). 

Le parentesi tonde possono essere sempre utilizzate per definire parti di espressioni da valutare prima degli operatori che si trovano all'esterno delle parentesi. Inoltre, quando vi sono parentesi tonde annidate, vale la regola che la prima parentesi chiusa incontrata si accoppia con l'ultima aperta e che vengono sempre valutate per prime le operazioni più interne. Così, ad esempio, l'espressione 
    a = 5 * (a + b / (c - 2));
è valutata come segue: dapprima è calcolata la differenza tra c e 2, poi viene effettuata la divisione di b per tale differenza. Il risultato è sommato ad a ed il valore ottenuto è moltiplicato per 5. Il prodotto, infine, è assegnato ad a. In assenza delle parentesi il compilatore avrebbe agito in maniera differente, infatti: 
    a = 5 * a + b / c - 2;
è valutata sommando il prodotto di a e 5 al quoziente di b diviso per c; al risultato è sottratto 2 ed il valore così ottenuto viene assegnato ad a

Vale la pena di presentare l'insieme degli operatori C, riassumendone in una tabella le regole di precedenza ed associatività; gli operatori sono elencati in ordine di precedenza decrescente. 

OPERATORI C 
Operatore
Descrizione
Associatività
() chiamata di funzione 
da sx a dx
[] indici di array 
. appartenenza a struttura 
-> appartenenza a struttura refernziata da puntatore 
! NOT logico 
da dx a sx
~ complemento a uno 
- meno unario (negazione) 
++ autoincremento 
-- autodecremento 
& indirizzo di 
* indirezione 
(tipo)  cast (conversione di tipo) 
sizeof() dimensione di 
* moltiplicazione 
da sx a dx
/ divisione 
% resto di divisione intera 
+ addizione 
da sx a dx
- sottrazione 
<< scorrimento a sinistra di bit 
da sx a dx
>> scorrimento a destra di bit 
< minore di 
da sx a dx
<= minore o uguale a 
> maggiore di 
>= maggiore o uguale a 
== uguale a 
da sx a dx
!= diverso da (NOT uguale a) 
& AND su bit 
da sx a dx
^ XOR su bit 
da sx a dx
| OR su bit 
da sx a dx
&& AND logico 
da sx a dx
|| OR logico 
da sx a dx
? : espressione condizionale 
da dx a sx
=, etc. operatori di assegnamento (semplice e composti) 
da dx a sx
, virgola (separatore di espressioni
da sx a dx
Come si vede, alcuni operatori possono assumere significati diversi. Il loro modo di agire sugli operandi è quindi talvolta desumibile senza ambiguità solo conoscendo il contesto di azione, cioè le specifiche espressioni in cui sono utilizzati. Di seguito è fornita una descrizione dettagliata degli operatori di cui ancora non si è detto in precedenza, elencati in ordine di precedenza decrescente, come da tabella, ma, al tempo stesso, raggruppati per analogia di significato. 

Not logico 

Il not logico si indica con il punto esclamativo. Esso consente di negare logicamente il risultato di un confronto, cioè di "capovolgerlo". Perciò, se ad esempio 
    (a > b)
è vera, allora 
    !(a > b)
risulta falsa. Ancora, l'espressione seguente 
    if(!(a = b)) ....
equivale a 
    a = b;
    if(!a) ....
che, a sua volta, è l'equivalente di 
    a = b;
    if(!(a != 0)) ....
cioè, in definitiva, 
    a = b;
    if(a == 0) ....
Si noti che l'operatore "!=", pur essendo formato da due simboli, è per il compilatore un unico token[1], la cui grafia, comunque, è perfettamente coerente con il significato dell'operatore "!" (vedere quanto detto circa gli operatori logici). 

Complemento a uno 

L'operatore di complemento a uno è rappresentato con la tilde ("~"). Il complemento ad uno di un numero si ottiene invertendo tutti i bit che lo compongono: ad esempio, con riferimento a dati espressi con un solo byte, il complemento a uno di 0 è 255, mentre quello di 2 è 253. Infatti, rappresentando il byte come una stringa di 8 bit, nel primo caso si passa da 00000000 a 11111111, mentre nel secondo da 00000010 si ottiene 11111101. Pertanto 
    a = 2;
    printf("%d\n",~a);
produce la visualizzazione proprio del numero 253

L'operatore di complemento a uno (o negazione binaria) non va confuso né con l'operatore di negazione logica, di cui si è appena detto, né con quello di negazione algebrica o meno unario ("-", vedere di seguito), dei quali si è detto poco sopra: del resto, la differenza tra i tre è evidente. Il primo "capovolge" i singoli bit di un valore, il secondo rende nullo un valore non nullo e viceversa, mentre il terzo capovolge il segno di un valore, cioè rende negativo un valore positivo e viceversa. 

Negazione algebrica 

Il segno meno ("­") può essere utilizzato come negazione algebrica, cioè per esprimere numeri negativi o, più esattamente, per invertire il segno di un valore: in tal caso esso ha precedenza maggiore di tutti gli operatori aritmetici, per cui 
    a = -b * c;
è valutata moltiplicando c per il valore di b cambiato di segno. Si osservi che le negazione algebrica di un valore non modifica il valore stesso, ma lo restituisce con segno opposto e identico modulo: nell'esempio appena riportato, il valore in b non viene modificato. 

Autoincremento e autodecremento 

Gli operatori di (auto)incremento e (auto)decremento sommano e, rispettivamente, sottraggono 1 alla variabile a cui sono applicati. L'espressione 
    ++a;
incrementa di 1 il valore di a, mentre 
    --a;
lo decrementa. E' molto importante ricordare che essi possono essere prefissi o suffissi; possono, cioè, sia precedere che seguire la variabile a cui sono applicati. Il loro significato rimane il medesimo (sommare o sottrarre 1), ma il loro livello di precedenza cambia. Nell'espressione 
    a = ++b;
ad a viene assegnato il valore di b incrementato di 1, perché, in realtà, dapprima è incrementata la variabile b e successivamente il suo nuovo valore è assegnato ad a. Invece, con 
    a = b++;
ad a è assegnato il valore di b e solo successivamente questa è incrementata. Analoghe considerazioni valgono nel caso dell'operatore di decremento. Ancora: nella 
    if(a > ++b) ....
la condizione è valutata dopo avere incrementato b, mentre nella 
    if(a > b++) ....
dapprima è valutata la condizione e poi viene incrementata b

La differenza tra operatore prefisso e suffisso, però, scompare quando l'autoincremento della variabile sia parametro di una chiamata a funzione: con riferimento ad una riga come 
    printf("%d\n",++a);
spesso non è possibile sapere a priori se a viene incrementata prima di passarne il valore a printf(), o se, al contrario, l'incremento è effettuato in seguito. Ci si potrebbe aspettare che la scrittura ++a determini l'incremento prima della chiamata, mentre a++ lo determini dopo; tuttavia il C non stabilisce una regola univoca. Ciò significa che ogni compilatore può regolarsi come meglio crede. E questo a sua volta significa che possono esserci compilatori che fissano a priori un modo univoco di procedere, ed altri che invece decidono caso per caso in fase di compilazione, sulla base, ad esempio, di opzioni di ottimizzazione del codice in funzione della velocità, della dimensione, e così via. E' dunque indispensabile consultare molto attentamente la documentazione del compilatore o, meglio ancora, evitare possibili ambiguità dedicando all'incremento della variabile un'istruzione separata dalla chiamata a funzione, anche in vista di un possibile porting del programma ad altri compilatori. 

Gli operatori "++" e "--" modificano sempre il valore della variabile[2] a cui sono applicati. 

Cast e conversioni di tipo 

In una espressione è sempre possibile avere operandi di tipo diverso. Non è poi così strano dividere, ad esempio, un numero in virgola mobile per un numero intero, oppure, anche se a prima vista può sembrare meno ovvio, moltiplicare un intero per un carattere. In ogni caso, comunque, il risultato dell'operazione deve essere di un unico tipo, di volta in volta ben determinato: in tali casi è sempre necessario, perciò, procedere a conversioni di tipo su almeno uno degli operandi coinvolti. 

Il C, al riguardo, fissa un ordine "gerarchico" dei tipi di dato intrinseci, e stabilisce due semplici regole che consentono di conoscere sempre a priori come verranno effettuate le necessarie conversioni. 

L'ordine gerachico dei tipi, decrescente da sinistra a destra, è il seguente: 
long double > double > float > long > int > short > char
Ne risulta che ogni tipo è di "grado" superiore ad ogni altro tipo elencato alla sua destra e di grado inferiore a quello dei tipi elencati alla sua sinistra. Sulla scorta di tale gerarchia, la prima regola stabilisce che nelle espressioni che non coinvolgono operatori di assegnamento, in ogni coppia di operandi l'operando di grado inferiore è convertito nel tipo dell'operando avente grado superiore. Così, ad esempio, in una operazione di confronto tra un float e un long, quest'ultimo è convertito in float prima che sia effettuato il confronto. 

La seconda regola riguarda invece le operazioni di assegnamento: l'espressione a destra dell'operatore di assegnamento è sempre convertita nel tipo della variabile che si trova a sinistra del medesimo, indipendentemente dal livello gerarchico dei dati coinvolti. 

Naturalmente le due regole possono trovare contemporanea applicazione quando ad una variabile sia assegnato il risultato di un'espressione che coinvolge operandi di tipi differenti: 
    int iVar;
    long lVar;
    float fVar;
    char cVar;

    ....
    iVar = fVar + lVar * cVar;
Nell'esempio, l'operatore di moltiplicazione ha precedenza rispetto a quello di somma, perciò viene dapprima calcolato il prodotto di lVar per cVar, dopo avere convertito cVar in long. Il valore ottenuto è poi sommato a quello contenuto in fVar, ma solo dopo averlo convertito in float. Il risultato, infine, viene convertito in int ed assegnato a iVar

Si tenga presente che le conversioni effettuate in modo automatico dal compilatore C implicano un troncamento della parte più significativa del valore convertito quando esso viene "degradato" ad un livello inferiore, ed un'aggiunta di bit nulli quando è "promosso" ad un tipo di livello superiore. Nel secondo caso il valore originario del dato può sempre venire conservato; nel primo, al contrario, esiste il rischio di perdere una parte (la più significativa) del valore convertito. 

L'affermazione risulta palese se si pensa, ad esempio, al caso di una conversione da int a long ed una viceversa: consideriamo due variabili, la prima di tipo int (16 bit) e la seconda di tipo long (32 bit), contenenti, rispettivamente, i valori 5027 (che in codice binario è 0001001110100011) e 2573945 (in binario 00000000001001110100011001111001): la conversione della prima in long implica l'aggiunta di 16 bit nulli alla sinistra di quelli "originali". Lo spazio occupato è ora di 32 bit, ma il valore di partenza non viene modificato. Nel convertire il long in int, al contrario, vengono eliminati i 16 bit più significativi (quelli più a sinistra): i 16 bit rimanenti sono 0100011001111001, che equivalgono, in notazione decimale, a 18041

Conversioni di tipo automatiche sono effettuate anche quando il tipo dei parametri passati ad una funzione non corrisponde al tipo dei parametri che la funzione "desidera". Inoltre, in questo caso, i char sono sempre convertiti in int, anche se la funzione si aspetta di ricevere proprio un char[3]. Va anche sottolineato che il compilatore, in genere, emette un messaggio di warning quando la conversione di tipo generata in modo automatico comporta il rischio di perdere una parte del valore coinvolto. 

Vi sono però spesso situazioni in cui il compilatore non è in grado di effettuare la conversione in modo automatico; ad esempio quando sono coinvolti tipi di dato non intrinseci, definiti dal programmatore (quali strutture, campi di bit, etc.). Altre volte, invece, si desidera semplicemente esplicitare una conversione che il compilatore potrebbe risolvere da sé, al fine di rendere più chiaro il codice o per evitare il warning ad essa correlato. 

In tutti questi casi si può ricorrere all'operatore di cast, il quale forza un qualunque valore ad appartenere ad un certo tipo. La notazione è la seguente: 
    (tipo)espressione
dove tipo può essere una qualsiasi delle parole chiave del C utilizzate nelle dichiarazioni di tipo ed espressione dev'essere una qualsiasi espressione sintatticamente corretta. Ad esempio: 
    int iVar;

    iVar = (int)3.14159;
La conversione illustrata può essere automaticamente eseguita dal compilatore, ma l'esplicitarla mediante l'operatore di cast incrementa la chiarezza del codice ed evita il messaggio di warning. Un altro caso in cui si effettua spesso il cast è l'inizializzazione di un puntatore far o huge con una costante a 32 bit: 
    char far *colVbuf = (char far *)0xB8000000L;   // ptr buffer video testo col.
La conversione automatica, in questo caso, non comporterebbe alcun errore, dal momento che la costante assegnata al puntatore è un dato a 32 bit, esattamente come il puntatore stesso: il compilatore emetterebbe però una segnalazione di warning, per evidenziare al programmatore che un dato di tipo long viene assegnato ad un puntatore far a carattere: una questione di forma, insomma. Di fatto la costante potrebbe essere scritta anche senza la "L" che ne indica inequivocabilmente la natura long, ma in quel caso il compilatore segnalerebbe, con un altro warning, che vi è una costante che, per il valore espresso, deve essere considerata long senza che ciò sia stato esplicitamente richiesto. 

Più significativo può essere l'esempio seguente: 
struct FARPTR {
    unsigned offset;
    unsigned segment;
};

    ....
    char far *cFptr;
    struct FARPTR fPtr;
    ....
    (char far *)fPtr = cFptr;
In questo caso la struttura di tipo FARPTR è utilizzata per accedere separatamente alla parte segmento e alla parte offset di un puntatore far. In pratica, il valore contenuto nel puntatore far è copiato nell'area di memoria occupata dalla struttura: si tratta di un'operazione che potrebbe provocare l'emissione di un messaggio di errore e l'interruzione della compilazione. La presenza dell'operatore di cast tranquillizza il compilatore; dal canto nostro sappiamo che struttura e puntatore occupano entrambi 32 bit, perciò siamo tranquilli a nostra volta. 

Operatore sizeof() 

Il compilatore C rende disponibile un operatore, sizeof(), che restituisce come int il numero di byte[4] occupato dal tipo di dato o dalla variabile indicati tra le parentesi. Esempietto: 
    int pippo;
    long pluto;
    float num;
    int bytes_double;
    ....
    printf("pippo occupa %d bytes\n",sizeof(pippo));
    printf("infatti un int ne occupa %d\n",sizeof(int));
    printf("un long occupa %d bytes\n",sizeof(long));
    printf("...e un fload %d\n",sizeof(float));
    bytes_double = sizeof(double);
    printf("Il double occupa %d bytes\n",bytes_double);
Si noti che sizeof() non è una funzione, ma un operatore: esso è dunque intrinseco al compilatore e non fa parte di alcuna libreria. Inoltre esso restituisce sempre un valore di tipo int, indipendentemente dal tipo di dato o di variabile specificato tra le parentesi. 

Operatori aritmetici 

Gli operatori aritmetici del C sono i simboli di addizione ("+"), sottrazione ("­"), divisione ("/") e moltiplicazione ("*"), quest'ultimo da non confondere con l'operatore di indirezione che utilizza il medesimo simbolo. Anche l'utilizzo di tali operatori appare piuttosto scontato; è comunque opportuno sottolineare che tra di essi valgono le normali regole di precedenza algebrica, per cui le operazioni di moltiplicazione e divisione si calcolano, in assenza di parentesi, prima di quelle di addizione e sottrazione. Così, ad esempio, l'espressione 
    a = b + c * 4 - d / 2;
è calcolata come 
    a = b + (c * 4) - (d / 2);
Vedere anche l'operatore di negazione algebrica

Resto di divisione intera 

Quando si effettua una divisione tra due interi, il C restituisce solamente la parte intera del risultato. Se esiste un resto, questo è perso. Ad esempio, l'espressione 
    a = 14 / 3;
assegna 4 ad a

Se interessa conoscere il resto della divisione, è necessario utilizzare l'operatore "%": 
    a = 14 % 3;
assegna ad a il valore 2, cioè il resto dell'operazione; in pratica, l'operatore "%" è complementare all'operatore "/", ma è applicabile esclusivamente tra valori rientranti nella categoria degli integral

Shift su bit 

Pur essendo classificato normalmente tra i linguaggi di alto livello, il C manifesta spesso la propria natura di linguaggio orientato al sistema: gli operatori su bit di cui dispone sono una delle caratteristiche che contribuiscono a renderlo particolarmente vicino alla macchina. Tali operatori consentono di agire sui dati integral considerandoli semplici sequenze di bit. 

Particolarmente interessanti risultano due operatori che permettono di traslare, cioè di "fare scorrere", di un certo numero di posizioni a destra o sinistra i bit di un valore: si tratta dei cosiddetti operatori di shift. In particolare, lo shift a sinistra si esprime col simbolo "<<", mentre quello a destra (indovinate un po') con ">>". Esempio: 
    a = 1;
    printf("%d\n",a <<= 2);
    printf("%d\n",a >> 1);
Il frammento di codice riportato produce la visualizzazione dei numeri 4 e 2; infatti, il numero 1 in forma binaria è 00000001. Traslando a sinistra i bit di due posizioni, si ottiene 00000100, che è, appunto, 4. L'operatore di assegnamento può essere composto con gli operatori su bit: ne segue che la seconda riga di codice modifica il valore di a, assegnandole il suo stesso valore traslato a sinistra di due posizioni. La seconda chiamata a printf() visualizza il valore 2, restituito dall'espressione che trasla a destra di una posizione i bit del valore presente in a (4), ma questa volta a non è modificata. 

Va osservato che l'operazione di shift rende privi di significato i primi o gli ultimi bit del valore (a seconda che la traslazione avvenga verso sinistra o, rispettivamente, verso destra)[5]: quegli spazi sono riempiti con bit di valore opportuno. Nel caso di shift a sinistra non vi è mai problema: i bit lasciati liberi sono riempiti con bit a zero; ma nel caso di uno shift a destra le cose si complicano. 

Se l'integral su cui è effettuato lo shift è senza segno, o è signed ma positivo, allora anche in questo caso sono utilizzati bit nulli come riempitivo. Se, al contrario, l'integral è di tipo signed ed è negativo, allora va tenuto presente che il suo bit più significativo, cioè quello all'estrema sinistra, è usato proprio per esprimere il segno. Alcuni processori estendono il segno, cioè riempiono i bit lasciati liberi dallo shift con bit a uno; altri invece inseriscono comunque bit nulli. Pertanto, a seconda del calcolatore su cui è eseguita, una operazione di shift a sinistra come la seguente: 
    signed char sc;

    sc = -1;       // In bits e' 11111111
    sc >>= 4;      // rimane 11111111 con E.S.; diventa 00001111 senza E.S.
può avere quale effetto un valore finale per sc pari ancora a ­1, se il processore effettua l'estensione del segno, oppure pari a 15 se non vi è estensione di segno. Cautela, dunque: consultare la documentazione della macchina[6] prima di azzardare ipotesi. 

Operatori logici di test 

Gli operatori logici di test possono essere suddivisi in due gruppi: quelli normalmente usati nei confronti tra valori e quelli utilizzati per collegare i risultati di due confronti. Ecco una breve serie di esempi relativi al primo gruppo: 
    (a == b)       // VERA se a e' UGUALE a b
    (a != b)       // VERA se a e' diversa da b
    (a < b)        // VERA se a e' strettamente minore di b
    (a > b)        // VERA se a e' strettamente maggiore di b
    (a <= b)       // VERA se a e' minore o uguale a b
    (a >= b)       // VERA se a e' maggiore o uguale a b
La grafia di detti operatori ed il loro significato appaiono scontati, ad eccezione, forse, dell'operatore di uguaglianza "==": in effetti i progettisti del C, constatato che nella codifica dei programmi i confronti per uguaglianza sono, generalmente, circa la metà degli assegnamenti, hanno deciso[7] di distinguere i due operatori "raddoppiando" la grafia del secondo per esprimere il primo. Ne segue che 
    a = b;
assegna ad a il valore di b, mentre 
    (a == b)
esprime una condizione che è vera se le due variabili sono uguali. La differente grafia dei due operatori consente di mortificare, ancora una volta, la povera regola KISS, rendendo possibile scrivere condizioni come 
    if(a = b) ....
Per quanto appena detto, è ovvio che tale scrittura non può significare "se a è uguale a b": si tratta infatti, in realtà, di un modo estremamente succinto per dire 
    a = b;
    if(a) ....
che, a sua volta, equivale a 
    a = b;
    if(a != 0) ....
cioè "assegna b ad a, e se il risultato (cioè il nuovo valore di a) è diverso da 0...", dal momento che il C, ogni qualvolta sia espressa una condizione senza secondo termine di confronto assume che si voglia verificane la non­nullità. Carino, vero? 

Veniamo al secondo gruppo. Gli operatori logici normalmente usati per collegare i risultati di due o più confronti sono due: si tratta del prodotto logico ("&&", o and) e della somma logica ("||", o or). 
    (a < b && c == d)      // AND: vera se entrambe sono VERE
    (a < b || c == d)      // OR: vera se ALMENO UNA e' VERA
E' possibile scrivere condizioni piuttosto complesse, ma vanno tenute presenti le regole di precedenza ed associatività. Ad esempio, poiché tutti gli operatori del primo gruppo hanno precedenza maggiore di quelli del secondo, la 
    (a < b && c == d)
è equivalente alla 
    ((a < b) && (c == d)
Nelle espressioni in cui compaiono sia "&&" che "||" va ricordato che il primo ha precedenza rispetto al secondo, perciò 
    (a < b || c == d && d > e)
equivale a 
    ((a < b) || ((c == d) && (d > e)))
Se ne trae, se non altro, che in molti casi usare le parentesi, anche quando non indispensabile, è sicuramente utile, dal momento che incrementa in misura notevole la leggibilità del codice e abbatte la probabilità di commettere subdoli errori logici. 

Operatori logici su bit 

Gli operatori logici su bit consentono di porre in relazione due valori mediante un confronto effettuato bit per bit. Consideriamo l'operatore di prodotto logico, o and su bit. Quando due bit sono posti in AND, il risultato è un bit nullo a meno che entrambi i bit valgano 1. La tabella illustra tutti i casi possibili nel prodotto logico di due bit, a seconda dei valori che cisacuno di essi può assumere. L'operazione consistente nel porre in AND due valori è spesso indicata col nome di "mascheratura", in quanto essa ha l'effetto di nascondere in modo selettivo alcuni bit: in particolare viene convenzionalmente chiamato "maschera" il secondo valore. Se nella maschera è presente uno zero, nel risultato c'è sempre uno zero in quella stessa posizione, mentre un 1 nella maschera lascia inalterato il valore del bit originario. Supponiamo, ad esempio, di voler considerare solo gli 8 bit meno significativi di un valore a 16 bit: 
    unsigned word;
    char byte;

    word = 2350;
    byte = word & 0xFF;
Il valore 2350, espresso in 16 bit, risulta 0000100100101110, mentre FFh è 0000000011111111. L'operazione di prodotto logico è rappresentabile come 
0000100100101110 &
0000000011111111 =

0000000000101110
ed il risultato è 46. Dall'esempio si trae inoltre che il simbolo dell'operatore di and su bit è il carattere "&": il contesto in cui viene utilizzato consente facilmente di distinguerlo a prima vista dall'operatore  address of, che utilizza il medesimo simbolo pur avendo significato completamente diverso. 
AND
0
1
0
0
0
1
0
1
Più sottile appare la differenza dall'operatore di and logico "&&", sebbene questo abbia grafia differente. L'and su bit agisce proprio sui singoli bit delle due espressioni, mentre l'and logico collega i valori logici delle medesime (vero o falso). Ad esempio, l'espressione 
    ((a > b) && c)
restituisce un valore diverso da 0 se a è maggiore di b e, contemporaneamente, c è diversa da 0. Invece, l'espressione 
    ((a > b) & c)
restituisce un valore diverso da 0 se a è maggiore di b e, contemporaneamente, c è dispari. Infatti un'espressione vera restituisce 1, e tutti i valori dispari hanno il bit meno significativo ad 1, pertanto il prodotto logico ha un bit ad 1 (quello meno significativo, ed è dunque diverso da 0) solo se entrambe le condizioni sono vere. 

L'operatore di or su bit è invece utilizzato per calcolare quella che viene comunemente indicata come somma logica di due valori. Quando due bit vengono posti in OR, il risultato è sempre 1, tranne il caso in cui entrambi i bit sono a 0. Il comportamento dell'operatore di somma logica è riassunto nella tabella. Si noti che il concetto di maschera può essere validamente applicato anche alle operazioni di OR tra due valori, in particolare quando si voglia assegnare il valore 1 ad uno o più bit di una variabile. Infatti la presenza di un 1 nella maschera porta ad 1 il corrispondente bit del risultato, mentre uno 0 nella maschera lascia inalterato il bit del valore originario (questo comportamento è l'esatto opposto di quello dell'operatore "&"). 
OR
0
1
0
0
1
1
1
1
L'operazione di OR sui bit dei valori 2350 e 255 (FFh) è rappresentabile come segue: 
0000100100101110 |
0000000011111111 =

0000100111111111
e restituisce 2599. Il simbolo dell'operatore di or su bit è "|", e non va confuso con quello dell'operatore di or logico "||"; del resto tra i due operatori esistono differenze di significato del tutto analoghe a quelle accennate poco fa circa gli operatori di and su bit e di and logico. 

Esiste un terzo operatore logico su bit: l'operatore di xor su bit, detto anche "or esclusivo". Il suo simbolo è un accento circonflesso ("^"). Un'operazione di XOR tra due bit fornisce risultato 0 quando i due bit hanno uguale valore (cioè sono entrambi 1 o entrambi 0), mentre restituisce 1 quando i bit hanno valori opposti (il primo 1 ed il secondo 0, o viceversa): la tabella evidenzia quanto affermato. 
XOR
0
1
0
0
1
1
1
0
Se ne trae che la presenza di un 1 in una maschera utilizzata in XOR, dunque, inverte il bit corrispondente del valore originario. Rispolverando ancora una volta (con la solenne promessa che sarà l'ultima) l'esempio del valore 2350 mascherato con un 255, si ha: 
0000100100101110 ^
0000000011111111 =

0000100111010001
Il risultato è 2513

Operatore condizionale 

L'operatore condizionale, detto talvolta operatore ternario in quanto lavora su tre operandi[8], ha simbolo "? :" e può essere paragonato ad una forma abbreviata della struttura di controllo if....else. La sua espressione generale è: 
    espressione1 ? espressione2 : espressione3
la quale significa: "se espressione1 è vera (cioè il suo valore è diverso da 0) restituisci espressione2, altrimenti restituisci espressione3". 

Ad esempio, l'istruzione 
printf("%c\n",(carat >= ' ') ? carat : '.');
visualizza il valore di carat come carattere solo se questo segue, nella codifica ASCII, lo spazio o è uguale a questo. Negli altri casi è visualizzato un punto. 

L'operatore condizionale consente di scrivere codice più compatto ed efficiente di quanto sia possibile fare con la if....else, penalizzando però la leggibilità del codice. 

Assegnamento 

L'operatore di assegnamento per eccellenza è l'uguale ("="), che assegna alla variabile alla propria sinistra il risultato dell'espressione alla propria destra. Data l'intuitività del suo significato ed utilizzo, non è il caso di dilungarsi su di esso: vale piuttosto la pena di considerarne l'utilizzo combinato con operatori aritmetici. 

In tutti i casi in cui vi sia un'espressione del tipo 
    a = a + b;
in cui, cioè, la variabile a sinistra dell'uguale compaia anche nell'espressione che gli sta a destra, è possibile utilizzare una forma abbreviata che si esprime "componendo" l'operatore di assegnamento con l'uguale e l'operatore dell'espressione. Si parla allora di operatori di assegnamento composti, in contrapposizione all'operatore di assegnamento semplice (il segno di uguale). Come al solito un esempio è più chiaro di qualunque spiegazione; l'espressione riportata poco sopra diventa: 
    a += b;
Formalizzando il tutto, un assegnamento del tipo 
variabile = variabile operatore espressione
può essere scritta (ma non si è obbligati a farlo) 
variabile operatore = espressione
Ecco l'elenco di tutti gli operatori di assegnamento composti: 
+=  -=  *=  /=  %=  >>=  <<=  &=  ^=  |=
Essi consentono di ottenere espressioni forse un po' criptiche, ma sicuramente assai concise. 

Separatore di espressioni 

In una sola istruzione C è possibile raggruppare più espressioni, non collegate tra loro da operatori logici, ma semplicemente elencate in sequenza per rendere più compatto (ma meno leggibile) il codice. Esempio: 
    int i, j, k;

    i = 0, j = 2, k =6;
La riga che inizializza le tre variabili è equivalente alle tre inizializzazioni eseguite in tre diverse righe. La virgola (",") agisce da separatore di espressioni e fa sì che queste siano eseguite in sequenza da sinistra a destra. Consideriamo ora l'istruzione che segue: 
printf("%d\n",i = 5, j = 4, k = 8);
Che cosa visualizza printf()? Inutile tirare a indovinare, esiste una regola ben precisa. L'operatore "virgola" restituisce sempre il risultato dell'ultima espressione valutata; in questo caso il valore 8, che è passato a printf() come parametro. 

L'operatore di separazione di espressioni viene spesso utilizzato quando sia necessario inizializzare più contatori in entrata ad un ciclo.

OK, andiamo avanti a leggere il libro... 

Non ci ho capito niente! Ricominciamo...