LABORATORUL 08

Variabile pointeri. Variabile referințe.

Durată: 2 ore Limbaj: C / C++ Precedent: Laboratorul 7 PDF îndrumar EN English version

Pointerii sunt variabile care primesc ca valoare adrese de memorie. Sunt mecanismul care face posibile alocarea dinamică, structurile de date înlănțuite și modificarea parametrilor unei funcții. Sunt totodată sursa celor mai frecvente erori grave dintr-un program C.

1Obiectivele lucrării

xvaloare: 42adresa: 0x1000pvaloare: 0x1000adresa: 0x2000p pastreaza adresa lui xp = &x | *p este x*p = 7 modifica x, nu p
Fig. - Un pointer este o variabila obisnuita, cu propria adresa, a carei valoare este adresa altei variabile. Operatorul & obtine adresa, operatorul * ajunge la valoarea de la acea adresa.
  • Declararea pointerilor și înțelegerea rolului simbolului *
  • Folosirea operatorilor & (adresă) și * (dereferențiere)
  • Aplicarea regulilor aritmeticii pointerilor
  • Exploatarea legăturii dintre pointeri și tablouri
  • Modificarea parametrilor unei funcții prin pointeri și prin referințe

2Ce este un pointer

Fiecare variabilă ocupă o zonă de memorie care are o adresă. Un pointer este o variabilă obișnuită, cu deosebirea că valoarea pe care o conține este chiar o astfel de adresă.

CategorieConținutExemplu
Pointeri de dateadrese de variabile sau constanteint *p = &x;
Pointeri de funcțiiadrese de cod executabilint (*f)(int);
AnalogieDacă o variabilă este o casă, valoarea ei este ce se află înăuntru, iar adresa este numărul de pe stradă. Pointerul este o hârtiuță pe care ai scris acel număr: nu conține casa, ci indicația unde se găsește.

3Declarare și operatori

sintaxă
tip * nume_variabila;

int    *p_i;        // pointer la int
float  *p_f;        // pointer la float
int    *tab_p[10];  // tablou de 10 pointeri catre intregi
float **p_p;        // pointer la pointer
void   *p_v;        // pointer generic: poate primi adresa oricarui tip
OperatorNumeCe faceExemplu
&referențierereturnează adresa unei variabilep = &x;
* (la declarare)marcaj de tiparată că variabila este pointerint *p;
* (în expresii)dereferențiereaccesează valoarea de la adresa memoratăy = *p;
Același simbol, două înțelesuriÎn int *p; steluța face parte din tip. În *p = 7; steluța este operator și înseamnă „scrie la adresa conținută de p". Distincția se face după context: declarație sau expresie.
Pointerul voidvoid * poate primi adresa oricărui tip de date, dar nu poate fi dereferențiat direct - compilatorul nu știe câți octeți să citească. Este necesară o conversie explicită: *(int *)p_v.

4Simulator de memorie

Schimbați ținta pointerului și urmăriți diferența dintre p, *p și &p. Selectați NULL pentru a vedea ce înseamnă un pointer care nu indică nimic.

Pointer și variabilele către care poate indica

5Aritmetica pointerilor

Adunarea unui întreg la un pointer nu adaugă octeți, ci elemente. Compilatorul înmulțește automat cu dimensiunea tipului indicat.

OperațieEfect realPentru int * (4 octeți)
p + 1avansează cu 1 elementadresa crește cu 4
p + navansează cu n elementeadresa crește cu 4·n
p - qnumărul de elemente dintre ele(diferența adreselor) / 4
p++trece la elementul următoradresa crește cu 4
ConsecințăAceeași expresie p + 1 avansează cu 1 octet pentru char *, cu 4 pentru int * și cu 8 pentru double *. Tipul pointerului este esențial, nu decorativ.

6Pointeri și tablouri

Numele unui tablou este, în majoritatea contextelor, chiar adresa primului element. De aceea indexarea și aritmetica pointerilor sunt notații echivalente:

Notație cu indiceNotație cu pointerSemnificație
v&v[0]adresa primului element
v[i]*(v + i)elementul de pe poziția i
&v[i]v + iadresa elementului i
p[i]*(p + i)funcționează și pentru pointeri
Deosebirea esențialăUn tablou nu este un pointer. sizeof(v) pentru int v[10] dă 40, în timp ce sizeof(p) pentru int *p dă 4 sau 8 - dimensiunea unei adrese. În plus, numele tabloului nu poate fi modificat: v++ este eroare, p++ este permis.

7Transmiterea parametrilor la funcții

În C, parametrii se transmit prin valoare: funcția primește o copie. Modificările făcute în interior se pierd la revenire. Pentru a modifica variabila originală, se transmite adresa ei.

ModSintaxăPoate modifica originalul?Disponibil în
Prin valoarevoid f(int x)nuC și C++
Prin pointervoid f(int *x)da, cu *x = ...C și C++
Prin referințăvoid f(int &x)da, direct cu x = ...doar C++

8Variabile referință (doar în C++)

O referință este un nume alternativ pentru o variabilă existentă. Spre deosebire de pointer, nu ocupă memorie proprie și nu are sintaxă specială la utilizare.

referinta.cpp
int x = 10;
int &r = x;        // r devine un alt nume pentru x

r = 20;            // modifica direct pe x, fara nicio steluta
cout << x;         // afiseaza 20
AspectPointerReferință
Trebuie inițializat la declararenuda, obligatoriu
Poate fi redirecționat spre altcevadanu, niciodată
Poate fi NULLdanu
Sintaxă la utilizare*pr (ca variabila obișnuită)
Ocupă memorie propriedade regulă nu

9Erori frecvente

1. Pointer neinițializat int *p; *p = 5; scrie la o adresă aleatoare - programul se oprește sau, mai rău, corupe alte date. Inițializați întotdeauna: int *p = NULL;
2. Dereferențierea unui pointer NULL Verificați înainte de folosire: if (p != NULL) { *p = 5; }
3. Pointer suspendat (dangling) Returnarea adresei unei variabile locale este o eroare gravă: variabila este distrusă la ieșirea din funcție, iar adresa devine nevalidă. int* gresit() { int x = 5; return &x; }
4. Declarații multiple pe același rând int *p, q; declară un pointer și un întreg obișnuit, nu doi pointeri. Steluța se aplică fiecărei variabile separat: int *p, *q;

10Cod sursă

baza.c - operatorii & și *
#include <stdio.h>

int main(void)
{
    int x = 42;
    int *p = &x;              // p primeste adresa lui x

    printf("x       = %d\n",  x);      // 42
    printf("&x      = %p\n",  (void*)&x);
    printf("p       = %p\n",  (void*)p);    // aceeasi adresa
    printf("*p      = %d\n",  *p);     // 42 - valoarea de la acea adresa
    printf("&p      = %p\n",  (void*)&p);  // unde este stocat pointerul insusi

    *p = 100;                 // modifica x prin intermediul pointerului
    printf("\ndupa *p = 100, x = %d\n", x);   // 100

    printf("sizeof(x) = %zu, sizeof(p) = %zu\n", sizeof(x), sizeof(p));
    return 0;
}
aritmetica.c - parcurgerea unui tablou
#include <stdio.h>

int main(void)
{
    int v[5] = {10, 20, 30, 40, 50};
    int *p = v;                   // echivalent cu &v[0]

    printf("Prin indice:  ");
    for (int i = 0; i < 5; i++) printf("%d ", v[i]);

    printf("\nPrin pointer: ");
    for (int i = 0; i < 5; i++) printf("%d ", *(p + i));

    printf("\nCu incrementare: ");
    p = v;
    for (int i = 0; i < 5; i++) printf("%d ", *p++);

    // adresele cresc din 4 in 4 octeti
    printf("\n\nAdrese:\n");
    for (int i = 0; i < 5; i++)
        printf("  &v[%d] = %p\n", i, (void*)&v[i]);

    // diferenta a doi pointeri = numarul de elemente dintre ei
    int *inceput = &v[0], *sfarsit = &v[4];
    printf("\nElemente intre inceput si sfarsit: %ld\n", sfarsit - inceput);

    return 0;
}
swap.c - de ce este nevoie de pointeri
#include <stdio.h>

void swapGresit(int a, int b)      // primeste COPII
{
    int aux = a; a = b; b = aux;   // schimba doar copiile
}

void swapCorect(int *a, int *b)    // primeste ADRESE
{
    int aux = *a; *a = *b; *b = aux;
}

int main(void)
{
    int x = 1, y = 2;

    swapGresit(x, y);
    printf("Dupa swapGresit:  x=%d y=%d\n", x, y);   // 1 2 - neschimbate

    swapCorect(&x, &y);
    printf("Dupa swapCorect:  x=%d y=%d\n", x, y);   // 2 1 - schimbate

    return 0;
}
referinte.cpp - varianta C++
#include <iostream>
using namespace std;

void swapRef(int &a, int &b)       // referinte: sintaxa curata
{
    int aux = a; a = b; b = aux;   // fara steluta
}

void adaugaLa(int &valoare, int cat)
{
    valoare += cat;
}

int main()
{
    int x = 1, y = 2;

    swapRef(x, y);
    cout << "x=" << x << " y=" << y << endl;   // 2 1

    adaugaLa(x, 10);
    cout << "x dupa adaugare = " << x << endl;   // 12

    // o referinta nu poate fi redirectata
    int a = 5, b = 9;
    int &r = a;
    r = b;              // ATENTIE: copiaza valoarea lui b in a,
                        // NU face ca r sa se refere la b
    cout << "a=" << a << " b=" << b << endl;    // 9 9

    return 0;
}

11Atelier de cod

Aici modul Pas cu pas face diferența: panoul din dreapta afișează adresa fiecărei variabile și, pentru pointeri, către ce anume arată în acel moment.

Un pointer care se plimbă printr-un tablou
#include <stdio.h>

int main(void)
{
    int v[4] = {10, 20, 30, 40};
    int *p;

    p = v;                    /* p arata catre v[0] */
    printf("*p       = %d\n", *p);

    p = p + 2;                /* +2 inseamna 2 elemente, nu 2 octeti */
    printf("dupa p+2 : *p = %d\n", *p);

    *p = 99;                  /* modifica v[2] prin pointer */
    printf("v[2]     = %d\n", v[2]);

    printf("p - v    = %d elemente\n", (int)(p - v));
    printf("sizeof(int) = %d octeti\n", (int)sizeof(int));
    return 0;
}
De încercatRulați pas cu pas și urmăriți linia lui p din panoul din dreapta: adresa crește cu 8 (adică 2 × 4 octeți), iar eticheta din dreapta se schimbă din v[0] în v[2].
Exercițiu - modificare prin pointeri
#include <stdio.h>

/* Aceasta functie NU functioneaza: primeste copii ale valorilor */
void schimbaGresit(int a, int b)
{
    int t = a; a = b; b = t;
}

/* Scrieti aici varianta corecta, care primeste adresele:
   void schimbaCorect(int *a, int *b) { ... }                */

int main(void)
{
    int x = 1, y = 2;

    schimbaGresit(x, y);
    printf("dupa schimbaGresit : x = %d, y = %d\n", x, y);

    /* Apelati schimbaCorect(&x, &y); */

    printf("dupa schimbaCorect : x = %d, y = %d\n", x, y);
    return 0;
}

12Sarcini de lucru

  • Declarați o variabilă și un pointer către ea; afișați valoarea, adresa și rezultatul dereferențierii.
  • Modificați variabila exclusiv prin pointer și verificați efectul.
  • Comparați sizeof pentru un int, un double și pointerii către ele.
  • Parcurgeți un tablou în trei moduri: cu indice, cu *(p+i) și cu *p++.
  • Afișați adresele elementelor și verificați că diferența este sizeof(tip).
  • Implementați funcția de interschimbare, mai întâi greșit (prin valoare), apoi corect (prin pointeri).
  • Scrieți o funcție care primește un vector și returnează prin pointeri atât minimul, cât și maximul.
  • În C++, rescrieți interschimbarea folosind referințe și comparați lizibilitatea codului.

13Aplicație de aprofundare

ExtindereScrieți o funcție care primește un tablou de întregi și returnează, printr-un singur apel, patru rezultate: suma, media, minimul și maximul - folosind exclusiv parametri de tip pointer. Extindeți apoi programul cu un tablou de pointeri către funcții (int (*operatii[4])(int, int)) care permite selectarea operației aritmetice dintr-un meniu.

14Întrebări de verificare

15Resurse