Warning
This page is located in archive. Go to the latest version of this course pages. Go the latest version of this page.

12. Heterogenní seznam

O polích v C víme, že mohou obsahovat pouze prvky stejného datového typu. Je to hlavně proto, že pokud je známá velikost datového typu, je možné rychle určit adresy prvků a pole efektivně procházet. Takovému poli říkáme obecně homogenní.

V některých jazycích je možné vytvářet pole heterogenní, tedy taková, která obsahují prvky různých datových typů. Například v Pythonu je pole možné reprezentovat datovým typem seznam (list). Funkce print podle datového typu prvku seznamu automaticky určí způsob, jak prvek reprezentovat na standardním výstupu.

$ python3
Python 3.8.10 (default, Sep 28 2021, 16:10:42)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> a = [1, 3.14, "ahoj"]
>>> print(a[0])
1
>>> print(a[1])
3.14
>>> print(a[2])
ahoj
>>>

Šlo by něco takového udělat i v C?

Varianta 1 - union

Protože prvky pole v céčku mohou být pouze jednoho datového typu, je třeba hodnoty jiných datových typů vhodně zapouzdřit. K tomu by se mohl hodit např. datový typ union, který umožňuje reprezentovat blok paměti jako různé datové typy.

typedef union 
{
    int i; 
    float f;
    char s[20];
} record;

Pole datového typu record zřejmě není nijak složité naplnit.

int main()
{
    record list[3];
    list[0].i = 1;
    list[1].f = 3.14;
    strcpy (list[2].s, "ahoj");
 
    return 0;
}

Na rozdíl od inicializace není tisk prvků takového pole úplně jednoduchý. V jazyce C není možné za běhu určovat datový typ proměnných a tak nelze rozhodnout, jak danou proměnnou tisknout na standardní vstup. Můžeme si pomoci třeba tak, že union zapouzdříme do struktury společně s informací o datovém typu, který byl pro konkrétní prvek seznamu uvažován.

Výsledný program by mohl vypadat třeba takto:

#include <stdio.h>
#include <string.h>
 
typedef enum {TYP_INT, TYP_FLOAT, TYP_STRING} datatypes;
 
typedef struct
{
    union {
        int i; 
        float f;
        char s[20];
    } data;
    datatypes type;
} record;
 
void print (record x)
{
    switch (x.type)
    {
        case TYP_INT:
            printf("%i\n", x.data.i);
            return;
        case TYP_FLOAT:
            printf("%f\n", x.data.f);
            return;
        case TYP_STRING:
            printf("%s\n", x.data.s);
    }
}
 
int main()
{
    record list[3];
    list[0].data.i = 1;
    list[0].type = TYP_INT;
    list[1].data.f = 3.14;
    list[1].type = TYP_FLOAT;
    strcpy (list[2].data.s, "ahoj");
    list[2].type = TYP_STRING; 
 
    print(list[0]);
    print(list[1]);
    print(list[2]);
 
    return 0;
}

Varianta 2 - generický datový typ

První varianta řešení heterogenního pole sice funguje a může rozhodně plnit svůj účel, ale má řadu nedostatků. Prvním nedostatkem je společný prostor pro všechny uvažované datové typy, který sice může být výhodný pro menší typy, ale v případě složitějších strukturovaných datových typů je union spíše na obtíž. Druhým problémem je obtížnější škálovatelnost - pro přidání nového datového typu, který bude polem podporován, je třeba změnit jak datový typ record, tak funkci pro tisk položek. Pro malé projekty to nemusí představovat problém, ale pokud by například v projektu bylo stanoveno, že typ record a funkce print se nesmí měnit a podpora nových datových typů vzniká nezávisle, stojíme před obtížně řešitelným problémem.

Jedno z možných řešení spočívá ve využití generického datového typu void * a ukazatelů na funkce. Řešení sice na první pohled vypadá složitěji, přináší však výhodu oddělení implementace jednotlivých datových typů, včetně rozdělení do hlavičkových souborů.

Pro účely uložení do pole je definován datový typ record, který kromě datové složky obsahuje i dva ukazatele na funkce. Tyto ukazatele, zajistí rozhraní pro volání funkcí určených pro tisk a dealokaci podporovaných datových typů.

#ifndef __LIST_H
#define __LIST_H
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
typedef struct {
    void * data;
    void (* print)(void *);
    void (* delete)(void *);
} record;
 
void print (void * x);
void delete (void * x);
 
#endif

V implementaci funkcí je vidět, že v těle funkce se volá funkce implementovaná novým datovým typem.

#include "list.h"
 
void print (void * x) {
    record * tmp = (record *)x;
    tmp->print(tmp);
}
 
void delete (void* x) {
    record * tmp = (record *)x;
    tmp->delete(tmp);
}

Implementace nového datového typu může pak vypadat třeba takto:

#ifndef __LIST_INT_H
#define __LIST_INT_H
 
#include "list.h"
 
typedef struct {
    int data;
} typ_int;
 
void print_int (void * x);
void delete_int (void * x);
record * new_int (int data);
 
#endif

#include "list_int.h"
 
void print_int (void * x) {
    record * tmp1 = (record *)x;
    typ_int * tmp2 = (typ_int *)(tmp1->data);
    printf("%i\n", tmp2->data);
}
 
void delete_int (void * x) {
    record * tmp = (record *)x;
    free(tmp->data);
    free (tmp);
}
 
record * new_int (int data) {
    typ_int * x = malloc(sizeof(typ_int));
    x->data = data;
 
    record * tmp = malloc(sizeof(record));
    // tmp->data = x;
    // tmp->print = print_int;
    // tmp->delete = delete_int;
    // v C99 lze nahradit zapisem s pomoci compound literal
    *tmp = (record){.data = x, .print = print_int, .delete = delete_int};
    return tmp;
}

Jiný datový typ:

#ifndef __LIST_FLOAT_H
#define __LIST_FLOAT_H
 
#include "list.h"
 
typedef struct {
    float data;
} typ_float;
 
void print_float (void * x);
void delete_float (void * x);
record * new_float (float data);
 
#endif

#include "list_float.h"
 
void print_float (void * x) {
    record * tmp1 = (record *)x;
    typ_float * tmp2 = (typ_float *)(tmp1->data);
    printf("%.2f\n", tmp2->data);
}
 
void delete_float (void * x) {
    record * tmp = (record *)x;
    free(tmp->data);
    free (tmp);
}
 
record * new_float (float data) {
    typ_float * x = malloc(sizeof(typ_float));
    x->data = data;
 
    record * tmp = malloc(sizeof(record));
    *tmp = (record){.data = x, .print = print_float, .delete = delete_float};
    return tmp;
}

Hlavní program pak může vypadat třeba takto:

#include "list.h"
#include "list_int.h"
#include "list_float.h"
 
int main ()
{
    record * list[3];
 
    list[0] = new_int (1);
    list[1] = new_float (3.14);
 
    print(list[0]);
    print(list[1]);
 
    delete(list[0]);
    delete(list[1]);
 
    return 0;
}

Celý projekt včetně Makefile: list.zip

courses/b0b99prpa/solutions/lab12.txt · Last modified: 2021/12/08 12:22 by viteks