Info 2, Negyedik heti előadás: mutatók

Utolsó módosítás: 2009. március 5.

A C nyelv egyik származtatott típusa a mutató (pointer). Egy mutató érték megadja egy másik változónak a helyét, így a mutatón keresztül ennek a változónak a tartalmát ki tudjuk olvasni, vagy módosítani tudjuk.

A mutatók szintaxisának alapja a következő.

  1. Minden típushoz külön mutatótípus tartozik. Ha T egy típus, akkor T-re mutató változót dekralálhatunk így: T *p;. Egy ilyen változó értéke tehát kijelöli egy T típusú változó helyét.
  2. Ha x egy T típusú változó (vagy bármilyen T típusú balérték), akkor az &x kifejezés egy mutató, ami x-re mutat, ennek a típusa T *. Az egyoperandusú & operátor tehát egy balértékből annak a címét állítja elő.
  3. Ha p egy mutató érték, ami egy létező változóra (vagy más balérték objektumra mutat), akkor a *p adja meg azt a változót, ahova ez mutat. Ezt a *p kifejezést ugyanúgy használhatjuk, mint egy változót: módosítjuk a mutatott változó tartalmát, ha egy értékadás bal oldalán használjuk; vagy kiolvashatjuk a tartalmát, ha egy kifejezésben máshol használjuk. A * operátor tehát egy mutatóból az általa mutatott balértéket állítja elő.

Egy egyszerű példa a következő.

#include <stdio.h>
int 
main(void) {
        int a;
        int *p;
        p = &a;
        *p = 42;
        printf("%d\n", a);
        a = 137;
        printf("%d\n", *p);
        return 0;
}

A kimenet a következő.

42
137

A p egy int-re mutató típusú változó. Miután ebbe beleraktuk az a változó címét, ezen keresztül írjuk és olvassuk is a változót.

A mutató értékeket lehet másolni is, például értékadással, függvénynek argumentumként átadással, vagy függvényből visszatérési értéknek adással.

Nézzük a következő példát.

#include <stdio.h>
void
nothing(int x) {
        x = 10;
}
void
modify(int *p) {
        *p = 10;
}
int 
main(void) {
        int a = 5;
        nothing(a);
        printf("%d\n", a);
        modify(&a);
        printf("%d\n", a);
        return 0;
}

A kimenet:

5
10

Itt a modify függvény paramétere int-re mutató típusú. Ezen keresztül a függvény a main függvényben lévő lokális változót közvetetten tudta elérni. Vegyük észre, hogy az a változó nevét nem használhattuk volna közvetlenül a modify függvényben, mivel ez lokális a main függvényre, ezért csak ott van hatókörben. Ennek ellenére a változó maga végig létezik a main függvény elindulásától a kilépéséig, ezért egy mutatón keresztül el szabad érni.

A mutatóknak ez a használata hasznos, ha egy függvény több eredményt szeretne átadni a hívónak, mint amennyit egyszerűen a visszatérési értékbe belerakhatna. Így működik például a scanf függvény is: ez egyszerre több adatot is be tud olvasni a bemenetről, ezért argumentumnak azokat a címeket adjuk át, ahova eltárolja a beolvasott adatokat, a visszatérési értéke pedig csak a sikerességet jelzi.

Amikor a nothing függvényt meghívjuk, akkor a függvény x paramétere létrejön, mint egy lokális változó a nothing-ban, és ennek az értéke a híváskor átadott argumentumra állítódik be. Ez a változó tehát máshol van, mint az a változó a main függvényben, a kettő értékét egymástól függetlenül lehet módosítani, amíg a nothing függvény fut. A nothing függvény tehát csak egy másolat értékét módosítja (ezt a másolatot a függvénytörzsben később fölhasználhatnánk valahogyan), az a változót nem.

Azt is vegyük észre, hogy a nothing és a modify függvényt különböző módon hívjuk meg

nothing(a);
modify(&a);

Az elsőnek egy egészt adunk argumentumként, mivel a paraméter típusa egész, a másodiknak egy int-re mutatót adunk át, mivel a paraméter int-re mutató. Fordítva nem hívhatnánk meg a függvényeket, mivel az argumentumukkal nem tudnának mit kezdeni: a fordító figyelmeztetést vagy hibát adna az eltérő típusok miatt.

Vegyük azt is észre, hogy a nothing függvény argumentuma lehetne egy konstans vagy ideiglenesen számított kifejezés is, mint például

nothing(a + a);
nothing(19);

de ezeknek a kifejezéseknek nem vehetjük a címét, így ez hibás lenne:

modify(&(a + a)); /* HIBÁS */
modify(&19); /* HIBÁS */

Az a + a vagy 19 kifejezések nem balértékek, nincsen címük, éppen ezért nem lehetne őket értékadás bal oldalán sem használni, mint például:

a + a = 10;
19 = 10;

Nézzünk még két dolgot, amit a mutatókkal nem szabad csinálni.

#include <stdio.h>
int 
global = 4;
int *
good(void) {
        return &global;
}
int *
bad(void) {
        int local = 8;
        return &local;
}
int 
main(void) {
        printf("%d\n", *good());
        printf("%d\n", *bad()); /* HIBÁS */
        return 0;
}

A bad függvényben deklarált lokális változó csak akkor jön létre, amikor a függvényt meghívjuk, és megsemmisül, amikor a függvény visszatér. Ezért aztán indirekt módon sem szabad elérni ezt a változót a függvény visszatérése után, így a main függvényben a kapott mutató rossz helyre mutat. Valójában a függvény minden hívásánál új lokális változó jön létre ugyanazon a néven, ami az előző hívásokban használt változókkal nem áll kapcsolatban.

Az indirekció * operátort tehát csak akkor szabad használni (akár olvasásra, akár írásra), ha biztosak vagyunk benne, hogy létezik az a dolog, amire az argumentuma mutat.

A másik hiba talán még nyilvánvalóbb.

#include <stdio.h>
int 
main(void) {
        int a;
        int *p;
        int *q;
        p = &a;
        scanf("%d", p); 
        printf("%d", *p);
        scanf("%d", q); /* HIBÁS */
        printf("%d", *q); /* HIBÁS */
        return 0;
}

A q változónak nem adtunk értéket, így nem tudhatjuk, hova mutat. A scanf függvény második hívása az ez által mutatott helyre akarna írni, ami szabálytalan, hasonlóan a *q olvasás is szabálytalan. Ha a programot lefordítjuk, a fordító figyelmeztet, hogy a q értékét inicializásás nélkül használjuk, de a fordító bonyolultabb esetben nem mindig tudja megtalálni az ilyen hibákat. Ha lefuttatjuk a programot, jó esetben a q változó olyan címre fog mutatni, ahol nincs a processznek memória lefoglalva, ezért egy Segmentation fault üzenettel leáll a program; rossz esetben nem kapunk rögtön hibát, hanem valami más adatot átír a program a memóriában, ami a futás során később okozhat gondot.

Megjegyzem, hogy a függvény elején a deklarációkat és az értékadást így is lehetne írni:

int a, *p = &a, *q;