Se poate trece la capitolul următor cu tasta ► și se poate reveni la un capitol precedent cu tasta ◄

Despre Pointeri


<

C
a
p
i
t
o
l
u
l

a
n
t
e
r
i
o
r

<

De ce există pointeri?

Existența pointerilor este pierdută în istoria calculatoarelor. Conceptul de pointer e la fel de vechi pe cât este istoria calculatorului acum. Procesorul în sine este un mare utilizator al lor, fiindcă, pentru a procesa orice, în primul rând e nevoie de aducă valorile necesare din memorie (RAM) în regiștrii săi, practic zicând: „Adu-mi valoarea de la adresa x”.

Pointer înseamnă indicator. Spre deosebire de tipurile standard de date precum integer sau float, unde știm că accesând variabila, vom obține un număr, în cazul pointerilor obținem o adresă.

Nu înțelegeți greșit, un pointer memorează tot un număr. Numerele sunt singurul element cu care un calculator lucrează. Indiferent că este un text, o imagine sau un sunet, totul e codificat în numere pentru un calculator. De fapt e codificat în doar două: 0 și 1. Care nici măcar nu sunt numere, ci două valori de tensiune diferite ale unui circuit electric. Valoarea 0 e dată de tensiune nulă, sau cel puțin mai mică de 1 volt, iar 1 este dat de o tensiune de 5 volți. Sau cel puțin mai mare decât 4 volți.

Revenind la pointer, când avem de-a face cu unul, memorăm de fapt adresa la care valoarea noastră se găsește.

Anatomia pointerului

El luat ca orice variabilă e categorizat de un nume și de o mărime. Putem să-l numim oricum (atâta timp cât respectăm regulilele de numire a variabilelor în general), iar dimensiunea sa e prestabilită.

Problema apare când mergem un pas mai departe. Ce referențiază?

Să luăm de exemplu pointerul numit ptr. Să zicem că el indică la adresa 1008. Întrebarea programului în acest moment ar fi: Cât trebuie să citesc de la adresa 1008? Un octet? Doi?

De aceea pointerul are și el un tip: un pointer poate fi integer sau float sau orice alt tip de date standard ori definit de programator.

Așadar în momentul în care declarăm pointerul ptr ca fiind de tip întreg, programul generat va ști că va avea de citit sau scris un număr care are o dimensiune de 2 octeți. Sau 4. (Tipul de dată întreg poate fi definit de mai multe lungimi, în funcție de standardele limbajului de programare folosit)

Ca să concluzionăm, pointerul are 3 caracteristici:

  • adresa pointerului
  • adresa la care face referire pointerul
  • valoarea de la adresa la care pointerul ne indică

Vizual ar fi:

Vizualiarea pointerilor După cum observăm pointerul se află la adresa 1001. Presupunem că are lungimea unui octet (ceea ce e total greșit fiindcă nu putem reține valori mai mari de 255 pe un octet), deci adresa de la 1002 ar fi liberă. Pe adresa 1001 e scrisă valoarea 1008.

În acest moment când accesăm pointerului nostru, programul va întelege să meargă la adresa indicată de pointer, adică să meargă la adresa 1008 și apoi să ia ce valoare se găsește acolo, în funcție de tipul pointerului.

Motivul pentru care există pointeri

Dacă ne uităm peste descrierea de mai sus, ne dăm seama că pointerii nu sunt chiar așa de grozavi. În primul rând, pentru a reține un întreg va fi nevoie de două locații în memorie: una pentru variabila pointer și cealaltă unde se memorează propriu-zis întregul și spre care pointerul nostru va indica. Așa ar fi mult mai simplu să avem o singură variabilă întreagă și să lăsăm pointerii.

Pointerii, în schimb, strălucesc când este vorba de structuri de date. Să luăm un exemplu de „amalgam” de informații: conceptul simplificat de persoană. Un nume de familie, un prenume și o vârstă. Două șiruri de caractere și un număr întreg.

În acest fel, un pointer va arată spre, dacă rezervăm 255 de caractere pentru fiecare șir, 512 octeți. Un caracter ocupă un octet, 255 ocupă 255 octeți, înmulțit cu 2 obținem 510 și cu 2 octeți ai întregului avem 512.

Până aici lucrurile arată mai bine. Un pointeri referențiază 512 octeți. Însă de ce să nu folosim pur și simplu o varibilă care să fie structura noastră de date, fără să ne complicăm cu pointeri?

Aici intervine o mică problemă în alocarea memoriei unui program. În programele DOS, spațiul de memorie al unei aplicații este limitat la 640 Kilo-octeți. Ceea ce ne limitează să declarăm un șir de maxim 32 767 numere întregi. Iar dacă avem și alte variabile, s-a terminat cu memoria. Iar dacă luăm structura dată ca exemplu mai sus, cu 512 octeți pe element, vom putea reține doar aproximativ 1 250 elemente.

Aici pointerii ne ajută semnificativ: ei se alocă într-o zonă extinsă de memorie decât cea rezervată inițial programului. Așa că scăpăm de limitări și aplicația va putea folosi mai mult decât cei 640 KOcteți.

În cazul în care un pointer îndică la un întreg sau număr real (float), nu câștigăm mare lucru. Spațiu de memorie unde e reținută adresa va fi tot în acei 640 KOcteți. Însă de îndată ce lucrăm cu structuri de date, lucrurile încep deja să se lumineze. Patru octeți referențiind 512 e altă mâncare de pește.

Memoria dinamică

Un alt beneficiu al pointerilor este alocarea și dealocarea lor dinamică. Când declarăm o variabilă globală, ea va fi accesibilă și va ocupa spațiu pe toată execuția programului. O variabilă locală va fi activă cât timp secvența pentru care am declarat-o este și ea rulată (aici referindu-mă în mare parte la funcții și proceduri). Oricum, variabilele clasice au o durată de viață de la declarare până la terminarea programului.

Un pointer poate fi alocat, însă, în schimb, poate fi și dealocat. Așadar, prin intermediul pointerilor, putem avea un șir cu un număr variabil de elemente. La o rulare șirului îi pot fi alocate 10 elemente. Însă la o altă rulare i se pot aloca 100 000 de elemente. Iar ștergerea lor poate fi făcută în timp ce aplicația rulează, în funcție de necesitățile algoritmului.

Structurile de date și pointerii - un cuplu perfect

Vorbeam adineaori despre o structură simplă de date: nume, prenume, vârstă. Să-i spunem persoană.

Așadar putem declara pointerului ptrPersoană care să refere o astfel de structură. Dar hai să ducem lucrurile mai departe. Să ne imaginăm un lanț de astfel de elemente.

În primă fază ne putem gândi să declarăm un alt pointer, ca și ptr2Persoană. Dar asta ne încurcă fiindcă nu vom putea gestiona un număr dinamic de persoane. Iar să declarăm un șir de pointeri, tot nu ne ajută, fiindcă unui șir îi trebuie spus explicit câte elemente are. Și pe noi ne interesează să aibă orice număr de elemente.

Aici apare frumusețea dintre structuri și pointeri: adăugăm un pointer către o nouă persoană exact în structura de dată:

{nume, prenume, vârstă, pointer_la_persoană}.

În fond, un pointer e tot un număr, și tot ce am făcut a fost să adăugăm încă un membru numeric structurii. Dar! Acum ptrPersoană ne indică primul element. Însă ptrPersoană.pointer_la_persoană ne indică adresa celui de-al doilea element. Și așa mai departe. Până când? Până pointer_la_persoană este egal cu 0.

Vizual ar fi:

Structuri de date folosind pointeri

În acest moment avem o înlănțuire de structuri de date pe care o putem parcurge într-o singură direcție: de la primul element până la un ultimul. Aceasta poartă denumirea de listă simplu înlănțuită. Dacă vrem să mergem în ambele direcții mai adăugăm un pointer numit, de exemplu: pointer_la_persoana_anterioară.

Putem să ne imaginăm o matrice unde fiecare element are 4 vecini. Putem să ne gândim la un cub unde fiecare element are 6 vecini. Și așa mai departe. Putem avea o listă octo-înlănțuită unde fiecare element va avea 6 vecini cât și o stare viitoare și una anterioară.

Însă, de multe ori, o listă simplu înlănțuită e suficientă. Chiar dacă are 10 000 000 de elemente majoritatea procesoarelor vor putea să o parcurgă instant din perspectiva noastră.

Dezavantaje

Pointerii sunt, fără doar și poate, o adiție foarte reușită programării. De altfel unele limbaje de programare au renunțat de tot la alte tipuri de date și lucrează numai cu pointeri.

Dar și ei au o parte întunecată. Alocarea și dealocarea lor. Pointerii nu sunt inițializați la declarare. Spațiu de memorie trebuie alocat. Desigur putem să-i dăm (în unele limbaje) unui pointer o valoare de la noi, însă în sistemele de operare noi, adică de la Windows 2000 încoace și orice distribuție de Linux, fiecare aplicație are spațiul ei posibil de memorie. Altfel am putea citi și modifica datele unui alt program care rulează. Și așa apar virușii și programele de spionare.

Revenind, pointerii trebuie să fie alocați prin mecanismele fiecarui limbaj de programare. Și fiecare limbaj de programare are un mecanism prin care cere sistemului de operare niște memorie liberă.

Iar cu alocarea vine și dealocarea. Desigur, la terminarea programului orice variabilă ce aparține lui va fi distrusă, chiar și în zona extinsă a memoriei, sistemul de operare știind cui i-a alocat memoria, iar la terminarea aplicației va fi suficient de descurcăreț să realizeze că zonele de memorie alocate inițial nu mai aparțin nimănui.

Însă dacă alocăm, fără să dealocăm, 10 elemente de suficiente ori în același program, vom constata că aplicația va consuma din ce în ce mai multă memorie până nu mai rămâne fizic spațiu în memorie. Chiar dacă dealocăm 9 elemente din cele 10, la suficiente ciclări, acelaș lucru se va petrece.

Așadar, precum în vorba: tot ce urcă trebuie să și coboare, la fel, orice zonă de memorie alocată va trebui dealocată.


>

C
a
p
i
t
o
l
u
l

u
r
m
ă
t
o
r

>

Ți-a fost de ajutor ce am scris aici?
Hei, mersi de răspuns.