Diferență între revizuiri ale paginii „PC Laborator 2.1 (opțional)”
Linia 206: | Linia 206: | ||
{| class="wikitable" | {| class="wikitable" | ||
− | !specifier !! | + | ! ''specifier'' !! Descriere !! Caractere extrase din stream |
|- | |- | ||
− | |i, u || | + | |i, u || întreg || Any number of digits, optionally preceded by a sign (+ or -). Decimal digits assumed by default (0-9), but a 0 prefix introduces octal digits (0-7), and 0x hexadecimal digits (0-f). |
|- | |- | ||
− | |d || | + | |d || întreg în baza 10 ||Any number of decimal digits (0-9), optionally preceded by a sign (+ or -). |
|- | |- | ||
− | |o || | + | |o || întreg în baza 8 ||Any number of octal digits (0-7), optionally preceded by a sign (+ or -). |
|- | |- | ||
− | |x || | + | |x || întreg în baza 16 || Any number of hexadecimal digits (0-9, a-f, A-F), optionally preceded by 0x or 0X, and all optionally preceded by a sign (+ or -). |
|- | |- | ||
− | |f, e, g, a (C99) || | + | |f, e, g, a (C99) || număr în virgulă mobilă ||A series of decimal digits, optionally containing a decimal point, optionally preceeded by a sign (+ or -) and optionally followed by the e or E character and a decimal integer (or some of the other sequences supported by strtod). Implementations complying with C99 also support hexadecimal floating-point format when preceded by 0x or 0X. |
|- | |- | ||
− | |c || | + | |c || caracter ||The next character. If a width other than 1 is specified, the function reads exactly width characters and stores them in the successive locations of the array passed as argument. No null character is appended at the end. |
|- | |- | ||
− | |s || | + | |s || șir de caractere ||Any number of non-whitespace characters, stopping at the first whitespace character found. A terminating null character is automatically added at the end of the stored sequence. |
|- | |- | ||
− | |p || | + | |p || adresă în memorie ||A sequence of characters representing a pointer. The particular format used depends on the system and library implementation, but it is the same as the one used to format %p in fprintf. |
|- | |- | ||
− | |[characters] || | + | |[characters] || set de caractere ||Any number of the characters specified between the brackets. A dash (-) that is not the first character may produce non-portable behavior in some library implementations. |
|- | |- | ||
− | |[^characters] || | + | |[^characters] || set de caractere negat ||Any number of characters none of them specified as characters between the brackets. |
|- | |- | ||
− | |n || | + | |n || număr caractere citite ||No input is consumed. The number of characters read so far from stream is stored in the pointed location. |
|- | |- | ||
− | |% ||% ||A % followed by another % matches a single %. | + | |% || % ||A % followed by another % matches a single %. |
|} | |} | ||
− | + | În afară de n, cel puțin un caracter va fi consumat din stream de către oricare alt specificator. Altfel, operația eșuează și citirea se încheie în acel punct. |
Versiunea de la data 23 septembrie 2015 19:09
Obiective
În urma parcurgerii acestui laborator studentul va fi capabil:
- să înțeleagă funcționalitatea preprocesorului și să folosească directive de preprocesare;
- să utilizeze stream-urile standard ale unui proces în Linux și în programe C;
- să utilizeze funcționalitatea de bază a unui sistem de revision control - Git.
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 numitprocessed.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șierulheader_file.h
nu este găsit de preprocesor, cu toate că este în directorul curent.
/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;
}
#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
:
#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ță:
- 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).
- Directiva ifndef verifică dacă macro-ul _HEADER_FILE_H_ nu este definit (header_file.h, linia 1).
- Deoarece nu este definit, tot textul dintre ifndef și endif se păstrează (header_file.h, liniile 2-7).
- Prima linie din text-ul păstrat este o directivă define care definește macro-ul_HEADER_FILE_H (header_file.h, linia 2).
- 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).
- Directiva ifndef verifică dacă macro-ul _HEADER_FILE_H_ nu este definit (header_file.h, linia 1).
- Deoarece el acum ESTE definit în cadrul include-ului anterior, tot textul dintre ifndef și endif se elimină (header_file.h, liniile 2-7).
Exemplu
- Modificați fișierul
test_preprocessor.c
pentru a include header-ulheader_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ă variabile variable este declarată o singură dată.
Stream-urile standard ale unui proces
După cum se poate observa în schema de mai sus, un proces care rulează într-un sistem de operare dispune de trei stream-uri de intrare ieșire implicite:
- Stream-ul standard de intrare (stdin) - este utilizat pentru a introduce date în proces și este implicit atașat tastaturii;
- Stream-ul standard de ieșire (stdout) - este utilizat pentru a extrage rezultate ale execuției procesului și este implicit atașat consolei;
- Stream-ul standard de eroare (stderr) - este tradițional utilizat pentru a extrage informații legate de erorile apărute în execuția procesului și este implicit atașat consolei;
Se observă că ambele stream-uri de ieșire sunt implicit trimise către consolă, unde utilizatorul procesului le poate vedea. Motivul pentru care sunt două este acela că aceste stream-uri pot fi redirectate către alte procese sau către fișiere, astfel se poate face o separare simplă și rapidă a mesajelor de eroare, în scopul depanării programului.
Toate aceste stream-uri sunt formatate pe octet (byte). În cele mai multe din cazuri, acești bytes reprezintă caractere codate ASCII.
Citirea din stdin
Pentru a citi de pe stream-ul standard de ieșire se folosește generic funcția fscanf
:
Funcția fscanf
este definită în header-ul stdio.h
(standard input/output) și se folosește în felul următor:
fscanf(stdin, "[format]", val1, val2, ...);
Formatul reprezintă modelul de date pe care ne așteptăm să-l primim de la tastatură. Acesta este un șir de caractere unde fiecare caracter este tratat în următorul fel:
- caracter alb - se va citi și ignora din stream orice caractere albe până la întâlnirea unui caracter care nu este alb (caracerele albe includ spații, linie nouă, caractere tab); un singur caracter alb în format validează oricâte caractere albe extrase din stream (inclusiv nici unul).
- caracter care nu este alb, exceptând specificatorul de format (%) - orice caracter care nu este alb (spațiu, linie nouă sau tab) sau care nu face parte dintr-un specificator de format (care începe cu %) face ca următorul caracter să fie citit din stream, comparat cu acest caracter din format, iar dacă sunt identice, ele sunt ignorate și se continuă cu următorul caracter; dacă nu sunt identice,
fscanf
se va opri cu eroare; - specificator de format - o secvență care începe cu caracterul "la sută" (%) indică un specificator de format, utilizat pentru a indica tipul și formatul unei date care trebuie citită și stocată într-una din variabilele specificate în apelul lui
fscanf
.
Specificatorul de format pentru fscanf
urmează următorul prototip:
%[*][width][length]specifier
unde caracterul specifier de la sfârșit este componenta cea mai importantă deoarece definește ce caractere sunt extrase din stream, interpretarea lor, și tipul de date corect al argumentului corespunzător:
specifier | Descriere | Caractere extrase din stream |
---|---|---|
i, u | întreg | Any number of digits, optionally preceded by a sign (+ or -). Decimal digits assumed by default (0-9), but a 0 prefix introduces octal digits (0-7), and 0x hexadecimal digits (0-f). |
d | întreg în baza 10 | Any number of decimal digits (0-9), optionally preceded by a sign (+ or -). |
o | întreg în baza 8 | Any number of octal digits (0-7), optionally preceded by a sign (+ or -). |
x | întreg în baza 16 | Any number of hexadecimal digits (0-9, a-f, A-F), optionally preceded by 0x or 0X, and all optionally preceded by a sign (+ or -). |
f, e, g, a (C99) | număr în virgulă mobilă | A series of decimal digits, optionally containing a decimal point, optionally preceeded by a sign (+ or -) and optionally followed by the e or E character and a decimal integer (or some of the other sequences supported by strtod). Implementations complying with C99 also support hexadecimal floating-point format when preceded by 0x or 0X. |
c | caracter | The next character. If a width other than 1 is specified, the function reads exactly width characters and stores them in the successive locations of the array passed as argument. No null character is appended at the end. |
s | șir de caractere | Any number of non-whitespace characters, stopping at the first whitespace character found. A terminating null character is automatically added at the end of the stored sequence. |
p | adresă în memorie | A sequence of characters representing a pointer. The particular format used depends on the system and library implementation, but it is the same as the one used to format %p in fprintf. |
[characters] | set de caractere | Any number of the characters specified between the brackets. A dash (-) that is not the first character may produce non-portable behavior in some library implementations. |
[^characters] | set de caractere negat | Any number of characters none of them specified as characters between the brackets. |
n | număr caractere citite | No input is consumed. The number of characters read so far from stream is stored in the pointed location. |
% | % | A % followed by another % matches a single %. |
În afară de n, cel puțin un caracter va fi consumat din stream de către oricare alt specificator. Altfel, operația eșuează și citirea se încheie în acel punct.