Generikus lista

Czirkos Zoltán · 2021.08.24.

Generikus lista: bármit tárolni tudó lista, void* pointer segítségével.

Ez az oldal egy olyan duplán láncolt listát mutat be, amelyik bármiféle adatot képes tárolni. Ezt úgy oldja meg, hogy a listában tárolt adat csak egy pointer – hogy a programozó azt mire használja, az már az ő dolga.

1. Generikus lista

Eddig a lista elemeinek adataihoz (pl. könyv címe, szerzője, megjelenési éve) adtunk hozzá egy pointert, ami lehetővé tette, hogy az adott struktúrából láncolt lista legyen. Akármilyen elemeket viszont nem fűzhetünk így listába – a struktúra megalkotásakor már a pointerrel gondolni kellett volna arra, hogy listát kell csinálni belőle. Érezhetően problémás, hogy a struktúra egyszerre tartalmazza az adatokat, és a listába fűzéshez szükséges pointereket.

Ehelyett csináljuk most azt, hogy létrehozunk egy ListaElem nevű struktúrát, amelyben csak egy void * mutató lesz. Ez tetszőleges típusú adatokra mutathat, amelyekhez a memóriát külön foglaljuk majd le:

typedef struct ListaElem {
    struct ListaElem *elozo, *kovetkezo;
    void *adat;
} ListaElem;

Tudjuk, hogy a void * típusú mutató bármilyen adatra mutathat. Ez azt jelenti, hogy a tetszőleges típusú adatunkra mutató pointert, pl. Pont *-ot betehetjük ide. Sajnos abban a pillanatban, hogy megtörténik a Pont*→void* konverzió, a típusinformáció elveszik. Ezért amikor az adatot kiolvassuk a láncolt listából, a pointert vissza kell alakítanunk a saját típusunkra, pl. egy (Pont*) cast-tal.

2. Adat a listába

Építsünk a fenti struktúra segítségével duplán láncolt, mindkét végén strázsát használó listát:

/* ezek meg strazsak lesznek  */
typedef struct Lista {
    ListaElem *eleje, *vege;
} Lista;

Tegyünk bele dinamikusan foglalt sztringeket!

char *str = (char*) malloc(5*sizeof(char));          // din. foglalt adat
strcpy(str, "alma");

ListaElem *uj = (ListaElem *) malloc(sizeof(ListaElem));  // új listaelem
uj->kovetkezo = l->eleje->kovetkezo;
uj->elozo = l->eleje;
l->eleje->kovetkezo->elozo = uj;
l->eleje->kovetkezo = uj;

uj->adat = str;                                // char* → void*

Amikor a listaelembe betesszük a pointert, a saját típusunkra mutató pointerből void* típusú pointer lesz.

3. A lista bejárása

A lista bejárása a következő kódrészlettel lehetséges:

ListaElem *futo;

for (futo = l->eleje->kovetkezo; futo != l->vege; futo = futo->kovetkezo)
    … l->adat …; // teendő a listaelemmel

A lista bejárása ugyan általánosságban így működik, a listával elvégzendő teendő azonban a lista adatainak konkrét típusától függ. Vegyük itt észre az ellentmondást: a listát úgy hoztuk létre, hogy ne hivatkozzon a tartalmazott adatok típusára, és így szeretnénk megoldani a lista feldolgozását is. Azonban a listaelemek feldolgozásakor szükségünk van arra, hogy tudjuk, mi a típus, különben nem tudunk vele kezdeni semmit. Ha a konkrét feldolgozási lépést is betesszük a ciklusba, akkor elveszítjük a fenti kódrészlet általános jellegét.

Ennek a problémának a megoldása úgy lehetséges, ha a listaelemek feldolgozását a programrészen kívülre száműzzük. Például úgy, hogy egy függvényt hívunk meg minden listaelem által tartalmazott pointerre:

void lista_mindegyiken(Lista *l, void (*fv)(void *)) {
    ListaElem *futo;

    for (futo = l->eleje->kovetkezo; futo != l->vege; futo = futo->kovetkezo)
        fv(futo->adat); /* amit a hivo ker */
}

Figyeljük meg a függvényre mutató pointer típusát! A paraméterként átvett fv függvény void* mutatót kell átvegyen, hiszen a listakezelő függvény nem ismeri a tartalmazott adatokat. Aki használja a listát, az viszont igen. Például ha a lista sztringeket tartalmaz, akkor egy függvény, amely a fenti bejárás rutinnak átadható, a következő formájú lehet:

void string_kiir(void *string) {
    printf("%s ", (char*) string); // void* → char*
}

Ebben a pointert a lista használója visszaalakítja char* típusúvá. (Ezt megteheti, mivel ő tudja, hogy a listába char*-okat tett.) A bejáró függvény hívása:

lista_mindegyiken(&l, string_kiir);

Ha ez a lista dinamikusan foglalt sztringeket tartalmaz, akkor a lista felszabadítása a következőképpen történhet:

lista_mindegyiken(&l, free);
lista_felszabadit(&l);

Vagyis először felszabadítjuk mindegyik tartalmazott sztringet, utána pedig magukat a listaelemeket. Vegyük észre: a free() függvény kompatibilis a várt függvényre mutató pointerrel, hiszen az is void visszatéréséi értékű, és void* paraméterrel rendelkezik!

4. Rendezés

Írjunk függvényt, amelyik rendezi a listát!

Mi alapján rendezünk, ha azt se tudjuk, mit jelentenek a listaelemek? A rendezést, vagyis az elemek kisebb-nagyobb viszonyát eldöntő függvényt is paraméterként vesszük át, függvényre mutató pointerként. Az a függvény pedig két összehasonlítandó elem memóriacímét veszi majd át, és strcmp()-konvencióval adja meg a választ (negatív: *a<*b, nulla: *a=*b, pozitív: *a>*b). Egy ilyen függvény prototípusa így néz ki:

int osszehasonlit(void const *a, void const *b);

Egy láncolt listát rendezni sok rendező algoritmussal ugyanolyan egyszerű, mint tömböt. Csak az indexeléseket kell kicserélnünk listaműveletekre (ptr=ptr->kovetkezo stb.) Vegyük észre, hogy bár általánosságban egy lista rendezése hatékonyabb úgy, ha az elemeket láncoljuk, de itt most sokkal egyszerűbb, ha nem így teszünk, hanem csak a listaelemekben tárolt void* pointereket cseréljük:

typedef int (*ListaElemHasonlito)(void const *, void const *);

void lista_rendez(Lista *l, ListaElemHasonlito hasonlit) {
    ListaElem *meddig;

    /* ures listat nem rendezzuk. ez azert is kell, mivel
       kulonben a lenti while ciklus feltetele orokke igaz lenne */
    if (l->eleje->kovetkezo == l->vege)
        return;
    /* a meddig fogja tartalmazni az utolso osszehasonlitando
       elemet (amit mar nem kell osszehasonlitani majd,
       hiszen nincs utana kovetkezo) */
    meddig = l->vege->elozo;
    /* amikor a meddig elerte a legelso elemet,
       akkor mar nem lesz mit csinalni */
    while (meddig != l->eleje->kovetkezo) {
        ListaElem *futo;

        for (futo = l->eleje->kovetkezo; futo != meddig; futo = futo->kovetkezo)
            if (hasonlit(futo->adat, futo->kovetkezo->adat)>0) {
                /* egyszerubb az adataikat megcserelni,
                   hiszen az jelen esetben csak egy pointer */
                void *temp = futo->adat;
                futo->adat = futo->kovetkezo->adat;
                futo->kovetkezo->adat = temp;
            }

        /* az utolso elem a helyere kerult; ennyivel kevesebbet
           rendezunk a kovetkezo korben */
        meddig = meddig->elozo;
    }
}

5. Letölthető

A letölthető lista.zip-ben három fájl található. A lista.c és lista.h a generikus lista megvalósítása, a listaproba.c pedig egy rövid példaprogram a függvények használatára. Érdemes megfigyelni, hogy a listakezelő függvényekben, és egyáltalán azokban a fájlokban sehol egy szó nem esik a tárolt típusról! A lista képes lenne bármilyen más típust is tárolni.