Tovább komplikálja a dolgokat az, hogy a tömb valójában nem egy valódi típus C esetén, hanem egy mutató az első elemre. Ebből adódóan egy tömb mérete nem tárolt, nem kérdezhető le kényelmesen a Length
tulajdonsággal.
Az egyszerű hossz lekérdezés másik hátránya, hogy a program sem tudja futás közben, hogy mekkora egy tömb. Ebből adódóan túl és alul is tudunk indexelni egy tömböt. Ez azt jelenti, hogy egy három elemű tömbből ki tudjuk venni a 4., de akár a -1. elemet is:
#include <stdio.h>
int main()
{
int tomb[] = {1, 2, 3};
int utana = 42;
printf("tomb[3]: %d\r\n", tomb[3]);
}
A program kimenete x64 Windows esetén:
tomb[3]: 42
A fenti példában a tömb elemeinek indexe 0, 1 és 2 lehetnének. Ennek ellenére, ha a 3. indexű elemet ki akarjuk venni, akkor nem kapunk hibát, sem fordítási figyelmeztetést. A fenti példában a 3. indexű elemre 42-t kapunk, ami az utana
változó tartalma, mivel a memóriában egymás után helyezkednek el. Itt azonban megjegyezném, hogy ez nem feltétlen igaz minden rendszerre, mivel a tömbök alul és felül indexelése szinten undefined behaviour, mivel az eltérő architektúrák eltérő memória modellt és címzést alkalmazhatnak.
Az alul és felül indexelés azért veszélyes, mert ha ez például egy felhasználói bevitel eredménye, akkor a programunk felülírhatja a memóriában saját magát, ami hibás működést eredményez. További veszély, hogy az ilyen hibák könnyen válhatnak tetszőleges kódfuttatást lehetővé tevő biztonsági résekké. Éppen ezért a C fordítók védekeznek valamilyen szinten ez ellen. Ha a függvényünkben tömb van, akkor a fordító a tömb után és a visszatérési érték elé generál egy kódrészletet, ami a függvénybe belépéskor egy véletlen számot helyez el a stack-en. A visszatérés előtt ellenőrzi ennek az épségét. Ha túlindexelés történt, akkor ez az érték sérülni fog és ebből tudja a hívó kódrészlet, hogy valami hiba történt. A hiba hatására pedig a programot a kódrészlet össze fogja omlasztani, hogy ne kerülhessen kiszámíthatatlan állapotba.
Ezt a védekezési módszert szokás stack cookie-nak vagy stack kanárinak is nevezni, de mint a fenti példából látható, ez sem tud minden ellen védeni. Éppen ezért tömbök esetén nagyon körültekintően kell indexelni.
Az alábbi példa a tömbök alapvető kezelését mutatja be:
#include <stdio.h>
/*szöveg kezelő függvények*/
#include <string.h>
int main()
{
/*tömb létrehozása elemek megadásával*/
int tomb[] = { 0, 1, 2, 3, 4 };
/*Tömb megadása mérettel és
minden elem 0-ra inicializálása*/
int masik[3] = {0};
/*memória másolása a masik változóba,
majd kiiratjuk a masik tömb 2. elemét (3)*/
memcpy(masik, &tomb[1], 3*sizeof(int));
printf("masik[2]: %d\r\n", masik[2]);
int tombhossz = sizeof(tomb) / sizeof(int);
printf("tomb[] hossza: %d\r\n", tombhossz);
/*kezdő tömb feltöltése 0-val*/
memset(tomb, 0, sizeof(int)*5);
return 0;
}
C#-hoz hasonlóan a tömb megadható explicit módon az elemeinek felsorolásával. A tömb létrehozása szintén nem inicializálja az elemeket. Éppen ezért, ha egy kezdő értékre szeretnénk minden elemet állítani, akkor a kapcsos zárójelek között egy értéket kell megadnunk.
A memcpy
függvény segítségével tudunk memóriát másolni, ami a <string.h>
header-ben található, mivel leginkább szövegek esetén alkalmazott. Ezzel tudjuk egy tömb teljes vagy részleges tartalmát másolni egy másik tömbbe. A függvény első paramétere a cél tömb, a második pedig a forrás tömb. A harmadik paraméter a másolandó memória mennyisége. A példában itt vesszük a tömb alap típusának int
méretét, majd ezt megszorozzuk hárommal, mert ennyi elemet szeretnénk másolni.
Mivel a tömb nem valódi típus és csak egy pointer az első elemre a memóriában, ezért tudunk olyan „trükköt” alkalmazni, hogy ha csak az első elemtől szeretnénk másolni akkor az első elem memória címét helyettesítjük be, mint a példában. Ez a megoldás természetesen a cél esetén is alkalmazható.
Egy másik fontos függvény a memset
, aminek a segítségével egy darab memóriát tudunk egy adott értékre beállítani. Az első paraméter itt is a cél tömb, a második paraméter az érték, amivel fel szeretnénk tölteni a tömböt, a harmadik paraméter pedig szintén a méret.
Ha a méretet változóban szeretnénk tárolni, akkor a size_t
típust alkalmazzuk. Ez egy előjel nélküli egész szám, amibe a platform által megcímezhető memória mennyisége biztos bele fog férni. Mérete 16 bitnél nem lehet kevesebb.
Egy érdekesség, hogy a []
operátor használata indexelésnél nem kötelező. Ha egy tömb 4. elemére vagyunk kíváncsiak, akkor a tomb[3]
helyett a *(tomb + 3)
is működik. Kétdimenziós tömbök esetén a tomb[2][3]
helyett pedig a *(*(tomb + 2) + 3)
is működik. A miért kérdésre a válasz az, hogy a tömb pointer voltából következik, hogy az indexelés sem más, mint pointer aritmetika.
Ennek az aritmetikának egy hozománya, hogy ha a tomb[3] = *(tomb + 3)
, akkor a tomb[3] = *(3 + tomb)
kifejezés is helyes. Ebből következik, hogy a tömb 4. elemére akár így is hivatkozhatunk: 3[tomb]
.
Ennek kapcsán azért megjegyezném, hogy attól, hogy valami lehetséges, még nem biztos, hogy a legjobb ötlet. Éppen ezért ennek a technikának a használatát kerüljük saját programjaink esetén.