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
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 -o nume_fișier
Efect 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)
Caută fișiere antet și în calea specificată Icale_catre_fisiere_antet -L cale_catre_biblioteci Caută fișiere bibliotecă și în calea specificată
-l nume_biblioteca
-W tip_warning
-c -S
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 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 Compilează și asamblează, dar nu link-editează. Rezultă fișiere obiect, cu extensia .o 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). 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 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) Indică lungimea câmpului din care se face citirea. În cazul în care e nespecificat, citirea Lung 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 L h
Șir de caractere Indică un întreg long sau un real double 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 [] 1 . şi -> ++ şi – ! ~ ++ şi – + şi 2 * & (tip) sizeof() * 3 / % 4 + şi 5 << si >> < <= 6 > >= == 7 != 8 & 9 ^ 10 | 11 &&
Descriere
Asociativitate Indexare stanga-dreapta Selecţie membru (prin structură, respectiv pointer) stânga-dreapta Postincrementare/postdecrementare stânga-dreapta Negare logică dreapta-stânga Complement faţă de 1 pe biţi dreapta-stânga Preincrementare/predecrementare dreapta-stânga + şi – unari dreapta-stânga Dereferenţiere dreapta-stânga Operator adresă dreapta-stânga Conversie de tip dreapta-stânga Mărimea în octeţi dreapta-stânga Înmulţire stânga-dreapta Împărţire stânga-dreapta Restul împărţirii stânga-dreapta Adunare/scădere stânga-dreapta Deplasare stânga/dreapta a biţilor stânga-dreapta Mai mic stânga-dreapta Mai mic sau egal stânga-dreapta Mai mare stânga-dreapta Mai mare sau egal stânga-dreapta Egal stânga-dreapta Diferit stânga-dreapta ŞI pe biţi stanga-dreapta SAU-EXCLUSIV pe biţi stânga-dreapta SAU pe biţi stânga-dreapta ŞI logic stânga-dreapta
12 13
14
15
|| SAU logic :? Operator condiţional = Atribuire += şi -= Atribuire cu adunare/scădere *= şi /= Atribuire cu multiplicare/împărţire %= Atribuire cu modulo &= si |= Atribuire cu ŞI/SAU ^= Atribuire cu SAU-EXCLUSIV <<= şi >>= Atribuire cu deplasare de biţi , Operator secvenţa
stânga-dreapta dreapta-stânga dreapta-stânga dreapta-stânga dreapta-stânga dreapta-stânga dreapta-stânga dreapta-stânga dreapta-stânga 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 #include 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 ); Calculează arcsinusul/arccosinusul valorii arg; rezultatul este double acos( double arg ); măsurat în radiani double atan( double arg ); double atan2( double y, Calculează arctangenta valorii arg, respectiv a fracției y/x double x ); double floor( double num );
Întoarce cel mai mare întreg mai mic sau egal cu num (partea întreagă inferioară) Întoarce cel mai mic întreg mai mare sau egal cu num (partea double ceil( double num ); întreagă superioară) double double double double double double
sin( double arg ); cos( double arg ); tan( double arg ); sinh( double arg ); cosh( double arg ); tanh( double arg );
Calculează sinusul/cosinusul/tangenta parametrului arg, considerată în radiani 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, Întoarce restul împarțirii lui x la y double 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
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: [];
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 vectoru */ 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 */
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 [].
De exemplu, putem avea: char vect[100]; int i = 90;
vect[0] = vect[5] = vect[i] = vect[i+1]
1; 10; 15; = 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
Generarea unui vector cu primele n numere Fibonacci: #include int main () { long fib[100]={1,1}; int n,i; printf ("n="); scanf("%d",&n); for (i=2; i
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 respectivă #include 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 #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
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: [][];
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: [][]...[];
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
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
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;
int b = 5;
char *c; void *buff = NULL; *a = 1;
a = &b;
*a = 5; valoarea 5
// Pointer // Variabila // Pointer catre un caracter (sau sir de caractere)
// Pointer catre void, initializat la NULL
// Asignare INVALIDA; a nu este initializat la o adresa de memorie // Asignare valida; a ia adresa variabilei b
// Asignare valida; continutul memoriei de la adresa a (care a fost in // 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
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
#include
// 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
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
#include
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
#include
#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
#include
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 for (i = 0; i for (j = 0; mat[i][j]
matrice < n; i++) j < n; 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
#include
#include
// 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 #include 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 #include 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 #include 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 #include 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 #include 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 #include 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 #include 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 #include 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 #include 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 #include 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 #include 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şanumitul „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 macrodefiniţ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 #include 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 "" # 1 "" # 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
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.