Adventi naptár

Czirkos Zoltán · 2021.08.24.

Változó hosszúságú tömbök

Tegyük fel, hogy a programunkban változó méretű tömböket használunk. Legyenek a tárolt adatok double típusúak. Minden lefoglalt memóriaterülethez a tömb méretét is természetesen meg szeretnénk jegyezni. Mivel egy lefoglalt memóriaterület (a tömb maga), és a méret két nagyon erősen összetartozó adat, az egészet egy struktúrába tesszük:

typedef struct {
    int meret;
    double *adat;
} DinTomb;

Így a függvényeinknek is kényelmesen át tudjuk adni a tömböt, mert a struktúra tartalmazza mindkét adatot. Ha ezekből a tömbökből is több van, illetve futás közben foglaljuk le őket, akkor magát a struktúrát is dinamikusan kell lefoglalnunk.

DinTomb *din_tomb_foglal(int meret) {
    DinTomb *uj = (DinTomb *) malloc(sizeof(DinTomb));
    uj->meret = meret;
    uj->adat = (double *) malloc(meret*sizeof(double));
    return uj;
}

DinTomb *szamok = din_tomb_foglal(30);
szamok->adat[15] = 3.14;

Ha szeretnénk egy ilyen tömböt felszabadítani, akkor pedig két free() hívásra van szükség. Egy malloc, egy free. És általában fordított sorrendben.

void din_tomb_free(DinTomb *dt) {
    free(dt->adat);     /* a double tomb */
    free(dt);           /* es a struktura */
}

Idáig tartott a kulturált megoldás.

1. A hackelés

Innentől jön a hackelés. Tudjuk azt, hogy a struktúrában az adattagok egymás után helyezkednek el (esetleg lehet közöttük néhány kitöltő (padding) bájt). Ha nem dinamikusan, hanem statikusan adjuk meg a tömb méretét, akkor kb. így néz ki a memóriatérkép:

typedef struct {
    int meret;
    double adat[4];
} DinTomb;
meret (int)
adat[0] (double)
adat[1] (double)
adat[2] (double)
adat[3] (double)

Ilyenkor az egyes tömbelemek, a tárolt számok is benne vannak magában a struktúrában. Ha egy ilyen struktúrányi helyet foglalunk dinamikusan, akkor értelemszerűen azoknak is le lesz foglalva a helye. Egy malloc() hívás kell majd csak, amihez egy free() tartozik, és ezért nem kell, vagy legalábbis nem érdemes külön függvényeket írni a foglalásra és a felszabadításra.

Kérdés az, hogy meg lehet-e ezt csinálni úgy is, hogy dinamikus legyen az adat[] tömb mérete. Márpedig meg lehet. Ha nagyobb területet foglalunk, akkor „túlindexelhetjük” a tömböt: jogosan, hiszen tudjuk, hogy nagyobb a terület. Definiáljuk ezért a struktúrát úgy, hogy a tömb egyetlen egy elemű, és foglaljuk le a következő módon:

typedef struct {
    int meret;
    double adat[1];         /* egy elemű tömb */    
} DinTomb;


DinTomb *uj = (DinTomb *) malloc(sizeof(DinTomb)+sizeof(double)*(meret-1));
uj->meret = meret;
return uj;

A sizeof(DinTomb) megadja a deklarált struktúra méretét; amelybe beleférnek a meret és az adat[1] adattagok. Vagyis a méret, és legalább egy számnak hely. Ha ehhez hozzáadjuk a további számok méretét (meret-1), akkor egy akkora memóriaterületet kapunk, amibe befér minden. A tömböt emiatt indexelhetjük 0-nál nagyobb számmal is. A fordító nem ellenőrzi a túlindexelést; mi pedig tudjuk, hogy pl. az adat[3] kifejezés hatására egy olyan double számra kapunk hivatkozást, amelyik még a lefoglalt memóriaterülethez tartozik. Nyilván, mert a tömbök elemei szorosan egymás mellett helyezkednek el. Felszabadítani ezt egyetlen egy free() hívással lehet, hiszen csak egy malloc() volt.

A dolog még egyszerűbb lenne, ha a C megengedné, hogy 0 méretű tömböt hozzunk létre: double adat[0], ilyenkor a memóriaterület méretének kiszámolásába sem kellene a -1. Amit amúgy igazából nyugodtan elhagyhatunk, mert 1 double-nyi memória nem a világ. De a 0 méretű tömböt nem engedi.

2. A hackelés C99-ben

A C99 megengedi azt, hogy definiálatlan méretű tömböt hozzunk létre (flexible array member), de csakis egy struktúrában, csakis a struktúra végén (utolsó tagjaként), csakis egy darabot. Vagyis a következő kódrészlet C99-ben legális:

#include <stdlib.h>
#include <stdio.h>

typedef struct {
    int meret;
    double adat[];   /* definiálatlan méretű, csak 1, csak a végén */
} DinTomb;

int main(void) {
    DinTomb *tomb;
    
    tomb = (DinTomb *) malloc(sizeof(DinTomb) + 10*sizeof(double));
    tomb->meret = 10;

    for (int i = 0; i < tomb->meret; ++i)
        tomb->adat[i] = i;
    for (int i = 0; i < tomb->meret; ++i)
        printf("tomb[%d] = %f\n", i, tomb->adat[i]);

    free(tomb);
    
    return 0;
}

Egy ilyen struktúrának egyébként csak dinamikusan foglalva van értelme; különben az adat tömb nulla méretű. Ezt ki is használjuk a méret kiszámításánál: a sizeof(DinTomb) értékéhez pont annyit kell hozzáadni, amekkora tömböt a struktúra végére szeretnénk biggyeszteni, se többet, se kevesebbet.