This shows you the differences between two versions of the page.
programare:laboratoare:lab04 [2025/10/10 12:54] darius.neatu [Obiective] |
programare:laboratoare:lab04 [2025/10/15 18:30] (current) darius.neatu [Referinţe] |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ===== PCLP Suport Teoretic pentru Laborator: Funcții. Recursivitate. Clase de stocare ===== | + | ===== PCLP Laborator04: Tablouri. Particularizare - vectori și matrici ===== |
+ | ===== Tablouri. Particularizare - vectori. Aplicaţii ===== | ||
- | ==== Noţiuni teoretice ==== | + | **Responsabili:** |
+ | * [[neatudarius@gmail.com|Darius Neațu (2019-2020)]] | ||
+ | * [[ion_dorinel.filip@cti.pub.ro|Dorinel Filip (2019-2020)]] | ||
+ | * [[andrei.parvu@cti.pub.ro|Andrei Pârvu]] | ||
- | 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. | + | ==== Obiective ==== |
- | Î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''. | + | În urma parcurgerii acestui laborator, studentul va fi capabil: |
- | ==== Definirea şi apelul unei funcţii în C ==== | + | * să declare şi să iniţializeze vectori/matrici (din declaraţie şi prin structuri iterative) |
+ | * să implementeze algoritmi simpli de sortare şi căutare pe vectori | ||
+ | * să folosească practici recunoscute şi recomandate pentru scrierea de cod sursă care implică lucrul cu tablouri | ||
+ | * să recunoască şi să evite erorile comune de programare legate de aceste structuri | ||
+ | * sa cunoască regulile de reprezentare ale tablourilor în memorie și să înteleagă modul în care compilatorul interpretează operatorii de indexare; | ||
- | 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: | ||
- | <code bash> tip_returnat nume_functie (tip_param1 nume_param1 , tip_param2 nume_param2, ...); </code> | + | ==== Noţiuni teoretice ==== |
- | Această declarare poartă numele de **antetul funcţiei** (**function signature** sau simplu** signature**). Lista de parametri **poate** lipsi. | + | === Vectori === |
- | Odată declarată, o funcţie trebuie **definită**, în sensul că trebuie expandat corpul acesteia cu instrucţiunile pe care trebuie să le execute. | + | 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. |
- | Definirea unei funcţii are forma: | + | Declaraţia unei variabile de tip vector se face în felul următor: |
+ | <code bash> | ||
+ | <tip_elemente> <nume_vector>[<dimensiune>]; | ||
+ | </code> | ||
+ | |||
+ | De exemplu, avem următoarele declaraţii de vectori: | ||
<code c> | <code c> | ||
- | tip_returnat nume_functie(tip_param1 nume_param1, tip_param2 nume_param2, ...) { | + | int a[100]; |
- | declaratii de variabile si instructiuni; | + | float vect[50]; |
- | + | ||
- | return expresie; | + | #define MAX 100 |
- | } | + | ... |
+ | unsigned long numbers[MAX]; | ||
</code> | </code> | ||
- | 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ă. | + | 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: | ||
- | Diferite părţi din definirea unei funcţii pot lipsi. Astfel, o funcţie minimală este: | + | <code c> |
+ | int a[3] = {1, 5, 6}; // Toate cele 3 elemente sunt initializate | ||
+ | float num[] = {1.5, 2.3, 0.2, -1.3}; // Compilatorul determina dimensiunea - 4 - a vectorului | ||
+ | unsigned short vect[1000] = {0, 2, 4, 6}; // Sunt initializate doar primele 4 elemente, iar toate celelalte sunt initializate cu valoarea 0 | ||
+ | </code> | ||
- | <code c> dummy() {} </code> | + | Î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'': |
- | 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ă. | + | <code c> |
+ | char string[100] = {97}; // Sirul va fi initializat cu: 97 (caracterul 'a') pe prima poziţie şi 99 de 0 | ||
+ | int v[100] = {0}; // Vectorul va fi umplut cu 0. | ||
+ | </code> | ||
- | Tipul returnat de o funcţie poate fi orice tip **standard** sau **definit** de utilizator (**struct**-uri - acoperite într-un laborator următor), inclusiv tipul **void** (care înseamnă că funcția nu returnează nimic). | + | Este important de remarcat faptul că elementele **neiniţializate** pot avea valori **oarecare**. La alocarea unui vector, compilatorul nu efectuează nici un fel de iniţializare şi nu furnizează nici un mesaj de eroare dacă un element este folosit înainte de a fi iniţializat. **Un program corect va iniţializa**, în orice caz, fiecare element înainte de a-l folosi. |
+ | Elementele se accesează prin expresii de forma ''<nume_vector>[<indice>]''. De exemplu, putem avea: | ||
- | Orice funcţie care întoare un rezultat trebuie să conţină instrucţiunea: | + | <code c> |
+ | char vect[100]; | ||
+ | vect[0] = 1; | ||
+ | vect[5] = 10; | ||
- | <code c> return expression; </code> | + | int i = 90; |
+ | vect[i] = 15; | ||
+ | vect[i + 1] = 20; | ||
+ | </code> | ||
- | 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. | + | ==== Stil de programare ==== |
- | Exemplu: | + | === Exemple de programe === |
- | <code c declarare.c> | + | Citirea unui vector de intregi de la tastatura: |
- | int min(int x, int y); | + | |
- | </code> | + | |
- | <code c definire.c> | + | <code c> |
- | int min(int x, int y) { | + | int main() |
- | if (x < y) { | + | { |
- | return x; | + | int a[100], n, i; /* vectorul a are maxim 100 de intregi */ |
+ | |||
+ | scanf("%d", &n); /* citeste nr de elemente vector */ | ||
+ | |||
+ | for (i = 0; i < n; i++) { | ||
+ | scanf("%d", &a[i]); /* citire elemente vector */ | ||
+ | } | ||
+ | |||
+ | for (i = 0; i < n; i++) { | ||
+ | printf("%d ", a[i]); /* scrie elemente vector */ | ||
} | } | ||
| | ||
- | return y; | + | return 0; |
} | } | ||
</code> | </code> | ||
- | Apelul unei funcţii se face specificând **parametrii efectivi** (parametrii care apar în declararea funcţiei se numesc **parametri formali**). | + | Generarea unui vector cu primele n numere Fibonacci: |
<code c> | <code c> | ||
- | int main() { | + | #include <stdio.h> |
- | int a, b, minimum; | + | int main() |
- | //........... | + | { |
- | x = 2; | + | long fib[100] = {1, 1}; |
- | y = 5; | + | int n, i; |
- | minimum = min(x, 4); | + | |
- | printf("Minimul dintre %d si 4 este: %d", x, minimum); | + | printf("n = "); |
- | printf("Minimul dintre %d si %d este: %d", x, y, min(x, y)); | + | scanf("%d", &n); |
+ | | ||
+ | for (i = 2; i < n; i++) { | ||
+ | fib[i] = fib[i - 1] + fib[i - 2]; | ||
+ | } | ||
+ | for (i = 0; i < n; i++) { | ||
+ | printf("%ld ", fib[i]); | ||
+ | } | ||
+ | |||
+ | return 0; | ||
} | } | ||
</code> | </code> | ||
- | ==== Transmiterea parametrilor ==== | + | <note warning> |
+ | Deşi modul în care se manifestă erorile din program poate fi surprinzător şi imprevizibil, cauzele care produc aceste erori sunt destul de comune şi pot fi grupate în mai multe categorii. Câteva dintre acestea sunt prezentate mai jos | ||
+ | </note> | ||
+ | * 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! | ||
- | Apelul unei funcţii se face specificând parametrii care se transmit acesteia. În limbajul C, dar şi în alte limbaje de programare există ** 2 moduri de transmitere a parametrilor**. Deoarece nu avem încă cunoștințele necesare pentru a înțelege ambele moduri, astăzi vom studia doar unul, urmând ca în laboratorul 8 să revenim și să îl explicăm și pe al doilea. | + | <note tip> |
+ | Definiţi dimensiunile prin constante şi folosiţi-le pe acestea în locul tastării explicite a valorilor în codul sursă. Astfel veţi evita neconcordanţe în cod dacă doriţi ulterior să modificaţi dimensiunile şi uitaţi să modificaţi peste tot prin cod. | ||
+ | </note> | ||
- | === Transmiterea parametrilor prin valoare === | + | <code c> |
- | + | #define MAX 100 | |
- | 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. | + | int vect[MAX]; |
- | + | </code> | |
- | Exemplu de transmitere a parametrilor prin valoare: | + | va fi de preferat în locul lui |
<code c> | <code c> | ||
- | min(x, 4); // se face o copie lui x | + | int vect[100]; |
</code> | </code> | ||
<note tip> | <note tip> | ||
- | Până acum aţi folosit în programele voastre funcţii care trimit valorile atât prin valoare (de exemplu ''printf()'') cât şi prin intermediul adresei de memorie (de exemplu ''scanf()''). Mecanismul de transfer al valorilor prin intermediul adresei de memorie unde sunt stocate va fi complet „elucidat” în laboratorul de pointeri. | + | 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. |
</note> | </note> | ||
- | ==== Funcţii recursive ==== | + | Exemplu: |
- | 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 o **condiţie de oprire**, astfel ca, la un moment dat, recurenţa să se oprească şi să se revină succesiv din apeluri. | + | <code c> |
+ | // program care citeşte un index şi o valoare, şi atribuie valoarea elementului din vector care se găseşte la poziţia respectivă | ||
+ | #include <stdio.h> | ||
+ | #define N 10 | ||
- | Condiţia trebuie să fie una generică, şi să oprească recurenţa în orice situaţie. Această condiţie se referă în general a 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. | + | int main() |
+ | { | ||
+ | int i, val; | ||
+ | int v[N]; | ||
- | ''Exemplu'': Calculul recursiv al factorialului | + | scanf("%d%d", &i, &val); |
- | <code c> | + | /* !!! Verific daca indexul este valid */ |
- | int fact(int n) { | + | if (i >= 0 && i < N) { |
- | if (n == 0) { | + | v[i] = val; |
- | return 1; | + | |
} else { | } else { | ||
- | return n * fact(n - 1); | + | printf("Introduceti un index >= 0 si < %d\n", N); |
} | } | ||
+ | |||
+ | return 0; | ||
} | } | ||
</code> | </code> | ||
- | sau, într-o formă mai compactă: | + | <note tip> |
+ | 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. | ||
+ | </note> | ||
+ | |||
+ | Exemplu: | ||
<code c> | <code c> | ||
- | int fact(int n) { | + | #include <stdio.h> |
- | return (n >= 1) ? n * fact(n - 1) : 1; | + | #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; | ||
} | } | ||
</code> | </code> | ||
- | Î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 [[http://en.wikipedia.org/wiki/Stack_overflow | stack overflow]] (vezi şi [[http://en.wikipedia.org/wiki/Infinite_recursion | Infinite recursion pe Wikipedia]]). | + | ==== Aplicaţii cu vectori ==== |
+ | |||
+ | === Căutări === | ||
+ | |||
+ | == Căutare secvenţială == | ||
- | ==== Funcţia main ==== | + | 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). |
- | Orice program C conţine cel puţin o funcţie, şi anume cea principală, numită ''main()''. Aceasta are un format special de definire: | + | ''Exemplu'': |
<code c> | <code c> | ||
- | int main(int argc, char *argv[]) | ||
- | { | ||
- | // some code | ||
- | return 0; | ||
- | } | ||
- | </code> | ||
- | 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''. Lucrul cu parametrii liniei de comandă va fi reluat într-un laborator viitor. | + | #define MAX 100 |
- | 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: | + | ... |
- | <code c> | + | int v[MAX], x, i; |
- | int main(void) | + | |
- | { | + | /* initializari */ |
- | // some code | + | ... |
- | return 0; | + | |
+ | int found = 0; | ||
+ | for (i = 0; i < MAX; i++) { | ||
+ | if (x == v[i]) { | ||
+ | found = 1; | ||
+ | break; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | if (found) { | ||
+ | printf("Valoarea %d a fost gasita in vector\n", x); | ||
+ | } else { | ||
+ | printf("Valoarea %d nu a fost gasita in vector\n", x); | ||
} | } | ||
+ | ... | ||
</code> | </code> | ||
- | În ambele cazuri, standardul impune 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. | + | == Căutare binară iterativă == |
- | Î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ă. | + | 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). |
- | ==== Tipul de date void ==== | + | 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. |
- | Tipul de date ''void'' are mai multe întrebuinţări. | + | 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. |
- | Atunci când este folosit ca tip returnat de o funcţie, specifică faptul că funcţia nu întoarce nici o valoare. Exemplu: | + | Pseudocodul pentru căutarea binară: |
<code c> | <code c> | ||
- | void print_nr(int number) { | + | // cauta elementul x in vectorul sortat v, intre pozitiile 0 si n-1 si returneaza pozitia gasita sau -1 |
- | printf("Numarul este %d", number); | + | int binary_search(int n, int v[NMAX], int x) { |
+ | int low = 0, high = n - 1; | ||
+ | |||
+ | while (low <= high) { | ||
+ | // De ce preferăm această formă față de (low + high) / 2 ? | ||
+ | int middle = low + (high - low) / 2; | ||
+ | |||
+ | if (v[middle] == x) { | ||
+ | // Am gasit elementul, returnam pozitia sa | ||
+ | return middle; | ||
+ | } | ||
+ | |||
+ | if (v[middle] < x) { | ||
+ | // Elementul cautat este mai mare decat cel curent, ne mutam in jumatatea | ||
+ | // cu elemente mai mari | ||
+ | low = middle + 1; | ||
+ | } else { | ||
+ | // Elementul cautat este mai mic decat cel curent, ne mutam in jumatatea | ||
+ | // cu elemente mai mici | ||
+ | high = middle - 1; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // Elementul nu a fost gasit | ||
+ | return -1; | ||
} | } | ||
</code> | </code> | ||
- | Atunci când este folosit în declaraţia unei funcţii, ''void'' semnifică faptul că funcţia nu primeşte nici un parametru. Exemplu: | + | <note tip> |
+ | Preferăm calcularea mijlocului intervalului [low, high] folosind formula ''x = low + (high - low) / 2'' deoarece formula perfect analogă ''x = (low + high) / 2'' poate da overflow pentru valori mari ale low si high. De altfel, acest bug a existat în biblioteca Java timp de 9 de ani. Puteți citi mai mult despre asta în [[http://googleresearch.blogspot.ro/2006/06/extra-extra-read-all-about-it-nearly.html | acest articol]]. | ||
+ | </note> | ||
+ | === 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. | ||
+ | |||
+ | Studiul unor algoritmi mai avansaţi de sortare, precum şi studiul complexităţii lor nu constituie obiectul acestui laborator. Acestea se vor relua mai detaliat în cadrul altor cursuri (AA/PA). | ||
+ | |||
+ | ===== Matrice. Operaţii cu matrice: adunare, înmulţire. Reprezentarea în memorie. ===== | ||
+ | |||
+ | **Responsabili:** | ||
+ | * [[neatudarius@gmail.com|Darius Neațu (CA 2019-2020)]] | ||
+ | * [[ion_dorinel.filip@cti.pub.ro|Dorinel Filip (CA 2019-2020)]] | ||
+ | * [[andrei.parvu@cti.pub.ro|Andrei Pârvu]] | ||
+ | |||
+ | [[https://docs.google.com/document/d/16GNacxqmnRjaBx7w8tt9rRprbpiOjx8njzIrv4Q0mI0/edit?usp=sharing|Probleme]] | ||
+ | |||
+ | [[https://we.tl/t-HTa9YV1Wvg|Teste]] | ||
+ | |||
+ | === 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: | ||
+ | |||
+ | <code bash> | ||
+ | <tip_elemente> <nume_matrice>[<dim_1>][<dim_2>]; | ||
+ | </code> | ||
+ | |||
+ | De exemplu, avem: | ||
<code c> | <code c> | ||
- | int init(void) { | + | int mat[5][10]; |
- | return 1; | + | </code> |
- | } | + | <code c> |
+ | #define MAX_ELEM 100 | ||
+ | float a[MAX_ELEM][MAX_ELEM]; | ||
</code> | </code> | ||
- | <note important> | + | 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ă. |
- | Această declaraţie nu este similară cu următorul caz: | + | |
+ | === 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: | ||
+ | |||
+ | <code bash> | ||
+ | <tip_elemente> <nume_tablou>[<dim_1>][<dim_2>]...[<dim_n>]; | ||
+ | </code> | ||
+ | |||
+ | De exemplu: | ||
<code c> | <code c> | ||
- | int init() { | + | int cube[3][3][3]; |
- | return 1; | + | |
- | } | + | |
</code> | </code> | ||
- | În cel de-al doilea caz, compilatorul nu verifică dacă funcţia este într-adevăr apelată fără nici un parametru. Apelul celei de-a doua funcţii cu un număr arbitrar de parametri nu va produce nici o eroare, în schimb apelul primei funcţii cu un număr de parametri diferit de zero va produce o eroare de tipul: | + | 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. În acest laborator ne vom rezuma totuşi la tablouri bidimensionale. |
- | ''too many arguments to function''. | + | === Adunarea si înmulţirea matricelor === |
- | </note> | + | |
+ | == Suma matricelor == | ||
- | ==== Clase de stocare. Fişiere antet vs. biblioteci ==== | + | Fie $A \in {\mathbb R}^{m \times n}$, $B \in {\mathbb R}^{m \times n}$, atunci $(A+B) \in {\mathbb R}^{m \times n}$, unde: $(A+B)_{i,j} = A_{i,j} + B_{i,j}$ |
- | <note tip> | + | |
- | Această secţiune este importantă pentru înţelegerea modului de lucru cu mai multe fişiere sursă şi cu bibliotecile oferite de GCC. Deşi în continuare sunt discutate în contextul funcţiilor, lucrurile se comportă aproximativ la fel şi în cazul variabilelor globale (a căror utilizare este, oricum, descurajată). | + | |
- | </note> | + | |
- | 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ă. | + | {{:programare:laboratoare:sum.png|}} |
- | ''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). | + | Exemplu: |
- | 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. | + | $$ |
+ | \Large | ||
+ | \begin{bmatrix} | ||
+ | 1 & 3 \\ | ||
+ | 0 & 4 \\ | ||
+ | 5 & 8 | ||
+ | \end{bmatrix} | ||
+ | + | ||
+ | \begin{bmatrix} | ||
+ | 2 & 5 \\ | ||
+ | 1 & 2 \\ | ||
+ | 6 & 1 | ||
+ | \end{bmatrix} | ||
+ | = | ||
+ | \begin{bmatrix} | ||
+ | 3 & 8 \\ | ||
+ | 1 & 6 \\ | ||
+ | 11 & 9 | ||
+ | \end{bmatrix} | ||
+ | $$ | ||
- | ''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ă. | + | == Înmulţirea matricelor == |
- | 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: | + | Fie $A \in {\mathbb R}^{m \times n}$, $B \in {\mathbb R}^{n \times p}$, atunci $(AB) \in {\mathbb R}^{m \times p}$, unde elementele $A.B$ sunt date de formula: |
+ | $(AB)_{i,j} = \sum_{r=1}^n A_{i,r}B_{r,j}$ | ||
- | <code c> | ||
- | static int compute_internally(int, int); | ||
- | </code> | ||
- | 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. | + | {{:programare:laboratoare:mul.png|}} Exemplul din stânga 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ă |
+ | $$ | ||
+ | (\mathbf{AB})_{1,2} = \sum_{r=1}^2 a_{1,r}b_{r,2} = a_{1,1}b_{1,2}+a_{1,2}b_{2,2} \\ | ||
+ | (\mathbf{AB})_{3,3} = \sum_{r=1}^2 a_{3,r}b_{r,3} = a_{3,1}b_{1,3}+a_{3,2}b_{2,3} | ||
+ | $$ | ||
- | ==== Exerciții Laborator CB/CD ==== | + | Exemplu: |
- | - Primul exercitiu presupune modificarea/adaugarea de instructiuni unui cod existent pentru a realiza anumite lucruri. In momentul actual programul afiseaza suma cifrelor unui numar. | + | $$ |
- | *Nu uitati ca trebuie sa utilizam un [[http://ocw.cs.pub.ro/courses/programare/coding-style| coding style]] adecvat atunci cand scriem sursele. | + | \Large |
- | <code c ex1.c> | + | |
+ | \begin{bmatrix} | ||
+ | 1 & 0 & 2 \\ | ||
+ | -1 & 3 & 1 | ||
+ | \end{bmatrix} | ||
+ | \cdot | ||
+ | \begin{bmatrix} | ||
+ | 3 & 1 \\ | ||
+ | 2 & 1 \\ | ||
+ | 1 & 0 | ||
+ | \end{bmatrix} | ||
+ | = | ||
+ | \begin{bmatrix} | ||
+ | 1 \times 3 + 0 \times 2 + 2 \times 1 & 1 \times 1 + 0 \times 1 + 2 \times 0 \\ | ||
+ | -1 \times 3 + 3 \times 2 + 1 \times 1 & -1 \times 1 + 3 \times 1 + 1 \times 0 | ||
+ | \end{bmatrix} | ||
+ | = | ||
+ | \begin{bmatrix} | ||
+ | 5 & 1 \\ | ||
+ | 4 & 2 | ||
+ | \end{bmatrix} | ||
+ | $$ | ||
- | #include <stdio.h> | + | === Reprezentarea în memorie === |
- | + | ||
- | int sum_recursive(int n) | + | |
- | { | + | |
- | if (n == 0) { | + | |
- | return 0; | + | |
- | } | + | |
- | + | ||
- | return n % 10 + sum_recursive(n / 10); | + | |
- | } | + | |
- | + | ||
- | + | ||
- | int main(void) | + | |
- | { | + | |
- | int nr; | + | |
- | + | ||
- | scanf("%d", &nr); | + | |
- | + | ||
- | printf("%d\n", sum_recursive(nr)); | + | |
- | + | ||
- | return 0; | + | |
- | } | + | |
- | </code> | + | 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: | ||
- | Cerinte: | + | <code c> |
- | *Scrieti o functie care realizeaza tot suma cifrelor, insa intr-un mod nerecursiv. | + | char a[100]; |
- | *Modificati functia recursiva astfel incat sa realizeze doar suma cifrelor impare. | + | a[0] = 1; |
- | *Modificati functia recursiva astfel incat la prima cifra impara sa nu mai mearga in recursivitate si sa intoarca suma realizata pana in acel moment. | + | 3[a] = 5; |
- | *Luati urmatoarea [[http://swarm.cs.pub.ro/~gmuraru/lab4-example.tar|arhiva]] si observati cum sunt structurate fisierele. Rulati **make** pentru a crea executabilul si **make clean** pentru stergerea fisierelor generate. | + | </code> |
- | **Următoarele două probleme vă vor fi date de asistent în cadrul laboratorului.** | + | 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; |
- | [[https://drive.google.com/file/d/1BH7Z__2eg5g5sIAMm5AETW4ityNexqk6/view?usp=sharing|Checker si teste laborator 4]] | + | 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); | ||
- | <spoiler Cum se foloseste checkerul> | + | <note> |
- | Pentru utilizarea checkerului: | + | Este de remarcat faptul că pentru **tablouri de dimensiuni m > 1**, este necesară precizarea **lungimilor ultimelor m - 1 dimensiuni**, pentru ca compilatorul să poată calcula adresa fiecărui element atunci când acesta este referit în program. |
- | *Se va scrie cate un fisier sursa pentru fiecare problema; | + | Mai multe detalii legate de reprezentarea tablourilor şi, în general, a datelor în memorie, vor fi date în laboratorul 8. |
- | *La finalul fiecarui printf utilizat pentru afisarea rezultatului trebuie sa existe un newline; | + | </note> |
- | *Sursa nu trebuie sa contina alte printf-uri in afara de cele care scriu rezultatul asteptat la stdout. | + | |
- | *Se va dezarhiva arhiva specifica exercitiului; | + | |
- | *In directorul curent se afla checkerul, executabilul generat, folderele de input si output specifice problemei; | + | |
- | *Se va rula “bash checker.sh <executabil>” unde <executabil> este numele executabilului generat; | + | |
- | </spoiler> | + | === Exemple de programe === |
- | <hidden> | + | * Declararea unei matrici unitate: |
- | Link direct către lista completă de probleme: [[https://github.com/cs-pub-ro/ComputerProgramming-internal/tree/master/Laboratories/Lab4|aici]] | + | <code c> |
- | </hidden> | + | #define M 20 /* nr maxim de linii si de coloane */ |
+ | int main() { | ||
+ | float unit[M][M]; | ||
+ | int i,j,n; | ||
+ | | ||
+ | printf("nr.linii/coloane: "); | ||
+ | scanf("%d", &n); | ||
+ | if (n > M) { | ||
+ | return; | ||
+ | } | ||
+ | | ||
+ | for (i = 0; i < n; i++) { | ||
+ | for (j = 0; j < n; j++) { | ||
+ | if (i != j) { | ||
+ | unit[i][j]=0; | ||
+ | } else { | ||
+ | unit[i][j]=1; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | return 0; | ||
+ | } | ||
+ | </code> | ||
- | ===== Exerciţii de Laborator ===== | + | * Citire/scriere de matrice de reali: |
- | - [2 pct]: Implementați o funcție **int factorial_iterativ(int n)** care returnează n! = 1 * 2 * 3 * … * (n – 1) * n calculat iterativ. Implementați funcția **int factorial_recursiv(int n)** care are același scop, dar implementarea este recursivă. | + | <code c> |
- | - [2 pct]: Scrieți o funcție recursivă și una iterativă care returnează suma cifrelor unui număr natural **int suma_cifre_recursiv(int n);** respectiv **int suma_cifre_iterativ(int n);**. | + | int main() { |
- | - [1 pct]: Scrieți o funcție care determină dacă un număr este prim sau nu (întoarce 1 dacă numărul este prim sau 0 în caz contrar): **int este_prim(int n);**. | + | int nl, nc, i, j; |
- | - [1.5 pct]: Scrieți o funcție care determină dacă un număr natural este palindrom sau nu (returnează 1 în cazul în care este palindrom sau 0 în caz contrar): **int este_palindrom(int n);** | + | float a[20][20]; |
- | - [1.5 pct]: Scrieți o funcție care citește de la tastatură un număr natural și calculează câți divizori sunt numere palindrom. Se va afișa la consolă divizorii care sunt palindrom, precum și numărul acestora. Alegeți un nume funcției implementate și apelați această funcție din main pentru a o testa. | + | |
- | - [2 pct]: Scrieți o funcție recursivă care citește de la tastatură câte un număr natural (citirea unui număr negativ duce la ignorarea sa) și incrementează un contor de fiecare data când este tastat un număr prim. Citirea se încheie când se întâlnește numărul 0. Afișați contorul după finalizarea citirii de numere de la tastatură. | + | /* Citire de matrice */ |
- | + | printf("nr.linii: "); | |
- | ===Bonus=== | + | scanf("%d", &nl); |
- | - [2 pct]: Folosind declarații si definiri de variabile și funcții, creați două fișiere main.c și autentificare.c, și apleați din funcția main din fișierul main.c o funcție void login(int username, int password) definită în autentificare.c. Funcția login va afișa mesajul “Autentificare cu success!”, respectiv “Autentificare esuata!” în urma verificării celor două argumente primite care trebuie să fie numere prime între ele, pentru ca autentificarea să aibă loc cu succes. De asemenea, sursa autentificare.c va cuprinde o variabilă numită status care va fi setată de funcția login cu 1 în cazul autentificării cu success, respective 0 în caz contrar. Afișați în main, valoarea acestei variabile. Compilați fișierele împreună și executați programul rezultat. Scriți fișierul autentificare.h cu antetul funcției implementată în autentificare.c și NU uitați să îl includeți în main.c. | + | printf("nr.coloane: "); |
- | <code bash> | + | scanf("%d", &nc); |
- | gcc main.c autentificare.c; ./a.out | + | |
+ | if (nl > 20 || nc > 20) { | ||
+ | printf("Eroare: dimensiuni > 20 \n"); | ||
+ | return ; | ||
+ | } | ||
+ | |||
+ | for (i = 0; i < nl; i++) { | ||
+ | for (j = 0; j < nc; j++) { | ||
+ | scanf("%f", &a[i][j]); | ||
+ | } | ||
+ | } | ||
+ | | ||
+ | /* Afisare matrice */ | ||
+ | for (i = 0; i < nl; i++) { | ||
+ | for (j = 0; j < nc; j++) { | ||
+ | printf("%f ",a[i][j]); | ||
+ | } | ||
+ | printf ("\n"); | ||
+ | } | ||
+ | |||
+ | return 0; | ||
+ | } | ||
</code> | </code> | ||
- | ===== Referinţe ===== | ||
- | * [[http://www.tutorialspoint.com/ansi_c/c_using_functions.htm | C - Using Functions]] | + | ==== Erori comune ==== |
- | * [[https://github.com/cs-pub-ro/ComputerProgramming/blob/master/Laboratories/Lab4/Lab4.pdf | Cheatsheat Functii]] | + | |
- | <hidden> | + | * 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. |
- | ===== Soluții ===== | + | |
+ | ==== Exerciții ==== | ||
+ | Exercițiile pentru laborator se găsesc pe [[https://acs-pclp.github.io/laboratoare/04 | PCLP Laborator04: Tablouri. Particularizare: vectori și matrici]]. | ||
+ | |||
+ | |||
+ | ===== Referinţe ===== | ||
+ | * [[https://github.com/cs-pub-ro/ComputerProgramming/blob/master/Laboratories/Lab5/Lab5.pdf| Cheatsheet vectori]] | ||
+ | * [[http://en.wikipedia.org/wiki/Binary_search | Wikipedia - Căutare binară]] | ||
+ | * [[http://en.wikipedia.org/wiki/Sorting_algorithm |Wikipedia - Algoritmi de sortare]] | ||
+ | * [[http://en.wikipedia.org/wiki/Matrix_multiplication | Wikipedia - Matrix Multiplication]] | ||
+ | * [[http://ocw.cs.pub.ro/courses/so/laboratoare/resurse/gdb | GDB - Tutorial]] | ||
- | </hidden> |