>> Inapoi <<

=========
Capitolul 11
=========
=======================================
Directive preprocesor si metodologie de programare
=======================================


---------------------------
Folosirea lui #include
---------------------------
Am discutat deja folosirea directivelor de preprocesare

        #include 
        #include 

O alta forma pentru #include este

        #include "nume_fisier"

Preprocesorul va inlocui aceasta linie cu o copie a fisierului precizat. Mai 
intai cautarea se face in directorul curent, apoi in alte locuri dependente de
sistem. Daca directiva este de forma

        #include 

atunci preprocesorul va cauta in alte locuri (deci nu in directorul curent). 
De exemplu, sub UNIX, fisierele header standard (cum ar fi "stdio.h", 
"stdlib.h") se gasesc de obicei in directorul

                /usr/include

Sub MS-DOS, aceste fisiere se gasesc in directorul

                /include

--------------------------
Folosirea lui #define
--------------------------
Directivele de preprocesare declarate cu "#define" au doua forme:

      -  #define identificator  sir_atomi
      -  #define identificator(id,...,id) sir_atomi

O definitie lunga (care nu dorim sa o scriem pe aceeasi linie poate fi 
continuata pe linia urmatoare punand un \ (backslash) la sfarsitul liniei 
curente). In primul caz, compilatorul va inlocui fiecare aparitie a 
"identificatorului" prin "sir_atomi" in restul fisierului (de la pozitia 
curenta in jos) cu exceptia celor care sunt incadrate intre ghilimele sau 
apostroafe.

-----------
Exemple:        
-----------
        #define NR_SEC_PE_ZI (60 * 60 * 24)
        #define PI 3.141592653
        #define C 299792.458       /* viteza luminii in km/sec */
        #define EOF (-1)       /* valoarea uzuala pt sfarsit de fisier */
        #define MAXINT 2147483647  /* numarul intreg maxim pe 4 octeti */
        #define DIMENS 250         /* dimensiunea unui sir */
        #define EPSILON 1.0e-9     /* limita numerica */
        
Deci, folosirea lui "#define" mareste claritatea si portabilitatea unui program.


--------------------
Sintaxa "dulce"
--------------------
Se foloseste pentru evitarea unor greseli frecvente sau ca un moft.  
--------
Exemplu:        #define EQ ==
--------   
Aceasta declaratie ajuta programatorul sa nu mai confunde = cu ==.

--------
Exemplu:        #define do  /* spatiu */
--------
De exemplu, acum putem simula instructiunea "while" din C ca un "while do" din Pascal sau Algol.

De exemplu, daca avem definitiile de sintaxa "dulce" de mai sus, putem spune ca instructiunile

                while (i EQ 1) do
                 {
                  . . . . . 
                 } 
si 
                while (i == 1)
                 {
                  . . . . . 
                 }               
sunt echivalente.


------------------------------
Macrouri cu argumente
------------------------------
Revenim la forma a doua a macrourilor cu argumente:

        #define identificator(id,...,id) sir_atomi

------------
Exemplu:        #define SQ(x)  ((x) * (x))
------------
Identificatorul x din #define este un parametru care va fi substituit in textul
ce urmeaza. Substitutia se face fara considerarea corectitudinii sintactice. De
exemplu,

        SQ(7 + w)     este echivalent cu    ((7 + w) * (7 + w))

Intr-o maniera similara,

    SQ(SQ(*p)) este echivalent cu  ((((*p) * (*p))) * (((*p) * (*p))))
    
Observati deci ca folosirea parantezelor (de exemplu, (x)) are o importanta 
deosebita, altfel nu s-ar respecta ordinea de evaluare.


-------------------------
Unde este greseala ?    #define SQ(x)  ((x) * (x));
-------------------------
Macrourile sunt folosite de obicei pentru a inlocui apelurile functiilor cu cod
liniar (scurte si fara variabile suplimentare). 

-----------
Exemplu: Macroul de mai jos defineste minimul a doua valori:
-----------
                #define min(x, y)  (((x) < (y)) ? (x) : (y))
                
Dupa aceasta definitie, o expresie de forma

                m = min(u, v)
                
se poate expanda de catre preprocesor la

                m = (((u) < (v)) ? (u) : (v))
                
Folosind aceasta definitie, putem defini minimul a patru valori, astfel 

        #define min4(a, b, c, d)   min(min(a,b), min(c, d))
        
O macro-definitie poate folosi functii si macrouri in corpul lor.

-----------
Exemple:
-----------    
                #define SQ(x)     ((x) * (x))
                #define CUB(x)    (SQ(x) * (x))
                #define F_POW(x)  sqrt(sqrt(CUB(x)))
                
O directiva de preprocesare de forma

                #undef identificator
                
va anula definitia precedenta a identificatorului.


---------------------------------------------------------
Definitii de tipuri si macrouri din 
---------------------------------------------------------
C pune la dispozitie facilitatea "typedef" pentru a asocia (redenumi) un tip cu unul specific. 

-----------
Exemplu:   typedef   char  uppercase;
-----------

Declaratia de mai sus face tipul "uppercase" sinonim cu "char". De exemplu, declaratiile de mai jos sunt valide:

        uppercase c, u[100];
        
Fisierul header  contine cateva definitii de tip:

  typedef int      ptrdiff_t; /* tip intors de diferenta pointerilor */
  typedef short    wchar_t;   /* tip caracter mare */
  typedef unsigned size_t;    /* tipul sizeof */
        
Tipul "ptrdiff_t" spune care este tipul returnat de o expresie implicata in 
diferenta a doi pointeri. In MS-DOS, acesta depinde de modelul de memorie ales
(tiny, short, large, far, huge), pe cand in UNIX, tipul folosit este "int".

Tipul "wchar_t" se foloseste pentru acele caractere care nu se pot reprezenta pe un octet (char -> int).

Reamintim ca operatorul "sizeof" este folosit pentru determinarea lungimii unui
tip sau a unei expresii. De exemplu, "sizeof(double) = 8". Tipul "size_t" este 
returnat de operatorul "sizeof". 

Un macrou definit in  este

                #define  NULL  0


---------------------------------
Sortare folosind "qsort()"
---------------------------------
Daca avem o multime relativ mica de elemente, atunci putem sa folosim sortare 
cu bule sau metoda sortarii prin selectie directa (care sunt de ordinul O(n^2))
Daca insa avem multe elemente, atunci este convenabil sa folosim metoda 
sortarii rapide ("quick sort"). Prototipul functiei "qsort()" se gaseste in 
. Acesta este

  void qsort(void *array, size_t n_els, size_t el_size, int compare(const void *, const void *));
                   
Argumentele acestei functii au rolul:

        array     - sirul care va fi sortat;
        n_els     - numarul de elemente ale sirului;
        el_size   - numarul de octeti necesar memorarii unui element;
        compare   - functia de comparare, ce se declara ca fiind int compare(const void *, const void *)
                      
Functia de comparare are ca argumente doi pointeri catre void. Aceasta returneaza 
un intreg care este mai mic, egal sau mai mare decat zero dupa cum primul 
argument este mai mic, egal sau mai mare decat al doilea argument. 

-----------
Exemplu:
-----------
Vom scrie un program ce foloseste "qsort()". Initializam un vector, il tiparim,
il sortam cu "qsort()", apoi il tiparim din nou.

#include 
#include 
#include 

#define N 11         /* dimensiunea sirului */

int cmp(const void *vp, const void *vq);  /* functia de comparare */
void init(double *a, int n);
void tipareste_sir(double *a, int n);
void main()
 {
  double a[N];
  init(a, N);
  tipareste_sir(a, N);
  qsort(a, N, sizeof(double), cmp);
  tipareste_sir(a, N);
 }
 int cmp(const void *vp, const void *vq)
 {
  const double *p = (const double *)vp;
  const double *q = (const double *)vq;
  double     diff = *p - *q;
  return ((diff >= 0.0) ? ((diff > 0.0) ? -1 : 0) : +1);
 }
 void init(double *a, int n)
 {
  int i;
   srand(time(NULL));               /* vezi rand() */
  for (i = 0; i < n; ++i)
    a[i] = (rand() % 1001) / 10.0;
 } 
void tipareste_sir(double *a, int n)
 {
  int i;
  for (i = 0; i < n; ++i)
   {
    if (i % 6 == 0)
      putchar('\n');
    printf("%12.1f", a[i]);
   }
  putchar('\n'); 
 } 
----------
Intrebari: 1. Ce trebuie sa modificati pentru a obtine ordinea crescatoare a 
----------     sirului ?
           2. Ce rol are "const" din declaratia lui "cmp()" ?

-------------------------------------------------------------------
Un exemplu de utilizare a macrourilor cu argumente
-------------------------------------------------------------------
Vom relua problema de mai sus, dar vom folosi macrouri cu argumente. Vom scrie
programul in doua fisiere, un fisier header "sort.h" si un fisier "sort.c". 
Fisierul header va contine directive de precompilare (#include, #define), 
precum si prototipuri pentru functiile noastre. Fisierul "sort.h" este

#include 
#include 
#include 
#include 
#define M 32
#define N 11
#define parte_fractionara(x) (x - (int) x)
#define caracter_aleator()   (rand() % 26 + 'a')
#define real_aleator()       (rand() % 100 / 10.0)
#define INIT(array, sz, type)          \
   if (strcmp(type, "char") == 0)      \
     for (i = 0; i < sz; ++i)          \
       array[i] = caracter_aleator();  \
   else                                \        
     for (i = 0; i < sz; ++i)          \    
       array[i] = real_aleator();
       
#define PRINT(array, sz, sir_control)  \
   for (i = 0; i < sz; ++i)            \
     printf(sir_control, array[i]);    \
   putchar('\n')
  
int compara_partea_fractionara(const void *, const void *);
int lexico(const void *, const void *);

Acum, vom scrie restul codului pentru programul nostru, si anume fisierul "sort.c".

#include "sort.h"

void main()
 {
  char a[M];
  float b[N];
  int i;
  srand(time(NULL));
  INIT(a, M, "char");
  PRINT(a, M, "%-2c");
  qsort(a, M, sizeof(char), lexico);
  PRINT(a, M, "%-2c");
  printf("---\n");
  INIT(b, N, "float");
  PRINT(b, N, "%-6.1f");
  qsort(b, N, sizeof(float), compara_partea_fractionara);
  PRINT(b, N, "%-6.1f");
 }
 
int compara_partea_fractionara(const void *vp, const void *vq)
 {
  const float *p = (const float *)vp, *q = (const float *)vq;
  float       x;
  x = parte_fractionara(*p) - parte_fractionara(*q);
  return((x < 0.0) ? -1 : (x == 0.0) ? 0 : +1);
 } 
 
int lexico(const void *vp, const void *vq)
 {
  const char *p = (const char *)vp, *q = (const char *)vq;
  return(*p - *q);
 }

------------------------------
Compilare conditionata
------------------------------
Preprocesorul are directive pentru compilare conditionata. Acestea pot fi 
folosite pentru dezvoltarea programelor si pentru scrierea codului mai portabil
de la o masina la alta. Fiecare directiva de forma

        #if     expresie_integrala_constanta
        #ifdef  identificator
        #ifndef identificator
        
implica compilarea conditionata a codului care urmeaza pana la directiva de precompilare 

        #endif
        
Pentru compilarea codului de mai sus, in cazul lui #if trebuie ca expresia 
constanta sa fie diferita de zero (true), in cazul lui #ifdef sau #ifdefined 
numele identificatorului trebuie sa fie definit anterior intr-o linie #define,
fara interventia directivei

        #undef identificator
        
In cazul lui #ifndef, numele identificatorului trebuie sa nu fie curent 
definit. Expresia constanta integrala folosita intr-o directiva de precompilare
nu poate contine operatorul "sizeof" sau un cast. Poate insa, folosi operatorul
de precompilare "defined" (valabil in ANSI C, dar nu si C traditional). 
Expresia

        defined identificator 

este echivalenta cu

        defined(identificator)
        
Acesta se evalueaza la 1 daca identificatorul este definit, si 0 in caz contrar.

-----------
Exemplu:
-----------
        #if defined(HP9000) || defined(SUN4) && !defined(VAX)
           . . . . .       /* cod dependent de masina */
        #endif
        
Uneori "printf()" este utila in scopuri de depanare. Presupunem ca la inceputul unui fisier am scris

        #define DEBUG 1

si in unele zone ale programului am scris

        #if DEBUG
          printf("debug:  a = %d\n", a);
        #endif

Daca dupa ce ne-am convins ca este bine ce se intampla si vrem sa nu mai vizualizam valoarea lui "a" in acest moment, atunci schimbam DEBUG in 0 (de exemplu).

O alta varianta ar fi sa nu initializam DEBUG. Scriem deci la inceputul fisierului

        #define DEBUG
        
Putem folosi #ifdef si #if si scriem:

        #ifdef DEBUG
          . . . . . 
        #endif

---------------------------
Macrouri predefinite
---------------------------
In ANSI C sunt 5 macrouri predefinite. Nu pot fi redefinite de catre programator. Ele au la inceput si sfarsit cate doua simboluri "underscore". 

     Macro predefinit                     Valoare
   ----------------------------------------------------------------
        __DATE__       Un sir ce contine data curenta   
        __FILE__         Un sir ce contine numele fisierului
        __LINE__        Un intreg reprezentand numarul liniei curente
        __STDC__       Daca implementarea=ANSI C, atunci acesta
                                 reprezinta un numar diferit de zero
        __TIME__        Un sir ce contine timpul curent
   ----------------------------------------------------------------

----------------------
Operatorii # si ##
----------------------   
Operatorii de preprocesare # si ## sunt valabili in ANSI C, dar nu si in C traditional. Operatorul unar # cauzeaza transformarea in sir a unui parametru formal dintr-o macro-definitie.

        #define mesaj_pentru(a, b)  \
          printf(#a " si " #b ": Te iubim !\n")
        void main() { mesaj_pentru(Carolina, Nicoleta); }  

La apelul acestui macrou, fiecare parametru al acestuia este inlocuit cu argumentul corespunzator, iar # cauzeaza ca argumentele sa fie puse intre ghilimele. Altfel spus, dupa preprocesare, in memorie se obtine: 

        void main() { printf("Carolina" " si " "Nicoleta" ": Te iubim !\n"); }

Deoarece sirurile constante separate prin spatiu se concateneaza, instructiunea de mai sus este echivalenta cu:

        void main() { printf("Carolina si Nicoleta: Te iubim !\n"); }

Operatorul binar ## este folosit la impartirea in tokenuri lexicale. 
-----------
Exemplu:
-----------  #define  X(i)  x ## i
                X(1) = X(2) = X(3);
                
va deveni dupa preprocesare

                x1 = x2 = x3;

-------------------------
Macroul "assert()"
------------------------
ANSI C pune la dispozitie macroul "assert()" din biblioteca standard "assert.h". Acest macrou poate fi folosit cand vrem sa ne asiguram ca o expresie are o anumita valoare. Vrem sa scriem o functie ale carei argumente satisfaca niste conditii.

-----------
Exemplu:
-----------
                #include 
                void f(char *p, int n)
                 {
                  . . . . . 
                  assert(P != NULL);
                  assert(n > 0 && n < 5);
                  . . . . .
                 }
                 
Daca vreo asertiune esueaza, atunci sistemul va tipari un mesaj si va opri executia programului. Iata o implementare posibila a lui "assert()". 

        #if defined(NDEBUG)
          #define assert(ignore)  ((void) 0)   /* ignorare */
        #else
          #define assert(expr)
            if (!(expr))                           \
             {                                     \
              printf("\n%s%s\n%s%s\n%s%d\n\n,      \
                     "Assertion failed: ", #expr,  \
                     "in file ", __FILE__,         \
                     "al line ", __LINE__);        \
             }                              
        #endif  
        
De remarcat ca daca NDEBUG este definit, atunci sunt ignorate toate asertiunile. Aceasta permite programatorului in timpul scrierii programului sa verifice pas cu pas executia programului. Functia "abort()" se gaseste in biblioteca standard.


----------------------------------------
Folosirea lui #error si #pragma
----------------------------------------
ANSI C contine si directivele de preprocesare #error si #pragma.

------------
Exemplu:
------------
                #if A_SIZE < B_SIZE
                  #error "tipuri incompatibile"
                #endif

Daca in timpul compilarii va apare o eroare prezenta intr-o directiva #error, atunci se va afisa mesajul respectiv. 
Directiva #pragma se foloseste pentru folosire specifica implementarii. Ea are forma generala:

                #pragma  atomi_lexicali

Aceasta cauzeaza o comportare ce depinde de fiecare compilator C in parte. 

----------------------------------------
Numerele liniilor unui program
----------------------------------------
O directiva de preprocesare de forma

                #line constanta_integrala "nume_fisier"
                
va determina compilatorul sa renumeroteze liniile textului sursa astfel incat 
urmatoarea linie sa aiba valoarea specificata si numele fisierului sursa curent
este "nume_fisier". Daca nu se precizeaza "nume_fisier", atunci se va face doar
numerotarea liniilor. 
Bineinteles, numerele asociate liniilor sunt ascunse pentru programator si apar numai la mesaje de eroare sau avertismente.

-----------------------------------------------
Exercitii propuse spre implementare
----------------------------------------------
1. Scrieti propria voastra functie "quicksort()" care sa fie echivalenta cu "qsort()" pus la dispozitie de sistemul C.
2. Definiti o macro-definitie pentru XOR(), numita "sau exclusiv". Un apel XOR(a,b)=true <=> a este true si b false, sau a false si b true. Scrieti si o macro-definitie XOR(a,b,c) si una XOR(a,b,c,d).
3. Scrieti un program C in care sa afisati valorile celor 5 macrouri predefinite.