>> Inapoi <<
========
Capitolul 8
========
============================
Functii, pointeri si clase de memorare
============================
Va amintiti ca daca o expresie este transmisa ca argument pentru o functie,
atunci se creeaza o copie a valorii expresiei care se transmite. Acest mecanism
este cunoscut sub numele de apel prin valoare ("call-by-value") si se foloseste
in limbajul C. Presupunem ca avem o variabila v si o functie f(). Daca scriem
v = f(v);
atunci valoarea returnata de functia f va schimba valoarea lui v, altfel nu. In
interiorul functiei f, nu se modifica valoarea lui v. Aceasta se datoreaza
faptului ca se transmite doar o copie a lui v catre f. In alte limbaje de
programare, un apel de functie poate schimba valoarea lui v din memorie. Acest
mecanism se mai numeste apel prin referinta ("call-by-reference"). Noi vom
simula apelul prin referinta transmitand adresele variabilelor ca argumente in
apelul functiei.
----------------------------------------------
Declararea si atribuirea pointerilor
---------------------------------------------
Pointerii sunt folositi in programe pentru accesarea memoriei si manipularea
adreselor. Deja ne-am intalnit cu adresele variabilelor ca argumente ale
functiei "scanf()". De exemplu, putem avea:
scanf("%d\n", &n);
Daca v este o variabila, atunci &v este o adresa (sau locatie) din memorie.
Operatorul de adresa & este unar si are aceeasi precedenta si asociativitate de
la dreapta la stanga ca si ceilalti operatori unari. Variabilele pointer pot fi
declarate in programe si apoi folosite pentru a lua valori adrese din memorie.
-----------
Exemplu: Declaratia
-----------
int i, *p;
defineste i de tip "int" si p "pointer catre int". Domeniul legal de valori
pentru orice pointer cuprinde adresa speciala 0 si o multime de numere naturale
care sunt interpretate ca fiind adrese masina ale sistemului C. De obicei,
constanta simbolica NULL este 0 (definita in ).
-----------
Exemple:
-----------
1. p = &i; /* valoarea lui p este adresa lui i */
2. p = 0; /* valoarea lui p este adresa speciala 0 */
3. p = NULL; /* echivalent cu p = 0; */
4. p = (int *) 1307; /* o adresa absoluta din memorie */
------------------------------
Adresare si indirectare
------------------------------
Am vazut ca operatorul de adresa & se aplica unei variabile si intoarce
valoarea adresei sale din memorie. Operatorul de indirectare (sau de
dereferentiere) se aplica unui pointer si returneaza valoarea scrisa in memorie
la adresa data de pointer. Intr-un anumit sens, acesti doi operatori sunt
inversi unul altuia. Pentru a intelege mai bine aceste notiuni, sa vedem pe un
exemplu ce se intampla in memorie:
-----------
Exemplu:
-----------
Presupunem ca avem declaratiile:
int i = 777, *p = &i;
Atunci, in memorie avem:
Nume Tip Valoare Adresa
---------------------------------------
| i | int | 777 | 3A38:0FFE |
---------------------------------------
/\ |
| |
(*p == i) * | -------- (p = &i)
| | &
| \/
------------------------------------------------
| p | int * | 3A38:0FFE | 3A38:0FFA |
------------------------------------------------
Nume Tip Valoare Adresa
Mentionam ca adresa unei variabile este dependenta de sistem (C aloca memorie
acolo unde poate).
-----------
Exemplu:
-----------
float x, y, *p;
p = &x;
y = *p;
Mai intai "p" se asigneaza cu adresa lui "x". Apoi, "y" se asigneaza cu
valoarea unui obiect la care pointeaza p (adica *p). Aceste doua instructiuni
de asignare se pot scrie
y = *&x;
care este echivalent cu
y = x;
Am vazut mai sus ca un pointer se poate initializa in timpul declararii sale.
Trebuie sa avem totusi grija ca variabilele din membrul drept sa fie deja
declarate.
----------
Exemplu: Unde este greseala ? int *p = &a, a;
----------
Tabelul de mai jos ilustreaza modul de evaluare a expresiilor cu pointeri.
----------
Exemplu:
----------
Presupunem ca avem declaratiile:
int i = 3, j = 5, *p = &i, *q = &j, *r;
double x;
-------------------------------------------------------------------
| Expresie | Expresie echivalenta | Valoare |
-------------------------------------------------------------------
p == & i p == (& i) 1
p = i + 7 p = (i + 7) gresit
* * & p * (* (& p)) 3
r = & x r = (& x) gresit
7 * * p / * q + 7 (((7 * (* p))) / (* q)) + 7 11
* (r = & j) *= * p (* (r = (& j))) *= (* p) 15
------------------------------------------------------------------
Spre deosebire de C traditional, in ANSI C, singura valoare intreaga care poate
fi asignata unui pointer este 0 (sau constanta NULL). Pentru asignarea oricarei
alte valori, trebuie facuta o conversie explicita (cast).
In cele ce urmeaza, vom scrie un program care ilustreaza legatura dintre
valoarea unui pointer si adresa lui.
-----------
Exemplu:
-----------
#include
#include
void main()
{
int i = 777, *p = &i;
clrscr();
printf("Valoarea lui i: %d\n", *p);
printf("Adresa lui i: %lu sau %p\n", &i, &i);
printf("Adresa lui i: %lu sau %p\n", p, p);
printf("Valoarea lui p: %lu sau %p\n", p, p);
printf("Adresa lui p: %lu sau %p\n", &p, &p);
getch();
}
Locatia curenta a unei variabile din memorie este dependenta de sistem.
Operatorul * (din expresia *p) va afisa valoarea scrisa la adresa care este
egala cu valoarea lui p. Adresa lui i (valoarea lui p) va fi afisata ca fiind
ceva de genul
3A38:0FFE
care reprezinta un numar scris in baza 16 (in care cifrele sunt 0, 1, ..., 9,
A, B, C, D, E, F) si are valoarea
3*16^7+10*16^6+3*16^5+8*16^4+ 15*16^2+15*16+14 = 976752638
De observat ca un pointer se memoreaza intotdeauna pe patru octeti indiferent
de tipul variabilei catre care se face referirea. Explicati de ce ?
---------------------------
Pointeri catre "void"
---------------------------
In C traditional, pointerii de tipuri diferite sunt considerati compatibili ca
asignare. In ANSI C, totusi, un pointer poate fi asignat altuia doar daca au
acelasi tip, sau cand unul dintre ei este de tipul "void". De aceea, putem
gandi "void *" ca un tip pointer generic.
----------
Exemple: Presupunem ca avem declaratiile
----------
int *p;
float *q;
void *v;
----------------------------------------------------------
| Asignari legale | Asignari ilegale |
----------------------------------------------------------
| p = 0; p = 1; |
| p = (int *) 1; v = 1; |
| p = v = q; p = q; |
| p = (int *) q; |
---------------------------------------------------------
Vom discuta in capitolele ulterioare despre functiile "calloc()" si "malloc()"
, care produc alocare dinamica a memoriei pentru vectori si structuri. Ele
returneaza un pointer catre "void", de aceea putem scrie:
int *a;
a = calloc(...);
In C traditional, trebuie sa facem conversie explicita:
a = (int *) calloc(...);
-----------------------------------
Apel prin adresa (referinta)
-----------------------------------
Am vazut ca C foloseste mecanismul apelului prin valoare ("call-by-value") in
cazul apelurilor functiilor si anume se fac copii ale parametrilor actuali care
se transmit functiilor. In cele ce urmeaza, vom descrie mecanismul apelului
prin adresa si astfel se va asigura modificarea valorii variabilei transmise.
Pentru aceasta, vom utiliza pointeri.
-----------
Exemplu:
-----------
#include
void interschimba(int *, int *);
void main()
{
int a = 3, b = 7;
printf("%d %d\n", a, b);
interschimba(&a, &b);
printf("%d %d\n", a, b);
}
void interschimba(int *p, int *q)
{
int tmp;
tmp = *p;
*p = *q;
*q = tmp;
}
Efectul apelului prin adresa este realizat prin:
1. Declararea parametrului functiei ca fiind un pointer;
2. Folosirea unui pointer de indirectare in corpul functiei;
3. Transmiterea adresei unui argument cand functia este apelata.
----------------------------------------------
Reguli pentru stabilirea domeniului
----------------------------------------------
Domeniul unui identificator este partea din textul unui program unde
identificatorul este cunoscut sau accesibil. Aceasta idee depinde de notiunea
de "bloc", care este o instructiune compusa cu declaratii.
Regula de baza in stabilirea domeniului este aceea ca identificatorii sunt
accesibili numai in blocul unde sunt declarati si necunoscuti in afara
granitelor blocului. Unii programatori folosesc acelasi nume de identificatori
rezenti in anumite blocuri.
-----------
Exemplu:
-----------
{
int a = 2;
printf("%d\n", a);
{
int a = 7;
printf("%d\n", a);
}
printf("%d\n", ++a);
}
Un program echivalent ar fi:
{
int a_afara = 2;
printf("%d\n", a_afara);
{
int a_inauntru = 7;
printf("%d\n", a_inauntru);
}
printf("%d\n", ++a_afara);
}
-------------------------
Clase de memorare
-------------------------
Orice variabila si functie are doua atribute:
tipul si clasa de memorare
Exista patru clase de memorare in C, automata, externa, registru si statica si
sunt date de urmatoarele cuvinte rezervate:
auto extern register static
Cea mai cunoscuta clasa de memorare este "auto".
----------------------------------
Clasa de memorare "auto"
----------------------------------
Variabilele declarate in interiorul functiilor sunt implicit automate. De aceea
clasa "auto" este cea mai cunoscuta dintre toate. Daca o instructiune compusa
(bloc) incepe cu declararea unor variabile, atunci aceste variabile sunt in
domeniu in timpul acestei instructiuni compuse (pana la intalnirea semnului }).
-----------
Exemplu:
-----------
auto int a, b, c;
auto float f;
Declaratiile variabilelor in blocuri sunt implicit automate.
La executie, cand se intra intr-un bloc, se aloca memorie pentru variabilele
automate. Variabilele sunt considerate locale acestui bloc. Cand se iese din
acest bloc, sistemul elibereaza zona de memorie ocupata de acestea si deci
valorile acestor variabile se pierd. Daca intram din nou in acest bloc, atunci
se aloca din nou memorie pentru aceste variabile, dar vechile valori sunt
necunoscute.
-------------------------------------
Clasa de memorare "extern"
-------------------------------------
O metoda de transmitere a informatiei in blocuri si functii este folosirea
variabilelor externe. Daca o variabila este declarata inafara functiei, atunci
acesteia i se aloca permanent memorie si spunem ca ea apartine clasei de
memorare "extern". O variabila externa este considerata globala tuturor
functiilor declarate dupa ea, si chiar dupa iesirea din blocuri sau functii,
ea ramane permanent in memorie.
-----------
Exemplu:
-----------
#include
int a = 1, b = 2, c = 3;
int f(void);
void main()
{
printf("%3d\n", f());
printf("%3d%3d%3d\n", a, b, c);
}
int f(void)
{
int b, c; /* b si c sunt locale, deci b, c globale sunt
mascate */
a = b = c = 4; /* valoarea lui a se modifica */
return(a + b +c);
}
Explicatia este foarte simpla. La inceput se memoreaza cate 2 octeti pentru "a"
, "b", "c". Cand ajungem la functia "f()", memoram inca cate doi octeti pentru
"b" si "c" (notate la fel din intamplare). La intoarcerea in functia apelanta,
aceste "b" si "c" noi nu mai exista pentru ca erau locale functiei "f()". Sa
vedem mai exact ce se intampla in memorie:
Inainte de apelul functiei "f()":
Nume Tip Valoare Adresa
---------------------------------------
| a | int | 1 | 3A38:0FFE |
---------------------------------------
---------------------------------------
| b | int | 2 | 3A38:0FFC |
---------------------------------------
---------------------------------------
| c | int | 3 | 3A38:0FFA |
---------------------------------------
In timpul executiei functiei "f()" (dupa a = b = c = 4):
Nume Tip Valoare Adresa
--------------------------------------
| a | int | 4 | 3A38:0FFE |
--------------------------------------
--------------------------------------
| b | int | 2 | 3A38:0FFC |
--------------------------------------
--------------------------------------
| c | int | 3 | 3A38:0FFA |
--------------------------------------
--------------------------------------
| b | int | 4 | 3A38:0FF8 |
--------------------------------------
--------------------------------------
| c | int | 4 | 3A38:0FF6 |
--------------------------------------
La intoarcerea in functia "main()":
Nume Tip Valoare Adresa
--------------------------------------
| a | int | 4 | 3A38:0FFE |
--------------------------------------
--------------------------------------
| b | int | 2 | 3A38:0FFC |
--------------------------------------
--------------------------------------
| c | int | 3 | 3A38:0FFA |
--------------------------------------
Deci, cuvantul rezervat "extern" spune compilatorului "cauta peste tot, chiar
si in alte fisiere !". Astfel, programul precedent se poate rescrie:
in fisierul "fisier1.c":
#include
int a = 1, b = 2, c = 3; /* variabile externe */
int f(void);
void main()
{
printf("%3d\n", f());
printf("%3d%3d%3d\n", a, b, c);
}
in fisierul "fisier2.c":
int f(void)
{
extern int a; /* cauta-l peste tot */
int b, c;
a = b = c = 4; /* valoarea lui a se modifica */
return(a + b +c);
}
Deci, putem conchide ca informatiile se pot transmite prin variabile globale
(declarate cu extern) sau folosind transmiterea parametrilor. De obicei se
prefera al doilea procedeu.
Toate functiile au clasa de memorare externa. De exemplu,
extern double sin(double);
este un prototip de functie valid pentru functia "sin()", iar pentru definitia
functiei, putem scrie:
extern double sin(double x)
{
..
}
--------------------------------------
Clasa de memorare "register"
--------------------------------------
Clasa de memorare "register" spune compilatorului ca variabilele asociate
trebuie sa fie memorate in registri de memorie de viteza mare, cu conditia ca
aceasta este fizic si semantic posibil. Daca limitarile resurselor si
restrictiile semantice (cateodata) fac aceasta imposibila, clasa de memorare
register va fi inlocuita cu clasa de memorare implicita "auto". De obicei,
compilatorul are doar cativa astfel de registri disponibili. Multi sunt
folositi de sistem si deci nu pot fi alocati.
Folosirea clasei de memorare "register" este o incercare de a mari viteza de
executie a programelor. De regula, variabilele dintr-o bucla sau parametrii
functiilor se declara de tip "register".
-----------
Exemplu:
-----------
{
register int i;
for (i = 0; i < LIMIT; ++i)
{
. . . . .
}
} /* la iesirea din bloc, se va elibera registrul i */
Declaratia
register i;
este echivalenta cu
register int i;
Daca lipseste tipul variabilei declarata intr-o clasa de memorare de tip
"register", atunci tipul se considera implicit "int".
-----------------------------------
Clasa de memorare "static"
-----------------------------------
Declaratiile "static" au doua utilizari distincte si importante:
a) permite unei variabile locale sa retina vechea valoare cand se reintra in
bloc (sau functie) (caracteristica ce este in contrast cu variabilele
"auto" obisnuite);
b) folosita in declaratii externe are alta comportare (vom discuta in
sectiunea urmatoare);
Pentru a ilustra a), consideram exemplul:
-----------
Exemplu:
-----------
void f(void)
{
static int contor = 0;
++contor;
if (contor % 2 == 0)
. . . . .
else
. . . . .
}
Prima data cand functia este apelata, "contor" se initializeaza cu 0. Cand se
paraseste functia, valoarea lui "contor" se pastreaza in memorie. Cand se va
apela din nou functia "f()", "contor" nu se va mai initializa, ba mai mult, va
avea valoarea care s-a pastrat in memorie la precedentul apel. Declararea lui
"contor" ca un "static int" in functia "f()" il pastreaza privat in "f()"
(adica numai aici i se poate modifica valoarea). Daca ar fi fost declarat in
afara acestei functii, atunci si alte il puteau accesa.
-------------------------------
Variabile externe statice
-------------------------------
Ne vom referi acum la folosirea lui "static" ca declaratie externa. Aceasta
pune la dispozitie un mecanism de "izolare" foarte important pentru
modularitatea programelor. Prin "izolare" intelegem vizibilitatea sau
restrictiile de domeniu.
Deosebirea dintre variabile externe si cele externe static este ca acestea din
urma sunt variabile externe cu restrictii de domeniu. Domeniul este fisierul
sursa in care ele sunt declarate. Astfel, acestea sunt inaccesibile pentru
functiile definite anterior in fisier sau definite in alte fisiere, chiar daca
functiile folosesc clasa de memorare "extern".
-----------
Exemplu:
-----------
void f(void)
{
. . . . . /* v nu este accesibil aici */
}
static int v; /* variabila externa statica */
void g(void)
{
. . . . . /* v poate fi folosit aici */
}
Vom mai prezenta un exemplu de generare a numerelor aleatoare bazata pe metode
de congruente liniara (Knuth, D., E.: "The Art of Computer Programming", 2nd
ed., vol. 2, "Seminumerical Algorithms", Reading Mass. Addison-Wesley, 1981).
-----------
Exemplu:
-----------
#define INITIAL_SEED 17 /* SEED - samanta */
#define MULTIPLIER 25273
#define INCREMENT 13849
#define MODULUS 65536
#define FLOATING_MODULUS 65536.0
static unsigned seed = INITIAL_SEED; /* externa, dar locala acestui
fisier */
unsigned random(void)
{
seed = (MULTIPLIER * seed + INCREMENT) % MODULUS;
return seed;
}
double probability(void)
{
seed = (MULTIPLIER * seed + INCREMENT) % MODULUS;
return (seed / FLOATING_MODULUS);
}
Functia "random()" produce o secventa aleatoare (aparenta) de numere intregi
situate intre 0 si MODULUS. Functia "probability()" produce o secventa
aleatoare (aparenta) de valori reale intre 0 si 1.
Observam ca un apel al functiei "random()" sau "probability()" produce o noua
valoare a variabilei "seed" care depinde de cea veche. Din moment ce "seed"
este o variabila externa statica, aceasta este locala acestui fisier si
valoarea sa se pastreaza de la un apel la altul. Putem acum crea functii in
alte fisiere care apeleaza aceste numere aleatoare fara sa avem grija efectelor
laterale.
Prezentam, in continuare, un ultim exemplu de utilizare a lui "static" ca
specificator de clasa de memorare pentru functii. Functiile declarate "static"
sunt vizibile doar in fisierul unde au fost declarate.
-----------
Exemplu:
-----------
void f(int a)
{
. . . . . /* g() este disponibil aici, dar nu si
in alte fisiere */
}
static int g(void)
{
. . . . .
}
--------------------------
Initializari implicite
--------------------------
In C, variabilele externe si statice care nu sunt explicit initializate de
catre programator, sunt initializate de catre sistem cu 0. Aceasta include
siruri, siruri de caractere, pointeri, structuri si inregistrari (union).
Pentru siruri (de caractere), aceasta inseamna ca fiecare element se
initializeaza cu 0, iar pentru structuri si "union" fiecare membru se
initializeaza tot cu 0. In contrast cu aceasta, variabilele "registru" si
"auto" nu se initializeaza de catre sistem, ci pornesc cu valori "garbage"
(adica cu ce se gaseste la momentul executiei la acea adresa).
-----------
Exemplu: Procesarea caracterelor
-----------
O functie care utilizeaza "return" poate returna o singura valoare. Daca dorim
sa trasmitem mai multe valori pentru mediul apelant, atunci trebuie sa
transmitem adresele unor variabile. Vrem sa procesam un sir de caractere (in
stilul "top-down") astfel:
- citeste caractere de la intrare pana cand avem EOF;
- schimba litere mici in litere mari;
- scrie pe fiecare linie trei cuvinte separate de un singur spatiu;
- numara caracterele si literele de la intrare.
#include
#include
#define NR_CUVINTE 3
int procesare(int *, int *, int *);
void main()
{
int c, numar_caractere = 0, numar_litere = 0;
while ((c = getchar()) != EOF)
if (procesare(&c, &numar_caractere, &numar_litere) == 1)
putchar(c);
printf("\n%s%5d\n%s%5d\n\n",
"Numar de caractere:", numar_caractere,
"Numar de litere: ", numar_litere);
}
int procesare(int *p, int *n_c_p, int *n_l_p)
{
static int contor = 0, ultim_caracter = ' ';
if (isspace(ultim_caracter) && isspace(*p))
return 0;
if (isalpha(*p))
{
++*n_l_p;
if (islower(*p))
*p = toupper(*p);
}
else
if (isspace(*p))
if (++contor % NR_CUVINTE == 0)
*p = '\n';
else
*p = ' ';
++*n_c_p;
ultim_caracter = *p;
return 1;
}
----------------------------------------
Definitii si declaratii de functii
---------------------------------------
Pentru compilator, declaratiile functiilor sunt date in multe moduri:
- apelul functiei
- definitia functiei
- prototipuri si declaratii explicite
Daca un apel de functie cum ar fi f(x) apare inainte de a fi declarata atunci
compilatorul presupune declaratia implicita
int f();
In stilul C traditional, declararea functiilor se face astfel:
int f(x)
double x;
{
. . . . .
}
Este responsabilitatea programatorului de a transmite o variabila de tip
"double". In stilul ANSI C, aceasta s-ar scrie:
int f(double x)
{
. . . . .
}
In acest caz, compilatorul stie tipul argumentelor din functia "f()". De
exemplu, daca un "int" este transmis ca parametru, atunci el va fi convertit
automat la "double".
Exista cateva limitari pentru definitiile si prototipurile functiilor. Clasa de
memorare a functiei, daca este prezenta, poate fi "extern" sau "static", dar nu
ambele; "auto" si "register" nu se pot folosi. Singura clasa care se poate
folosi in lista de tipuri a parametrilor este "register". Parametrii nu se pot
initializa.
--------------------------------------------------
Calificatorii de tip "const" si "volatile"
--------------------------------------------------
Comitetul ANSI a adaugat cuvintele rezervate "const" si "volatile" pentru
limbajul C (acestea nu sunt disponibile in limbajul C traditional). De obicei,
"const" este plasat intre clasa de memorare si tipul variabilei.
-----------
Exemplu: static const int k = 3;
-----------
Citim aceasta "k este o constanta de tip int cu clasa de memorare static".
Deoarece "k" are tipul "const", atunci putem initializa "k", dar nu mai poate
fi reasignat (incrementat sau decrementat). Chiar daca variabila este
calificata ca fiind "const", aceasta nu se poate folosi pentru precizarea
lungimii unui sir.
-----------
Exemplu:
-----------
const int n = 3;
int v[n]; /* gresit */
Deci o variabila calificata "const" nu este echivalenta cu o constanta
simbolica.
Un pointer necalificat nu poate fi asignat cu adresa unei variabile calificata
"const".
-----------
Exemplu:
-----------
const int a = 7;
int *p = &a; /* gresit */
Motivul este ca "p" este un pointer obisnuit catre "int" si l-am putea folosi
mai tarziu in expresii de genul "++*p". Totusi, utilizand pointeri, putem
schimba valoarea lui a (ceea ce contravine conceptului de constanta).
-----------
Exemplu:
-----------
const int a = 7;
const int *p = &a;
Nu vom putea modifica valoarea lui "a", utilizand "*p". Pointerul "p" nu este
constant (putem face p++).
Presupunem ca vrem ca "p" sa fie constant, si nu "a". Consideram declaratiile:
int a;
int * const p = &a;
Ultima declaratie spune ca "p este un pointer constant catre int, si valoarea
sa initiala este adresa lui a". Apoi, nu mai putem asigna o valoare lui p, dar
putem da valori lui "*p".
Consideram acum un exemplu si mai interesant:
------------
Exemplu:
------------
const int a = 7;
const int * const p = &a;
Ultima declaratie spune ca p este un pointer constant catre o constanta
intreaga. Nici "p", nici "*p", nu mai pot fi reasignate. In contrast cu "const"
, calificatorul "volatile" este rar folosit. Un obiect "volatile" este unul ce
poate fi modificat intr-un mod nespecificat de catre hard.
-----------
Exemplu: Consideram declaratia
-----------
extern const volatile int real_time_clock;
Clasa de memorare "extern" inseamna "cauta-l oriunde, in acest fisier sau in
alte fisiere". Calificatorul "volatile" presupune ca obiectul poate fi
modificat de hard. Din moment ce apare si calificatorul "const", inseamna ca
obiectul nu poate fi modificat din program.
-----------------------------------------------
Exercitii propuse spre implementare
-----------------------------------------------
1. Daca "i" si "j" sunt de tip "int", iar "p" si "q" sunt pointeri catre "int",
precizati care dintre urmatoarele asignari sunt corecte:
p = &i; p = &*&i; i = (int) p; q = &p;
*q = &j; i = (*&)j; i = *&*&j; i = (*p)++ + *q;
2. Scrieti o functie C care sa faca o permutare circulara a cinci variabile.
(1,2,3,4,5) -> (2,3,4,5,1).
3. Fie codul C
int v = 7, *p = &v, **q = &p;
printf("%p\n%d\n%p\n%p\n%d\n%p\n%p\n%p\n%d\n",
&v, *&v, &p, *&p, **&p, &q, *&q, **&q, ***&q);
Explicati de ce anumite numere se repeta ! Observati ca am folosit combinatia
"*&", si nu "&*". Explicati daca exista situatii unde "&*" este corect semantic.
4. Scrieti un program C care arata pe cati octeti sunt memorati pointerii catre
tipurile fundamentale de date. Ce observati ?