Pointerek és tömbök

Pointerek és tömbök

Ahhoz, hogy jobban megértsük a programjaink működését, beszélnünk kell a pointerek és tömbök kapcsolatáról. Előljáróban elmondhatjuk azt, hogy valamennyi művelet, ami elvégezhető tömbindexeléssel, az mutatók használatával is megvalósítható. Ehhez ismernünk kell persze azt, hogy hogyan is vannak a tömb elemei eltárolva a memóriában, illetve meg kell ismerkednünk azokkal a műveletekkel, amelyekkel egy pointer értékét tudjuk módosítani.

Amikor egy int a[10]; tömböt deklarálunk, akkor a memóriában allokálódik egy 10*sizeof(int) bájtnyi memóriaterület a tömb számára, amelynek egyes elemeire hivatkozhatunk a tömbelem hivatkozásokkal: a[0], a[1], ..., a[9].

Ha a pa deklarációja int *pa;, akkor a pa = &a[0]; értékadás úgy állítja be a pa-t, hogy az a tömb nulladik elemére mutasson, azaz az a[0] címét tartalmazza. Ha dereferenciával hivatkozunk a pa által mutatott változóra, akkor megkapjuk azt a tömbelemet, amire mutat a pa, azaz jelen esetben az x = *pa; értékadás bemásolja a[0] tartalmát x-be.

Pointer aritmetika

Ha egy int *pa; módon deklarált pointer értékét megnöveljük 1-gyel, akkor annak értéke annyival nő, amennyi méreten tárolódik az adat, amire pa mutat. Jelen esetben a pa + 1 kifejezés eredménye pa-nál sizeof(int)-tel lesz nagyobb.

Mivel a tömb elemei rendre követik egymást a memóriában, így ha eredetileg pa a tömb nulladik elemére mutatott, akkor a pa + 1 kifejezés már a tömb 1-es indexű értékére hivatkozik. Vagyis *(pa + 1) az a[1] tartalmát adja. Sőt, úgy általában

  • a pa + i az i. indexű (azaz a[i]) elem címe (&a[i]),
  • a *(pa + i) az a[i] elem értéke.

Mivel csak és kizárólag pa típusától függ az, hogy valójában a növekmény mekkora, így ez tetszőleges esetben működik. A pointeraritmetika alapja, hogy a pointert mindig az általa mutatott típus méretének függvényében változtatjuk. Az alapműveletek alapján több műveletet is értelmezhetünk:

  • a pa += i vagy pa -= i műveleteket az egész műveletekhez hasonló módon értelmezhetjük, azaz ha a pa egy tömbelemre mutatott, akkor a művelet hatására az azt követő illetve megelőző i. elemre fog mutatni;
  • a ++ vagy -- alkalmazása után (amely művelet lehet prefix, vagy postfix alakú, a művelet eredménye ettől függ) a pa értéke eggyel nő vagy csökken.

Mutatók kivonása szintén megengedett: ha p és q azonos típusú mutatók, akkor a p - q kifejezés a két mutató közötti tömbelemek (előjeles) darabszámát adja. (Arra persze a programozónak kell figyelnie, hogy ez legális érték-e, vagyis p és q valóban ugyanannak a tömbnek két elemére mutat-e?)

Amennyiben a pointer void* típusú, akkor a pointeraritmetika az egységet 1 bájtnak veszi.

Az említett műveleteken kívül (mutató és integer összeadása és kivonása, két mutató kivonása és összehasonlítása) minden más mutatóművelet tilos! Azaz nincs megengedve két mutató összeadása, szorzása, osztása, mutatók léptetése, maszkolása, sem pedig float vagy double mennyiségeknek mutatókhoz történő hozzáadása.

Pointerek és tömbök

Az indexelés és a pointer aritmetika között láthatóan nagyon szoros kapcsolat van. Gyakorlatilag a tömbre való hivatkozást a fordító a tömb kezdetét megcímző mutatóvá alakítja át. Mivel a tömb neve tulajdonképpen egy mutatóérték, az illető tömb nulladik elemének címe, a pa = &a[0] értékadás írható egyszerűbben pa = a-ként. Továbbá az a[i] hivatkozás írható *(a + i) alakban is (a[i] kiértékelésekor a C fordító azonnal átalakítja ezt *(a + i)-vé; a két alak teljesen egyenértékű) Ha mindkét elemre alkalmazzuk az & operátort, akkor látható, hogy &a[i] és a + i szintén azonosak: mindkettő az a-t követő i. elem címe.

Másrészről, ha a pa mutató, akkor azt kifejezésekben indexelhetjük tömbként is pa[i] alakban, ami ugyanaz lesz, mint a *(pa + i) kifejezés.

Röviden: bármilyen tömb vagy indexkifejezés leírható, mint egy mutató plusz egy eltolás, illetve fordítva. Van azonban egy fontos eltérés a tömbnév és a mutató között:

  • A mutató változóhivatkozás, így van értelme a pa = a vagy a pa++ kifejezéseknek.
  • A tömbnév azonban nem változóhivatkozás, csak egy pointer érték, így az a = pa, a++ vagy a pa = &a műveletek nem megengedettek (bár egyes fordítók utóbbit megérthetik, erre nem lehet számítani).

Tömbök és mutatók között meglevő különbségre világíthat még rá a következő példa is. Legyen a következő két deklarációnk:

1
2
char Honap[12][20];
char *honap[12];

Nyilván mind a honap[3][4], mind a Honap[3][4] szabályos változó hivatkozások, a megvalósítások között azonban már lényeges eltérések vannak. A Honap tömb méretét fixen ismerjük, ez egy 12*20 = 240 karakterből álló tömb. A honap tömb ezzel szemben pointerekből áll, amik sztringekre mutathatnak (lehetnek ezek bármekkora, akár 20 hosszú sztringek is). Azaz az össz memória, amire itt szükség van, az a 12 pointerneknek kellő memórai, plusz a 12 tetszőleges méretű memória.

Ha úgy deklaráljuk ezeket a mutatókat, hogy egyből inicializáljuk is, akkor rögtön tudjuk mindkettő tényleges memóriaigényét. A könnyebb összehasonlíthatóság miatt ugyanazzal az inicializáló listával inicializáljuk őket. (Azért, hogy a hónapok sorszáma megfeleljen a tömbbeli indexnek, a tömb 0. helyét egy semleges szöveggel láttuk el.)

1
2
char Honap[][20] = { "nincs 0. hónap",
    "január", "február", ... , "december" };

kep

Ebben az esetben minden sztringre tehát jut 20 karakter. Minden sztringet a '\0' karakter zár. Amelyik sztring rövidebb, annak a "kitöltetlen" karakterei számunkra érdektelenek (ezek az ábra kék színű mezői). (Ezeket az értékeket a fordító a tömb inicializálásának szabályai szerint 0 értékkel inicializálja.)

1
2
char *honap[] = { "nincs 0. hónap",
    "január", "február", ... , "december" };

kep

Itt a sztringeket inicializáló lista szavainak külön foglalódik memória a program egy olyan szegmensében, amit a konstans adatok tárolására tart fent. Mivel ez egy nem írható része a memóriának a program futása során, ezt hatékonyan le lehet foglalni abban a fix méretben, ami az inicializáló lista szavai számára szükséges. Azaz nem lesznek "érdektelen" területek lefoglalva. Emellett persze kell egy tömb, amibe a mutatók kerülnek. Ezek úgy indicializálódnak, hogy mindegyik a soron következő sztringre mutat.

Tömbök függvényparaméterként

Amikor a tömbnév egy függvénynek adódik át, a függvény valójában a tömb kezdetének címét kapja meg. A hívott függvényen belül a formális paraméter tehát egy címet tartalmazó változó. Amikor a tömbnév adódik át valamelyik függvénynek, a függvény tetszése szerint hiheti azt, hogy tömböt vagy mutatót kapott, és ennek megfelelően kezelheti azt.

Akár mindkét típusú műveletet használhatja, ha ez célszerűnek és világosnak látszik. Ezért lehet, hogy a sztringkezelő műveletekben a paraméterként kapott sztring megadható char str[] alakban is, de megadható char *str módon is. Az, hogy adott esetben melyiket használjuk, azon múlik, hogy a függvényen belül miként szeretnénk használni.

Mivel a függvénynek átadott tömb nem más, mint egy memóriacím, így arra is lehetőség van, hogy ne a teljes tömböt, csak annak egy darabját adjuk át. Ha például adott egy a tömb, akkor az f(&a[2]), illetve az f(a + 2) függvényhívás is a tömb 2-es indexű elemének címét adja át a függvénynek. Ilyen hívás esetén az f() függvény a tömb harmadik elemével kezdődő tömbrészletet kapja meg paraméterként, ezt tekinti a használandó tömbnek, amivel dolgoznia kell.

Amikor deklaráljuk a függvényt, a függvény fejléce lehet akár f(int arg[]) vagy f(int *arg).
Látjuk, hogy 1 dimenziós tömb esetében a tömb elemszámát sem kell megadni. Azonban több dimenziós tömbök esetében a helyes címszámítás érdekében a további dimenziók méretét meg kell adni.


Utolsó frissítés: 2020-10-21 18:18:34