137309474 Invata Limbajul de Programare C
-
Upload
rangertalon -
Category
Documents
-
view
151 -
download
9
description
Transcript of 137309474 Invata Limbajul de Programare C
Invata Limbajul de Programare C – Partea 1
Bine ati venit pe ItAssistant. Aceasta noua serie de tutoriale isi propune sa va familiarizeze cu limbajul
de programare C si conceptele Programarii Structurata.
Introducere
C este un limbaj de programare structurată menit să simplifice scrierea programelor apropiate de masină.
A fost creat de către Dennis Ritchie în perioada 1968-1973 și a fost dezvoltat în strânsă legatură cu
sistemul de operare Unix, care a fost rescris în întregime în C. Utilizarea limbajului s-a extins cu trecerea
timpului de la sisteme de operare și aplicaţii de sistem la aplicaţii generale.
Deşi în prezent, pentru dezvoltarea aplicaţiilor complexe au fost create limbaje de nivel mai înalt (Java,
C#, Python), C este în continuare foarte folosit la scrierea sistemelor de operare şi a aplicațiilor de
performanţă mare sau dimensiune mică (în lumea dispozitivelor embedded). Nucleele sistemelor
Windows şi Linux sunt scrise în C.
Compilatorul GCC
GCC este unul dintre primele pachete software dezvoltate în cadrul Proiectului GNU (GNU‟s Not Unix) de
către Free Software Foundation. Deși GCC se traducea iniţial prin GNU C Compiler, acesta a devenit
între timp un compilator multifrontend, multi-backend, având suport pentru o serie largă de limbaje, ca C,
C++, Objective-C, Ada, Java, etc, astfel că denumirea curentă a devenit GNU Compiler Collection.
Compilatorul GCC rulează pe o gamă largă de echipamente hardware (procesoare din familia: i386,
alpha, vax, m68k, sparc, HPPA, arm, MIPS, PowerPC, etc.) și de sisteme de operare (GNU/Linux, DOS,
Windows 9x/NT/2000, Solaris, Tru64, VMS, Ultrix, Aix ), fiind la ora actuală cel mai portat compilator.
Compilatorul GCC se apelează din linia de comandă, folosind diferite opțiuni, în funcție de rezultatul
care se dorește (specificarea de căi suplimentare de căutare a bibliotecilor/fișierelor antet, link-area unor
biblioteci specifice, opțiuni de optimizare, controlul stagiilor de compilare, al avertisementelor, etc.).
Pentru exemplificare vom considera următorul program foarte simplu:
/*hello.c*/
#include <stdio.h>
int main() {
printf("Hello from your first program!");
return 0;
}
Pentru compilarea programului se va lansa comanda (în linia de comandă):
gcc hello.c
presupunând că fișierul sursă se numește hello.c (este esențial ca extensia să fie c și nu C (C mare – așa
cum este cazul fișierelor produse de Borland C++ 3.1), deoarece aceasta din urmă este interpretată de
către compilator ca fiind extenesie de fișier C++).
În funcţie de sistemul de operare folosit, pentru executarea programului astfel obținut se va lansa fie
comanda
pentru Linux
a
pentru Windows, fie comanda
./a.out
Prima comandă are ca efect compilarea și link-editarea (rezolvarea apelurilor de funcții) fișierului
sursăhello.c, generându-se un fișier executabil, al cărui nume implicit este a.out în cazul sistemelor Linux
și a.exeîn cazul sistemelor Windows. Pentru un control mai fin al comportării compilatorului, sunt
prezentate în tabelul următor cele mai folosite opţiuni (pentru lista completă studiaţi pagina de manual
pentru GCC – man gcc):
Opțiune Efect
-o nume_fișier Numele fișierului de ieşire va fi nume_fişier. În cazul în care această opțiune nu este setată, se va folosi numele implicit (pentru fișiere executabile: a.out – pentru Linux și a.exe – pentru Windows)
-Icale_catre_fisiere_antet
Caută fișiere antet și în calea specificată
-L cale_catre_biblioteci Caută fișiere bibliotecă și în calea specificată
-l nume_biblioteca
Link-editează librăria nume_biblioteca. Atenție!!! nume_bibliotecă nu este întotdeauna același cu numele fișierului antet prin care se include această bibliotecă. Spre exemplu, pentru includerea bibliotecii de funcții matematice, fișierul antet este math.h, iar biblioteca este m
-W tip_warning
Afișează tipurile de avertismente specificate (Pentru mai multe
detalii man gcc saugcc --help). Cel mai folosit tip este all. Este
indicat ca la compilarea cu -Wall să nu apară nici un fel de avertismente
-c Compilează și asamblează, dar nu link-editează. Rezultă fișiere obiect, cu extensia .o
-S Se opreste după faza de compilare, fară să asambleze. Rezultă cod assembler in fișiere cu extensia .s
Spre exemplu:
gcc -o exemplu exemplu.c -lm -Wall
are ca efect compilarea și link-editarea fişierului exemplu.c, cu includerea bibliotecii matematice, afişând
toate avertismentele. Fişierul de ieşire se va numi exemplu.exe (extensia e determinată de sistemul de
operare, dar şi de conţinutul fişierului sursă).
Utilitarul Make
Utilitarul make determină automat care sunt părțile unui proiect care trebuie recompilate ca urmare a
operării unor modificări și declanşează comenzile necesare pentru recompilarea lor. Pentru a putea utiliza
make, este necesar un fișier de tip makefile numit de obicei Makefile (sau makefile) care descrie relațiile
de dependenţă între diferitele fișiere din care se compune programul şi care specifică regulile de
actualizare pentru fiecare fişier în parte.
În mod normal, într-un program, fişierul executabil este actualizat (recompilat) pe baza fișierelor-obiect,
care la rândul lor sunt obținute prin compilarea fișierelor sursă. Totuși, acest utilitar poate fi folosit pentru
orice proiect care conţine dependenţe şi cu orice compilator/utilitar care poate rula în linia de comandă.
Odată creat fișierul makefile, de fiecare dată când apare vreo modificare în fișierele sursă, este suficient
să rulăm utilitarul make pentru ca toate recompilările necesare să fie efectuate.
Programul make utilizează fișierul Makefile ca bază de date şi pe baza timpilor ultimei modificări a
fișierelor din Makefile decide care sunt fișierele care trebuie actualizate. Pentru fiecare din aceste fișiere,
sunt executate comenzile precizate in Makefile.
În continuare prezentăm un exemplu simplu de makefile.
# Declarațiile de variabile
CC = gcc
CCFLAGS = -Wall -lm
SRC = radical.c
PROGRAM = radical
# Regulă de compilare
all:
$(CC) -o $(PROGRAM) $(SRC) $(CCFLAGS)
# Regulile de "curațenie" (se folosesc pentru ștergerea fișierelor intermediare si/sau
rezultate)
.PHONY : clean
clean :
rm -f $(PROGRAM) core *~
Pachetul MinGW
Compilatorul GCC este disponibil pentru Windows în pachetul MinGW. Pentru instalare folosiţi fişierul
.exe. Antenţie! Installer-ul are nevoie de acces la Internet pentru a putea descărca diversele componente.
Tot de la această adresă se poate descărca si pachetul MSYS care conține variante portate ale
utilitaruluimake și ale programelor folosite cel mai frecvent în makefile-uri (rm, cd, cat, etc.).
Instalarea este simplă, folosind un setup intuitiv. Pentru funcționarea corectă a pachetului MinGW/MSYS,
este însă necesară adăugarea directorului unde se află binarele instalate la variabila de mediu PATH (în
mediul Windows). Pentru GCC, aceasta este de obicei C:MinGWbin. Pentru sistemele Windows 2000 și
mai recente setarea variabilei de mediu PATH se realizează astfel:
click dreapta pe My Computer > Properties
din tabul Advanced se selectează Environment Variables
aici dublu click pe elementul Path din System Variable
adaugați la sfarșitul șirului de la Variable value calea către directorul care conține binarele
compilatorului GCC (vezi imaginea).
Pentru Ubuntu puteti folosi urmatoarea comanda pentru a instalat make
apt-get install build-essential
Editoare
Pentru editarea surselor se poate folosi orice editor de text. Astfel, putem menționa:
Linux: vi(m), pico, joe, nano, emacs, mcedit – cu interfața în mod text si Kate, KWrite, GEdit,
Scribes – cu interfață grafică.
Windows: Crimson Editor, Notepad++, Textpad, etc.
Java: jEdit
Deși lista nu este completă, editoarele specificate au pe langă facilitățile standard (Cut/Copy, wordwrap)
si suport pentru syntax highlight, auto-indent, etc. ceea ce le face să fie mai prietenoase și să ajute în
scrierea codului.
Interacțiunea program-utilizator
Majoritatea algoritmilor presupun introducerea unor date de intrare și calcularea unor rezultate. În cazul
programelor de consolă, datele sunt introduse de la tastatură și afișate pe ecran (alte variante sunt
folosirea fișierelor sau preluarea datelor de la un hardware periferic).
Programul dat ca exemplu mai sus folosește funcția de afișare printf. Această funcție realizează transferul
și conversia de reprezentare a valorii întregi / reale in șir de caractere sub controlul unui format (specificat
ca un șir de caractere):
printf("format", expr_1, expr_2, ..., expr_n);
unde expr_i este o expresie care se evaluează la unul din tipurile fundamentale ale limbajului. Este
necesar ca pentru fiecare expresie să existe un specificator de format, şi viceversa.
În caz contrar, compilatorul va returna o eroare (în afara cazului în care formatul este obtinut la rulare).
Sintaxa unui descriptor de format este:
% [ - ] [ Lung ] [ .frac ] [ h|l|L ] descriptor
Semnificația câmpurilor din descriptor este descrisă în tabelul următor:
Câmp Descriere
- Indică o aliniere la stânga în câmpul de lungime Lung (implicit alinierea se face la dreapta).
Lung
Dacă expresia conține mai puțin de Lung caractere, ea este precedată de spații sau zerouri, dacă Lung începe printr-un zero. Dacă expresia conține mai mult de Lung caractere, câmpul de afișare este extins. În absența lui Lung, expresia va fi afișată cu atâtea caractere câte conține.
frac Indică numărul de cifre după virgulă (precizia) cu care se face afișarea.
l Marchează un long int, în timp ce pentru reali l determină afișarea unei valori double.
h Marchează un short int
L Precede unul din descriptorii f,e,E,g,G pentru afișarea unei valori de tip long double.
Tabelul următor prezintă descriptorii și conversiile care au loc:
Descriptor Descriere
d Întreg cu semn în baza 10
u Întreg fără semn în baza 10
o Întreg fără semn în baza 8
x sau X Întreg fără semn în baza 16. Se folosesc literele a, b, c, d, e, f mici, respectiv mari
c Caracter
s Șir de caractere
f Real zecimal de forma [-]xxx.yyyyyy (implicit 6 cifre după virgulă)
e sau E Real zecimal în notație exponențială. Se folosește e mic, respectiv E mare
g La fel ca și e, E și f dar afișarea se face cu număr minim de cifre zecimale
Citirea cu format se realizează cu ajutorul funcției scanf() astfel:
scanf("format", &var_1, &var_2, ..., &var_n)
care citește valorile de la intrarea standard în formatul precizat și le depune în variabilele var_i, returnând
numarul de valori citite.
Atenție! Funcția scanf primește adresele variabilelor în care are loc citirea. Pentru tipuri fundamentale
și/sau structuri, aceasta se obține folosind operatorul de adresă – &.
Sintaxa descriptorului de format în acest caz este:
% [*] [ Lung ] [ l ] descriptor
Semnificația campurilor din descriptor este descrisă în tabelul următor:
Câmp Descriere
* Indică faptul că valoarea citită nu se atribuie unei variabile. (valoarea citită poate fi folosită pentru specificarea lungimii câmpului)
Lung Indică lungimea câmpului din care se face citirea. În cazul în care e nespecificat, citirea are loc până la primul caracter care nu face parte din număr, sau până la „n‟ (linie nouă/enter)
d Întreg în baza 10
o Întreg în baza 8
x Întreg în baza 16
f Real
c Caracter
s Șir de caractere
L Indică un întreg long sau un real double
h Indică un întreg short
Pentru scrierea și citirea unui singur caracter, biblioteca stdio.h mai definește și
funcțiile getchar() șiputchar():
getchar() are ca efect citirea cu ecou a unui caracter de la terminalul standard. Caracterele
introduse de la tastatură sunt puse într-o zonă tampon, până la acționarea tastei ENTER,
moment în care în zona tampon se introduce caracterul rând nou. Fiecare apel getchar() preia
următorul caracter din zona tampon.
putchar() afișează caracterul având codul ASCII egal cu valoarea expresiei parametru.
Nota: getchar() și putchar() nu sunt de fapt funcții, ci niște macroinstrucțiuni definite în stdio.h
Pentru citirea și scrierea unei linii biblioteca stdio.h definește funcțiile gets() și puts():
gets(zona) - introduce de la terminalul standard un șir de caractere terminat prin acționarea
tastei ENTER. Funcția are ca parametru adresa zonei de memorie în care se introduc caracterele
citite. Funcția returnează adresa de început a zonei de memorie; la întalnirea sfarșitului de fișier
(CTRL+Z) funcția returnează NULL.
puts(zona) – afișează la terminalul standard șirul de caractere din zona dată ca parametru, până
la caracterul null (), care va fi înlocuit prin caracterul linie nouă. Funcția returnează codul ultimului
caracter din șirul de caractere afișate sau -1 în caz de eroare.
Invata Limbajul de Programare C – Partea 2
Bine ati venit pe ItAssistant. Aceasta noua serie de tutoriale isi propune sa va familiarizeze cu limbajul
de programare C si conceptele Programarii Structurata.
Noţiuni teoretice – Tipuri fundamentale de date
Tipurile de date reprezintă tipul de informație care poate fi stocat într-o variabilă. Un tip de data definește
atât gama de valori pe care o poate lua o variabilă de un anume tip cât și operațiile care se pot efectua
asupra ei. În continuare sunt prezentate tipurile fundamentale ale limbajului C, împreună cu o scurtă
descriere a acestora:
char – reprezentat printr-un număr pe 8 biți (un byte), stochează un caracter, definit în C printr-un
număr în intervalul [-128; +127]. De observat că valorile pozitive codifică caracterele standard
ASCII
int - stochează numere întregi. Lungimea sa (și implicit plaja de valori) este dependentă de
compilator si sistemul de operare considerat. De obicei, pe Linux, int se reprezintă pe 32 de biți
(deci 4 bytes). În acest caz, poate memora numere între –2.147.483.648 și 2.147.483.647
float - reprezintă un număr real stocat în virgulă mobilă, simplă precizie (7 cifre), în gama de
valori 3.4E +/- 38(reprezentat pe 4 bytes)
double - reprezinta un număr real stocat în virgulă mobilă, dublă precizie (15 cifre), în gama de
valori 1.7E +/- 308 (reprezentat pe 8 bytes)
Acestor tipuri fundamentale li se mai pot adăuga un număr de calificatori, după cum urmează:
short - aplicabil doar pentru int, rezultând, de obicei, un întreg pe 2 octeți.
long – aplicabil doar pentru int. Rezultă un întreg pe 32 de biți (4 octeți), schimbare semnificativa
doar pentru Windows)
unsigned - precizează faptul că valoarea variabilei este pozitivă. Aplicabil doar tipurilor întregi.
În cazul în care este absolut necesar ca tipul întreg să aibă o anumită lungime, este indicată consultarea
cu atenție a documentației compilatorului. Compilatorul GCC pune în acest sens la dispoziția
programatorului, următoarele tipuri de întregi cu lungime clar specificată: uint_8, uint_16, uint_32, uint_64
– pentru întregi fară semn pe 8, 16, 32 respectiv 64 de biți int_8, int_16, int_32, int_64 – pentru întregi cu
semn reprezentați pe 8, 16, 32 respectiv 64 de biți.
Determinarea corectă a tipurilor de date care vor fi folosite este esențială pentru securitatea și buna
funcționare a aplicațiilor pe care le scrieți. În cazul în care valoarea conținută de o variabilă depașește
limitele impuse de tipul de date folosit, se produce așa-numit-ul over-flow care poate cauza erori aparent
inexplicabile. (Ca o anecdotă, în fiecare an (până acum trei sau patru ani), Bill Gates primea de la FISC o
scrisoare prin care era somat să iși platească taxele, deoarece apărea in evidențele lor ca având datorii
însemnate. Asta deoarece valoarea averii lui (mult peste 4.000.000.000$) producea un overflow în softul
folosit de către FISC. În final situația a fost soluționată, introducând un câmp special pentru el în softul
folosit. (A modifica softul peste tot ar fi introdus un plus de stocare nejustificat pentru fiecare din cei
aproximativ 300.000.000 de cetațeni ai SUA.) )
Operatori
Operatorii limbajului C pot fi unari, binari sau ternari, fiecare având o precedenţă şi o asociativitate bine
definite. Tabelul următor sintetizează operatorii limbajului C. Operatorii sunt prezentaţi în ordine
descrescătoare a priorităţii.
Precedenţă Operator Descriere Asociativitate
1
[] Indexare stanga-dreapta
. şi -> Selecţie membru (prin structură, respectiv pointer) stânga-dreapta
++ şi – Postincrementare/postdecrementare stânga-dreapta
2
! Negare logică dreapta-stânga
~ Complement faţă de 1 pe biţi dreapta-stânga
++ şi – Preincrementare/predecrementare dreapta-stânga
+ şi - + şi – unari dreapta-stânga
* Dereferenţiere dreapta-stânga
& Operator adresă dreapta-stânga
(tip) Conversie de tip dreapta-stânga
sizeof() Mărimea în octeţi dreapta-stânga
3
* Înmulţire stânga-dreapta
/ Împărţire stânga-dreapta
% Restul împărţirii stânga-dreapta
4 + şi - Adunare/scădere stânga-dreapta
5 << si >> Deplasare stânga/dreapta a biţilor stânga-dreapta
6
< Mai mic stânga-dreapta
<= Mai mic sau egal stânga-dreapta
> Mai mare stânga-dreapta
>= Mai mare sau egal stânga-dreapta
7 == Egal stânga-dreapta
!= Diferit stânga-dreapta
8 & ŞI pe biţi stanga-dreapta
9 ^ SAU-EXCLUSIV pe biţi stânga-dreapta
10 | SAU pe biţi stânga-dreapta
11 && ŞI logic stânga-dreapta
12 | | SAU logic stânga-dreapta
13 :? Operator condiţional dreapta-stânga
14
= Atribuire dreapta-stânga
+= şi -= Atribuire cu adunare/scădere dreapta-stânga
*= şi /= Atribuire cu multiplicare/împărţire dreapta-stânga
%= Atribuire cu modulo dreapta-stânga
&= si |= Atribuire cu ŞI/SAU dreapta-stânga
^= Atribuire cu SAU-EXCLUSIV dreapta-stânga
<<= şi >>= Atribuire cu deplasare de biţi dreapta-stânga
15 , Operator secvenţa stânga-dreapta
Trebuie avută în vedere precedenţa operatorilor pentru obţinerea rezultatelor scontate. Dacă unele tipuri
de precedenţă (cum ar fi cea a operatorilor artimetici) sunt evidente şi nu prezintă (aparent) probleme (şi
datorită folosirii lor dese), altele pot duce la erori greu de găsit. De exemplu, următorul fragment de cod
nu produce rezultatul dorit, deoarece:
if ( flags & MASK == 0)
{ ... }
se evaluează mai întai egalitatea care produce ca rezultat (0 pentru False, și 1 pentru True) după care se
aplică Și pe biți între falgs și 1.
Pentru a obţine rezultatul dorit se vor folosi parantezele:
if ( (flags & MASK) == 0)
{ ... }
acum mai întâi se va face ȘI pe biți între flags și MASK, după care se verifică egalitatea.
O expresie este o secventă de operanzi și operatori (validă din punct de vedere al sintaxei limbajului C)
care realizează una din funcțiile: calculul unei valori, desemnarea unui obiect (variabilă) sau funcţii sau
generarea unui efect lateral.
O altă greşeală frecventă este utilizarea greşită a operatorilor = şi ==. Primul reprezintă atribuire, al doilea
comparaţie de egalitate. Apar deseori erori ca:
if ( a = 2 )
{ ... }
Compilatorul consideră condiţia corectă, deoarece este o expresie validă în limbajul C care face atribuire,
care se evaluează mereu la o valoare nenulă.
Măsurarea timpului de execuție a programelor
Uneori este utilă măsurarea timpului de execuție a unei anumite parți a unui program sau chiar a
întregului program. În acest scop putem folosi funcția clock() din fișierul antet time.h. Această funcție
întoarce o aproximare a numărului de cicluri de ceas trecute de la pornirea programului. Pentru a obţine o
valoare în secunde, împărțim această valoare la constanta CLOCKS_PER_SEC. Funcţia are antetul:
clock_t clock( void );
Următorul fragment este un exemplu de utilizare a acestei funcții:
#include <stdio.h>
#include <time.h>
clock_t t_start, t_stop;
float seconds;
// Marcam momentul de inceput
t_start = clock();
// Executam operatia pentru care masuram timpul de executie
// [....]
// Marcam momentul de sfarsit
t_stop = clock();
seconds = ((float)(t_stop - t_start))/ CLOCKS_PER_SEC;
printf("Timp de executie: %.3f sec.\n", seconds);
Următorul fragment este un exemplu de funcție care are ca scop oprirea programului pentru un anumit
timp:
void wait ( int seconds )
{
clock_t endwait;
endwait = clock () + seconds * CLOCKS_PER_SEC ;
while (clock() < endwait) {}
}
Funcții matematice
Fișierul antet math.h conține un set de funcții matematice des utilizate în programe. Câteva dintre
acestea sunt:
Antet Descriere
double asin( double arg );
double acos( double arg ); Calculează arcsinusul/arccosinusul valorii arg; rezultatul este măsurat în radiani
double atan( double arg );
double atan2( double y,
double x ); Calculează arctangenta valorii arg, respectiv a fracției y/x
double floor( double num
); Întoarce cel mai mare întreg mai mic sau egal cu num (partea întreagă inferioară)
double ceil( double num ); Întoarce cel mai mic întreg mai mare sau egal cu num (partea întreagă superioară)
double sin( double arg );
double cos( double arg );
double tan( double arg );
Calculează sinusul/cosinusul/tangenta parametrului arg, considerată în radiani
double sinh( double arg );
double cosh( double arg );
double tanh( double arg );
Calculează sinusul/cosinusul/tangenta hiperbolică a parametrului arg
double exp( double arg ); Întoarce valoarea earg
double pow( double base,
double exp ); Întoarce valoarea basearg
double log( double num ); Calculează logaritmul natural (de bază e) al valorii arg
double log10( double num
); Calculează logaritmul în baza 10 al parametrului
double sqrt( double num ); Calculează radăcina pătrată a parametrului
double fmod( double x,
double y ); Întoarce restul împarțirii lui x la y
double fabs( double arg ); Întoarce valoarea absolută a lui arg
Generarea numerelor aleatoare
Valorile aleatoare (a căror valoare nu poate fi prezisă dinaintea rulării programului şi care diferă între 2
rulări) pot fi generate în C cu funcţia:
int rand( void );
care face parte din antetul stdlib.h. Această întoarce o valoare cuprinsă între 0 și RAND_MAX (valoare
care este dependenta de librariile folosite, dar care se garantează a fi minim 32767).
Numerele generate nu sunt cu adevărat aleatoare, ci pseudo-aleatoare; aceste numere sunt uniform
distribuite pe orice interval, dar șirul de numere aleatoare generate este dependent de prima valoare,
care trebuie aleasă de utilizator sau programator. Această valoare, numită seed, se selectează cu
funcţia:
void srand( unsigned int seed );
Cea mai întalnită utilizare a funcției de inițializare presupune setarea unui seed egal cu valoarea ceasului
sistemului de la pornirea programului, prin instrucțiunea:
srand( (unsigned) time( NULL ) );
d = rand(); //generează valori random.
Funcția time() din fişierul antet time.h întoarce numărul de secunde trecute de la ora 00:00, din data de 1
ianuarie 1970. Funcția primește şi un parametru de tip pointer, care reprezintă adresa unei variabile în
care se salvează valoarea returnată.
Invata Limbajul de Programare C – Partea 3
Bine ati venit pe ItAssistant. Aceasta noua serie de tutoriale isi propune sa va familiarizeze cu limbajul
de programare C si conceptele Programarii Structurata.
Instructiunile limbajului C
If-else
if…else este cea mai simplă instrucţiune condiţională. Poate fi folosită în mai multe forme:
if( condiţie )
{
// instrucţiuni
//...
}
if( condiţie )
{
//instrucţiuni
//...
}
else
{
//alte instrucţiuni
//...
}
if( condiţie1 )
{
...
}
else if( condiţie2 )
{
...
}
...
else if( condiţieN )
{
...
}
Instrucţiunea evaluează expresia condiţie şi execută instrucţiunile dintre acolade doar dacă rezultatul
este nenul. În varianta cu else, pentru rezultat nul este executat blocul de instrucţiuni aflat după else.
În al treilea caz, sunt evaluate pe rând condiţiile şi este executat blocul corespunzător primei condiţii
adevărate. Un exemplu de folosire este:
if( a == b )
printf( "Numerele sunt egale" );
else
if( a > b )
printf( "A este mai mare" )
Switch
Switch este o instrucţiune menită să simplifice structurile condiţionale cu mai multe condiţii.
switch( expresie )
{
case constanta1:
//instrucţiuni1
case constanta2:
//instrucţiuni2
...
default:
//instrucţiuni
}
Valoarea expresie este evaluată la un tip intreg, apoi această valoare este comparată cu fiecare
constantă; este rulat blocul de instrucţiuni al valorii găsite. În caz ca numărul nu este egal cu nici una
dintre constante, este executat blocul aflat după default.
int main()
{
char c;
printf("Alegeţi o opţiune:\n\t[a] afişare\n\t[s] ştergere\n\t[e] ieşire\n");
scanf("%c", &c);
printf("Aţi ales: ");
switch(c)
{
case 'a':
printf("afişare");
break;
case 's':
printf("ştergere");
break;
case 'e':
printf("ieşire");
break;
default:
printf("O opţiune inexistentă");
break;
}
return 0;
}
int main( )
{
int n, n2;
printf( "Introduceţi o valoare între 0 şi 5:" );
scanf( "%d", &n );
n2 = 1;
switch( n )
{
case 5:
n2 *= 2;
/* fără break, continuă la următoarea instrucţiune */
case 4:
n2 *= 2;
case 3:
n2 *= 2;
case 2:
n2 *= 2;
case 1:
n2 *= 2;
case 0:
printf( "2 la puterea %d este %d\n", n, n2 );
break;
default:
printf( "Valoare invalidă\n" );
}
return 0;
}
Instrucţiuni de repetiţie
while
while execută un bloc de instrucţiuni atâta timp cât o anumită condiţie este adevărată. Forma generală a
unui ciclu while este:
while (expresie)
{
//instrucţiuni
}
Câtă vreme expresie are o valoare nenulă, instrucţiunile din blocul de după while sunt executate.
Expresia este reevaluată după fiecare ciclu. Un astfel de ciclu poate să se execute o dată, de mai multe
ori sau niciodată, în funcţie de valoarea la care se evaluează expresia.
do … while
do … while este o instrucţiune repetitivă similara cu cea precedentă, singura diferenţa fiind că expresia
este evaluată după executarea instrucţiunilor, nu înainte. Astfel, blocul va fi executat cel puţin o dată.
do
{
//instrucţiuni
} while (expresie);
for
for reprezintă o formă mai simplă de a scrie un while însotit de o expresie iniţiala şi de o expresie de
incrementare. Forma sa este:
for( expresie1 ; expresie2 ; expresie3 )
{
//instrucţiuni
}
Secvenţa de cod de mai sus este echivalentă cu:
expresie1
while (expresie2)
{
instrucţiuni
expresie3
}
În cazul instrucţiunii for, oricare dintre cele 3 expresii poate lipsi. Lipsa expresiei condiţionale este
echivalentă cu o buclă infinită, cum ar fi:
for( ; ; )
{
/* instrucţiunile de aici sunt intr-o buclă infinită */
}
Exemplul următor prezintă un ciclu cu funcţionalitate identică (tipărirea primelor 10 numere naturale),
folosind cele 3 instrucţiuni repetitive:
int main()
{
short i;
printf("Ciclu for\n");
for (i=1;i<=10;i++)
printf("i=%d\n", i);
printf("Ciclu while\n");
i=1;
while (i <= 10)
{
printf("i=%d\n", i);
i++;
}
printf("Ciclu do while\n");
i=0;
do
{
i++;
printf("i=%d\n", i);
} while(i < 10);
}
În exemplu nu am mai pus acolade la instrucţiunea executată de for. Pentru blocuri de o singură
instrucţiune, nu este nevoie sa folosim acoladele.
Instrucţiuni speciale
break
break, pe lângă utilizarea descrisă la instrucţiunea switch, poate fi folosită pentru a ieşi forţat dintr-o
instrucţiune de repetiţie. Secventa următoare este echivalentă cu cele de mai sus:
i = 0;
for( ; ; )
{
i++;
if( i > 10 )
break; /* ieşire forţată din bucla */
printf( “i=%d\n”, i );
}
continue
continue forţează terminarea iteraţiei curente a buclei si trecerea la iteraţia următoare. În cazul
instrucţiuniifor, acest lucru presupune executarea instrucţiunii de incrementare; apoi se evaluează
condiţia de continuare a buclei. Exemplul următor demonstrează implementarea unei bucle infinite cu
ajutorul instrucţiunii continue:
for( i = 0 ; i < 10 ; )
{
if( i == 0 )
continue;
i++;
}
return
return este instrucţiunea de terminare a funcţiei curente. Aceasta poate fi apelată in forma return; în cazul
funcţiilor care returnează void şi în forma return rezultat; pentru funcţiile care întorc o valoare.
goto
goto este o instrucţiune de salt a execuţiei. Instrucţiunea primeşte ca parametru o etichetă; următoarea
instrucţiune executată după goto este cea de la eticheta dată.
int main( )
{
goto et;
printf( “Asta nu apare la executie\n” );
et:
printf( “Asta apare la rulare\n” );
return 0;
}
Atenţie! În majoritatea cazurilor, utilizarea instrucţiunii goto nu este recomandată şi poate fi evitată
folosind alte instrucţiuni de control şi funcţii. Programele care folosesc această instrucţiune pentru a sări
între secvenţe îndepărtate de cod sunt dificil de depanat şi analizat.
Invata Limbajul de Programare C – Partea 4
Bine ati venit pe ItAssistant. Aceasta noua serie de tutoriale isi propune sa va familiarizeze cu limbajul
de programare C si conceptele Programarii Structurata.
Programare modulară. Funcţii în limbajul C. Dezvoltarea algoritmilor folosind funcţii
Funcţiile împart taskuri complexe în bucăţi mici mai uşor de înţeles şi de programat. Acestea pot fi
refolosite cu alte ocazii, în loc să fie rescrise de la zero. De asemenea, funcţiile sunt utile pentru a
ascunde detalii de funcţionare ale anumitor părţi ale programului, ajutând la modul de lucru al acestuia.
Utilizând funcţii, care reprezintă unitatea fundamentală de execuţie a programelor C, se obţine o divizare
logică a programelor mari şi complexe.
Împărţirea programelor în funcţii este arbitrară şi depinde de modul de gândire a celui care le creează. De
obicei, funcţiile cuprind o serie de instrucţiuni care efectuează un calcul, realizează o acţiune,
implementează un algoritm, etc. Crearea funcţiilor trebuie să se bazeze pe următoarele
principii: claritate, lizibilitate, uşurinţă în întreţinere, reutilizabilitate.
Definirea şi apelul unei funcţii în C
Caracteristicile definitorii ale unei funcţii în C sunt: numele, parametrii de apel şi valorea returnată.
Sintaxa standard de declarare a unei funcţii este:
tip_returnat nume_functie (tip_param1 [nume_param1] [, tip_param2 [nume_param2],
...]);
Această declarare poartă numele de antetul funcţiei.
Odată declarată, o funcţie trebuie definită, în sensul că trebuie expandat corpul acesteia cu instrucţiunile
pe care trebuie să le execute.
Definirea unei funcţii are forma:
tip_returnat nume_functie (tip_param1 [nume_param1] [, tip_param2 [nume_param2], ...])
{
declaratii de variabile si instructiuni
return expresie;
}
Limbajul C permite separarea declaraţiei unei funcţii de definiţia acesteia (codul care o implementează).
Pentru ca funcţia să poată fi folosită, este obligatorie doar declararea acesteia înainte de codul care o
apelează. Definiţia poate apărea mai departe în fişierul sursă, sau chiar într-un alt fişier sursă sau
bibliotecă.
Diferite părţi din definirea unei funcţii pot lipsi. Astfel, o funcţie minimală este:
dummy() {}
Funcţia de mai sus nu face absolut nimic, nu întoarce nici o valoare şi nu primeşte nici un argument, însă
din punct de vedere al limbajului C este perfect validă.
Tipul returnat de o funcţie poate fi orice tip standard sau definit de utilizator. Dacă nu se specifică tipul
returnat de funcţie se consideră implicit tipul int.
Orice funcţie care întoare un rezultat trebuie să conţină instrucţiunea:
return expresie;
Expresia este evaluată şi convertită la tipul de date care trebuie returnat de funcţie. Această instrucţiune
termină şi execuţia funcţiei, indiferent dacă după aceasta mai urmează sau nu alte instrucţiuni. Dacă este
cazul, se pot folosi mai multe instrucţiuni return pentru a determina mai multe puncte de ieşire din funcţie,
în raport cu evoluţia funcţiei.
Exemplu:
int min(int x, int y)
{
if(x<y)
return x;
return y;
}
Apelul unei funcţii se face specificând parametrii efectivi (parametrii care apar în declararea funcţiei se
numesc parametri formali).
int main()
{
int a, b, minim;
//...........
x=2;
y=5;
minim=min(x,4);
printf("Minimul dintre %d si 4 este: %d",x,minim);
printf("Minimul dintre %d si %d este: %d",x,y,min(x,y));
}
Transmiterea parametrilor
Apelul unei funcţii se face specificând parametrii care se transmit acesteia. În limbajul C, dar şi în alte
limbaje de programare există două moduri de transmitere a parametrilor.
Transmiterea parametrilor prin valoare
Funcţia va lucra cu o copie a variabilei pe care a primit-o şi orice modificare din cadrul funcţiei va opera
asupra aceste copii. La sfârşitul execuţiei funcţiei, copia va fi distrusă şi astfel se va pierde orice
modificare efectuată. Pentru a nu pierde modificările făcute se foloseşte instrucţiunea return, care poate
întoarce, la terminarea funcţiei, noua valoare a variabilei. Problema apare în cazul în care funcţia
modifică mai multe variabile şi se doreşte ca rezultatul lor să fie disponibil şi la terminarea execuţiei
funcţiei.
Transmiterea parametrilor prin referinţă
Rezolvarea problemei anterioare se face transmiţând funcţiei adresa variabilei cu care urmează să
lucreze. Astfel, funcţia nu prelucrează o copie a valorii variabilei, ci chiar valoarea aflată la adresa
furnizată. Orice parametru care se doreşte a fi prelucrat de funcţie, şi noua sa valoare utilizată chiar şi
după terminarea funcţiei, se va transmite funcţiei prin referinţă.
Exemplu de transmitere a parametrilor prin valoare:
min(x,4)
Exemplu de transmitere a parametrilor prin referinţă:
suma(&x,&y,&sum)
Funcţii recursive
O funcţie poate să apeleze la rândul ei alte funcţii. Dacă o funcţie se apelează pe sine însăşi, atunci
funcţia este recursivă. Pentru a evita un număr infinit de apeluri recursive, trebuie ca funcţia să includă în
corpul ei ocondiţie de oprire, astfel ca, la un moment dat, recurenţa să se oprească şi să se revină
succesiv din apeluri. Condiţia trebuie să fie una generică, şi să oprească recurenţa în orice situaţie.
Această condiţie se referă în general la parametrii de intrare, pentru care la un anumit moment, răspunsul
poate fi returnat direct, fără a mai fi necesar un apel recursiv suplimentar.
Exemplu: Calculul recursiv al factorialului
long fact(int n)
{
if(n==0) return 1;
else return n*fact(n-1);
}
sau, într-o formă mai compactă:
long fact(int n)
{
return (n>=1) ? n*fact(n-1) : 1;
}
ntotdeauna trebuie avut grijă în lucrul cu funcţii recursive deoarece, la fiecare apel recursiv, contextul este
salvat pe stivă pentru a putea fi refăcut la revenirea din recursivitate. În acest fel, în funcţie de numărul
apelurilor recursive şi de dimensiunea contextului (variabile, descriptori de fişier, etc.) stiva se poate
umple foarte rapid, generând o eroare de tip stack overflow.
Funcţia main
Orice program C conţine cel puţin o funcţie, şi anume cea principală, numită main(). Aceasta are un
format special de definire:
int main(int argc, char **argv);
Primul parametru, argc, reprezintă numărul de argumente primite de către program la linia de comandă,
incluzând numele cu care a fost apelat programul. Al doilea parametru, argv, este un pointer către
conţinutul listei de parametri al căror număr este dat de argc.
Atunci când nu este necesară procesarea parametrilor de la linia de comandă, se poate folosi forma
prescurtată a definiţiei funcţiei main, şi anume:
int main();
În ambele cazuri, standardele impun ca main să întoarcă o valoare de tip întreg, care să reprezinte codul
execuţiei programului şi care va fi pasată înapoi sistemului de operare, la încheierea execuţiei
programului. Astfel, instrucţiunea return în funcţia main va însemna şi terminarea execuţiei programului.
În mod normal, orice program care se execută corect va întoarce 0, şi o valoare diferită de 0 în cazul în
care apar erori. Aceste coduri ar trebui documentate pentru ca apelantul programului să ştie cum să
adreseze eroarea respectivă.
Tipul de date void
Tipul de date void are mai multe întrebuinţări.
Atunci când este folosit ca tip returnat de o funcţie, specifică faptul că funcţia nu întoarce nici o valoare.
Exemplu:
void print_nr(int numar)
{
printf("Numarul este %d", numar);
}
Atunci când este folosit în declaraţia unei funcţii, void semnifică faptul că funcţia nu primeşte nici un
parametru. Exemplu:
int init(void)
{
return 1;
}
Pointerii pot fi de asemenea declaraţi void, însă nu pot fi dereferenţiaţi fără o operaţie de cast explicit.
Clase de stocare. Fişiere antet vs. biblioteci
După cum se ştie, într-un fişier sursă (.c) pot fi definite un număr oarecare de funcţii. În momentul în care
programul este compilat, din fiecare fişier sursă se generează un fişier obiect (.o), care conţine codul
compilat al funcţiilor respective. Aceste funcţii pot apela la rândul lor alte funcţii, care pot fi definite în
acelaşi fişier sursă, sau în alt fişier sursă. În orice caz, compilatorul nu are nevoie să ştie care este
definiţia funcţiilor apelate, ci numai semnătura acestora (cu alte cuvinte, declaraţia lor), pentru a şti cum
să realizeze instrucţiunile de apel din fişierul obiect. Acest lucru explică de ce, pentru a putea folosi o
funcţie, trebuie declarată înaintea codului în care este folosită.
Fişierele antet conţin o colecţie de declaraţii de funcţii, grupate după funcţionalitatea pe care acestea o
oferă. Atunci când includem un fişier antet (.h) într-un fişier sursă (.c), compilatorul va cunoaşte toate
semnăturile funcţiilor de care are nevoie, şi va fi în stare să genereze codul obiect pentru fiecare fişier
sursă în parte. (NOTĂ: Astfel nu are sens includerea unui fişier .c în alt fişier .c; se vor genera două
fişiere obiect care vor conţine definiţii comune, şi astfel va apărea un conflict de nume la editarea
legăturilor).
Cu toate acestea, pentru a realiza un fişier executabil, trebuie ca fiecare funcţie să fie definită. Acest lucru
este realizat de către editorul de legături; cu alte cuvinte, fiecare funcţie folosită în program trebuie să fie
conţinută în fişierul executabil. Acesta caută în fişierele obiect ale programului definiţiile funcţiilor de care
are nevoie fiecare funcţie care le apelează, şi construieşte un singur fişier executabil care conţine toate
aceste informaţii. Bibliotecile sunt fişiere obiect speciale, al căror unic scop este să conţină definiţiile
funcţiilor oferite de către compilator, pentru a fi integrate în executabil de către editorul de legături.
Clasele de stocare intervin în acest pas al editării de legături. O clasă de stocare aplicată unei funcţii
indică dacă funcţia respectivă poate fi folosită şi de către alte fişiere obiect (adică este externă), sau
numai în cadrul fişierului obiect generat din fişierul sursă în care este definită (în acest caz funcţia este
statică). Dacă nu este specificată nici o clasă de stocare, o funcţie este implicit externă.
Cuvintele cheie extern şi static, puse în faţa definiţiei funcţiei, îi specifică clasa de stocare. De exemplu,
pentru a defini o funcţie internă, se poate scrie:
static int compute_internally(int, int)
Funcţia compute_internally nu va putea fi folosită decât de către funcţiile definite în acelaşi fişier sursă şi
nu va fi vizibilă de către alte fişiere sursă, în momentul editării legăturilor.
nvata Limbajul de Programare C – Partea 5
Bine ati venit pe ItAssistant. Aceasta noua serie de tutoriale isi propune sa va familiarizeze cu limbajul
de programare C si conceptele Programarii Structurata.
Tablouri. Particularizare – vectori.
Vectori
Printr-un vector se înţelege o colecţie liniară şi omogenă de date. Un vector este liniar pentru că
datele(elementele) pot fi accesate în mod unic printr-un index. Un vector este, de asemenea, omogen,
pentru că toate elementele sunt de acelaşi tip. În limbajul C, indexul este un număr întreg pozitiv şi
indexarea se face începând cu 0.
Declaraţia unei variabile de tip vector se face în felul următor:
<tip_elemente> <nume_vector>[<dimensiune>];
De exemplu, avem următoarele declaraţii de vectori:
int a[100];
float vect [50];
#define MAX 100
...
unsigned long numere[MAX
Este de remarcat că vectorul este o structură statică: dimensiunea acestuia trebuie să fie o constantă la
compilare şi nu poate fi modificată în cursul execuţiei programului. Astfel, programatorul trebuie să
estimeze o dimensiune maximă pentru vector, şi aceasta va fi o limitare a programului.
De obicei, se folosesc constante simbolice (ca în ultimul exemplu) pentru aceste dimensiuni maxime,
pentru ca ele să poată fi ajustate uşor la nevoie. De asemenea, în cadrul unei declaraţii, se pot iniţializa
cu valori constante componente ale vectorului, iar în acest caz, dimensiunea vectorului poate rămâne
neprecizată (compilatorul o va determina din numărul elementelor din listă).
De exemplu:
int a[3] = {1, 5, 6}; /* Toate cele 3 elemente sunt initializate */
float num[] = {1.5, 2.3, 0.2, -1.3}; /* Compilatorul determina dimensiunea - 4 - a vectorului
*/
unsigned short vect[1000] = {0, 2, 4, 6}; /* Sunt initializate doar primele 4 elemente */
În cazul special în care specificăm dimensiunea şi doar un singur element la initializare, primul element
va fi cel specificat, iar toate celelalte elemente ale vectorului vor fi iniţializate la 0:
char sir[100] = {97}; /* Sirul va fi initializat cu: 97 (caracterul 'a') pe prima poziţie şi 99 de 0
*/
Este important de remarcat faptul că elementele neiniţializate pot avea valori oarecare. La alocarea unui
vector, compilatorul nu efectuează nici un fel de iniţializare şi nu furnizează nici un mesaj de eroare dacă
un element este folosit înainte de a fi iniţializat. Un program corect va iniţializa, în orice caz, fiecare
element înainte de a-l folosi. Elementele se accesează prin expresii de
forma <nume_vector>[<indice>].
De exemplu, putem avea:
char vect[100];
int i = 90;
vect[0] = 1;
vect[5] = 10;
vect[i] = 15;
vect[i+1] = 20;
Stil de programare. Exemple de programe
Citirea unui vector de intregi de la tastatura:
int main()
{
int a[100],n,i; /* vectorul a are maxim 100 de intregi */
scanf ("%d",&n); /* citeste nr de elemente vector */
for (i=0; i<n; i++)
scanf ("%d", &a[i]); /* citire elemente vector */
for (i=0;i<n;i++)
printf ("%d ", a[i]); /* scrie elemente vector */
return 0;
}
Generarea unui vector cu primele n numere Fibonacci:
#include <stdio.h>
int main ()
{
long fib[100]={1,1};
int n,i;
printf ("n=");
scanf("%d",&n);
for (i=2; i<n ;i++)
fib[i]=fib[i-1]+fib[i-2];
for (i=0; i<n;i++)
printf ("%ld ",fib[i]);
return 0;
}
Erori comune
Depăşirea limitelor indicilor (index out of bounds) este o eroare frecventă, ce poate duce la
blocarea programului sau a sistemului şi poate fi evitată prin verificarea încadrării în intervalul
valid.
Indici folosiţi greşit în bucle imbricate (index cross-talk). Sunt multe cazuri în care pe un nivel al
buclei se foloseşte, de exemplu vect[i], şi pe nivelul imbricat vect[j], când de fapt se dorea
folosirea lui i. Mare atenţie şi în astfel de cazuri!
#define MAX 100
int vect[MAX]
va fi de preferat în locul lui
int vect[100]
Sfat: Verificaţi că indicii se încadrează între marginile superioară şi inferioară a intervalului de valori
valide. Acest lucru trebuie în general făcut în cazul în care datele provin dintr-o sursă externă: citite de la
tastatură sau pasate ca parametri efectivi unei funcţii, de exemplu.
Exemplu:
// program care citeşte un index şi o valoare, şi atribuie valoarea elementului din vector care se găseşte la poziţia
respectivă
#include <stdio.h>
int main()
{
int i, val;
int v[10];
scanf("%d %d",&i,&val);
// !!! Verific daca indexul este valid
if(i >=0 && i < 10)
v[i] = val;
else
{
printf("Introduceti un index >= 0 si < 10");
}
return 0;
}
Sfat: Folosiţi comentarii pentru a explica ce reprezintă diverse variabile. Acest lucru vă va ajuta atât pe
voi să nu încurcaţi indici, de exemplu, cât şi pe ceilalţi care folosesc sau extind codul vostru.
Exemplu:
#include <stdio.h>
#define N 100
int main()
{
int v[N];
int i,j; // indecsii elementelor ce vor fi interschimbate
int aux; // variabila ajutatoare pentru interschimbare
... // initializari
/* Interschimb */
aux = v[i];
v[i] = v[j];
v[j] = aux;
return 0;
}
Aplicaţii cu vectori
Căutări
Căutare secvenţială
Când avem de a face cu un vector nesortat (şi nu numai în acest caz), cea mai simplă abordare pentru a
găsi o valoare, este căutarea secvenţială. Cu alte cuvinte, se compară, la rând, fiecare valoare din vector
cu valoarea căutată. Dacă valoarea a fost găsită, căutarea se poate opri (nu mai are sens să parcugem
vectorul până la capăt, dacă nu se cere acest lucru explicit).
Exemplu:
#define MAX 100
...
int v[MAX],x,i;
/*initializari*/
...
for(i=0;i<MAX;i++)
if(x==v[i])
{
printf("Valoarea %d a fost gasita in vector\n",x);
break;
}
printf("Valoarea %d nu a fost gasita in vector\n",x);
...
Căutare binară iterativă
Dacă vectorul pe care se face căutarea este sortat, algoritmul mai eficient de folosit în acest caz este
căutarea binară. Presupunem că vectorul este sortat crescător (pentru vectori sortaţi descrescător,
raţionamentul este similar). Valoarea căutată, x, se compară cu valoarea cu indexul N/2 din vector, unde
N este numărul de elemente. Dacă x este mai mic decât valoarea din vector, se caută în prima jumătate a
vectorului, iar dacă este mai mare, în cea de-a doua jumătate.
Căutarea în una dintre cele două jumătăţi se face după acelaşi algoritm. Conceptual, căutarea binară
este un algoritm recursiv, dar poate fi implementat la fel de bine într-un mod iterativ, folosind indecşii
corespunzători bucăţii din vector în care se face căutarea. Aceşti indecşi se modifică pe parcursul
algoritmului, într-o buclă, în funcţie de comparaţiile făcute.
Evoluţia algoritmului este ilustrată în imaginea de mai jos.
Pseudocodul pentru căutarea binară:
căutare_binară (v[0..N], x)
{
low = 0
high = N - 1
cât timp (low <= high)
{
mid = (low + high) / 2
dacă v[mid] > value
high = mid - 1
altfel
dacă v[mid] < value
low = mid + 1
altfel
găsit x pe poziţia mid
}
x nu a fost găsit
}
Sortări
Bubble Sort
Metoda bulelor este cea mai simplă modalitate de sortare a unui vector, dar şi cea mai ineficientă. Ea
funcţionează pe principiul parcurgerii vectorului şi comparării elementului curent cu elementul următor.
Dacă cele două nu respectă ordinea, sunt interschimbate. Această parcurgere este repetată de suficiente
ori până când nu mai există nici o interschimbare în vector.
Sortarea prin selecţie
Sortarea prin selecţie oferă unele îmbunătăţiri în ceea ce priveşte complexitatea, însă este departe de a fi
considerat un algoritm eficient. Presupunând că se doreşte sortarea crescătoare a vectorului, se caută
minimul din vector, şi se interschimbă cu primul element – cel cu indexul 0. Apoi se reia acelaşi procedeu
pentru restul vectorului.
Motivul pentru care algoritmul de sortare prin selecţie este mai eficient este acela că vectorul în care se
caută minimul devine din ce în ce mai mic, şi, evident, căutarea se face mai repede la fiecare pas.
nvata Limbajul de Programare C – Partea 6
Bine ati venit pe ItAssistant. Aceasta noua serie de tutoriale isi propune sa va familiarizeze cu limbajul de
programare C si conceptele Programarii Structurata.
Matrice. Operaţii cu matrice: adunare, înmulţire. Reprezentarea în memorie.
Matrice
Matricea este o colecţie omogenă şi bidimensională de elemente. Acestea pot fi accesate prin intermediul
a doi indici, numerotaţi, ca şi în cazul vectorilor, începand de la 0. Declaraţia unei matrice este de forma:
<tip_elemente> <nume_matrice>[<dim_1>][<dim_2>];
De exemplu, avem:
int mat[5][10]
#define MAX_ELEM 100
float a[MAX_ELEM][MAX_ELEM]
Numărul de elemente ale unei matrice va fi dim_1*dim_2, şi semnificaţia fiecărei dimensiuni este o
chestiune ce ţine de logica programului. În matematică, prima dimensiune poate să însemne linia şi a
doua coloana pentru fiecare element, însa acest lucru nu este obligatoriu. Este necesar totuşi, pentru
funcţionarea corectă a programului, să se respecte semnificaţiile alese pe întreg parcursul codului sursă.
Tablouri multidimensionale
Vectorii şi matricele se pot extrapola la noţiunea generală de tablou cu mai multe dimensiuni, care se
declară în modul următor:
<tip_elemente> <nume_tablou>[<dim_1>][<dim_2>]...[<dim_n>];
De exemplu:
int cub[3][3][3]
Deşi, în cazul a mai mult de 3 dimensiuni, tablourile pot să nu mai aibă sens concret sau fizic, acestea
pot fi deosebit de utile în multe situaţii.
Adunarea si înmulţirea matricelor
Suma matricelor:
Exemplu:
Înmulţirea matricelor:
Exemplu:
Exemplul de mai sus prezintă cum se calculează valorile (1,2) si (3,3) ale AB daca A este o matrice 3×2,
si B o matrice 2×3. Pentru calculul unui element din matrice se consideră o linie respectiv o coloană din
fiecare matrice conform săgeţilor. Elementele din acestea sunt înmulţite câte 2 conform înmulţirii pe
vectori, apoi suma produselor constituie elementul din matricea finală.
Reprezentarea în memorie
Cunoaşterea reprezentării în memorie a tablourilor vă ajută să înţelegeţi mai bine cum se lucrează cu
aceste tipuri de date şi să evitaţi atât erorile comune, cât şi pe cele mai subtile. Aşa cum se ştie, fiecare
variabilă are asociata o anumită adresă în memorie şi ocupă o anumită lungime, măsurată în octeţi.
Standardul C impune ca un tablou să fie memorat într-o zonă continuă de memorie, astfel ca pentru un
tabloul de forma: T tab[dim1][dim2]…[dimn]; dimensiunea ocupată în memorie va
fi sizeof(T)*dim1*dim2*…*dimn.
Vom considera în continuare cazul particular al unui vector vect de lungime n, şi al unui element oarecare
al acestuia, de pe pozitia i. Atunci când întalneşte numele vect, compilatorul va intelege “adresa în
memorie de la care începe vectorul vect”. Operatorul de indexare [] aplicat numelui vect instruieşte
compilatorul să “evalueze acel element de tipul T, care se află pe pozitia i în vectorul care începe de la
adresa vect”. Acest lucru se poate exprima direct: “evaluarea variabilei de tip T de la adresa vect + i *
sizeof(T)“.
În ultima formulare observaţi ca nu mai intervine sub nici o formă dimensiunea vectorului dată la
declarare. Aceea a fost necesară doar compilatorului, ca sa ştie câtă memorie să aloce pentru
reprezentarea acestuia.
De asemenea, observaţi că sunt permise indexari în afara spaţiului de memorie alocat, şi astfel
programul va putea, din greşeala, accesa alte zone de memorie, lucru care poate avea repercursiuni
grave.
În cel mai bun caz programul nostru se va comporta foarte ciudat (erori în locuri total imprevizibile), şi în
cel mai rău caz întreg sistemul va fi blocat (în cazul sistemelor care nu au implementate spaţii virtuale de
memorie proprii fiecărei aplicaţii – platformele Windows NT si Linux).
Faptul că graniţa dintre vectori şi adrese de memorie este atât de fină în limbajul C, sintaxa acestuia
permite expresii ciudate, de forma:
char a[100];
a[0] = 1;
3[a] = 5
Instrucţiunea din urmă înseamna pur şi simplu “asignează 5 variabilei de tip char de la adresa 3 + a *
sizeof(char) = 3 + a“. Observaţi că aceasta este echivalentă cu a[3] = 5;
De asemenea, un alt avantaj apare la definirea unui parametru al unei funcţii, de tip vector, caz în care nu
este necesară precizarea dimensiunii acestuia: void sort(int[] vect, n);
Este de remarcat faptul că pentru tablouri de dimensiuni m > 1, este necesară precizarea lungimilor
primelorm – 1 dimensiuni, pentru ca compilatorul să poată calcula adresa fiecărui element atunci când
acesta este referit în program.
Stil de programare
Declararea unei matrici unitate:
#define M 20 /* nr maxim de linii si de coloane */
int main ()
{
float unit[M][M];
int i,j,n;
printf("nr.linii/coloane: ");
scanf("%d",&n);
if (n > M)
return;
for (i=0;i<n;i++)
for (j=0;j<n;j++)
if (i !=j)
unit[i][j]=0;
else
unit[i][j]=1;
return 0;
}
Citire/scriere de matrice de reali:
int main ()
{
int nl,nc,i,j; float a[20][20];
/* Citire de matrice */
printf("nr.linii: ");
scanf("%d",&nl);
printf("nr.coloane: ");
scanf("%d",&nc);
if (nl >20 || nc >20)
{
printf("Eroare: dimensiuni > 20 \n");
return;
}
for (i=0;i<nl;i++)
for (j=0;j<nc;j++)
scanf("%f", &a[i][j]);
/* Afisare matrice */
for (i=0;i<nl;i++)
{
for (j=0;j<nc;j++)
printf("%f ",a[i][j]);
printf ("\n");
}
return 0;
}
Erori comune
Inversarea indicilor pentru elementele unei matrice sau tablou. E usor sa-l inversezi pe i cu j in expresia
A[i][j] astfel ca trebuie sa fiti atenti cand scrieti astfel de cod. Luati in considerare si folosirea de nume mai
sugestive pentru variabile.
Invata Limbajul de Programare C – Partea 7
Bine ati venit pe ItAssistant. Aceasta noua serie de tutoriale isi propune sa va familiarizeze cu limbajul de
programare C si conceptele Programarii Structurata.
Pointeri. Abordarea lucrului cu tablouri folosind pointeri.
Noţiunea de pointer
Un pointer este o variabilă care reţine o adresă de memorie. În C, aceste adrese de memorie pot fi de
mai multe feluri:
Adresa unor date de un anumit tip (tip elementar, structură, şir de caractere, etc.). În acest caz,
operaţiile cu pointeri sunt determinate de dimensiunea tipului de date (numărul de bytes ocupaţi
în memorie).
Adresa unei funcţii (adresa la care punctul curent de execuţie va sări, în cazul în care acea
funcţie este apelată).
Adresa unei adrese de memorie; acest tip de pointer poate fi redus la prima situaţie, în cazul în
care se consideră pointerul un tip de date numeric de lungime 32 de biţi (cantitatea maximă
adresabilă de memorie pentru programul nostru este de 4 GB, pentru arhitecturi pe 32 de biţi).
Adresa unei zone cu conţinut necunoscut (pointer către void); de exemplu, putem avea operaţii
cu blocuri mari de memorie, al cărui conţinut nu ne interesează (de exemplu citirea în memorie a
unui fişier, sau folosirea memoriei pe post de buffer) şi în acest caz adresele blocurilor sunt
reprezentate prin pointeri către void.
Notă:
Dimensiunea unui pointer nu este întotdeauna de 32 de biţi, ci depinde de arhitectura şi sistemul de
operare pe care programul este compilat. Pe sisteme pe 64 de biţi, un pointer va avea 64 de biţi. De
asemenea, dimensiunea în memorie pentru o variabilă de tip pointer nu este în mod necesar egală cu
dimensiunea unui tip de date întreg. Cu alte cuvinte, sizeof(void*) nu este in mod necesar egal cu
sizeof(int) (deşi sunt egale în cele mai multe situaţii), şi de aceea nu trebuie să vă bazaţi pe acest fapt în
programele voastre.
În lucrul cu pointeri sunt folosiţi asupra variabilelor doi operatori: operatorul * (de dereferenţiere) şi
operatorul & (de referenţiere). Amândoi apar în faţa variabilei asupra căreia acţionează şi au efecte
complementare.
Operatorul * este aplicat unei variabile de tip pointer şi are funcţia de a obţine valoarea stocată la adresa
respectivă. Utilizat în declaraţia unei variabile, acest operator are funcţia de a specifica faptul că avem
de-a face cu o variabilă pointer la tipul respectiv. Operatorul & este aplicat unei variabile de un anumit
tip şi obţine adresa în memorie a variabilei respective.
Atenţie!
Când este declarat un pointer, nu este alocată şi zona de memorie în care să fie stocată valoarea către
care indică pointerul respectiv! Întotdeauna când folosiţi (dereferenţiaţi) un pointer, trebuie ca acesta să
puncteze către o zona validă de memorie a programului vostru. Acesta poate fi, de exemplu, adresa unei
variabile declarate în prealabil, sau adresa unui bloc de memorie alocat dinamic (după cum vom vedea
mai departe).
De asemenea, un pointer poate fi iniţializat la constanta NULL (valoarea 0), compatibilă cu orice tip de
pointer, şi care indică, prin convenţie, un pointer neiniţializat. Utilizarea unui pointer asignat la o adresă
oarecare (invalidă), poate avea consecinţe imprevizibile la executia programului, de la comportamente
ciudate la blocarea sistemului!
Exemplu:
int *a; // Pointer
int b = 5; // Variabila
char *c; // Pointer catre un caracter (sau sir de caractere) void *buff = NULL; // Pointer catre void, initializat la NULL *a = 1; // Asignare INVALIDA; a nu este initializat la o adresa de memorie
a = &b; // Asignare valida; a ia adresa variabilei b *a = 5; // Asignare valida; continutul memoriei de la adresa a (care a fost initializata mai sus) ia
valoarea 5 // Acest lucru este echivalent cu "b = 5;"
Atenţie!
În cazul declaraţiilor de pointeri, operatorul * este asociat numelui variabilei, şi nu numelui tipului, astfel
că, pentru o declaraţie de mai multe variabile, operatorul * trebuie să apară pentru fiecare variabilă în
parte şi este recomandat ca şi formatarea codului să indice această asociere. De exemplu:
char *sir1, sir2, sir3; // sir1 e pointer, sir2 si sir3 sunt caractere int *a, *b, *c; // a, b si c sunt pointeri char* a, b; // Doar a este pointer; formatarea codului este
nerecomandata
Operatorul * poate fi folosit şi în specificarea numelui unui tip (de exemplu în cazul unui cast), şi în acest
caz el apare după numele tipului. De exemplu:
void *var = NULL;
int *num = (int *)var; // Operatie valida, dar riscanta
Atenţie!
Un pointer către void nu poate fi folosit direct în operaţii cu pointeri, ci trebuie convertit mai întâi la un
pointer către un tip de date. De exemplu:
void *mem;
//[...] *mem = 10; // Operatie ILEGALA
((int*)mem) = 10; // Operatie legala, dar riscanta
Tipuri de pointeri
Pointeri la date
Operaţiile cu pointeri la date pot fi rezumate la următoarele categorii:
Indirectarea folosind operatorul *, pentru acces la datele acelui pointer. De exemplu:
*p = y; // Ia valoarea y si pune-o la adresa indicata de p x = *p; // Ia valoarea de la adresa indicata de p si pune-o in
variabila x
*s1++ = *s2++;
Atribuirea la un pointer; în partea dreaptă poate fi un pointer de acelaşi tip (eventual cu conversie
de tip), constanta NULL, sau o expresie cu rezultat pointer. De exemplu:
p1 = p2;
p = NULL;
p = (int*)malloc(n);
Atribuirea între tipuri diferite de pointeri se poate face numai cu cast explicit şi, printre altele,
permite interpretarea diferită a unor date din memorie, ca în exemplul următor:
int n; short s1, s2;
s1 = *( (short*)&n); // Extrage primul cuvant din intregul n
s2 = *( (short*)&n + 1); // Extrage cel de-al doilea cuvant din intregul n
Compararea sau scăderea a două variabile pointer de acelaşi tip. Operaţia de scădere returnează
diferenţa în elemente dintre cele două adrese de memorie (cu alte cuvinte, diferenţa în octeţi
dintre două adrese de tip*, împărţită la sizeof(tip)).
Adunarea sau scăderea unui întreg la un pointer, incrementarea sau decrementarea unui pointer.
Aceste operaţii lucrează în multipli de dimensiunea tipului de date la care pointerii se referă,
pentru a permite accesul la memorie ca într-un vector. De exemplu:
int *num;
num++; // Aduna la adresa initiala pe sizeof(num), dand acces // la urmatorul intreg care ar fi stocat daca zona aceea de memorie ar fi
organizata // sub forma unui vector
num = num + 5; // Incrementeaza adresa cu 5*sizeof(num);
Pointeri la tablouri
O variabilă vector conţine adresa de început a vectorului (adresa primei componente a vectorului), şi de
aceea este echivalentă cu un pointer la tipul elementelor din vector. Această echivalenţă este exploatată,
de obicei, în argumentele de tip vector şi în lucrul cu vectori alocaţi dinamic. De exemplu, pentru
declararea unei funcţii care primeşte un vector de întregi şi dimensiunea lui, avem două posibilităţi:
void printVec(int a[], int n);
sau:
void printVec(int *a, int n);
Interesant este că în interiorul funcţiei ne putem referi la elementele vectorului a fie prin indici, fie prin
indirectare, indiferent de felul cum a fost declarat parametrul vector a:
void printVec (int a[], int n)
{
int i;
for (i=0;i<n;i++)
printf ("%6d",a[i]); // Indexare
}
void printVec (int *a, int n)
{
int i;
for (i=0;i<n;i++)
printf ("%6d", *a++); // Indirectare
}
Astfel, există următoarele echivalenţe de notaţii pentru un vector a:
a[0] == *a
a[1] == *(a+1)
a[k] == *(a+k)
&a[0] == a
&a[1] == a+1
&a[k] == a+k
Diferenţa dintre o variabilă pointer şi un nume de vector este aceea că un nume de vector este un
pointer constant (adresa sa este alocată de către compilatorul C şi nu mai poate fi modificată la execuţie),
deci nu poate apărea în stânga unei atribuiri, în timp ce o variabilă pointer are un conţinut modificabil prin
atribuire sau prin operaţii aritmetice. De exemplu:
int a[100], *p;
p = a; ++p; //corect a = p; ++a; //EROARE
De asemenea, o variabilă de tip vector conţine şi informaţii legate de lungimea vectorului şi dimensiunea
totală ocupată în memorie, în timp ce un pointer doar descrie o poziţie în memorie (e o valoarea
punctuală). Operatorul sizeof(v) pentru un vector v[N] de tipul T va fi N*sizeof(T), în timp
ce sizeof(v) pentru o variabila v de tipul T* va fi sizeof(T*), adică dimensiunea unui pointer.
Ca o ultimă notă, este importat de remarcat că o funcţie poate avea ca rezultat un pointer, dar nu poate
avea ca rezultat un vector.
Pointeri în funcţii
În cadrul funcţiilor, pointerii pot fi folosiţi, printre altele, pentru:
Transmiterea de rezultate prin argumente
Transmiterea unei adrese prin rezultatul funcţiei
Utilizarea unor funcţii cu nume diferite (date prin adresele acestora)
O funcţie care trebuie să modifice mai multe valori primite prin argumente sau care trebuie să transmită
mai multe rezultate calculate în cadrul funcţiei trebuie să folosească argumente de tip pointer. De
exemplu, o funcţie care primeşte ca parametru un număr, pe care il modifica:
// Functie care incrementeaza un intreg n modulo m
int incmod (int *n, int m) {
return ++(*n) % m; }
// Utilizarea functiei int main() {
int n = 10;
int m = 15;
incmod(&n, m);
// Afisam noua valoare a lui n printf("n: %d", n); return 0;
}
O funcţie care trebuie să modifice două sau mai multe argumente, le va specifica pe acestea individual,
prin câte un pointer, sau într-un mod unificat, printr-un vector, ca în exemplul următor:
void inctime (int *h, int *m, int *s); // sau
void inctime (int t[3]); // t[0]=h, t[1]=m, t[2]=s
O funcţie poate avea ca rezultat un pointer, dar acest pointer nu trebuie să conţină adresa unei variabile
locale. De obicei, rezultatul pointer este egal cu unul din argumente, eventual modificat în funcţie. De
exemplu:
// Incrementare pointer p
char *incptr(char *p) { return ++p;
}
O variabila locală are o existenţă temporară, garantată numai pe durata execuţiei funcţiei în care este
definită (cu excepţia variabilelor locale statice), şi de aceea adresa unei astfel de variabile nu trebuie
transmisă în afara funcţiei, pentru a fi folosită ulterior. De exemplu, următoarea secvenţă de cod
estegreşită:
// Vector cu cifrele unui nr intreg int *cifre (int n) {
int k, c[5]; // Vector local
for (k=4;k>=0;k--) { c[k]=n%10;
n=n/10; }
return c; // Aici este eroarea ! }
Astfel, o funcţie care trebuie să transmită ca rezultat un vector poate fi scrisă corect în două feluri:
Primeşte ca argument adresa vectorului (definit şi alocat în altă funcţie) şi depune rezultatele la
adresa primită (soluţia recomandată!);
Alocă dinamic memoria pentru vector (folosind malloc), iar această alocare se menţine şi la
ieşirea din funcţie.
Pointeri la funcţii
Anumite aplicaţii numerice necesită scrierea unei funcţii care să poată apela o funcţie cu nume
necunoscut, dar cu prototip şi efect cunoscut. De exemplu, o funcţie care să calculeze integrala definită a
oricărei funcţii cu un singur argument sau care să determine o radăcină reala a oricărei ecuaţii (neliniare).
Aici vom lua ca exemplu o funcţie listf care poate afişa (lista) valorile unei alte funcţii cu un singur
argument, într-un interval dat şi cu un pas dat. Exemple de utilizare a funcţiei listf pentru afişarea valorilor
unor funcţii de bibliotecă:
int main() {
listf (sin, 0.0, 2.0*M_PI, M_PI/10.0);
listf (exp, 1.0, 20.0, 1.0); return 0;
}
Problemele apar la definirea unei astfel de funcţii, care primeşte ca argument numele (adresa) unei
funcţii. Prin convenţie, în limbajul C, numele unei funcţii neînsoţit de o listă de argumente şi de
parantezele () specifice unui apel este interpretat ca un pointer către funcţia respectivă (fără a se folosi
operatorul de adresare &).
Deci sin este adresa funcţiei sin(x) în apelul funcţiei listf. Declararea unui argument formal (sau a unei
variabile) de tip pointer la o funcţie are forma următoare:
tip (*pf) (lista_arg_formale);
unde:
pf este numele argumentului (variabilei) pointer la funcţie
tip este tipul rezultatului funcţiei
Parantezele sunt importante, deoarece absenţa lor modifică interpretarea declaraţiei. De exemplu,
putem avea:
tip * f(lista_arg_formale) // functie cu rezultat pointer, si NU pointer
În concluzie, definirea funcţiei listf este:
void listf (double (*fp)(double), double min, double max, double pas) { double x,y;
for (x=min; x<=max; x=x+pas) {
y=(*fp)(x); // apel functie de la adresa din "fp" printf ("\n%20.10lf %20.10lf", x, y); }
}
O eroare de programare care trece de compilare şi se manifestă la execuţie este apelarea unei funcţii
fără paranteze; compilatorul nu apelează funcţia şi consideră că programatorul vrea să folosească adresa
funcţiei. De exemplu:
if (kbhit) break; // echivalent cu if(1) break;
if (kbhit()) break; // iesire din ciclu la apasarea unei taste
Expresii complexe cu pointeri
Deşi sunt întâlnite mai rar în practică, limbajul C permite declararea unor tipuri de date complexe,
precum:
char *( *(*var)() )[10];
//7 6 4 2 1 3 5
În interpretarea acestor expresii, operatorii () şi [] au precedenţa în faţa * şi modul de interpretare al
acestor expresii este pornind din interior spre exterior. Astfel expresia dată ca exemplu mai sus este
(numerele de sub expresie reprezintă ordinea de interpretare):
1. o variabila var
2. care este un pointer la o funcţie
3. fără nici un parametru
4. şi care întoarce un pointer
5. la un vector de 10 elemente
6. de tip pointer
7. către tipul char
Folosind acest procedeu, se pot rezolva şi alte situaţii aparent extrem de complexe:
unsigned int *(* const *name[5][10] ) ( void );
care semnifică: “o matrice de 5×10 de pointeri către pointeri constanţi la o funcţie, care nu ia nici un
parametru, şi care întoarce un pointer către tipul unsigned int”.
Invata Limbajul de Programare C – Partea 8
Bine ati venit pe ItAssistant. Aceasta noua serie de tutoriale isi propune sa va familiarizeze cu limbajul de
programare C si conceptele Programarii Structurata.
Alocarea dinamică a memoriei
Funcţii de alocare şi eliberare a memoriei
Aceste funcţii standard sunt declarate în fişierul antet stdlib.h. Cele trei funcţii de alocare au ca rezultat
adresa zonei de memorie alocate (de tip void*) şi ca argument comun dimensiunea, în octeţi, a zonei de
memorie alocate (de tip size_t ).
Dacă cererea de alocare nu poate fi satisfăcută pentru că nu mai există un bloc continuu de dimensiunea
solicitată, atunci funcţiile de alocare au rezultat NULL (ce reprezintă un pointer de tip void* la adresa de
memorie 0, care prin convenţie este o adresă invalidă – nu există date stocate în acea zonă).
La apelarea funcţiilor de alocare se folosesc:
Operatorul sizeof pentru a determina numărul de octeţi necesar unui tip de date (variabile);
Operatorul de conversie (cast) pentru adaptarea adresei primite la tipul datelor memorate la
adresa respectivă (conversie necesară atribuirii între pointeri de tipuri diferite).
De exemplu:
char *str = (char*) malloc(30); // Aloca memorie pentru 30 de
caractere
int *a = (int*) malloc ( n * sizeof(int)); // Aloca memorie pt. n numere intregi
Atenţie! Dimensiunea memoriei luată ca parametru de malloc() este specificată în octeţi, indiferent de
tipul de date care va fi stocat în acea regiune de memorie! Din acest motiv, pentru a aloca suficientă
memorie, numărul dorit de elemente trebuie înmulţit cu dimensiunea unui element, atunci când are loc un
apelmalloc()
Alocarea de memorie pentru un vector şi iniţializarea zonei alocate cu zerouri se poate face cu
funcţia calloc.
Exemplu:
int *a= (int*) calloc(n, sizeof(int) ); // Aloca memorie pentru n numere
intregi
Codul de mai sus este perfect echivalent (dar mai rapid) cu următoarea secvenţă de instrucţiuni:
int i;
int *a = (int*) malloc(n * sizeof(int));
for (i = 0; i < n; i++) {
a[i] = 0;
}
Notă: În timp ce funcţia malloc() ia un singur parametru (o dimensiune în octeţi), funcţia calloc() primeşte
două argumente, o lungime de vector şi o dimensiune a fiecărui element. Astfel, această funcţie este
specializată pentru memorie organizată ca un vector, în timp ce malloc() nu ţine cont de structura
memoriei.
Realocarea unui vector care creşte (sau scade) faţă de dimensiunea estimată anterior se poate face cu
funcţia realloc, care primeşte adresa veche şi noua dimensiune şi întoarce noua adresă:
a = (int *) realloc (a, 2*n* sizeof(int)); // Dublare dimensiune anterioara
(n)
În exemplul anterior, noua adresă este memorată tot în variabila pointer a, înlocuind vechea adresă
(care nu mai este necesară şi nici nu mai trebuie folosită). Funcţia realloc() realizează următoarele
operaţii:
Alocă o zonă de dimensiunea specificată ca al doilea argument
Copiază la noua adresă datele de la adresa veche (primul argument al funcţiei)
Eliberează memoria de la adresa veche.
Funcţia free() are ca argument o adresă (un pointer) şi eliberează zona de la adresa respectivă (alocată
prin apelul unei funcţii de tipul …alloc). Dimensiunea zonei nu mai trebuie specificată deoarece este
ţinută minte de sistemul de alocare de memorie în nişte structuri interne.
Vectori alocaţi dinamic
Structura de vector are avantajul simplităţii şi economiei de memorie faţă de alte structuri de date folosite
pentru memorarea unei colectii de informaţii între care există anumite relaţii. Între cerinţa de
dimensionare constantă a unui vector şi generalitatea programelor care folosesc astfel de vectori există o
contradicţie.
De cele mai multe ori programele pot afla (din datele citite) dimensiunile vectorilor cu care lucrează şi deci
pot face o alocare dinamică a memoriei pentru aceşti vectori. Aceasta este o solutie mai flexibilă, care
foloseşte mai bine memoria disponibilă şi nu impune limitări arbitrare asupra utilizării unor programe.
În limbajul C nu există practic nici o diferenţă între utilizarea unui vector cu dimensiune fixă şi utilizarea
unui vector alocat dinamic, ceea ce încurajează şi mai mult utilizarea unor vectori cu dimensiune
variabilă.
De observat că nu orice vector cu dimensiune constantă este un vector static; un vector definit într-o
funcţie (alta decat main()) nu este static deoarece nu ocupă memorie pe toata durata de execuţie a
programului, deşi dimensiunea sa este stabilită la scrierea programului.
Exemplul urmator arata cum se poate defini si utiliza un vector alocat dinamic:
int main() { int n,i;
int *a; // Adresa vector alocat dinamic
printf ("n=");
scanf ("%d", &n); // Dimensiune vector
a = (int *) calloc(n, sizeof(int)); // Alternativ: a = (int*) malloc
(n*sizeof(int)); printf ("Componente vector: \n");
for (i = 0; i < n; i++)
scanf ("%d", &a[i]); // Sau scanf (“%d”, a+i); for (i = 0; i < n; i++) // Afisare vector printf ("%d ",a[i]);
free(a); // Nu uitam sa eliberam memoria
return 0;
}
Există şi cazuri în care datele memorate într-un vector rezultă din anumite prelucrări, iar numărul lor nu
poate fi cunoscut de la începutul execuţiei. Un exemplu poate fi un vector cu toate numerele prime mai
mici ca o valoare dată. În acest caz se poate recurge la o realocare dinamică a memoriei.
În exemplul următor se citeşte un număr necunoscut de valori întregi într-un vector extensibil:
#define INCR 100 // cu cat creste vectorul la fiecare realocare
int main() {
int n,i,m;
float x, *v; // v = adresa vector n = INCR;
i = 0;
v = (float *)malloc (n*sizeof(float)); // Dimensiune initiala vector
while ( scanf("%f",&x) != EOF) {
if (++i == n) { // Daca este necesar... n = n + INCR; // ... creste dimensiune vector v = (float*) realloc(vector, n*sizeof(float));
}
v[i] = x; // Memorare in vector numar citit }
for (i = 0; i < n; i++) // Afisare vector printf ("%f ",v[i]);
free(vector);
return 0;
}
Realocarea repetată de memorie poate conduce la fragmentarea memoriei heap, adică la crearea unor
blocuri de memorie libere dar neadiacente şi prea mici pentru a mai fi reutilizate ulterior. De aceea,
politica de realocare pentru un vector este uneori dublarea capacităţii sale anterioare. Astfel numărul de
realocări pe parcursul execuţiei programului va deveni foarte mic – un calcul matematic simplu arată că
acest număr este proporţional cu logaritmul dimensiunii maxime (finale) a vectorului.
Matrice alocate dinamic
Alocarea dinamică pentru o matrice este importantă deoarece:
Foloseşte economic memoria şi evită alocări acoperitoare, estimative.
Permite matrice cu linii de lungimi diferite (denumite uneori ragged arrays, datorită formelor
“zimţate” din reprezentările grafice)
Reprezintă o soluţie bună la problema argumentelor de funcţii de tip matrice.
Daca programul poate afla numărul efectiv de linii şi de coloane al unei matrice (cu dimensiuni diferite de
la o execuţie la alta), atunci se va aloca memorie pentru un vector de pointeri (funcţie de numărul liniilor)
şi apoi se va aloca memorie pentru fiecare linie (funcţie de numărul coloanelor) cu memorarea adreselor
liniilor în vectorul de pointeri. O astfel de matrice se poate folosi la fel ca o matrice declarată cu
dimensiuni constante.
Exemplu:
int main () { int **a;
int i, j, nl, nc;
printf ("nr. linii="); scanf (“%d”,&nl);
printf ("nr. col. ="); scanf (“%d”,&nc);
a = (int**) malloc (nl * sizeof(int*)); // Alocare pentru vector de pointeri
for (i = 0; i < n; i++) a[i] = (int*) calloc (nc, sizeof(int)); // Alocare pentru o linie si
initializare la zero
// Completare diagonala matrice unitate for (i = 0; i < nl; i++)
a[i][i]=1; // a[i][j]=0 pentru i != j
// Afisare matrice printmat(a, nl, nc);
free(a); // Nu uitam sa eliberam!
return 0; }
Funcţia de afişare a matricei se poate defini astfel:
void printmat(int **a, int nl, int nc) {
for (i = 0; i < nl; i++) {
for (j = 0; j < nc; j++) printf("%2d", a[i][j]);
printf("\n"); }
}
Notaţia a[i][j] este interpretată astfel pentru o matrice alocată dinamic:
a[i] conţine un pointer (o adresă b)
b[j] sau b+j conţine întregul din poziţia j a vectorului cu adresa b.
Astfel, a[i][j] este echivalent semantic cu expresia cu pointeri *(*(a + i) + j).
Totuşi, funcţia printmat() dată anterior nu poate fi apelată dintr-un program care declară argumentul
efectiv ca o matrice cu dimensiuni constante.
Exemplul următor este corect sintactic dar nu se execută corect:
int main() { int x[2][2] = { {1, 2}, {3, 4} }; // O matrice patratica cu 2 linii si 2
coloane printmat((int**)x, 2, 2); return 0;
}
Explicaţia este interpretarea diferită a conţinutului zonei de la adresa aflată în primul argument:
funcţiaprintmat() consideră că este adresa unui vector de pointeri (int *a[]), iar programul principal
consideră că este adresa unui vector de vectori (int x[][2]), care este reprezentat liniar in memorie.
Se poate defini şi o funcţie pentru alocarea de memorie la execuţie pentru o matrice:
int **newmat(int nl, int nc) { // Rezultat adresa matrice int i;
int **p = (int **)malloc(nl * sizeof(int*));
for (i = 0; i < n; i++)
p[i] = (int*)calloc(nc, sizeof(int));
return p;
}
Stil de programare. Exemple de programe.
Exemplul 1: Funcţie echivalentă cu funcţia de bibliotecă strdup():
#include <string.h>
#include <alloc.h>
// Alocare memorie si copiere sir char *strdup(char* adr) {
int len = strlen(adr); char *rez = (char *)malloc(len);
strcpy(rez, adr);
return adr;
}
// Utilizare "strdup"
#include <stdio.h>
int main() {
char s[80], *d;
do {
if (gets(s) == 0)
break;
d = strdup(s);
puts(d);
free(d);
} while (1);
return 0; }
Exemplul 2: Vector alocat dinamic (cu dimensiune cunoscută la execuţie)
#include <stdio.h>
#include <stdlib.h>
int main() {
int n, i;
int *a; // Adresa vector
printf("n="); scanf("%d",&n); // Dimensiune vector
a = (int*)malloc(n * sizeof(int));
printf("componente vector: \n");
for (i = 0; i < n; i++) // Citire vector scanf("%d", &a[i]);
for (i = 0; i < n; i++) // Afisare vector printf("%d", a[i]);
free(a);
return 0; }
Exemplul 3: Vector realocat dinamic (cu dimensiune necunoscută)
#include <stdio.h>
#include <stdlib.h>
#define INCR 4
int main() { int n, i, m;
float x, *v;
n = INCR;
i = 0;
v = (float *)malloc(n * sizeof(float));
while (scanf("%f", &x) != EOF) {
if (i == n) {
n = n + INCR;
v = (float *)realloc(v, n * sizeof(float));
}
v[i++] = x;
}
m = i;
for (i = 0; i < m; i++) printf("%.2f ", v[i]);
free(v);
return 0;
}
Exemplul 4: Matrice alocată dinamic (cu dimensiuni cunoscute la execuţie)
#include <stdio.h>
#include <stdlib.h>
int main() { int n, i, j;
int **mat; // Adresa matrice
// Citire dimensiuni matrice printf("n="); scanf("%d",&n);
// Alocare memorie ptr matrice mat = (int **)malloc(n * sizeof(int *));
for (i = 0; i < n; i++) mat[i] = (int *)calloc(n, sizeof(int));
// Completare matrice
for (i = 0; i < n; i++)
for (j = 0; j < n; j++) mat[i][j] = n * i + j + 1;
// Afisare matrice
for (i = 0; i < n; i++) {
for (j = 0;j < n; j++)
printf("%6d", mat[i][j]);
printf("\n"); }
return 0;
}
Exemplul 5: Vector de pointeri la şiruri alocate dinamic
/* Creare / afisare vector de pointeri la siruri */
#include <stdio.h> #include <stdlib.h>
#include <string.h>
// Afisare siruri reunite in vector de pointeri void printstr(char *vp[], int n) { int i;
for(i = 0; i < n; i++)
printf("%s\n", vp[i]);
}
// Ordonare vector de pointeri la siruri void sort(char *vp[], int n) { int i, j;
char *tmp;
for (j = 1; j < n; j++)
for (i = 0; i < n - 1; i++) if (strcmp(vp[i], vp[i+1]) > 0) {
tmp = vp[i];
vp[i] = vp[i+1];
vp[i+1] = tmp; }
}
// Citire siruri si creare vector de pointeri
int readstr (char * vp[]) { int n = 0;
char *p, sir[80];
while (scanf("%s", sir) == 1) {
p = (char *)malloc(strlen(sir) + 1);
strcpy(p, sir);
vp[n] = p;
++n;
}
return n;
}
int main() { int n; char *vp[1000]; // vector de pointeri, cu dimensiune fixa
n = readstr(vp); // citire siruri si creare vector sort(vp, n); // ordonare vector printstr(vp, n); // afisare siruri
retrun 0;
}
Practici recomandate
Aveţi grijă ca variabilele de tip pointer să indice către adrese de memorie valide înainte de a fi
folosite; consecinţele adresării unei zone de memorie aleatoare sau invalide (NULL) pot fi dintre
cele mai imprevizibile.
Utilizaţi o formatare a codului care să sugereze asocierea operatorului * cu variabila asupra
căreia operează; acest lucru este în special valabil pentru declaraţiile de pointeri.
Nu returnaţi pointeri la variabile sau tablouri definite în cadrul funcţiilor, întrucât valabilitatea
acestora încetează odată cu ieşirea din corpul funcţiei.
Verificaţi rezultatul funcţiilor de alocare a memoriei, chiar dacă dimensiunea pe care doriţi s-o
rezervaţi este mică.
Atunci când memoria nu poate fi alocată rezultatul este NULL iar programul vostru ar trebui să
trateze explicit acest caz (finalizat, de obicei, prin închiderea “curată” a aplicaţiei).
Nu uitaţi să eliberaţi memoria alocată dinamic, folosind funcţia free(). Memoria rămasă
neeliberată încetineşte performanţele sistemului şi poate conduce la erori (bug-uri) greu de
depistat.
Clase de stocare
Clasa de stocare (memorare) arată când, cum şi unde se alocă memorie pentru o variabilă (vector).
Orice variabilă C are o clasă de memorare care rezultă fie dintr-o declaraţie explicită, fie implicit din locul
unde este definită variabila. Există trei moduri de alocare a memoriei, dar numai două corespund unor
clase de memorare:
Static: memoria este alocată la compilare în segmentul de date din cadrul programului şi nu se
mai poate modifica în cursul execuţiei. Variabilele externe, definite în afara funcţiilor, sunt implicit
statice, dar pot fi declarate static şi variabile locale, definite în cadrul funcţiilor.
Automat: memoria este alocată automat, la activarea unei funcţii, în zona stivă alocată unui
program şi este eliberată automat la terminarea funcţiei. Variabilele locale unui bloc (unei funcţii)
şi argumentele formale sunt implicit din clasa auto.
Dinamic: memoria se alocă la execuţie în zona heap alocată programului, dar numai la cererea
explicită a programatorului, prin apelarea unor funcţii de bibliotecă (malloc, calloc, realloc).
Memoria este eliberată numai la cerere, prin apelarea funcţiei free. Variabilele dinamice nu au
nume şi deci nu se pune problema clasei de memorare (atribut al variabilelor cu nume).
Variabilele statice pot fi iniţializate numai cu valori constante (pentru că se face la compilare), dar
variabilele auto pot fi iniţializate cu rezultatul unor expresii (pentru că se face la execuţie). Toate
variabilele externe (şi statice) sunt automat iniţializate cu valori zero (inclusiv vectorii). Cantitatea de
memorie alocată pentru variabilele cu nume rezultă automat din tipul variabilei şi din dimensiunea
declarată pentru vectori. Memoria alocată dinamic este specificată explicit ca parametru al funcţiilor de
alocare.
O a treia clasă de memorare este clasa register pentru variabile cărora, teoretic, li se alocă registre ale
procesorului şi nu locaţii de memorie, pentru un timp de acces mai bun. În practică nici un compilator
modern nu mai ţine cont de acest cuvânt cheie, folosind automat registre atunci când codul poate fi
optimizat în acest fel (de exemplu când observă că nu se accesează niciodata adresa variabilei în
program).
Memoria neocupată de datele statice şi de instrucţiunile unui program este împărţită între stivă şi heap.
Consumul de memorie pe stivă este mai mare în programele cu funcţii recursive şi număr mare de apeluri
recursive, iar consumul de memorie heap este mare în programele cu vectori şi matrice alocate (şi
realocate) dinamic.
Invata Limbajul de Programare C – Partea 9
Bine ati venit pe ItAssistant. Aceasta noua serie de tutoriale isi propune sa va familiarizeze cu limbajul de
programare C si conceptele Programarii Structurata.
Prelucrarea şirurilor de caractere. Funcţii.
Şiruri de caractere
Un caracter se declară în C de forma: char a=‟a‟; Pentru initializarea lui, se observă că am pus un
caracter între apostroafe.
Un şir de caractere presupune practic un vector de caractere. Cea mai simplă declaraţie fiind: char
a[10]= “cuvant”;
Pentru iniţializarea unui şir de caractere, spre deosebire de un singur caracter am folosit ghilimelele.
Cum s-a prezentat anterior, o variabilă vector conţine adresa de început a vectorului(adresa primei
componente a vectorului), şi de aceea este echivalentă cu un pointer la tipul elementelor din vector. Deci
declaraţiile de mai jos vor declara fiecare cate un şir de caractere:
char a[5]; char *b="unsir";
char *c;
Diferenţa între ele este însa că primele două declaraţii vor aloca 5 pozitii în memorie, pe când ultima nu
va aloca nici o zona de memorie, necesitând sa fie ulterior alocată, folosind funcţiile de alocare dinamică
(malloc(), calloc(), realloc()). Un mic exemplu de citire a unui şir, caracter cu caracter pana la 0:
#include <stdio.h>
#include <string.h>
int main () {
char s[30],c;
int n=0;
do{
scanf("%c", &c)
if(c=='0') break;
s[n++]=c;
}while(1);
s[n]='\0';
printf("%s",s);
return 0;
}
Pentru citirea si afisarea unui şir de caractere se poate folosi flagul „s‟ la citirea cu scanf sau afişarea
cuprintf. Deasemenea biblioteca stdio.h defineşte funcţiile gets() şi puts() pentru lucrul cu şiruri de
caractere.
gets(zona) – citeşte de la terminalul standard un şir de caractere terminat cu linie noua (enter).
Funcţia are ca parametru adresa zonei de memorie în care se introduc caracterele citite.
Funcţia returneaza adresa de început a zonei de memorie;
puts(zona) – afişeaza la terminalul standard şirul de caractere din zona data ca parametru, până
la caracterul terminator de şir, care va fi înlocuit prin caracterul linie noua.
Funcţia are ca parametru adresa zonei de memorie de unde începe afişarea caracterelor.
Funcţia returneaza codul ultimului caracter din şirul de caractere afişat şi -1 daca a aparut o
eroare.
Atenţie! Funcţia gets() va citi de la tastatura câte caractere sunt introduse, chiar daca şirul declarat are o
lungime mai mică. Presupunem un şir declarat: char a[]=”unsir” , care va avea deci 5 caractere. Citind
un şir de lungime mai mare ca 5 de la tastatura, în şirul a, la afişare vom vedea ca s-a reţinut tot sirul!(nu
doar primele 5 caractere). Nimic deosebit până acum. Dar dacă luăm în considerare că citirea
caracterelor auxiliare se face în continuare în zona de memorie, ne punem problema ce se va
suprascrie?!
Raspunsul este: nu se ştie… poate nimic important pentru programul nostru, poate ceva ce il va bloca
sau duce la obţinerea de date eronate.
Pentru a evita aceasta se recomandă utilizarea fgets()
fgets(zona, lung_zona, stdin) – citeşte de la stdin un şir de caractere terminat printr-o linie nouă
dacă lungimea lui este mai mică decat lung_zona sau primele lung_zona caractere în caz
contrar. Parametrii sunt: zona de memorie, lungimea maxima admisă a şirului, şi terminalul
standard de intrare. În cazul în care şirul dorit are lungime mai mică decât cea maximă, înaintea
terminatorului de şir, în zona de memorie va fi reţinut şi enter-ul dat.
Funcţii din string.h
Pentru manipularea şirurilor de caractere în limbajul C se folosesc funcţii declarate în fişierul string.h.
Vom încerca să le detaliem putin pe cele mai des folosite.
strlen() size_t strlen ( const char * str );
Returneaza lungimea unui şir dat ca parametru. (numarul de caractere
până la întalnirea terminatorului de şir)
Exemplu:
#include <stdio.h>
#include <string.h>
int main () {
char text[256];
printf ("Introduceti un text: ");
gets (text);
printf ("Textul are %u caractere.\n",strlen(text));
return 0;
}
memset() void * memset ( void * ptr, int val, size_t num );
În zona de memorie dată de pointerul ptr, sunt setate primele num poziţii la valoarea dată de val.
Funcţia returnează şirul ptr.
Exemplu:
#include <stdio.h>
#include <string.h>
int main () {
char str[] = "nu prea vreau vacanta!";
memset (str,'-',7);
puts (str);
return 0;
}
Iesire:
------- vreau vacanta!
memmove() void * memmove ( void * destinatie, const void * sursa, size_t num );
Copiază un număr de num caractere de la sursă, la zona de memorie indicată de destinaţie. Copierea
are loc ca şi cum ar exista un buffer intermediar, deci sursa si destinatia se pot suprapune. Funcţia nu
verifică terminatorul de şir la sursă, copiază mereu num bytes, deci pentru a evita depăsirea trebuie ca
dimensiunea sursei sa fie mai mare ca num. Funcţia returnează destinaţia.
Exemplu:
#include <stdio.h>
#include <string.h>
int main () {
char str[] = "memmove can be very useful......";
memmove (str+20,str+15,11);
puts (str);
return 0;
}
Iesire:
memmove can be very very useful.
memcpy() void * memcpy ( void * destinatie, const void * sursa, size_t num );
Copiază un număr de num caractere din şirul sursă in şirul destinaţie. Funcţia returnează şirul destinaţie.
Exemplu:
#include <stdio.h>
#include <string.h>
int main () {
char str1[]="Exemplu";
char str2[40];
char str3[40];
memcpy (str2,str1,strlen(str1)+1);
memcpy (str3,"un sir",7);
printf ("str1: %s\nstr2: %s\nstr3: %s\n",str1,str2,str3);
return 0;
}
Iesire:
str1: Exemplu
str2: Exemplu
str3: un sir
strcpy() char * strcpy ( char * destinatie, const char * sursa );
Copiază şirul sursă in şirul destinaţie. Şirul destinaţie va fi suprascris. Funcţia asigură plasarea
terminatorului de şir în şirul destinaţie după copiere. Funcţia returneaza şirul destinaţie.
strncpy() char * strncpy ( char * destinatie, const char * sursa, size_t num );
Asemeni cu strcpy(), dar in loc de a fi copiată toata sursa sunt copiate doar primele num caractere.
Exemplu:
#include <stdio.h>
#include <string.h>
int main () {
char str1[]="Exemplu";
char str2[40];
char str3[40];
strcpy (str2,str1);
strncpy (str3,"un sir",2);
printf ("str1: %s\nstr2: %s\nstr3: %s\n",str1,str2,str3);
return 0;
}
Iesire:
str1: Exemplu
str2: Exemplu
str3: un
strcat() char * strcat ( char * destinatie, const char * sursa );
Concatenenaza şirul sursă la şirul destinaţie. Funcţia returnează şirul destinaţie.
strncat() char * strncat ( char * destinatie, const char * sursa, size_t num );
Asemeni cu strcat(), dar în loc de a fi concatenată toată sursa sunt concatenate doar
primele num caractere.
Exemplu:
#include <stdio.h>
#include <string.h>
int main () {
char str[80];
strcpy (str,"ana ");
strcat (str,"are ");
strcat (str,"mere ");
puts (str);
strncat (str,"si pere si prune", 7);
puts (str);
return 0;
}
Iesire:
ana are mere
ana are mere si pere
strcmp() int strcmp ( const char * str1, const char * str2 );
Compară şirul str1 cu şirul str2, verificându-le caracter cu caracter. Valoarea returnată este 0 daca cele
şiruri sunt identice, mai mare ca 0 daca str1 este “mai mare”(alfabetic) şi mai mic ca zero altfel.
Exemplu:
#include <stdio.h>
#include <string.h>
int main () {
char cuv[] = "rosu";
char cuv_citit[80];
do {
printf ("Ghiceste culoarea...");
gets (cuv_citit);
} while (strcmp (cuv,cuv_citit) != 0);
puts ("OK");
return 0;
}
strchr() char * strchr (const char * str, int character );
Caută caracterul c în şirul str şi returnează un pointer la prima sa apariţie.
strrchr() char * strrchr (const char * str, int character );
Caută caracterul c în şirul str şi returnează un pointer la ultima sa apariţie.
strstr() char * strstr (const char *str1, const char *str2 );
Caută şirul str2 în şirul str1 şi returnează un pointer la prima sa apariţie, sau NULL dacă nu a fost găsit.
strdup() char * strdup (const char *str);
Realizează un duplicat al şirului str, pe care îl şi returnează.
Exemplu:
#include <stdio.h>
#include <string.h>
int main () {
char str[80], * d;
do {
if (gets(str)==0)
break;
d=strdup(str);
puts(d);
} while (1);
return 0;
}
strtok() char * strtok ( char * str, const char * delimitatori );
Funcţia are rolul de a împarţi şirul str în tokens(subşiruri separate de orice caracter aflat în lista de
delimitatori), prin apelarea ei succesivă.
La primul apel, parametrul str trebuie sa fie un şir de caractere, ce urmează a fi împartit. Apelurile
urmatoare, vor avea în loc de str, NULL conţinuând împarţirea aceluiaşi şir.
Funcţia va returna la fiecare apel un token(un subsir), ignorând caracterele cu rol de separator aflate în
şirul de delimitatori. O dată terminat şirul, funcţia va returna NULL.
Exemplu:
#include <stdio.h>
#include <string.h>
int main () {
char str[] ="- Uite, asta e un sir.";
char * p;
p = strtok (str," ,.-");
/* separa sirul in "tokeni" si afiseaza-i pe linii separate. */
while (p != NULL) {
printf ("%s\n",p);
p = strtok (NULL, " ,.-");
}
return 0;
}
Iesire:
Uite
asta
e
un
sir
Invata Limbajul de Programare C – Partea 10
Bine ati venit pe ItAssistant. Aceasta noua serie de tutoriale isi propune sa va familiarizeze cu limbajul de
programare C si conceptele Programarii Structurata.
Structuri. Uniuni. Matrici rare
Structuri
Structurile sunt tipuri de date în care putem grupa mai multe variabile eventual de tipuri diferite (spre
deosebire de vectori, care conţin numai date de acelasi tip).
O structură se poate defini astfel:
struct nume_structura { declaratii_de_variabile };
Exemple:
struct student {
char nume[40];
int an;
float medie;
};
(nume, an etc. se numesc “campurile” structurii)
struct complex { /* pentru memorarea unui număr complex cu dublă precizie */
double re;
double im;
};
Declararea şi iniţializarea unor variabile de tip structură se poate face astfel:
struct student s1 = {"Popescu Ionel", 3, 9.25};
struct complex c1, c2;
struct complex v[10];
Pentru simplificarea declaraţiilor, putem asocia unei structuri un nume de tip de date:
typedef struct student Student;
...
Student s1, s2, s3;
Accesul la membrii unei structuri se face prin operatorul “.”:
s1.nume = "Ionescu Raluca";
În cazul pointerilor la structuri, accesul la membri se poate face astfel:
Student *stud = (Student *)malloc(sizeof(Student));
(*stud).medie = 9.31;
/* altă modalitate mai simplă şi mai des folosită: */
stud -> medie = 9.31;
Atribuirile de structuri se pot face astfel:
struct complex n1, n2;
...
n2 = n1;
Prin această atribuire se realizează o copiere bit cu bit a elementelor lui n1 în n2.
Alt exemplu de utilizare: După cum se vede mai jos trebuie facută diferenta când definim un tip şi cand
declaram o variabila de tip struct sau typedef struct.
typedef struct {
int data;
int text;
} S1; // este un typedef pentru S1, functional in C si C++
struct S2 {
int data;
int text;
}; // este un typedef pentru S2, functional numai in C++
struct {
int data;
int text;
} S3;
// este o declaratie a lui S3, variabila de tip struct nu defineste un tip
// spune compilatorului sa aloce memorie pentru variablia S3
int main(){
// ce se intampla la declarare variabile de tip S1,S2,S3
S1 mine1; // este un typedef si va merge
S2 mine2; // este un typedef si va merge
S3 mine3; // nu va merge pt ca S3 nu este un typedef.
// ce se intampla la utilizare variabile S1,S2,s3
S1.data = 5; // da eroare deoarece S1 este numai un typedef.
S2.data = 5; // da eroare deoarece S2 este numai un typedef.
S3.data = 5; // merge doarece S3 e o variabila
return 0;
}
Atenţie! Dacă declaraţi pointeri la structuri, nu uitaţi să alocaţi memorie pentru aceştia înainte de a
accesa câmpurile structurii. Nu uitaţi să alocaţi şi câmpurile structurii, care sunt pointeri, înainte de
utilizare, dacă este cazul. De asemenea fiţi atenţi şi la modul de accesare al câmpurilor.
Diferenţa dintre copierea structurilor şi copierea pointerilor
Pentru exemplificarea diferenţei dintre copierea structurilor şi copierea pointerilor să considerăm
urmatorul exemplu:
struct exemplu {
int n;
char *s;
}
struct exemplu s1, s2;
char *litere = "abcdef";
s1.n = 5;
s1.s = strdup(litere);
s2 = s1;
s2.s[1]='x';
După atribuirea s2 = s1;, s2.s va avea o valoare identică cu s1.s. Deoarece s este un pointer (o
adresă de memorie), s2.s va indica aceeaşi adresa de memorie ca şi s1.s. Deci, după modificarea celui
de-al doilea caracter din s2.s, atat s2.s cât si s1.s vor fi axcdef.
De obicei acest efect nu este dorit şi nu se recomandă atribuirea de structuri atunci cand acestea contin
pointeri.
Totuşi, putem atribui ulterior lui s2.s o altă valoare (o altă adresă), iar ca urmare a acestei operaţii,
stringurile vor fi distincte din nou.
Un alt caz (diferit de cel expus anterior) este cel al atribuirii aceleiaşi structuri către două variabile pointer
diferite:
struct exemplu {
int n;
char * s;
}
struct exemplu s1;
struct exemplu* p1;
struct exemplu* p2;
p1 = &s1;
p2 = &s2;
În acest caz observăm că din nou p1->s şi p2->s indică către acelaşi şir de caractere, dar aici adresa
către şirul de caractere apare memorată o singura dată; spre deosebire de cazul anterior, dacă
modificăm adresa din p2->s, ea se va modifica automat şi în p1->s.
Uniuni
Uniunile sunt asemănătoare structurilor, dar lor li se rezervă o zonă de memorie ce poate conţine, la
momente de timp diferite, variabile de tipuri diferite. Sunt utilizate pentru a economisi memoria (se
refoloseşte aceeaşi zonă de memorie pentru a stoca mai multe variabile).
Uniunile se pot declara astfel:
union numere {
int i;
float f;
double v;
}; /* se poate utiliza si typedef... */
union numere u1, u2;
Când scriem ceva într-o uniune (de exemplu când facem o atribuire de genul u1.f = 7.4), ceea ce citim
apoi trebuie să fie de acelaşi tip, altfel vom obţine rezultate eronate (adică trebuie să utilizam u1.f, nu u1.v
sau u1.i).
Programatorul trebuie să ţină evidenţa tipului variabilei care este memorată în uniune în momentul curent
pentru a evita astfel de greşeli. Operaţiile care se pot face cu structuri se pot face şi cu uniuni; o structura
poate conţine uniuni şi o uniune poate conţine structuri.
Exemplu:
#include <stdio.h>
#include <stdlib.h>
typedef union {
int Wind_Chill;
char Heat_Index;
} Condition;
typedef struct{
float temp;
Condition feels_like;
} Temperature;
int main(){
Temperature *tmp;
tmp = (Temperature *)malloc(sizeof(Temperature));
printf("\nAddress of Temperature = %u", tmp);
printf("\nAddress of temp = %u, feels_like = %u", &(*tmp).temp, &(*tmp).feels_like);
printf("\nWind_Chill = %u, Heat_Index= %u\n", &((*tmp).feels_like).Wind_Chill,
&((*tmp).feels_like).Heat_Index);
return 0;
}
La rulare va afisa:
Address of Temperature = 165496
Address of temp = 165496, feels_like = 165500
Wind_Chill = 165500, Heat_Index= 16550
Ce este o matrice rara?
O matrice rară (cu circa 90% din elemente 0) este păstrată economic sub forma unei structuri, care
conţine următoarele câmpuri:
int L,C – numărul de linii/coloane al matricei rare
int N – numărul de elemente nenule
int LIN[] – vectorul ce păstrează liniile în care se află elemente nenule
int COL[] – vectorul ce păstrează coloanele în care se află elemente nenule
float X[] – vectorul ce păstrează elementele nenule
Invata Limbajul de Programare C – Partea 11
Bine ati venit pe ItAssistant. Aceasta noua serie de tutoriale isi propune sa va familiarizeze cu limbajul de
programare C si conceptele Programarii Structurata.
Operatii cu fişiere. Aplicaţii folosind fişiere.
Un fişier este o structură dinamică, situată în memoria secundară (pe disk-uri). Limbajul C permite
operarea cu fişiere:
de tip text – un astfel de fişier conţine o succesiune de linii, separate prin NL („n‟)
de tip binar – un astfel de fişier conţine o succesiune de octeti, fără nici o structură.
Prelucrarea unui fişier presupune asocierea acestuia cu un canal de I/E (numit flux sau stream).
Există trei canale predefinite, care se deschid automat la lansarea unui program:
stdin - fişier de intrare, text, este intrarea standard – tastatura
stdout - fişier de iesire, text, este ieşirea standard – ecranul monitorului.
stderr – fişier de iesire, text, este ieşirea standard unde sunt scris mesajele de eroare – ecran.
Pentru a prelucra un fişier, trebuie parcurse următoarele etape:
se defineşte o variabilă de tip FILE * pentru accesarea fişierului; FILE * este un tip structură
definit înstdio.h, care conţine informaţii referitoare la fişier şi la tamponul de transfer de date între
memoria centrală şi fişier (adresa, lungimea tamponului, modul de utilizare a fişierului, indicator
de sfârsit, de poziţie în fişier)
se deschide fişierul pentru un anumit mod de acces, folosind funcţia de bibliotecă fopen, care
realizează şi asocierea între variabila fişier şi numele extern al fişierului
se prelucrează fişierul în citire/scriere
cu funcţiile specifice
se inchide fişierul folosind funcţia de bibliotecă fclose.
Mai jos se prezintă restul funcţiilor de prelucrare a fişierelor:
FILE *fopen(const char *nume_fis, const char *mod);
deschide fişierul cu numele dat pentru acces de tip mod.
Returnează pointer la fişier sau NULL dacă fişierul nu poate fi deschis; valoarea returnată este memorată
în variabila fişier, care a fost declarată pentru accesarea lui.
Modul de deschidere poate fi:
“r” - readonly , este permisă doar citirea dintr-un fişier existent
“w” - write, crează un nou fişier, sau dacă există deja, distruge vechiul continut
“a” - append, deschide pentru scriere un fişier existent ( scrierea se va face în continuarea
informaţiei deja existente în fişier, deci pointerul de acces se plasează la sfârşitul fişierului )
“+” - permite scrierea şi citirea – actualizare (ex: “r+”, “w+”, “a+”). O citire nu poate fi direct
urmată
de o scriere şi reciproc. Întâi trebuie repoziţionat cursorul de acces printr-un apel la fseek.
“b” - specifică fişier de tip binar
“t” - specifică fişier de tip text (implicit), la care se face automat conversia CR-LF(“nf”) în sau din
CR („n‟).
int fclose(FILE *fp);
închide fişierul şi eliberează zona tampon; returnează 0 la succes, EOF la eroare
int fseek(FILE *fp, long offset, int whence);
repoziţionează pointerul asociat unui fişier . Offset – numărul de octeţi între poziţia dată de whence şi
noua poziţie.
whence - are una din cele trei valori posibile:
SEEK_SET = 0 – Căutarea se face de la începutul fişierului
SEEK_CUR = 1 - Căutare din poziţia curentă
SEEK_END = 2 - Căutare de la sfârşitul fişierului
long ftell(FILE* fp);
întoarce poziţia curentă în cadrul lui fp
int fgetpos(FILE* fp, fpos_t* ptr);
această funcţie memorează în variabila fptr poziţia curentă în cadrul fişierului fp (ptr va putea fi folosit
ulterior cu funcţia fsetpos).
int fsetpos(FILE* fp, const fpos_t* ptr);
această funcţie setează poziţia curentă în fişierul fp la valoarea ptr, obţinută anterior prin funcţia fgetpos.
int feof(FILE *fis);
returnează 0 dacă nu s-a detectat sfârşit de fişier la ultima operaţie de citire, respectiv o valoare nenulă
(adevărată) pentru sfârşit de fişier.
FILE* freopen(const char* filename, const char* mode, FILE* fp);
se închide fişierul fp, se deschide fişierul cu numele filename în modul mode şi acesta se asociază la fp;
se întoarce fp sau NULL în caz de eroare.
int fflush(FILE* fp);
Această funcţie se utilizează pentru fişierele deschise pentru scriere şi are ca efect scrierea în fişier a
datelor din bufferul asociat acestuia, care înca nu au fost puse în fişier.
Citirea şi scrierea în/din fişiere
Citirea/scrierea în fişiere se poate face în doua moduri (în functie de tipul fişierului): în mod text sau în
mod binar. Principalele diferenţe dintre cele doua moduri sunt:
în modul text, la sfarsitul fişierului se pune un caracter suplimentar, care indica sfârşitul de fişier.
În DOS şi Windows se utilizeaza caracterul cu codul ASCII 26 (Ctrl-Z), iar în Unix se utilizează
caracterul cu codul ASCII 4. Dacă citim un fişier în mod text, citirea se va opri la intâlnirea acestui
caracter, chiar dacă mai există şi alte caractere după el. În modul binar nu există caracter de
sfârşit de fişier (mai precis, caracterul cu codul 26, respectiv 4, este tratat la fel ca şi celelalte
caractere).
în DOS şi Windows, în modul text, sfârşitul de linie este reprezentat prin două caractere, CR
(Carriage Return, cod ASCII 13) şi LF (Line Feed, cod ASCII 10). Atunci când în modul text
scriem un caracter „n‟ (LF) în fişier, acesta va fi convertit într-o secventă de 2 caractere CR şi LF.
Când citim în mod text dintr-un fişier, secvenţa CR, LF este convertită într-un „n‟ (LF). În Unix,
sfârşitul de linie este reprezentat doar prin caracterul LF. În mod binar, atât în DOS-Windows cât
şi în Unix, sfârşitul de linie este reprezentat doar prin caracterul LF.
Modul binar se utilizează de obicei pentru a scrie în fişier datele exact aşa cum sunt reprezentate în
memorie (cu functiile fread, fwrite) – de exemplu pentru un număr intreg se va scrie reprezentarea internă
a acestuia, pe 2 sau pe 4 octeti.
Modul text este utilizat mai ales pentru scrierea cu format (cu funcţiile fprintf, fscanf) – în cazul acesta
pentru un număr întreg se vor scrie caracterele ASCII utilizate pentru a reprezenta cifrele acestuia (adică
un şir de caractere cum ar fi “1″ sau “542″).
Citire/scriere cu format
int fprintf(FILE* fp, const char *format, ...);
int fscanf(FILE* fp, const char *format, ...);
Funcţiile sunt utilizate pentru citire/scriere în mod text şi sunt asemănătoare cu printf/scanf (diferenţa fiind
că trebuie dat pointerul la fişier ca prim parametru).
Citire/scriere la nivel de caracter
int fgetc(FILE* fp); // întoarce următorul caracter din
fişier, EOF la sfârşit de fişier
char *fgets(char* s, int n, FILE* fp); // întoarce următoarele n caractere de
la pointer sau pâna la sfârşitul de linie
int fputc(int c, FILE* fp); //pune caracterul c in fişier
int ungetc(int c, FILE* fp); // pune c în bufferul asociat lui fp (c va fi
următorul caracter citit din fp)
Citire/scriere fără conversie
size_t fread(void* ptr, size_t size, size_t nrec, FILE* fp);
size_t fwrite(const void* ptr, size_t size, size_t nrec, FILE* fp);
Cu aceste funcţii lucrăm cand deschidem fişierul în mod binar; citirea/scrierea se face fără nici un fel de
conversie sau interpretare. Se lucrează cu “înregistrări”, adică zone compacte de memorie: funcţia fread
citeşte nrec înregistrări începănd de la poziţia curentă din fişierul fp, o înregistrare având dimensiunea
size.
Acestea sunt depuse în tabloul ptr. “Înregistrările” pot fi asociate cu structurile din C – adică în mod uzual,
tabloul ptr este un tablou de structuri (dar în loc de structuri putem avea şi tipuri simple de date).
Invata Limbajul de Programare C – Partea 12
Bine ati venit pe ItAssistant. Aceasta noua serie de tutoriale isi propune sa va familiarizeze cu limbajul de
programare C si conceptele Programarii Structurata.
Limbajul C – Parametrii liniei de comandă
Pentru a controla execuţia unui program, de multe ori este de dorit furnizarea datelor de lucru înaintea
lansării în execuţie a programului, acesta urmând să se execute apoi fără intervenţia utilizatorului (aşa-
numitul „batch mode”). Acest lucru se realizează prin intermediul parametrilor liniei de comandă. (Un
exemplu cunoscut este lansarea compilatorului gcc în linia de comandă cu diverse argumente, care îi
spun ce şi cum sa compileze.)
Din punct de vedere al utilizatorului, parametrii liniei de comandă sunt simple argumente care se adaugă
după numele unui program, în linia de comandă, la rularea sa. Elementele acestei liste de argumente
sunt şiruri de caractere separate de spaţii. Argumentele care conţin spaţii pot fi confinate într-un singur
argument prin inchiderea acestuia între ghilimele. Shell-ul este cel care se ocupă de parsarea liniei de
comandă şi de crearea listei de argumente.
Exemplu de apelare a unui program cu argumente în linia de comandă:
gcc -Wall -I/usr/include/sys -DDEBUG -o "My Shell" myshell.c
În acest caz, argumentele liniei de comandă sunt în acest caz:
gcc
-Wall
-I/usr/include/sys
-DDEBUG
-o
My Shell
myshell.c
Din punct de vedere al programatorului, parametrii liniei de comandă sunt accesibili prin utilizarea
parametrilor funcţiei main(). Astfel, când se doreşte folosirea argumentelor liniei de comandă, funcţia
main() se va defini astfel:
int main(int argc, char *argv[])
Astfel, funcţia main() primeşte, în mod formal, doi parametri, un întreg şi un vector de şiruri de caractere.
Numele celor două variabile nu e obligatoriu să fie argc şi argv, dar tipul lor, da. Semnificaţia lor este
următoarea:
int argc (argument count) – reprezintă numărul de parametrii ai liniei de comandă. După cum
se vede din exemplul anterior, există cel puţin un parametru, acesta fiind chiar numele
programului (numele care a fost folosit pentru a lansa în execuţie programul – şi care poate fi
diferit de numele executabilului – de exemplu prin crearea unui symlink, în Linux).
char *argv[] (arguments value) – reprezintă un vector de şiruri de caractere, având argc
elemente (indexate de la 0 la argc – 1). Întotdeauna argv[0] conţine numele programului.
Parametrii liniei de comandă se pot accesa prin intermediul vectorului argv şi pot fi prelucraţi cu funcţiile
standard de prelucrare a şirurilor de caractere.
Funcţii cu număr variabil de parametri
Exemplele discutate pana acum primeau ca parametrii un număr prestabilit de parametrii, de tipuri bine
precizate. Acest lucru se datorează faptului că limbajul C este un limbaj strong-typed. În cele mai multe
cazuri acesta este un aspect pozitiv întrucât compilatorul va sesiza de la compilare situaţiile în care se
încearcă pasarea unui număr eronat de parametrii sau a unor parametrii de tipuri necorespunzătoare.
Limbajul C permite totuşi şi declararea şi folosirea funcţiilor cu un număr (şi eventual tip) variabil de
parametri. Astfel, numărul şi tipul tuturor parametrilor va fi cunoscut doar la rulare, biblioteca standard
Cpunând la dispoziţie o serie de definiţii de tipuri şi macro definiţii care permit parcurgerea listei de
parametri a unei funcţii cu număr variabil de parametri. Exemplul cel mai comun de astfel de funcţii sunt
funcţiile din familia printf(), scanf().
Limbajul C – Definirea funcţiilor cu număr variabil de parametri
Prototipul funcţiilor cu număr variabil de parametrii arată în felul următor:
tip_rezultat nume_funcţie(listă_parametrii_fixaţi, ...);
Notaţia „ ,… ” comunică compilatorului faptul că funcţia poate primi un număr arbitrar de parametrii
începând cu poziţia în care apare. De exemplu, prototipul funcţiei fprintf() arată în felul următor:
void fprintf(FILE*, const char*, ...);
Astfel, funcţia fprintf() trebuie să primească un pointer la o structură FILE şi un string de format şi
eventual mai poate primi 0 sau mai multe argumente. Modul în care ele vor fi interpretate fiind determinat
în cazul de faţă de conţinutul variabilei de tip const char* (acesta este motivul pentru care pot apărea
erori la rulare în condiţiile în care încercăm să tipărim un număr întreg cu %s ).
Implementarea funcţiilor cu număr variabil de parametri
Pentru prelucrarea listei de parametrii variabili este necesară includerea fişierului antet stdarg.h. Acesta
conţine declaraţii pentru tipul de date variable argument list (va_list) şi o serie de macrodefiniţii pentru
manipularea unei astfel de liste. În continuare este detaliat modul de lucru cu liste variabile de parametri.
1. Declararea unei variabile de tip va_list (denumită de obicei args, arguments, params)
2. Iniţializarea variabilei de tip va_list cu lista de parametri cu care a fost apelată funcţia se realizează cu
macrodefiniţia va_start(arg_list, last_argument) unde:
arg_list reprezintă variabila de tip va_list prin intermediul căreia vom accesa lista de parametri a
funcţiei.
last_argument reprezintă numele ultimei variabile fixate din lista de parametri a funcţiei (în
exemplul următor, aceasta este şi prima variabilă, numită first).
3. Accesarea variabilelor din lista de parametri se realizează cu macro-definiţia va_arg(arg_list, type),
unde
arg_list are aceeaşi semnificaţie ca mai sus.
type este un nume de tip şi reprezintă tipul variabilei care va fi citită. La fiecare apel se
avansează în listă. În cazul în care se doreşte întoarcerea în listă, aceasta trebuie reiniţializată iar
elementul dorit este accesat prin apeluri succesive de va_arg()
4. Eliberarea memoriei folosite de lista de parametri se realizează prin intermediul macro-
definiţieiva_end(arg_list), unde arg_list are aceeaşi semnificaţie ca mai sus.
Exemplu: Afişarea unui număr variabil de numere naturale. Lista este terminată cu un număr negativ.
#include <stdio.h>
#include <stdarg.h>
void list_ints(int first, ...);
int main() {
list_ints(-1);
list_ints(128, 512, 768, 4096, -1);
list_ints('b', 0xC0FFEE, 10000, 200, -2);
/* apel corect deoarece castul la int este valid ;) */
list_ints(1, -1);
return 0;
}
void list_ints(int first, ...) {
/* tratam cazul special cand nu avem nici un numar (in afara de
delimitator) */
if (first < 0)
{
printf("No numbers present (besides terminator)\n");
return;
}
/*lista de parametri*/
va_list args;
int current = first;
/* initializam lista de parametri */
va_start(args, first);
printf("These are the numbers (excluding terminator):\n");
do {
printf("%d ", current);
}
/* parcurgem lista de parametri pana ce intalnim un numar negativ */
while ((current = va_arg(args, int)) >= 0);
printf("\n");
/* curatam lista de parametrii */
va_end(args);
}
Invata Limbajul de Programare C – Partea 13
Bine ati venit pe ItAssistant. Aceasta noua serie de tutoriale isi propune sa va familiarizeze cu limbajul de
programare C si conceptele Programarii Structurata.
Preprocesorul C
Preprocesorul este componenta din cadrul compilatorului C care realizează preprocesarea.
În urma acestui pas, toate instrucţiunile de preprocesare sunt înlocuite (substituite), pentru a genera cod
C „pur”. Preprocesarea este o prelucrare exclusiv textuală a fişierului sursă. În acest pas nu se fac nici un
fel de verificări sintactice sau semantice asupra codului sursă, ci doar sunt efectuate substituţiile din text.
Astfel, preprocesorul va prelucra şi fişiere fără nici un sens în C.
Spre exemplu, fiind considerat fişierul rubbish.c cu următorul conţinut:
#define EA Ana
#define si C
#ifdef CIFRE
#define CINCI 5
#define DOUA 2
EA are mere. Mara are DOUA pere shi CINCI cirese.
#endif
Vasilica vrea sa cante o melodie in si bemol.
La rularea comenzii
gcc -E -DCIFRE rubbish.c
se va obţine următoare ieşire (se cere compilatorului să execute doar pasul de preprocesare (-E),
definind în acelaşi timp şi simbolul CIFRE (-DCIFRE) :
# 1 "rubbish.c"
# 1 "<built-in>"
# 1 "<command line>"
# 1 "rubbish.c"
Ana are mere. Mara are 2 pere shi 5 cirese.
Vasilica vrea sa cante o melodie in C bemol.
Cele mai importante instrucţiuni de preprocesare sunt prezentate în continuare.
Incluziune
Probabil cea mai des folosită instrucţiune de preprocesare este cea de incluziune, de forma
#include <nume_fişier>
sau
#include "nume_fisier"
care are ca rezultat înlocuirea sa cu conţinutul fişierului specificat de nume_fişier. Diferenţa dintre cele
două versiuni este că cea cu paranteze unghiulare caută nume_fişier doar în directorul standard de
fişiere antet (numit deobicei include), iar cea cu ghilimele caută atât în directorul include cât şi în
directorul curent.
C – Definirea de simboluri
Definirea de simboluri este cel mai des folosită în conjuncţie cu instrucţiunile de procesare condiţionată,
fiind folosită pentru activarea şi dezactivarea unor segmente de cod în funcţie de prezenţa unor simboluri.
Definirea unui simbol se face în cod cu instrucţiunea
#define SIMBOL
sau se poate realiza şi la compilare, prin folosirea flagului -D al compilatorului (după cum am văzut în
exemplul precedent).
Un simbol poate fi de asemenea „şters” folosind instrucţiunea
#undef SIMBOL
în cazul în care nu se mai doreşte prezenţa simbolului de preprocesor ulterior definirii sale.
C – Definirea de macro-uri
Instrucţiunile de preprocesare mai pot fi folosite şi pentru definirea de constante simbolice şi
macroinstrucţiuni. De exemplu
#define CONSTANTA valoare
va duce la înlocuirea peste tot în cadrul codului sursă a şirului CONSTANTA cu şirul valoare. Înlocuirea
nu se face totuşi în interiorul şirurilor de caractere.
O macroinstrucţiune este similară unei constante simbolice, ca definire, dar acceptă parametrii. Este
folosită în program în mod asemănător unei funcţii, dar la compilare, ea este înlocuită în mod textual cu
corpul ei. În plus, nu se face nici un fel de verificare a tipurilor. Spre exemplu:
#define MAX(a, b) a > b ? a : b
va returna maximul dintre a şi b, iar
#define DUBLU(a) 2*a
va returna dublul lui a.
Atenţie! Deoarece preprocesarea este o prelucrare textuală a codului sursă, în cazul exemplului de mai
sus, macroinstrucţiunea în forma prezentată nu va calcula întotdeauna dublul unui număr.
Astfel, la un apel de forma:
DUBLU(a + 3)
în pasul de preprocesare se va genera expresia
2*a+3
care bineînţeles că nu realizează funcţia dorită.
Pentru a evita astfel de probleme, este bine ca întotdeauna în corpul unui macro, numele „parametrilor”
să fie închise între paranteze (ca de exemplu:)
#define SQUARE(a) (a)*(a)
C – Instrucţiuni de compilare condiţionată
Instrucţiunile de compilare condiţionată sunt folosite pentru a „ascunde” fragmente de cod în funcţie
de anumite condiţii. Formatul este următorul:
#if conditie
....
#else
....
#endif
unde conditie este este o expresie constantă întreagă. Pentru realizarea de expresii cu mai multe opţiuni
se poate folosi şi forma #elif:
#if conditie
...
#elif conditie2
...
#elif conditie3
...
#else
...
#endif
De obicei condiţia testează existenţa unui simbol. Scenariile tipice de folosire sunt:
dezactivarea codului de debug o dată ce problemele au fost remediate
compilare condiţionată în funcţie de platforma de rulare
prevenirea includerii multiple a fişierelor antet
În aceste cazuri se foloseşte forma
#ifdef SIMBOL
sau
#ifndef SIMBOL
care testează dacă simbolul SIMBOL este definit, respectiv nu este definit.
Prevenirea includerii multiple a fişierelor antet se realizează astfel:
#ifndef _NUME_FISIER_ANTET_
#define _NUME_FISIER_ANTET_
/* corpul fisierului antet */
/* prototipuri de functii, declaratii de tipuri si de constante */
#endif
Astfel, la prima includere a fişierului antet, simbolul _NUME_FISIER_ANTET_ nu este definit.
Preprocesorul execută ramura #ifndef în care este definit simbolul _NUME_FISIER_ANTET_ şi care
conţine şi corpul – conţinutul util – al fişierului antet.
La următoarele includeri ale fişierului antet simbolul _NUME_FISIER_ANTET_ va fi definit
iar preprocesorulva sări direct la sfârşitul fişierului antet, după #endif.
Alte instrucţiuni #pragma expresie
Sunt folosite pentru a controla din codul sursă comportamentul compilatorului (modul în care generează
cod, alinierea structurilor, etc.) iar formatul lor diferă de la compilator la compilator. Pentru a determina ce
opţiuni #pragma aveţi la dispoziţie consultaţi manualul compilatorului.
#error MESSAGE
La întâlnirea acestei instrucţiuni de preprocesare compilatorul va raporta o eroare, având ca text
explicativ mesajul MESSAGE.
#line NUMBER FILENAME
Această instrucţiune de preprocesare modifică numărul liniei curente în valoarea specificată
de NUMBER. În cazul în care este prezent şi parametru opţional FILENAME este modificat şi numele
fişierului sursă curent.
Astfel, mesajele de eroare şi avertismentele produse de compilator vor folosi numere de linie (şi eventual
nume de fişiere) inexistente, „imaginare”, conform acestei instrucţiuni.