Informatika2-2012/Eloadas04

A MathWikiből

Tartalomjegyzék

Mutatók (pointer-ek)

Mi is egy mutató?

A mutató (angolul pointer) típusú adat egy másik adat címét tárolja, vagyis azt, hogy hol van tárolva az adat a számítógép memóriájában. (Ez tulajdonképpen csak egy szám, ami azonosítja a memória egyik "rekeszét".) (Első kép innen.)

Minden mutatót ugyanolyan belső ábrázolással tárolunk (hiszen mindegyik egy cím egy memóriaterületre, egész pontosan a terület "kezdőpontjára"), mégis van "típusa", ami hivatkozott objektum típusát határozza meg. Ebből tudja majd a program futás közben, a mutató által mutatott objektum használatakor, hogy "mekkora" az objektum maga, vagyis mekkora az az adatterület a memóriában, ami a mutatott változóhoz tartozik.

Mire jók a mutatók?

Első ránézésre a mutatók csak bonyolítják a dolgokat. Programozóként miért kell tudnunk egy változó címét? A Python jól el tudta rejteni előlünk azt, hogy a változók hogyan és hol is léteznek fizikailag a memóriában (sőt még a változók típusát is elrejti nagyjából), és ez eléggé kényelmes volt.

A C egy alacsonyabb szintű nyelv, itt "gépközelibb" eszközökkel kell dolgoznunk. Ez egyrészt kényelmetlen lehet, másrészt viszont lehetőséget ad olyan optimalizálásokra, olyan hatékony programok írására, ami egy magasabb szintű nyelven nehézkes vagy lehetetlen lenne. (Pl: bitenkénti műveletek, bool vektor tárolása)

Konkrétabban mire jók a mutatók?

  • komplex adatstruktúrák kialakítására (láncolt listák, fák)
  • tömbök címzésére
  • függvénynek paraméterként átadható: csak a cím másolódik, ez egyrészt hatékonyabb egy nagy adatstruktúra esetén, másrészt így a mutatott objektumot megváltoztathatja a függvény
  • akár több mutatónk is lehet ugyanarra a memóriacímre: adatok sorbarendezése többféle sorrendben, adat-többszörözés nélkül

Hogy néz ki egy mutató C-ben?

Két operátort kell ismernünk a mutatók használatához (mindkettőt a változó neve elé kell írni amire vonatkoztanni akarjuk, és lehet köztük szóköz is):

  • Egy létező változó címét a "&" operátorral kérhetjük el.
  • A "*" operátorral kérhetjük el a dolgot/objektumot amire egy mutató mutat

Egy egyszerű példa: a "szam" egész típusú változó értékét a mutatóján keresztül állítjuk be. (Emlékeztető: az értékadás operátora az "=", és ez mindig a bal oldali dolognak ad új értéket, a bal oldali változó új értéke a jobb oldali kifejezés kiértékelésének eredménye lesz.)

/* egy szám deklarálása */ 
int szam;
/* létrehozzuk a mutatót, a típusa "olyan mutató ami int típusra mutat"  */
int *szam_ptr;
 
/* itt még nincsenek "összekötve" a fenti változók, 
nincs is értékük (ill. a lokális int-nem 0 lesz az értéke), 
a lényeg hogy a memóriában lefoglalódott nekik a hely*/ 
 
/* most értéket adunk a mutatónknak, azt mondjuk mutasson 
a "szam"-ra vagyis a "szam" címével tesszük egyenlővé */
szam_ptr = &szam;
 
/* végül a "szam" értékét beállítjuk 3-ra a mutatót használva */
*szam_ptr = 3;

Nem kötelező, de ajánlott, hogy a mutató típusú változóknak a neve is utaljon erre, vagyis végződjön a neve "_ptr"-re, vagy kezdődjön "p_"-vel (mindegy melyiket választod, de utána - legalább egy programon belül - mindig csak azt a jelölést használd amit választottál!). Ez segít a kód megértésében, csökkenti a hibák valószínűségét.

Figyelem! Így NE!

int szam;
int *szam_ptr;
 
/* ha kihagyjuk ezt a lépést:*/
/* szam_ptr = &szam; */
 
/* és így írunk arra a memóriaterületre amire a szam_ptr mutat... */
*szam_ptr = 3;

... akkor nem tudjuk hogy mi is fog történni, a szam_ptr értéke véletlenszerűan akármi lehet, és mi erre a random helyre írunk! Véletlenül felülírhatunk más változókat, vagy akár magát a futó programkódot! Nehéz megtalálni egy ilyen hibát, ugyanis nem lesz determinisztikus a programunk működése, lehet hogy legtöbbször teljesen jól le fog futni, néha azonban kiszámíthatatlan hibák fognak történni.

Ennek elkerülésére egyrészt ha lehet a mutatókat rögtön a létrehozáskor definiáljuk is, pl:

int szam;
int * szam_ptr = & szam;

vagy definiáljuk a mutatót a NULL értékkel, ami azt jelenti hogy ez a mutató még nem mutat sehova:

int *szam_ptr = NULL;

Tömbök címzése, "pointer aritmetika"

Mint tudjuk, a C-beli tömbök csak azonos típusú elemeket tartalmazhatnak, amik értelemszerűen fix méretű helyet foglalnak el a memóriában. Azt is érdemes tudni, hogy a tömbök elemei mindig sorban és közvetlenül egymás után lesznek a elérhetőek a memóriában. (Ezért ha nagyon nagy tömböt akarunk lefoglalni az problémás lehet, hiszan akkora helyet "egyben" kell találni a gép memóriájában.)

Egy tömb elemei tehát sorban vannak. Ez lehetővé teszi hogy az egyes elemek címeit (pointereit) összehasonlítsuk (aminek nagyobb a címe az hátrébb van a tömbben), vagy akár műveleteket végezzünk velük (összeadás, szorzás).

Így hozhatunk létre két mutatót amik egy tömb 4. és 8. elemére mutatnak:

	double *d_ptr1, *d_ptr2;
	double a[10];
	d_ptr1 = &a[3];
	d_ptr2 = &a[7];
Mutatók összehasonlítása

Csak olyan mutatókat hasonlítsunk össze, amik ugyanannak a tömbnek az elemeire mutatnak! A használható operátorok:

==  !=  >  <  >=  <=

Például a fenti kódot kiegészíthetjük így, hogy a későbbi elemet írja ki:

	double *d_ptr1, *d_ptr2;
	double a[10];
	d_ptr1 = &a[3];
	d_ptr2 = &a[7];
	if (d_ptr2 > d_ptr1) {
		printf("%lf\n", *d_ptr2);  /* a mutatott értéket írjuk ki ! */
	} else {
		printf("%lf\n", *d_ptr1);
	}
Mutatók összeadása, kivonása

(Képek itt.)

A pointer aritmetika arra ad lehetőséget, hogy egyszerűen léptessük a mutatóinkat egy tömbön belül. Ha egyet hozzáadunk a mutatóhoz, az a következő tömbbeli elemre fog mutatni. Továbbírjuk az eredeti kódot, d_ptr2 az ötödik elemre fog mutatni a tömbben:

	double *d_ptr1, *d_ptr2;
	double a[10];
	d_ptr1 = &a[3];
	d_ptr2 = &a[7];
	double *d_ptr3 = d_ptr1 + 1;
	/* és ezzel akár értéket is adhatunk a[4]-nek vagyis az 5. elemnek: */
	*d_ptr3 = 11;
	/* a rákövetkező (hatodik) elemet is beállítjuk, a d_ptr1-et és összeadást használva: */
	*(d_ptr1+2) = 45;

Az utolsó sorban muszáj a zárójeleket kitenni, mert a * operátor "erősebben köt" a változónévhez mint az összeadás operátora (az operátorok erősségi sorrendjéről, vagyis a precedenciákról később tanulunk részletesebben).

Ha van két mutatónk amik ugyanannak a tömbnek az elemeire mutatnak, akkor a különbségük megmondja hogy hány elem van köztük a tömbben. Például:

	double *d_ptr1, *d_ptr2;
	double a[10];
	d_ptr1 = &a[1];
	d_ptr2 = &a[9];
	printf("%d \n", (d_ptr2 - d_ptr1)); 
	/* kiírja hogy 8 */

Többdimenziós tömbök

Létrehozhatunk többdimenziós tömböket is, pl egy 4x3-asat:

  int size1 = 4;
  int size2 = 3;
  int a[size1][size2];

Ezt valahogy így képzelhetjük el:

                   +-----+-----+-----+
       a[0]   ---> | a00 | a01 | a02 |
                   +-----+-----+-----+
                   +-----+-----+-----+
       a[1]   ---> | a10 | a11 | a12 |
                   +-----+-----+-----+
                   +-----+-----+-----+
       a[2]   ---> | a20 | a21 | a22 |
                   +-----+-----+-----+
                   +-----+-----+-----+
       a[3]   ---> | a30 | a31 | a32 |
                   +-----+-----+-----+

De a memóriában ez is lineárisan foglalhat helyet, összesen size1 * size2 * sizeof(int) méretű helyet fog elfoglalni.

Legkényelmesebb úgy használni hogy a címzés is két(vagy több)szintű:

  int size1 = 4;
  int size2 = 3;
  int a[size1][size2];
 
  a[0][0] = 2; /* a fenti ábrán az "a00" elemnek adunk értéket */
  a[3][2] = 5; /* a fenti ábrán az "a32" elemnek adunk értéket */


Végül egy kicsit hosszab kód, amiben a kétdimenziós tömb elemein végigmegyünk, és kétféleképpen is elérjük az egyes elemeket:

  • először a szögletes zárójeles indexeléssel
  • másodszor pedig a tömb kezdő elemének címéből számolt mutatókkal
#include <stdio.h>
int main(void) {
    int size1 = 4;
    int size2 = 3;
    int a[size1][size2];
	int *ptr;
    int i, j;
    int e;  /* mutató eltolását számoljuk benne  */
    ptr = &a[0][0];     /* mutató a00-ra */
    printf("\n\n");
 
    for (i = 0; i < size1; i++) {
      for (j = 0; j < size2; j++) {
      	e = i*size2 + j;
        printf("a[%d][%d] = %d \t\t", i, j, a[i][j]);
        printf("ptr + %d = %d\n", e, *(ptr + e));
      }
    }
    return 0;
}

Ha lefordítjuk és futtatjuk, valami ilyesmi lesz a kimenet:

 a[0][0] = 134513112 		ptr + 0 = 134513112
 a[0][1] = -1081288636 		ptr + 1 = -1081288636
 a[0][2] = -1216907324 		ptr + 2 = -1216907324
 a[1][0] = 0 		ptr + 3 = 0
 a[1][1] = -1217024512 		ptr + 4 = -1217024512
 a[1][2] = 1 		ptr + 5 = 1
 a[2][0] = 0 		ptr + 6 = 0
 a[2][1] = 1 		ptr + 7 = 1
 a[2][2] = 0 		ptr + 8 = 0
 a[3][0] = 0 		ptr + 9 = 0
 a[3][1] = 0 		ptr + 10 = 0
 a[3][2] = 0 		ptr + 11 = 0

Ezen a példán látszik, hogy a lefoglalt tömbök elemei nincsenek "kitakarítva", memóriaszemét van bennük. Az értékek betöltéséről vagy 0-ra inicializálásról nekünk kell gondoskodnunk.

De az is látszik, hogy a kétféle címzési mód ugyanazt eredményezte.

Mutató átadása függvénynek

Azzal hogy egy változó címét adjuk át, lehetőséget kap a függvény, hogy a változó értékét módosítsa. Ezzel a "trükkel" az is megoldható, hogy egyszerre több kiszámított értéket is "elkérjünk" a függvénytől: egy-egy változó-címet adunk át minden olyan dologhoz, amit szeretnénk hogy a függvény kitöltsön.

Egy kis példa apluszminusz függvény a kapott a és b egészek összegét és különbségét is kiszámolja, az egyiket visszaadja ahagyományos módon a return paranccsal, a másikat egy kapott címre írja:

#include <stdio.h>
 
/* visszaadja a es b különbségét, és a *sum -ba irja az összegüket */
int pluszminusz(int a, int b, int* sum_ptr) {
    /* sum_ptr egy mutató -> (*sum_ptr) egy int változó */	
    *sum_ptr = a + b;   /* precedencia:  (*sum) = (a+b) */
    return a - b;
}
 
int main() {
    int x = 22;
    int y = 9;
    int kulonbseg, osszeg;
    int * osszeg_ptr = &osszeg;  /* fontos hogy értéket kapjon a mutató mielőtt használjuk,
                                    egy létező változó cimét! (különben segmantation fault) */
    kulonbseg = pluszminusz(x, y, osszeg_ptr);
    printf("Kulonbseg: %d \t Osszeg: %d \n ", kulonbseg, *osszeg_ptr );
    return 0;
}

Források és további olvasnivalók


Ellenőrző kérdések

  • Az alábbiak közül melyik egy érvényes mutató-deklaráció?
 A. int x;
 B. int &x;
 C. ptr x;
 D. int *x;
 
  • Az alábbiak közül melyik kifejezés jelenti azt hogy "a b változó címe a memóriában" ?
 A. *b;
 B. b;
 C. &b;
 D. address(b);
 
  • Melyik kifejezés adja meg azt az értéket amely azon a memóriacímen van tárolva ahová a p mutató mutat?
 A. p;
 B. val(p);
 C. *p;
 D. &p;
Személyes eszközök