pc_cap6 fara 6.7 ; 6.8 - operatorul de secventiere
TRANSCRIPT
6. Operatori şi expresii
Expresiile sunt categorii sintactice fundamentale pentru limbajul C. Practic cele mai multe şi
mai des folosite instrucţiuni sunt construite pe baza expresiilor. Expresiile reprezintă un
mijloc extrem de puternic de manipulare a valorilor, pentru că instrucţiunile tuturor limbajelor
procedurale acţionează asupra datelor prin intermediul acestora.
6.1 Evaluarea expresiilor
Singura operaţie ce se poate efectua asupra expresiilor este cea de evaluare. În urma evaluării
unei expresii rezultă întotdeauna o valoare, care este utilizată în cadrul instrucţiunilor
limbajului într-un mod sau altul, în funcţie de tipul instrucţiunii.
Din punct de vedere sintactic, o expresie este formată din operatori, operanzi şi
eventual perechi de paranteze rotunde. Operanzii reprezintă elemente asupra cărorra se poate
acţiona, iar operatorii reprezintă diferitele operaţii ce se pot efectua asupra operanzilor.
Perechile de paranteze rotunde specifică diferitele subexpresii ce sunt puse în evidenţă şi care
sunt tratate de către compilator drept operanzi.
Operanzii pot fi constante, nume de variabile, nume de funcţii sau subexpresii. Ei
generează valori fie direct (în cazul constantelor sau a variabilelor simple), fie după realizarea
unei opraţii de prelucrare.
Exemple. 2.5 /* operand simplu */
a /* operand simplu */
v[3] /* 3 este operand simplu */
/* v este operand asupra caruia se efectueaza
operatia de indexare */
f(n) /* n este operand simplu */
/* f este operand asupra caruia se efectueaza
operatia de apel de functie */
a*(b+2) /* doi operanzi:
- a este operand simplu,
- (b+2) este subexpresie
*/
x = 3 /* doi operanzi simpli (x si 3);
Atentie: atribuirea este considerata
operator in limbajul C */
Limbajul C dispune de o mulţime de operatori, fapt ce permite o mare flexibilitate în
descrierea prelucrărilor din cadrul programelor. Din punct de vedere al tipului operatorilor şi
al semnificaţiei lor, aceştia se pot împărţi în următaorele clase:
operatori aritmetici
operatori relaţionali
operatori logici
operatori de atribuire
operatori de adrsare
operatori la nivel de bit
operatori specifici limbajului
În funcţie de numărul de operanzi asupra cărora se aplică, operatorii pot fi unari, binari,
ternari, sau în cazul general n-ari (se aplică asupra a n operanzi).
Exemple. x-y /* operator binar */
i-- /* operator unar */
-a-b /* primul operator este unar */
al doilea este binar */
n?a:b /* ?: este operator ternar */
Din punct de vedere al poziţiei operatorilor faţă de operanzi, aceştia pot fi în poziţie prefixă
(preced operanzii), postfixă (urmează după operanzi) sau infixă (operatorul se află între
operanzi). Primele două poziţii sunt specifice operatorilor unari, pe când ultima este specifică
celor binari.
Exemple. n++ /* operatorul ++ este in pozitie postfixa */
--i /* operatorul -- este in pozitie prefixa */
a+b /* operatorul + este in pozitie infixa */
Pentru evaluarea unei expresii se iau în considerare alte două proprietăţi ale operatorilor:
precedenţa şi asociativitatea acestora. Precedenţa determină prioritatea operatorilor, iar
asociativitatea determină ordinea de aplicare a operatorilor consecutivi.
Pentru a determina ordinea de aplicare a operatorilor asupra operanzilor într-o expresie
fără paranteze, se au în vedere următoarele elemente:
întâi se grupează operatorii în clase de precedenţă; într-o clasă de precedenţă toţi
operatorii au aceeaşi prioritate;
la evaluare, operatorii se aplică în ordinea descrescătoare a precedenţei;
în cadrul fiecărei clase, ordinea depinde de asociativitatea acestora (de la stânga la
dreapta sau invers).
Exemplu. Pentru expresia: a + 2 * 3 + b + 4 + d * e * 5
clasele de precedenţă în ordinea descrescătoare sunt:
1) 2 * 3, d * e * 5
asociativitate de la stânga la dreapta
2) a + (2 * 3) + b + 4 + (d * e * 5)
asociativitate de la stânga la dreapta
Tabelul următor conţine toţi operatorii limbajului C grupaţi în ordinea descrescătoare a
precedenţei. Pentru fiecare clasă se specifică şi regula de asociere.
Operator Utilizare Semnificaţie Asociere ()
[]
.
->
--
++
f(e)
v[i]
r.c
p->c
a--
a++
apel funcţie
indexare
selecţie
selecţie indirectă
postdecrementare
postincrementare
---->
-
+
--
++
!
~
*
&
sizeof
()
-n
+n
--a
++a
!i
~i
*p
&x
sizeof(x)
(d) e
schimbare de semn
plus unar
predecrementare
preincrementare
negaţie logică
negare la nivel de bit
adresare indirectă
preluare adresă
determinare dimensiune memorie
conversie de tip (cast)
<----
*
/
%
v1 * v2
v1 / v2
i1 % i2
Înmulţire
împărţire
determinare rest (modulo)
---->
+
-
v1 + v2
v1 – v2
Adunare
Scădere
---->
<<
>>
i1 << i2
i1 >> i2
deplasare stânga
deplasare dreapta
---->
<
<=
>
>=
v1 < v2
v1 <= v2
v1 > v2
v1 >= v2
mai mic
mai mic sau egal
mai mare
mai mare sau egal
---->
==
!=
v1 == v2
v1 != v2
Egal
Diferit
---->
& i1 & i2 “şi” la nivel de bit ---->
^ i1 ^ i2 “sau exclusiv” la nivel de bit ---->
| i1 | i2 “sau” la nivel de bit ---->
&& i1 && i2 şi logic ---->
|| i1 || i2 sau logic ---->
?: i1 ? v1 :
v2
operator condiţional ---->
=
*=
/=
-=
+=
&=
^=
|=
<<=
>>=
a = v
a *= v
a /= v
a -= v
a += v
a &= v
a ^= v
a |= v
a <<= v
a >>= v
operatori de atribuire <----
, e1, e2 Secvenţiere ---->
În cazul în care o expresie conţine subexpresii, se modifică ordinea de aplicare a operatorilor.
Regula este simplă, are un caracter recursiv şi descrie ordinea de aplicare a operaţiilor
algebrice: dacă într-o expresie există subexpresii, acestea sunt evaluate înaintea celorlalţi
operanzi.
Exemplu. Fie expresia: e =
2
1
))(**(**
e
e
hgfedcba
- în expresia: a*b+c*e2, se evaluează întâi expresia e2;
- în expresia: d+e*f*e1, se evaluează întâi expresia e1;
- expresiile: a*b+c+e2, d+e*f*e1 se evaluează conform regulilor de precedenţă;
Observaţie. Se va nota în continuare cu Eval operaţia de evaluare. De exemplu, pentru
expresia precedentă, evaluarea ei se poate descrie astfel: Eval(e) = Eval (a*b+c*(d+e*f*(g+h))) =
= Eval (a*b+c*Eval(d+e*f*Eval(g+h)))
Spre deosebire de alte limbaje de programare, în limbajul C evaluarea unei expresii poate
avea şi efecte laterale datorate modificării valorilor unor variabile în timpul evaluării. Acest
lucru se poate întâmpla datorită operatorului de atribuire.
Exemplu. if ( x + ( y = v[k]) < a)
z = 1;
else
z = 2;
În expresia condiţională anterioară, se evaluează întâi subexpresia y=v[k], care are drept
efect modificarea valorii variabilei y.
Această facilitate permite descrierea mai compactă a unor secvenţe de prelucrări care
în mod normal necesită mai multe instrucţiuni de atribuire. În cazul utilizării excesive a
acestui stil de programare, există însă riscul srierii unor programe criptice, greu de înţeles şi
nu întotdeauna corecte.
Un alt aspect ce intervine în operaţia de evaluare a unei expresii se referă la
conversiile automate ale tipurilor de date ale diferitelor valori ce intervin într-o expresie.
Există mai multe motive ce impun conversii de tip, principala cuză fiind determinată de
tipurile de date diferite ale operanzilor asupra cărora se aplică un operator.
Marea majoritate a operatorilor, în special cei aritmetici, necesită operanzi de acelaşi
tip, iar din acest motiv, în cazul în care operanzii sunt de tipuri diferite, este necesară (dacă
este posibil) o operaţie de conversie a valorii unui operand sau a ambilor operanzi. Acestă
operaţie se numeşte în mod uzual reechilibrare, regula de bază fiind următoarea:
dacă operanzii sunt de tip unsigned int şi long atunci
dacă valorile unsigned int pot fi reprezentate ca long atunci
tipul echilibrat este long
altfel
tipul echilibrat este unsigned int
altfel
tipul echilibrat este tipul unuia dintre cei doi operanzi,
care apare ultimul în lista:
int
unsigned int
long
unsigned long
float
double
long double
Tipul de date al valorii rezultate în urma evaluării unei expresii simple este stabilit în urma
operaţiei de echilibrare.
Aşa cum se observă, regula presupune existanţa a doi operanzi. În cazul operatorilor
unari, este evident faptul că nu este necesară operaţia deechilibrare. În cazul operatorului
condiţional, care este singurul operator ternar al limbajului C, ultimii doi operanzi sunt cei
care contează la determinarea tipului valorii rezultate.
6.2 Operatori aritmetici
Operatorii aritmetici se împart în mod uzual în două categorii: operatori aditivi, specifici
operaţiilor de adunare şi scădere, precum şi operatori multiplicativi, specifici operaţiilor de
înmulţire şi împărţire. În plus, există operatori de incrementare şi decrementare, care
reprezintă un caz particular al operatorilor aditivi.
Operatorii aditivi sunt reprezentaţi de caracterele + şi – şi în mod uzual sunt operatori
binari. Aceleaşi caractere pot însă fi utilizate şi pentru specificarea unor operatori unari:
operatorul de schimbare de semn (de exemplu: -4, -(a+b)),
operatorul unar plus, care nu are însă nici un efect (de exemplu +4)
Aceşti operatori unari au o poziţie prefixă.
Operatorii multiplicativi sunt întotdeauna binari:
operatorul * reprezintă operaţia de înmulţire, fiind utilizat atât pentru numerele
întregi, cât şi pentru cele reale;
operatorul / reprezintă operaţia de împărţire, dar are o semnificţie distinctă pentru
numerele întregi:
în cazul în care cel puţin un operand este real, operatorul reprezintă operaţia de
împărţire reală;
în cazul în care ambii operanzi sunt întregi, operatorul reprezintă operaţia de
determinare a câtului împărţirii celor doi operanzi; De exemplu: - Eval(2.0/4.0) = 0.5
- Eval(2/4.0) = 0.5
- Eval(2/4) = 0
operatorul % este utilizat doar pentru operatori întregi şi reprezintă operaţia de
determinare a restului împărţirii celor două valori; de exemplu: Eval(2%4) = 2.
Operatorii de incrementare (++) şi decrementare (--) sunt operatori unari care pot fi folosiţi
în cazul în care se doreşte ca valoarea unei variabile să crească cu 1 (incrementare) sau să
scadă cu 1 (decrementare). De exemplu, următoarele instrucţiuni sunt echivalente: k = k + 1; k++;
k = k - 1; k--;
Din punct de vedere al priorităţii operatorilor aritmetici, aceştia se împart în grupe de
prioritate, a căror ordine descrescătoare este:
operatori de incrementare şi decrementare
operatori aditivi unari
operatori multiplicativi
operatori aditivi binari
Utilizarea operatorilor de incrementare şi decrementare necesită anumite precauţii, deoarece
poziţia lor poate fi atât infixă, cât şi postfixă. Privit doar ca un efect lateral, nu este nici o
deosebire între utilizarea postfixă şi cea prefixă, deoarece în ambele cazuri modificarea valorii
operandului este aceeaşi. Probleme pot apare doar în cazul în care expresia ce conţine astfel
de operatori este utilizată ca operand în cadrul altei expresii.
De exemplu, instrucţiunea: n = k++;
este echivalentă cu secvenţa: n = k;
k = k + 1;
pe când instrucţiunea: n = ++k;
este echivalentă cu secvenţa: k = k + 1;
n = k;
Se observă faptul că în cele două cazuri, valoarea variabilei n este diferită după evaluarea
expresiei respective.
În general, la evaluarea unei expresii ce conţine un operand care este o subexpresie care
conţine la rândul ei un operator de incrementare/decrementare se utilizează următoarea regulă:
în cazul unui operator prefix de incrementare/decrementare, întâi se modifică valoarea
operandului aferent operatorului şi apoi se evaluează expresia;
în cazul unui operator postfix, se evaluaează întâi expresia cu valoarea veche a
operandului aferent operatorului de incrementare/decrementare şi apoi se modifică
valoarea operandului.
Un caz uzual de utilizare al operatorilor de incrementare/decrementare se referă la operaţiile
cu tablouri, când se doreşte modificarea indicilor. De exemplu, instrucţiunea următoare
iniţializează un tablou cu valorile: xk = k, k = 0, 1, …, n-1: for (k=0; k<n; x[k++]=k);
6.3 Operatori de relaţie
Operatorii de relaţie corespund operaţiilor de comparare din matematică şi presupun
compararea valorilor a două expresii:
< mai mic <= mai mic sau egal > mai mare
>= mai mare sau egal == egal != diferit
Aceşti operatori sunt binari şi presupun operaţia de echilibrare a valorilor operanzilor. Din
punct de vedere al priorităţii, ei se împart în două clase de precedenţă:
<, <=, >, >=
==, !=
Din punct de vedere matematic, rezultatul unei operaţii de comparare este o valoare logică.
Conform regulilor de interpretare a valorilor logice în limbajul C, rezultă că rezultatul
evaluării unei expresii de relaţie poatr fi:
- constanta zero, în cazul în care valorile operanzilor nu respectă relaţia aferentă
operatorului;
- o constantă întregă diferită de zero (în mod uzual canstanta 1 pentru multe
compilatoare) în caz contrar.
Observaţie. Nu trebuie confundat operatorul de comparare (==) cu cel de atribuire (=). În
cazul utilizării din greşeală a operatorului de atribuire în locul celui de comparare, efectul
lateral al operaţiei de atribuire este greu de detectat în anumite situaţii.
Exemplul 6.1. Secvenţa următoare testează dacă o valoare dintr-un şir de numere este egală
cu o anumită valoare dată. int k, n, y = 7, v[4];
/* ... */
for (k=0; k<n; k++)
if (v[k] == y)
printf(“Valoare gasita in sir pe pozitia %d\n”, k);
Dacă în interiorul instrucţiunii if s-ar fi scris: if (v[k] = y)
printf(“Valoare gasita in sir pe pozitia %d\n”, k);
expresia condiţională ar fi fost tot timpul adevărată, deoarece prin atribuirea v[k]=y,
valoarea lui v[k] ar fi fost alterată la valoarea 7.
O altă problemă în utilizarea expresiilor condiţionale o poate constitui apariţia operatorilor de
incrementare/decrementare, datorită posibiltăţii poziţiei prefixe sau postfixe a acestora. În
acest caz rezultatul evaluării unei expresii condiţionale depinde de poziţia operatorilor.
Exemplul 6.2. Următoarea secvenţă calculează suma: S = 1 + 1/2 + … 1/(n-1) int n = 4;
float s = 0;
while (--n > 0)
s = s +1.0/n;
La începutul fiecărei iteraţii se decrementează valoarea lui n şi apoi se evaluează expresia
condiţională, astfel încât la a 4-a iteraţie n va avea valoarea 1. După decrementare n va avea
valoarea zero şi execuţia instrucţiunii while se termină.
Dacă se inversează poziţia operatorului de decrementare: while (n-- > 0)
s = s +1.0/n;
execuţia programului în acest caz va genera o eroare (împărţire la zero). În acest caz, la
începutul fiecărei iteraţii se evaluează întâi expresia condiţională şi apoi se decremantează
valoarea lui n. Astfel, la iteraţia a 4-a n va avea valoarea 1 şi deci expresia condiţională este
adevărată, dar înainte de execuţia instrucţiunii de atribuire interne instrucţiunii while,
valoarea lui n se va decrementa, instrucţiunea fiind de fapt: s = s +1.0/0;
6.4 Operatori logici
Rolul operatorilor logici este acela de a putea reprezenta expresii condiţionale
corespunzătoare propoziţiilor compuse din logica matematică. Din acest motiv, operatorii
logici corespund principalelor operaţii logice:
! negaţie logică && conjuncţie logică (şi logic) || disjuncţie logică (sau logic)
De exemplu, pentru a specifica faptul că un număr real se află într-un interval:
x [a, b]
se poate scrie propoziţia logică:
(x a) (x b)
Transcrierea acesteia în limbajul C este: (x >= a) && (x <= b)
Observaţie. Prezenţa parantezelor rotunde nu este necesară, deoarece după cum se poate
observa din tabelul tuturor operatorilor, operatorii de relaţie au o precedenţă mai mare decât
operatorii logici şi subexpresiile x >= a , x >= a se evaluează primele.
Rezultatul evaluării unei expresii logice se determină în conformitate cu tabelel de adevăr ale
celor trei operaţii. În figura 6.1, a şi b reprezintă doi operanzi întregi, iar x o valoare întreagă
diferită de zero:
Observaţie. Datorită regulii de interpretare a constantelor logice şi a semanticii opeatorilor
logici şi relaţionali, anumite expresii condiţionale pot avea forme diferite de reprezentare. De
exemplu, următoarele două perechi de expresii sunt echivalente: Eval(x == 0) = Eval(!x)
Eval(x != 0) = Eval(x)
Ordinea de evaluare a expresiilor logice este de la stânga la dreapta. Datorită acestui fapt şi în
concordanţă cu semantica operatorilor logici, în anumite situaţii o expresie logică nu este
evaluată până la sfârşit.
a b
0
0
x
x
0 0
0 x
a && b
a b
0
0
x
x
0 x
x x
a || b
0
x
x
0
a !a
!a
Figura 6.1. Operatori logici
Pentru reducerea timpului de execuţie al programelor, compilatoarele limbajului C
întrerup evaluarea unei expresii logice atunci când rezultatul devine sigur. Următoarele două
cazuri sunt uzuale:
a) Într-o expresie de forma t1 || t2 || … || tn, se întrerupe evaluarea
expresiei la apariţia primului operand tk diferit de zero;
b) Într-o expresie de forma t1 && t2 && … && tn, se întrerupe evaluarea
expresiei la apariţia primului operand tk care are o valoare egală cu zero;
Observaţie. Regula de întrerupere a evaluării expresiilor logice în condiţiile prezentate
anterior poate conduce la neevaluarea anumitor operanzi. Acest fapt poate avea consecinţe
nedorite în program, dacă operanzii au asociate efecte laterale (în cazul neevaluării lor,
efectele laterale nu mai apar).
Exemplul 6.3. Se consideră un şir de numere reale x1, x2, …, xn. Să se determine suma
pătratelor numerelor pozitive, dar şi a celor negative al căror pătrat nu depăşeşte o valoare
dată y. #include <stdio.h>
int main() {
int k, n;
double y, d, x[10], s = 0;
scanf(“%d%lf”, &n, &y);
for (k=0; k<n; k++) {
scanf(“%lf”, &x[k]);
if ((d=x[k]*x[k])<y || x[k] > 0)
s = s + d;
}
printf(“\ns = %lf”, s);
return 0;
}
Dacă în programul precedent s-ar inversa poziţia operanzilor din expresia condiţională: if (x[k] > 0 || (d=x[k]*x[k])<y)
programul ar conţine o eroare: în cazul în care x[k]>0, a doua subexpresie nu mai este
evaluată şi valoarea variabilei d nu este calculată corect.
Din acest motiv, este indicat ca expresiile cu efecte laterale să fie plasate în faţă, sau
atribuirile ce constituie efecte laterale să fie scoase în afara (dar înaintea) expresiilor
condiţionale.
6.5 Operatori de atribuire
Operaţia de atribuire are o semnificaţie asemănătoare atribuirii din celelalte limbaje de
rpogramare. Operatorul de atribuire (=) este binar, iar sintaxa atribuirii este:
variabilă = expresie
Din punct de vedere pragmatic, atribuirea presupune două acţiuni distincte, o operaţie de
evaluare şi una de scriere în memorie şi constă în scrierea valorii rezultate în urma evaluării
expresiei din dreapta operatorului de atribuire în zona de mamorie asociată variabilei din
partea stângă. Din acest motiv, în sintaxa anterioară, variabilă nu înseamnă neapărat
numele unei variabile simple, ci o construcţie sintactică ce are asociată o zonă de memorie.
Există două noţiuni importante legate de lucrul cu variabile şi valori: noţiunea de L-
valoare şi cea de R-valoare. Valoarea curentă a unei variabile (conţinutul zonei de memorie
ataşată) reprezintă R-valoarea variabilei (right value, valoarea din partea dreaptă a
operatorului de atribuire), iar adresa zonei de memorie ataşată variabilei este o L-valoare (left
value, valoarea din partea stângă a operatorului de atribuire).
Deoarece în dreapta operatorului de atribuire poate fi o expresie, care evaluată produce
o valoare, prin extensie se numeşte R-valoare orice valoare specifică unui tip de date
fundamental sau derivat, cu excepţia funcţiilor şi tablourilor. În mod asemănător, deoarece în
partea stângă a operatorului de atribuire poate fi o construcţie sintactică ce generează o adresă
de memorie, se numeşte L-valoare adresa de memorie a unei valori de orice tip. În plus, există
noţiunea de F-valoare, care este o referinţă către o funcţie.
Exemple. int n = 10, v[7];
float x, *px;
struct { int k; float r; } a;
int f(int);
int (*pf)(float) = f1;
/* pf este un pointer la o functie cu un parametru
de tip float care returneaza un intreg
*/
void f2(float);
v[3] = n;
/* n este R-valoare, v[3] este L-valoare */
x = 1;
/* 1 este R-valoare, x este L-valoare */
px = &x;
/* &x este R-valoare, px este L-valoare */
a.k = v[3];
/* v[3] este R-valoare, valoarea componantei a 4-a
a tabloului v, a.k este L-valoare, adresa
de memorie a componentei k din variabila a
*/
n = (*pf)(v[3]);
/* v[3] este R-valoare, n este L-valoare,
*pf este F-valoare, o referinta spre functia f1,
iar (*pf)(v[3]) este R-valoare
*/
f2(*px);
/* *px este o R-valoare, continutul zonei de
memorie indicata de px, iar f2 este F-valoare
*/
Rezulă că în cazul general, o atribuire se poate reprezenta astfel:
L-valoare = R-valoare
O problemă poate apare în cadrul operaţiei de atribuire dacă operandul din stânga şi din
dreapta atibuirii nu au acelaşi tip de date. În cazul în care tipurile sunt incompatibile se
generează un mesaj de eroare, iar în cazul în care ele sunt compatibile, dar diferite, are loc o
conversie de tip. Mai exact, valoarea expresiei din dreapta este convertită la tipul variabilei
din stânga. În general, rezultatul unei asemenea conversii este dependent de implementare şi
poate conduce la depăşiri.
Exemple. float s;
s = 2;
/* se converteste intregul 2 la valoarea
reala 2.0; corect
*/
int n;
n = 12.73;
/* se converteste valoarea reala 12.73 la valoarea
intreaga 12 prin trunchiere; corect
*/
n = 124E200;
/* depasire, deoarece partea intreaga a numarului
124*10200 nu se poate reprezenta ca o valoare
intreaga; gresit
*/
După cum s-a precizat anterior, o particularitate a limbajului C este aceea că tratează
atribuirea ca un operator uzual ce poate interveni în cadrul expresiilor. Aceasta permite
modificarea valorii unei variabile în timpul evaluării unei expresii, ceea ce reprezintă un efect
lateral al operaţiei de evaluare (al cărui scop principal este să producă o valoare după
evaluare).
Exemplul 6.4. Se va relua problema referitoare la determinarea celui mai mare divizor
comun. În programul anterior, din exemplul 3.5, expresia folosită pentru determinarea restului
era evaluată de două ori: o dată în expresia condiţională a instrucţiunii while, iar a doua oară
în corpul acesteia.
while(m%n != 0) {
r = m%n;
m = n;
n = r;
}
În continuare se va evalua expreia o singură dată.
#include <stdio.h>
int cmmdc(int m, int n) {
int r;
while (r = m % n) {
m = n;
n = r;
}
return n;
}
int main() {
int m, n;
printf(“\nm, n: “);
scanf(“%d%d”, &m, &n);
printf(“\ncmmdc = %d”, cmmdc(m, n));
return 0;
}
Datorită asocierii de la dreapta la stânga a operatorului de atribuire (se evaluează întâi
operandul din dreapta şi apoi ce din stânga), limbajul C permite atribuirea multiplă. De
exemplu, în secvenţa următoare: int a, b, c;
c = b = a = 5;
se atribuie întâi variabilei a valoarea 5, apoi lui b şi apoi lui c. Instrucţiunea anterioară este
tratată de către compilator ca având forma următoare: c = (b = (a = 5));
În afara operatorului simplu de atribuire, limbajul C mai conţine o categorie de operatori de
atribuire compuşi. Rolul lor este acela de a descrie mai compact operaţii de atribuire de
forma:
variabilă = variabilă operator expresie
fără scrierea de două ori a L-valorii (în acest caz operator este un operator binar).
Pentru expresiile de forma anterioară, scrierea compactă este următoarea:
variabilă = operator_compus expresie
unde operator_compus se obţine prin alăturarea operatorului binar la cel de atribuire.
Operatorii binari ce se asociază operatorului de atribuire sunt în mod uzual cei aritmetici şi la
nivel de bit. Principalii operatori compuşi de atribuire sunt: +=
-=
*=
/=
%=
&=
^=
|=
<<=
>>=
De exemplu, instrucţiunea: x = x + y;
se poate scrie condensat folosind un operator compus de atribuire astfel: x += y;
Avantajul utilizării operatorilor compuşi de atribiure apare în situaţiile în care L-valoarea din
cadrul atribuirii nu este o variabilă simplă, ci o expresia mai complexă, conţinând la rândul ei
mai mulţi operatori şi operanzi. De exemplu, instrucţiunea: grupa[i].situatie.nr_restante =
grupa[i].situatie.nr_restante + 1;
se poate scrie mai simplu astfel: grupa[i].situatie.nr_restante += 1;
Observaţie. În cazul în care expresia din dreapta operatorului compus de atribuire conţine la
rândul ei operatori, aceasta este tratată de către compilator ca o subexpresie inclusă între
paranteze. De exemplu, instrucţiunea: x *= y + 1;
este echivalentă cu:
x = x * (y + 1);
şi nu cu instrucţiunea: x = x * y + 1;
Exemplul 6.5. Secvenţa următoare calculează suma şi produsul elementelor unui şir de
numere: int k, n = 4;
float v[4] = {1, 2, 3, 4}, s = 0, p = 1;
for (k=0; k<n; k++) {
s += v[k];
p *= v[k];
}
6.6 Operatori de adresare
Operatorii de adresare sunt folosiţi în câteva cazuri tipice:
pentru a avea acces la o componentă dintr-un element compus (operatorii de indexare
şi de selecţie);
pentru a determina adresa unui element al unei anumite entităţi (operatori de
determinare a adresei);
pentru a avea acces la o anumită entitate accesibilă doar prin adresa sa de memorie
(operatori de adresare indirectă).
Rezultatul evaluării unei operaţii de adresare este interpretat în funcţie de contextul în care
apare, fie ca o L-valoare (dacă apare în stânga unei atribuiri) indicând adresa de memorie a
unui obiect, fie ca o R-valoare, indicând valoarea curentă a elementului respectiv.
Operatorii de adresare sunt următorii:
a) Operatorul de indexare, reprezentat de caracterele “[]”, este un operator binar, utilizat în
expresii de forma:
tablou [ expresie indice ]
Se observă poziţia specială a operatorului faţă de operanzi: operatorul, format din două
caractere (ca şi operatorul de apel de funcţie), încadrează operandul al doilea.
Primul operand reprezintă un tablou, iar al doilea o expresie întreagă specificând
indicele elementului selectat. Expresia ce conţine operatorul de selecţie returnează
valoarea elementului specificat.
b) Operatorul de selecţie directă, este reprezentat de caracterul “.” şi este utilizat pentru a
selecta o componentă dintr-o structură sau uniune. Este un operator cu o poziţie infixă,
separând structura respectivă de numele componentei:
structură . selector
Despre acest operator se va discuta în cadrul capitolului rezervat structurilor şi uniunilor.
c) Operatorul de selecţie indirectă, este reprezentat de caracterele “->” şi este utilizat
pentru a selecta o componentă dintr-o structură sau uniune prin intermediul unui pointer.
Expresia de utilizare este:
pointer -> selector
Despre acest operator se va discuta în cadrul capitolului rezervat structurilor şi uniunilor.
d) Operatorul de determinare a adresei este reprezentat de caracterul “&” şi utilizat în
expresii de forma:
& entitate
Operatorul returnează adresa de memorie a entităţii respective.
Un exemplu frecvent de utilizare este în cazul apelului funcţiei scanf, pentru citirea
valorilor unor variabile de intrare. Operatorul este folosit pentru determinarea adresei de
meorie a acestor variabile. De exemplu: int n;
scanf(“%d”, &n);
Despre alte cazuri de utilizare a acestui operator se va discuta în cadrul capitolului
rezervat pointerilor şi tablourilor.
e) Operatorul de adresare indirectă este reprezentat de caracterul “*” şi utilizat în expresii
de forma:
* pointer
unde este o entitate ce memorează adresa unui element din program. Rezultatul evaluării
unei asemenea expresii este valoarea elementului a cărui adresă este memorată de către
pointer.
Exemple: int a = 3, b, *p;
p = &a; /* pointerul p memoreaza adresa variabilei a */
b = *p; /* b primeste valoarea elementului a carui
adresa este memorata de p, adica 3
*/
Şi despre acest operator se va discuta în cadrul capitolului rezervat structurilor şi
uniunilor.
6.7 Operatori la nivel de bit
Operatorii la nivel de bit reprezintă operaţii specifice limbajelor de asamblare. Spre deosebire
de ceilalţi operatori ai limbajului C, această categorie prelucrează fiecare cifră binară din
cadrul operanzilor (care în acest caz sunt numai de tip întreg).
Operatorii la nivel de bit corespund în general operaţiilor principale din logica booleană
(operatorul de negaţie este singurul operator unar, ceilalţi sunt binari):
operatorul ~ reprezintă operaţia de negaţie;
operatorul & reprezintă operaţia booleană “şi”;
operatorul ^ reprezintă operaţia booleană “sau exclusiv”;
operatorul | reprezintă operaţia booleană “sau”.
În plus, există doi operatori binari de deplasare:
operatorul << pentru operaţia de deplasare la stânga;
operatorul >> pentru operaţia de deplasare la dreapta.
Operatorii ce corespund operaţiilor booleene operează asupra cifrelor binare conform
următoarelor tabele de adevăr din figura 6.2 (a şi b reprezintă cifre binare):
Pentru exemplificare, în tabelul următor se prezintă câteva cazuri de aplicare a acestor
operatori asupra unor operanzi de tip char:
a 0 0 0 1 0 1 1 0
b 0 1 1 1 1 0 1 1
~a 1 1 1 0 1 0 0 1
a & b 0 0 0 1 0 0 1 0
a | b 0 1 1 1 1 1 1 1
a ^ b 0 1 1 0 1 1 0 1
În cazul operatorilor de deplasare, cei doi operanzi au semnificaţii diferite: primul reprezintă
numărul ai cărui biţi se deplasează, iar al doilea specifică numărul de poziţii cu care se face
deplasarea în direcţia respectivă. De exemplu, expresia n << 2 specifică faptul că cifrele
binare ale numărului n se vor deplasa cu două poziţii spre stânga.
La deplasarea spre stânga cu k cifre, primii k biţi ai numărului se pierd, iar ultimii k se
completează cu zero. La deplasarea spre dreapta cu k cifre, ultimii k biţi ai numărului se
pierd, iar primii k se completează cu cifra binară 0 sau 1, cifră ce depinde de implementare
(valoarea nu se specifică in standardul limbajului).
Dacă valoarea expresiei de deplasare este negativă, sau depășește lungimea de
memorie a operandului din stânga, semnificația operației este nedefinită.
Exemplu pentru un operand de tip char:
a 0 0 1 1 1 1 0 1
a << 2 1 1 1 1 0 1 0 0
a >> 2 0 0 0 0 1 1 1 1
În exemplul precedent s-a considerat că la deplasarea spre dreapta se introduc zerouri. În
mod uzual, valoarea cifrei cu care se completează primii k biţi este aleasă astfel încât
operaţia să fie echivalentă cu o împărţire cu 2k (pentru numere pozitive cifra este 0, iar pentru
numere negative este 1). Această alegere este sugerată de următoarea observaţie: în cazul
deplasării spre stânga cu k cifre, în cazul în care nu se produce o depăşire, rezultatul obţinut
este echivalent cu o înmulţire a numărului cu 2k.
Pentru exemplificare, se poate compara numărul: n = 00000101
cu numărul n1 = n << 2:
n1 = 00010100 (înmulțire cu 22).
În mod similar, n2 = n1 >> 2:
n2 = 00000101 (împărțire cu 22)
a b
0
0
1
1
0 0
0 1
a & b
a b
0
0
1
1
0 1
1 1
a | b
a b
0
0
1
1
0 1
1 0
a ^ b
0
1
1
0
a ~a
~a
Figura 6.2. Operatori la nivel de bit
Exemplul 6.6. Un exemplu elocvent de utilizare a operatorilor la nivel de bit îl constituie
cazul programelor care lucrează cu informaţii codificate pe porţiuni. Să presupunem că un
program trebuie să conducă un robot. Pentru simplitate, se consideră doar următoarele
comenzi:
- deplasare înainte,
- deplasare înapoi,
- deplasare la stânga,
- deplasare la dreapta,
- apucare a unui obiect (închidere mână),
- lăsare a unui obiect (deschidere mână).
Pentru primele patru comenzi, trebuie specificat în plus şi distanţa pe care trebuie să se
deplaseze robotul (considerată în milimetri).
Pentru codificarea a celor 6 comenzi diferite sunt necesare 3 cifre binare. În cazul în
care o comandă conţine 8 cifre binare (un operand de tip char), ultimele 5 cifre vor codifica
distanţa. Un exemplu de codificare al tipului comenzilor:
- 000: deplasare înainte,
- 001: deplasare inapoi,
- 010: deplasare spre stânga,
- 011: deplasare spre dreapta,
- 100: apucare obiect,
- 101: lăsare obiect.
Să presupunem de exemplu că există o comandă de deplasare spre stânga cu 6 mm de forma: | 0 1 0 | 0 0 1 1 0 |
Programul trebuie să detecteze tipul comenzii, iar în cazul în care comanda este de deplasare,
să determine distanţa de deplasare.
Funcţia care decodifică o comandă şi apelează funcţiile specifice ce realizează
operaţiile respective trebuie să izoleze cele două câmpuri. Ea returnează un rezultat întreg,
specificând dacă s-a putut sau nu decodifica şi executa respectiva comandă:
int DecodificareComanda(unsigned char comanda) {
unsigned char masca_tip _operatie = 0x7;
/* masca_tip_operatie = 00000111 */
unsigned char masca_distanta = 0x1f;
/* masca_distanta = 00011111 */
unsigned char tip_operatie, distanta;
masca_tip_operatie <<= 5;
/* masca_tip_operatie devine 11100000 */
tip_operatie = (comanda & masca_tip_operatie) >> 5;
distanta = comanda & masca_distanta;
if (tip_operatie == 0)
DeplasareInainte(distanta);
else if (tip_operatie == 1)
DeplasareInapoi(distanta);
else if (tip_operatie == 2)
DeplasareStanga(distanta);
else if (tip_operatie == 3)
DeplasareDreapta(distanta);
else if (tip_operatie == 4)
ApucareObiect();
else if (tip_operatie == 5)
LasareObiect();
else
Return 0;
return 1;
}
O mască reprezintă un grup de cifre binare 1 consecutive şi se utilizează pentru a putea reţine
din mai multe cifre binare un grup de cifre dorit. Selecţia grupului de cifre se realizează cu
ajutorul operaţiei “şi” la nivel de bit între numărul dorit şi şablon.
În exemplu, s-au utilizat două măşti: prima reţine primii 3 biţi (masca_tip_operatie),
iar a doua ultimii 5 biţi (masca_distanta). După efectuarea operaţiei “şi” la nivel de bit, se
obţin utmătoarele structuri de cifre binare:
Comanda b7 b6 b5 b4 b3 b2 b1 b0
Masca_tip_operatie 1 1 1 0 0 0 0 0
Masca_distanta 0 0 0 1 1 1 1 1
Tip_operatie b7 b6 b5 0 0 0 0 0
Distanta 0 0 0 b4 b3 b2 b1 b0
Tip_operatie >> 5 0 0 0 0 0 b7 b6 b5
Valoarea corectă pentru masca_tip_operatie se obţine după o deplasare spre stânga cu 5 cifre
binare. După selectarea biţilor b7, b6 şi b5 din cadrul octetului aferent unei comenzi, aceştia
trebuie deplasaţi spre dreapta cu 5 cifre binare. În acest mod, octetul corespunzător variabilei
tip_operatie poate fi utilizat simplu prin valoarea sa în cadrul expresiilor condiţionale din
instrucţiunile if (de exemplu: 00000101 reprezintă valoarea 5, 00000100 reprezintă
valoarea 4, etc.).
6.8 Operatori specifici ai limbajului C
Limbajul C conţine o serie de operatori specifici, care pot mări flexibilitatea şi compactitatea
programelor.
A) Operatorul sizeof
Acesta este un operator special al limbajului, fiind singurul operator care nu evaluează
valoarea operandului asociat. Operatorul sizeof nu se aplică asupra unei valori, ci asupra
unui tip de date, forma sa de utilizare fiind:
sizeof ( tip )
sau
sizeof expresie_unară
În primul caz operatorul se aplică direct asupra tipului de date specificat, iar în al doilea se
aplică asupra tipului de date al expresiei respective.
Operatorul returnează o valoare întreagă, reprezentând numărul de octeţi necesari
pentru memorarea valorilor tipului asociat.
Observaţie. Expresia unară este o expresie care nu conţine operatori binari sau ternari. În
cazul unei expresii complexe, operatorul sizeof se va aplica numai asupra primului
operand. De exemplu, considerându-se secvenţa: int n1, n2, n3, a = 5;
double b = 2.2;
n1 = sizeof a + b;
n2 = sizeof (a + b);
n3 = sizeof(double);
se observă faptul că valorile lui n2 şi n3 sunt egale. Presupunând faptul că valorile de tip int
se memorează pe 4 octeţi şi cele de tip double pe 8 octeţi, valorile lui n2 şi n3 sunt ambele
egale cu 8 (în cazul lui n2, nu se evaluaează expresia a + b, ci se determină tipul de date al
acesteia), iar valoarea lui n1 este 7 (conversia valorii 7.2 la valoare întregă). Se remarcă
următoarele echivalenţe: n1 = Eval(sizeof a + b) =
= Eval(sizeof a) + Eval(b)
În cazul în care operandul lui sizeof este numele unui tablou, acesta nu este convertit la un
pointer. Valoarea returnată în acest caz reprezintă numărul total de octeţi alocat tabloului
respectiv. De exemplu, în secvenţa: int n1, n2, a[10], *p;
n1 = sizeof (a);
n2 = sizeof (p);
presupunând faptul că valorile de tip int se reprezintă pe 2 octeţi, valoarea lui n1 este 20, pe
când valoarea lui n2 (p are tipul int*) este în general 4 (adică numărul de octeţi necesari
pentru memorarea adresei unui obiect de tip int).
B) Operatorul de conversie de tip
Acest operator, numit şi operator cast, determină schimbarea forţată a tipului de date a
valorii unei expresii sau subexpresii la un alt tip de date specificat explicit de programator.
Operatorul cast are o poziţie prefixă, forma de utilizare fiind următoarea:
( tip ) expresie_unară
Se evaluează expresia unară, iar rezultatul este convertit la tipul specificat între paranteze.
Observaţie. Ca şi în cazul operatorului sizeof, trebuie avută grijă în cazul în care expresia
în care apare operatorul cast este mai complexă (operatorul se aplică doar asupra
operandului următor). De exemplu, în cazul în care variabilele a şi b sunt de tip întreg: int a = 5, b = 7;
în cadrul expresiei: (double) a / b;
operatorul cast se aplică doar asupra variabilei a schimbându-i valoarea întragă 5 în constanta
de tip double 5.0, astfel încât operatorul / joacă rol de împăţire reală.
O folosire uzuală a operatorului cast este în cazul apelului funcţiilor, fie când se doreşte
conversia tipului de date a valorii returnate de o funcţie, fie când se doreşte conversia tipului
de date al parametrilor de apel.
Exemple.
1) O funcţie des utilizată pentru alocarea memoriei în zona heap a programului este
malloc, care are prototipul: void* malloc(size_t);
Dacă se doreşte să se aloce memorie pentru o valoare de tip float, se poate utiliza un
pointer, ca în secvenţa următoare: float *p;
p = (float*)malloc(sizeof(float));
Se converteşte astfel tipul void* la tipul float* pentru a exista compatibilitate cu tipul
variabilei p.
2) O funcţie matematică corespunzătoare operaţiei de ridicare la putere este funcţia pow, care
are prototipul: double pow(double, double);
În cazul în care a şi b sunt două variabile întregi, apelul acestei funcţii se poate scrie: x = pow((double)a, (double)b);
Observaţie. Operatorul nu afecatează valoarea variabilelor din cadrul expresiei, astfel încât a
şi b îşi păstrează valorile iniţiale.
C) Operatorul condiţional (?:)
Acesta este singurul operator ternar al limbajului C şi are următoarea formă de utilizare:
expr.1 ? expr.2 : expr.3
Denumirea sa provine de la faptul că valoarea de evaluare a unei asemenea expresii depinde
de tipul primei expresii:
0)1.exp(),3.exp(
0)1.exp(),2.exp()3.exp:2.exp?1.exp(
rEvaldacarEval
rEvaldacarEvalrrrEval
Avantajul utilizării unui asemenea operator constă în faptul că permite scrierea unui cod
compact şi evitarea utilizării intrucţiunilor if.
Exemplul 6.7. Presupunând faptul că variabila c este de tip caracter şi memorează în timpul
execuţiei programului o cifră a bazei de numeraţie 16:
c {‘0’, ‘1’, ..., ‘9’} {‘a’, ‘b’, ..., ‘f’}
instrucţiunea următoare determină valoarea zecimală echivalentă a acesteia: if (c >= ’0’ && c <= ’9’)
v = c – ‘0’;
else
v = c – ‘a’ + 10;
Scris condensat, această instrucţiune este echivalentă cu: v = (c >= ’0’ && c <= ’9’) ? c – ‘0’ : c – ‘a’ + 10;
Observaţie. S-a utilizat poziţia relativă a caracterelor alfabetice şi numerice din tabela de
caractere.
Operatorii condiţionali pot fi imbricaţi. De exemplu, pentru a determina valoarea maximă a
trei numere, x, z şi z, se poate scrie funcţia următoare: double max3(double x, double y, double z) {
return (x > y) ? (x > z ? x : z) : (y > z ? y : z) ;
}
D) Operatorul de secvenţiere (,)
Acesta este un operator de evaluare secvenţială a unor expresii. Forma uzuală de folosire este
următoarea:
expr.1 , expr.2 , ... , expr.k
unde cel puțin o expresie este o expresie de atribuire. Ea se utilizează atunci când sintaxa
limbajului presupune prezenţa unei singure expresii, dar algoritmul problemei de rezolvat
necesită prelucrarea secvenţială a mai multor expresii.
Operatorul de secvențiere nu poate fi utilizat pentru a separa elementele dintr-o listă.
În mod uzual, el se folosește în cadrul unei expresii condiționale, sau într-o expresie ce
conține paranteze.
Evaluarea unei asemenea expresii se face de la stânga la dreapta, valoarea de evaluare fiind de
fapt valoarea ultimei expresii:
Eval(expr.1, expr.2 , ... , expr.k) = Eval(expr.k)
Celelalte expresii sunt folosite de obicei pentru efectele laterale pe care le produc.
De exemplu, presupunând că variabilele x, y, z şi t sunt de tip numeric, atunci instrucţiunea: z = (x > y) ? x : y;
atribuie lui z valoarea maximă, pe când instrucţiunea: z = (x > y) ? (t = x, x = y, y = t) : y;
are acelaşi efect, dar în plus realizează şi ordonarea crescătoare a valorilor x şi y. O utilizare
eronată este următoarea: z = (x > y) ? (t = y, y = x, x = t) : y;
O altă utilizare uzuală a operatorului de secvenţiere este în cadrul instrucțiunii for.
Exemplul 6.8. Funcţia următoare afişează poziţiile succesive ale unui cal pe tabla de şah,
plecând din poziţia (a, 1) şi utilizând tot timpul aceeaşi mişcare:
void PozitieCal(int jos, int dreapta) {
int i, j;
for (i=j=1; i<=8 && j<=8; i+ = jos, j+= dreapta)
printf(“%c %d\n”, ‘a’-i, j);
}
Funcţia converteşte valorile numerice 1, 2, …, 8 la caracterele ‘a’, ‘b’, …, ‘h’. Parametrii jos
şi dreapta pot avea doar valorile (1, 2) sau (2, 1). Exemplu de utilizare:
int main() {
PozitieCal(1, 2);
PozitieCal(2, 1);
return 0;
}
6.9 Probleme
6.1. Să se scrie un program pentru rezolvarea ecuaţiaei bipătrate: a*x4 + b*x
2 + c = 0.
6.2. Să se scrie un program pentru rezolvarea problemei 2.3.
6.3. Să se scrie un program pentru rezolvarea problemei 2.11.
6.4. Se consideră un punct P în plan specificat prin coordonatele sale şi un segment de
dreaptă specificat prin coordonatele extremităţilor. Să se scrie un program care să
determine dacă punctul P se află sau nu pe segment.
6.5. Se consideră două segmente de dreaptă specificate prin coordonatele extremităţilor. Să
se scrie un program care să determine intersecţia segmentelor.
6.6. Să se scrie un program pentru determinarea naturii unui patrulater specificat prin
coordonatele vârfurilor.
6.7. Să se scrie un program pentru calculul ariei determinate de intersecția a două patrulatere
specificate prin coordonatele vârfurilor.
6.8. Se consideră o placă din oțel, de formă poligonală, specificată prin coordonatele
vîrfurilor poligonului în ordine trigonometrică. Să se determine centrul de greutate al
plăcii.
6.9. Să se determine dacă un număr întreg n este prim.
6.10. Se consideră operaţii cu mulţimi de numere întregi din domeniul 0, 1, …, 31. Utilizând
operatorii la nivel de bit, să se scrie funcţii C pentru principalele operaţii: intersecţia,
reuniunea şi diferenţa a două mulţimi, testul de incluziune a unei mulţimi în altă
mulţime, precum şi testul de apartenenţă a unui element la o mulţime.
6.11. Utilizând funcţiile C de la problema 6, să se scrie un program pentru determinarea
tuturor numerelor prime mai mici decât un număr n dat, utilizând un algoritm numit
“Ciurul lui Erathostene”. Se utilizează două mulţimi, a şi b, cu următoarea semnificaţie:
- la început, a conţine toate numerele de la 2 la n, iat b va fi vidă;
- la sfârşitul calculului, a va fi vidă, iar b va conţine numerele dorite.