Obsah
1. Svět jednohlavičkových knihoven pro jazyky C a C++
2. Výhody a nevýhody jednohlavičkových knihoven
3. Ukázka typické jednohlavičkové knihovny: stb_image_write.h
4. Korektní způsob použití knihovny stb_image_write.h
5. Prototypy funkcí vs definice funkcí
7. Oddělený překlad jednohlavičkové knihovny?
8. Rozdělení překladu knihovny stb_image_write.h od zbytku aplikace
9. Kombinace většího množství jednohlavičkových knihoven
10. Vykreslení Perlinova šumu s uložením výsledného obrázku na disk
11. Jednohlavičkové knihovny s implementací (datových) kontejnerů
12. Vektory s dynamicky měnitelnou kapacitou
13. Demonstrační příklady: manipulace s vektory
15. Demonstrační příklady: použití slovníků
16. Jednohlavičková knihovna pro vyhodnocování výrazů
17. Příklady realizace vyhodnocování výrazů bez proměnných i s proměnnými
18. Příloha: Makefile soubor pro překlad všech demonstračních příkladů
19. Repositář s demonstračními příklady
1. Svět jednohlavičkových knihoven pro jazyky C a C++
V dnešním článku se seznámíme s konceptem takzvaných jednohlavičkových knihoven (header-only library) v ekosystému programovacích jazyků C a C++. Jak již název napovídá, jedná se o knihovny, které jsou celé tvořeny pouze jediným hlavičkovým souborem, jenž obsahuje definice maker, definice funkcí a současně i jejich deklarace. V závislosti na konkrétní knihovně je možné tyto hlavičkové soubory přímo vložit do zdrojových kódů příkazem preprocesoru #include, ovšem některé z těchto knihoven umožňují i oddělený překlad (knihovna se v takovém případě přeloží do objektového souboru, který se následně může slinkovat s ostatními objektovými soubory). Jednohlavičkové soubory jsou poměrně populární, zejména kvůli snadnosti jejich instalace a zařazení do vyvíjeného projektu. Na druhou stranu však zejména v případě programovacího jazyka C můžeme narazit na určitá omezení, která jsou daná jak typovým systémem tohoto jazyka, tak i neexistencí jmenných prostorů (namespace).
2. Výhody a nevýhody jednohlavičkových knihoven
Jak jsem se již zmínil v úvodní kapitole, jsou jednohlavičkové knihovny ve světě programovacích jazyků C a C++ poměrně populární. Přispívá k tomu i fakt, že takové knihovny lze velmi snadno začlenit do vytvářeného projektu a navíc nejsou vývojáři nuceni používat sofistikované (a mnohdy zbytečně komplikované) správce projektů a balíčků; vystačí si s nástroji make, git nebo i jen s nástrojem wget. Ostatně v této oblasti neexistuje pro jazyky C ani C++ jednotný a uznávaný standard, takže by se mohlo s nadsázkou říci, že jakékoli řešení je lepší než žádné řešení.
Ovšem kromě předností má toto řešení i několik více či méně závažných záporů. Často se zmiňuje fakt, že modifikace provedené v kódu vkládané jednohlavičkové knihovny vyžaduje přeložení všech dalších zdrojových kódů, které tuto knihovnu používají. To je však většinou nutné provést v každém případě, tedy i kdyby se používal oddělený překlad (vyžaduje to koncept hlavičkových souborů). V případě, že se jednohlavičková knihovna vkládá přímo do zdrojového kódu i s těly funkcí, bude pochopitelně delší i čas překladu tohoto zdrojového kódu. Tento problém mnohé knihovny řeší tak, že umožňují oddělený překlad. A poslední problém (který lze též do jisté míry řešit odděleným překladem) spočívá v tom, že jazyk C nepodporuje změnu jmenných prostorů, takže teoreticky může nastat situace, kdy knihovna i kód použijí stejný symbol v různých kontextech. Jednohlavičkové knihovny musí všechny své symboly začínat stejným prefixem a naopak ostatní kód by takový prefix používat neměl.
3. Ukázka typické jednohlavičkové knihovny: stb_image_write.h
První jednohlavičkovou knihovnou, se kterou se v dnešním článku alespoň ve stručnosti seznámíme, je knihovna nazvaná stb_image_write.h. Tato knihovna obsahuje definice funkcí sloužících pro ukládání rastrových obrázků do formátů PNG, BMP, TGA, JPG a HDR. Jedná se o tyto funkce:
int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes); int stbi_write_bmp(char const *filename, int w, int h, int comp, const void *data); int stbi_write_tga(char const *filename, int w, int h, int comp, const void *data); int stbi_write_jpg(char const *filename, int w, int h, int comp, const void *data, int quality); int stbi_write_hdr(char const *filename, int w, int h, int comp, const float *data);
Dále tato knihovna obsahuje zobecněné funkce, které lze využít například při posílání rastrového obrázku přes sockety atd. Tyto funkce neprovádí přímé manipulace se soubory, ale provedou uložení rastrového obrázku do zvoleného formátu v operační paměti a přitom zavolají nastavenou callback funkci:
int stbi_write_png_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data, int stride_in_bytes); int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); int stbi_write_tga_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const float *data); int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality);
Callback funkce, která bude automaticky zavolána, musí mít tuto hlavičku:
void stbi_write_func(void *context, void *data, int size);
Navíc je možné přes další funkce ovlivnit například úroveň komprimace u formátu PNG atd. (výchozí úroveň je nízká, takže lze PNG dále optimalizovat, například nástrojem pngcrush atd.).
Knihovnu stb_image_write.h získáme snadno:
$ wget https://raw.githubusercontent.com/nothings/stb/refs/heads/master/stb_image_write.h
Saving 'stb_image_write.h'
HTTP response 200 [https://raw.githubusercontent.com/nothings/stb/refs/heads/master/stb_image_write.h]
stb_image_write.h 100% [=====================================================================================>] 19.88K --.-KB/s
[Files: 1 Bytes: 19.88K [68.56KB/s] Redirects: 0 Todo: 0 Errors: 0 ]
4. Korektní způsob použití knihovny stb_image_write.h
Použití knihovny stb_image_write.h je poměrně snadné. Ukážeme si to na několika jednoduchých a krátkých demonstračních příkladech. V prvním příkladu provedeme uložení rastrového obrázku o velikosti 4×4 pixely do formátu PNG. Jednotlivé pixely jsou reprezentovány 32bitovou hodnotou RGBA (nejvyšší bajt nese informaci o průhlednosti, další tři bajty pak barvové složky RGB, ovšem v pořadí modrá-zelená-červená). To tedy znamená, že obrázek 4×4 pixely s černým okrajem, uvnitř něhož je uložen červený, zelený, žlutý a modrý pixel, lze reprezentovat následovně:
uint32_t image[] = {
0xff000000, 0xff000000, 0xff000000, 0xff000000,
0xff000000, 0xff0000ff, 0xff00ff00, 0xff000000,
0xff000000, 0xff00ffff, 0xffff0000, 0xff000000,
0xff000000, 0xff000000, 0xff000000, 0xff000000,
};
Knihovnu stb_image_write.h vložíme do zdrojového kódu takto (včetně definice makrosymbolu):
#define STB_IMAGE_WRITE_IMPLEMENTATION #include "stb_image_write.h"
Uložení rastrového obrázku do souboru typu PNG je snadné:
stbi_write_png("test.png", šířka, výška, bajtů_na_pixel, rastrový_obrázek, stride_délka_řádku);
Úplný zdrojový kód dnešního prvního demonstračního příkladu může vypadat následovně:
#include <stdint.h>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#define WIDTH 4
#define HEIGHT 4
#define RGBA 4
uint32_t image[] = {
0xff000000, 0xff000000, 0xff000000, 0xff000000,
0xff000000, 0xff0000ff, 0xff00ff00, 0xff000000,
0xff000000, 0xff00ffff, 0xffff0000, 0xff000000,
0xff000000, 0xff000000, 0xff000000, 0xff000000,
};
int main(void) {
stbi_write_png("test.png", WIDTH, HEIGHT, RGBA, image, 4*sizeof(uint32_t));
return 0;
}
Překlad zdrojového kódu tohoto demonstračního příkladu je snadný:
$ gcc -Wall -pedantic simple_image_2.c -o simple_image_2
Po překladu tento příklad spustíme:
$ ./simple_image_2
V pracovním adresáři by měl po spuštění vzniknout rastrový obrázek uložený do souboru s názvem test.png:
$ ls -1 simple_image_1.c simple_image_2 stb_image_write.h test.png
Ověříme si, zda se skutečně jedná o rastrový obrázek:
$ file test.png test.png: PNG image data, 4 x 4, 8-bit/color RGBA, non-interlaced
Výsledek po zvětšení dvacetinásobném zvětšení získaného rastrového obrázku:
5. Prototypy funkcí vs definice funkcí
V předchozím demonstračním příkladu jsme před importem jednohlavičkové knihovny definovali i symbol STB_IMAGE_WRITE_IMPLEMENTATION:
#define STB_IMAGE_WRITE_IMPLEMENTATION #include "stb_image_write.h"
Tento symbol zajistí, že se při importu vloží do zdrojového kódu nejenom hlavičky všech funkcí, ale i jejich implementace.
Otestujme si, co se stane v případě, že se pouze pokusíme o vložení hlavičkového souboru bez definice STB_IMAGE_WRITE_IMPLEMENTATION:
#include "stb_image_write.h"
Pokud se nyní pokusíme o překlad takto upraveného příkladu, dojde k chybě ve fázi slinkování:
$ gcc -Wall -pedantic simple_image_1.c /usr/bin/ld: /tmp/ccwoMWRf.o: in function `main': simple_image_1.c:(.text+0x25): undefined reference to `stbi_write_png' collect2: error: ld returned 1 exit status
Upravený zdroj demonstračního příkladu bude vypadat následovně:
#include <stdint.h>
#include "stb_image_write.h"
#define WIDTH 4
#define HEIGHT 4
#define RGBA 4
uint32_t image[] = {
0xff000000, 0xff000000, 0xff000000, 0xff000000,
0xff000000, 0xff0000ff, 0xff00ff00, 0xff000000,
0xff000000, 0xff00ffff, 0xffff0000, 0xff000000,
0xff000000, 0xff000000, 0xff000000, 0xff000000,
};
int main(void) {
stbi_write_png("test.png", WIDTH, HEIGHT, RGBA, image, 4*sizeof(uint32_t));
return 0;
}
6. Kontroly chybových stavů
Většina jednohlavičkových knihoven je naprogramována zkušenými vývojáři. I z tohoto důvodu je většinou vyřešen i problém hlášení chybových stavů. V jazyce C neexistuje koncept klasických výjimek, takže se většinou chybový stav oznamuje v návratové hodnotě funkce (nebo nastavením nějaké globální proměnné). Příkladem může být i funkce stbi_write_png, která vrací jedničku v případě, že funkce proběhla v pořádku a nulu v opačném případě:
#include <stdio.h>
#include <stdint.h>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#define WIDTH 4
#define HEIGHT 4
#define RGBA 4
uint32_t image[] = {
0xff000000, 0xff000000, 0xff000000, 0xff000000,
0xff000000, 0xff0000ff, 0xff00ff00, 0xff000000,
0xff000000, 0xff00ffff, 0xffff0000, 0xff000000,
0xff000000, 0xff000000, 0xff000000, 0xff000000,
};
int main(void) {
int status = stbi_write_png("test.png", WIDTH, HEIGHT, RGBA, image, 4*sizeof(uint32_t));
printf("Success? %s\n", status ? "yes" : "no");
return 0;
}
Tento příklad přeložíme běžným způsobem:
$ gcc -Wall -pedantic simple_image_3.c -o simple_image_3
Po spuštění se nejenom vytvoří výsledný obrázek, ale navíc se i zobrazí informace o tom, jestli byla tato operace úspěšná či nikoli:
$ ./simple_image_3 Success?
7. Oddělený překlad jednohlavičkové knihovny?
Jednou z nevýhod při zápisu celého kódu knihovny v jediném hlavičkovém souboru je fakt, že je (zdánlivě) nutné překlad neustále opakovat při každém vložení kódu knihovny konstrukcí #include. Mnohé jednohlavičkové knihovny tento problém ovšem řeší, a to tak, že umožňují oddělený překlad knihovny. Konkrétně v případě knihovny stb_image_write.h jsme viděli, že pro vložení definic všech funkcí je nutné nejdříve definovat symbol preprocesoru STB_IMAGE_WRITE_IMPLEMENTATION. To tedy znamená, že by bylo možné provést oddělený překlad pouze jednohlavičkové knihovny tak, jak to zhruba odpovídá následujícímu příkazu:
$ gcc -D STB_IMAGE_WRITE_IMPLEMENTATION -c stb_image_write.h
Ovšem zde se projevuje jedna z vlastností překladače GCC – pokud se překládá hlavičkový soubor (to překladač pozná podle koncovky), nebude výsledkem objektový kód (.o), ale soubor s koncovkou .gch, který obsahuje předkompilované hlavičky (což ovšem v tomto případě vůbec nepotřebujeme):
$ ls -1 simple_image_1.c stb_image_write.h stb_image_write.h.gch
I tento problém je však možné snadno vyřešit, což si ukážeme v další kapitole.
8. Rozdělení překladu knihovny stb_image_write.h od zbytku aplikace
Aby překladač GCC přeložil hlavičkový soubor stejným způsobem jako běžný zdrojový soubor napsaný v jazyku C, musíme překladači předat přepínač -x c. Překlad knihovny stb_image_write do objektového kódu obsahujícího všechny funkce provedeme následovně:
$ gcc -D STB_IMAGE_WRITE_IMPLEMENTATION -x c -c stb_image_write.h
Nyní by měl pracovní adresář obsahovat (minimálně) tyto soubory:
$ ls -1 simple_image_1.c stb_image_write.h stb_image_write.o
Následně můžeme, zcela odděleně, přeložit zdrojový kód simple_image1.c, který jsme si ukázali ve čtvrté kapitole:
$ gcc -c simple_image_1.c
Obsah pracovního adresáře:
$ ls -1 simple_image_1.c simple_image_1.o stb_image_write.h stb_image_write.o
Na závěr oba objektové soubory slinkujeme:
$ gcc simple_image_1.o stb_image_write.o -o simple_image_1
Obsah pracovního adresáře po poslední operaci:
$ ls -1 simple_image_1 simple_image_1.c simple_image_1.o stb_image_write.h stb_image_write.o
Nyní již můžeme právě vytvořený spustitelný soubor skutečně spustit:
$ ./simple_image_1
Výsledkem bude nový rastrový obrázek:
$ file test.png test.png: PNG image data, 4 x 4, 8-bit/color RGBA, non-interlaced
$ nm -g stb_image_write.o | grep " T " 0000000000000000 T stbi_flip_vertically_on_write 0000000000000ae0 T stbi_write_bmp 0000000000000a70 T stbi_write_bmp_to_func 0000000000001938 T stbi_write_hdr 00000000000018c8 T stbi_write_hdr_to_func 00000000000054c8 T stbi_write_jpg 0000000000005451 T stbi_write_jpg_to_func 00000000000039a1 T stbi_write_png 0000000000003a5b T stbi_write_png_to_func 0000000000003302 T stbi_write_png_to_mem 0000000000000f90 T stbi_write_tga 0000000000000f20 T stbi_write_tga_to_func 0000000000001c46 T stbi_zlib_compress
9. Kombinace většího množství jednohlavičkových knihoven
V praxi se dříve či později setkáme s požadavkem využití většího množství jednohlavičkových knihoven v jednom projektu. To, zda bude skutečně možné zkombinovat více knihoven, do značné míry závisí na tom, jestli byli původní autoři důslední v pojmenovávání identifikátorů v jednotlivých knihovnách. To se týká maker, datových typů, funkcí a teoreticky i globálních proměnných. Pokud obsahují všechny tyto identifikátory nějaký unikátní prefix, měla by být kombinace většího množství knihoven prakticky proveditelná, což si ostatně ukážeme v navazující kapitole.
10. Vykreslení Perlinova šumu s uložením výsledného obrázku na disk
Kombinaci více jednohlavičkových knihoven si ukážeme na demonstračním příkladu, který po svém spuštění vykreslí známý Perlinův šum a uloží výsledek do rastrového obrázku. Pro výpočet Perlinova šumu použijeme knihovnu stb_perlin.h, jejíž zdrojový kód (v hlavičkovém souboru) získáme snadno:
$ wget https://raw.githubusercontent.com/nothings/stb/refs/heads/master/stb_perlin.h
Obě knihovny můžeme do projektu vložit v libovolném pořadí:
#define STB_IMAGE_WRITE_IMPLEMENTATION #include "stb_image_write.h" #define STB_PERLIN_IMPLEMENTATION #include "stb_perlin.h"
Celý výpočet i s uložením výsledného souboru vypadá následovně:
#include <stdio.h>
#include <stdint.h>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#define STB_PERLIN_IMPLEMENTATION
#include "stb_perlin.h"
#define WIDTH 512
#define HEIGHT 512
#define RGBA 4
uint32_t image[HEIGHT][WIDTH] = {0};
int main(void) {
int x, y;
for (y=0; y<HEIGHT; y++) {
for (x=0; x<WIDTH; x++) {
int i = (int)(250.0*stb_perlin_turbulence_noise3(x/50., y/50., 0, 2.1, 0.5, 6));
if (i>255) i=255;
uint32_t color = (0xff << 24) + (i << 16) + (i << 8) + i;
image[y][x] = color;
}
}
stbi_write_png("perlin.png", WIDTH, HEIGHT, RGBA, image, WIDTH*sizeof(uint32_t));
return 0;
}
Po překladu a spuštění tohoto demonstračního příkladu bychom měli získat rastrový obrázek perlin.png, který bude obsahovat Perlinův šum reprezentovaný ve stupních šedi:
Ukažme si ještě jeden příklad generování Perlinova šumu. Nyní je šum počítán odděleně pro každou barvovou složku:
int r = (int)(300.0*stb_perlin_turbulence_noise3(x/200., y/200., 0, 2.2, 0.5, 6)); int g = (int)(300.0*stb_perlin_turbulence_noise3(x/200., y/200., 0, 1.9, 0.5, 7)); int b = (int)(300.0*stb_perlin_turbulence_noise3(x/200., y/200., 0, 2.1, 0.5, 8));
Upravený zdrojový kód demonstračního příkladu:
#include <stdio.h>
#include <stdint.h>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#define STB_PERLIN_IMPLEMENTATION
#include "stb_perlin.h"
#define WIDTH 512
#define HEIGHT 512
#define RGBA 4
uint32_t image[HEIGHT][WIDTH] = {0};
int main(void) {
int x, y;
for (y=0; y<HEIGHT; y++) {
for (x=0; x<WIDTH; x++) {
int r = (int)(300.0*stb_perlin_turbulence_noise3(x/200., y/200., 0, 2.2, 0.5, 6));
if (r>255) r=255;
int g = (int)(300.0*stb_perlin_turbulence_noise3(x/200., y/200., 0, 1.9, 0.5, 7));
if (g>255) g=255;
int b = (int)(300.0*stb_perlin_turbulence_noise3(x/200., y/200., 0, 2.1, 0.5, 8));
if (b>255) b=255;
uint32_t color = (0xff << 24) + (r << 16) + (g << 8) + b;
image[y][x] = color;
}
}
stbi_write_png("perlin.png", WIDTH, HEIGHT, RGBA, image, WIDTH*sizeof(uint32_t));
return 0;
}
Výsledek (podle očekávání) je zcela odlišný od předchozího příkladu:
11. Jednohlavičkové knihovny s implementací (datových) kontejnerů
Vzhledem k tomu, že standardní knihovna programovacího jazyka C prakticky neobsahuje žádnou implementaci datových kontejnerů, nebude pravděpodobně velkým překvapením, že existuje poměrně velké množství jednohlavičkových knihoven s implementací takových kontejnerů. Setkáme se s implementacemi dynamických polí (ať již mají jakýkoli název), map (slovníků), stromových datových struktur (tree), ale například i jednosměrných a obousměrných front (queue, deque) nebo zásobníků (stack) a cyklických front (ring buffer). V navazujících kapitolách si ukážeme dva zástupce těchto knihoven. První z nich implementuje dynamická pole (ovšem nazývá je vektory), druhá knihovna pak obsahuje implementaci map (slovníků).
12. Vektory s dynamicky měnitelnou kapacitou
První knihovnou, která implementuje nějaký datový kontejner, je knihovna nazvaná vec.h. V této knihovně nalezneme implementaci vektorů, což je ovšem poněkud zavádějící označení. Jedná se totiž o vektory v takové podobě, v jaké je můžeme znát z programovacího jazyka C++: vektory jsou ve standardní knihovně C++ realizovány formou polí s rychlým přístupem k prvkům v konstantním čase O(1), ale s možností přidávání a odstraňování prvků (vektory tedy dynamicky mění svoji velikost). A stejným způsobem je navržena i knihovna vec.h.
Hlavičkový soubor s úplnou implementací všech potřebných maker a pomocných funkcí pro práci s vektory získáme snadno:
$ wget https://raw.githubusercontent.com/OguzhanUmutlu/vec.h/refs/heads/main/vec.h
Nejdříve je nutné vytvořit nový typ představující vektor určitých hodnot:
vec_define(type, NAME)
Makrosystémem jazyka C jsou pro tento nový vektor vytvořeny všechny potřebné funkce, například:
NAME_alloc() NAME_push() NAME_pop() NAME_at() NAME_set() NAME_empty() NAME_reverse() NAME_shrink()
13. Demonstrační příklady: manipulace s vektory
V prvním demonstračním příkladu, který využívá vektory (dynamické pole) nadeklarujeme datové typy Name (prvek vektoru) a Names (vlastní vektor). Následně se do vektoru vygenerovanou funkcí Names_push vloží trojice prvků, získáme prvek na pozici 1 a poté obsah vektoru smažeme:
#include <stdio.h>
#include "vec.h"
typedef char* Name;
vec_define(Name, Names);
vec_define_free_simple(Name, Names);
vec_define_print(Name, Names, printf("%s", a));
int main() {
Names names;
Names_init(&names);
Name p1 = {"Alice"};
Name p2 = {"Bob"};
Name p3 = {"Charlie"};
Names_push(&names, p1);
Names_push(&names, p2);
Names_push(&names, p3);
Names_print(names);
printf("\n\n");
Name p = Names_at(names, 1);
printf("Element at index 1: \"%s\"\n", p);
Names_clear(&names);
return 0;
}
Ve druhém demonstračním příkladu je ukázáno, jakým způsobem je možné iterovat (procházet) přes všechny prvky vektoru. Používá se zde vygenerovaná funkce Names_at, která má konstantní časovou složitost:
#include <stdio.h>
#include "vec.h"
typedef char* Name;
vec_define(Name, Names);
vec_define_free_simple(Name, Names);
vec_define_print(Name, Names, printf("%s", a));
int main() {
Names names;
Names_init(&names);
printf("Initial size=%d and capacity=%d\n", names.size, names.capacity);
Name p1 = {"Alice"};
Name p2 = {"Bob"};
Name p3 = {"Charlie"};
Names_push(&names, p1);
Names_push(&names, p2);
Names_push(&names, p3);
printf("Actual size=%d and capacity=%d\n", names.size, names.capacity);
Names_print(names);
printf("\n\n");
int i;
for (i=0; i<names.size; i++) {
Name p = Names_at(names, i);
printf("Element at index %d: \"%s\"\n", i, p);
}
Names_clear(&names);
return 0;
}
A konečně příklad třetí je inspirován přímo dokumentací ke knihovně vec.h. Pracuje se zde s typem vektoru nazvaným People, který obsahuje prvky typu Person, což jsou z pohledu programovacího jazyka C struktury:
#include <stdio.h>
#include "vec.h"
typedef struct Person {
char name[50];
int age;
} Person;
vec_define(Person, People);
vec_define_free_simple(Person, People);
vec_define_print(Person, People, printf("%s:%d", a.name, a.age));
int main() {
People people;
People_init(&people);
Person p1 = {"Alice", 30};
Person p2 = {"Bob", 25};
Person p3 = {"Charlie", 35};
People_push(&people, p1);
People_push(&people, p2);
People_push(&people, p3);
People_print(people);
Person p = People_at(people, 1);
printf("Element at index 1: { name: \"%s\", age: %d }\n", p.name, p.age);
Person updated = {"Bob Jr.", 26};
People_set(&people, 1, updated);
printf("Modified element at index 1: { name: \"%s\", age: %d }\n",
People_at(people, 1).name, People_at(people, 1).age);
Person popped = People_pop(&people);
printf("Popped: { name: \"%s\", age: %d }\n", popped.name, popped.age);
People_clear(&people);
return 0;
}
14. Mapy (slovníky)
Dynamické pole, jehož jednu implementaci jsme si popsali v předchozích dvou kapitolách, je pochopitelně velmi často využívaným datovým kontejnerem. Ovšem prakticky stejně často se můžeme setkat i s požadavkem na použití mapy (map) resp. slovníku (dictionary). Jednohlavičková implementace tohoto typu kontejneru pochopitelně existuje, a to dokonce v několika variantách. V dnešním článku se ve stručnosti seznámíme s možnostmi poskytovanými knihovnou nazvanou hashmap.h. Tu je možné získat následovně:
$ wget https://raw.githubusercontent.com/sheredom/hashmap.h/refs/heads/master/hashmap.h
Tato knihovna poskytuje všechny potřebné operace pro práci se slovníky: vložení prvku (put), získání (přečtení) prvku (get), smazání prvku (remove), test na existenci prvku (nepřímo) a taktéž procházení prvky uloženými ve slovníku (ovšem v odlišném pořadí, než v jakém byly prvky do slovníku vloženy).
15. Demonstrační příklady: použití slovníků
V této kapitole si ukážeme několik příkladů použití slovníků. V příkladu prvním jsou do slovníku přidány dva prvky pod klíči „root“ a „user“. Povšimněte si, že se předávají jak samotné klíče (řetězce), tak i délky těchto klíčů. Prvky je nutné předávat ukazatelem, což mj. znamená, že následující příklad pracuje korektně jen díky tomu, že se slovník používá v jediné funkci (ukládané hodnoty jsou umístěny na zásobníkový rámec!):
#include <stdio.h>
#include "hashmap.h"
int main(void) {
struct hashmap_s hashmap;
hashmap_create(10, &hashmap);
int x = 0;
int y = 1000;
hashmap_put(&hashmap, "root", strlen("root"), &x);
hashmap_put(&hashmap, "user", strlen("user"), &y);
int *id= hashmap_get(&hashmap, "root", strlen("root"));
if (id != NULL)
printf("%d\n", *id);
else
printf("not found\n");
id = hashmap_get(&hashmap, "user", strlen("user"));
if (id != NULL)
printf("%d\n", *id);
else
printf("not found\n");
id = hashmap_get(&hashmap, "other", strlen("other"));
if (id != NULL)
printf("%d\n", *id);
else
printf("not found\n");
hashmap_destroy(&hashmap);
}
Výsledkem budou hodnoty dvou prvků a informace o tom, že třetí prvek nebyl nalezen:
0 1000 not found
Zatímco v prvním demonstračním příkladu byly použity operace get a put, ve druhém příkladu je sledována i kapacita slovníku a navíc se používá operace remove. Ta však nemění kapacitu – reorganizaci slovníku je nutné provést explicitně (to má své kladné i záporné stránky):
#include <stdio.h>
#include "hashmap.h"
int main(void) {
struct hashmap_s hashmap;
hashmap_create(2, &hashmap);
int x = 0;
int y = 1000;
printf("Capacity: %d\n", hashmap_capacity(&hashmap));
hashmap_put(&hashmap, "root", strlen("root"), &x);
hashmap_put(&hashmap, "user", strlen("user"), &y);
hashmap_put(&hashmap, "foo", strlen("foo"), &y);
hashmap_put(&hashmap, "bar", strlen("bar"), &y);
hashmap_put(&hashmap, "baz", strlen("baz"), &y);
printf("Capacity: %d\n", hashmap_capacity(&hashmap));
int *id= hashmap_get(&hashmap, "root", strlen("root"));
if (id != NULL)
printf("%d\n", *id);
else
printf("not found\n");
id = hashmap_get(&hashmap, "user", strlen("user"));
if (id != NULL)
printf("%d\n", *id);
else
printf("not found\n");
id = hashmap_get(&hashmap, "other", strlen("other"));
if (id != NULL)
printf("%d\n", *id);
else
printf("not found\n");
hashmap_remove(&hashmap, "foo", strlen("foo"));
hashmap_remove(&hashmap, "bar", strlen("bar"));
hashmap_remove(&hashmap, "baz", strlen("baz"));
hashmap_remove(&hashmap, "root", strlen("root"));
hashmap_remove(&hashmap, "user", strlen("user"));
printf("Capacity: %d\n", hashmap_capacity(&hashmap));
hashmap_destroy(&hashmap);
}
Povšimněte si, že kapacita slovníku skutečně stále roste:
Capacity: 2 Capacity: 8 0 1000 not found Capacity: 8
V příkladu třetím se slovníkem prochází (v pseudonáhodném pořadí). Pro každý nalezený prvek se volá callback funkce iterate, která vrací hodnotu 1 znamenající „pokračuj dále“ (návratovou hodnotou lze řídit, kdy se má průchod zastavit):
#include <stdio.h>
#include "hashmap.h"
static int iterate(void* const context, void* const value) {
int *x = value;
printf("%d\n", *x);
return 1;
}
int main(void) {
struct hashmap_s hashmap;
hashmap_create(10, &hashmap);
int x = 0;
int y = 1000;
hashmap_put(&hashmap, "root", strlen("root"), &x);
hashmap_put(&hashmap, "user", strlen("user"), &y);
int a = 1, b = 2, c = 3;
hashmap_put(&hashmap, "foo", strlen("foo"), &a);
hashmap_put(&hashmap, "bar", strlen("bar"), &b);
hashmap_put(&hashmap, "baz", strlen("baz"), &c);
int* value;
hashmap_iterate(&hashmap, iterate, &value);
hashmap_destroy(&hashmap);
}
Hodnoty nalezených prvků jsou vráceny a vytištěny v pseudonáhodném pořadí:
1 1000 3 0 2
16. Jednohlavičková knihovna pro vyhodnocování výrazů
Poslední jednohlavičkovou knihovnou, se kterou se v dnešním článku setkáme, je knihovna, která se jmenuje ceval.h. Tato knihovna dokáže vyhodnotit výrazy uložené do řetězce. Výrazy, přesněji řečeno infixové výrazy, mohou obsahovat základní aritmetické operátory, logické operátory, volání některých funkcí (goniometrické atd.) a pochopitelně i závorky ovlivňující prioritu prováděných operací. Ve výrazech se sice očekávají konstantní hodnoty, což je velké omezení, ovšem ukážeme si jeden trik, který nám umožní do výrazů předat i hodnoty proměnných a navíc i „externě“ řídit, které hodnoty lze předat a které nikoli. Samotné vyhodnocení (evaluation) výrazů je tedy bezpečnou operací.
Hlavičkový soubor s touto knihovnou získáme snadno:
$ wget https://raw.githubusercontent.com/erstan/ceval-single-header/refs/heads/e_t/ceval.hSaving 'ceval.h'
HTTP response 200 [https://raw.githubusercontent.com/erstan/ceval-single-header/refs/heads/e_t/ceval.h]
ceval.h 100% [=====================================================================================>] 7.33K --.-KB/s
[Files: 1 Bytes: 7.33K [11.64KB/s] Redirects: 0 Todo: 0 Errors: 0 ]
17. Příklady realizace vyhodnocování výrazů bez proměnných i s proměnnými
V prvním příkladu se vyhodnocuje výraz 1+2*3:
#include <stdio.h>
#include "ceval.h"
int main(void) {
static char expression[] = "1+2*3";
double result = ceval_result(expression);
printf("Result=%f\n", result);
return 0;
}
Tento příklad ve skutečnosti nebude přeložen, protože knihovna ceval.h vyžaduje funkce ze standardní knihovny stdlib.h, ovšem nijak se nesnaží načítat hlavičky příslušných funkcí. To je obecně nedostatek a jednohlavičkové knihovny by se měly chovat zcela autonomně.
Zdrojový kód příkladu opravíme:
#include <stdio.h>
#include <stdlib.h>
#include "ceval.h"
int main(void) {
static char expression[] = "1+2*3";
double result = ceval_result(expression);
printf("Result=%f\n", result);
return 0;
}
Nyní již dostaneme očekávaný výsledek:
Result=7.000000
Můžeme se pochopitelně pokusit o vyčíslení složitějšího výrazu:
#include <stdio.h>
#include <stdlib.h>
#include "ceval.h"
int main(void) {
static char expression[] = "(3-20)*(14+20)+20*(14-3+20)";
double result = ceval_result(expression);
printf("Result=%f\n", result);
return 0;
}
Výsledkem je v tomto případě (kupodivu) hodnota 42:
Result=42.000000
Ukažme si trik, kterým je možné do výrazu předat hodnoty nějakých proměnných (resp. obecně céčkovských výrazů). Výraz použitý v dalším příkladu obsahuje názvy parametrů s procenty, takže ho můžeme transformovat funkcí snprintf s tím, že ve výsledném výrazu budu za parametry dosazeny skutečné hodnoty:
#include <stdio.h>
#include <stdlib.h>
#include "ceval.h"
int main(void) {
double a = 3;
double b = 20;
double c = 14;
static char expression[] = "(%f-%f)*(%f+%f)+%f*(%f-%f+%f)";
char buffer[200];
snprintf(buffer, sizeof(buffer), expression, a, b, c, b, b, c, a, b);
double result = ceval_result(buffer);
printf("Result=%f\n", result);
return 0;
}
Výsledkem bude opět hodnota 42:
Result=42.000000
Knihovna ceval.h podporuje i volání některých běžných funkcí, což si taktéž otestujeme:
#include <stdio.h>
#include <stdlib.h>
#include "ceval.h"
int main(void) {
static char expression[] = "sin(deg2rad(45))";
double result = ceval_result(expression);
printf("Result=%f\n", result);
return 0;
}
Nyní by měla být výsledkem polovina druhé odmocniny dvojky:
Result=0.707107
Navíc dokáže knihovna ceval.h zobrazit strom odpovídající zadanému výrazu:
#include <stdlib.h>
#include "ceval.h"
int main(void) {
static char expression[] = "1+2*3";
ceval_tree(expression);
return 0;
}
Strom se zobrazí takovým způsobem, že uzel je zcela vlevo a nejsou zvýrazněny hrany mezi uzly:
3
*
2
+
1
Zobrazení stromu se složitějším výrazem:
#include <stdlib.h>
#include "ceval.h"
int main(void) {
static char expression[] = "(3-20)*(14+20)+20*(14-3+20)";
ceval_tree(expression);
return 0;
}
Výsledek:
20
+
3
-
14
*
20
+
20
+
14
*
20
-
3
A konečně zobrazení stromu výrazu, ve kterém se výsledek jedné funkce předá do funkce jiné:
#include <stdlib.h>
#include "ceval.h"
int main(void) {
static char expression[] = "sin(deg2rad(45))";
ceval_tree(expression);
return 0;
}
A takto je naznačena struktura stromu výrazu:
45
deg2rad
sin
18. Příloha: Makefile soubor pro překlad všech demonstračních příkladů
Všechny výše uvedené demonstrační příklady je možné přeložit s využitím souboru Makefile, který je zobrazen pod tímto odstavcem. Povšimněte si, že tento soubor obsahuje i cíle (targets), které v případě potřeby stáhnou i potřebné jednohlavičkové knihovny:
CC=gcc
all: stb_image_write.h ceval.h vec.h hashmap.h \
evaluation_2 evaluation_3 evaluation_4 evaluation_5 \
evaluation_6 evaluation_7 evaluation_8 \
vector_usage_1 vector_usage_2 vector_usage_3 \
simple_image_1 simple_image_2 simple_image_3 simple_image_4 \
hashmap_usage_1 hashmap_usage_2 hashmap_usage_3 \
perlin_noise perlin_noise_2
clean:
rm -f ceval.h
rm -f stb_image_write.h
rm -f stb_image_write.h
rm -f vec.h
rm -f hashmap.h
rm -f evaluation_1 evaluation_2 evaluation_3 evaluation_4 evaluation_5
rm -f evaluation_6 evaluation_7 evaluation_8
rm -f vector_usage_1 vector_usage_2 vector_usage_3
rm -f simple_image_1 simple_image_2 simple_image_3 simple_image_4
rm -f hashmap_usage_1 hashmap_usage_2 hashmap_usage_3
rm -f perlin_noise perlin_noise_2
evaluation_1: evaluation_1.c ceval.h
$(CC) -lm -O0 $< -o $@
evaluation_2: evaluation_2.c ceval.h
$(CC) -lm -O0 $< -o $@
evaluation_3: evaluation_3.c ceval.h
$(CC) -lm -O0 $< -o $@
evaluation_4: evaluation_4.c ceval.h
$(CC) -lm -O0 $< -o $@
evaluation_5: evaluation_5.c ceval.h
$(CC) -lm -O0 $< -o $@
evaluation_6: evaluation_6.c ceval.h
$(CC) -lm -O0 $< -o $@
evaluation_7: evaluation_7.c ceval.h
$(CC) -lm -O0 $< -o $@
evaluation_8: evaluation_8.c ceval.h
$(CC) -lm -O0 $< -o $@
hashmap_usage_1: hashmap_usage_1.c hashmap.h
$(CC) -O0 $< -o $@
hashmap_usage_2: hashmap_usage_2.c hashmap.h
$(CC) -O0 $< -o $@
hashmap_usage_3: hashmap_usage_3.c hashmap.h
$(CC) -O0 $< -o $@
vector_usage_1: vector_usage_1.c vec.h
$(CC) -O0 $< -o $@
vector_usage_2: vector_usage_2.c vec.h
$(CC) -O0 $< -o $@
vector_usage_3: vector_usage_3.c vec.h
$(CC) -O0 $< -o $@
simple_image_1: simple_image_1.c stb_image_write.h
$(CC) -O0 $< -o $@
simple_image_2: simple_image_2.c stb_image_write.h
$(CC) -O0 $< -o $@
simple_image_3: simple_image_3.c stb_image_write.h
$(CC) -O0 $< -o $@
simple_image_4: simple_image_4.c stb_image_write.h
$(CC) -O0 $< -o $@
perlin_noise: perlin_noise.c stb_image_write.h stb_perlin.h
$(CC) -O0 $< -o $@
perlin_noise_2: perlin_noise_2.c stb_image_write.h stb_perlin.h
$(CC) -O0 $< -o $@
stb_image_write.h:
wget https://raw.githubusercontent.com/nothings/stb/refs/heads/master/stb_image_write.h
ceval.h:
wget https://raw.githubusercontent.com/erstan/ceval-single-header/refs/heads/e_t/ceval.h
vec.h:
wget https://raw.githubusercontent.com/OguzhanUmutlu/vec.h/refs/heads/main/vec.h
hashmap.h:
wget https://raw.githubusercontent.com/sheredom/hashmap.h/refs/heads/master/hashmap.h
stb_perlin.h:
wget https://raw.githubusercontent.com/nothings/stb/refs/heads/master/stb_perlin.h
19. Repositář s demonstračními příklady
Všechny demonstrační příklady, s nimiž jsme se v dnešním článku seznámili a které jsou určeny pro překlad s využitím prakticky libovolného moderního překladače jazyka C, jsou dostupné, jak je zvykem, na GitHubu. V tabulce níže jsou uvedeny odkazy na jednotlivé zdrojové kódy psané v jazyku C, které typicky vyžadují nějakou „jednosouborovou“ knihovnu – viz výše uvedený soubor Makefile, který obsahuje příslušné příkazy pro stažení těchto knihoven:
| # | Příklad | Stručný popis příkladu | Adresa |
|---|---|---|---|
| 1 | Makefile | definice cílů pro překlad všech demonstračních příkladů z této tabulky | https://github.com/tisnik/8bit-fame/blob/master/header-only/Makefile |
| 2 | evaluation1.c | vyhodnocování výrazů, ukázka závislosti knihovny na standardní knihovně | https://github.com/tisnik/8bit-fame/blob/master/header-only/evaluation1.c |
| 3 | evaluation2.c | vyhodnocování výrazů, jednoduchý výraz, korektní varianta příkladu | https://github.com/tisnik/8bit-fame/blob/master/header-only/evaluation2.c |
| 4 | evaluation3.c | vyhodnocování výrazů, složitější výraz | https://github.com/tisnik/8bit-fame/blob/master/header-only/evaluation3.c |
| 5 | evaluation4.c | vyhodnocování výrazů, náhrada symbolů za hodnoty proměnných | https://github.com/tisnik/8bit-fame/blob/master/header-only/evaluation4.c |
| 6 | evaluation5.c | vyhodnocování výrazů, vestavěné funkce | https://github.com/tisnik/8bit-fame/blob/master/header-only/evaluation5.c |
| 7 | evaluation6.c | vyhodnocování výrazů, zobrazení stromu s jednoduchým výrazem | https://github.com/tisnik/8bit-fame/blob/master/header-only/evaluation6.c |
| 8 | evaluation7.c | vyhodnocování výrazů, zobrazení stromu se složitějším výrazem | https://github.com/tisnik/8bit-fame/blob/master/header-only/evaluation7.c |
| 9 | evaluation8.c | vyhodnocování výrazů, zobrazení stromu s výrazem, ve kterém se volají funkce | https://github.com/tisnik/8bit-fame/blob/master/header-only/evaluation8.c |
| 10 | hashmap_usage1.c | základní operace s mapou v jazyku C | https://github.com/tisnik/8bit-fame/blob/master/header-only/hashmap_usage1.c |
| 11 | hashmap_usage2.c | vymazání prvků z mapy | https://github.com/tisnik/8bit-fame/blob/master/header-only/hashmap_usage2.c |
| 12 | hashmap_usage3.c | iterace nad prvky uloženými v mapě | https://github.com/tisnik/8bit-fame/blob/master/header-only/hashmap_usage3.c |
| 13 | simple_image1.c | uložení rastrového obrázku do PNG, příklad s odděleným překladem knihovny | https://github.com/tisnik/8bit-fame/blob/master/header-only/simple_image1.c |
| 14 | simple_image2.c | uložení rastrového obrázku do PNG, příklad, který lze přeložit v jednom kroku | https://github.com/tisnik/8bit-fame/blob/master/header-only/simple_image2.c |
| 15 | simple_image3.c | uložení rastrového obrázku do PNG s kontrolou chyb | https://github.com/tisnik/8bit-fame/blob/master/header-only/simple_image3.c |
| 16 | simple_image4.c | uložení rastrového obrázku do PNG, rozdělení definice a deklarace funkcí z knihovny | https://github.com/tisnik/8bit-fame/blob/master/header-only/simple_image4.c |
| 17 | vector_usage1.c | vektor řetězců, základní varianta | https://github.com/tisnik/8bit-fame/blob/master/header-only/vector_usage1.c |
| 18 | vector_usage2.c | vektor řetězců, složitější varianta | https://github.com/tisnik/8bit-fame/blob/master/header-only/vector_usage2.c |
| 19 | vector_usage3.c | vektor struktur (záznamů) | https://github.com/tisnik/8bit-fame/blob/master/header-only/vector_usage3.c |
| 20 | perlin_noise.c | výpočet a vykreslení Perlinova šumu s uložením výsledku do obrázku typu PNG | https://github.com/tisnik/8bit-fame/blob/master/header-only/perlin_noise.c |
20. Odkazy na Internetu
- Header-only (Wikipedia)
https://en.wikipedia.org/wiki/Header-only - C/C++ open-source libraries with minimal dependencies
https://github.com/r-lyeh/single_file_libs - single-file public domain (or MIT licensed) libraries for C/C++
https://github.com/nothings/stb/tree/master - Top 19 C single-header Projects
https://www.libhunt.com/l/c/topic/single-header - ceval-single-header
https://github.com/erstan/ceval-single-header - Zdrojový kód knihovny ceval
https://github.com/erstan/ceval-single-header/blob/e_t/ceval.h - Are single header libraries good?
https://www.reddit.com/r/C_Programming/comments/12u7s37/are_single_header_libraries_good/ - Nanoprintf, a tiny header-only vsnprintf that supports floats! Zero dependencies, zero libc calls. No allocations, < 100B stack, < 5K C89/C99
https://www.reddit.com/r/C_Programming/comments/cae52j/comment/et9amng/ - Lessons learned about how to make a header-file library
https://github.com/nothings/stb/blob/master/docs/stb_howto.txt - Libraries That Quietly Revolutionized C
https://www.youtube.com/watch?v=kS_GqDp6IT4 - C (programming language)
https://en.wikipedia.org/wiki/C_(programming_language) - ANSI C
https://en.wikipedia.org/wiki/ANSI_C - C++
https://en.wikipedia.org/wiki/C%2B%2B - GNU Make
https://www.gnu.org/software/make/ - Cmake
https://cmake.org/ - Avoid the Temptation of Header-Only Libraries
https://dev.to/pauljlucas/avoid-the-temptation-of-header-only-libraries-33an - header-only-library
https://github.com/topics/header-only-library?o=desc&s=updated - vec.h – Lightweight Dynamic Arrays in C
https://github.com/OguzhanUmutlu/vec.h - stb_image_write.h
https://raw.githubusercontent.com/nothings/stb/refs/heads/master/stb_image_write.h - The SQLite Amalgamation
https://sqlite.org/amalgamation.html - SQLite Download Page
https://sqlite.org/download.html - hashmap.h
https://github.com/sheredom/hashmap.h - C Function Declaration and Definition
https://www.w3schools.com/c/c_functions_decl.php - Using Precompiled Headers
https://gcc.gnu.org/onlinedocs/gcc/Precompiled-Headers.html - Perlin noise
https://en.wikipedia.org/wiki/Perlin_noise



