Prima pagină > Tutoriale C++ > Capitolul 9: Siruri si pointeri

Capitolul 9: Siruri si pointeri


Siruri si pointeri

Un sir (se mai spune si vector) este o secventa de date ce contine articole de acelasi tip, indexate si memorate contiguu. De obicei, sirurile se folosesc pentru reprezentarea unui numar mare de valori omogene (in capitolul urmator vom studia sirurile de caractere). O declaratie obisnuita de sir aloca memorie incepand de la adresa de baza. Numele sirului este un pointer constant la aceasta adresa de baza.
O alta notiune pe care o vom explica este transmiterea sirurilor ca argumente in functii.

9.1 Siruri uni-dimensionale

Exemplu:

Presupunem ca vrem sa lucram cu trei intregi:
int a1, a2, a3;
Totusi, daca avem mai multe numere este anevoios sa declaram numerele in acest fel. Solutia consta in utilizarea unui sir (de lungime trei) de intregi.
int a[3];
Elementele acestui sir vor fi accesate astfel: a[0] a[1] a[2]
Deci numarul 3 reprezinta lungimea sirului, elementele sale fiind indexate incepand cu numarul 0. Aceasta este o trasatura a limbajului C.
O declaratie de sir uni-dimensional este un tip urmat de un identificator urmat la randul lui de paranteze patrate ce cuprind o expresie integrala constanta. Valoarea expresiei constante, care trebuie sa fie pozitiva, se numeste lungimea sirului si ea specifica numarul de elemente ale sirului. Pentru memorarea elementelor intr-un sir, compilatorul rezerva un spatiu de memorie corespunzator, pornind de la adresa de baza. Dimensiunea spatiului de memorie este egala cu numarul de elemente ale sirului inmultit cu numarul de octeti necesari memorarii unui element al sirului.

Exemplu:

Vom scrie un mic program care initializeaza un sir, tipareste valorile sale si insumeaza elementele sirului.

#include
#define N 5

void main()
{
int a[N]; /* aloca spatiu de memorie pentru a[0], a[1], a[2], a[3] si a[4] */
int i, suma = 0;

for (i = 0; i < N; ++i) /* initializeaza sirul */
a[i] = 7 + i * i;
for (i = 0; i < N; ++i) /* tipareste sirul */
printf(„a[%d] = %d „, i, a[i]);
for (i = 0; i < N; ++i) /* insumeaza elementele sirului */
suma += a[i];
printf(„\nsuma = %d\n”, suma); /* tipareste suma lor */
}
Sa vedem ce se intampla in memorie ?

Nume
Tip
Valoare
Adresa
a[4]
int
23
3A38:0FFE
a[3]
int
16
3A38:0FFC
a[2]
int
11
3A38:0FFA
a[1]
int
8
3A38:0FF8
a[0]
int
7
3A38:0FF6

Deci vectorul (sirul) „a” se va memora incepand de la adresa 3A38:0FF6. Deci „a = &a[0]”.
Se recomanda definirea lungimii unui sir ca o constanta simbolica (folosind directiva „#define”).

9.2 Initializarea sirurilor

Sirurile pot apartine claselor de memorare „auto”, „extern”, „static” sau „constant”, dar nu pot fi „register”. Ca si variabilele simple, sirurile pot fi initializate in timpul declararii lor. Initializarea sirurilor se face folosind acolade si virgule.

Exemplu:

float x[7] = {-1.1, 0.2, 33.0, 4.4, 5.05, 0.0, 7.7};
Asta inseamna, echivalent:
x[0] = -1.1;
x[1] = 0.2;
. . . . .
x[6] = 7.7;

Daca lista de valori de initializare este mai mica decat numarul de elemente ale sirului, atunci elementele ramase se initializeaza cu 0. Daca un sir declarat „extern” sau „static” nu este initializat, atunci sistemul initializeaza toate elementele cu 0. Vectorii declarati constanti sau automatic (cei impliciti) sunt initializati cu valori „garbage” (adica cu valorile existente in momentul executiei in memorie la acele adrese). C traditional permite doar initializarea vectorilor declarati „extern” sau „static”, pe cand ANSI C permite initializarea sirurilor automate si constante.
Daca un sir este declarat fara precizarea lungimii si initializat cu o serie de valori, atunci lungimea sa se considera implicit numarul de valori initiale.

Exemplu:

Declaratiile
int a[] = {3, 4, 5, 6}; si int a[4] = {3, 4, 5, 6};
sunt echivalente.

9.3 Indexul unui sir

Presupunem ca avem declaratia
int i, a[lungime];
Pentru accesarea unui element din sir, vom scrie „a[i]”, sau mai general „a[expresie]”, unde „expresie” este o expresie integrala. „i” de mai sus se numeste index al sirului „a” si poate avea valori intre 0 si „lungime-1”. Daca indexul depaseste acest domeniu, compilatorul va da eroare in timpul executiei programului.

Exemplu:

#include
#include

void main()
{
int c, i, litera[26];

for (i = 0; i < 26; ++i) /* initializarea vectorului cu 0 */
litera[i] = 0;
while ((c = getchar()) != EOF) /* numararea literelor */
if (isupper(c))
++litera[c – ‘A’];
for (i = 0; i < 26; ++i) /* tiparirea rezultatelor */
{
if (i % 6 == 0)
printf(„\n”);
printf(„%5c:%4d”, ‘A’ + i, litera[i]);
}
printf(„\n\n”);
}
Acest program citeste de la tastatura sau dintr-un fisier (folosind indirectarea) un sir de caractere si numara in vectorul „litera” fiecare aparitie (in parte) a literelor.

9.4 Relatia dintre vectori si pointeri

Am vazut ca numele unui sir (de exemplu „a”) este o adresa, deci poate fi privit ca valoare a unui pointer. Deci sirurile si pointerii pot fi priviti oarecum la fel in ceea ce priveste modul cum sunt folositi pentru accesarea memoriei. Cu toate acestea, sunt cateva diferente (subtile si importante). Cum numele unui sir este o adresa fixa (particulara), atunci aceasta o putem gandi ca un pointer constant. Cand este declarat un sir, compilatorul trebuie sa aloce o adresa de baza si un spatiu suficient de memorie care trebuie sa contina toate elementele sirului. Adresa de baza a unui sir este locatia initiala din memorie unde sirul este memorat; aceasta coincide cu adresa primului element (de index 0) al sirului.

Exemplu: Presupunem ca avem declaratiile:

#define N 100
int a[N], *p;

Atunci sistemul va rezerva octetii (sa zicem) numerotati 300, 302, …, 498 ca fiind adresele elementelor a[0], a[1], …, a[99]
Instructiunile
p = a; si p = &a[0];
sunt echivalente si vor asigna lui „p” valoarea 300 (ca adresa de memorie). Aritmetica pointerilor pune la dispozitie o alternativa pentru indexarea sirurilor.
Instructiunile p = a + 1; si p = &a[1]; sunt echivalente si va asigna lui „p” valoarea 302 (adresa, bineinteles).

Exemplu: Presupunem ca avem un sir ale carui elemente au deja valori. Pentru a face suma elementelor, putem folosi pointeri.

suma = 0;
for (p = a; p < &a[N]; ++p)
suma += *p;

9.5 Pointeri aritmetici si lungimea elementelor

Pointerii aritmetici reprezinta una din trasaturile puternice ale limbajului C. Daca variabila „p” este pointer catre un tip particular, atunci expresia „p + 1” reprezinta adresa masina pentru memorarea sau accesarea urmatoarei variabile de acest tip. In mod similar, expresiile p + i ++p p += i au sens. Daca „p” si „q” sunt pointeri catre elemente de tip vector, atunci „p – q” intoarce valoarea „int” si reprezinta numarul de elemente dintre „p” si „q”. Chiar daca expresiile pointer si expresiile aritmetice seamana, exista diferente mari intre cele doua tipuri de expresii.

Exemplu:

void main()
{
double a[2], *p, *q;
p = &a[0]; /* pointeaza catre baza sirului */
q = p + 1; /* echivalent cu q = &a[1]; */
printf(„%d\n”, q – p); /* se va tipari 1 */
printf(„%d\n”, (int) q – (int) p)); /* se va tipari 8 */
}

9.6 Trimiterea sirurilor ca argumente pentru functii

Intr-o definitie de functie, un parametru formal care este declarat ca un sir este de fapt un pointer. Cand este trimis un sir, atunci se trimite de fapt adresa de baza (evident prin „call-by-value”). Elementele vectorului nu sunt copiate. Ca o conventie de notatie, compilatorul permite folosirea parantezelor patrate ([,]) in declararea pointerilor ca parametri.

Exemplu:

Suma elementelor unui sir de tip vector

int suma(int a[], int n) /* n dimensiunea sirului */
{
int i, s = 0;
for (i = 0; i < n; ++i)
s += a[i];
return s;
}

In antetul functiei precedente, declaratia:
int a[]; este echivalenta cu int *a;
Pe de alta parte, declaratiile de mai sus nu sunt echivalente daca se utilizeaza in alta parte:
– prima se refera la creearea unui pointer constant (fara spatiu de memorie);
– a doua va crea o variabila pointer.

Presupunem ca „v” este declarat ca fiind un sir de 100 de elemente de tip „int”. Dupa ce am atribuit valori elementelor sale, putem utiliza functia „suma()” pentru a aduna anumite valori ale lui „v”.

Apel
Ce se calculeaza si se returneaza ?
suma(v, 100)
v[0] + v[1] + … + v[99]
suma(v, 88)
v[0] + v[1] + … + v[87]
suma(&v[7], k-7)
v[7] + v[8] + … + v[k – 1]
suma(v + 7, 2 * k)
v[7] + v[8] + … + v[2 * k + 6]

Exemplu:

Sortare cu bule – „Bubble sort”

Algoritmii eficienti de sortare au, de obicei, O(n*log n) operatii. Metoda sortarii cu bule este ineficienta din acest punct de vedere deoarece are O(n^2) operatii. Totusi, pentru siruri de lungime mica, numarul de operatii este acceptabil. Un cod „elegant” ar fi:
void interschimba(int *, int *);

void bubble(int a[], int n) /* n este lungimea lui a[] */
{
int i, j;

for (i = 0; i < n – 1; ++i)
for (j = n – 1; i < j; –j)
if (a[j – 1] > a[j])
interschimba(&a[j – 1], &a[j]);
}

9.7 Siruri multidimensionale

Limbajul C permite siruri de orice tip, inclusiv siruri de siruri. Putem obtine siruri de dimensiune 2, 3, … .

Exemple:

int a[100]; <- sir de dimensiune 1
int b[2][7]; <- sir de dimensiune 2
int c[5][3][2]; <- sir de dimensiune 3

Pornind de la adresa de baza, toate elementele sirului sunt memorate contiguu in memorie. Prin definitie un tablou bidimensional este de fapt un tablou unidimensional ale carei elemente sunt fiecare in parte cite un tablou. Prin urmare, indicii se scriu astfel a[i][j] in loc de a[i, j] ca in majoritatea limbajelor. In plus un tablou bidimensional poate fi tratat in mai multe moduri decat in alte limbaje. Elementele sunt memorate pe linii, ceea ce inseamna ca indicele din dreapta variaza primul in asa fel incit elementele sunt accesate in ordinea memoriei.

9.8 Vectori 2-dimensionali

Presupunem ca avem un vector 2-dimensional cu elemente intregi.
int a[3][5];
Incepand cu adresa de baza, compilatorul va aloca spatiu contiguu pentru 15 intregi. Atunci putem gandi acest vector ca o matrice, astfel:

col1
col2
col3
col4
col5
a[0][0]
a[0][1]
a[0][2]
a[0][3]
a[0][4]
a[1][0]
a[1][1]
a[1][2]
a[1][3]
a[1][4]
a[2][0]
a[2][1]
a[2][2]
a[2][3]
a[2][4]

Pentru a[i][j] avem expresiile, de exemplu, echivalente:
*(a[i] + j)
(*(a + i))[j]
*((*(a + i)) + j)
*(&a[0][0] + 5*i + j)

Putem gandi „a[i]” ca a „i”-a coloana a lui „a” (numarand de la 0), si „a[i][j]” ca elementul din linia „i”, coloana „j” a sirului (numarand de la 0). Numele sirului („a”) este tot una cu „&a[0]”; acesta este un pointer catre un sir de 5 intregi. Adresa de baza este „&a[0][0]”, si nu „a”. Ultimul exemplu de mai sus reflecta functia de corespondenta in memorie dintre valoarea pointerului si indicele sirului.
Cand un vector multidimensional este un parametru formal in definitia unei functii, toate dimensiunile, exceptand prima trebuie specificate.

Exemplu:

Presupunem ca sunt date elementele vectorului „a”. Functia de mai jos se poate folosi pentru suma elementelor unui sir. Atentie ! Trebuie specificat numarul de coloane.
int suma(int a[][5])
{
int i, j, suma = 0;

for (i = 0; i < 3; ++i)
for (j = 0; j < 5; ++j)
suma += a[i][j];
return suma;
}
In antetul functiei, urmatoarele declaratii sunt echivalente:
int a[][5] int (*a)[5] int a[3][5]
Constanta 3 actioneaza ca o reminiscenta a omului, dar compilatorul nu tine cont de ea.
Nou venitii in C sunt uneori confuzi in legatura cu deosebirea dintre un tablou bidimensional si un tablou de pointeri cum ar fi „a” din exemplul de mai sus. Fiind date declaratiile
int a[10][10];
int *b[10];

utilizarile lui „a” si „b” pot fi similare, in sensul ca a[5][5] si b[5][5] sunt ambele referinte legale ale aceluiasi „int”.
Avantaje pentru utilizarea vectorilor (dezavantaje pentru pointeri):
– „a” este un tablou in toata regula: toate cele 100 celule de memorie trebuie alocate, iar pentru gasirea fiecarui element
se face calculul obisnuit al indicelui;
– pentru „b”, oricum prin declararea sa se aloca 10 pointeri; fiecare trebuie facut sa pointeze un tablou de intregi.

Presupunind ca fiecare pointeaza cate 10 elemente din tablou, atunci vom obtine 100 celule de memorie rezervate, plus cele 10 celule pentru pointeri. Astfel tabloul de pointeri utilizeaza sensibil mai mult spatiu si poate cere un procedeu explicit de
initializare.
Avantaje pentru utilizarea pointerilor (dezavantaje pentru vectori):
– accesarea unui element se face indirect prin intermediul unui pointer, in loc sa se faca prin inmultire si adunare;
– liniile tabloului pot fi de lungimi diferite. Aceasta inseamna ca nu orice element al lui b este constrins sa pointeze pe un vector de 10 elemente, unii pot pointa pe cate 2 elemente, altii pe cate 20 si altii pe niciunul.

9.9 Vectori 3-dimensionali

Vectorii de dimensiune mai mare decat 3 lucreaza intr-un mod similar. Daca avem declaratia int a[7][9][2];
atunci compilatorul va aloca spatiu pentru 7*9*2 intregi. Adresa de baza a sirului este „&a[0][0][0]”, iar functia de corespondenta in memorie este specificata de a[i][j][k] care este echivalent cu *(&a[0][0][0] + 9*2*i + 2*j + k)

9.10 Initializarea vectorilor

Exista mai multe moduri de a initializa un vector multidimensional.

Exemplu: Urmatoarele declaratii sunt echivalente:

int a[2][3] = {1, 2, 3, 4, 5, 6};
int a[2][3] = {{1, 2, 3}, {4, 5, 6}};
int a[][3] = {{1, 2, 3}, {4, 5, 6}};

Indexarea se face dupa linii. Daca nu sunt suficiente elemente care sa initializeze vectorul, atunci restul elementelor sunt initializate cu 0. Daca prima componenta lipseste, atunci compilatorul extrage lungimea din numarul de perechi de acolade interioare.

Exemplu: Consideram initializarea:

int a[2][2][3] = {
{{1, 1, 0}, {2, 0, 0}},
{{3, 0, 0}, {4, 4, 0}}
};

O initializare echivalenta poate fi data si astfel:
int a[][2][3] = {{{1, 1}, {2}}, {{3}, {4, 4}}};
De obicei, daca un sir declarat „auto” nu este explicit initializat, atunci elementele sirului vor contine valori „garbage”.
Sirurile „static” si „external” sunt initializate implicit cu 0. Iata un mod simplu de a initializa toate valorile unui vector cu 0:
int a[2][2][3] = {0};

9.11 Alocarea dinamica a memoriei

C pune la dispozitie pentru alocarea memoriei functiile „calloc()” si „malloc()” din biblioteca standard. Aceste functii au prototipul declarat in . Acest lucru va permite rezervarea memoriei pentru un vector (de exemplu) in care ii aflam dimensiunea abia la rularea in executie (pana acum declararam dimensiunea unui vector cu #define). Un apel de tipul calloc(n, dimensiune_tip) va returna un pointer catre un spatiu din memorie necesar pentru memorarea a „n” obiecte, fiecare pe „dimensiune_tip” octeti. Daca sistemul nu poate aloca spatiul cerut, atunci acesta va returna valoarea NULL.
In ANSI C, tipul „size_t” este dat ca „typedef” in . De obicei, tipul este „unsigned”. Definitia tipului este folosita in prototipurile functiilor „calloc()” si „malloc()”:
void *calloc(size_t, size_t);
void *malloc(size_t);

Deoarece pointerul returnat de aceste functii are tipul „void”, acesta poate fi asignat altor pointeri fara conversie explicita (cast). Totusi unele sisteme nu accepta aceasta conversie, deci ea trebuie facuta explicit. Octetii rezervati de „calloc()” sunt automat initializati cu 0, pe cand cei rezervati cu „malloc()” nu sunt initializati (deci vor avea valori „garbage”). Numele „calloc”, respectiv „malloc”, provine de la „contiguous allocation”, respectiv „memory allocation”.

Exemplu: Program care citeste dimensiunea unui sir interactiv

#include
#include

void main()
{
int *a, i, n, suma = 0;

printf(„\n%s”, „Citirea dimensiunii unui sir interactiv.\n\nDati numarul de elemente a sirului: „);
scanf(„%d”, &n);
a = calloc(n, sizeof(int)); /* aloca spatiu pentru n intregi */
/* daca da eroare de conversie de tip, atunci adaugati dupa semnul =, conversia (int *) */
for (i = 0; i < n; ++i)
scanf(„%d”, &a[i]);
for (i = 0; i < n; ++i)
suma += a[i];
free(a); /* eliberarea spatiului */
printf(„\n%s%7d\n%s%7d\n\n”, „Numarul de elemente: „, n, „Suma elementelor : „, suma);
}
Prototipul functiei „free()” se gaseste in si este void free(void *ptr);
Spatiul alocat de „calloc()” si „malloc()” ramane ocupat pana cand este eliberat de catre programator. Acesta nu se elibereaza cand se iese dintr-o functie (in care s-a facut rezervarea de memorie).
In programul de mai sus, instructiunea
a = calloc(n, sizeof(int));
este echivalenta cu
a = malloc(n * sizeof(int));
Singura diferenta este deci initializarea cu 0 in cazul functiei „calloc()”.

Exemplu: Exemplu de citire interactiva a dimensiunii si a elementelor unei matrice de intregi
void main()
{
int N;
int ** a, *ptr;

printf(„Introduceti dimensiunea matricii:”);
scanf(„%d”, &N);
a = (int **) calloc(N * N, sizeof(int));

ptr=&a[0][0];
printf(„\n\nIntroduceti elementele matricii:\n”);

for (i = 0;i < N; ++i)
for (j = 0;j < N; ++j)
{
printf(„a[%d][%d]=”, i, j);
scanf(„%d”, ptr++);
}
}

Categorii:Tutoriale C++
  1. Niciun comentariu până acum.
  1. No trackbacks yet.

Lasă un răspuns

Completează mai jos detaliile despre tine sau dă clic pe un icon pentru autentificare:

Logo WordPress.com

Comentezi folosind contul tău WordPress.com. Dezautentificare / Schimbă )

Poză Twitter

Comentezi folosind contul tău Twitter. Dezautentificare / Schimbă )

Fotografie Facebook

Comentezi folosind contul tău Facebook. Dezautentificare / Schimbă )

Fotografie Google+

Comentezi folosind contul tău Google+. Dezautentificare / Schimbă )

Conectare la %s

%d blogeri au apreciat asta: