>> 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 ?