Zápočtový úkol z Jazyka C (KMI/JC)

Naprogramujte modul pro práci s řetězci. Tento modul exportuje strukturu pro řetězec.

typedef struct
{
    char *data;
    int   len;
} String;

Dále struktury String_Builder a String_Iterator, jejichž položky studenti navrhnou sami.

String_Builder

String_Builder (dále SB) si lze přestavit jako řetězec, který možno prodlužovat tiskem na jeho konec. (Funkce s ním pracující sami zajišťují, aby měl vždy alokován dostatek paměti.) Pro práci s SB implementujte následující funkce.

//
//  Inicializuje sb. Nealokuje žádnou paměť pro uchovávání obsahu.
//  Po inicialize SB vypadá, jakoby obsahoval prázdný řetězec 
//  (tj. po prevodu na String, viz dále, bude výsledný String prázdný).
// 
void sb_init(String_Builder* sb); 

//
//  Vytvoří SB jako kopii existujícího řetězce
//
String_Builder sb_from_string(String s);

//
//  Neuvolňuje žádnou paměť SB, pouze nastaví jeho položky tak, aby obsah
//  SB odpovídal prázdnému řetězci.
//
void sb_clear(String_Builder* sb);

//
//  Uvolní paměť, kterou sb používá pro uchování řetězce.
//
void sb_destroy(String_Builder* sb);

//
//  Funkce analogická printf ze stdio.h, která tiskne do SB.
//  Ve formátovacím řetězci podporuje následující direktivy a typy
//  - #i     typ int,
//  - #S     řetězec typu String,
//  - #s     řetězec typu char*, 
//  - #c     znak typu char,
//  - #f     float/double (na 6 míst),
//
//  Dále podporuje speciální znaky '\t', '\n', '\"' jako printf.
//
void sb_printf(String_Builder*, String format, ...);

//
// Transformuje SB na String, jehož data jsou nově alokována (tj. SB lze pořád používat).
//
String sb_clone_to_string(String_Builder sb); 

Příklad použití

String_Builder sb;
sb_init(&sb);

sb_printf(&sb, "#i #f #s\ndalsi radek", 10, 3.2, "ahoj svete");
String first = sb_clone_to_string(sb);

sb_clear(&sb);
sb_printf(&sb, "predtim bylo %S", first);
 
String second = sb_clone_to_string(sb);

sb_destroy(&sb);

// 
// předpokládejme, že funkce str_print vytiskne String
// Potom následující
//
str_print(second);
//
// vede k tisku
//
predtim bylo 10 3.200000 ahoj svete
dalsi radek

String

Implementujte následující funkce pro práci s řetězci.

//
// Následující čtyři funkce manuálně ALOKUJÍ paměť pro datové položky řetězců,
// vytváří tak nové kopie.
//

// transformace C řetězce na String a zpět
String  str_clone_from_cstring(char *cstring);
char*   str_clone_to_cstring(String *);

// vytvoření klonu řetězce
String  str_clone(String src);

// spojí řetězce obsažené v poli array (s n prvky) do jednoho, 
// přitom je od sebe oddělí pomocí řetězce sep;
String  str_join(String array[], int n, String sep);

//
// Funkce uvolní vnitřní paměť řetězce a nastaví jej tak,
// aby vypadal jako prázdný řetězec.
//
void str_destroy(String* src);

//
// Následující tři funkce NEALOKUJÍ paměť pro `data` řetězce, ale jejich položka `data`
// ukazuje do této položky jiného řetězce (nebo na C řetězec).
//

// Vytvoří String z C řetězce
String  str_wrap(char* cstring);

// Rozdělí `src` na části oddělené oddělovačem `sep`, a vrátí je jako pole.
// Jejich počet zapíše do n. 
String* str_split(String src, String sep, int *n);

// Zepředu a zezadu src odstraní všechny znaky obsažené v cutset
// a výsledný řetězec vrátí jako výsledek.
String  str_trim(String src, String cutset);

String_Iterator

Slouží pro procházení částí řetězce, které jsou odděleny oddělovačem. Lze předpokládat, že během procházení se procházený řetězec nemění. Jsou-li vedle sebe dva oddělovače, bereme to tak, že je mezi nimi prázdný řetězec. Je-li oddělovač na konci nebo začátku řetězce, bereme to tak, že je za nebo před ním prázdný řetězec. Neobsahuje-li řetězec oddělovač, skládá se pouze z jedné části.

//
// inicializuje iterátor pro řetězec src a oddělovač delimiter.
//
String_Iterator si_create(Strint src, String delimiter);

//
// Vrátí 1 a nastaví slice na další část řetězce. 
// Pokud už byly projity všechny části řetězce, vrátí 0.
// 
int si_next(String_Iterator* s, String *slice);

//
// Restartuje iteraci od začátku
//
void si_restart(String_Iterator* s);

Příklad použití. Následující kód

//
// předpokládejme, že funkce str_println vytiskne String a potom znak nového řádku
//

String foo = str_clone_from_cstring("resim-*-domaci-*-ulohu");
String delim = str_clone_from_cstring("-*-");

String_Iterator si = si_create(foo, delim);
String s;
while( si_next(&si, &s) )
{
    str_println(s);
}

vytiskne následující.

resim
domaci
ulohu

Správa paměti uvnitř modulu.

Implementujte funkci

void set_allocation_functions(void* (*alloc_func)(size_t), void (*free_func)(void *));

S její pomocí se nastaví funkce, které bude modul používat pro alokaci a dealokaci paměti ve funkcích zmíněných v předchozích kapitolách. Přitom můžete předpokládat, že alloc_func je funkce, která se chová jako malloc, a free_func je funkce, která se chová jako free.

Modul bude uchovávat informaci o tom, že došlo k chybě při alokaci. K přístupu k této informaci půjde použít následujících funkcí.

//
//  vrací 1, pokud došlo k chybě při alokaci, jinak vrací 0.
//
int mem_error();

//
//  vrací číslo řádku, na kterém se program nacházel, když k chybě alokace došlo
//
int mem_error_line();

Samotné funkce, ve kterých došlo k chybě alokace, rozumně doběhnou (tj. vrátí rozumnou návratovou hodnotu, uvolní paměť, která má být uvolněna atd.).

Následující funkčnost bude v programu zkompilována, pouze pokud je definováno makro MEM_CHECK.

Modul si bude udržovat informace o alokované a uvolňované paměti. Sledovány jsou pouze alokace a dealokace prováděné s pomocí funkcí předaných modulu pomocí set_allocation_functions. Paměť pro samotné udržování informací o alokacích je alokována a uvolňována pomocí knihovních funkcí malloc a free. Dále modul implementuje následující funkci.

//
// Vytiskne na stdout tabulku o aktuálně alokované paměti (tj. ještě neuvolněné). 
// Pro každou alokaci vytiskne adresu,
// počet alokovaných bajtů a číslo řádku, na kterém byla v kódu alokace provedena.
// 
void mem_report();

Pokud dojde k uvolnění sledované paměti, která již byla v minulosti uvolněna (tj. double free), modul vytiskne informaci o tom, že k tomu došlo (včetně řádku, na kterém k tomu došlo) a skončí pádem pomocí assert.

Modul s testy

Součástí řešení bude i modul s testy, které otestují funkčnost části modulu pro práci s řetězci (tj. vše mimo kapitoly Správa paměti uvnitř modulu.)

Spouštění testů půjde ovládat pomocí funkce.

//
// spustí testy a vypíše vhodné informace [test prosel/neprosel atd.]
//
void run_tests();

Očekává se, že student navrhne rozumné testy, které pokryjí i okrajové případy.

Knihovní funkce

Ze string.h jsou povoleny pouze funkce pro práci s pamětí (začínají mem); z stdio.h pouze printf; z stdlib.h funkce pro alokaci a dealokaci paměti. Dále je povolena assert.h, math.h a stdarg.h.

Jiné knihovní funkce povoleny nejsou.

Struktura projektu

Odevzdejte adresář se soubory strings.h, strings.c (modul pro práci s řetězci), test.c, test.h (modul s testy), volitelně i soubor README (textový soubor s případnými dalšími informacemi). Při nedodržení předchozího je úkol považován za nesplněný.

Moduly musí být z pohledu jazyka C správně strukturovány (např. soukromé funkce a data jsou static).

Úkol budu kontrolovat s pomocí gcc, sanitizeru a programu valgrind.