Introducere în programare - curs și laborator - suport electronic

(C) 2024-2025 Bogdan Pătruț

Meniu

Lectia 1 | Lectia 2 | Lectia 3 | Lectia 4 | Lectia 5 | Lectia 6 | Lectia 7
Material pentru laboratoare

Lectia 3 - Enumerări, structuri, tablouri și pointeri

În acest curs vor fi prezentate tipurile de date enumerare, tablouri, structuri. Vor fi prezentate unele lucruri mai puțin cunoscute privind reprezentarea tablourilor, parcurgerea lor, funcții cu tablouri ca argumente. De asemenea, se vor explica câmpurile de biți și legătura dintre pointeri și tablouri.

Rezumat prima parte



Rezumat partea a doua



Video

3.1. Enumerări

Există situaţii în practica în care un program trebuie să prelucreze variabile de tip întreg care au câteva valori discrete cărora li se atribuie o anumită semnificaţie.
Exemplu Spre exemplu, un program care prelucrează date calendaristice va trebui să poată prelucra variabile care semnifică zilele săptămânii. În acest caz, se poate stabili o convenţie prin care fiecărei zile din săptămână i se atribuie o valoare întreagă (spre exemplu ziua de luni se reprezintă prin valoarea 0, ziua de duminica se reprezintă prin valoarea 6).
ATENTIE! Codul acesta nu va functiona in C++, ci doar in C.
			int main ()
			{
				int azi = 3; /* joi */
				int ieri = azi – 1;
				return 0;
			}
		
Dificultatea constă însă în memorarea de către programator a valorilor asociate fiecărei zile din săptămână. O prima soluţie ar fi utilizarea unor constante declarate astfel:
			const int luni = 0; const int marti = 1; const int miercuri = 2; const int joi = 3;
			const int vineri = 4; const int sambata = 5; const int duminica = 6;
			int main () { 
				int azi = joi; /* joi */ 
				int ieri = azi – 1; 
				return 0;
			}
		
Aceasta soluţie îmbunătăţeşte claritatea programului, dar presupune un oarecare efort şi atenţie sporită pentru declararea constantelor luni ... duminica.
În situaţii de tipul acesta, pentru a spori claritatea programelor, limbajul C oferă posibilitatea de a defini o enumerare Limbajul C permite definire unui set de constante numerice întregi între care există o legătură din punctul de vedere al programatorului utilizând o declarație de tip enum.
ATENTIE! Codul acesta nu va functiona in C++, ci doar in C.
Exemple
			enum zile { luni, marti, miercuri, joi, vineri, sambata, duminica };
			int main ()
			{
				enum zile azi; enum zile ieri;	...
				azi = joi; ieri = azi – 1;
				return 0;
			}
			
			enum culori { rosu, albastru, verde, violet, galben, portocaliu, maro };
			int main ()
			{
				enum culori culoare1; enum culori culoare2;	...
				culoare1 = verde; culoare2 = culoare1 + 2;
				return 0;
			}
		
Sintaxa completă pentru declararea unei enumerări este următoarea:
		enum nume_enumerare {
		constanta1 = valoare1,
		constanta2 = valoare2,
		...,
		constantaN = valoareN
	} variabila1, ...,variabilaN;
	
În aceasta declaraţie, nume_enumerare este opţional, dar dacă se specifică, se pot crea ulterior şi alte variabile întregi asociate acestui tip (enum zile azi; enum zile ieri). De asemenea, valorile valoare1 ... valoareN, asociate fiecărei constante sunt opţionale. Dacă nu se specifică explicit, se aplică următoarea regulă: Așadar variabilele declarate pe baza unei enumerari sunt variabile de tip întreg. Ulterior, în program, simbolurile constanta1 ... constantaN se tratează ca orice constante cu valorile stabilite pe baza regulii precedente şi se pot atribui oricăror variabile de tip întreg.
Exemplu
		Ele sunt asociate enumerării zile doar pentru a indica intenţia programatorului de a folosi 
		aceste variabile pentru a reprezenta zilele săptămânii, îmbunătăţind claritatea programului.
		
Odată cu declararea unei enumerari, opţional, se pot declara şi variabile care să fie asociate acelei enumerări (variabila1 ... variabilaN).
Exemplu
			enum gen_substantiv {
			neutru, /* valoarea implicită 0 */
			feminin = 12, /* valoarea explicită 12 */
			masculin /* valoarea implicită 13 */
		} gen1, gen2; /* 2 variabile întregi */
	
Evident, aceeaşi constantă (simbol) nu poate să apară în mai multe declaraţii de enumerări: enum zile { luni, marti, miercuri, joi, vineri, sambata, duminica }; /* eroare, constantele sambata şi duminica au fost definite deja */ enum zile_weekend { sambata, duminica };
Exemple
		enum culori {
			rosu, /* valoare implicită 0 */
			verde, /* valoare implicită 1 */
			albastru /* valoare implicită 2 */
		} c; /* variabila de tip întreg */
		enum zile {
			luni = 1, /* valoare explicită 1 */
			marti, /* valoare implicită 2 */
			miercuri, joi, vineri, sambata,
			duminica /* ajunge la valoarea 7 */
		} ieri, azi, maine;
		int main ()
		{
			c = rosu; azi = joi; ieri = azi – 1; maine = azi + 1;
			printf ("Azi este: %d \n", azi); /* afiseaza 4 */
			char sir [80];
			switch (c)
			{
				case rosu: strcpy (sir, "rosu"); break;
				case verde: strcpy (sir, "verde");break;
				case albastru: strcpy (sir, "albastru"); break;
				default: strcpy (sir, "Eroare, contactaţi programatorul !");
			}
			printf ("Culoarea: %s \n", sir); /*afişează "rosu" */
			return 0;
		}
		

3.2. Tablouri

Tablourile reprezintă o colecție de variabile de același tip, apelate cu același nume. Componentele tabloului sunt identificate și accesate cu ajutorul indicilor. Un tablou ocupă locații de memorie contigue, în ordinea indicilor. Tablourile pot fi: unidimensionale (1 – dimensionale, vectori, șiruri) (un caz special constituindu-l șirurile de caractere), bidimensionale (2 - dimensionale) sau cu mai multe dimensiuni.

3.2.1. Tablouri unidimensionale

Acestea se declară astfel:
		tip numeTablou[dimensiune];
	
Numărul de octeți necesari reprezentării unui tablou unidimensional este = sizeof(tip) * dimensiune.
Exemplu
			#define NMAX 25
			int a[NMAX]; // int a[25];
		
Reprezentare Ordinea de memorare a elementelor din tablou este ordinea indicilor: 0, 1, ..., NMAX-1, elementele tabloului fiind în zone de memorie contigue (una langa alta);
La un tablou, operaţiile (de citire, scriere, actualizare) se realizează prin intermediul componentelor, folosind iterații forwhile sau do-while:
Exemplu
			for (i=0; i<n; i++) a[i]=0; 
			for (i=0; i<n; i++) c[i]=a[i]+ b[i];
Numele unui tablou reprezintă atât un nume de variabilă, cât și un pointer către primul element din tablou. Astfel, avem următoarele echivalențe:
Exemple de iniţializare a unui tablou unidimensional

Exemplu de memorare a unui vector
Variante de parcurgere a unui vector Avem un vector (tablou unidimensional) a de numere. Dorim să calculăm suma elementelor sale, după ce am inițializat suma cu 0. Avem mai multe modalități de a parcurge tabloul, care ține de legătura dintre tablouri și pointeri.
Tablourile ca argumente ale funcțiilor
Exemplul 1 Să considerăm că avem un tablou i și apelăm o funcție functia cu argumentul i.

În acest caz, functia va putea avea una din următoarele antete, în care argumentul poate fi: pointer la un numar întreg, un tablou cu un număr specificat de elemente întregi, un tablou cu un număr nespecificat de elemente întregi.
Exemplul 2 În următorul exemplu, funcția insert_sort are ca argument un tablou cu un număr nespecificat de elemente întregi. Deoarece argumentul a[] este echivalent cu pointerul *a, funcția va modifica elementele tabloului a, realizând, de pildă, sortarea prin inserție a elementelor vectorului a, avand n elemente.
Exemplul 3 În acest exemplu, presupunem ca funcția suma dorește să realizeze suma celor n elemente din tabloul a.

3.2.2. Șiruri de caractere

Șirurile de caractere sunt tablouri unidimensionale de tip char. Fiecare element al tabloului este un caracter. Ultimul caracter al șirului este caracterul nul '\0'. Acesta marchează sfârșitul șirului. Exemplu:
char sir[10];
Aceasta este o declarație pentru un șir de 9 caractere (sir[0], sir[1]..., sir[8]), la care se adaugă sir[9], ce va marca sfârșitul șirului. Al 10-lea caracter va fi caracterul nul '\0' (sau NULL sau 0). Un șir de caractere ("string") este o zonă de memorie ocupată cu caractere/"char"-uri (un "char" ocupă un octet), terminată cu un octet de valoare 0 (deoarece caracterul ‘\0’ are codul ASCII egal cu 0). O variabilă care reprezintă un șir de caractere este un pointer (reține adresa) la primul octet.
Un șir de caractere se poate reprezenta ca:
Exemplu declarare șir de caractere
Declarare șiruri cu inițializare
Asignarea șirurilor de caractere (=) se poate face doar la declarare:
			char sir[10];
			sir = "Hello"; // gresit
			char sir[10] = "Hello";// corect
		
Pentru a copia conținutul unui șir în alt șir, se folosește funcția strcpy.
			char sir[10] = "Hello";
			char s[10], t[10];
			strcpy(s,sir);
			strcpy(t,"Buna");
		
Compararea între șiruri de caractere nu e posibilă prin operatorul ==
		char sir_unu[10] = "alba";
		char sir_doi[10] = "neagra";
		sir_unu == sir_doi;   // warning: operator has no effect
	
Pentru comparare se folosește o funcție specială, numită strcmp. Apelul strcmp(s,t) va returna 0, dacă s și t sunt identice, un număr <0, dacă s este lexicografic înaintea lui t, respectiv un numar >0, dacă s este lexicografic după t.

Șirurile de caractere din C pot conține caractere din cele 256 din lista ASCII (American Standard Code for Information Interchange). Comparațiile între șirurile de caractere se fac lexicografic, pe baza codurilor ASCII ale caracterelor, de la stânga la dreapta.
Codurile ASCII de bază (0-127)
Coduri ASCII extinse (128-255)
Așadar, macrodefinițiile şi funcţiile pentru şiruri se folosesc pentru a face diferite operații pe șiruri de caractere. De pildă, strcat permite concatenarea unui șir la un alt șir, strlen() returnează lungimea unui șir (numărul de caractere, fără '\0') etc. Vom reveni asupra lor în cursul următor.
Câteva macrodefiniții

3.2.3. Tablouri bidimensionale

Declararea unui tablou bidimensional (matrice) se face astfel:
		tip numeTablou[m][n];
		int a[m][n];
	
Matricea ocupă o memorie contiguă de m×n locaţii, iar componentele sunt identificate prin doi indici: Variabilele componente ale acestei matrice sunt: a[0][0], a[0][1], …, a[0][n-1], a[1][0], a[1][1], …, a[1][n-1], …, a[m-1][0], a[m-1][1], …, a[m-1][n-1] Ordinea de memorare a componentelor este dată de ordinea lexicografică a indicilor
Explicație pe un exemplu
În figura de mai sus, a este de tip int[2][3], a[0] este de tip int[3], a[1] este de tip int[3], iar fiecare element din matrice este de tip int.
Parcurgerea unui tablou bidimensional se poate realiza după cum urmează:
		double a[MMAX][NMAX]; // declarare tablou bidimensional (matrice de numere reale în precizie dublă)
		double suma;  // suma elementelor din tablou 
		/* citirea elementelor, prin parcurgerea după linii, coloane */
		for (i = 0; i < m; i++)
    		for (j = 0; j < n; j++)
        		cin >> a[i][j];
		/ * suma tuturor elementelor din tablou */
		suma = 0;
		for (i = 0; i < m; i++)
			for (j = 0; j < n; j++)
				suma += a[i][j];

Un tablou bidimensional poate fi privit ca un tablou unidimensional în care fiecare componentă este un tablou unidimensional.
Exemplu
Accesarea elementelor unui tablou bidimensional  
Ca și în cazul tablourilor unidimensionale, există expresii echivalente cu a[i][j], care fac legătura dintre tablouri și pointeri:
Tablouri bidimensionale ca argumente de funcție:
		int minmax(int t[][NMAX], int i0, int j0, int m, int n)
		{
		 //...
		}
		/* utilizare */
		if (minmax(a,i,j,m,n))
		{
		  // ... 
		}
	

Iniţializarea tablourilor (vectori, siruri, matrice)
		int a[] = {-1, 0, 4, 7};
		/* echivalent cu */
		int a[4] = {-1, 0, 4, 7};
		char s[] = "un sir";         /* echivalent cu */
		char s[7] = {'u', 'n', ' ', 's', 'i', 'r', '\0'};
		int b[2][3] = {1,2,3,4,5,6}  /* echivalent cu */
		int b[2][3] = {{1,2,3},{4,5,6}} /*echivalent cu*/
		int b[][3] = {{1,2,3},{4,5,6}}
	

3.3. Pointeri

  1. Pointer = variabilă care conține o adresă din memorie, adresă care este localizarea unui obiect (de obicei altă variabilă)
  2. Oferă posibilitatea modificării argumentelor de apelare a funcțiilor
  3. Permit alocarea dinamică
  4. Pot îmbunătăți eficiența unor rutine
  5. Pointeri neinițializați
  6. Pointeri ce conțin valori inadecvate

3.3.1. Declararea unei variabile pointer:

tip *nume_pointer;
int *p, i;  // int *p; int i;
p = 0;
p = NULL;
p = &i;
p = (int*) 232;
Semnificația lui p = &i; Operatorul de dereferenţiere (indirectare) * :       int *p; Valoarea directă a lui p este adresa unei locaţii iar *p este valoarea indirectă a lui p: ceea ce este memorat în locaţia respectivă int a = 1, *p; p = &a; sizeof(int*) = sizeof(double*) = ...

3.3.2. Expresii cu pointeri



Exemplu
		#include <iostream>
		using namespace std;
		int main(void){
    		int i=5, *p = &i; float *q; void *v;
			q = (float*)p; v = q;
			cout << "p = " << p << ", *p = " << *p << "\n";
    		cout << "q = " << q << ", *q = " << *q << "\n";
    		cout << "v = " << v << ", *v = " << *((float*)v)<<"\n";
			return 0;
		}
	

3.3.3. Aritmetica pointerilor


Video legătura pointeri - tablouri https://youtu.be/ASVB8KAFypk

3.3.4. Diferențe între pointeri și referințe

Iată o secvență de cod ce inițializează un număr folosind pointeri (C,C++):
	void SetInt(int *i){
		(*i) = 5;
	}
	void main(){ 
		int x;
		SetInt(&x);
	}
	
și o secvență de cod ce inițializează un număr folosind referințe (C++):
	void SetInt(int &i){
		i = 5;
	}
	void main(){ 
		int x;
		SetInt(x);
	}
	
Cele două secvențe de cod fac același lucru, la prima vedere părând că nu există diferențe între conceptul de pointer și cel de referință.
Totuși, există unele diferențe între pointeri și referințe:
  1. Pointerii pot să rămână neinițializați, în timp ce referințele trebuie inițializate.
    		int i = 10; int *p = &i; int &ref = i;
    		int j = 20; int *p1;
    		int &ref1; // eroare la compilare - referința nu a fost inițializată
    		
  2. Pointerii își pot schimba valoarea în timpul execuției programului, referințele rămân doar cu valoarea cu care au fost inițializate.
    		int i =10; int j = 20;
    		int *p = &i; p = &j;
    		int &ref = i;
    		&ref = j; // eroare la compilare - referința nu își poate schimba valoarea
    		
  3. Pointerii acceptă operații aritmetice (+, - , ++). Referințele nu acceptă nici o operație aritmetică.
    		int i = 10; int j = 20;
    		int *p = &i; //pointer către i
    		p++; //pointerul se mută la variabila j
    		(*p) = 30;
    		cout<< j;  //30
    		int &ref = i;
    		ref++; // i va avea valoarea 11
    		(&ref)++; //eroare la compilare - referința nu suportă operații aritmetice 
    		
  4. Pointerii pot fi convertiți ("castați") la alte tipuri de pointer (orice pointer poate fi convertit la void*). Referințele nu pot fi convertite ("castate").
    		int i =10;
    		char *p = (char *)&i;
    		char &ref = i; // eroare la compilare, referința de tip char poate puncta doar la char
    		
  5. Există pointeri către pointeri, dar nu referințe către referințe.
    		int i =10; int *p = &i;
    		int **p_to_p = &p;
    		**p_to_p = 20; //valoarea lui i devine 20
    		int j = 10;
    		int &ref = j;
    		int & &ref_to_ref = ref; // eroare la compilare
    		
  6. Pot exista vectori de pointeri, dar nu vectori de referințe.

3.3.5. Pointeri la pointeri

Putem să avem pointer către o variabilă de tipul pointer, așa cum putem avea pointer către orice alt tip de variabilă.
	int main(){
	int valoare =10;
	int *pointer = &valoare;
	int **pointer_la_pointer = &pointer;
	cout<< valoare; //10
	cout<< pointer; // adresa de memorie a variabilei valoare
	cout<< *pointer; //10
	cout<< pointer_la_pointer // adresa de memorie a pointerului pointer
	cout<< *pointer_la_pointer // adresa de memorie a variabilei valoare
	cout<< **pointer_la_pointer; //10
	

3.3.6. Pointeri la funcții

În C un pointer referențiază o adresă de memorie. Astfel, pe lângă pointerii normali ( cei care duc la adresa unor variabile) putem avea și pointeri către o funcție (pointerul duce la adresa începutul de cod a funcției). Un pointer către o funcție este declarat în felul următor:
	tip_returnat (*nume_funcție)(tip parametri)
	
După ce definim un pointer către o funcție va trebui să îi asignăm o funcție. Exemplu de program care folosește pointeri către funcții:
	#include <stdio.h>
	#include <iostream>
	int suma(int x, int y){
		return x+y;
	}
	int main(){
		int (*pointer_functie)(int,int);
		pointer_functie = suma;
		int s1 = pointer_functie(10,15);
		int s2 = suma(10,15);
		cout<<s1<<"  "<<s2; // 25 25
		return 0;
	}
	

3.4. Crearea tipurilor de date

Limbajul C permite crearea tipurilor de date în 5 moduri:

3.4.1. Structuri





Inițializarea structurilor


Structuri imbricate și tablouri de structuri


Operații permise cu structuri
Definirea unei structuri nu ocupă memorie ci doar creează un tip nou de date

3.4.2. Câmpuri de biți

Un câmp de biți este un tip special de membru al unei structuri, care definește cât de lung (mare) trebuie să fie acel câmp, în biți. Astfel, putem stoca mai multe variabile într-un singur octet. Nu se poate obține adresa unui câmp de biți. Folosirea câmpurilor de biți este foarte utilă în economisirea memoriei utilizate.
Sintaxa
Exemplul 1

Exemplul 2

3.4.3. Uniuni

Uniunea este un tip special de structură, ai cărei membri folosesc, la momente diferite, aceeași locație de memorie. De obicei, membrii unei uniuni au tipuri diferite.
Sintaxa
Exemplul 1
Exemplul 2