PC Laborator 3.1 (opțional)

De la WikiLabs
Versiunea din 29 octombrie 2017 21:41, autor: Rhobincu (discuție | contribuții) (→‎Obiective)
(dif) ← Versiunea anterioară | Versiunea curentă (dif) | Versiunea următoare → (dif)
Jump to navigationJump to search

Obiective

În urma parcurgerii acestui laborator studentul va fi capabil să înțeleagă funcționalitatea preprocesorului și să folosească directive de preprocesare.

Preprocesorul C

Preprocesarea unui fișier cu cod C este prima etapă din lanțul de build, și reprezintă, așa cum îi spune și numele, o etapă de dinainte de procesarea (compilarea) efectivă. Motivul pentru care se realizează această etapă inițială este faptul că prin preprocesarea codului se pot elimina secțiuni din program care ori nu sunt necesare, ori nu sunt compilabile pe un anumit procesor sau sistem de operare. Spre exemplu, pentru a folosi o interfață serială, în sistemul de operare Windows, și compilatorul Visual Studio, există un tip de date care se numește HANDLE. Acest tip de date nu există în Linux, aici fiind înlocuit simplu cu int. În acest caz, compilarea unui program care folosește HANDLE pe Linux va eșua cu eroare. Aici intervin directivele de preprocesare, după cum vom vedea în continuare.

În acest laborator se va discuta despre:

  • #include
  • #define
  • #undef
  • #ifdef
  • #ifndef
  • #else
  • #endif

Directiva #include

Probabil #include este cea mai frecvent utilizată directivă de preprocesare, dar și cea mai ușor de înțeles. Efectiv, preprocesorul caută fișierul specificat între paranteze unghiulare sau ghilimele în lista de directoare dintr-o listă cunoscută și apoi înlocuiește directiva #include cu conținutul fișierului respectiv.

Exemplu

  • Scrieți un fișier nou într-un editor de text care să conțină următorul cod:
int main(){
    return 0;
}
  • Salvați fișierul cu numele test_preprocessor.c în directorul ~/work/prenume_nume
  • Scrieți un fișier Makefile cu o singură rețetă care să producă un fișier numit processed.c din fișierul de mai sus, apelând compilatorul C (gcc) cu fanionul corespunzător pentru a face doar preprocesare (vezi PC Laborator 1).
  • Ce diferențe există între fișierul original și fișierul preprocesat?
  • Scrieți un nou fișier numit header_file.h, care să conțină următorul text:
// This is the start of the header file
int variable;
// This is the end of the header file
  • Modificați fișierul test_preprocesor.c prin înserția pe prima linie a unei directive include:
#include <header_file.h>
int main(){
    return 0;
}
  • Rulați din nou comanda make. Observați mesajul de eroare: fișierul header_file.h nu este găsit de preprocesor, cu toate că este în directorul curent.
Atenție: Fișierele antet (header) sunt căutate doar în anumite căi predefinite în compilator (de exemplu /usr/include). Pentru a face preprocesorul să caute și în alte directoare, calea până la acestea trebuie specificată la compilare, folosind fanionul -I
  • Adăugați comenzii de compilare din Makefile următorul fanion: -I. Asta va spune compilatorului (care mai departe va spune preprocesorului) să caută fișiere antet și în directorul curent (.)
  • Rulați din nou comanda make.
  • Afișați conținutul celor două fișiere C. Ce diferențe observați?

Directivele #define și #undef

Directiva #define este utilizată pentru a defini macro-uri de preprocesor. Acestea se folosesc în trei feluri distincte:

  • #define token value - este definit macro-ul token existența acestuia putând fi testată cu directivele #ifdef și #ifndef; dacă token este utilizat în program, după definirea lui, el va fi înlocuit cu value (efectiv, această operație este identică cu un "Search and Replace" dintr-un editor de text, unde token este înlocuit cu value.
#define PI 3.1415

float a = 2 * PI;
  • #define token - este definit macro-ul token, existența acestuia putând fi testată cu directivele #ifdef și #ifndef; dacă token este utilizat în program, după definirea lui, va fi șters de peste tot unde apare (este de fapt cazul de mai sus unde value este de fapt un string de lungime zero).
#include <stdio.h>
#define DEBUG

int main(){
#ifdef DEBUG
    printf("Debug is ON!\n");
#endif
    return 0;
}
  • #define token(arg1,arg2,..) expression - este definit macro-ul token, existența acestuia putând fi testată cu directivele #ifdef și #ifndef; token este utilizat în program ca o funcție, iar el va fi înlocuit de expression, în care arg1,arg2,... vor fi înlocuite cu valorile din program.
#include <stdio.h>
#define MAX(a,b) (a < b ? b : a)

int main(){
    printf("Value is %d!\n", MAX(4,5)); // Replaced to: printf("Value is %d!\n", (4 < 5 ? 5 : 4));
//  printf("Value is %d!\n", MAX(4 + 1, 5 + 1)); // Replaced to: printf("Value is %d!\n", (4 + 1 < 5 + 1 ? 5 + 1 : 4 + 1));
    return 0;
}


Atenție: O greșeală frecventă în definirea macrourilor este adăugarea unui caracter ; la sfârșitul liniei. Acesta se va înlocui și el în momentul în care macro-ul se expandează în text:
#include <stdio.h>
#define PI 3.1415;

int main(){
    float f = PI * 2; // Replaced to: float f = 3.1415; * 2; 
    return 0;
}

Se vede imediat că sintaxa este greșită și compilatorul va genera o eroare.

Pentru a anula definiția unui macro, se folosește directiva #undef astfel:

#undef PI
#undef DEBUG
#undef MAX

Exemplu

  • Modificați fișierul test_preprocessor.c în felul următor:
#define PI 3.1415
#define INC(x) (x + 1)
#define DEBUG

int main(){
    printf("PI is %f\n", PI);
    printf("%d comes after 4\n", INC(4));

#ifdef DEBUG
    printf("This runs in debug mode\n");
#else
    printf("This runs in release mode\n");
#endif
    return 0;
}
  • Rulați comanda make în consolă.
  • Afișați cele două fișiere .c. Observați diferențele.

Directivele #ifndef și #define pe post de gardă pentru dublă incluziune

Luând ca punct de plecare primul exemplu de la directiva #include, oare ce se întâmplă dacă header-ul header_file.h este inclus de două ori?

1#include "header_file.h"
2#include "header_file.h"
3int main(){
4    return 0;
5}

Răspunsul este simplu, preprocesorul va înlocui ambele linii cu conținutul fișierului, lucru care va face ca variabila variable să fie definită de două ori, lucru care sintactic greșit în C. Sigur, nimeni nu va include în mod voit un header de două ori, dar este posibil ca indirect lucrul acesta să se întâmple. Spre exemplu, dacă în fișierul header_file.h este inclus stdio.h, iar în test_preprocessor.c sunt incluse și header_file.h și stdio.h. În această situație, stdio.h ajunge să fie inclus de două ori. Pentru a evita problemele apărute în această situație, se folosește garda de incluziune (include guard). Aceasta se adaugă în fiecare fișier header:

1#ifndef _HEADER_FILE_H_
2#define _HEADER_FILE_H_
3
4// This is the start of the header file
5int variable;
6// This is the end of the header file
7
8#endif

Includerea acestui fișier de către preprocesor se realizează acum în următoarea secvență:

  1. Prima directivă include din fișerul sursă C va copia conținutul fișierului de mai sus în locul unde este inclus (test_preprocesor.c, linia 1).
  2. Directiva ifndef verifică dacă macro-ul _HEADER_FILE_H_ nu este definit (header_file.h, linia 1).
  3. Deoarece nu este definit, tot textul dintre ifndef și endif se păstrează (header_file.h, liniile 2-7).
  4. Prima linie din text-ul păstrat este o directivă define care definește macro-ul_HEADER_FILE_H (header_file.h, linia 2).
  5. A doua directivă include din fișerul sursă C va copia conținutul fișierului de mai sus în locul unde este inclus (test_preprocesor.c, linia 2).
  6. Directiva ifndef verifică dacă macro-ul _HEADER_FILE_H_ nu este definit (header_file.h, linia 1).
  7. Deoarece el acum ESTE definit în cadrul include-ului anterior, tot textul dintre ifndef și endif se elimină (header_file.h, liniile 2-7).

Alternativă la include guards - #pragma once

Problema cu include guards este că pe lângă faptul ca este o construcție complicată care are nevoie de linii adăugate în fișierul header și la început și la sfârșit, numele macro-ului definit se poate repeta accidental în două sau mai multe fișiere, în această situație neincluzându-se decât primul din aceste fișiere.

O alternativă non-standard dar suportată de majoritatea compilatoarelor (inclusiv C++) este utilizarea directivei #pragma once:

1#pragma once
2
3// This is the start of the header file
4int variable;
5// This is the end of the header file

Exemplu

  • Modificați fișierul test_preprocessor.c pentru a include header-ul header_file.h de două ori, conform exemplului de mai sus.
  • Rulați comanda make și confirmați dubla declarare a variabilei variable.
  • Adăugați gardă de incluziune fișierului header, conform exemplului de mai sus.
  • Rulați comanda make și confirmați că variabila variable este declarată o singură dată.
  • Înlocuiți garda de incluziune cu #pragma once
  • Rulați comanda make și confirmați că variabila variable este declarată o singură dată.