Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Základy

Tato kapitola obsahuje základní informace o programování v jazyce C. Předpokládáme, že čtenář má již nějaké zkušenosti s programováním v procedurálním jazyce. Čtenář musí skousnout situaci, kdy narazíme na programovací konstrukt, který je vysvětlen až později.

Předpokládáme, že program bude běžet na platformě x86/x86-64.

Normy jazyka C

Jazyk C je popsán mezinárodní normou ISO/IEC 9899. Ta se přibližně jednou za deset let reviduje, takže „jazyk C“ ve skutečnosti označuje několik příbuzných jazyků, které se v detailech liší. Než napíšeme první program, je proto potřeba říct, o kterém z nich se tu vlastně bude mluvit.

Přehled dosavadních verzí vypadá zhruba takto.

Bezne oznaceni   Rok    Co prineslo
------------------------------------------------------------------------
C89 / C90        1989   Prvni norma. Deklarace promennych museli byt
                        na zacatku bloku, komentare jen /* ... */.
 
C99              1999   Komentare //, deklarace kdekoliv v bloku
                        (tedy i v hlavicce cyklu for), typ long long,
                        typ _Bool, slozene literaly, pole promenne delky.
 
C11              2011   Anonymni struktury a uniony, staticka tvrzeni,
                        vlakna. Pole promenne delky se stala volitelnymi.
 
C17              2018   Zadne nove vlastnosti, jen opravy nejasnosti
                        v textu normy C11.
 
C23              2024   Klicova slova bool, true, false. Zavazne
                        pouziti dvojkoveho doplnku pro znamenkove typy.
                        Mnoho dalsich noveho.
------------------------------------------------------------------------

V tomto textu předpokládáme normu C17. Je to dnes nejrozšířenější volba, podporují ji všechny běžné překladače a od C11 se liší jenom formulacemi. Na místech, kde novější C23 něco mění, na to upozorníme poznámkou; takových míst není mnoho, ale některá jsou důležitá.

Norma nepopisuje jazyk úplně do posledního detailu. Řadu věcí nechává záměrně otevřených, aby šlo C efektivně přeložit na různém hardwaru. Setkáme se přitom se třemi různými druhy „otevřenosti“ a budeme je pečlivě rozlišovat.

  • Chování určené implementací znamená, že si překladač smí vybrat z několika možností, ale musí svou volbu zdokumentovat a držet se jí. Takto je určena například velikost typu int nebo to, zda je typ char znaménkový. Na takové chování se lze spolehnout, ale jenom v rámci jedné platformy.

  • Nespecifikované chování znamená, že si překladač vybere z několika možností a dokumentovat to nemusí. Sem patří třeba pořadí, ve kterém se vyhodnotí argumenty funkce.

  • Nedefinované chování (anglicky undefined behaviour) znamená, že norma neklade na chování programu žádná omezení. Program může spadnout, může vypsat nesmysl, ale také může zdánlivě fungovat správně — a přestat fungovat po přidání nesouvisejícího řádku nebo po zapnutí optimalizací. Nedefinované chování není chyba, kterou překladač ohlásí; je to chyba, kterou musíme sami neudělat. Budeme na ně v textu opakovaně narážet a pokaždé je označíme.

Několik konkrétních hodnot, které norma nechává na implementaci, v textu používáme napevno, aby se o příkladech dalo mluvit srozumitelně: velikosti typů a podobné údaje uvádíme tak, jak vypadají na 64-bitovém unixovém systému s překladačem gcc. Na jiných platformách mohou být jiné a vždy, když na tom bude záležet, to zdůrazníme.

Překladače gcc i clang implicitně nepoužívají čistou normu, ale její rozšířenou variantu (gnu17, u novějších verzí gnu23), která obsahuje navíc vlastní rozšíření. Kód, který se přeloží, tak nemusí být přenositelný. Normu je proto lepší uvést výslovně.

# preklad podle normy C17 se zapnutymi varovanimi
gcc -std=c17 -Wall -Wextra -g program.c -o program
 
# spusteni prelozeneho programu
./program

Přepínač -std=c17 vybírá normu. Přepínače -Wall a -Wextra zapínají varování; ta nejsou chyby a překlad nezastaví, ale téměř vždy upozorňují na něco, co je potenciální chyba. Berte je vážně. Přepínač -g přidá do programu informace pro ladicí nástroje.

Chceme-li si vyzkoušet něco z novější normy, stačí přepínač změnit na -std=c23. Starší překladače ji nemusí znát celou.

První program

/*
	Toto je víceřádkový komentář.
	Nelze je vnořovat.	
*/

// Toto je jednořádkový komentář, platí do konce řádku 

//
// je potřeba kvůli funkci printf, přesný mechanismus je vysvětlen později
//
#include <stdio.h>

//
// definice funkce main, provedení programu odpovídá provedení funkce main
//
int main()
{
	//
	// volání funkce printf
	// argument "Ahoj svete" je řetězcový literál.
	//
	printf("Ahoj svete!!\n");
	return 0;
}

Proměnné, funkce, pole

Proměnné

Proměnnou definujeme následovně.

typ identifier;            // bez inicializace
typ identifier = init;     // s inicializací

Při definici je nutné specifikovat typ proměnné pomocí klíčového slova. Inicializace je nepovinná. Při inicializaci platí pravidla jako při provádění operátoru přiřazení. Přístup k neinicializované proměnné vede k nedefinovanému chování. Neinicializovaná proměnná totiž nějakou hodnotu má, jenom není dopředu na úrovni popisu jazyka definováno jakou.

int x;
int y = 10;

printf("%i\n", y);     // toto volání printf vypíše hodnotu proměnné y
printf("%i\n", x);     // nedefinované chování, přístup k neinicializované proměnné

Blok je část kódu uzavřená mezi { }, které si odpovídají: Bloky lze vytvářet jenom tak, aby se zanořovaly, otevírací i uzavírací závorka bloku musí být uvnitř nadřazeného bloku. Takto bloky přirozeně vytváří stromovou hierarchii, jejím kořenem je úroveň samotného souboru, tj. obsah souboru mimo všechny bloky.

C je jazyk s lexikálním rozsahem platnosti proměnných. Proměnná existuje od místa své definice do konce bloku, ve kterém je definována. Pokud je v nějakém bloku použita proměnná (např. součást výrazu atd.), je hledána v aktuálním bloku a poté v blocích nadřazených. Hledání končí na úrovni souboru, pokud tam není proměnná nalezena, vyhlásí překladač chybu. Proměnné, které jsou definovány na úrovni souboru označujeme jako globální. Ve stejném bloku, mimo podbloky, nemohou být definovány dvě proměnné stejného jména.

Důsledkem mechanismu vyhledávání proměnných je jejich shadowing.

int x = 10;
{
	int x = 100;
	printf("%i\n", x);      // vytiskne 100
}
printf("%i\n", x);          // vytiskne 10

Funkce

Funkci definujeme následujícím způsobem.

type identifier (parameter_list)
{
	// body
}

První řádek je tzv. hlavička funkce, kde je postupně typ návratové hodnoty funkce, jméno funkce a v závorkách seznam argumentů funkce: čárkami oddělená posloupnost dvojic typ id, specifikujíc typ argumentů a jejich jména. Příkaz return val; ukončí vykonávání funkce a jako návratovou hodnotu vrátí hodnotu výrazu val. Pokud chceme funkci bez návratové hodnoty, napíšeme na místo typu návratové hodnoty klíčové slovo void a používáme return bez val, nebo jej nepoužijeme vůbec a funkce končí, když kód doběhne na konec těla funkce.

int sum(int a, int b)
{
	return a + b;
}

void print_numbers(int a, int b, int c)
{
	printf("%i %i %i\n", a, b, c);
}

Funkci voláme následovně.

identifier(list_of_values)

Tady je identifier jméno funkce a list_of_values je čárkami oddělený seznam výrazů. Ty se vyhodnotí a výsledky těchto vyhodnocení se přiřadí argumentům funkce (hodnoty se kopírují, argumenty se předávají hodnotou). Pokud má funkce návratovou hodnotu, je výsledkem vyhodnocení volání funkce hodnota, kterou funkce vrátí.

int z = sum(1 + 4, 7);
printf("%i\n", z);        // vytiskne 12

print_numbers(z, z, 3);    // vytiskne 12 12 3

Pole

Pole je indexovaná kolekce hodnot stejného typu. Indexujeme pomocí čísel 0, 1, 2 …

Pole lze definovat následujícími způsoby.

type a[no_els]; 
type b[no_els] = init; 
type c[] = init; 

Výraz type je typ prvků, a, b, c jsou jména polí, no_els je počet prvků pole: toto musí být hodnota známá v době překladu, například literál. Výraz init je inicializační výraz pro pole.

{ val_0, val_1, ..., val_n }

Výrazy val_ se vyhodnotí a výsledky vyhodnocení se po řadě přiřadí na indexy 0, 1, …. Pokud je počet výrazů menší než velikost pole, jsou zbylé hodnoty vynulovány. Další možností je

{ [0] = val_0, [3] = val_3, [5] = val_5 }

Výrazy v hranatých závorkách jsou indexy, na které se při inicializaci zapíše hodnota na pravé straně =. Prvky na indexech, které v inicializačním výrazu nejsou, jsou vynulovány. Pokud v definici pole není uveden počet prvků, je převzat z inicializačního výrazu.

Pole s velikostí, která není známa v době překladu (tzv. pole proměnné délky, VLA), jazyk C od normy C99 umožňuje, ale my je používat nebudeme. Takové pole se alokuje na zásobníku a není žádný způsob, jak zjistit, že se tam nevešlo — program místo toho spadne. (A existují i další problémy s nimi.) Pro pole, jejichž velikost známe až za běhu, budeme používat malloc, viz kapitola Paměť.

K jednotlivým prvků pole přistupujeme pomocí indexu zapsaného do hranatých závorek za jméno pole.

Pole si nenese informaci o své velikosti a jazyk nekontroluje, zda při přístupu k poli používáme korektní index. Přístup k poli pomocí neplatného indexu vede k nedefinovanému chování.

Pole lze předat jako argument funkci, obvykle je s ním nutné předat i jeho velikost.

/* n je velikost pole a */
int find_max(int a[], int n)
{
	int max = a[0];
	for (int i = 1; i < n; i += 1)
	{
		if (a[i] > max)
		{
			max = a[i];
		}
	}
	return max;
}

Tisk

K tisku budeme používat funkci printf ze standardní knihovny. Její hlavička je

int printf(format_string, values)

Argument format_string je řetězec, který může obsahovat speciální instrukce pro tisk. Tyto instrukce jsou ve tvaru %instr, kde instr je vlastní instrukce. Každé instrukci ve format_string odpovídá ve values, seznamy výrazů oddělených čárkami, jedna hodnota, která se pomocí dané instrukce vytiskne (bráno zleva doprava). Instrukce se většinou týkají toho, jaký typ hodnoty se tiskne, podrobnosti lze nalézt v manuálu ke standardní knihovně. Základní instrukce jsou v příkladu níže.

printf("hello");     // žádné speciální instrukce, vytiskne se hello
printf("%i", -2);    // instrukce %i -- tisk celého znaménkového čísla
printf("%u", 15);    // instrukce %u -- tisk celého bezznaménkového čísla

instrukce     popis
-------------------
%c            znak
%s            řetězec
%f            číslo s desetinnou čárkou

Typový systém

Jazyk C je staticky typovaný (typy se kontrolují při překladu) a slabě typovaný (děje se mnoho implicitních typových konverzí).

Skalární typy

Pro celá čísla máme char, short, int, long, long long. Tyto typy mají ještě bezznaménkovou variantu uvozenou klíčovým slovem unsigned. Pro čísla s desetinnou čárkou máme typy float, double, long double.

Standard jazyka zaručuje minimální velikosti a rozsahy hodnot jednotlivých typů. (Velikost typu lze zjistit pomocí sizeof(typ).) Dále je zaručeno

sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)

a

sizeof(float) <= sizeof(double) <= sizeof(long double)

Skutečné velikosti se liší podle překladače a platformy. Například na 64 bitovém unixovém OS s překladačem gcc je

typ          sizeof(typ)
------------------------
char             1
short            2
int              4
long             8
long long        8
float            4
double           8
long double     16
------------------------

přičemž celá čísla jsou typicky uložena ve dvojkové (doplňkové) soustavě a čísla s desetinnou čárkou podle IEEE 754 standardu. Pokud jsou potřeba typy s garantovanou velikostí a znaménkovostí, lze použít typy definované v hlavičkovém souboru stdint.h.

Jazyk C historicky neměl typ pro pravdivostní hodnotu, platila konvence, že 0 je nepravda a ostatní hodnoty jsou pravda. V C99 by přidán typ _Bool a <stdbool.h> obsahující bool, true, false, v C23 je bool klíčovým slovem. Příkazy testující podmínku např. if, while se drží i historické konvence.

Typ char je z historických důvodů trochu speciální. Standard definuje tři typy signed char, unsigned char a char. Typ char je různý od obou těchto typů, na některých platformách se chová jako signed char (x86, budeme tedy toto chování předpokládat) na jiných jako unsigned char (např. ARM).

Desetinná čísla se ukládají ve dvojkové soustavě a s omezeným počtem bitů. Většinu zlomků proto nelze uložit přesně — stejně jako nelze v desítkové soustavě přesně zapsat jednu třetinu. Hodnota 0.1 je v paměti uložena jako číslo velmi blízké jedné desetině, nikoliv jako jedna desetina.

//
// s vetsim poctem desetinnych mist je videt, ze ulozena hodnota
// neni presne 0.1
//
printf("%.17f\n", 0.1);         // vytiskne 0.10000000000000001
 
printf("%.17f\n", 0.1 + 0.2);   // vytiskne 0.30000000000000004
printf("%i\n", 0.1 + 0.2 == 0.3);   // vytiskne 0

Z toho plyne důležité praktické pravidlo: desetinná čísla neporovnávejte operátorem ==. Místo toho zjišťujeme, zda se od sebe liší méně než o zvolenou toleranci.

#include <math.h>  // je potřeba překládat s flagem -lm
 
double a = 0.1 + 0.2;
double b = 0.3;
 
//
// fabs vraci absolutni hodnotu
// hodnota tolerance zavisi na tom, s jak velkymi cisly pocitame
//
if (fabs(a - b) < 1e-9) { printf("prakticky stejne\n"); }

Ze stejného důvodu nikdy neřídíme cyklus podmínkou na přesnou rovnost desetinných čísel. Následující cyklus by nikdy neskončil, protože se po deseti přičteních dostaneme na 0.99999999999999989, nikoliv na 1.0.

// spatne, nekonecny cyklus
for (double x = 0.0; x != 1.0; x += 0.1) { /* ... */ }
 
// dobre, ridime celociselnym citacem
for (int i = 0; i < 10; i += 1)
{
	double x = i / 10.0;
	/* ... */
}

Kolik číslic si typ zapamatuje, závisí na jeho velikosti: float zhruba 6 platných číslic, double zhruba 15. Proto v C počítáme s double, pokud nemáme důvod pro opak. Pro peněžní částky se desetinné typy nehodí vůbec — bývá lepší ukládat je jako celý počet haléřů v typu long long.

Desetinné typy umí kromě běžných čísel uložit ještě tři zvláštní hodnoty: kladné nekonečno, záporné nekonečno a NaN (z anglického not a number). Vznikají tam, kde výsledek operace nelze vyjádřit číslem — a na rozdíl od celočíselných typů přitom nejde o chybu ani o nedefinované chování. Program běží dál a zvláštní hodnota se dalšími výpočty šíří.

printf("%f\n", 1.0 / 0.0);     // vytiskne inf
printf("%f\n", -1.0 / 0.0);    // vytiskne -inf
printf("%f\n", 0.0 / 0.0);     // vytiskne nan
printf("%f\n", 1e308 * 10);    // vytiskne inf, vysledek je prilis velky
 
double x = 0.0 / 0.0;
printf("%f\n", x + 1);         // vytiskne nan, NaN se siri dal

Hodnota NaN má jednu vlastnost, která překvapí: není rovna ničemu, ani sama sobě. Všechna porovnání s ní kromě != jsou nepravdivá. Test x == x je proto jediný přenositelný způsob, jak zjistit, že proměnná NaN neobsahuje — a zároveň důvod, proč se na výsledky porovnávání desetinných čísel nelze slepě spoléhat.

#include <math.h>
 
double x = 0.0 / 0.0;
 
printf("%i\n", x == x);      // vytiskne 0
printf("%i\n", x != x);      // vytiskne 1
printf("%i\n", isnan(x));    // vytiskne 1, citelnejsi zapis tehoz
 
double y = 1.0 / 0.0;
printf("%i\n", isinf(y));    // vytiskne 1

Makra isnan, isinf a isfinite jsou v hlavičkovém souboru math.h, kde najdeme i konstanty INFINITY a NAN. Na rovnost s NAN ovšem nikdy netestujeme pomocí ==; k tomu slouží právě isnan.

Znaky a řetězce

Typ char je používán k uložení znaků podle ASCII tabulky (která znaky kóduje pomocí čísel). Znakové literály jsou mezi apostrofy. Některé speciální znaky je nutné v literálech zapisovat pomocí escapování, například znak nového řádku je \n.

/*
	x a y níže se rovnají
*/
char x = 'A';      // znakový literál
char y = 65;       // numerický literál

Řetězec v C je uložen v poli typu char. Přitom znaky čteme od začátku pole až po hodnotu 0. Poslední znak řetězce je tak znak na indexu o jedna menším, než na kterém je hodnota 0. Pole, ve kterém je řetězec uložen, tak musí být alespoň o jedna větší, než samotný řetězec (potřebujeme jedno místo pro hodnotu 0), může být ovšem větší. Řetězcové literály píšeme do uvozovek.

char string[] = "hellope";
/*
pole je velikosti 8
string[0] je znak 'h'
string[1] je znak 'e'
...
string[6] je znak 'e'
string[7] je znak '\0' (hodnota 0)
*/

Alias typu

Alias k typu vytvoříme následujícím způsobem.

typedef def_expr;

Výraz def_expr je ve tvaru definice proměnné, k typu v něm vytvoříme alias tvořený identifikátorem proměnné v něm. V následujícím příkladu vytvoříme k typu int alias number.

typedef int number;

Strukturovaný datový typ

Položky různých typů lze spojit do strukturovaného typu, který vytvoříme

struct type_id 
{
	item_1;
	item_2;
	...
	item_n;
};

kde jednotlivé položky item_1 až item_n jsou zápisy ve formě type item_id, type je tu typ položky a item_id je jméno položky. Nově vytvořený typ je struct type_id. Ke kratšímu pojmenování lze použít alias, je možný i následující zápis.

typedef struct type_id
{
	item_1;
	item_2;
	...
	item_n;
} type_id;

Tímto vytvoříme typ pojmenovaný jenom type_id. (Pokud ponecháme první výskyt type_id, vytvoříme typy struct type_id a type_id současně; první výskyt type_id jde vynechat, potom vytvoříme pouze type_id.)

Předchozí funguje protože

struct type_id 
{
	item_1;
	item_2;
	...
	item_n;
};

s případným vynecháním type_id je platným zápisem typu. Ten lze používat na místech, kde se obvykle píše jméno typu. Můžeme například vytvořit proměnné typu, který nemá jméno.

struct 
{
	item_1;
	item_2;
	...
	item_n;
} var1, var2;

Inicializační výraz strukturovaného typu vytvoříme následovně.

{ init_1, init_2, ..., init_n }

Výrazy init_1 až init_n jsou inicializační výrazy jednotlivých položek. Pokud některé položky vynecháme, jsou vynulovány. Alternativně lze použít zápis

{ 
	.item_id_1 = init_1,
	.item_id_2 = init_2,
	...
	.item_id_n = init_n,
}

přičemž lze opět vynechat některé položky. Chceme-li vynulovat všechny položky, lze použít následující výraz.

{ 0 }

Pro přístup k jednotlivým položkám použijeme operátoru . (tečka).

var.item_id

Příklad

/*
	Příklad ukazuje použití struktury pro reprezentaci
	vektoru ve dvourozměrném euklidovkém prostoru.
	Funkce počítají skalární součin a násobení skalárem.
*/
typedef struct 
{
	float x;
	float y;
} vec2f;

float dot_product(vec2f a, vec2f b)
{
	return a.x * b.x + a.y * b.y;
}

vec2f product(vec2f a, float b)
{
	return (vec2f){b * a.x, b * a.y};	
}

int main()
{
	/* inicializace */
	vec2f u = { .x = 0.3, .y = 0.5 };
	vec2f v = { .x = 1.2, .y = 10.5 };
	
	/* zápis do položek */
	u.x += 10;
	v.y = 6;

	/* volání funkcí */
	float dp = dot_product(u, v);
	vec2f p  = product(v, 4.1);
	
	return 0;
}

Velikost strukturovaného typu se nemusí rovnat součtu velikostí jeho položek, může být i větší. Překladač se může rozhodnout, že mezi dvěma položkami ponechá nějaké prázdné místo. (Překladač lze přinutit, aby to nedělal, ale tím se zabývat nebudeme).

Výčtový typ

Hodnotami výčtového typu je množina pojmenovaných celočíselných konstant. Typ spolu s konstantami vytvoříme následovně

enum type_id
{
	id_1,
	id_2,
	id_3,
	...
	id_n
};

Vytvoříme typ enum type_id (pro kratší alias lze jako u strukturovaného typu použít typedef). Jména jednotlivých konstant jsou id_1, id_2, ..., jejich hodnoty jsou po řadě 0,1,2 .... Konstantám lze v definici typu přiřadit explicitní hodnoty.

enum type_id
{
	id_1 = value_1,
	id_2,
	id_3 = value_3,
	...
	id_n
};

Přiřazení hodnot není povinné. Pokud nemá konstanta v definici přiřazenu hodnotu, je její hodnota o jedna větší, než hodnota předchozí konstanty. Pokud nemá hodnotu přiřazenu první konstanta, je její hodnota 0.

Typické využití je pro určování druhů entit (například chyb, tokenů apod).

/*
	Následující enum lze použit pro určení typu chyby,
	která se může stát při zpracování souboru. 
*/

typedef enum
{
	FE_NONE,
	FE_NOT_FOUND,
	FE_PERMISSION_DENIED,
	FE_BAD_FORMAT,
} file_error;

/*
	První konstantou je FE_NONE pro žádnou chybu, toto je výhodný
	trik, protože FE_NONE má hodnotu 0, která je jediná nepravda,
	ostatní položky mají hodnotu indikující pravdu.
*/	

file_error err = process_file(filename);
if (err) 
{
	// tady ošetříme chybu
}

/*
	V následujícím výčtovém typu má poslední konstanta
	hodnotu, která se rovná počtu konstant. Toho lze 
	použít pro vytvoření pole, kde jednotlivá políčka
	indexujeme pomocí konstant.
*/

typedef enum
{
	TT_OPERATOR,
	TT_IDENTIFICATOR,
	TT_BRACE,
	TT_TOKEN_TYPE_COUNT,
} token_type;
	
int last_token_offsets[TT_TOKEN_TYPE_COUNT];
last_token_offsets[TT_BRACE] = 10;

Jména konstant jsou globální (pro soubor, ve kterém jsou definována), na začátek těchto jmen většinou dáváme zkratku jména typu, abychom zmenšili pravděpodobnost konfliktu jména konstanty se jménem nečeho jiného. Jména konstant jsou často kapitálkami.

Union

Definice unionu je podobná definici strukturovaného typu

union type_id
{
	item_1;
	item_2;
	...
};

Všechny položky (hodnoty) unionu ovšem v paměti začínají na jednom místě a překrývají se tak (velikost unionu je pak velikostí jeho největší položky zvětšení o vycpávku kvůli zarovnání, podobně jako u struct). Za platnou tak můžeme považovat pouze hodnotu jedné položky. Lze využít při potřebě šetřit pamět, spolu s výčtovým typem lze vytvořit obdobu tzv. tagged unionu.

typedef enum 
{
	RT_INT,
	RT_STRING,
	RT_FLOAT,
	
} result_type;


typedef struct
{
	result_type type;
	union 
	{
		int int_value;
		char *string;
		float float_value;
		
	} value;
} tagged;

/*
	při zpracování hodnoty typu tagged potom nejdříve zkontrolujeme
	položky type a podle ní víme, kterou položku z unionu value máme
	zpracovat.
*/

tagged x = function_that_returns_tagged();
if (x.type == RT_INT)
{
	// zpracujeme int
	process_int(x.value.int_value);
}
else if (x.type == RT_STRING)
{
	// zpracujeme string
	process_string(x.value.string);
}
else 
{
	// zpracujeme float
	process_float(x.value.float_value);
}

Konverze a přetypování

Přiřadíme-li hodnotu jednoho typu do proměnné typu jiného, převede překladač hodnotu automaticky. Takové konverzi se říká implicitní a už jsme ji nevědomky několikrát použili.

double d = 3;      // z celeho cisla 3 se stane 3.0
int    i = 3.9;    // desetinna cast se zahodi, i ma hodnotu 3
char   c = 65;     // c ma hodnotu znaku 'A'

Někdy ale potřebujeme konverzi vynutit tam, kde by k ní sama od sebe nedošlo. K tomu slouží přetypování (anglicky cast), které zapisujeme tak, že před výraz uvedeme v kulatých závorkách cílový typ.

(typ) výraz

Přetypování je skutečně potřeba, když dělíme dvě celá čísla, např. int, a chceme výsledek jako desetinné číslo.

int a = 7;
int b = 2;
 
double wrong = a / b;
double right = (double)a / b;
 
printf("%f\n", wrong);   // vytiskne 3.000000
printf("%f\n", right);   // vytiskne 3.500000

Ve druhém případě je a převedeno na double ještě před dělením, a protože operátor dělení pak pracuje s jedním operandem typu double, převede se na double i b. Výsledek dělení je proto 3.5.

V prvním případě proběhne konverze na double také, ale až po dělení, kdy už je desetinná část ztracena. Přetypování má vyšší prioritu než binární operátory, takže v zápisu (double)a / b se přetypovává pouze a, nikoliv celý výraz a / b.

Podle toho, mezi jakými typy převádíme, může, ale nemusí dojít ke ztrátě informace. Rozhodující přitom není „šířka“ (tj. velikost) cílového typu, ale to, zda dokáže zdrojovou hodnotu reprezentovat. To jsou dvě různé věci a právě jejich zaměňování bývá zdrojem chyb.

Připomeňme také, že norma o velikostech typů zaručuje jenom nerovnosti sizeof(int) <= sizeof(long) <= sizeof(long long), nikoliv ostré nerovnosti. Typy int a long mohou být na některých platformách stejně velké, takže o převodu mezi nimi nelze bez znalosti platformy mluvit jako o „rozšíření“.

Pro přesné určení pravidel potřebujeme hodnost (anglicky rank) typu. Hodnost je uspořádání celočíselných typů podle „šířky“ v tomto pořadí

long long  >  long  >  int

přičemž znaménkový a bezznaménkový typ téhož jména mají hodnost stejnou: například unsigned long a long jsou si v hodnosti rovny, stejně tak unsigned int a int. Převody se řídí následujícími pravidly.

  • Mezi celočíselnými typy téže znaménkovosti je převod na typ vyšší hodnosti vždy bezpečný. Znaménkový typ obsahuje všechny hodnoty užšího znaménkového typu, bezznaménkový všechny hodnoty užšího bezznaménkového.

  • Z bezznaménkového typu na znaménkový je převod bezpečný jenom tehdy, je-li cílový typ skutečně širší. Jsou-li oba stejně velké, největší hodnoty se do cílového typu nevejdou.

    //
    // na 64-bitovem unixu, kde ma unsigned int 4 bajty a long 8 bajtu
    //
    unsigned int u = 4000000000u;
    long         l = u;       // bez ztraty, l ma hodnotu 4000000000
    
    //
    // unsigned long i long long tam ale maji oba 8 bajtu
    //
    unsigned long long ull = 18446744073709551615ull;
    long long          ll  = ull;   // hodnota se nevejde, vysledek je -1
    
  • Ze znaménkového typu na bezznaménkový není převod bezpečný nikdy, je-li hodnota záporná — a to bez ohledu na to, o kolik je cílový typ širší. Bezznaménkové typy prostě žádné záporné hodnoty neobsahují, takže se výsledek počítá modulo 2^N.

    int                x = -1;
    unsigned long long u = x;
    
    //
    // prevod na sirsi typ, presto se hodnota nezachovala
    // -1 + 2^64 == 18446744073709551615
    //
    printf("%llu\n", u);   // vytiskne 18446744073709551615
    
  • Mezi desetinnými typy je převod na širší typ (float na double, double na long double) vždy bezpečný.

  • Převod na užší desetinný typ (např. double na float) je naopak jediný případ, kdy se u desetinných typů něco ztrácí. Norma rozlišuje tři situace:

    1. Vejde-li se hodnota do cílového typu přesně, zůstane beze změny.
    2. Je-li v rozsahu, ale přesně reprezentovatelná není, je výsledkem nejbližší vyšší, nebo nejbližší nižší reprezentovatelná hodnota — a která z nich, to je určeno implementací.
    3. Je-li mimo rozsah cílového typu, jde o nedefinované chování.
    double d = 0.1;
    float  f = d;      // presnost se ztraci, hodnota je v rozsahu
    
    printf("%.20f\n", d);          // 0.10000000000000000555
    printf("%.20f\n", (double)f);  // 0.10000000149011611938
    

    Ztráta přesnosti nastává i hluboko uvnitř rozsahu. Typ float má na mantisu jen 24 bitů, takže celá čísla reprezentuje přesně pouze do 2^24; double s 53 bity až do 2^53.

    double big = 16777217.0;       // 2^24 + 1
      
    printf("%.0f\n", (double)(float)big);   // vytiskne 16777216
    

    Zpětný převod proto hodnotu nevrátí: (double)(float)x se obecně nerovná x. Mezivýsledky se z tohoto důvodu nepočítají ve float, pokud pro to není zvláštní důvod.

    Bod 3 je v praxi méně dramatický, než zní. Splňuje-li překladač normu IEC 60559 (tedy IEEE 754), což poznáme podle definovaného makra __STDC_IEC_559__, je konverze korektně zaokrouhlená, příliš velká hodnota dá nekonečno a příliš malá nulu.

    printf("%g\n", (double)(float)1e300);   // vytiskne inf
    printf("%g\n", (double)(float)1e-60);   // vytiskne 0
    

    Spoléhat se na to ovšem lze jenom tam, kde je zmíněné makro definováno.

  • Převod z desetinného typu na celočíselný desetinnou část zahodí, a to směrem k nule, nikoliv zaokrouhlením.

    printf("%i\n", (int)3.9);    // vytiskne 3
    printf("%i\n", (int)-3.9);   // vytiskne -3, nikoliv -4
    

    Nevejde-li se výsledek do cílového typu, jde o nedefinované chování. Výraz (int)1e30 tedy není „nějaké velké číslo“, ale chyba.

  • Převod z celočíselného typu na desetinný může ztratit přesnost, i když se hodnota do rozsahu vejde. Typ double má na uložení mantisy 53 bitů, takže celá čísla větší než 2^53 už reprezentovat nedokáže.

    long long big = 9007199254740993LL;   // 2^53 + 1
    double    d   = big;
    
    printf("%lld\n", (long long)d);   // vytiskne 9007199254740992
    
  • Převod na užší celočíselný typ hodnotu ořízne. Je-li cílový typ bezznaménkový, počítá se výsledek modulo 2^N, kde N je počet bitů cílového typu.

    int i = 300;
    printf("%u\n", (unsigned char)i);   // vytiskne 44, protoze 300 - 256 == 44
    

    Je-li cílový typ znaménkový a hodnota se do něj nevejde, je výsledek v normě C17 implementačně definovaný. Norma C23 už předepisuje ořezání ve dvojkovém doplňku, tedy chování, které mají všechny běžné překladače.

Kdy přetypovat nepotřebujeme

Přetypování překladači říká „vím, co dělám“. Tím ale zároveň umlčí případné varování, které by nás na skutečnou chybu upozornilo. Než přetypování napíšete, ověřte si proto, že varování opravdu nemá pravdu.

char *s = "";
 
//
// pretypovani tady jenom umlci varovani prekladace
// chyba zustava: strlen(s) - 1 se pocita v size_t
// a pro prazdny retezec da SIZE_MAX
//
if ((int)(strlen(s) - 1) >= 0) { /* ... */ }
 
//
// spravne je opravit vyraz, ne ho pretypovat
//
if (strlen(s) >= 1) { /* ... */ }

Na co si dát pozor

Zápis (vec2f){ .x = 1.0f, .y = 2.0f } z podsekce Struktury vypadá jako přetypování, ale není jím. Jde o složený literál, tedy o zápis nové hodnoty daného typu. Přetypování má vždy vpravo od závorky výraz, složený literál složené závorky.

Přetypovávat lze i pointery, tam ale platí další pravidla a omezení, kterými se budeme zabývat v kapitole Paměť.

Operátory

Operátory (= akce, výpočet) aplikujeme na operandy (= data), a získáme tím nějaký výsledek (= data). Ve složitějších výrazech se operátory vyhodnocují v pořadí podle priority, toto pořadí lze změnit uzávorkováním. Operátory se stejnou prioritou jsou vyhodnocovány podle asociativity (zleva či zprava), opět to jde změnit pomocí uzávorkování.

Implicitní konverze operandů

Většina binárních operátorů počítá s hodnotami stejného typu a téhož typu je i výsledek. Pokud je v kódu takový operátor aplikován na hodnoty různých typů, jsou operandy nejdříve implicitně převedeny na společný typ. Těmto konverzím se říká obvyklé aritmetické konverze (anglicky usual arithmetic conversions).

Obvyklé aritmetické konverze se provádějí u těchto operátorů.

Operátory                       Poznámka
------------------------------------------------------------------
+  -  *  /  %                   binární tvary
<  >  <=  >=                    porovnání
==  !=                          rovnost a nerovnost
&  |  ^                         bitové, kromě posuvů
------------------------------------------------------------------

U následujících operátorů se obvyklé aritmetické konverze neprovádějí. Tyto výjimky jsou zdrojem časných nedorozumění, proto je uvádíme přehledně.

Operátory        Co se deje misto toho
------------------------------------------------------------------
<<  >>           Kazdy operand se povysi ZVLAST (viz nize).
                 Typ vysledku je typem povyseneho LEVEHO operandu.
 
&&  ||           Operandy se pouze testuji na nulovost,
                 zadny spolecny typ nevznika.
 
!                Operand se testuje na nulovost, vysledek je int.
 
~                Provede se pouze celociselne povyseni
   -  +          jednoho operandu (unarni tvary).
 
=                Prava strana se prevede na typ leve strany,
                 to je uplne jine pravidlo (viz Operator prirazeni).
------------------------------------------------------------------

Pro porovnávání pointerů platí vlastní pravidla, o kterých mluvit nebudeme.

Je-li aspoň jeden operand neceločíselného typu, postupuje se podle následujících pravidel (aplikovaných seshora dolů).

  1. Je-li jeden operand long double, je druhý konvertován na long double.
  2. Je-li jeden operand double, je druhý konvertován na double.
  3. Je-li jeden operand float, je druhý konvertován na float.

Jsou-li oba operandy celočíselné, provede se nejdříve na každém z nich zvlášť tzv. celočíselné povýšení (anglicky integer promotion). Typy menší než int (znaménkové i bezznaménkové verze char a short) jsou přetypovány na int, pokud int dokáže reprezentovat všechny hodnoty původního typu. Jinak jsou přetypovány na unsigned int. Na běžných platformách (kde je int větší než short) to znamená, že se všechny typy menší než int povýší na int, tedy i ty bezznaménkové. To je důležité: po povýšení už žádný bezznaménkový operand nemusí zbýt.

short          a = -1;
unsigned short b = 1;
 
//
// oba operandy se povysi na int, vysledek je typu int
// vyraz a < b je proto pravdivy, jak bychom cekali
//
printf("%i\n", a < b);   // vytiskne 1
printf("%i\n", a + b);   // vytiskne 0

Na povýšené operandy se aplikují následující pravidla (seshora dolů).

  1. Mají-li oba operandy stejný typ, nekonvertuje se nic.
  2. Jsou-li oba operandy znaménkové, nebo oba bezznaménkové, je operand nižší hodnosti převeden na typ operandu vyšší hodnosti.
  3. Je-li hodnost bezznaménkového operandu větší nebo rovna hodnosti znaménkového operandu, je znaménkový operand převeden na typ toho bezznaménkového.
  4. Jinak: dokáže-li znaménkový typ reprezentovat všechny hodnoty bezznaménkového typu, je bezznaménkový operand převeden na tento znaménkový typ. Pokud to nedokáže, jsou oba operandy převedeny na bezznaménkovou variantu onoho znaménkového typu.

Bod 4 závisí na skutečných velikostech typů na dané platformě. Tentýž zdrojový kód proto může dát na různých platformách různý výsledek. Nelze tedy celou věc shrnout do jediného pevného pořadí typů — a právě proto je tu popsána takto podrobně.

Příklady

Typy int a unsigned int mají stejnou hodnost, takže se uplatní bod 3 (nerovnost „větší nebo rovna“ je splněna rovností). Znaménkový operand je převeden na unsigned int a výsledkem je hodnota typu unsigned int.

Analogicky pro long s unsigned long a pro long long s unsigned long long.

Převod záporného čísla na bezznaménkový typ není nedefinované chování. Je definován jako výpočet modulo 2^N, kde N je počet bitů cílového typu. Prakticky to znamená, že se k záporné hodnotě tak dlouho přičítá 2^N, až padne do rozsahu cílového typu — u čísel v rozsahu int tedy právě jednou.

int x = -1;
 
//
// -1 + 2^32 == -1 + 4294967296 == 4294967295
// coz je UINT_MAX, tedy nejvetsi hodnota typu unsigned int
//
printf("%u\n", (unsigned)x);   // vytiskne 4294967295

Samotný součet se počítá také modulo 2^N.

int      x = -1;
unsigned u = 1;
 
//
// 1. x se prevede na unsigned int, tedy na 4294967295
// 2. 4294967295 + 1 == 4294967296, coz je modulo 2^32 rovno 0
//
printf("%u\n", x + u);   // vytiskne 0

Obě konverze modulo 2^N se zde navzájem vyrušily a výsledek 0 odpovídá tomu, co bychom čekali od -1 + 1 ve znaménkové aritmetice. To není náhoda.

Modulo se řídí cílovým typem, nikoliv zdrojovým.

int x = -1;
 
printf("%u\n",   (unsigned int)x);        // -1 + 2^32, vytiskne 4294967295
printf("%llu\n", (unsigned long long)x);  // -1 + 2^64, vytiskne 18446744073709551615

Pokud výsledek hned převedeme zpět na znaménkový typ, aritmetika obvykle „vyjde“, protože počítání modulo 2^N je vratné.

unsigned u = 3;
int      x = -5;
 
//
// (unsigned)-5 == -5 + 4294967296 == 4294967291
// 4294967291 + 3 == 4294967294  (modulo 2^32)
//
printf("%u\n", u + x);   // vytiskne 4294967294
 
int r = u + x;
printf("%i\n", r);       // vytiskne -2, tedy vysledek, ktery jsme chteli

(Zpětný převod velké bezznaménkové hodnoty na int je v C17 implementačně definovaný, na všech běžných platformách se ale chová jako zde. Norma C23 už jej definuje závazně.)

Problém nastane ve chvíli, kdy je bezznaménkový mezivýsledek použit jako bezznaménkový. To se děje ve třech situacích.

  • Porovnávání. Tady se nic nevrací zpět, výsledkem je rovnou pravda nebo nepravda.

    int      x = -1;
    unsigned u = 1;
    
    if (x < u) { printf("ano\n"); }
    else       { printf("ne\n");  }
    
    //
    // vytiskne "ne"
    // x je prevedeno na 4294967295, coz je vetsi nez 1
    //
    
  • Dělení a bitové posuvy. Tady se „vratnost“ ztratí.

    unsigned u = 3;
    int      x = -5;
    
    printf("%u\n", (u + x) / 2);   // vytiskne 2147483647
    
    //
    // spravna odpoved je -1
    // 4294967294 / 2 == 2147483647, coz uz neodpovida zadne zaporne hodnote
    //
    
  • Cokoliv, kde vystupuje size_t Návratový typ operátoru sizeof i funkce strlen je size_t, což je bezznaménkový typ. Následující je pravděpodobně nejčastější varianta této chyby.

    char *s = "";
    
    //
    // strlen(s) je 0, typu size_t
    // 0 - 1 se pocita v size_t a da SIZE_MAX, tedy 18446744073709551615
    // podminka je splnena a cyklus cte daleko za konec retezce
    //
    for (int i = 0; i <= strlen(s) - 1; i += 1)
    { 
        printf("%c", s[i]);   // nedefinovane chovani
    }
    

    Stejná past číhá i v opačně běžícím cyklu přes pole.

    size_t n = 0;
    
    //
    // i je bezznamenkove, podminka i >= 0 je proto splnena vzdy
    // jde o nekonecny cyklus, ktery navic hned v prvni iteraci
    // pristupuje na index SIZE_MAX
    //
    for (size_t i = n - 1; i >= 0; i -= 1)
    {
        // telo
    }
    

Příklady, kde selže intuitivní pořadí typů

Mohlo by se zdát, že si vystačíme s jednoduchým pořadím, kde je každý bezznaménkový typ „větší“ než odpovídající znaménkový a zároveň menší než následující větší znaménkový. Následující dva příklady ukazují, že to tak není.

//
// prvni priklad
// na 64-bitovem unixu s gcc, kde ma long long i unsigned long 8 bajtu
//
long long     a = -1;
unsigned long b = 1;
 
//
// unsigned long ma NIZSI hodnost nez long long, bod 3 se tedy neuplatni.
// Bod 4: long long ma maximum 2^63-1, unsigned long ma maximum 2^64-1,
// takze long long vsechny hodnoty unsigned long nereprezentuje.
// Oba operandy se proto prevedou na unsigned long long
// a z -1 se stane 18446744073709551615.
//
printf("%i\n", a < b);   // vytiskne 0
//
// druhy priklad
//
long         a = -1;
unsigned int b = 1;
 
//
// Na 64-bitovem unixu ma long 8 bajtu a unsigned int 4 bajty.
// Bod 4: long vsechny hodnoty unsigned int reprezentuje,
// b se prevede na long, vysledek je 1.
//
// Na 32-bitove platforme (nebo na 64-bitovych Windows) ma long
// 4 bajty a vsechny hodnoty unsigned int reprezentovat nedokaze.
// Oba operandy se prevedou na unsigned long a vysledek je 0.
//
printf("%i\n", a < b);   // vytiskne 1 nebo 0 podle platformy

Druhý příklad je dobrou ilustrací toho, proč se u přenositelného kódu vyplatí míchání znaménkových a bezznaménkových typů rovnou vyloučit, místo abychom pro každý výraz zjišťovali, co se stane.

Dobré zvyky

  • Nemíchejte ve výrazu znaménkové a bezznaménkové typy.

  • Sjednoťte typ řídicí proměnné s typem meze. Buď obojí bezznaménkové, nebo obojí znaménkové s explicitním přetypováním na jednom místě.

    char *s = "ahoj";
    
    // varianta 1: obojí bezznaménkově
    for (size_t i = 0; i < strlen(s); i += 1)
    {
        printf("%c", s[i]);
    }
    
    // varianta 2: obojí znaménkově, pretypovani na jednom miste
    int len = (int)strlen(s);
    for (int i = 0; i < len; i += 1)
    {
        printf("%c", s[i]);
    }
    
  • Neodečítejte od bezznaménkových hodnot, dokud nevíte, že jsou dostatečně velké. Místo i <= n - 1 pište i < n.

    // spatne pro n == 0
    for (size_t i = 0; i <= n - 1; i += 1) { /* telo */ }
    
    // dobre
    for (size_t i = 0; i < n; i += 1) { /* telo */ }
    
  • Pro pozpátku běžící cyklus s bezznaménkovým indexem použijte následující idiom.

    //
    // podminka i-- > 0 nejdriv porovna, potom snizi
    // v tele cyklu ma i postupne hodnoty n-1, n-2, ..., 0
    // pro n == 0 se telo neprovede ani jednou
    //
    for (size_t i = n; i-- > 0; )
    {
        printf("%i\n", array[i]);
    }
    
  • Bezznaménkové typy používejte tam, kde k tomu máte důvod, tedy pro bitové operace, pro hodnoty, kde skutečně chcete počítání modulo, a pro velikosti a indexy, když pracujete s knihovními funkcemi vracejícími size_t. Nepoužívejte je jenom proto, že „záporné číslo nedává smysl“ — to samo o sobě žádný problém neřeší, jenom přesune chybu z rozpoznatelné záporné hodnoty na obrovské kladné číslo. Viz poznámka v sekci Poznámka k úkolům.

  • Hodnoty typu size_t tiskněte pomocí %zu, nikoliv %lu.

    printf("velikost int: %zu\n", sizeof(int));
    
  • Zapněte si varování překladače. Naprostou většinu chyb popsaných výše překladač odhalí sám, pokud mu to dovolíte.

    # -Wextra zahrnuje -Wsign-compare, ktere hlasi porovnani
    # znamenkoveho a bezznamenkoveho operandu
    gcc -std=c17 -Wall -Wextra -g program.c
    
    # prisnejsi varianta, hlasi i implicitni konverze menici znamenko
    gcc -std=c17 -Wall -Wextra -Wsign-conversion -g program.c
    

    Například pro cyklus s strlen z předchozí podsekce vypíše gcc s -Wextra zhruba následující.

    warning: comparison of integer expressions of different signedness:
    'int' and 'long unsigned int' [-Wsign-compare]
    

Aritmetické operátory

Aritmetické operátory počítají s hodnotami stejného typu a téhož typu je i výsledek. Jsou-li operandy různých typů, jsou nejdříve převedeny na společný typ podle pravidel v podsekci Implicitní konverze operandů.

Níže je tabulka aritmetických operátorů.

Operátor    Arita     Popis
-----------------------------------------------------------------------
 -            1       Prefixový unární operátor, mění znaménko
 +            1       Prefixový unární operátor, nemění hodnotu
+,-,*,/       2       Binární operátory, klasické aritmetické operace
%             2       Binární operátor,  zbytek po celočíselném dělení

Priorita a asociativita se řídí stejnými pravidly jako v matematice.

Jsou-li oba operandy operátoru / celočíselné, jde o celočíselné dělení: desetinná část výsledku se zahodí (ořízne se směrem k nule). Chceme-li dělit s desetinnou částí, musí být aspoň jeden operand neceločíselného typu.

printf("%i\n", 7 / 2);          // vytiskne 3, nikoliv 3.5
printf("%f\n", 7 / 2.0);        // vytiskne 3.500000
printf("%i\n", -7 / 2);         // vytiskne -3, orezani je smerem k nule
printf("%i\n", -7 % 2);         // vytiskne -1

Celočíselné dělení nulou (operátory / i %) vede k nedefinovanému chování.

Přetečení a podtečení

Celočíselné typy mají omezený rozsah. Výsledek aritmetické operace se do něj nemusí vejít, a to i tehdy, když jsou oba operandy zcela běžná čísla. Mluvíme pak o přetečení (výsledek je větší než maximum typu), případně o podtečení (výsledek je menší než minimum typu). Obojí se v C řeší stejně a zásadně přitom záleží na tom, zda je typ znaménkový.

Bezznaménkové typy podle normy přetéct nemohou. Veškerá aritmetika se u nich počítá modulo 2^N, kde N je počet bitů typu, přesně tak, jak jsme to popsali u konverzí. Výsledek je tedy vždy jednoznačně daný.

unsigned int u = 4294967295u;   // UINT_MAX
 
//
// 4294967295 + 1 == 4294967296, coz je modulo 2^32 rovno 0
//
printf("%u\n", u + 1);   // vytiskne 0
 
unsigned int v = 0;
 
//
// 0 - 1 == -1, coz je modulo 2^32 rovno 4294967295
//
printf("%u\n", v - 1);   // vytiskne 4294967295

Že je chování definované ovšem neznamená, že je správné. Druhý příklad je přesně ta chyba, na kterou jsme narazili u cyklů s size_t: program nespadne, jenom počítá s číslem 4294967295 tam, kde jsme chtěli -1.

Přeteče-li výsledek u znaménkového typu, jde o nedefinované chování. Norma neříká, že se hodnota „přetočí“ do záporných čísel — neříká vůbec nic.

int i = 2147483647;   // INT_MAX
 
//
// nedefinovane chovani
// NELZE spolehat na to, ze vyjde -2147483648
//
printf("%i\n", i + 1);

Překladač smí při optimalizaci předpokládat, že k přetečení nedojde, a na tomto předpokladu stavět. Následující funkce proto s přepínačem -O2 vrací vždy 1, přestože „ručním dosazením“ INT_MAX bychom čekali 0.

int is_increasing(int x)
{
	//
	// prekladac uvazuje takto: kdyby x + 1 pretekalo, slo by
	// o nedefinovane chovani, ktere v korektnim programu nenastava.
	// Muze tedy predpokladat, ze x + 1 > x plati vzdy,
	// a cely vypocet nahradit konstantou 1.
	//
	return x + 1 > x;
}

Ze stejného důvodu se z následujícího cyklu může stát cyklus nekonečný.

//
// prekladac smi predpokladat, ze i nikdy nepretece,
// tedy ze podminka i > 0 plati porad
//
for (int i = 1; i > 0; i *= 2)
{
	printf("%i\n", i);
}

Testovat přetečení až po výpočtu proto nemá smysl. Test se buď vyhodnotí jinak, než čekáme, nebo ho překladač odstraní úplně.

Přetečení nezpůsobuje jenom sčítání a násobení.

#include <limits.h> // hlavičkový soubor obsahující max a min hodnoty typu
 
int i = INT_MIN;
 
printf("%i\n", -i);       // nedefinovane, |INT_MIN| je o 1 vetsi nez INT_MAX
printf("%i\n", i / -1);   // nedefinovane, ze stejneho duvodu
printf("%i\n", i % -1);   // nedefinovane, i kdyz je vysledek zjevne 0

Rozsah znaménkových typů totiž není symetrický: INT_MIN je -2147483648, ale INT_MAX je jen 2147483647. Záporných hodnot je o jednu více. (předpokládáme 32 bitový int).

Připomeňme také, že dělení nulou (operátory / i %) je nedefinované chování vždy, bez ohledu na znaménkovost.

Aritmetika s typy menšími než int k přetečení nevede, protože se operandy nejdříve povýší na int. Přetéct může teprve výsledné přiřazení zpět do užšího typu, a to je běžná konverze, nikoliv nedefinované chování.

signed char a = 100;
signed char b = 100;
 
//
// a + b se pocita v typu int a da 200, k pretecení nedochazi
//
printf("%i\n", a + b);   // vytiskne 200
 
//
// teprve tady se 200 nevejde do signed char
// jde o konverzi, ktera je v C17 implementacne definovana
//
signed char c = a + b;
printf("%i\n", c);       // typicky vytiskne -56

Jak se přetečení bránit.

  • Kontrolujte předem, ne potom. Test se musí zapsat tak, aby k přetečení vůbec nedošlo. Pro sčítání dvou hodnot typu int vypadá takto.

    #include <limits.h>
    
    //
    // vraci 1, pokud lze a + b spocitat bez pretecení
    //
    int can_add(int a, int b)
    {
        if (b > 0 && a > INT_MAX - b) { return 0; }
        if (b < 0 && a < INT_MIN - b) { return 0; }
        return 1;
    }
    

    Meze jednotlivých typů najdete v hlavičkovém souboru limits.h jako makra INT_MIN, INT_MAX, LONG_MAX, UINT_MAX a podobně.

  • Volte dostatečně velký typ. Přetečení často není potřeba ošetřovat, stačí mu předejít. Padesáté Fibonacciho číslo se do int nevejde (fib(47) je 2971215073, zatímco INT_MAX je 2147483647), do long long ano.

  • Používejte nástroje. Překladače gcc i clang umí přetečení znaménkových typů odhalit za běhu.

    # program pri pretecení vypise, kde k nemu doslo
    gcc -std=c17 -Wall -Wextra -fsanitize=undefined -g program.c -o program
    

    Tento přepínač odhalí i řadu dalších druhů nedefinovaného chování, se kterými se setkáme v kapitole Paměť. Vyplatí se ho zapínat vždy, když program ladíme.

U desetinných typů k nedefinovanému chování nedochází. Výsledek, který je příliš velký, se uloží jako nekonečno; výsledek příliš malý na to, aby se dal rozlišit od nuly, se uloží jako nula.

Operátor přiřazení

Slouží k přiřazení hodnoty na nějaké zapisovatelné místo. Syntax je následující.

lvalue = rvalue

Výraz lvalue musí označovat místo, kam se dá zapisovat (například proměnnou, položku struktury, prvek v poli), rvalue je výraz, který se vyhodnotí, a výsledek je zapsán do lvalue. Dochází k implicitní konverzi typu rvalue na typ lvalue. (Tato konverze je něco jiného než obvyklé aritmetické konverze z podsekce Implicitní konverze operandů. Tam se hledá společný typ obou operandů; tady se pravá strana vždy převede na typ levé strany, ať už je jakýkoliv.) Tato konverze může být ztrátová: například dojde k zaokrouhlování, ztrátě desetinné části čísla, zachování pouze spodních bajtů celého čísla. (Některé konverze typů jsou zakázány, operátor provádí pouze konverze mezi kompatibilními typy, detaily lze nalézt ve standardu.) Výsledkem operátoru přiřazení je hodnota levého operandu po přiřazení, tedy až po provedení případné konverze. Výsledek není lvalue, nelze mu proto znovu přiřadit.

char c;
int  i = (c = 300);
//
// do c se ulozi 300 orezane na typ char
// vysledkem prirazeni je tato orezana hodnota, nikoliv 300
//
printf("%i\n", i);   // netiskne 300

Operátor přiřazení lze kombinovat s binárními operátory, analogicky následujícímu příkladu, kde jej kombinujeme s operátorem +.

int x = 10;
/* místo  */
x =  x + 3;
/* můžeme psát */
x += 3;

Operátory porovnání

Následující operátory porovnávají číselné typy. Výsledek je 1 pokud je porovnání pravdivé, nebo 0 jinak. Operátory jsou následující.

<, >, <=, >=, ==, !=

Test rovnosti je tvořen dvěma rovnítky, nikoliv jedním. Jedno rovnítko je operátor přiřazení. Test nerovnosti se zapisuje !=

Jsou-li operandy různých typů, jsou před porovnáním převedeny na společný typ podle pravidel v podsekci Implicitní konverze operandů. U porovnání je to podstatně zrádnější než u aritmetiky: výsledkem je rovnou pravda nebo nepravda, takže se případná konverze nemá jak „vrátit zpět“.

int      x = -1;
unsigned u = 1;
//
// x se prevede na unsigned int, tedy na 4294967295
// porovnani je proto nepravdive
//
printf("%i\n", x < u);   // vytiskne 0

Logické operátory

Operátory pro konjunkci (logická spojka a), disjunkci (logická spojka nebo) a negaci.

Operátor    Popis
--------------------------------------
&&          konjunkce
||          disjunkce
!           negace, unární, prefixový
--------------------------------------

Operátory konjunkce a disjunkce jsou asociativní zleva a vyhodnocují se líně. Uveďme si to na příkladu konjunkce.

/* 
	Uvažme následující konjunkci 
*/
x && y && z && w

/* 
	Díky asociativitě zleva je to vlastně
*/
((x && y) && z) && w

/*
   Pokud je x == 1, y == 1, z == 0, w == 1, probíhá výpočet následovně:

	1. x && y  dá  výsledek 1
	2. 1 && z  dá  výsledek 0, výpočet hodnoty výrazu končí s výsledkem 0
   
    V tomto vyhodnocení je líné to, že 0 && w už se nepočítá.	
	
	Líného vyhodnocení lze s výhodou využít, pokud je ve výrazu 

	(pred && op)
	
	výraz pred kontrolou toho, jestli můžeme výraz op bezpečně vyhodnotit.
	Pokud se totiž pred vyhodnotí na 0, výraz op už se nevyhodnocuje.
*/

U operátorů &&, || a ! k obvyklým aritmetickým konverzím nedochází. Operandy se pouze testují na nulovost, žádný společný typ nevzniká. Výsledkem je vždy 0 nebo 1 typu int.

Bitové operátory

Bitové operátory aplikujeme na celočíselné operandy. Umožňují pracovat s jednotlivými bity těchto operandů. Jedná se logické operátory aplikované po bitech, nebo bitové posuvy. Operátory si vysvětlíme na následujících příkladech.

Předpokládejme, že x == 100 a y == 50 jsou jednobajtová čísla (typu unsigned char). Ve dvojkové soustavě máme

x ==  0 1 1 0 0 1 0 0
y ==  0 0 1 1 0 0 1 0

Potom máme

konjunkce po bitech
x       0 1 1 0 0 1 0 0
y       0 0 1 1 0 0 1 0
-----------------------
x & y   0 0 1 0 0 0 0 0


disjunkce po bitech 
x       0 1 1 0 0 1 0 0
y       0 0 1 1 0 0 1 0
-----------------------
x | y   0 1 1 1 0 1 1 0

nonekvivalence po bitech
x       0 1 1 0 0 1 0 0
y       0 0 1 1 0 0 1 0
-----------------------
x ^ y   0 1 0 1 0 1 1 0

negace po bitech
x       0 1 1 0 0 1 0 0
-----------------------
~x      1 0 0 1 1 0 1 1

bitový posuv doleva, tady o 2 bity, lze o obecný počet bitů
(zprava nahrazujem prázdná místa 0)
x       0 1 1 0 0 1 0 0
-----------------------
x << 2  1 0 0 1 0 0 0 0

bitový posuv doprava o 2 bity, lze o obecný počet bitů 
(zleva nahrazujeme prázdná místa 0 pro unsigned typy, 
u signed typů je nahrazen znaménkovým bitem: 1 u záporných
hodnot, 0 u kladných)
x       0 1 1 0 0 1 0 0
-----------------------
x >> 2  0 0 0 1 1 0 0 1

U operátorů &, | a ^ se provádějí obvyklé aritmetické konverze podle podsekce Implicitní konverze operandů.

U operátorů posuvu << a >> tomu tak není. Každý operand se povýší zvlášť a typem výsledku je typ povýšeného levého operandu. Na pravý operand se typ levého operandu nijak nepřenáší. Pravý operand posuvu musí být nezáporný a zároveň ostře menší než počet bitů povýšeného levého operandu. Jinak jde o nedefinované chování. Při posuvu doprava se zleva doplňují nuly, je-li levý operand bezznaménkový nebo je-li znaménkový a nezáporný. Je-li levý operand znaménkový a záporný, je výsledek v normě C17 implementačně definovaný (na běžných překladačích se doplňuje znaménkový bit, tedy jedničky); norma C23 už toto chování předepisuje závazně.

Příklady výše ukazují pro přehlednost pouze spodní bajt výsledku. Ve skutečnosti se x typu unsigned char nejdříve povýší na int, takže například

unsigned char x = 100;
printf("%i\n", ~x);        // vytiskne -101, nikoliv 155
printf("%i\n", x << 2);    // vytiskne 400, zadne bity se neztraci
//
// chceme-li vysledek zpet v jednom bajtu, musime pretypovat
//
printf("%u\n", (unsigned char)~x);       // vytiskne 155
printf("%u\n", (unsigned char)(x << 2)); // vytiskne 144

Inkrement a dekrement

Zvýšení proměnné o jedničku je v programech tak časté, že pro ně má C vlastní operátory: ++ pro zvýšení (inkrement) a -- pro snížení (dekrement). Operandem musí být lvalue, nikoliv libovolný výraz.

int i = 5;
 
i += 1;    // zapis, ktery pouzivame v tomto textu
i++;       // znamena totez

Oba operátory lze zapsat před operand i za něj a v tom je rozdíl. Prefixový tvar ++i proměnnou nejdříve zvýší a výsledkem výrazu je už nová hodnota. Postfixový tvar i++ proměnnou také zvýší, ale výsledkem výrazu je hodnota původní.

int i;
int j;
 
i = 5;
j = ++i;
printf("i = %i, j = %i\n", i, j);   // vytiskne i = 6, j = 6
 
i = 5;
j = i++;
printf("i = %i, j = %i\n", i, j);   // vytiskne i = 6, j = 5

Stojí-li operátor v příkazu sám, jsou oba tvary zaměnitelné: i++; i ++i; udělá totéž. Rozdíl se projeví teprve tehdy, když hodnotu výrazu někde použijeme.

V tomto textu budeme kvůli čitelnosti používat zápis i += 1. Operátory ++ a -- ale potkáte v každém cizím programu, zejména v hlavičkách cyklů, kde jsou naprosto běžné.

for (int i = 0; i < 10; i++)
{
	printf("%i\n", i);
}

Podmíněný operátor

Chceme-li podle podmínky vybrat jednu ze dvou hodnot, nemusíme psát celý if. Slouží k tomu podmíněný operátor, kterému se pro jeho tři operandy říká také ternární.

podmínka ? hodnota_když_pravda : hodnota_když_nepravda

Vyhodnotí se podmínka. Je-li pravdivá, výsledkem celého výrazu je druhý operand, jinak třetí.

int a = 10;
int b = 20;
 
int max = a > b ? a : b;
 
printf("%i\n", max);   // vytiskne 20

Totéž zapsané pomocí if je delší a vyžaduje proměnnou zavést zvlášť:

int max;
 
if (a > b) { max = a; }
else       { max = b; }

Podobně jako u logických spojek se vyhodnocuje líně: provede se vždy jenom jeden z obou výrazů, nikdy oba. Toho lze využít k ošetření okrajových případů.

//
// deleni nulou se neprovede, protoze pri n == 0
// se treti operand vubec nevyhodnocuje
//
double average = n == 0 ? 0.0 : (double)sum / n;

Nejčastěji se podmíněný operátor hodí uvnitř volání funkce, kam by se if psát nedal.

printf("nalezeno %i %s\n", n, n == 1 ? "prvek" : "prvku");

Na druhý a třetí operand se aplikují obvyklé aritmetické konverze, takže typ výsledku nemusí odpovídat ani jednomu z nich zvlášť.

//
// druhy operand je int, treti double
// typem celeho vyrazu je proto double
//
printf("%f\n", 1 ? 1 : 2.0);   // vytiskne 1.000000

Podmíněné operátory kvůli čitelnosti nezanořujte. Nepoužívejte v nich výrazy s vedlejšími účinky - je primárně určen k výběru hodnoty, nikoliv k řížení běhu programu.

Kontrola běhu programu

Větvení

if (tval)
{
	// blok při nenulovém tval
}
else 
{
	// blok při nulovém tval
}

Větev else je nepovinná. Pokud blok obsahuje pouze jeden příkaz, lze závorky vynechat. Při vynechávání závorek bloků, nemusí být na první pohled patrné, ke kterému if patří dané else: toto je dáno pravidlem, že patří k nejbližšímu if, které připadá v úvahu (tj. není obsazeno jiným else podle struktury bloků). Toto pravidlo se zhusta používá v konstrukci známé jako žebřík.

if (tval_1)
{
	// tval_1 je pravda (tj. není 0)
}
else if (tval_2)
{
	// tval_1 je nepravda (tj. je 0)
	// tval_2 je pravda
}
else if (tval_3)
{
	// tval_1 a tval_2 jsou nepravda
	// tval_3 je pravda
}
else 
{
	// tval_1, tval_2, tval_3 jsou nepravda
}

Není špatným zvykem psát závorky pro bloky skoro vždy (mimo speciálních idiomů, například žebříku).

Dalším příkazem pro větvení je switch.

switch (výraz)
{
	case konstanta_1:
		příkazy
		break;
 
	case konstanta_2:
		příkazy
		break;
 
	default:
		příkazy
}

Vyhodnotí se výraz a program skočí na to návěstí case, jehož konstanta se hodnotě rovná. Neodpovídá-li žádné, skočí se na návěstí default; pokud default uvedeno není, neprovede se nic.

switch (operator)
{
	case '+':
		printf("%i\n", a + b);
		break;
 
	case '-':
		printf("%i\n", a - b);
		break;
 
	case '*':
		printf("%i\n", a * b);
		break;
 
	default:
		printf("neznamy operator\n");
}

Pro switch platí dvě omezení. Výraz v závorce musí být celočíselného typu, nelze tedy větvit podle desetinného čísla ani podle řetězce. A konstanty u návěstí case musí být konstantní výrazy známé v době překladu a navzájem různé; nelze tam napsat proměnnou.

Příkaz break ukončí celý switch. Vynecháme-li ho, program pokračuje do dalšího case — a to i tehdy, když jeho konstanta hodnotě neodpovídá. Této vlastnosti se říká propadávání (anglicky fall through).

switch (znak)
{
	case 'a':
		printf("A");
		// chybi break, propada se dal
 
	case 'b':
		printf("B");
		break;
}
 
//
// pro znak 'a' vytiskne "AB", nikoliv "A"
//

Zapomenuté break je nejčastější chyba při psaní switch. Občas se ale propadávání hodí, chceme-li pro několik hodnot provést totéž.

switch (znak)
{
	case 'a':
	case 'e':
	case 'i':
	case 'o':
	case 'u':
		printf("samohlaska\n");
		break;
 
	default:
		printf("souhlaska\n");
}

Rozdíl mezi oběma případy je v tom, že v druhém příkladu jsou návěstí uvedena bezprostředně za sebou a žádné příkazy mezi nimi nejsou. Je-li propadávání záměrné a nějaké příkazy mezi návěstími jsou, je zvykem napsat tam komentář, aby bylo jasné, že nejde o zapomenuté break.

Zvlášť dobře se switch hodí pro výčtové typy. Překladač totiž s přepínačem -Wall upozorní na hodnotu výčtu, kterou jsme opomněli ošetřit — což je kontrola, kterou u řetězu if–else nedostaneme.

enum direction { DIR_UP, DIR_DOWN, DIR_LEFT, DIR_RIGHT };
 
void move(enum direction d)
{
	switch (d)
	{
		case DIR_UP:    y -= 1; break;
		case DIR_DOWN:  y += 1; break;
		case DIR_LEFT:  x -= 1; break;
 
		//
		// zapomenuty DIR_RIGHT prekladac ohlasi:
		// warning: enumeration value 'DIR_RIGHT' not handled in switch
		//
	}
}

V příkladu je default je vynechán schválně. S ním by totiž překladač považoval všechny hodnoty za ošetřené a nic by nehlásil.

Cykly

Základní cyklus.

while (tval)
{
	// tělo
}

Běh je následující

  1. Vyhodnotíme tval, pokud není pravdivý, cyklus končí
  2. Provedeme tělo cyklu a přejdeme k bodu 1.

Krokovací cyklus.

for (iter; tval; incr)
{
	// tělo
}

Běh je následující

  1. Když cyklus začíná, provede se část iter. Ta může obsahovat i definici proměnné, tato proměnná je potom platná jenom v cyklu, uchovává si ale hodnotu mezi jednotlivými iteracemi.
  2. Potom se vyhodnotí tval. Pokud je pravdivá, provede se tělo cyklu, jinak cyklus skončí.
  3. Provede se výraz incr a přechází se k bodu 2.

Části iter, tval i incr lze vynechat (středníky musí zůstat), pokud chybí tval je brán jako pravdivý.

Cyklus s alespoň jednou iterací.

do 
{
	// tělo
} while (tval);

Podobné cyklu while, pouze vyhodnocujeme tval na konci těla cyklu, nikoliv na začátku. V důsledku toho se tělo cyklu provede vždy aspoň jednou.

/*
	Příklad: výpis čísel od 0 do n (n už se netiskne)
*/
int n = 10;

{ 
	// pomocí while
	int i = 0;
	while(i < n)
	{
		printf("%i\n", i);
		i += 1;
	}
}

{
	// pomocí for
	for(int i = 0; i < n; i += 1)
	{
		printf("%i\n", i);
	}	
}

{ 
	// pomocí do while
	// nefunguje pro n == 0 
	int i = 0;
	do
	{
		printf("%i\n", i);
		i += 1;
	} while(i < n);
}

Vykonávání libovolného cyklu lze přerušit provedením příkazu break. Ukončit provádění aktuální iterace cyklu a skočit dopředu na konec těla cyklu lze příkazem continue.

Poznámka k úkolům

Pokud se v úkolech vyskytují čísla, vhodně zvolte typ. Většinou stačí int, případně float. Můžete předpokládat, že funkce dostanou správné argumenty: pokud je například v úkolu napsáno, že argumentem funkce má být přirozené číslo, můžete pro ně zvolit typ int a přitom ve funkci nekontrolovat, že argument je nezáporný. Nemusíte volit unsigned int.

/* funkce zjišťující, jestli je n prvočíslo */
int is_prime(int n)
{
	/* následující test není potřeba */
	if (n <= 0) 
	{
		// a very bad error !!!!
	}
}

Úkoly

  1. Ve vybraném vývojovém prostředí vytvořte program, který vytiskne větu: Ahoj světe. Program zkompilujte a spusťte.

  2. Napište funkce pro výpočet obsahu obdélníka a kruhu.

  3. Napište funkci pro převod malého písmena na velké.

  4. Napište program, který vypíše velikosti všech skalárních typů.

  5. Napište program, který vytiskne první a poslední číslici tříciferného čísla.

  6. Napište funkci, která zvlášť vytiskne celou a desetinnou část desetinného čísla.

  7. Napište funkci, která vrátí i-tý bit čísla (bezznaménkového typu).

  8. Napište funkci, která vypíše hodnoty všech bitů čísla typu unsigned char.

  9. Pomocí unsigned char lze reprezentovat podmnožinu množiny {0,..,7} následovně: bit na indexu i je roven 1, pokud i patří do podmnožiny, jinak je roven 0. Naprogramujte funkce pro test podmnožinovosti a výpočet rozdílu.

  10. Napište funkci, která spočítá minimum tří čísel.

  11. Napište funkci, která rozhodne, zda je daný rok přestupný.

  12. Napište funkci, která vypočítá n-té Fibonacciho číslo.

  13. Napište funkci, která vytiskne všechna čtyřciferná čísla, jejichž součet číslic je dělitelný 7.

  14. Napište funkci, který pro přirozená čísla x, y vypíše všechna čísla větší než x a menší než y taková, že se čtou stejně zepředu jako zezadu (např. 12321, 1001).

  15. Napište funkci, která pro přirozené číslo vytiskne jeho rozklad na prvočísla.

  16. Napište funkci, která rozhodne, zda jsou dvě pole stejná, když

    • pole jsou stejná, pokud na všech indexech mají stejné hodnoty.
    • (těžší úkol) pole jsou stejná, pokud můžeme proházet prvky v prvním poli tak, aby bylo stejné jako druhé pole podle definice z předchozího bodu.
  17. Napište funkci, která ověří, zda-li jsou prvky v poli uspořádány vzestupně.

  18. Napište funkci, který určí maximální počet znaků, na kterých konec řetězce r1 stejný jako začátek řetězce r2.

    Například pro "ahoj" a "hojnost" jsou to tři znaky, odpovídající řetězci "hoj".
    a h o j
    h o j n o s t
    

    Nepoužívejte funkce ze string.h.

  19. Napište funkci, která pro řetězce x, y spočítá, kolikrát se x vyskytuje jako podřetězec y. Jednotlivé výskyty se mohou překrývat. Nepoužívejte funkce ze string.h.

  20. Napište program, který pro dvojice typů (int, unsigned int), (long, unsigned int) a (long long, unsigned long) vytiskne, zda je výraz a < b pravdivý pro a == -1 a b == 1. Výsledky porovnejte s pravidly z podsekce o implicitních konverzích.

  21. Následující funkce má vrátit počet prvků pole, které jsou menší než zadaná mez. Pro některé vstupy nefunguje. Najděte chybu, vysvětlete ji pomocí pravidel o konverzích a opravte ji.

    int count_below(int array[], size_t n, int limit)
    {
    	int count = 0;
    	for (int i = 0; i <= n - 1; i += 1)
    	{
    		if (array[i] < limit) { count += 1; }
    	}
    	return count;
    }
    
  22. Napište funkci double average(int array[], int n), která vrátí aritmetický průměr prvků pole. Ověřte, že pro pole {1, 2} vrací 1.5 a nikoliv 1.0.

  23. Napište funkci int days_in_month(int month, int year), která vrátí počet dní v zadaném měsíci. Využijte propadávání pro měsíce se stejným počtem dní. Nezapomeňte na přestupné roky.

  24. Určete, co vytiskne následující kód, a ověřte si to překladem.

    int i = 3;
    printf("%i\n", i++);
    printf("%i\n", i);
    printf("%i\n", ++i);
    
  25. Vytvořte typ Time pro čas s položkami pro hodiny, minuty a sekundy. Naprogramujte funkce

    Time time_from_seconds(int total);   // prevede pocet sekund na Time
    int  time_to_seconds(Time t);        // prevede Time na pocet sekund
    Time time_add(Time a, Time b);       // secte dva casy, vysledek normalizuje
    void time_print(Time t);             // vytiskne ve tvaru HH:MM:SS
    

    Normalizací se myslí, že výsledek nemá více než 59 sekund ani více než 59 minut. Funkce time_print musí tisknout čísla vždy na dvě číslice (v manuálu k printf najděte, jak toho docílit).

  26. Vytvořte výčtový typ Weekday pro dny v týdnu. Naprogramujte funkce

    int     is_weekend(Weekday d);     // vraci 1 pro sobotu a nedeli, jinak 0
    void    weekday_print(Weekday d);  // vytiskne cesky nazev dne
    Weekday weekday_next(Weekday d);   // po nedeli nasleduje pondeli
    

    První dvě funkce napište pomocí switch bez větve default a přeložte s přepínačem -Wall. Vyzkoušejte, co se stane, když jednu z hodnot výčtu vynecháte, a co se stane, když default doplníte.

  27. Naprogramujte pomocí podmíněného operátoru (bez if) funkce

    int clamp(int x, int lo, int hi);  // vraci x omezene do intervalu <lo,hi>
    int sign(int x);                   // vraci -1, 0 nebo 1
    

    U funkce sign si všimněte, že se bez zanoření podmíněných operátorů neobejdete. Napište i variantu s if a rozhodněte, která je čitelnější.

  28. Následující funkce má vrátit počet číslic nezáporného čísla. Pro jeden konkrétní vstup vrací špatný výsledek. Najděte ho, vysvětlete proč a opravte.

    int count_digits(int n)
    {
    	int count = 0;
    	while (n > 0)
    	{
    		count += 1;
    		n /= 10;
    	}
    	return count;
    }
    

Řešení úkolů

Úkol 1

Vytvořte program, který vytiskne větu „Ahoj světe“, přeložte jej a spusťte.

#include <stdio.h>

int main()
{
	printf("Ahoj svete\n");
	return 0;
}
gcc -std=c17 -Wall -Wextra -g program.c -o program
./program

Diakritiku raději v prvním programu nepoužívejte. Zda se vypíše správně, závisí na kódování zdrojového souboru i na nastavení terminálu, a s tím se v tuto chvíli nemá smysl zdržovat.

Úkol 2

Napište funkce pro výpočet obsahu obdélníka a kruhu.

#define PI 3.14159265358979

double rect_area(double a, double b)
{
	return a * b;
}

double circle_area(double r)
{
	return PI * r * r;
}

Pozor na dvě věci. Obsah kruhu je πr², nikoliv 2πr — to je obvod. A konstantu PI nelze zavést jako const double, protože by to nebyl konstantní výraz; v C se pro ni používá makro (kapitola Preprocesor). Hlavičkový soubor math.h nabízí M_PI, to ale není v normě.

Úkol 3

Napište funkci pro převod malého písmena na velké.

char to_upper(char c)
{
	if (c >= 'a' && c <= 'z') { return (char)(c - 'a' + 'A'); }
	return c;
}

Řešení se opírá o to, že malá písmena mají v ASCII po sobě jdoucí kódy. Znaky, které písmena nejsou, funkce vrací nezměněné.

Přetypování (char) je potřeba proto, že c - 'a' + 'A' je typu int (kapitola Implicitní konverze operandů). Bez něj překladač s -Wconversion upozorní na zúžení.

Standardní knihovna má pro totéž funkci toupper z ctype.h. Ta pracuje správně i pro jiné znakové sady, ale pozor: jejím argumentem musí být hodnota reprezentovatelná v unsigned char, takže se jí předává toupper((unsigned char)c).

Úkol 4

Napište program, který vypíše velikosti všech skalárních typů.

#include <stdio.h>

int main()
{
	printf("char          %zu\n", sizeof(char));
	printf("short         %zu\n", sizeof(short));
	printf("int           %zu\n", sizeof(int));
	printf("long          %zu\n", sizeof(long));
	printf("long long     %zu\n", sizeof(long long));
	printf("float         %zu\n", sizeof(float));
	printf("double        %zu\n", sizeof(double));
	printf("long double   %zu\n", sizeof(long double));
	return 0;
}

Direktiva pro size_t je %zu, nikoliv %lu. Na 64-bitovém unixu by fungovaly obě, ale jinde nemusí.

Bezznaménkové varianty mají stejnou velikost jako znaménkové, vypisovat je zvlášť proto nemá smysl.

Úkol 5

Napište program, který vytiskne první a poslední číslici tříciferného čísla.

void print_first_last(int n)
{
	printf("prvni: %i, posledni: %i\n", n / 100, n % 10);
}

První číslici dostaneme celočíselným dělením stem, poslední zbytkem po dělení deseti. Pro n == 507 vypíše prvni: 5, posledni: 7.

Pro záporná čísla by obojí vyšlo záporné (ořezání je směrem k nule), podle poznámky k úkolům ale můžeme předpokládat správný vstup.

Úkol 6

Napište funkci, která zvlášť vytiskne celou a desetinnou část desetinného čísla.

#include <math.h>

void print_parts(double x)
{
	double ip;
	double fp = modf(x, &ip);
	printf("cela: %.0f, desetinna: %f\n", ip, fp);
}

Funkce modf z math.h vrátí desetinnou část a celou zapíše na předanou adresu (překládejte s -lm). Obě části mají stejné znaménko jako x.

Bez math.h lze psát i

void print_parts(double x)
{
	long long ip = (long long)x;    // orezani smerem k nule
	printf("cela: %lld, desetinna: %f\n", ip, x - (double)ip);
}

Tato varianta ale selže pro čísla, která se do long long nevejdou — jde o nedefinované chování, viz podsekci Konverze a přetypování.

Úkol 7

Napište funkci, která vrátí i-tý bit čísla (bezznaménkového typu).

int get_bit(unsigned int x, int i)
{
	return (int)((x >> i) & 1u);
}

Posuneme požadovaný bit na pozici 0 a maskou 1u zahodíme zbytek.

Druhý operand posuvu musí být nezáporný a ostře menší než počet bitů povýšeného levého operandu, tedy zde 0 <= i < 32. Jinak jde o nedefinované chování; v ostrém kódu by tu patřil assert.

Úkol 8

Napište funkci, která vypíše hodnoty všech bitů čísla typu unsigned char.

void print_bits(unsigned char x)
{
	for (int i = 7; i >= 0; i -= 1)
	{
		printf("%i", (x >> i) & 1);
	}
	printf("\n");
}

Jdeme od nejvyššího bitu k nejnižšímu, aby výpis odpovídal běžnému zápisu čísla. Pro x == 100 vypíše 01100100.

Všimněte si, že x se před posuvem povýší na int — u posuvu doprava to ničemu nevadí, protože unsigned char je vždy nezáporný.

Úkol 9

Podmnožina množiny {0,…,7} v unsigned char: test podmnožinovosti a rozdíl.

//
// a je podmnozinou b prave tehdy, kdyz a neobsahuje nic,
// co by nebylo v b
//
int is_subset(unsigned char a, unsigned char b)
{
	return (a & ~b) == 0;
}

unsigned char set_diff(unsigned char a, unsigned char b)
{
	return (unsigned char)(a & ~b);
}

Obojí stojí na výrazu a & ~b, tedy „prvky a, které nejsou v b“.

Přetypování u set_diff je nutné: ~b je díky povýšení typu int a má nastavené i horní bity, takže bez ořezání bychom vrátili nesmysl. Právě tohle je případ z podsekce Bitové operátory, kde příklady ukazují jenom spodní bajt.

Další užitečné operace: sjednocení a | b, průnik a & b, test prvku (a >> i) & 1, přidání prvku a | (1u << i).

Úkol 10

Napište funkci, která spočítá minimum tří čísel.

int min3(int a, int b, int c)
{
	int m = a;
	if (b < m) { m = b; }
	if (c < m) { m = c; }
	return m;
}

Pomocí podmíněného operátoru lze totéž napsat jako

int min3(int a, int b, int c)
{
	int m = a < b ? a : b;
	return m < c ? m : c;
}

Zápis a < b ? (a < c ? a : c) : (b < c ? b : c) je také správný, ale zanořené podmíněné operátory se špatně čtou — viz doporučení v podsekci Podmíněný operátor.

Úkol 11

Napište funkci, která rozhodne, zda je daný rok přestupný.

int is_leap(int y)
{
	return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
}

Rok je přestupný, je-li dělitelný čtyřmi, s výjimkou celých staletí, která musí být dělitelná čtyřmi sty. Kontrola: 2024 ano, 1900 ne, 2000 ano, 2023 ne.

Úkol 12

Napište funkci, která vypočítá n-té Fibonacciho číslo.

long long fib(int n)
{
	if (n <= 0) { return 0; }

	long long prev = 0;
	long long cur  = 1;

	for (int i = 1; i < n; i += 1)
	{
		long long next = prev + cur;
		prev = cur;
		cur  = next;
	}
	return cur;
}

Iterativní řešení potřebuje jenom dvě poslední hodnoty, takže je v lineárním čase a konstantní paměti.

Rekurzivní varianta je sice kratší, ale exponenciálně pomalá — počítá tytéž hodnoty znovu a znovu a pro n kolem 45 už je nepoužitelná.

Návratový typ je long long záměrně. Do int se vejde nejvýše fib(46) == 1836311903; už fib(47) == 2971215073 by přeteklo, což je u znaménkového typu nedefinované chování (viz podsekci Přetečení a podtečení). Do long long se vejde fib(92).

Úkol 13

Napište funkci, která vytiskne všechna čtyřciferná čísla, jejichž součet číslic je dělitelný 7.

int digit_sum(int n)
{
	int s = 0;
	while (n > 0)
	{
		s += n % 10;
		n /= 10;
	}
	return s;
}

void print_div7(void)
{
	for (int n = 1000; n <= 9999; n += 1)
	{
		if (digit_sum(n) % 7 == 0) { printf("%i\n", n); }
	}
}

Takových čísel je 1282. Pomocná funkce digit_sum používá standardní dvojici % 10 pro poslední číslici a /= 10 pro její odstranění.

Úkol 14

Pro přirozená čísla x, y vypište všechna čísla větší než x a menší než y, která se čtou stejně zepředu i zezadu.

int is_palindrome(int n)
{
	int rev  = 0;
	int orig = n;

	while (n > 0)
	{
		rev = rev * 10 + n % 10;
		n /= 10;
	}
	return rev == orig;
}

void print_palindromes(int x, int y)
{
	for (int n = x + 1; n < y; n += 1)
	{
		if (is_palindrome(n)) { printf("%i\n", n); }
	}
}

Číslo obrátíme tak, že z něj postupně odebíráme poslední číslici a přikládáme ji k výsledku. Mezi 1000 a 2000 je takových čísel deset: 1001, 1111, 1221, …, 1991.

Pozor: pro velmi velká čísla by rev mohlo přetéct. Pro čtyřciferná čísla ze zadání to nehrozí.

Úkol 15

Pro přirozené číslo vytiskněte jeho rozklad na prvočísla.

void factorize(int n)
{
	for (int d = 2; d * d <= n; d += 1)
	{
		while (n % d == 0)
		{
			printf("%i ", d);
			n /= d;
		}
	}

	//
	// co zbylo, je bud 1, nebo prvocislo vetsi nez odmocnina
	//
	if (n > 1) { printf("%i", n); }
	printf("\n");
}

Zkoušíme dělitele od 2 nahoru a každým dělíme, dokud to jde. Tím máme jistotu, že narazíme jenom na prvočísla: složená čísla už byla vydělena svými menšími činiteli.

Cyklus stačí vést do odmocniny z n, protože největší prvočíselný dělitel nad odmocninou může být nejvýše jeden — a ten vytiskneme za cyklem. Pro 360 vyjde 2 2 2 3 3 5.

Podmínku píšeme jako d * d <= n, nikoliv d <= sqrt(n) — vyhneme se tak desetinné aritmetice i jejím nepřesnostem.

Úkol 16

Rozhodněte, zda jsou dvě pole stejná, když (a) mají na všech indexech stejné hodnoty; (b) lze prvky prvního pole proházet tak, aby vzniklo druhé.

int arrays_equal(int a[], int b[], int n)
{
	for (int i = 0; i < n; i += 1)
	{
		if (a[i] != b[i]) { return 0; }
	}
	return 1;
}

Druhá část je těžší: jde o rovnost multimnožin, takže se musí shodovat i počty opakování.

Nejjednodušší řešení se opírá o jedinou myšlenku: pro každý prvek prvního pole ověříme, že se v obou polích vyskytuje stejněkrát.

//
// spocita, kolikrat se hodnota value vyskytuje v poli a
//
int count_occurrences(int a[], int n, int value)
{
	int count = 0;
	for (int i = 0; i < n; i += 1)
	{
		if (a[i] == value) { count += 1; }
	}
	return count;
}
 
int arrays_perm(int a[], int b[], int n)
{
	for (int i = 0; i < n; i += 1)
	{
		if (count_occurrences(a, n, a[i]) != count_occurrences(b, n, a[i]))
		{
			return 0;
		}
	}
	return 1;
}

Mohlo by se zdát, že jsme nic neověřili o hodnotách, které jsou v b, ale v a nikoliv. Ty ale vypadnout nemohou: sečteme-li počty výskytů přes všechny různé hodnoty z a, dostaneme v poli a právě n. Protože se všechny tyto počty shodují i v b, je v b také n prvků s hodnotami z a — a víc jich tam být nemůže, protože b má délku n. Žádná cizí hodnota se tam tedy nevejde.

Opakující se hodnoty se kontrolují zbytečně vícekrát — pro {5,5,5} se tatáž kontrola provede třikrát. Vyhnout se tomu lze přeskočením hodnot, které už jsme dřív viděli:

int arrays_perm_fast(int a[], int b[], int n)
{
	for (int i = 0; i < n; i += 1)
	{
		//
		// vyskytuje se a[i] uz nekde pred indexem i? pak uz je hotovo
		//
		int already_done = 0;
		for (int j = 0; j < i; j += 1)
		{
			if (a[j] == a[i]) { already_done = 1; break; }
		}
		if (already_done) { continue; }
 
		if (count_occurrences(a, n, a[i]) != count_occurrences(b, n, a[i])) { return 0; }
	}
	return 1;
}

Alternativní řešení je pomocí odškrtávání spárovaných prvků.

int arrays_perm_marking(int a[], int b[], int n)
{
	int used[MAX_N] = { 0 };    // MAX_N musi byt aspon n
 
	for (int i = 0; i < n; i += 1)
	{
		int found = 0;
		for (int j = 0; j < n; j += 1)
		{
			if (!used[j] && a[i] == b[j])
			{
				used[j] = 1;   // tento prvek b uz je spotrebovan
				found = 1;
				break;
			}
		}
		if (!found) { return 0; }
	}
	return 1;
}

Nevýhodou tohoto řešení je, že potřebuje pomocné pole. Pole used musí mít pevnou velikost, protože pole proměnné délky (VLA) nepoužíváme. (Dynamická alokace je až v kapitole Paměť.)

Obě řešení jsou kvadratická. Rychlejší je obě pole setřídit a porovnat po prvcích (n log n) nebo — je-li rozsah hodnot malý — spočítat výskyty do pomocného pole indexovaného hodnotou, což vede na lineární čas.

Úkol 17

Ověřte, zda jsou prvky v poli uspořádány vzestupně.

int is_sorted(int a[], int n)
{
	for (int i = 1; i < n; i += 1)
	{
		if (a[i - 1] > a[i]) { return 0; }
	}
	return 1;
}

Stačí porovnávat sousední dvojice. Cyklus začíná od 1, aby index i - 1 byl vždy platný — kdybychom začali od 0, četli bychom a[-1].

Prázdné i jednoprvkové pole funkce označí za uspořádané, což je správně. Použití > místo >= znamená, že opakující se hodnoty nevadí.

Úkol 18

Určete maximální počet znaků, na kterých je konec řetězce r1 stejný jako začátek řetězce r2.

int my_len(char s[])
{
	int n = 0;
	while (s[n]) { n += 1; }
	return n;
}

int overlap(char r1[], char r2[])
{
	int l1 = my_len(r1);
	int l2 = my_len(r2);
	int max = l1 < l2 ? l1 : l2;

	//
	// zkousime od nejdelsiho prekryvu, prvni nalezeny je i nejdelsi
	//
	for (int k = max; k > 0; k -= 1)
	{
		int ok = 1;
		for (int i = 0; i < k; i += 1)
		{
			if (r1[l1 - k + i] != r2[i]) { ok = 0; break; }
		}
		if (ok) { return k; }
	}
	return 0;
}

Pro "ahoj" a "hojnost" vrátí 3. Délku počítáme sami, funkce ze string.h zatím nepoužíváme.

Klíčové je jít od nejdelšího překryvu dolů — jinak bychom našli nějaký překryv, ale ne nutně nejdelší. Délka překryvu nemůže být větší než kratší z obou řetězců.

Úkol 19

Spočítejte, kolikrát se x vyskytuje jako podřetězec y; výskyty se mohou překrývat.

int count_sub(char x[], char y[])
{
	int lx = my_len(x);
	int ly = my_len(y);
	int count = 0;

	if (lx == 0 || lx > ly) { return 0; }

	for (int start = 0; start + lx <= ly; start += 1)
	{
		int ok = 1;
		for (int i = 0; i < lx; i += 1)
		{
			if (y[start + i] != x[i]) { ok = 0; break; }
		}
		if (ok) { count += 1; }
	}
	return count;
}

Zkoušíme každou možnou počáteční pozici. Protože po nalezení výskytu posouváme start jenom o jedna, počítají se i překrývající se výskyty: "aa" v "aaaa" se vyskytuje třikrát.

Podmínka start + lx <= ly zajistí, že nikdy nečteme za koncem y. Zápis start <= ly - lx by byl v kódu výše ekvivalentní. Kdybychom ovšem změnili délky na typ size_t, choval by se pro lx > ly katastrofálně.

Úkol 20

Pro dvojice typů (int, unsigned int), (long, unsigned int) a (long long, unsigned long) vytiskněte, zda je a < b pravdivé pro a == -1 a b == 1.

#include <stdio.h>

int main()
{
	{
		int          a = -1;
		unsigned int b = 1;
		printf("(int, unsigned int)        : %i\n", a < b);
	}
	{
		long         a = -1;
		unsigned int b = 1;
		printf("(long, unsigned int)       : %i\n", a < b);
	}
	{
		long long     a = -1;
		unsigned long b = 1;
		printf("(long long, unsigned long) : %i\n", a < b);
	}
	return 0;
}

Na 64-bitovém unixu s gcc vypíše 0, 1, 0.

  • int vs unsigned int: stejná hodnost, uplatní se bod 3. Znaménkový operand se převede na unsigned int, z -1 se stane 4294967295, porovnání je nepravdivé.
  • long vs unsigned int: long má vyšší hodnost, bod 3 se neuplatní. Bod 4: 64-bitový long reprezentuje všechny hodnoty 32-bitového unsigned int, takže se b převede na long a vyjde 1. Na 32-bitové platformě by ale vyšlo 0.
  • long long vs unsigned long: unsigned long má nižší hodnost, bod 3 se neuplatní. Bod 4: long long nereprezentuje všechny hodnoty 64-bitového unsigned long, takže se oba operandy převedou na unsigned long long a vyjde 0.

Překladač všechna tři porovnání ohlásí s -Wextra jako -Wsign-compare.

Úkol 21

Funkce count_below pro některé vstupy nefunguje. Najděte chybu, vysvětlete ji a opravte.

Chyba je v podmínce cyklu:

for (int i = 0; i <= n - 1; i += 1)

Parametr n je typu size_t, tedy bezznaménkový. Výraz n - 1 se proto počítá v size_t, a pro n == 0 nedá -1, ale SIZE_MAX. Podmínka je splněna, cyklus se rozběhne a funkce čte daleko za koncem pole — nedefinované chování.

Navíc se v porovnání i <= n - 1 míchá znaménkový int a bezznaménkový size_t; i se převede na size_t.

Oprava spočívá v tom, že od bezznaménkové hodnoty vůbec neodečítáme:

int count_below(int array[], size_t n, int limit)
{
	int count = 0;
	for (size_t i = 0; i < n; i += 1)
	{
		if (array[i] < limit) { count += 1; }
	}
	return count;
}

Řídicí proměnná je nyní také size_t, takže se v porovnání nemíchají znaménkovosti a -Wsign-compare mlčí. Pro n == 0 se tělo cyklu neprovede ani jednou.

Úkol 22

Napište funkci double average(int array[], int n). Ověřte, že pro pole {1, 2} vrací 1.5.

double average(int array[], int n)
{
	long long sum = 0;
	for (int i = 0; i < n; i += 1)
	{
		sum += array[i];
	}
	return (double)sum / n;
}

Podstatné je přetypování. Bez něj by se sum / n počítalo celočíselně, desetinná část by se zahodila a pro {1, 2} by funkce vrátila 1.0. Konverze na double až u výsledku dělení nepomůže — v tu chvíli už je informace ztracena.

Součet akumulujeme v long long, aby u dlouhých polí nepřeteklo. Pro n == 0 bychom dělili nulou; podle poznámky k úkolům předpokládáme smysluplný vstup, jinak by tu patřil assert(n > 0).

Úkol 23

Napište funkci int days_in_month(int month, int year). Využijte propadávání pro měsíce se stejným počtem dní.

int days_in_month(int month, int year)
{
	switch (month)
	{
		case 1: 
		case 3: 
		case 5: 
		case 7:
		case 8: 
		case 10: 
		case 12:
			return 31;

		case 4: 
		case 6: 
		case 9: 
		case 11:
			return 30;

		case 2:
			return is_leap(year) ? 29 : 28;

		default:
			return 0;    // neplatny mesic
	}
}

Návěstí jsou uvedena bezprostředně za sebou a žádné příkazy mezi nimi nejsou, jde o záměrné a čitelné propadávání.

Příkaz break tu není potřeba, protože každá větev končí return. Kdybychom místo návratu přiřazovali do proměnné, break by nutný byl.

Úkol 24

Určete, co vytiskne následující kód, a ověřte si to překladem.

Program vytiskne 3, 4, 5.

int i = 3;
printf("%i\n", i++);   // vytiskne 3, i je potom 4
printf("%i\n", i);     // vytiskne 4
printf("%i\n", ++i);   // i se zvysi na 5, vytiskne 5

Postfixový i++ proměnnou zvýší, ale hodnotou výrazu je hodnota původní. Prefixový ++i naopak vrací hodnotu novou.

Všimněte si, že každé volání printf je samostatný příkaz, takže je mezi nimi jasně dané pořadí. Kdybychom všechny tři výrazy dali do jediného volání

printf("%i %i %i\n", i++, i, ++i);   // NESPECIFIKOVANE CHOVANI

šlo by o nespecifikované chování: proměnná i by se v jednom výrazu měnila dvakrát a norma neurčuje, v jakém pořadí se argumenty vyhodnotí.

Úkol 25

Vytvořte typ Time pro čas s položkami pro hodiny, minuty a sekundy a naprogramujte funkce pro převod, součet a tisk.

typedef struct
{
	int hours;
	int minutes;
	int seconds;
} Time;

Time time_from_seconds(int total)
{
	Time t;
	t.hours   = total / 3600;
	t.minutes = (total % 3600) / 60;
	t.seconds = total % 60;
	return t;
}

int time_to_seconds(Time t)
{
	return t.hours * 3600 + t.minutes * 60 + t.seconds;
}

//
// normalizaci nemusime resit rucne: prevedeme oba casy na sekundy,
// secteme a prevedeme zpet
//
Time time_add(Time a, Time b)
{
	return time_from_seconds(time_to_seconds(a) + time_to_seconds(b));
}

void time_print(Time t)
{
	printf("%02i:%02i:%02i", t.hours, t.minutes, t.seconds);
}

Nejzajímavější je time_add. Sčítat položky zvlášť a pak přenášet přebytek by šlo taky, ale bylo by to delší a snáze by se v tom udělala chyba. Převod na sekundy a zpět normalizaci zařídí zadarmo.

Direktiva %02i znamená „vypiš aspoň na dvě místa a chybějící doplň nulami“. Bez nuly by %2i doplňovalo mezerami.

Úkol 26

Vytvořte výčtový typ Weekday a naprogramujte is_weekend, weekday_print a weekday_next.

typedef enum
{
	MON, TUE, WED, THU, FRI, SAT, SUN,
	DAY_COUNT           // posledni konstanta se rovna poctu dnu
} Weekday;

int is_weekend(Weekday d)
{
	switch (d)
	{
		case MON: case TUE: case WED: case THU: case FRI: return 0;
		case SAT: case SUN: return 1;
		case DAY_COUNT: return 0;
	}
	return 0;
}

void weekday_print(Weekday d)
{
	switch (d)
	{
		case MON: printf("pondeli"); break;
		case TUE: printf("utery");   break;
		case WED: printf("streda");  break;
		case THU: printf("ctvrtek"); break;
		case FRI: printf("patek");   break;
		case SAT: printf("sobota");  break;
		case SUN: printf("nedele");  break;
		case DAY_COUNT: printf("???"); break;
	}
}

//
// diky DAY_COUNT nemusime nikde psat sedmicku
//
Weekday weekday_next(Weekday d)
{
	return (Weekday)((d + 1) % DAY_COUNT);
}

Vypustíme-li ze switch například větev pro SUN, gcc s -Wall ohlásí

warning: enumeration value 'SUN' not handled in switch [-Wswitch]

Když doplníme default. Varování zmizí. Překladač totiž vidí, že je ošetřena každá možnost, a přestane zkoumat, které konstanty jsou vyjmenovány. Přesně proto default u switch nad výčtem nepíšeme — přijdeme tím o nejužitečnější kontrolu, kterou nám překladač nabízí.

Zde je DAY_COUNT ošetřen zvlášť právě proto, aby default nebyl potřeba. Za switch je ještě return 0, protože překladač nemá jak vědět, že do Weekday nikdo nepřiřadí nesmyslnou hodnotu.

Úkol 27

Naprogramujte pomocí podmíněného operátoru (bez if) funkce clamp a sign.

int clamp(int x, int lo, int hi)
{
	return x < lo ? lo : (x > hi ? hi : x);
}

int sign(int x)
{
	return x < 0 ? -1 : (x > 0 ? 1 : 0);
}

Obě funkce potřebují rozlišit tři případy, a podmíněný operátor umí vybrat jen ze dvou. Zanoření je proto nutné. Varianta s if:

int sign_if(int x)
{
	if (x < 0) { return -1; }
	if (x > 0) { return 1; }
	return 0;
}

Rozhodnout, která variante je čitelnější, je obecně věc vkusu. Tady má if navrch: tři případy jsou pod sebou a nejsou zanořené, takže je na první pohled vidět, že jsou vyčerpány všechny možnosti. Právě proto text doporučuje podmíněné operátory nezanořovat.

Vnitřní závorky nejsou nutné — podmíněný operátor je asociativní zprava, takže a ? b : c ? d : e se čte jako a ? b : (c ? d : e). Psát je se ale vyplatí, protože bez nich musí čtenář pravidlo znát.

Existuje i zápis bez jakéhokoliv větvení:

int sign_branchless(int x) { return (x > 0) - (x < 0); }

Oba výrazy v závorkách dají 0 nebo 1, takže rozdíl vyjde −1, 0, nebo 1. Je to elegantní a v knihovnách se to používá (viz porovnávací funkce v kapitole Funkce), ale pro čtenáře je to hlavolam.

Úkol 28

Funkce count_digits vrací pro jeden konkrétní vstup špatný výsledek. Najděte ho, vysvětlete proč a opravte.

Chybným vstupem je nula. Funkce pro n == 0 vrátí 0, ačkoliv číslo 0 má jednu číslici.

Příčinou je, že podmínka n > 0 se testuje před prvním průchodem tělem. Pro n == 0 není splněna hned napoprvé, tělo se neprovede ani jednou a count zůstane na nule.

Nejčistší oprava je použít cyklus, který tělo provede vždy aspoň jednou:

int count_digits(int n)
{
	int count = 0;
	do
	{
		count += 1;
		n /= 10;
	} while (n > 0);
	return count;
}

Alternativou je ošetřit nulu zvlášť:

int count_digits(int n)
{
	if (n == 0) { return 1; }
	int count = 0;
	while (n > 0) { count += 1; n /= 10; }
	return count;
}

Obojí je správně; varianta s do–while je kratší a nemá zvláštní případ. Je to zároveň přesně ta situace, pro kterou do–while existuje: tělo se má provést aspoň jednou.

Pro záporná čísla funkce stále vrací 0 — zadání ale mluví o nezáporných číslech, takže to podle poznámky k úkolům řešit nemusíme.

Paměť

Pointery

Z pohledu programu se pamět jeví jako pole bajtů, kde indexu říkáme adresa. Je-li v paměti uložen nějaký objekt, jsou jeho jednotlivé bajty uloženy za sebou, přičemž první bajt je uložen na nejmenší adrese. Za adresu vícebajtového objektu považujeme právě adresu jeho prvního bajtu.

Adresní prostor je množina adres a je typicky dána velikostí adresy. Je-li například adresa 64-bitová, jak tomu je na moderních počítačích, obsahuje adresní prostor adresy od 0 do 2^64 - 1. Díky virtuální paměti pracuje každý proces s vlastním adresním prostorem: tatáž adresa znamená v různých procesech různé místo ve fyzické paměti a jeden proces se k paměti druhého nedostane. Naprostá většina adres z tohoto prostoru ovšem platná není. Operační systém procesu zpřístupní jen ty oblasti, o které si řekne. Přístup na kteroukoliv jinou adresu program ukončí (např. chybou segmentation fault).

Základní operace, které můžeme s pamětí dělat, je zjištění adresy na níž je konkrétní objekt (např. proměnná). To se provádí pomocího unárního prefixového operátoru adresy: &.

int x = 10;
printf("%p\n", (void*)&x);  // vypiseme adresu, na ktere je promenna x, 
                            // %p vyzaduje typ void* o tom pozdeji

Pomocí %p vypisuje printf adresy, obvykle v hexadecimální soustavě.

Adresu můžeme uložit i do proměnné, k tomu ale potřebujeme znát její typ. Ke každému existujícímu typu z je v programu automaticky i typ adresa, na které je uložena hodnota typu z. Proměnným a často i hodnotám tohoto nového typu programátoři říkají pointer na z. Klíčovým slovem pro tento nový typ je pak z*.

// typ 'pointer na int' zapisujeme int*
int x = 10;
int* ptr_x = &x; 

Při definici pointeru se znak * ve skutečnosti váže ke jménu proměnné, to se ovšem projevuje pouze při definici více proměnných na jednom řádku. To ovšem v kurzu nepovažujeme za dobrý styl a nebudeme to dělat. Stejně tak mohou být před * bílé znaky.

Je nutné si uvědomit, že samotná proměnná ptr_x je v paměti a operátorem adresy lze zjistit na jaké adrese.

int x = 10;
int* ptr_x = &x; 
int** ptr_ptr_x = &ptr_x;

Druhou základní operací je přístup k hodnotě na dané adrese. To se dělá pomocí unárního prefixového operátoru dereference: *. Tímto způsobem můžeme hodnotu číst i na dané místo zapisovat.

int x = 10;
int* ptr_x = &x; 
printf("%i\n", *ptr_x); // vytiskne 10
*ptr_x = 20;            // zapise na adresu ptr_x hodnotu 20
printf("%i\n", x);      // vytiskne 20

Je nutné si uvědomit, že k přístupu k hodnotě na dané adrese je nutné znát typ této hodnoty. To je zajištěno typovým systémem, kdy je v typu pointeru zachycen typ hodnoty na dané adrese.

Dereferencování pointeru, které vede k přístupu na adresu paměti, která není programu přidělena, vede k nedefinovanému chování. O takovém pointeru řekneme, že je neplatný. Obecně nelze testovat, jesli je pointer neplatný. Existuje ovšem jedna výjimka a tou je adresa 0 (jakéhokoliv typu). Ta je vždy neplatná a jako obecná pravdivostní hodnota odpovídá nepravdě. Je proto dobrým zvykem udržovat hodnoty neplatných pointerů rovny 0. V cizím kódu se místo 0 často píše makro NULL z stddef.h. Znamená to totéž a lze je pro účely testování platnosti pointeru zaměňovat. (Nemusí ovšem být totožné, NULL nemusí mít všechny bity rovny 0.)

// pokud udrzujeme neplatne pointery rovny 0, nasledujici nevede k nedefinovanemu chovani
int* ptr_x = 0;

// nejaky kod, ktery mozna manipuluje s ptr_x

if (ptr_x) 
{ 
	printf("%i\n", *ptr_x);
}

Pole, pointery a pointerova aritmetika

Jméno pole se ve výrazech konvertuje na pointer na jeho první prvek (tzv. degenerace). Výjimkou je sizeof a operátor &.

int a[] = { 0,1,2,3,4,5 };
int* ptr_3 = a;
printf("a[0] = %i\n", *a);   // tiskne 0
*ptr_3 = 10;
printf("a[0] = %i\n", a[0]);  // tiskne 10

Pointer je konstatní, nelze tak měnit jeho hodnotu.

int b[] = { 10, 20 };
a = b;  // Chyba: a neni lvalue

Při předání pole jako argumentu funkce pole degeneruje na pointer.

void fce(int array[]) 
{
	// array je typu int*
	// size je tedy sizeof(int*) / sizeof(int)
	int size = sizeof(array)/sizeof(array[0]); 
	printf("ve funkci: %i\n", size);
}

int main() 
{
	int a[] = { 0,1,2,3,4,5 };
	int size = sizeof(a) / sizeof(a[0]);
	printf("v mainu: %i\n", size);
	fce(a);
}

Kvůli degeneraci tak vlastně můžeme do funkce předat přímo pointer.

void fce(int* array) 

Jak je to s přístupem k prvkům pole? To řeší pointerová aritmetika. Protože pole je v paměti v souvislém bloku, lze adresu prvku na indexu i získat tak, že adresu začátku pole zvětšíme o i-krát velikost jednoho prvku pole. Tím se v paměti posuneme z prvního bajtu prvku na indexu 0 na první bajt prvku na indexu i. V programu počet bajtů, o které je nutno se posunout nemusíme počítat, aritmetické operace s pointery to udělají za nás. K pointeru lze totiž přičíst, nebo od něj odečíst, celé číslo. Výsledkem přičtení čísla c k pointeru ptr je pointer, který získáme zvětšením ptr o c-krát velikost typu, na který ptr ukazuje. Pokud by byl například ptr pointer na int, tak bychom je zvětšili o c-krát sizeof(int). Odečítání celého čísla funguje analogicky.

int a[] = {0,1,2};
printf("velikost int: %zu\n", sizeof(int));
for (int i = 0; i < 3; i += 1) 
{
	printf("index: %i, adresa: %p\n", i, (void*)(a + i));
}

Pointerové aritmetiky v kombinaci s operátorem dereference můžeme využít pro přístup k prvkům pole. Použití [] je totiž jenom syntaktický cukr pro pointerovou aritmetiku a následnou dereferenci.

int array[] = { 1, 2, 3, 4 };
int* ptr = array + 1;    
printf("array[2] = %i\n", array[2]);      // vytiskne 3
printf("*(array+2) = %i\n", *(array+2));  // vytiskne 3
printf("ptr[1] = %i\n", ptr[1]);          // vytiskne 3

Operátor dereference má větší prioritu než sčítání, je proto nutné použít závorky. Protože je navíc operátor sčítání komutativní, je korektní i následující kuriózní přístup k prvku na indexu 2.

printf("array[2] = %i\n", 2[array]);        // vytiskne 3

Všimněme si také přístupu pomocí pointeru ptr. Ten ukazuje, že pole v C skutečně jsou adresou prvního prvku a současně každý pointer můžeme chápat jako adresu prvního prvku v poli (i když je toto pole vlastně jednoprvkové). Přístup k jednotlivým prvkům je potom realizován pomocí pointerové aritmetiky. V našem případě se můžeme na ptr dívat jako na pole, která začíná druhým prvkem array.

Pointery stejného typu můžeme od sebe odečíst, výsledkem je čísle, jehož absolutní hodnota je rovna počtu prvků daného typu, který se mezi obě adresy vejde. Je to tedy rozdíl adres (v bajtech) dělený velikostí typu (je použito celočíselné dělení). Výsledek je typu ptrdiff_t: je to znaménkový celočíselný typ, jehož šířka závisí na implementaci (může to bý například long). Operace je definována pouze pro adresy ukazující do stejného pole, jinak výsledek není definován.

Pointery také můžeme mezi sebou porovnávat.

Pointery a struktury

Pointeru na strukturu lze použít k přístupu k položkám struktury s pomocí operátoru ->, alternativně lze použít dereference následované operátorem . (tečka).

typedef struct 
{
	int pol1;
	float pol2;
} My_Str;

My_Str str = {10, 13.1};
My_Str* ptr = &str;
// oba radky vytisknou totez
printf("polozka1: %i, polozka2: %f\n", ptr->pol1, ptr->pol2);
printf("polozka1: %i, polozka2: %f\n", (*ptr).pol1, (*ptr).pol2); 

Operátory adresy a dereference mají menší prioritu než operátor tečky, proto jsme v příkladu museli použít závorky. V opačném případě bychom udělali dvě chyby: přistupovali bychom k položce struktury pomocí tečky a přitom ptr je pointer na strukturu; a dereferencovali bychom int a float. Naopak, při použití operátorů adresy a dereference na položky struktury závorky můžeme vynechat (můžeme je psát kvůli čitelnosti, nebo “pro jistotu”).

printf("adresa prvni polozky: %p\n", (void*)&str.pol1);

Struktura nemůže obsahovat položku stejného typu jako je sama. Může ovšem obsahovat položku, která je pointerem na typ, které je sama. Toho často využíváme při kostrukci datových struktur, například spojového seznamu.

typedef struct _node 
{
	int key;               // data v uzlu
	struct _node* next;    // pointer na dalsi prvek v seznamu 
} Node;


Node storage[10];
for(int i = 0; i < 10; i += 1) 
{
	// naplnime seznam
	storage[i].key = i;
	if (i < 9) 
	{
		storage[i].next = &storage[i+1];
    }
    else 
	{
		storage[i].next = 0;       // posledni prvek nema souseda
    }
}

// projdeme seznam a vytiskneme jej
Node *first = storage;
while(first) 
{
	printf("%i ", first->key);
	first = first->next;
}
printf("\n");

Parametry předané odkazem

Už víme, že pokud předáme funkci pole jako argument a funkce změní některé jeho prvky, zachovají se tyto změny i po skončení funkce na místě, ze kterého jsme ji volali. Děje se tak proto, že volané funkci nepředáváme jako argument pole, ale jeho adresu.

Tohoto mechanismu můžeme využít i pro jiné argumenty než pole, pokud chceme umožnit, aby funkce mohla změnit jejich hodnotu. Stačí předat adresu argumentu, místo jeho hodnoty. Toho se často používá, potřebujeme-li z funkce vrátit více než jednu hodnotu.

// funkce pro celociselne deleni
// argument rem je predan odkazem, je to adresa, na kterou funkce zapisuje

int division(int a, int b, int* rem) {
	*rem = a % b;
	return a / b;
}

int main() {
	int x = 0;
	int y = 0;
	x = division(10, 3, &y);  // tady predame adresu promenne y
	printf("vysledek %i, zbytek %i\n", x, y); // vypise: vysledek 3, zbytek 1
}

void pointer, přetypování

V jazyce C existuje pointer bez informace o typu hodnoty, na kterou pointer ukazuje. To je užitečné například v situaci, kdy pracujeme čistě s pamětí (a nepotřebujeme znát typ hodnot tam uložených). Například při kopírování, porovnávání apod. Typ takového pointeru je void*, programátoři mu říkají void pointer.

Void pointer lze implicitně přetypovat na pointer jakéhokoliv typu, a pointer jakéhokoliv typu lze implicitně přetypovat na void pointer. Přetypování mezi ostatními typy pointerů je nutno provádět explicitně (jinak překladač vypíše warning nebo chybu).

Příkladem jednoduché funkce pracující s pamětí je tisk jejího obsahu na obrazovku. Jako argumenty funkce postačuje adresa začátku paměti a její velikost v bajtech.

void dump_mem(void *mem, size_t size); 

Pamět budeme tisknout po bajtech do tabulky, která bude mít na každém řádku 8 bajtů. Jeden bajt vytiskneme jako dvouciferné hexadecimální číslo (zamyslete se proč je to vhodné). Ve funkci využijeme toho, že typ unsigned char má vždy velikost jeden bajt. Na začátku funkce (implicitně) přetypujeme pointer mem na pointer na unsigned char a můžeme se na pamět dívat jako na pole bajtů. Poté stačí toto pole projít a vytisknout.

void dump_mem(void *mem, size_t size) {
	unsigned char *bytes = mem; // tady pretypujeme
	for (size_t i = 0; i < size; i += 1, bytes += 1) {
		if (i && !(i % 8)) {
			printf("\n"); 
		}
		printf("%.2X ", *bytes); 
  }
  printf("\n");
}

Chceme-li funkci použít, musíme získat její argumenty: získat adresu začátku paměti a její velikost.

int x = 300;
dump_mem(&x, sizeof(x));  // tady dochazi k implicitnimu pretypovani int* na void*

float f = 2.4;
dump_mem(&f, sizeof(f));

struct { char c; int a; } s = {'x', 55 };
dump_mem(&s, sizeof(s));

char a[] = "ahoj svete";
dump_mem(a, strlen(a) + 1);

double b[5] = { 1.1, 2.2, 3.3, 0, 4.4 };
dump_mem(b, sizeof(b));

Přetypování z void* na unsigned char* je vždy bezpečné. Jinde může dojít k nedefinovanému chování. Prvním důvodem jsou tzv. trap reprezentace. Mohou existovat typy, kde ne každý obsah paměti odpovídá platné hodnotě daného typu. Takový obsah paměti, který neodpovídá platné hodnotě je past: pokus o dereferenci vede k nedefinovanénu chování. Podle standardu je jediný typ, který zaručeně nemá trap reprezentaci, právě unsigned char. Ostatní typy trap reprezentaci mít mohou, ale nemusí. Dalším problémem může být tzv. zarovnání (anglicky alignment): na některých architekturách je vyžadováno, aby vícebajtové objekty začínaly na adrese s určitou vlastností, například na adrese dělitelné velikostí objektu. Dereference nezarovnané adresy vede k nedefinovanému chování.

Pro explicitní konverze mezi pointery jiných typů existují ve standardu pravidla (strict aliasing rules), která svým rozsahem do kurzu nepatří. Doporučuji se prozatím takovým konverzím vyhýbat.

Některé funkce ze standardní knihovny

Pro manipulaci s pamětí lze využít některé funkce ze string.h. Jsou to zejména:

  • void* memcpy(void* dest, void* src, size_t count)

    Zkopíruje count bajtů z adresy src na adresu dest. Oblasti paměti odkud a kam se kopíruje se nesmí překrývat.

  • void* memmove(void* dest, void* src, size_t count)

    Zkopíruje count bajtů z adresy src na adresu dest. Oblasti paměti odkud a kam se kopíruje se mohou překrývat.

  • void* memset(void* dest, int ch, size_t count)

    Zkopíruje hodnotu (unsigned char)ch do count bajtů paměti začínajících adresou dest.

  • int memcmp(void* lhs, void* rhs, size_t count)

    Lexikograficky porovná prvních count bajtů paměti na adresách lhs a rhs. Vrací znaménko rozdílu mezi hodnotami prvních dvou bajtů na kterých se lhs a rhs liší. Pokud takové bajty neexistují, vrací 0.

Další podrobnosti o zmíněných funkcích si čtenář nastuduje z referenční příručky. Důležité je podívat zejména okrajové chování (např. když je některý argument 0). Skutečné hlavičky obsahují ještě klíčové slovo const, ke kterému se vrátíme v kapitole Další.

Životnost objektů za běhu programu

Pro nás význam: kdy je objekt v paměti během běhu programu? (anglicky storage duration). Když je pamět objektu přidělena, říkáme, že je pro něj alokována. Uvolnění paměti objektu někdy říkáme dealokace.

Existují tři typy životnosti, statická, automatická a manuální. (Manuální životnosti se někdy také říká alokovaná, od C11 přibyla životnost pro vlákna, ale tou se zabývat nebudeme.)

Pro objekty se statickou životností je paměť alokována při startu programu, uvolněna na konci běhu programu. O její alokaci je rozhodnuto při překladu programu. Statickou životnost mají zejména

  • objekty definované na úrovni souboru, například globální proměnné, složené literály.
  • lokální proměnné definované se storage class static,
  • řetězcové literály (globální i uvnitř funkcí).

Objekty lze inicializovat na hodnotu známou v době překladu. Bez explicitní inicializace v kódu jsou automaticky inicializovány na 0.

Storage class static se u proměnné specifikuje při definici, píše se na první místo. Proměnná si uchovává hodnotu mezi jednotlivými zavoláními funkce.

int fce() {
	static int count;  // inicializace na 0 pri startu programu
	count += 1;
	return count;      // vraci kolikrat byla zavolana
}

Řetězcové literály jsou pouze ke čtení, pokus o jejich změnu vede k nedefinovanému chování. Mohou být uloženy tak, že se překrývají a sdílejí svůj konec.

char *r = "retezcovy literal";
r[0] = 'x';  // nedefinovane chovani

Složené literály lze měnit.

typedef struct {
	float x;
	float y;
} vec2f;

// globalni promena
vec2f *ptr = &(vec2f){.x=3.2f, .y=2.1f};

// nekde uvnitr funkce
ptr->x = 2.4;   // OK

Alokace a dealokace objektů s automatickou životností se děje automaticky za běhu programu. Mezi objekty s automatickou životností patří

  • lokální proměnné, které nemají storage class static,
  • lokální složené literály,
  • dočasné objekty vytvořené jako návratové hodnoty funkcí (navíc jsou pouze ke čtení)

Objekty typicky vznikají v momentě vstupu do bloku, ve kterém jsou definovány a zanikají při výstupu z tohoto bloku. Lokální proměnné nejsou automaticky inicializovány, je nutné je inicializovat explicitně. Je chyba vracet z funkce adresu objektu s automatickou životností, objekt je totiž po skončení funkce dealokován. Dereference adresy pak vede k nedefinovanému chování.

int* fce() {
  int array[] = {1,2,3};
  return array;            // chyba !!!!!
}

// nekde v kodu
int *a = fce();
a[0] = 5;    // nedefinovane chovani

K manuální správě paměti aplikační programátor většinou využije nejakou knihovnu, kde je implementován alokátor paměti (ten se stará o přidělování paměti, který ji sám získává od operačního systému, případně spravuje paměť se statickou životností). Rozhraní alokátoru ze standardní knihovny je dáno funkcemi z hlavičkového souboru stdlib.h.

  • alokační funkce malloc, calloc,
  • dealokační funkce free,
  • realokační funkce realloc.

Princip použití alokačních funkcí je následující: funkci předáme počet bajtů, které chceme alokovat. Funkce vrátí adresu prvního bajtu souvislého kusu paměti požadované velikosti. V případě, že se alokace nepovede, vrací funkce 0.

Funkci free předáme jako argument adresu, kterou někdy předtím vrátila některá alokační nebo realokační funkce, a která ukazuje na doposud neuvolněnou paměť.free tuto paměť uvolní. Pokud jí předáme 0, nedělá free nic. Pokud jí ovšem předáme jakoukoliv jinou adresu, vede to nedefinovanému chování.

Funkce realloc umožňuje změnit velikost již alokované paměti.

Podrobnosti k jednotlivým funkcím si čtenář najde v referenční příručce.

Alokace paměti je první operace, se kterou jsme se setkali a která může za běhu selhat, aniž by na tom bylo cokoliv nelegálního. Její návratovou hodnotu je proto potřeba kontrolovat. Nejjednodušší způsob, jak to udělat, je makro assert z hlavičkového souboru assert.h.

#include <assert.h>
#include <stdlib.h>
 
int *array = malloc(sizeof(int) * 10);
 
//
// pokud je array nulovy pointer, program se ukonci
// a vypise, ve kterem souboru a na kterem radku k tomu doslo
//
assert(array);

Makro dostane jeden výraz. Je-li výraz pravdivý, neudělá assert nic a program pokračuje. Je-li nepravdivý, vypíše se na chybový výstup zpráva s názvem souboru, číslem řádku a zněním výrazu a program se okamžitě ukončí.

Je-li při překladu definováno makro NDEBUG, všechna volání assert z programu zmizí. Toho se využívá pro finální, odladěnou verzi programu, u které už kontroly nechceme platit. Jak přesně to udělat je v kapitole Preprocesor. Z toho plyne pravidlo, které je nutné dodržet bez výjimky: do assert nikdy nedávejte výraz s vedlejším účinkem. Zmizí-li totiž assert, zmizí i ten vedlejší účinek.

Primárně slouží assert pro odhalování chyb programátora, tedy situací, o kterých tvrdíme, že nastat nemohou. Například ověření předpokladů na začátku funkce nebo o kontrolu, že jsme někde nespočítali nesmysl.

void set_element(int *array, int n, int index, int value)
{
	//
	// tvrdime, ze funkce nikdy nebude volana se spatnym indexem
	// pokud bude, chceme se to dozvedet hned
	//
	assert(index >= 0 && index < n);
 
	array[index] = value;
}

Naopak se assert nehodí na situace, které za běhu legitimně nastat mohou: neexistující soubor, neplatný vstup od uživatele, nedostatek paměti. Ty je potřeba ošetřit a nějak na ně zareagovat, ne program ukončit. (Během vývoje můžeme dát assert i na místa, kam ošetření chyb doplníme později.)

V tomto textu assert používáme i nesprávně v situacích, na které se nehodí. Je to proto, aby příklady kódu zůstaly krátké. Makro lze ze stejného důvodu používat i při řešení úkolů.

Chyby při práci s pamětí — zápis za konec pole, použití uvolněné paměti, dvojí free — překladač neohlásí. Odhalí je ale AddressSanitizer, který stačí zapnout při překladu:

gcc -std=c17 -Wall -Wextra -g -fsanitize=address program.c -o program

Program pak při chybě vypíše, o jakou chybu šlo a na kterém řádku k ní došlo. Podrobněji v kapitole Další.

Nyní následují příklady použití typického použití alokačních funkcí a častých chyb.

int m = 20;
int* array = malloc(m * sizeof(int)); // alokace pro pole int velikosti m
assert(array);  // pro ucely kurzu staci tato kontrola, 
                // obecne muze byt potreba komplikovanejsi test uspechu alokace

// pracuj s polem array

free(array);   // uvolnime pamet
array = 0;     // nastavime pointer na neplatny

Funkce realloc muze při změně velikosti alokovane paměti přesunout její obsah na jinou adresu a původní paměť uvolnit.

int realloc_size[] = {10, 12, 512, 32768, 65536, 32768};
int m = sizeof(realloc_size)/sizeof(realloc_size[0]);
int *next = 0;

for (int i = 0; i < m; i += 1) {
	int *ret = realloc(next, sizeof(int) * realloc_size[i]);
    assert(ret);
    printf("%p -> %p\n", next, ret);   // pokud dojde k presunu, vypisi se ruzne adresy
    next = ret;
}
free(next);

Mezi hlavní chyby při manuální správě paměti patří tzv. memory leak a double free. První chyba nastane tak, že v programu zapomeneme adresu alokované paměti bez toho, abychom ji uvolnili. Tím pádem už ji nikdy uvolnit nemůžeme. Program má pak tuto paměť alokovánu zbytečně. Ke druhé chybě dojde tak, že se alokovanou paměť pokusíme uvolnit dvakrát (nebo vícekrát), mimo první pokus to vede k nedefinovanému chování.

Na závěr dodejme, že po skončení programu všechnu jeho paměť uvolní operační systém.

Existují také jiné přístupy ke správě paměti, které nahrazují nebo vylepšují manuální správu, například tzv. počítání referencí nebo garbage kolektory. Ty jsou v určité formě pro jazyk C přístupné jako knihovny (např.Boehm garbage collector).

Úkoly

  1. Napište funkci swap_int, která prohodí hodnoty dvou proměnných typu int.

  2. Naprogramujte vlastní verze knihovních funkcí strlen a strcmp (string.h), které nepoužijí indexů, ale pouze pointerové aritmetiky a dereference.

  3. Vytvořte strukturu s několika položkami a aspoň jednu její proměnnou. Vypište informace o layoutu této proměnné: pro každou položku vypište její adresu a velikost. Vypište i adresu a velikost samotné proměnné.

  4. Naprogramujte funkci pro výpočet průměru a mediánu pole čísel (např. float). Jednu z hodnot vraťte pomocí argumentu předaného odkazem. (Funkce může změnit pořadí prvků v poli.)

  5. Napište funkci, která v poli řetězců najde znak, který se vyskytuje v nejvíce řetězcích v tomto poli.

  6. Uvažujme následující strukturu pro řetězec. Položka data není zakončená 0, místo toho si pamatuje svoji délku v položce len.

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

    Řětězce budeme vytvářet pomocí funkce (není nutno se zabývat jejím tělem, zejména ne funkcí malloc).

    String clone_from_cstring(char *cstring)
    {
        String ret = {0};
        ret.len = strlen(cstring);
        if (ret.len == 0) { ret.data = 0; }
        else 
        {
            ret.data = malloc(ret.len);
            assert(ret.data);
            memcpy(ret.data, cstring, ret.len);
        }
        return ret;
    }
    

    Naprogramujte následující funkce

    String get_prefix(String str, char delimiter, String* remainder);
    

    Funkce vrátí prefix řetězce str až do znaku delimiter, který ovšem do výsledku nepatří. Do argumentu remainder zapíše zbytek řetězce bez delimiteru. Tedy pokud spojíme návratovou hodnotu, delimiter, pokud jej str obsahuje, a remainder dostaneme str (vše chápáno jako řetězce). Pokud je první symbol str roven delimiter, vrací funkce prázdný řetězec. Pokud str delimiter neobsahuje, je výsledkem celý řetězec. Funkce v tomto případě musí nastavit remainder na prázdný řetězec. Funkce vytváří novou strukturu string, ovšem nealokuje novou pamět pro pole typu char, položka data je ukazatelem do pole, na které ukazuje položka str.data. Stejně tak pro remainder.

    int string_to_long(String s, long* result);
    

    Funkce převede číslo zapsané jako řetězec na long. Předpokládáme kódování v desítkové soustavě. Číslo může být záporné. Pokud je s prázdný, je výsledkem převodu 0. Výsledné číslo je zapsáno do argumentu result. Funkce vrací 1, pokud se povedlo číslo z řetězce převést, jinak vrací 0.

    int compute_the_thing(String s, long* result);
    

    s je text, který obsahuje na každém řádku mezerami oddělená celá čísla. Funkce spočítá číslo, které dostane následovně: čísla v každém řádku sečte; výsledek dostane tak, že sečte součty sudých řádků a od toho odečte součet součtů lichých řádků. Toto číslo zapíše do result. Pokud se část řetězce nepovede převést na číslo, vrátí funkce 0, jinak vrací 1. Například, pokud je s

    2 2
    2 2 2
    -2 3
    

    jsou součty řádků 4, 6, 1 a výsledek je -4 + 6 -1 = 1.

  7. Naprogramujte funkci Token parse(char string[]), určenou k převodu prvního slova řetězce string na hodnotu. (První slovo je do bílého znaku nebo do konce řetězce). Typ Token (který také definujte) přitom musí být technikou tagged unionu schopen reprezentovat následující případy

    • je-li první slovo desetinným číslem, pak je hodnota typu double
    • je-li první slovo celý číslem, pak je hodnota typu int
    • prvním slovem je jedno z klíčových slov if for while
    • string je prázdný
    • nastala chyba (situace nepokryté předchozími body)
  8. Upravte funkci dump_mem tak, aby na začátku každého řádku vypsala adresu prvního bajtu na tomto řádku. Přidejte možnost vypisovat bajty jako posloupnost 8 bitů (je nutné využít bitových operátorů).

  9. Naprogramujte vlastní verze funkcí memcpy, memmove, memcmp, memset. Funkce mohou být implementovány naivně.

  10. Najděte způsob, jak bez použití sizeof programově zjistit velikost nějakého typu, například float.

  11. Napište funkci pro detekci endianity a pro převod mezi big endian a small endian (například pro typ unsigned int, nebo obecnou funkci). Wiki o endianitě.

  12. Implementujte funkci, která vrátí nově alokovaný řetězec, jehož obsah vznikne spojením dvou řetězců předaných funkci jako argumenty.

  13. Implementuje zásobník pomocí dynamického pole.

    typedef struct 
    {
    	int *data;    // pole pro vlozena data
    	int top;      // pocet vlozenych prvku
    	int cap;	  // velikost pole data
    } Stack;
      
    Stack create_stack() 
    {
         return (Stack){0};
    }
    

    Doprogramujte operaci push tak, aby v momentě, kdy je zásobník zaplněn, tato operace realokovala položku data na dvojnásobnou velikost.

    Doprogramujte operaci pop tak, aby v momentě, kdy je zásobník zaplněn z jedné čtvrtiny, zmenšila položku data na polovinu.

První nenulovou velikost zásobníku vyberte jako malou mocninu 2, např. 16.

Doprogramujte funkci destroy_stack, pro uvolnění alokované paměti zásobníku.

  1. Vytvořte strukturu struct matrix pro matici desetinných čísel (double), s položkami pro rozměry matice (počet řádků a sloupců) a data (obsah matice). O vnitřním uložení a adresování jednotlivých prvků matice rozhodněte sami. Nejjednodušší je uložit data do jednorozměrného pole a index počítat jako radek * cols + sloupec.

    Naprogramujte funkce:

    // 
    // Funkce nastaví v `m` položky pro rozměry matice a alokuje pamět pro  data.
    //
    void allocate_matrix(struct matrix *m, int rows, int cols);
    
    //
    // Funkce inicializuje obsah podle pole `array` v row-major pořadí:
    // řádky matice jsou naskládány za sebe.  Můžete
    // předpokládat, že `array` je dostatečně velké.
    //
    void initialize_matrix_from_array(struct matrix *m, double *array);
    
    //
    // Funkce dealokuje paměť a nastaví odpovídající pointery na 0.
    //
    void destroy_matrix(struct matrix *m);
    
    //
    // Funkce vynásobí matice (předpokládejme, že vynásobit jdou). Pro data
    // výsledné matice alokuje novou paměť.
    //
    struct matrix multiply_matrix(struct matrix a, struct matrix b);
    
    //
    // Funkce vytiskne matici jako tabulku.
    //
    void print_matrix(struct matrix a);
    
  2. Naprogramujte následující funkci.

    char** split_string(char *str, char delimiter, int *n);
    

    Funkce rozdělí řetězec str na části oddělené znakem delimiter, ten přitom do žádné části nepatří. Funkce vrátí takto vzniklé řetězce v nově alokovaném poli, každý z jeho prvků je klonem části původního řetězce, tj. jeho paměť je
    nově alokována. Počet vzniklých řetězců zapíše na adresu n.

    Příklady rozdělení řetězce (delimiter je '-')

    "ahoj-svete"       -> { "ahoj", "svete" }
    "ahoj--svete"      -> { "ahoj", "", "svete" }
    "-ahoj-svete"      -> { "", "ahoj", "svete" }
    "ahoj-svete-"      -> { "ahoj", "svete", "" }
    "ahoj"             -> { "ahoj" }
    
  3. Naprogramujte spojový seznam celých čísel, jehož uzly jsou alokovány pomocí malloc.

    typedef struct _node
    {
    	int key;
    	struct _node *next;
    } Node;
    
    Node *list_push_front(Node *head, int key);  // vraci novou hlavu
    Node *list_find(Node *head, int key);        // vraci uzel s klicem, jinak 0
    Node *list_remove(Node *head, int key);      // odstrani prvni uzel s klicem
    void  list_print(Node *head);
    void  list_destroy(Node *head);              // uvolni vsechny uzly
    

    Program přeložte se sanitizerem (-fsanitize=address) a ověřte, že po zavolání list_destroy nehlásí žádnou neuvolněnou paměť.

  4. Naprogramujte strukturu, která si udržuje pole řetězců a umí ho zvětšovat.

    typedef struct
    {
    	char **items;
    	int    count;
    	int    cap;
    } String_List;
    
    void list_add(String_List *l, char *s);   // prida KLON retezce s
    void list_destroy(String_List *l);        // uvolni vse
    

    Zamyslete se nad tím, kolik volání free musí list_destroy udělat.

Řešení úkolů

Úkol 1

Napište funkci swap_int, která prohodí hodnoty dvou proměnných.

void swap_int(int *a, int *b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

// volani
int x = 1, y = 2;
swap_int(&x, &y);      // x je 2, y je 1

Funkce dostane adresy obou proměnných a zapisuje přes dereferenci. Kdyby brala int a, int b, prohodila by jenom své lokální kopie a volající by nic nepoznal.

Úkol 2

Naprogramujte vlastní strlen a strcmp pouze pomocí pointerové aritmetiky a dereference, bez indexů.

size_t my_strlen(char *s)
{
	char *p = s;
	while (*p) { p += 1; }
	return (size_t)(p - s);    // rozdil pointeru, typ ptrdiff_t
}

int my_strcmp(char *a, char *b)
{
	while (*a && *a == *b) { a += 1; b += 1; }
	return (int)(unsigned char)*a - (int)(unsigned char)*b;
}

U my_strlen je pěkně vidět, k čemu je odčítání pointerů: rozdíl adres vydělený velikostí typu dá přímo počet prvků.

U my_strcmp je podstatné přetypování na unsigned char. Norma říká, že se znaky porovnávají jako unsigned char, a na platformě se znaménkovým char by bez něj vyšlo pro bajty nad 127 porovnání opačně.

Cyklus končí, jakmile narazíme na rozdíl nebo na konec a. Jsou-li řetězce stejné, *a i *b jsou 0 a výsledek je 0.

Úkol 3

Vypište layout struktury: pro každou položku její adresu a velikost, a totéž pro celou proměnnou.

struct layout { char c; int a; double d; };

int main()
{
	struct layout L;

	printf("cela struktura: velikost %zu, adresa %p\n", sizeof(L), (void*)&L);
	printf("  c: velikost %zu, adresa %p\n", sizeof(L.c), (void*)&L.c);
	printf("  a: velikost %zu, adresa %p\n", sizeof(L.a), (void*)&L.a);
	printf("  d: velikost %zu, adresa %p\n", sizeof(L.d), (void*)&L.d);
	return 0;
}

Typický výstup na 64-bitovém unixu:

cela struktura: velikost 16, adresa 0x7ffd...130
  c: velikost 1, adresa 0x7ffd...130
  a: velikost 4, adresa 0x7ffd...134
  d: velikost 8, adresa 0x7ffd...138

Součet velikostí položek je 1 + 4 + 8 = 13, struktura má ale 16 bajtů. Mezi c a a jsou tři nevyužité bajty, protože int musí začínat na adrese dělitelné čtyřmi — to je zarovnání, o kterém mluví podsekce o void*.

Zkuste si pořadí položek změnit na char c; double d; int a; a podívat se, jak se velikost změní. Vycpávky bývá nejméně, seřadíme-li položky od největší po nejmenší.

Pozor na %p — vyžaduje void*, přetypování je proto nutné.

Úkol 4

Funkce pro výpočet průměru a mediánu pole; jednu hodnotu vraťte argumentem předaným odkazem.

double mean_and_median(double *a, int n, double *median)
{
	double sum = 0;
	for (int i = 0; i < n; i += 1) { sum += a[i]; }

	//
	// medianem je prostredni prvek setrideneho pole,
	// zadani dovoluje pole prehazet
	//
	sort_doubles(a, n);

	*median = (n % 2) ? a[n/2] : (a[n/2 - 1] + a[n/2]) / 2.0;
	return sum / n;
}

Pro pole {5,1,4,2,3} vyjde průměr 3 a medián 3.

Třídicí funkci si napište sami: stačí naivní bubble sort. ( Lze použít i qsort ze stdlib.h — ta ovšem potřebuje pointer na porovnávací funkci, což je látka z pozdější kapitoly Funkce. )

Pozor na past při testování. Následující zápis je nedefinované chování:

printf("%f %f\n", mean_and_median(arr, 5, &med), med);   // SPATNE

Norma neurčuje pořadí vyhodnocení argumentů, takže med se může přečíst dřív, než do něj funkce zapíše. Správně:

double m = mean_and_median(arr, 5, &med);
printf("%f %f\n", m, med);

Úkol 5

V poli řetězců najděte znak, který se vyskytuje v nejvíce řetězcích.

int most_common_char(char **strings, int n)
{
	int counts[256] = { 0 };

	for (int i = 0; i < n; i += 1)
	{
		//
		// v kazdem retezci zapocitame kazdy znak nejvyse jednou,
		// zajima nas v kolika RETEZCICH se vyskytuje
		//
		int seen[256] = { 0 };

		for (char *p = strings[i]; *p; p += 1)
		{
			unsigned char c = (unsigned char)*p;
			if (!seen[c]) { seen[c] = 1; counts[c] += 1; }
		}
	}

	int best = -1, bestcount = 0;
	for (int c = 0; c < 256; c += 1)
	{
		if (counts[c] > bestcount) { bestcount = counts[c]; best = c; }
	}
	return best;
}

Pomocné pole seen je tu klíčové. Bez něj bychom počítali celkový počet výskytů, nikoliv počet řetězců — a "aaa" by přebilo tři různé řetězce.

Index do pole musí být unsigned char, jinak by na platformě se znaménkovým char byl pro bajty nad 127 záporný a četli bychom mimo pole.

Úkol 6

Funkce get_prefix, string_to_long a compute_the_thing nad typem String.

String get_prefix(String str, char delimiter, String *remainder)
{
	size_t i = 0;
	while (i < str.len && str.data[i] != delimiter) { i += 1; }

	String prefix = { str.data, i };

	if (i == str.len)
	{
		//
		// delimiter se nenasel, zbytek je prazdny retezec
		//
		remainder->data = 0;
		remainder->len  = 0;
	}
	else
	{
		remainder->data = str.data + i + 1;    // preskocime delimiter
		remainder->len  = str.len - i - 1;
	}
	return prefix;
}

Žádná paměť se nealokuje — obě vrácené struktury ukazují dovnitř původního pole. To je celý smysl typu String: podřetězec je jenom dvojice (adresa, délka).

int string_to_long(String s, long *result)
{
	size_t i = 0;
	int    neg = 0;
	long   value = 0;

	if (s.len == 0) { *result = 0; return 1; }

	if (s.data[0] == '-' || s.data[0] == '+')
	{
		neg = (s.data[0] == '-');
		i = 1;
		if (s.len == 1) { return 0; }     // samotne znamenko
	}

	for (; i < s.len; i += 1)
	{
		char c = s.data[i];
		if (c < '0' || c > '9') { return 0; }
		value = value * 10 + (c - '0');
	}

	*result = neg ? -value : value;
	return 1;
}

Funkce vrací 1 při úspěchu a 0 při chybě, výsledek zapisuje odkazem. Jde o typický způsob, jak z funkce vrátit hodnotu i informaci o úspěchu.

Na přetečení value se tu nekontroluje; v ostrém kódu by bylo potřeba před každým vynásobením deseti ověřit, že se výsledek do long vejde (viz podsekci Přetečení a podtečení).

int compute_the_thing(String s, long *result)
{
	long total = 0;
	int  row   = 1;
	String rest = s;

	while (rest.len > 0 || rest.data != 0)
	{
		String line = get_prefix(rest, '\n', &rest);

		long rowsum = 0;
		String word_rest = line;
		while (word_rest.len > 0)
		{
			String w = get_prefix(word_rest, ' ', &word_rest);
			if (w.len > 0)
			{
				long v;
				if (!string_to_long(w, &v)) { return 0; }
				rowsum += v;
			}
		}

		//
		// sude radky pricitame, liche odecitame
		// prvni radek je lichy
		//
		total += (row % 2 == 0) ? rowsum : -rowsum;
		row += 1;

		if (rest.data == 0) { break; }
	}

	*result = total;
	return 1;
}

Všimněte si, jak se get_prefix používá dvakrát ve dvou úrovních: jednou na rozdělení textu na řádky, podruhé na rozdělení řádku na slova. Podmínka w.len > 0 ošetří dvě mezery za sebou.

Úkol 7

Naprogramujte funkci Token parse(char string[]) pro převod prvního slova řetězce na hodnotu; typ Token má být tagged union.

typedef enum { TT_INT, TT_DOUBLE, TT_KEYWORD, TT_EMPTY, TT_ERROR } TokenType;
typedef enum { KW_IF, KW_FOR, KW_WHILE } Keyword;

typedef struct
{
	TokenType type;
	union
	{
		int     i;
		double  d;
		Keyword kw;
	} value;
} Token;

Pro případy TT_EMPTY a TT_ERROR se žádná položka unionu nepoužívá; to nevadí, tagged union to dovoluje.

static int is_space(char c) { return c==' ' || c=='\t' || c=='\n' || c=='\r'; }

Token parse(char string[])
{
	Token t = { TT_ERROR, {0} };

	//
	// najdeme prvni slovo
	//
	size_t start = 0;
	while (string[start] && is_space(string[start])) { start += 1; }
	if (!string[start]) { t.type = TT_EMPTY; return t; }

	size_t end = start;
	while (string[end] && !is_space(string[end])) { end += 1; }

	size_t len = end - start;
	char  *w   = string + start;

	//
	// klicova slova
	//
	if (len == 2 && !memcmp(w, "if", 2))    { t.type=TT_KEYWORD; t.value.kw=KW_IF;    return t; }
	if (len == 3 && !memcmp(w, "for", 3))   { t.type=TT_KEYWORD; t.value.kw=KW_FOR;   return t; }
	if (len == 5 && !memcmp(w, "while", 5)) { t.type=TT_KEYWORD; t.value.kw=KW_WHILE; return t; }

	//
	// znamenko a celociselna cast
	//
	size_t i = 0;
	int    neg = 0;
	if (w[0]=='-' || w[0]=='+') { neg = (w[0]=='-'); i = 1; }
	if (i == len) { return t; }              // samotne znamenko je chyba

	long   ip = 0;
	size_t digits = 0;
	while (i < len && w[i] >= '0' && w[i] <= '9') { ip = ip*10 + (w[i]-'0'); i += 1; digits += 1; }
	if (digits == 0) { return t; }

	if (i == len) { t.type = TT_INT; t.value.i = (int)(neg ? -ip : ip); return t; }

	//
	// desetinna cast
	//
	if (w[i] != '.') { return t; }
	i += 1;

	double frac = 0, scale = 1;
	size_t fdigits = 0;
	while (i < len && w[i] >= '0' && w[i] <= '9') { frac = frac*10 + (w[i]-'0'); scale *= 10; i += 1; fdigits += 1; }

	//
	// musime dojit az na konec slova a za teckou musi neco byt
	//
	if (i != len || fdigits == 0) { return t; }

	double v = (double)ip + frac/scale;
	t.type = TT_DOUBLE;
	t.value.d = neg ? -v : v;
	return t;
}

Podstatná je poslední kontrola i != len. Bez ní by "1.2.3" prošlo jako 1.2, protože bychom se po zpracování desetinné části nepodívali, zda ve slově ještě něco nezbylo.

Zpracování hodnoty vždy začíná kontrolou type:

Token t = parse(text);
switch (t.type)
{
	case TT_INT:     printf("%i\n", t.value.i); break;
	case TT_DOUBLE:  printf("%f\n", t.value.d); break;
	case TT_KEYWORD: /* ... */ break;
	case TT_EMPTY:   /* ... */ break;
	case TT_ERROR:   /* ... */ break;
}

Úkol 8

Upravte dump_mem tak, aby vypisovala adresu prvního bajtu na řádku, a přidejte možnost vypisovat bajty jako posloupnost bitů.

void dump_mem(void *mem, size_t size, int as_bits)
{
	unsigned char *bytes = mem;

	for (size_t i = 0; i < size; i += 1)
	{
		if (i % 8 == 0) { printf("%p  ", (void*)(bytes + i)); }

		if (as_bits)
		{
			for (int b = 7; b >= 0; b -= 1) { printf("%i", (bytes[i] >> b) & 1); }
			printf(" ");
		}
		else
		{
			printf("%.2X ", bytes[i]);
		}

		if (i % 8 == 7) { printf("\n"); }
	}
	if (size % 8) { printf("\n"); }    // dokoncime neuplny radek
}

Adresu tiskneme na začátku každého osmého bajtu. Poslední if dopíše konec řádku, pokud velikost není násobkem osmi — jinak by výpis neskončil novým řádkem.

Bitový výpis je stejný jako v úkolu 8 z kapitoly Základy: jdeme od nejvyššího bitu k nejnižšímu.

Úkol 9

Naprogramujte vlastní memcpy, memmove, memcmp a memset.

void *my_memcpy(void *dest, const void *src, size_t n)
{
	unsigned char       *d = dest;
	const unsigned char *s = src;
	for (size_t i = 0; i < n; i += 1) { d[i] = s[i]; }
	return dest;
}

void *my_memset(void *dest, int ch, size_t n)
{
	unsigned char *d = dest;
	for (size_t i = 0; i < n; i += 1) { d[i] = (unsigned char)ch; }
	return dest;
}

int my_memcmp(const void *lhs, const void *rhs, size_t n)
{
	const unsigned char *a = lhs, *b = rhs;
	for (size_t i = 0; i < n; i += 1)
	{
		if (a[i] != b[i]) { return a[i] - b[i]; }
	}
	return 0;
}

Zajímavá je jedině memmove, která musí fungovat i pro překrývající se oblasti. Kopíruje-li se doprava (dest > src), přepsali bychom při kopírování zepředu zdrojová data dřív, než je přečteme. Řešení je kopírovat v takovém případě odzadu:

void *my_memmove(void *dest, const void *src, size_t n)
{
	unsigned char       *d = dest;
	const unsigned char *s = src;

	if (d < s)
	{
		for (size_t i = 0; i < n; i += 1) { d[i] = s[i]; }
	}
	else
	{
		//
		// odzadu; idiom i-- > 0 funguje i pro bezznamenkovy typ
		//
		for (size_t i = n; i-- > 0; ) { d[i] = s[i]; }
	}
	return dest;
}

Všechny čtyři funkce převádějí void* na unsigned char*. To je jediné přetypování, které je podle normy vždy bezpečné.

Úkol 10

Najděte způsob, jak bez použití sizeof programově zjistit velikost nějakého typu.

size_t size_of_float(void)
{
	float a[2];
	return (size_t)((unsigned char*)&a[1] - (unsigned char*)&a[0]);
}

Prvky pole leží v paměti hned za sebou, takže vzdálenost mezi adresami dvou sousedních prvků je velikost typu. Přetypováním na unsigned char* docílíme toho, že se rozdíl počítá v bajtech — bez něj by odčítání float* dalo 1, protože pointerová aritmetika už dělí velikostí typu.

Totéž s pointerovou aritmetikou bez pole:

size_t size_of_float2(void)
{
	float *p = 0;
	return (size_t)((unsigned char*)(p + 1) - (unsigned char*)p);
}

Tato varianta je ovšem formálně nedefinované chování — aritmetika s nulovým pointerem není povolena, i když v praxi funguje. Právě proto sizeof existuje.

Úkol 11

Napište funkci pro detekci endianity a pro převod mezi big endian a little endian.

int is_little_endian(void)
{
	unsigned int   x = 1;
	unsigned char *b = (unsigned char*)&x;

	//
	// je-li nejmene vyznamny bajt na nejnizsi adrese, jde o little endian
	//
	return b[0] == 1;
}

//
// pro 32-bitovy typ
//
unsigned int byte_swap(unsigned int x)
{
	return ((x & 0x000000FFu) << 24)
	     | ((x & 0x0000FF00u) <<  8)
	     | ((x & 0x00FF0000u) >>  8)
	     | ((x & 0xFF000000u) >> 24);
}

Pro 0x12345678 vrátí byte_swap hodnotu 0x78563412.

Detekce funguje proto, že se na paměť díváme jako na pole bajtů — což smíme jen přes unsigned char*. Maska a posuv v byte_swap přesouvají jednotlivé bajty; všechny literály jsou bezznaménkové (u), aby se nikde nemíchaly znaménkovosti.

Obecná varianta pro libovolný typ prohodí bajty na místě:

void reverse_bytes(void *p, size_t size)
{
	unsigned char *b = p;
	for (size_t i = 0; i < size / 2; i += 1)
	{
		unsigned char t = b[i];
		b[i] = b[size - 1 - i];
		b[size - 1 - i] = t;
	}
}

Úkol 12

Implementujte funkci, která vrátí nově alokovaný řetězec vzniklý spojením dvou řetězců.

char *concat(char *a, char *b)
{
	size_t la = strlen(a);
	size_t lb = strlen(b);

	char *ret = malloc(la + lb + 1);    // +1 pro ukoncovaci 0
	assert(ret);

	memcpy(ret, a, la);
	memcpy(ret + la, b, lb);
	ret[la + lb] = 0;

	return ret;
}

Nejčastější chybou je zapomenout + 1 na ukončovací nulu. Volající funkce musí zajistit uvolnění paměti. (Toto je u funkcí potřeba dokumentovat.)

char *s = concat("ahoj ", "svete");
printf("%s\n", s);
free(s);

Úkol 13

Implementujte zásobník pomocí dynamického pole.

void push(Stack *s, int v)
{
	if (s->top == s->cap)
	{
		int newcap = (s->cap == 0) ? 16 : s->cap * 2;
		int *p = realloc(s->data, (size_t)newcap * sizeof(int));
		assert(p);                  // pri neuspechu zustava s->data platne
		s->data = p;
		s->cap  = newcap;
	}
	s->data[s->top] = v;
	s->top += 1;
}

int pop(Stack *s)
{
	assert(s->top > 0);
	s->top -= 1;
	int v = s->data[s->top];

	//
	// zmensujeme az pri ctvrtinovem zaplneni, ne pri polovicnim:
	// jinak by stridave push a pop porad realokovaly
	//
	if (s->cap > 16 && s->top * 4 <= s->cap)
	{
		int newcap = s->cap / 2;
		int *p = realloc(s->data, (size_t)newcap * sizeof(int));
		assert(p);
		s->data = p;
		s->cap  = newcap;
	}
	return v;
}

void destroy_stack(Stack *s)
{
	free(s->data);
	s->data = 0;
	s->top  = 0;
	s->cap  = 0;
}

Návratovou hodnotu realloc vždy ukládáme do pomocné proměnné. Zápis s->data = realloc(s->data, ...) by při neúspěchu přepsal původní adresu nulou a paměť by se stala nedosažitelnou.

Podmínka s->cap > 16 zabrání tomu, abychom klesli pod počáteční velikost. Zmenšování až na čtvrtině je záměr: kdybychom zmenšovali na polovině, střídavé push/pop na hranici by realokovalo při každém volání.

Po destroy_stack nastavujeme data na 0, aby se struktura dala bezpečně použít znovu a aby případné druhé volání free nic neudělalo.

Úkol 14

Struktura struct matrix a funkce pro alokaci, inicializaci, násobení, tisk a dealokaci.

struct matrix
{
	int     rows;
	int     cols;
	double *data;    // row-major: prvek (i,j) je data[i*cols + j]
};

void allocate_matrix(struct matrix *m, int rows, int cols)
{
	m->rows = rows;
	m->cols = cols;
	m->data = malloc((size_t)rows * (size_t)cols * sizeof(double));
	assert(m->data);
}

void initialize_matrix_from_array(struct matrix *m, double *array)
{
	for (int i = 0; i < m->rows * m->cols; i += 1) { m->data[i] = array[i]; }
}

void destroy_matrix(struct matrix *m)
{
	free(m->data);
	m->data = 0;
	m->rows = 0;
	m->cols = 0;
}

struct matrix multiply_matrix(struct matrix a, struct matrix b)
{
	struct matrix c;
	allocate_matrix(&c, a.rows, b.cols);

	for (int i = 0; i < a.rows; i += 1)
	{
		for (int j = 0; j < b.cols; j += 1)
		{
			double s = 0;
			for (int k = 0; k < a.cols; k += 1)
			{
				s += a.data[i*a.cols + k] * b.data[k*b.cols + j];
			}
			c.data[i*c.cols + j] = s;
		}
	}
	return c;
}

void print_matrix(struct matrix a)
{
	for (int i = 0; i < a.rows; i += 1)
	{
		for (int j = 0; j < a.cols; j += 1) { printf("%8.2f", a.data[i*a.cols + j]); }
		printf("\n");
	}
}

Data ukládáme do jednorozměrného pole a index počítáme ručně jako radek * cols + sloupec (row-major, tedy řádky za sebou). Je to jednodušší i rychlejší než pole pointerů na řádky: celá matice je v jednom souvislém bloku, takže stačí jedno malloc a jedno free.

multiply_matrix vrací strukturu hodnotou. Kopíruje se přitom jenom trojice (rows, cols, data) — data samotná zůstávají tam, kde jsou. Volající musí výsledek uvolnit destroy_matrix.

Úkol 15

Naprogramujte char **split_string(char *str, char delimiter, int *n).

static char *clone_n(char *src, size_t n)
{
	char *r = malloc(n + 1);
	assert(r);
	memcpy(r, src, n);
	r[n] = 0;
	return r;
}

char **split_string(char *str, char delimiter, int *n)
{
	//
	// pocet casti je o jedna vetsi nez pocet oddelovacu
	//
	int parts = 1;
	for (char *p = str; *p; p += 1) { if (*p == delimiter) { parts += 1; } }

	char **ret = malloc((size_t)parts * sizeof(char*));
	assert(ret);

	int   idx   = 0;
	char *start = str;
	for (char *p = str; ; p += 1)
	{
		if (*p == delimiter || *p == 0)
		{
			ret[idx] = clone_n(start, (size_t)(p - start));
			idx += 1;
			start = p + 1;
			if (*p == 0) { break; }
		}
	}

	*n = parts;
	return ret;
}

Klíčem je, že počet částí spočítáme jedním průchodem, takže pole pointerů alokujeme jednou a nemusíme ho zvětšovat. Prázdné části vznikají přirozeně: p - start je pro dva oddělovače za sebou nula.

Uvolnění je dvoustupňové a je snadné na něj zapomenout:

void free_split(char **a, int n)
{
	for (int i = 0; i < n; i += 1) { free(a[i]); }   // jednotlive retezce
	free(a);                                        // pole pointeru
}

Přeložte si řešení s -fsanitize=address a ověřte, že po free_split nehlásí žádnou neuvolněnou paměť.

Úkol 16

Naprogramujte spojový seznam celých čísel, jehož uzly jsou alokovány pomocí malloc.

Node *list_push_front(Node *head, int key)
{
	Node *n = malloc(sizeof(Node));
	assert(n);

	n->key  = key;
	n->next = head;
	return n;        // novy uzel je novou hlavou
}

Node *list_find(Node *head, int key)
{
	for (Node *p = head; p; p = p->next)
	{
		if (p->key == key) { return p; }
	}
	return 0;
}

void list_print(Node *head)
{
	for (Node *p = head; p; p = p->next) { printf("%i ", p->key); }
	printf("\n");
}

void list_destroy(Node *head)
{
	while (head)
	{
		Node *next = head->next;    // adresu si musime ulozit PRED uvolnenim
		free(head);
		head = next;
	}
}

Podmínka p v cyklu využívá toho, že nulový pointer odpovídá nepravdě, takže konec seznamu poznáme bez explicitního porovnání.

V list_destroy je klíčové pořadí. Zápis free(head); head = head->next; by četl z už uvolněné paměti — tedy nedefinované chování, které AddressSanitizer hlásí jako use-after-free.

Nejzajímavější je odstranění uzlu, protože se liší podle toho, zda mažeme hlavu, nebo prvek uvnitř. Oběma případům se lze vyhnout pointerem na pointer:

Node *list_remove(Node *head, int key)
{
	//
	// link ukazuje na misto, kam je zapsana adresa aktualniho uzlu
	// na zacatku je to promenna head, pozdeji polozka next predchudce
	//
	Node **link = &head;

	while (*link)
	{
		if ((*link)->key == key)
		{
			Node *dead = *link;
			*link = dead->next;     // prepojime, at uz je to hlava nebo ne
			free(dead);
			break;
		}
		link = &(*link)->next;
	}
	return head;
}

Kdo si na Node ** netroufá, může napsat i přímočařejší variantu se zvláštní větví pro hlavu — je delší, ale funguje stejně.

Úkol 17

Naprogramujte strukturu, která si udržuje pole řetězců a umí ho zvětšovat.

void list_add(String_List *l, char *s)
{
	//
	// nejdriv misto na dalsi pointer
	//
	if (l->count == l->cap)
	{
		int newcap = (l->cap == 0) ? 4 : l->cap * 2;
		char **p = realloc(l->items, (size_t)newcap * sizeof(char*));
		assert(p);
		l->items = p;
		l->cap   = newcap;
	}

	//
	// potom klon samotneho retezce, vcetne ukoncovaci nuly
	//
	size_t len   = strlen(s);
	char  *clone = malloc(len + 1);
	assert(clone);
	memcpy(clone, s, len + 1);

	l->items[l->count] = clone;
	l->count += 1;
}

void list_destroy(String_List *l)
{
	for (int i = 0; i < l->count; i += 1) { free(l->items[i]); }   // kazdy retezec
	free(l->items);                                               // a pole pointeru
	l->items = 0;
	l->count = 0;
	l->cap   = 0;
}

Volání free je count + 1. Alokací je totiž také count + 1: jedna na pole pointerů a jedna na každý klonovaný řetězec. To je stejná dvoustupňová struktura jako u split_string z úkolu 15 a je to nejčastější místo, kde v této kapitole vzniká únik paměti.

Zdvojnásobování kapacity znamená, že je počet realokací logaritmický v počtu prvků. Např. po pěti voláních list_add je count roven 5 a cap roven 8 (kapacita rostla 0 → 4 → 8).

Klonujeme len + 1 bajtů, abychom vzali i ukončovací nulu. Právě tím se tato struktura liší od typu String z úkolu 6, který si délku pamatuje a nulu nepotřebuje.

Klonování je podstatné: kdybychom uložili rovnou předaný pointer, záviselo by to na tom, jak dlouho původní řetězec žije. Pro řetězcový literál by to prošlo, pro lokální pole nikoliv.

Funkce

Variadické funkce

Variadické funkce jsou funkce s proměnným počtem parametrů. Variadické funkce mají povinné argumenty, těch je v každém zavolání stejný počet, a volitelné argumenty, jejichž počet se může mezi voláními může lišit. Každá variadická funkce musí mít alespoň jeden povinný argument.

Zjištění počtu a typu volitelných argumentů uvnitř funkce se neděje automaticky, řeší se konvencí. Existují v zásadě tři přístupy.

  • Předpokládáme pevný typ volitelných argumentů, v povinném argumentu předáváme jejich počet.

    //hlavicka
    double sum_of_doubles(int count, ...);  // hlavicka
    
    // volani
    sum_of_doubles(3, 1.2, 2.2, 3.3);  // vysledek je 6.6 
    sum_of_doubles(2, 2.1, 3.3);       // vysledek je 5.4
    
  • Předpokládáme pevný typ argumentů, ale poslední nepovinný argument má speciální hodnotu, například 0.

    //hlavicka
    int sum_of_ints(int first, ...);  // hlavicka
    
    // volani
    sum_of_ints(1, 2, 3, 0);  // vysledek je 6
    sum_of_ints(2, 2, 0);     // vysledek je 4
    
  • Informaci o typech i počtu volitelných argumentů předáme pomocí povinných argumentů. Tento přístup používá například funkce printf.

K nepovinným argumentům přistupujeme pomocí maker z stdarg.h.

  • typ va_list pro udržování nezpracovaných argumentů,
  • va_start nastaví va_list na všechny volitelné argumenty,
  • va_arg vrátí hodnotu prvního argumentu ve va_list, potřebuje znát typ, první argument odstraní z va_list.
  • va_end ukončí práci s nepovinnými argumenty.

Podrobnosti v následujících příkladech.

double sum_of_doubles(int n, ...) 
{
	va_list args;
	double ret = 0;
	va_start(args, n); // tady se musi predat va_list a posledni povinny argument 
	
	for (int i = 0; i < n; i += 1) 
	{
		ret += va_arg(args, double); // tady se musi predat va_list a typ argumentu
	}
	
	va_end(args);  // ukonceni prace s argumenty
	return ret;
}

//
// nutno volat aspon dvema argumenty !!!
// jinak UB
//
int sum_of_ints(int first, ...) 
{
	va_list args;
	int ret = first;
	va_start(args, first);

	while (1) 
	{
		int val = va_arg(args, int);
		if (!val) break;
		ret += val;
	}
	
	va_end(args);
	return ret;
}

void almost_printf(char *format, ...) 
{
	va_list args;
	int index = 0;
	va_start(args, format);
	
	while(format[index]) 
	{
		switch(format[index]) 
		{
			case 'i':
				printf("%i ", va_arg(args,int));
				break;
			case 'd':
				printf("%f ", va_arg(args,double));
				break;
		}
		index += 1;
	}

	va_end(args);
}

Při předávání variadických argumentů dochází k automatické konverzi typů: float je konvertován na double, u celočíselných typů dochází k celočíselné promoci.

Pointery na funkce

S pomocí pointerů na funkce lze v omezené míře pracovat s funkcemi vyššího řádu. Pointer na funkci sebou nese i typové informace: typ návratové hodnoty a počet a typy argumentů. Zápis tohoto typu podobný jako zápis hlavičky funkce bez jmen argumentů, pouze jméno funkce nahradíme (*).

int (*)(int*, int)   // typ pointer na fci, ktera vraci int, 
	                 // jeji argumenty jsou typu int* a int.

Při definici proměnné se identifikátor píše dovnitř závorky za *.

int (*fce_ptr)(int*, int);   // definice pointeru 

Předchozí syntax je trochu nešikovná, proto se často používá typedef.

typedef int (*p_fun)(int* , int); // typ se jmenuje p_fun
p_fun ptr;   // definice promenne

nebo alternativně

typedef int fun_type(int *, int); // definujeme typ pro funkci, nikoliv pointer na funkci
fun_type* ptr;

S pointery na funkce nelze provádět aritmetiku, ani aplikovat operátory adresy a dereference (resp, tyto operátory nedělají nic). Můžeme ovšem funkci volat, syntax volání je přitom stejná jako u běžného volání funkce.

Podobně jako u polí, jméno funkce degeneruje na pointer na funkci.

typedef int p_fun(int* , int);

int array_sum(int *array, int n) {
	int r = 0;
	for (int i = 0; i < n; i += 1) {
		r += array[i];
	}
	return r;
}

int array_product(int *array, int n) {
	int r = 1;
	for (int i = 0; i < n; i += 1) {
		r *= array[i];
	}
	return r;	
}

int main() {
	int a[] = { 1, 2, 3, 4, 5 };
	int n = sizeof(a)/sizeof(*a);
	
	p_fun* reduce = array_sum;      // reduce ukazuje na funkci array_sum
	printf("%i\n", reduce(a, n));   // vytiskne 15, vola array_sum
	reduce = array_product;
	printf("%i\n", reduce(a, n));   // vytiskne 120, vola array_product
	return 0;
}

S pointery na funkce lze pracovat jako s jinými typy v C. Lze tvořit jejich pole, předávat je do funkcí jako argumenty, mohou být návratovými hodnotami funkcí atd.

Pointer na funkci může být neplatný, v tom případě vede pokus o volání k nedefinovanému chování. Pointeru na funkci je možné přiřadit 0 a pointery na funkce lze porovnávat.

Jedno z využití pointerů na funkce je tvorba analogie generických funkcí známých z jiných jazyků. Ideou je, že funkci naprogramujeme jednou a ona poté pracuje pro více typů. Ukážeme si to na příkladu binárního vyhledávání.

typedef int compare_fn(void *, void *);  // argumenty jsou void*, aby byla funkce obecna

int compare_int(void *a, void *b) 
{ 
    int *aa = a; // tuto funkci budeme volat pouze s int*,
    int *bb = b; // muzeme bezpecne pretypovat

    if (*aa < *bb) return -1;
    if (*bb < *aa) return 1;

    return 0;
}

int compare_long(void *a, void *b) 
{
    long* aa = a;
    long* bb = b;

    if (*aa < *bb) return -1;
    if (*bb < *aa) return 1;

    return 0;
}

//
//  genericka funkce pro binarni vyhledavani
//

int binary_search(void *array,              // pole, ve kterem vyhledavame
                  void *key,                // ukazatel na hodnotu klice
                  size_t n,                 // pocet prvku pole
                  size_t element_size,      // velikost jednoho prvku v bajtech
                  compare_fn* cmp)          // funkce pro porovnani dvou prvku
{

	unsigned char *bytes = array;

	size_t lo = 0;
	size_t hi = n;          // polouzavreny interval [lo, hi)

	while (lo < hi)
	{
		size_t mid = lo + (hi - lo) / 2;

		int cmp_result = cmp(key, bytes + mid * element_size);

		if (cmp_result == 0)     { return (int)mid; }
		else if (cmp_result > 0) { lo = mid + 1; }
		else                     { hi = mid;     }
	}

	return -1;
}

//
//  pro jednotlive typy vytvorem funkce tvorici rozhrani
//
int binary_search_int(int* array, int key, size_t n) 
{
	return binary_search(array, &key, n, sizeof(int), compare_int);
}

int binary_search_long(long* array, long key, size_t n) 
{
	return binary_search(array, &key, n, sizeof(long), compare_long);
}

int main() 
{
	int array_int[20];
	long array_long[20];

	for(int i = 0; i < 20; i += 1) 
	{
		array_int[i] = i;
		array_long[i] = i;
	}

	int key_int_ok = 10;
	int key_int_fail = 30;

	long key_long_ok = 5;
	long key_long_fail = -3;

	printf("index of %i in array_int is %i\n",
		    key_int_ok,
		    binary_search_int(array_int, key_int_ok, 20));

	printf("index of %i in array_int is %i\n",
		   key_int_fail,
           binary_search_int(array_int, key_int_fail, 20));

	printf("index of %li in array_long is %i\n",
           key_long_ok,
           binary_search_long(array_long, key_long_ok, 20));

	printf("index of %li in array_long is %i\n",
           key_long_fail,
           binary_search_long(array_long, key_long_fail, 20));
		 
	return 0;
}

Úkoly

  1. Navrhněte strukturu pro komplexní číslo a naprogramujte variadickou funkci pro výpočet sumy komplexních čísel využívající prvního přístupu k určení počtu volitelných argumentů.

  2. Napište funkci long double prumer(char* format, ...), která výpočítá aritmetický průměr ze zadaných hodnot různých datových typů. Typy předávaných hodnot jsou určeny pomocí parametru format, který může tvořit libovolná posloupnost znaků odpovídající typům následujících parametrů - i pro typ int, d pro typ double a l pro long double.

  3. Napište funkci double* map(double (*fce)(double), double* input, int len), která na hodnoty pole input aplikuje funkci fce a vrátí pole výsledných hodnot. Velikost pole input parametrem len.

  4. Napište funkci double akumulator(double (*fce)(double, double), double numbers[], int len), která zpracuje pomocí předané funkce fce hodnoty z pole numbers, jehož velikost je dána parametrem len. Můžete předpokládat, že len je vždy alespoň 2.

  5. S použitím příkladu z kapitoly vytvořte funkci pro binární vyhledávání zlomků.

  6. Do následujícího kódu doprogramujte generickou funkci pro třídění a s její pomocí funkce pro třídění zlomků a unsigned. (Můžete si doprogramovat libovolné pomocné funkce.) Je zakázáno použít třídicí funkci ze standardní knihovny.

    typedef struct
    {
        int numerator;
        int denominator;
    } Fraction;
    
    typedef int compare_fn(void*, void*);
    
    int compare_fraction(void *a, void *b)
    {
        // TODO
    }
    
    int compare_unsigned(void *a, void *b)
    {
        // TODO
    }
    
    void generic_sort(void *array, size_t n, size_t element_size, compare_fn* cmp)
    {
        // TODO
    }
    
    void fraction_sort(Fraction *array, int n)
    {
        // TODO
    }
    
    void unsigned_sort(unsigned int* array, int n)
    {
        // TODO
    }
    
  7. Naprogramujte vlastní verzi funkce printf, která s podporou tiskových direktiv %i, %f a %z, kde poslední direktiva vede k tisku zlomku. Při řešení lze využít funkci printf.

  8. Naprogramujte jednoduchou kalkulačku, která operaci vybírá indexem do pole pointerů na funkce, nikoliv pomocí switch nebo řetězu if.

    typedef enum { OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_COUNT } Operation;
    
    double apply(Operation op, double a, double b);
    

    Funkci apply napište tak, aby obsahovala pole OP_COUNT pointerů na funkce a operaci provedla jediným zavoláním. Ošetřete dělení nulou. Porovnejte výsledný kód s variantou napsanou pomocí switch a rozmyslete si, kdy je která přehlednější.

  9. Naprogramujte funkci

    int filter(int *input, int n, int *output, int (*predicate)(int));
    

    která do pole output zkopíruje ty prvky input, pro které predicate vrátí nenulovou hodnotu, a vrátí jejich počet. Otestujte ji s predikáty pro sudost, prvočíselnost a zápornost.

  10. Naprogramujte funkci

    typedef double binary_op(double, double);
    
    binary_op *get_operation(char symbol);
    

    která pro znak '+', '-', '*' a '/' vrátí pointer na odpovídající funkci a pro jiný znak vrátí 0. Volající musí návratovou hodnotu zkontrolovat, než ji zavolá.

Řešení úkolů

Úkol 1

Navrhněte strukturu pro komplexní číslo a naprogramujte variadickou funkci pro výpočet sumy komplexních čísel využívající prvního přístupu k určení počtu volitelných argumentů.

typedef struct
{
	double re;
	double im;
} Complex;

//
// prvni pristup: pocet volitelnych argumentu je v povinnem argumentu
//
Complex complex_sum(int count, ...)
{
	Complex ret = { 0, 0 };
	va_list args;
	va_start(args, count);

	for (int i = 0; i < count; i += 1)
	{
		Complex c = va_arg(args, Complex);
		ret.re += c.re;
		ret.im += c.im;
	}

	va_end(args);
	return ret;
}

Volání využívá složených literálů:

Complex s = complex_sum(3, (Complex){1,2}, (Complex){3,-1}, (Complex){0.5,0.5});
// s je 4.5 + 1.5i

Strukturu lze variadickou funkcí předat, protože se na ni nevztahují automatické konverze — do va_arg se předá přesně ten typ, který jsme poslali. U typů menších než int nebo u float by to takto nefungovalo, viz konec sekce o variadických funkcích.

Kdyby uživatel předal jiný počet argumentů, než slibuje count, šlo by o nedefinované chování. Variadická funkce nemá jak si to ověřit.

Úkol 2

Napište funkci long double prumer(char *format, ...), kde i značí int, d značí double a l značí long double.

long double prumer(char *format, ...)
{
	long double sum = 0;
	int         n   = 0;

	va_list args;
	va_start(args, format);

	for (int i = 0; format[i]; i += 1)
	{
		switch (format[i])
		{
			case 'i': sum += (long double)va_arg(args, int);    n += 1; break;
			case 'd': sum += (long double)va_arg(args, double); n += 1; break;
			case 'l': sum += va_arg(args, long double);         n += 1; break;
			default: break;     // neznamy znak ignorujeme
		}
	}

	va_end(args);
	return n ? sum / n : 0;
}

// prumer("idl", 2, 3.0, (long double)4.0)  ->  3.0

Typ ve va_arg musí přesně odpovídat tomu, co bylo předáno. Kdybychom u 'l' napsali va_arg(args, double), přečetli bychom nesprávný počet bajtů a všechny další argumenty by se rozsypaly.

Při volání je literál 4.0 typu double, nikoliv long double. Pro direktivu 'l' je potřeba psát 4.0L nebo přetypovat.

Úkol 3

Napište funkci double *map(double (*fce)(double), double *input, int len).

double *map(double (*fce)(double), double *input, int len)
{
	double *out = malloc((size_t)len * sizeof(double));
	assert(out);

	for (int i = 0; i < len; i += 1) { out[i] = fce(input[i]); }

	return out;
}

// pouziti
static double twice(double x) { return 2 * x; }

double in[] = { 1, 2, 3 };
double *out = map(twice, in, 3);     // { 2, 4, 6 }
free(out);                           // uvolnuje volajici!

Zápis double (*fce)(double) je pointer na funkci, která bere i vrací double. Závorky kolem *fce jsou nutné — double *fce(double) by byla deklarace funkce vracející double*.

Při volání se píše jenom jméno funkce, bez &: jméno funkce degeneruje na pointer stejně jako jméno pole.

Funkce alokuje nové pole, takže ho volající musí uvolnit. Alternativou by bylo nechat cílové pole předat jako parametr.

Úkol 4

Napište funkci double akumulator(double (*fce)(double, double), double numbers[], int len).

double akumulator(double (*fce)(double, double), double numbers[], int len)
{
	double acc = numbers[0];
	for (int i = 1; i < len; i += 1) { acc = fce(acc, numbers[i]); }
	return acc;
}

static double add(double a, double b)  { return a + b; }
static double maxd(double a, double b) { return a > b ? a : b; }

double nums[] = { 3, 1, 4, 1, 5 };
akumulator(add,  nums, 5);    // 14
akumulator(maxd, nums, 5);    // 5

Jde o klasickou operaci fold: první prvek je počáteční hodnota a funkce se postupně aplikuje na dosavadní výsledek a další prvek. Právě proto zadání říká, že len je aspoň 2 — pro prázdné pole bychom neměli co vrátit a museli bychom počáteční hodnotu dostat jako další parametr.

Úkol 5

S použitím příkladu z kapitoly vytvořte funkci pro binární vyhledávání zlomků.

Generická funkce binary_search z výkladu zůstává beze změny. Stačí dodat porovnávací funkci a obal:

int compare_fraction(void *a, void *b)
{
	Fraction *x = a;
	Fraction *y = b;

	//
	// x/xd < y/yd  prave kdyz  x*yd < y*xd  (kladne jmenovatele)
	// nasobime v long, aby soucin nepretekl
	//
	long left  = (long)x->numerator * y->denominator;
	long right = (long)y->numerator * x->denominator;

	return (left > right) - (left < right);
}

int binary_search_fraction(Fraction *array, Fraction key, size_t n)
{
	return binary_search(array, &key, n, sizeof(Fraction), compare_fraction);
}

Zlomky nelze porovnávat po položkách; musíme je převést na společného jmenovatele. Křížové násobení je přesnější než převod na double, který by u velkých čísel ztrácel přesnost.

Výraz (left > right) - (left < right) vrací −1, 0 nebo 1 bez větvení. Zápis return left - right; by byl chybný: rozdíl může přetéct.

Přetypování na long před násobením je podstatné. (long)(x->numerator* y->denominator) by nepomohlo — součin by se spočítal v int a přetekl by ještě před konverzí.

Klíč se předává adresou (&key), protože binary_search pracuje s void*. Parametr key je lokální kopie, takže je to bezpečné.

Úkol 6

Doprogramujte generickou funkci pro třídění a s její pomocí funkce pro třídění zlomků a unsigned.

void generic_sort(void *array, size_t n, size_t element_size, compare_fn *cmp)
{
	unsigned char *bytes = array;

	//
	// jeden prvek si musime umet odlozit, velikost zname az za behu
	//
	unsigned char *tmp = malloc(element_size);
	assert(tmp);

	//
	// insertion sort
	//
	for (size_t i = 1; i < n; i += 1)
	{
		memcpy(tmp, bytes + i * element_size, element_size);

		size_t j = i;
		while (j > 0 && cmp(bytes + (j-1) * element_size, tmp) > 0)
		{
			memcpy(bytes + j * element_size, bytes + (j-1) * element_size, element_size);
			j -= 1;
		}

		memcpy(bytes + j * element_size, tmp, element_size);
	}

	free(tmp);
}

int compare_unsigned(void *a, void *b)
{
	unsigned int x = *(unsigned int*)a;
	unsigned int y = *(unsigned int*)b;
	return (x > y) - (x < y);
}

void fraction_sort(Fraction *array, int n)
{
	generic_sort(array, (size_t)n, sizeof(Fraction), compare_fraction);
}

void unsigned_sort(unsigned int *array, int n)
{
	generic_sort(array, (size_t)n, sizeof(unsigned int), compare_unsigned);
}

Protože velikost prvku známe až za běhu, nemůžeme si odkládací proměnnou deklarovat — musíme ji alokovat. Prohazování prvků probíhá přes memcpy nad bajty.

U compare_unsigned nelze psát return x - y;. Odčítání dvou unsigned int je modulární, takže pro x < y vyjde obrovské kladné číslo a třídění by bylo úplně špatně.

Poznámka k typu compare_fn. V zadání je definován jako typ funkce, nikoliv pointeru:

typedef int compare_fn(void*, void*);   // typ funkce
compare_fn *cmp;                        // proto se pise hvezdicka

Ve výkladu se objevuje i varianta typedef int (*compare_fn)(void*, void*), kde už je pointer součástí typu a píše se pak jenom compare_fn cmp. Obojí je správně, jenom je potřeba vědět, který zápis zrovna používáme.

Úkol 7

Naprogramujte vlastní verzi printf s podporou direktiv %i, %f a %z, kde poslední vede k tisku zlomku.

typedef struct { int numerator; int denominator; } Fraction;

void my_printf(char *format, ...)
{
	va_list args;
	va_start(args, format);

	for (int i = 0; format[i]; i += 1)
	{
		if (format[i] != '%') { putchar(format[i]); continue; }

		i += 1;    // preskocime '%'
		switch (format[i])
		{
			case 'i':
				printf("%i", va_arg(args, int));
				break;

			case 'f':
				printf("%f", va_arg(args, double));
				break;

			case 'z':
			{
				Fraction f = va_arg(args, Fraction);
				printf("%i/%i", f.numerator, f.denominator);
				break;
			}

			case '%':
				putchar('%');
				break;

			case 0:
				//
				// format koncil znakem '%', vratime se
				// aby vnejsi cyklus korektne skoncil
				//
				i -= 1;
				break;

			default:
				putchar('%');
				putchar(format[i]);
				break;
		}
	}
	va_end(args);
}

Direktivy %i a %f jenom předáme skutečnému printf, jak zadání dovoluje. Zajímavé je %z, kde si strukturu vytáhneme a naformátujeme sami.

Případ case 0 je snadné přehlédnout: pokud řetězec končí osamoceným %, zvýšili jsme i za konec řetězce a bez ošetření bychom četli mimo pole.

Větev pro %f je dobrou ilustrací automatických konverzí: i kdyby volající předal float, přijde do funkce jako double, takže va_arg(args, double) je správně a va_arg(args, float) by bylo špatně.

Úkol 8

Naprogramujte kalkulačku, která operaci vybírá indexem do pole pointerů na funkce, nikoliv pomocí switch nebo řetězu if.

static double op_add(double a, double b) { return a + b; }
static double op_sub(double a, double b) { return a - b; }
static double op_mul(double a, double b) { return a * b; }
static double op_div(double a, double b) { return b == 0.0 ? 0.0 : a / b; }

double apply(Operation op, double a, double b)
{
	//
	// poradi prvku MUSI odpovidat poradi konstant ve vyctu
	//
	static double (*table[OP_COUNT])(double, double) = { op_add, op_sub, op_mul, op_div };

	assert(op >= 0 && op < OP_COUNT);
	return table[op](a, b);
}

Zápis double (*table[OP_COUNT])(double, double) se čte zevnitř ven: table je pole OP_COUNT pointerů na funkci beroucí dva double a vracející double. Závorky kolem *table[...] jsou nutné.

Pole je static, aby se neinicializovalo znovu při každém volání. Jeho velikost je OP_COUNT, takže po přidání nové operace do výčtu překladač ohlásí, že v inicializátoru chybí prvek.

Dělení nulou ošetřujeme uvnitř op_div. Celočíselné dělení nulou je nedefinované chování; u double bychom sice dostali inf, ale vracet nulu je tu čitelnější.

Kontrola assert je podstatná: op je typu Operation, ale nic nebrání někomu přiřadit do něj (Operation)99 a index mimo pole by byl nedefinované chování.

Varianta se switch by byla zhruba stejně dlouhá a pro čtyři operace možná i čitelnější. Tabulka vyhrává tehdy, když je operací hodně, když se sestavuje za běhu, nebo když se má dát rozšířit bez zásahu do apply.

Úkol 9

Naprogramujte funkci filter s predikátem předaným jako pointer na funkci.

int filter(int *input, int n, int *output, int (*predicate)(int))
{
	int count = 0;
	for (int i = 0; i < n; i += 1)
	{
		if (predicate(input[i]))
		{
			output[count] = input[i];
			count += 1;
		}
	}
	return count;
}

Predikáty pro otestování:

static int is_even(int x)     { return x % 2 == 0; }
static int is_negative(int x) { return x < 0; }

static int is_prime(int x)
{
	if (x < 2) { return 0; }
	for (int d = 2; d * d <= x; d += 1) { if (x % d == 0) { return 0; } }
	return 1;
}

Index i prochází vstup, count ukazuje na první volné místo ve výstupu — a protože se zvyšuje jenom při úspěchu, jsou výsledky ve výstupním poli za sebou bez mezer.

Zápis if (predicate(...)) stačí; porovnávat s nulou není potřeba, protože zadání mluví o „nenulové hodnotě“, což je přesně to, co if testuje.

Alokaci výstupního pole necháváme na volajícím, takže funkce sama nealokuje ani neuvolňuje. Volající musí zajistit, že se do output vejde až n prvků.

Úkol 10

Naprogramujte funkci get_operation, která vrací pointer na funkci.

typedef double binary_op(double, double);

binary_op *get_operation(char symbol)
{
	switch (symbol)
	{
		case '+': return op_add;
		case '-': return op_sub;
		case '*': return op_mul;
		case '/': return op_div;
		default:  return 0;
	}
}

Použití — návratovou hodnotu je nutné zkontrolovat, než ji zavoláme:

binary_op *f = get_operation('*');
if (f) { printf("%f\n", f(6, 7)); }     // vytiskne 42.000000
else   { fprintf(stderr, "neznamy operator\n"); }

Všimněte si, jak se typedef vyplatil. Bez něj by hlavička vypadala

double (*get_operation(char symbol))(double, double);

což je zápis, který se čte velmi obtížně: get_operation je funkce beroucí char a vracející pointer na funkci dvou double vracející double. S typedef je z toho čitelné binary_op *get_operation(char).

Zde je binary_op typ funkce, nikoliv pointeru — proto se u návratového typu píše hvězdička. Kapitola ukazuje i druhou možnost, kde je pointer součástí typu:

typedef double (*binary_op_ptr)(double, double);
binary_op_ptr get_operation(char symbol);    // uz bez hvezdicky

Obojí je správně, jen je potřeba vědět, kterou variantu zrovna používáme.

Vracet 0 pro neznámý znak je tatáž konvence jako u neplatného pointeru na data: nulový pointer na funkci je vždy neplatný a odpovídá nepravdě, takže se dá testovat přímo v if. Zavolat ho by bylo nedefinované chování — proto ta kontrola.

Vstup a výstup pomocí proudů

Proud je prostředek, kterým lze s pomocí standardní knihovny pracovat se soubory a dalšími vstupně-výstupními zařízeními (např. klávesnice, terminál). Existují i další prostředky, například systémová volání nebo jiné, nestandardní, knihovny. Těm se ovšem věnovat nebudeme.

Funkce, o kterých budeme mluvit, mohou skončit chybou. Uvedeme si jenom, jakým způsobem poznáme, že k chybě došlo a proč k ní mohlo dojít. Zpracování chyby a to, jak se má program chovat, pokud k chybě dojde, je mimo rozsah tohoto kurzu. V příkladech budeme možnost, že k chybě dojde buď ignorovat, nebo použijeme assert.

Proudy ze standardní knihovny

Proud je objekt, do kterého lze zapisovat nebo z něj číst. Otevřením souboru jej na proud napojíme a poté zapisování do či čtení z proudu odpovídá zapisování do souboru či čtení ze souboru. Jsou definovány i další, tzv. standardní proudy. Pomocí nich můžeme zapisovat na standardní výstup (tam zapisuje např.printf), na standardní chybový výstup a číst ze standardního vstupu.

Proud je reprezentován strukturou FILE, v programu pracujeme s ukazateli na tuto strukturu. Získáme jej pomocí funkce fopen.

FILE* fopen(char* path, char* mode);

První argument je cesta k otevíranému souboru. Ta je buď absolutní, nebo relativní vzhledem k aktuálnímu pracovnímu adresáři (např. aktuální adresář shellu, když program spouštíme). Textový řetězec mode určuje způsob otevření souboru. Jeden ze znaků mode obvykle udává režim otevření: ten je buď textový nebo binární. Další znak pak to, co se souborem můžeme dělat: číst jej, zapisovat do něj, připojit něco na jeho konec, vytvořit jej, pokud neexistuje, nebo nějaká kombinace předchozího. Nejpoužívanější možnosti ukážeme níže.

/*
POPIS ZNAKU
'r' otevreno pro cteni
'w' otevreno pro zapis, existujici soubor je prepsan
'a' otevreno pro zapis, pokud soubor existuje, zapisujeme na jeho konec
'b' otevreno v binarnim rezimu

PRIKLADY KOMBINACI
"r" cteni v textovem rezimu
"w" zapis v textovem rezimu 
"rb" cteni v binarnim rezimu 
"wb" zapis v binarnim rezimu
*/

Pokud se v řetězci nenachází znak b, otevře se proud v textovém režimu. Některé verze knihovny tolerují i t jako příznak otevření v textovém režimu, ovšem ve standardu není.

Pokud otevření souboru selže, vrátí fopen 0. Tuto situaci je potřeba ošetřit, pro účely kurzu se spokojíme s assercí. (Pro zjištění důvodu lze někdy použít kombinaci errno a strerror.)

FILE *st = fopen("path/to/file", "rt");
assert(st);

Nepoužívaný proud se zavírá funkcí fclose.

V textovém režimu čteme a zapisujeme znaky, v binárním režimu bajty. Ačkoliv se zdá, že je to totéž, rozdíl je například u konce řádku ve Windows, který končí dvojící bajtů '\r' '\n'.

Ze streamu se čte a zapisuje se do něj pomocí funkcí z stdio.h. Stream si udržuje místo, ze kterého se bude číst při příštím zavolání funkce pro čtení. Po přečtení nějakého úseku (např. znaku, řádku apod.) posune toto místo na první bajt nepřečteného obsahu.

Pokud se knihovní funkci pro čtení ze streamu čtení nepovede, programátor to pozná z návratové hodnoty, případně může zkontrolovat příznaky streamu pro konec souboru a pro chybu. To se dělá funkcemi

// vraci true, pokud byl neuspech predchozi funkce zpusoben tim, 
// ze jsme na konci souboru 
int feof(FILE* stream); 

// vraci true, pokud byl neuspech predchozi funkce zpusoben
// jinou chybou.
int ferror(FILE* stream); 

Analogické úvahy platí i pro zápis.

Textový režim

Základní funkce pro čtení je

int getc(FILE *stream);

Přečte ze streamu jeden znak, jako hodnoty int získanou přetypováním z unsigned char. (Dolní bajt je naše hodnota). Při neúspěšném čtení vrací konstantu EOF, která má zápornou hodnotu (typicky -1). Pokud dojde k úspěšnému čtení, návratová hodnota getc je nezáporná. Srovnáním návratové hodnoty s EOF tak vždycky správně zjistíme, jesli došlo k chybě. Toto srovnání ovšem musíme provést předtím, než návratovou hodnotu přetypujeme na char, např.

int z;
while ((z = getc(in)) != EOF)
{
	char c = (char)z;   // pretypovani az po testu
	fputc(z, stdout);
}

Problém může nastat v následujícím příkladu.

char c;
while ((c = getc(in)) != EOF) { ... }
//
// pokud nacteme bajt 0xFF, ulozi se jako -1, tedy jako EOF
// cyklus skonci uprostred souboru
//

Větší část textu lze načíst pomocí funkce fgets (tu si čtenář dostuduje z referenční příručky).

Základními funkcemi pro zápis jsou

int fputc(int ch, FILE* stream);
int fputs(char *s, FILE* stream);
int fprintf(FILE* stream, char* format, ...); 

Funkce fputc zapíše do streamu znak, který dostane přetypováním ch na unsigned int. Funkce fputs zapíše do streamu řetězec. V případě neúspěchu obě funkce vracejí EOF a nastaví streamu flag ferror.

Funkce fprintf funguje jako printf pouze tiskne do streamu. Při chybě vrací zápornou hodnotu, jinak počet vytistěných znaků.

Zápis do streamu je bufferovaný, zapsané změny se v napojeném souboru (či zařízení) mohou projevit později. Chceme-li si jejich okamžité projevení, můžeme k tomu použít funkci

int fflush(FILE* stream);

V případě neúspěchu vrací EOF. Tato funkce je volána (či je prováděn kód jí velmi podobný) i při zavření streamu, proto fclose také může neuspět a vrátit EOF.

Existují tři standardní proudy, přistupné jsou pomocí proměnných stdin, stdout, stderr.

Jako příklad si uvedeme funkci, která načte obsah souboru na disku a vytiskne jej na standardní výstup.

void print_file(char *filename) 
{
	FILE *in = fopen(filename, "rt");
	assert(in);

	int z = 0;
	while (1) 
	{
		z = getc(in);

	    if (z == EOF)    // konec souboru nebo chyba cteni
		{ 
			if (ferror(in)) 
			{ 
				fprintf(stderr, "chyba cteni\n");
				assert(0);
			}
			break;
		}
		fputc(z, stdout);
	}
	fclose(in);
}

Binární režim

Čteme a zapisujeme bajty, které při čtení nejsou nijak interpretovány.

Funkce pro čtení

size_t fread(void *dest, size_t element_size, size_t count, FILE *in)

Čteme count prvků, každý o velikost element_size bajtů, ze streamu in, ukládáme je do pole na adrese dest. Vrací počet přečtených prvků, pokud bylo čtení bez chyby, pak se count a návratová hodnota rovnají. Je nutné myslet na to, že na adrese dest musí být naalokováno dostatek paměti, jinak dojde k nedefinovanému chování.

Funkce pro zápis

size_t fwrite(void *src, size_t element_size, size_t count, FILE *out)

do streamu out zapise count položek, každy o velikosti element_size uložených za sebou na adrese src. Funkce vrací počet zapsaných položek.

Můžeme také pracovat s akturální pozicí, ze které v souboru čteme. Pozice je hodnota typu long a je to index bajtu od začátku. Aktuální pozici můžeme získat pomocí funkce

long ftell(FILE* stream)

Aktuální pozici také můžeme nastavit pomocí funkce

int fseek(FILE *stream, long offset, int start)

kde offset je počet bajtů, o které se chceme posunout a start určuje místo, ze kterého se chceme posunout. Pro tento účel jsou definovány konstanty

SEEK_SET zacatek souboru
SEEK_CUR aktualni pozice
SEEK_END konec souboru

V následujícím příkladu zapíšeme obsah pole do souboru a zase je načteme.

char *filename = "tmp.xx";

// zapis do souboru

float out_array[4] = { 1.0f, 2.0f, 3.0f, 4.0f };
	
FILE *out = fopen(filename, "wb");
assert(out);

size_t written = fwrite(out_array, sizeof(float), 4, out);
assert(written == 4);

fclose(out);
	
// cteni ze souboru, od druheho prvku
FILE *in = fopen(filename, "rb");
// posuneme se o jeden float dopredu
int fail = fseek(in, sizeof(float), SEEK_SET);
assert(!fail);

float in_array[3]= { 0.0f, 0.0f, 0.0f };
size_t read = fread(in_array, sizeof(float), 3, in);
assert(read == 3);
fclose(in);	
	
for(int i = 0; i < 3; i += 1) {
	printf("%f ", in_array[i]);
}
printf("\n");

Práce s binárními soubory a čtení binárních formátů má svá specifika, musíme si dávat pozor na velikosti typů, endianitu apod.

Úkoly

  1. Napište program, který spočítá počet znaků, řádků a slov v textovém souboru. (Analogie textové utility wc).

  2. Napište funkci, která z proudu načte jeden řádek, když předem neznáme délky řádků. Zajistěte, aby šla funkce používat opakovaně. (Musíte vymyslet vhodné argumenty, jejich předání a vypořádat se s alokacemi).

  3. Nastudujte v referenční příručce a vyzkoušejte funkci fgets. Naprogramujte funkci, která pomocí fgets načte řádek do bufferu pevné velikosti a pozná, že se celý řádek nevešel.

  4. Napište program, který načte textový soubor a do jiného souboru uloží kopii jeho obsahu tak, že jeden řádek bude mít maximálně 80 znaků. Přitom nerozdělí žádná slova kratší než 80 znaků a minimalizuje počet řádků.

  5. PBM je textový formát pro uložený černobílého obrázku (každý pixel je buď černý nebo bílý).

    • Řádky začínající znakem # jsou komentáře
    • První nekomentovaný řádek obsahuje řetězec "P1".
    • Druhý nekomentovaný řádek obsahuje dvě čísla v desítkové soustavě oddělená mezerou, tato čísla udávají šiřku a výšku obrázku (v pixelech).
    • Počínaje třetím nekomentovaným řádkem obsahuje soubor pixelová data. Jeden pixel je buď 0 nebo 1. Pixely jsou zapsány za sebou po řádcích odshora obrázku, jeden řádek obrázky na jeden řádek souboru. Mezi pixely na jednom řádku je mezera.

    Doplňte chybějící kód v následujícím. (Můžete si dopsat pomocné funkce atd.)

    //
    //  Pro jednoduchost případné chyby (nelze otevřít soubor atd.)
    //  řešte pouze ukončením programu pomocí assert
    //
    //  Volitelně lze dopsat funkci pro dealokaci vnitřních dat
    //  struktury Picture (ale není to nutné, jde o procvičení práce
    //  se streamy).
    //
    
    //
    // Struktura pro černobílý obrázek
    //
    typedef struct 
    {
    	// TODO
    } Picture;
    
    //
    // nahraje ze souboru obrázek ve formátu pbm
    // 
    Picture load_from_pbm(char* filename)
    {
    	// TODO 
    }
    
    //
    // zrcadlově obrátí obrázek podle svislé osy
    //
    void mirror_picture(Picture* p)
    {
    	// TODO
    }
    
    //
    // uloží obrázek v pbm formátu
    //
    void save_to_pbm(char* filename, Picture src)
    {
    	// TODO
    }
    
    //
    // příklad použití
    //
    int main()
    {
    	Picture picture = load_from_pbm("test.pbm");
    	mirror_picture(&picture);
    	save_to_pbm("foo.pbm", picture);
    }
    
  6. Uvažme ukládání řetězců do souboru v binárním formátu.

    Naprogramujte funkci void append(char* string, char* filename), která na konec souboru přidá nový řetězec.

    Naprogramujte funkci long search(char* string, char* filename), která prohledá soubor a pokud obsahuje záznam o daném řetězci, vrátí jeho offset od začátku souboru. Jinak vrátí -1.

    V souboru odpovídá řetězci jeden záznam, záznamy jsou v souboru za sebou. Jeden záznam vypadá následovně.

    offset  velikost   vyznam
    --------------------------------------------------------------------------
         0       2          velikost retezce v bajtech
    
         2       ?          ulozeny retezec, chapan jako posloupnost unsigned 
                            char bez 0 na konci
    --------------------------------------------------------------------------
    
  7. Run Length Encoding (RLE) je velmi jednoduchý bezeztrátový kompresní princip. Vysvětlíme jej na posloupnostech symbolů a později si řekneme, jak jej uplatníme v jednoduchém kompresním programu.

    Vstupem RLE je posloupnost symbolů (symbol je zde abstraktní pojem, který nijak nevztahujeme k jazyku C, znakům v textu atd.). Tu převedeme do komprimované podoby s pomocí následujícího principu:

    • Pokud se v posloupnosti za sebou nachází n-krát symbol x, převedeme ji na dvojici (x, n).

    • Předchozí bod aplikujeme na maximálně dlouhé (= nejdou prodloužit) podposloupnosti vstupní posloupnosti v pořadí, ve kterém se ve vstupní posloupnosti vyskytují.

    Například posloupnost aaabaabcccccbccccd zakódujeme na

    (a,3)(b,1)(a,2)(b,1)(c,5)(b,1)(c,4)(d,1)
    

    Naprogramujte základ jednoduchého kompresního programu.

    Soubor pro kompresi budeme otevírat v binárním módu. Pro RLE budeme za jeden symbol považovat jeden bajt souboru. Dvojici (x, n), kde x je symbol a n je počet jeho výskytů budeme do výsledného souboru ukládat pomocí dvou bajtů: do prvního uložíme x, do druhého uložíme n-1. Takto jsme schopni uložit pouze dvojice, kde je n <= 256. Dvojice, kde je n>256, si představíme jako posloupnost n/256 dvojic (x, 256) následovanou dvojicí (x, r), kde r = n % 256. Tuto poslední dvojici vynecháme, pokud je r == 0. Tyto dvojice poté uložíme stejným způsobem, jako dvojice s n<=256.

    Kompresní program implementujte jako funkce

    void RLE_encode(char* in_path, char *out_path);
    void RLE_decode(char* in_path, char *out_path);
    
  8. Napište program, který čte ze standardního vstupu a zapisuje na standardní výstup, přičemž převede všechna malá písmena na velká. Program nesmí otevírat žádný soubor. Vyzkoušejte ho přesměrováním:

    ./program < vstup.txt > vystup.txt
    echo "ahoj svete" | ./program
    

    Chybová hlášení tiskněte na stderr a ověřte, že se do vystup.txt nedostanou.

  9. Napište funkci void copy_file(char *src, char *dst), která zkopíruje soubor v binárním režimu po blocích o velikosti 4096 bajtů pomocí fread a fwrite. Ošetřete poslední, neúplný blok. Porovnejte rychlost s variantou, která kopíruje po jednotlivých bajtech pomocí getc a fputc.

  10. Napište funkci long find_byte(char *filename, unsigned char b), která vrátí offset prvního výskytu bajtu b v souboru, nebo -1. Napište i variantu, která hledá od konce souboru — tam využijte fseek se SEEK_END a ftell.

  11. Soubor obsahuje na každém řádku čísla oddělená středníkem. Napište funkci, které se předá jméno souboru a číslo sloupce, a která vytiskne na stdout součet hodnot v tomto sloupci. Řádky, ve kterých sloupec chybí, přeskočte a jejich počet vypište na stderr.

Řešení úkolů

Úkol 1

Napište program, který spočítá počet znaků, řádků a slov v textovém souboru (analogie wc).

void wc(char *filename)
{
	FILE *in = fopen(filename, "r");
	assert(in);

	long chars = 0, lines = 0, words = 0;
	int  in_word = 0;
	int  z;

	while ((z = getc(in)) != EOF)
	{
		chars += 1;
		if (z == '\n') { lines += 1; }

		if (z == ' ' || z == '\t' || z == '\n') { in_word = 0; }
		else if (!in_word) { in_word = 1; words += 1; }   // zacatek noveho slova
	}

	fclose(in);
	printf("%ld %ld %ld %s\n", lines, words, chars, filename);
}

Slova počítáme pomocí příznaku in_word: nové slovo začíná tam, kde po bílém znaku přijde znak, který bílý není. Bez příznaku bychom počítali mezery a dvě mezery za sebou by daly špatný výsledek.

Návratová hodnota getc musí být v int, ne v char — jinak by se bajt 0xFF stal nerozlišitelným od EOF.

Řádky počítáme podle znaků nového řádku, takže poslední řádek bez ukončujícího \n se nezapočítá. Skutečné wc se chová stejně.

Úkol 2

Napište funkci, která z proudu načte jeden řádek, když předem neznáme délky řádků. Zajistěte, aby šla používat opakovaně.

//
// vraci nove alokovany retezec bez znaku noveho radku,
// nebo 0, pokud uz nebylo co cist
// pamet uvolnuje volajici
//
char *read_line(FILE *in)
{
	size_t cap = 16;
	size_t len = 0;
	char  *buf = malloc(cap);
	assert(buf);

	int z;
	while ((z = getc(in)) != EOF && z != '\n')
	{
		if (len + 1 >= cap)          // +1 kvuli ukoncovaci nule
		{
			cap *= 2;
			char *p = realloc(buf, cap);
			assert(p);
			buf = p;
		}
		buf[len] = (char)z;
		len += 1;
	}

	if (z == EOF && len == 0) { free(buf); return 0; }   // konec souboru

	buf[len] = 0;
	return buf;
}

Použití:

char *line;
while ((line = read_line(in)))
{
	printf("[%s]\n", line);
	free(line);
}

Návrh rozhraní je tu podstatnější než kód. Funkce vrací nově alokovanou paměť, takže musí být jasně řečeno, že ji uvolňuje volající. Alternativou by bylo předat buffer a jeho velikost odkazem a nechat funkci, aby si ho v případě potřeby zvětšila — to je přesně to, co dělá POSIXová getline.

Kapacitu zdvojnásobujeme, takže je celkový počet realokací logaritmický v délce řádku.

Rozlišení „prázdný řádek“ versus „konec souboru“ je v podmínce z == EOF && len == 0. Prázdný řádek vrátí prázdný řetězec, konec souboru vrátí 0.

Úkol 3

Nastudujte v referenční příručce a vyzkoušejte funkci fgets.

char *fgets(char *str, int count, FILE *stream);

Načte nejvýše count - 1 znaků do str a ukončí ho nulou. Čtení skončí na znaku nového řádku (ten se do bufferu uloží) nebo na konci souboru. Vrací str, nebo 0, pokud se nenačetlo nic.

char buf[64];
while (fgets(buf, sizeof(buf), in))
{
	printf("[%s]", buf);
}

Dvě věci, které je potřeba vědět:

  • Znak \n v bufferu zůstává. Chceme-li ho odstranit, hledá se ručně.
  • Nevešel-li se řádek celý, fgets skončí bez \n a zbytek řádku zůstává v proudu. Rozlišit obojí lze právě podle přítomnosti \n.
//
// vraci 1, pokud se cely radek vesel
//
int read_whole_line(FILE *in, char *buf, int size)
{
	if (!fgets(buf, size, in)) { return 0; }

	size_t len = strlen(buf);
	if (len > 0 && buf[len-1] == '\n') { buf[len-1] = 0; return 1; }
	return 0;    // radek byl delsi nez buffer
}

Funkce gets (bez f) dělala totéž bez omezení velikosti, a proto z jazyka byla v C11 odstraněna. Nikdy ji nepoužívejte.

Úkol 4

Napište program, který uloží kopii textového souboru tak, že jeden řádek bude mít maximálně 80 znaků, nerozdělí žádné slovo kratší než 80 znaků a minimalizuje počet řádků.

void wrap_file(char *src, char *dst, int width)
{
	FILE *in  = fopen(src, "r"); assert(in);
	FILE *out = fopen(dst, "w"); assert(out);

	int  col = 0;           // kolik znaku uz je na aktualnim radku
	char word[1024];
	int  wl = 0;            // delka rozepsaneho slova
	int  z;

	while (1)
	{
		z = getc(in);
		int is_sep = (z == EOF || z == ' ' || z == '\n' || z == '\t');

		if (!is_sep)
		{
			if (wl < 1023) { word[wl] = (char)z; wl += 1; }
			continue;
		}

		if (wl > 0)
		{
			//
			// vejde se slovo i s oddelujici mezerou na aktualni radek?
			//
			if (col > 0 && col + 1 + wl > width) { fputc('\n', out); col = 0; }
			else if (col > 0) { fputc(' ', out); col += 1; }

			fwrite(word, 1, (size_t)wl, out);
			col += wl;
			wl = 0;
		}

		if (z == EOF) { break; }
	}

	if (col > 0) { fputc('\n', out); }
	fclose(in);
	fclose(out);
}

Algoritmus je hladový: slovo dáme na aktuální řádek, pokud se tam i s oddělující mezerou vejde, jinak začneme nový. Tím je počet řádků minimální.

Klíčová je podmínka col + 1 + wl > width: ta jednička je mezera, kterou bychom museli před slovo napsat. Na začátku řádku (col == 0) se mezera nepíše.

Původní rozdělení na řádky se ztrácí, což zadání dovoluje. Chtěli-li bychom zachovat odstavce, museli bychom si pamatovat prázdné řádky.

Řešení výše je omezeno na slova délky nejvýše 1024 znaků (delší slova rozdělí). Abychom dostali verzi, ve které je délka slova neomezena, musíme nahradit pole word dynamickým polem.

Úkol 5

Načtení, zrcadlení a uložení obrázku ve formátu PBM.

Formát načteme jako proud položek, nikoliv po řádcích. Ušetříme si tím několik starostí: nepotřebujeme buffer pro ukládání řádků i starost o to, kde přesně řádky končí. Navíc získáme řešení, které zvládne i soubory, jejichž řádkování zadání nepředpokládá.

typedef struct
{
	int            width;
	int            height;
	unsigned char *data;    // radky za sebou, jeden bajt na pixel
} Picture;

První vrstva: znak po znaku, komentáře neviditelné

//
// vrati dalsi znak souboru, komentare (# az do konce radku) preskoci
//
static int pbm_next_char(FILE *in)
{
	int z = getc(in);
 
	while (z == '#')
	{
		while ((z = getc(in)) != EOF && z != '\n') { }   // zbytek radku zahodime
		if (z != EOF) { z = getc(in); }                  // a vezmeme dalsi znak
	}
	return z;
}
 
static int pbm_is_space(int z)
{
	return z == ' ' || z == '\t' || z == '\n' || z == '\r' || z == '\v' || z == '\f';
}
 
//
// prvni znak, ktery neni bily ani soucasti komentare
//
static int pbm_skip_space(FILE *in)
{
	int z;
	do { z = pbm_next_char(in); } while (pbm_is_space(z));
	return z;
}

Jakmile čteme pomocí pbm_next_char, komentáře pro zbytek programu prostě neexistují — nikde jinde už se na # nemusíme ptát.

Druhá vrstva: čtení jednotlivých položek

static void pbm_expect_magic(FILE *in)
{
	int a = pbm_skip_space(in);
	int b = pbm_next_char(in);       // mezi P a 1 mezera byt nesmi
	assert(a == 'P' && b == '1');
}
 
static int pbm_read_int(FILE *in)
{
	int z = pbm_skip_space(in);
	assert(z >= '0' && z <= '9');
 
	int value = 0;
	while (z >= '0' && z <= '9')
	{
		assert(value <= (2000000000 - 9) / 10);   // at nepreteceme
		value = value * 10 + (z - '0');
		z = pbm_next_char(in);
	}
 
	//
	// z uz k cislu nepatri, vratime ho do proudu pro dalsi cteni
	//
	if (z != EOF) { ungetc(z, in); }
	return value;
}
 
static int pbm_read_pixel(FILE *in)
{
	int z = pbm_skip_space(in);
	assert(z == '0' || z == '1');
	return z - '0';
}

Funkce ungetc vrátí jeden znak zpět do proudu, takže ho příští čtení dostane znovu. Norma zaručuje, že jeden znak vrátit lze vždy; na víc se spoléhat nelze. Je to standardní způsob, jak si při ručním rozboru poradit s tím, že konec čísla poznáme až podle znaku, který za ním následuje.

Kontrola přetečení v pbm_read_int není zbytečná: rozměry čteme ze souboru, tedy z dat, kterým nemůžeme věřit. Bez ní by stačil soubor s dostatečně dlouhým číslem a šlo by o nedefinované chování.

Třetí vrstva: obrázek

Picture load_from_pbm(char *filename)
{
	FILE *in = fopen(filename, "r");
	assert(in);
 
	Picture p;
	pbm_expect_magic(in);
	p.width  = pbm_read_int(in);
	p.height = pbm_read_int(in);
	assert(p.width > 0 && p.height > 0);
 
	p.data = malloc((size_t)p.width * (size_t)p.height);
	assert(p.data);
 
	//
	// pixely ctame jako souvislou posloupnost, na radkovani nezalezi
	//
	for (int i = 0; i < p.width * p.height; i += 1)
	{
		p.data[i] = (unsigned char)pbm_read_pixel(in);
	}
 
	fclose(in);
	return p;
}

Data držíme v jednorozměrném poli a indexujeme y * width + x, stejně jako u matice v kapitole Paměť. Protože se pixely čtou jako jeden dlouhý proud, je úplně jedno, jak jsou v souboru rozdělené do řádků. Všimněte si, že assert v pbm_read_pixel odhalí i soubor, ve kterém je pixelů méně, než slibuje hlavička: čtení narazí na EOF, který číslicí není.

void mirror_picture(Picture *p)
{
	for (int y = 0; y < p->height; y += 1)
	{
		for (int x = 0; x < p->width / 2; x += 1)
		{
			unsigned char t = p->data[y*p->width + x];
			p->data[y*p->width + x] = p->data[y*p->width + p->width-1-x];
			p->data[y*p->width + p->width-1-x] = t;
		}
	}
}
 
void save_to_pbm(char *filename, Picture src)
{
	FILE *out = fopen(filename, "w");
	assert(out);
 
	fprintf(out, "P1\n%i %i\n", src.width, src.height);
 
	for (int y = 0; y < src.height; y += 1)
	{
		for (int x = 0; x < src.width; x += 1)
		{
			fprintf(out, "%i", src.data[y*src.width + x]);
			if (x + 1 < src.width) { fputc(' ', out); }
		}
		fputc('\n', out);
	}
	fclose(out);
}
 
void destroy_picture(Picture *p)
{
	free(p->data);
	p->data = 0;
	p->width = 0;
	p->height = 0;
}

Zrcadlení běží jenom do poloviny řádku — kdybychom šli přes celý, prohodili bychom každou dvojici dvakrát a obrázek by zůstal beze změny.

Úkol 6

Binární formát pro ukládání řetězců: funkce append a search.

void append(char *string, char *filename)
{
	FILE *out = fopen(filename, "ab");     // "a" pripoji na konec
	assert(out);

	size_t len = strlen(string);
	assert(len <= 65535);                  // do dvou bajtu se vic nevejde

	//
	// delku zapisujeme po bajtech, abychom nezavisel na endianite
	// (little endian: nizsi bajt prvni)
	//
	unsigned char hdr[2];
	hdr[0] = (unsigned char)(len & 0xFF);
	hdr[1] = (unsigned char)((len >> 8) & 0xFF);

	fwrite(hdr, 1, 2, out);
	fwrite(string, 1, len, out);
	fclose(out);
}

long search(char *string, char *filename)
{
	FILE *in = fopen(filename, "rb");
	if (!in) { return -1; }

	size_t target = strlen(string);
	long   offset = 0;

	unsigned char hdr[2];
	char          buf[65536];

	while (fread(hdr, 1, 2, in) == 2)
	{
		size_t len = (size_t)hdr[0] | ((size_t)hdr[1] << 8);

		if (fread(buf, 1, len, in) != len) { break; }   // poskozeny soubor

		if (len == target && memcmp(buf, string, len) == 0)
		{
			fclose(in);
			return offset;
		}
		offset += 2 + (long)len;
	}

	fclose(in);
	return -1;
}

Délku skládáme z jednotlivých bajtů ručně místo fwrite(&len, sizeof(len), 1, out). Kdybychom zapsali size_t přímo, závisel by formát souboru na velikosti typu i na endianitě dané platformy — a soubor by nešel přenést jinam.

Porovnáváme nejdřív délky a teprve potom obsah. Tím se ušetří většina volání memcmp a zároveň se ověří, že porovnáváme stejně dlouhé bloky.

Soubor je nutné otevřít v binárním režimu ("rb", "ab"). V textovém režimu by se na Windows bajt 0x0A převáděl na dvojici a offsety by neseděly.

Úkol 7

Naprogramujte funkce RLE_encode a RLE_decode.

void RLE_encode(char *in_path, char *out_path)
{
	FILE *in  = fopen(in_path, "rb");  assert(in);
	FILE *out = fopen(out_path, "wb"); assert(out);

	int cur = getc(in);
	while (cur != EOF)
	{
		//
		// spocitame delku behu stejnych bajtu
		//
		long n = 1;
		int  next;
		while ((next = getc(in)) == cur) { n += 1; }

		//
		// do jedne dvojice se vejde nejvyse 256 opakovani,
		// do druheho bajtu ukladame n-1
		//
		while (n >= 256)
		{
			fputc(cur, out);
			fputc(255, out);
			n -= 256;
		}
		if (n > 0)
		{
			fputc(cur, out);
			fputc((int)(n - 1), out);
		}

		cur = next;
	}
	fclose(in);
	fclose(out);
}

void RLE_decode(char *in_path, char *out_path)
{
	FILE *in  = fopen(in_path, "rb");  assert(in);
	FILE *out = fopen(out_path, "wb"); assert(out);

	int x;
	while ((x = getc(in)) != EOF)
	{
		int c = getc(in);
		assert(c != EOF);            // liche mnozstvi bajtu = poskozeny soubor

		for (int i = 0; i <= c; i += 1) { fputc(x, out); }
	}
	fclose(in);
	fclose(out);
}

Konstrukce while (n >= 256) řeší okrajový případ ze zadání elegantněji než počítání n / 256 a n % 256: pro n dělitelné 256 se prostě poslední dvojice nezapíše, protože po odečtení zbyde nula. Právě to je ta výjimka „poslední dvojici vynecháme, pokud je r == 0“ — kdybychom zapsali (x, 0), uložili bychom do druhého bajtu hodnotu −1.

Dekódování je jednodušší: čteme po dvojicích a c + 1 krát vypíšeme bajt. Cyklus proto běží i <= c, nikoliv i < c.

Všimněte si, že RLE může data i zvětšit — soubor bez opakování bude po zakódování dvakrát delší. Skutečné kompresní formáty proto kombinují RLE s dalšími technikami nebo si u každého bloku pamatují, zda se vyplatilo ho komprimovat.

Úkol 8

Program, který čte ze standardního vstupu a zapisuje na standardní výstup, přičemž převede malá písmena na velká.

#include <stdio.h>

int main()
{
	int z;
	while ((z = getc(stdin)) != EOF)
	{
		if (z >= 'a' && z <= 'z') { z = z - 'a' + 'A'; }

		if (fputc(z, stdout) == EOF)
		{
			fprintf(stderr, "chyba zapisu\n");
			return 1;
		}
	}
	return 0;
}

Návratová hodnota getc je opět v int, nikoliv v char — jinak by bajt 0xFF nešel odlišit od EOF.

Úkol 9

Funkce copy_file, která kopíruje soubor po blocích o velikosti 4096 bajtů.

void copy_file(char *src, char *dst)
{
	FILE *in  = fopen(src, "rb"); assert(in);
	FILE *out = fopen(dst, "wb"); assert(out);

	unsigned char buf[4096];
	size_t n;

	//
	// fread vraci pocet SKUTECNE prectenych prvku,
	// posledni blok tak vyjde kratsi sam od sebe
	//
	while ((n = fread(buf, 1, sizeof(buf), in)) > 0)
	{
		size_t written = fwrite(buf, 1, n, out);
		assert(written == n);
	}

	assert(!ferror(in));    // cyklus mohl skoncit i chybou, ne jen koncem souboru
	fclose(in);
	fclose(out);
}

Neúplný poslední blok se řeší sám: velikost prvku je 1 bajt a počet prvků 4096, takže fread vrátí kolik zbylo, a přesně tolik zapíšeme. Kdybychom to obrátili (velikost prvku 4096, počet 1), vrátila by funkce na konci nulu a poslední neúplný blok bychom ztratili.

Cyklus končí i při chybě čtení, proto je za ním ferror. Bez té kontroly by se poškozený soubor tvářil jako úspěšně zkopírovaný.

Soubor je nutné otevřít v binárním režimu. V textovém by se na Windows převáděly konce řádků a kopie by nebyla přesná.

Úkol 10

Funkce find_byte hledající offset prvního výskytu bajtu, a varianta hledající od konce.

long find_byte(char *filename, unsigned char b)
{
	FILE *in = fopen(filename, "rb");
	if (!in) { return -1; }

	long offset = 0;
	int  z;

	while ((z = getc(in)) != EOF)
	{
		if ((unsigned char)z == b) { fclose(in); return offset; }
		offset += 1;
	}

	fclose(in);
	return -1;
}

Hledání od konce se neobejde bez fseek a ftell:

long find_byte_last(char *filename, unsigned char b)
{
	FILE *in = fopen(filename, "rb");
	if (!in) { return -1; }

	if (fseek(in, 0, SEEK_END) != 0) { fclose(in); return -1; }
	long pos = ftell(in);           // velikost souboru v bajtech

	while (pos > 0)
	{
		pos -= 1;
		fseek(in, pos, SEEK_SET);
		int z = getc(in);
		if (z != EOF && (unsigned char)z == b) { fclose(in); return pos; }
	}

	fclose(in);
	return -1;
}

Kombinace fseek(SEEK_END) a ftell je standardní způsob, jak zjistit velikost souboru. Funguje spolehlivě jenom v binárním režimu — v textovém je hodnota z ftell neprůhledná a nemusí odpovídat počtu bajtů.

Pozor na pořadí v cyklu: nejdřív pos -= 1, potom čtení. Po SEEK_END totiž pos ukazuje za poslední bajt, kde už žádný není.

Porovnání (unsigned char)z == b je podstatné. getc vrací hodnotu v rozsahu 0 až 255, ale kdybychom si ji uložili do char, na platformě se znaménkovým char by se z hodnot nad 127 staly záporné a porovnání by selhalo.

Hledání od konce je výrazně pomalejší — pro každý bajt se volá fseek. U velkého souboru by se vyplatilo číst bloky odzadu a prohledávat je v paměti.

Úkol 11

Funkce, které se předá jméno souboru a číslo sloupce a která vypíše součet hodnot v tomto sloupci; řádky bez daného sloupce přeskočí a jejich počet vypíše na stderr.

void sum_column(char *filename, int column)
{
	FILE *in = fopen(filename, "r");
	assert(in);

	char   line[4096];
	double total   = 0;
	long   skipped = 0;

	while (fgets(line, sizeof(line), in))
	{
		int   col   = 0;
		char *start = line;
		int   found = 0;

		for (char *p = line; ; p += 1)
		{
			if (*p == ';' || *p == '\n' || *p == 0)
			{
				if (col == column)
				{
					//
					// docasne ukoncime pole nulou, aby strtod vedelo, kde skoncit
					//
					char save = *p;
					*p = 0;

					char  *end;
					double v = strtod(start, &end);
					if (end != start) { total += v; found = 1; }

					*p = save;
				}
				col   += 1;
				start  = p + 1;
				if (*p == '\n' || *p == 0) { break; }
			}
		}

		if (!found) { skipped += 1; }
	}

	fclose(in);

	printf("soucet sloupce %i: %f\n", column, total);
	fprintf(stderr, "preskocenych radku: %ld\n", skipped);
}

Funkce strtod ze stdlib.h převede začátek řetězce na double a přes parametr end řekne, kam až se dostala. Je-li end rovno start, nepřevedla se ani jedna číslice, což poznáme jako chybějící nebo nečíselnou hodnotu. Tím je ošetřen i řádek x;y.

Trik s dočasným zápisem nuly a jejím obnovením je běžný postup, jak knihovní funkci podstrčit část řetězce bez kopírování. Používat ho lze jen na zapisovatelný buffer — u řetězcového literálu by šlo o nedefinované chování.

Kdybychom se chtěli vyhnout bufferu fixní velikosti a použití knihovní funkce strtod, můžeme tak učinit následovně:

Řádek si nikam neukládáme. Čteme znak po znaku a číslo skládáme průběžně, jak znaky přicházejí — díky tomu nepotřebujeme ani buffer, ani žádnou funkci na rozbor textu. Vystačíme si s stdio.h a assert.h.

Inkrementální rozbor čísla

Stav rozepsaného čísla si držíme ve struktuře. Číslo se skládá ze znaménka, číslic před tečkou a číslic za ní; navíc si pamatujeme, zda dosud nepřišlo něco, co do čísla nepatří.

typedef struct
{
	int    valid;      // zatim nic neodporuje zapisu cisla
	int    has_digit;
	int    sign_seen;
	int    neg;
	int    after_dot;
	int    decimals;   // pocet cislic za teckou
	int    ended;      // uz jsme za cislem (nasledovala mezera)
	double value;      // vsechny cislice jako jedno cele cislo
} Number;
 
static void number_reset(Number *n)
{
	n->valid     = 1; 
	n->has_digit = 0; 
	n->sign_seen = 0; 
	n->neg       = 0;
	n->after_dot = 0; 
	n->decimals  = 0; 
	n->ended     = 0; 
	n->value     = 0.0;
}

Jádrem je funkce, které se předá jeden znak a která podle něj stav posune. Jakmile jednou narazí na něco nečekaného, nastaví valid na 0 a dál už jen mlčky vrací.

static void number_feed(Number *n, int c)
{
	if (!n->valid) { return; }
 
	if (c == ' ' || c == '\t' || c == '\r')
	{
		if (n->has_digit) { n->ended = 1; }              // mezera ZA cislem
		else if (n->sign_seen || n->after_dot) { n->valid = 0; }
		return;                                          // jinak mezera PRED cislem
	}
 
	if (n->ended) { n->valid = 0; return; }              // neco az za bílým znakem
 
	if (c == '+' || c == '-')
	{
		if (n->sign_seen || n->has_digit || n->after_dot) { n->valid = 0; return; }
		n->sign_seen = 1;
		n->neg = (c == '-');
		return;
	}
 
	if (c == '.')
	{
		if (n->after_dot) { n->valid = 0; return; }      // druha tecka
		n->after_dot = 1;
		return;
	}
 
	if (c >= '0' && c <= '9')
	{
		n->has_digit = 1;
		n->value = n->value * 10.0 + (c - '0');
		if (n->after_dot) { n->decimals += 1; }
		return;
	}
 
	n->valid = 0;                                        // cokoliv jineho
}

Všimněte si, jak se zachází s desetinnou částí. Číslice nepřičítáme rovnou jako zlomky, ale skládáme je všechny do jednoho celého čísla a počítáme si, kolik jich bylo za tečkou. Teprve na konci vydělíme příslušnou mocninou deseti.

//
// vraci 1, pokud je n platnym cislem; vysledek zapise do out
//
static int number_finish(Number *n, double *out)
{
	if (!n->valid || !n->has_digit) { return 0; }
 
	double p = 1.0;
	for (int i = 0; i < n->decimals; i += 1) { p *= 10.0; }
 
	*out = (n->neg ? -n->value : n->value) / p;
	return 1;
}

Podmínka !n->has_digit odmítne prázdné n i samotné znaménko nebo tečku. Mocninu deseti počítáme cyklem, takže nepotřebujeme pow ani math.h.

Průchod souborem

void sum_column(char *filename, int column)
{
	FILE *in = fopen(filename, "r");
	assert(in);
 
	double total   = 0.0;
	long   skipped = 0;
 
	int    col   = 0;
	int    found = 0;    // mel tento radek platnou hodnotu v hledanem sloupci?
	int    any   = 0;    // obsahuje radek vubec neco?
	Number num;
	number_reset(&num);
 
	while (1)
	{
		int z = getc(in);
 
		if (z != '\n' && z != EOF) { any = 1; }
 
		if (z == ';' || z == '\n' || z == EOF)
		{
			//
			// konec pole v souboru: je-li to ten hledany sloupec, zkusime ho pouzit
			//
			if (col == column)
			{
				double v;
				if (number_finish(&num, &v)) { total += v; found = 1; }
			}
			col += 1;
			number_reset(&num);
 
			//
			// konec radku
			//
			if (z == '\n' || z == EOF)
			{
				if (any && !found) { skipped += 1; }
				col = 0; found = 0; any = 0;
				if (z == EOF) { break; }
			}
			continue;
		}
 
		number_feed(&num, z);
	}
 
	fclose(in);
 
	printf("soucet sloupce %i: %f\n", column, total);
	fprintf(stderr, "preskocenych radku: %ld\n", skipped);
}

Celý program je jeden průchod souborem, ve kterém se nikdy nedrží víc než jeden znak. Paměťová náročnost je proto konstantní bez ohledu na délku řádku.

Konec pole a konec řádku se zpracovávají na témže místě, protože oba znamenají „právě dočtené pole je hotové“. Rozdíl je jen v tom, že konec řádku navíc vyhodnotí celý řádek a vynuluje čítač sloupců. Díky tomu funguje i soubor, který posledním řádkem nekončí znakem nového řádku — větev pro EOF je tatáž jako pro '\n'.

Příznak any odliší skutečný řádek od prázdného posledního řádku, který by se jinak započítal jako přeskočený.

Rozbor nepodporuje exponenciální zápis (1e5) ani jiný desetinný oddělovač než tečku. Obojí by šlo doplnit rozšířením number_feed o další stavy.

Velmi dlouhá čísla přesnost typu double postupně vyčerpají, stejně jako u kterékoliv jiné varianty. Na rozdíl od strtod tu ovšem nedochází k nedefinovanému chování, jen k zaokrouhlení.

Preprocesor

Preprocesor je součást překladače, která zpracovává zdrojový kód před tím, než je dále zpracován. Na zdrojovém kódu provádí textové operace (mazání, vkládání a nahrazování textu). Tyto operace jsou prováděny s následujícími cíly

  • smazání komentářů,
  • expanze maker,
  • podmíněný překlad (podmíněné vynechání/vložení části kódu),
  • vložení obsahu jiných souborů.

Překladače umožňují prohlédnout si kód poté, co je zpracován preprocesorem. Pro překladač gcc toho lze dosáhnout pomocí

gcc -E -P source.c

Je důležité si uvědomit, že preprocesor je součástí překladu. Nemůže proto například znát hodnoty proměnných, ty jsou známy až za běhu programu. Provádí jenom textové substice.

Části kódu určené pro preprocesor, jsou uvozené znakem #. Ty, kterým preprocesor rozumí říkáme direktivy preprocesoru.

Makra

Makra definujeme pomocí #define. Lze také oddefinovat pomocí #undef macro_name.

Makra bez argumentů — definice symbolů

Makro definujeme následovně

#define name replacement

Preprocesor v kódu nahradí text name textem replacement. Nenahrazuje výskyty name, které jsou součástí identifikátorů, klíčových slov nebo se vyskytují v řetězcových literálech.

Například kód

#define PI 3.14
#define ERROR  printf("Chyba") 

double circle_perimeter(double r)
{
	if (r > 0) { return 2 * PI * r; }	
	ERROR;
	return -1;
}

předělá preprocesor na následující.

double circle_perimeter(double r)
{
	if (r > 0) { return 2 * 3.14 * r; }
	printf("Chyba");
	return -1;
}

Symbol lze definovat i bez replacement části. Říkáme tím pouze, že symbol je definován (o použití více později).

V případě, že replacement chceme napsat na více řádků, lze řádek zlomit pomocí znaku \ (zpětné lomítko), přitom je to bráno jako jeden řádek.

#define HELLO printf("%s %s", \
                     "hello", \
					 "world")

Podle standardu existují některá předdefinovaná makra, například

  • __LINE__, expanduje na číslo řádku, na kterém se makro nachází (celočíselný literál)
  • __FILE__, expanduje na jméno souboru (řetězcový literál)
  • __DATE__, expanduje na aktuální datum (řetězcový literál)
  • __TIME__, expanduje na aktuální čas (řetězcový literál)

Další makra může definovat konkrétní překladač. Například seznam maker definovaných překladačem gcc lze vytisknout pomocí následujího řádku pro shell.

echo "" | gcc -dM -E -

Makra s argumenty

#define name(parameter-list) replacement

parameter-list je čárkami oddělený seznam jmen parametrů. Při expanzi jsou jména parametrů vyskytující se v replacement (s jednou výjimkou) nahrazena argumenty makra.

// makro pro druhou mocninu
#define square(x)  x*x

// "volani" makra
square(value) 
// vede k expanzi na
value*value

Při psaní maker je nutno dávat si pozor na mnohá nebezpečí, například následující použití makra square vede k nečekanému výsledku.

square(x+1)
// expanduje na 
x+1*x+1
// a to je něco jiného, než očekáváme: hodnota výrazu je 2x + 1, nikoliv (x+1)*(x+1)

Při psaní maker je tak výhodné využívat uzávorkování v maximální míře: závorky okolo jmen argumentů a závorky okolo celého výrazu.

#define square(x) ((x)*(x))

Problémem může být také situace, kdy je parametr makra použit v jeho těle vícekrát, a příslušný argument je výraz s vedlejším efektem.

#define square(x) ((x)*(x))

int foo(int x)
{
	printf("launching %i misiles\n", x);
	return x;
}

// výraz
int y = square(foo(3));
// expanduje na 
int y = ((foo(3))*(foo(3)));
// a vedlejší efekt tak nastane dvakrát

Řešením je psát makra tak, aby se v jejich těle parametry neopakovaly, nebo jim nepředávat jako argumenty výrazy s vedlejším efektem.

int z = foo(3); // vedlejší efekt nastane pouze jednou
int y = square(z);

Výraz #par v těle makra, kde par je jméno parametru, expanduje na řetězcový literál, jehož obsahem je příslušný argument.

#define print_value(val) printf("value of " #val " is %f", val)

int main()
{
	double x = 3;
	double y = 4;
	print_value(x + y);
	return 0;
}

// expanduje na 
int main()
{
	double x = 3;
	double y = 4;
	printf("value of x + y is %f", x + y);
	return 0;
}	

Výraz a ## b expanduje na ab. Toto lze využít k vytváření nových identifikátorů spojováním slov.

#define VAR(x) variable_ ## x

VAR(10)   // expanduje na variable_10
VAR(f)   // expanduje na variable_f 

S výhodou to lze použít pro automatické vytváření specializovaných verzí generických funkcí (viz kapitola 3)

// funkce prohodí n bajtů na adresách a,b
void mem_swap(void *a, void *b, size_t n);

//specializacni makro
#define define_swap(type) void swap_ ## type (type *a, type *b) {\
	                        mem_swap(a, b, sizeof(type));            \
						  }
define_swap(double)
// expanduje na
void swap_double(double *a, double *b) {
	mem_swap(a, b, sizeof(double));
}

Operátory # a ## vypadají na první pohled jako práce s textem, ale nejsou to ony. Preprocesor totiž nepracuje se znaky, nýbrž s tokeny — s nejmenšími smysluplnými částmi programu, jako je identifikátor, číslo, řetězcový literál nebo operátor. Z toho plynou dvě omezení, na která se naráží velmi snadno.

Spojuje se jenom jeden token.
Operátor ## spojí poslední token svého levého operandu s prvním tokenem operandu pravého. Zbytek pravého operandu zůstane, kde byl. Vraťme se k makru define_swap z předchozího příkladu.

#define define_swap(type) void swap_ ## type (type *a, type *b) {\
	                        mem_swap(a, b, sizeof(type));         \
						  }

Pro jednoslovné typy funguje bez problémů.

define_swap(double)
 
//
// vznikne:
// void swap_double (double *a, double *b) { mem_swap(a, b, sizeof(double)); }
//

Jakmile má ale jméno typu více než jeden token, makro se rozpadne.

define_swap(unsigned int)
 
//
// swap_ se spoji pouze s tokenem unsigned, token int zustane za nim
// vznikne:
// void swap_unsigned int (unsigned int *a, ...)
//
// coz je syntakticka chyba
//

define_swap(char *)
 
//
// vznikne:
// void swap_char *(char * *a, ...)
//
// opet syntakticka chyba
//

Řešením je vytvořit si k takovému typu nejdřív jednoslovný alias pomocí typedef.

typedef unsigned int uint;
typedef char        *cstring;
 
define_swap(uint)      // vznikne swap_uint
define_swap(cstring)   // vznikne swap_cstring

Spojený výsledek musí být navíc platný token. Spojíme-li swap_ a double, vznikne identifikátor swap_double, což platný token je. Kdybychom se ale pokusili spojit například číslo a znaménko, žádný token by nevznikl a šlo by o nedefinované chování.

#define paste(a, b) a ## b
 
paste(swap_, double)   // v poradku, vznikne identifikator swap_double
paste(1, 5)            // v poradku, vznikne cislo 15
paste(1, +)            // nedefinovane chovani, "1+" neni jeden token

Argumenty u # a ## se nerozvíjejí.
Druhé omezení je méně nápadné a překvapí i zkušené programátory. Obvykle platí, že argument makra se nejdřív sám rozvine a teprve rozvinutý se dosadí do těla. To ale neplatí pro argument, který v těle sousedí s operátorem # nebo ## — ten se dosadí tak, jak byl napsán.

Podívejme se na makro print_value z předchozího příkladu. Parametr val se v jeho těle vyskytuje dvakrát: jednou za operátorem # a jednou samostatně.

#define print_value(val) printf("value of " #val " is %f", val)
#define LIMIT 100.0
 
print_value(LIMIT);
 
//
// prvni vyskyt je za #, proto se nerozvine  -> "LIMIT"
// druhy vyskyt je samostatny, proto se rozvine -> 100.0
//
// vznikne:
// printf("value of " "LIMIT" " is %f", 100.0);
//
// program vytiskne: value of LIMIT is 100.000000
//

Tady je to vlastně přesně to, co chceme — makro má vypsat, jak byl výraz zapsán, a k tomu jeho hodnotu. U operátoru ## ale stejné chování obvykle vadí.

#define CONCAT(a, b) a ## b
#define PREFIX foo
 
CONCAT(PREFIX, _bar)
 
//
// argument a sousedi s ##, proto se PREFIX nerozvine
// vznikne identifikator PREFIX_bar, nikoliv foo_bar
//

Pomůže vložit mezi ně ještě jednu vrstvu. Argument se totiž rozvine při předání do vnitřního makra, protože v těle vnějšího makra už u ## nestojí.

#define CONCAT_(a, b) a ## b        // vnitrni makro, tady se spojuje
#define CONCAT(a, b)  CONCAT_(a, b) // vnejsi makro, tady se rozvine
 
#define PREFIX foo
 
CONCAT_(PREFIX, _bar)   // vznikne PREFIX_bar
CONCAT(PREFIX, _bar)    // vznikne foo_bar

V těle makra CONCAT nesousedí a ani b s žádným ##, takže se oba argumenty rozvinou — z PREFIX se stane foo. Teprve potom se zavolá CONCAT_(foo, _bar), které je spojí.

Totéž platí pro stringifikaci.

#define STR_(x) #x
#define STR(x)  STR_(x)
 
#define VERSION 3
 
printf("%s\n", STR_(VERSION));   // vytiskne VERSION
printf("%s\n", STR(VERSION));    // vytiskne 3

Tento dvojitý zápis vypadá jako zbytečné zdvojení, ale je to standardní a všeobecně používaný postup.

Nejčastější použití je vytvoření jména, které je v každém řádku jiné. Připomeňme, že __LINE__ je předdefinované makro s číslem aktuálního řádku.

#define UNIQUE_(a, b) a ## b
#define UNIQUE(a, b)  UNIQUE_(a, b)
 
int UNIQUE(tmp, __LINE__);
 
//
// bez mezivrstvy by vznikl identifikator tmp__LINE__
// s ni vznikne napriklad tmp42
//

Je-li v těle makra operátorů ## několik za sebou, norma neurčuje, v jakém pořadí se provedou. Různé překladače se mohou chovat různě.

#define TRIPLE(a, b, c) a ## b ## c   // radeji se tomu vyhneme

V praxi to obvykle dopadne dobře, ale spolehnout se na to nelze. Potřebujeme-li spojit více tokenů, je bezpečnější použít vnořená volání.

Nyní diskutovaná pravidla shrneme:

Pravidlo                                  Dusledek
------------------------------------------------------------------------
## spojuje jen prvni token argumentu      pro viceslovne typy pouzijeme
                                          typedef
 
vysledek musi byt platny token            nelze spojovat libovolne znaky
 
argument u # a ## se nerozvine            potrebujeme-li rozvinutou
                                          hodnotu, pridame makro navic
 
poradi vice ## je nespecifikovane         vice ## v jednom tele radeji ne
------------------------------------------------------------------------

Nakonec krátké shrnutí procesu expanze maker

  1. Pokud se jedná o makro s argumenty, nejdříve prozkoumáme jestli předané argumenty obsahují makra. Pokud ano, jsou expandována.
  2. Do textu je vložena textová náhrada makra z jeho definice. U makra s argumenty jsou jména parametrů nahrazena expanzemi argumentů, a jsou provedeny převody na řetězcové literály a spojení pomocí ##.
  3. Pokud se ve výsledném textu nachází nějaká makra, jsou expandována.

Variadická makra

Makra s proměnným počtem argumentů. Například

#define pprint(fmt, ...) printf("[pretty] " fmt, __VA_ARGS__) 

Nepovinné argumenty v těle nahrazují __VA_ARGS__. Například

pprint("%i %i", i, j);

expanduje na

printf("[pretty] " "%i %i", i, j);

Pozor na případ, kdy nepovinné argumenty žádné nejsou. Makro pprint výše se pak rozvine na printf("[pretty] " fmt, ) s přebytečnou čárkou, což je chyba překladu. Norma C23 na to má __VA_OPT__(,), které čárku vloží jenom tehdy, když nějaký argument následuje. Překladače gcc a clang navíc podporují starší zápis, ##__VA_ARGS__, který čárku naopak odstraní, není-li co za ní.

Podmíněný překlad

#if constant_expression
   // code
#endif

constant_expression je výraz obsahující literály, logické operátory a operátory porovnání, operátor defined a případně již definovaná makra, která se na ně expandují. Pokud je hodnota constant_expression nenulová, je část mezi #if a #endif v kódu ponechána, jinak je vypuštěna. Výraz lze doplnit o #else a #elif s obvyklým významem.

Lze použít se speciálním výrazem defined(symbol), který je nenulový, pokud je symbol již definované makro. Toto lze využít k zařazení či vynechání kódu určeného pro specifické situace (debug/release build atd.) nebo platformy. Definovat jednoduchá makra lze totiž i přímo pomocí přepínačů překladače, například pro gcc

# definuje makro Symbol
gcc -DSymbol

# definuje makro Symbol na literál 10
gcc -DSymbol=10

Příklad použití

#if defined(WIN)
	// kód specifický pro windows
#elif defined(UNIX)
	// kód specifický pro unix
#else
	// kód pro ostatní platformy, případně vyvolání chyby
	// viz níže
#endif

Výše používáme operátor defined, který je pravdivý, pokud je jeho argument již definované makro.

// #if defined(SYMB)  lze nahradit #ifdef SYMB
// #if !defined(SYMB) lze nahradit #ifndef SYMB

S podmíněným překladem můžeme výhodně použít dalších direktiv preprocesoru

  • #error message při překladu se vytiskne message a je vyvolána chyba překladu
  • #warning message při překladu je vypsáno message jako warning

Například v části #else předchozího příkladu bychom mohli dát

#error Won't compile on unsupported platforms. We support only windows and unix.

Vložení obsahu jiných souborů

Direktiva #include slouží k vložení obsahu jiného souboru do zdrojového kódu. Soubor, který se má vložit, lze specifikovat dvojím způsobem

#include <path-to-file>
#include "path-to-file"

V prvním případě je soubor hledán v předdefinovaných adresářích (ty jsou dány nastavením systému, případně je lze předat jako argumenty překladači) a path-to-file je brána jako relativní cesta z nějakého takového adresáře. Druhém případě se nejdříve prohledává adresář, ve kterém je uložen aktuální soubor (obsahující #include). (Někdy se poté vyhledávají předdefinované adresáře.

Obvykle je nutno zabránit tomu, aby byl obsah nějakého souboru vložen vícekrát. (To hrozí například pokud vkládáme dva různé soubory, které samy obsahují directivu #include, která vkládá stejný soubor.) K tomu lze výhodně využít podmíněného překladu (předpokládejme například, že jde o soubor myheader.h)

#if !defined(MY_HEADER_H)
#define MY_HEADER_H

// kód

#endif

U většiny překladačů také funguje vložení následující direktivy.

#pragma once

Pragma

Mimo výše zmiňované direktivy existují i další. Například direktiva #pragma, jejíž syntax i sémantika je závislá na konkrétním překladači.

Úkoly

  1. Napište makro

    is_digit(base, character)
    

    pro testování, zda je znak určený výrazem character číslicí soustavy se základem daným base. Makro musí korektně fungovat pro základy soustav od 2 do 36 a libovolné znaky. Pro číslice s hodnotou vyšší než 9 používejte pro jednoduchost pouze velká písmena anglické abecedy.

    Příklady použití

    if (is_digit(8,'8') != 0) { printf("Ano\n"); } 
    else                      { printf("Ne\n");  }
    
    if (is_digit(10+6, '0'+4) != 0) { printf("Ano\n"); } 
    else                            { printf("Ne\n");  }
    
    if (is_digit(30, '@') != 0) { printf("Ano\n"); } 
    else                        { printf("Ne\n");  }
    
    //
    //  vede k výpisu následujícího
    //
    Ne
    Ano
    Ne
    
  2. Upravte makro

    #define LOG(fmt, ...) printf("[LOG] " fmt "\n", __VA_ARGS__)
    

    tak, aby tisklo i soubor a řádek, na kterém je použito.

  3. K úkolu 7 z kapitoly Funkce doprogramujte makro EMIT_SORT(type), které expanduje na definici třídící funkce pro type. Přitom předpokládejte, že porovnáváci funkce pro type se jmenuje compare_type (např. pro double je to compare_double)

  4. Napište makra MIN(a, b) a MAX(a, b). Ověřte, že fungují správně i pro následující volání, a pokud ne, opravte je.

    MIN(1, 2)
    MIN(x + 1, y)
    MIN(a, b) * 2
    

    Potom vysvětlete, proč je následující volání problematické i u správně napsaného makra, a jak se tomu vyhnout.

    int i = 0;
    int m = MAX(i++, 5);
    
  5. Napište makro ARRAY_SIZE(a), které vrátí počet prvků pole. Vyzkoušejte ho ve funkci main a potom ho zavolejte uvnitř funkce, které bylo pole předáno jako argument. Vysvětlete, proč tam vrátí něco jiného (odkaz na kapitolu Paměť), a rozmyslete si, zda s tím jde něco udělat.

  6. Napište makro DEBUG_PRINT(fmt, ...), které se chová jako printf, ale

    • navíc před text vypíše jméno souboru a číslo řádku,
    • pokud je při překladu definováno makro NDEBUG, expanduje na nic.

    Makro musí fungovat i tehdy, když se volá bez nepovinných argumentů, tedy DEBUG_PRINT("zacatek funkce\n"). Ověřte obojí:

    gcc -std=c17 -Wall -Wextra program.c -o program
    gcc -std=c17 -Wall -Wextra -DNDEBUG program.c -o program
    

    Pomocí gcc -E -P si prohlédněte, jak vypadá kód po preprocesoru v obou případech.

  7. Napište makro STATIC_ASSERT(cond), které způsobí chybu při překladu, pokud je cond nepravdivé, a nic neudělá, je-li pravdivé. Za běhu programu nesmí makro nic stát.

    Nápověda: pole o záporném počtu prvků je chyba při překladu. Makro tedy může expandovat na definici pole, jehož velikost je 1 pro pravdivou podmínku a -1 pro nepravdivou. Aby šlo makro použít vícekrát v jednom souboru, musí mít každé takové pole jiné jméno — použijte __LINE__ a spojování tokenů. Nezapomeňte na to, že argument sousedící s ## se nerozvíjí.

    STATIC_ASSERT(sizeof(int) == 4);     // projde
    STATIC_ASSERT(sizeof(int) == 8);     // chyba pri prekladu
    

    (Norma C11 má pro totéž klíčové slovo _Static_assert, C23 pak static_assert. Vyzkoušejte si i je a porovnejte chybovou hlášku.)

  8. Určete, na co se rozvine každý z následujících výrazů a co program vytiskne. Potom makra opravte tak, aby dělala to, co jejich jméno slibuje.

    #define HALF(x)      x / 2
    #define AREA(w, h)   w * h
    #define IS_ZERO(x)   x == 0
    
    printf("%i\n", HALF(4 + 4));
    printf("%i\n", AREA(2 + 1, 3));
    printf("%i\n", 10 * IS_ZERO(0));
    
  9. K typu ze cvičení o čase (kapitola Základy) napište makro

    DEFINE_GETTER(type, field)
    

    které expanduje na definici funkce vracející danou položku struktury. Například DEFINE_GETTER(Time, hours) vytvoří funkci int Time_get_hours(Time t) { return t.hours; }.

Řešení úkolů

Úkol 1

Napište makro is_digit(base, character) pro testování, zda je znak číslicí soustavy se základem base. Makro musí korektně fungovat pro základy 2 až 36 a libovolné znaky.

#define is_digit(base, character)                          \
	( ( (character) >= '0' && (character) <= '9'           \
	    && (character) - '0'      < (base) )               \
	  ||                                                   \
	  ( (character) >= 'A' && (character) <= 'Z'           \
	    && (character) - 'A' + 10 < (base) ) )

Číslice s hodnotou 0 až 9 se zapisují znaky '0' až '9', číslice s hodnotou 10 až 35 velkými písmeny. Znak je číslicí dané soustavy, pokud do jedné z těchto dvou skupin patří a zároveň je jeho hodnota menší než základ.

Připomeňme příklady ze zadání.

is_digit(8, '8')        // 0: '8' ma hodnotu 8, coz neni < 8
is_digit(10+6, '0'+4)   // 1: v soustave o zakladu 16 je '4' cislici
is_digit(30, '@')       // 0: '@' neni ani cislice, ani velke pismeno

Závorky kolem (base) nejsou ozdoba. Druhý příklad předává jako základ výraz 10+6; bez závorek by se (character) - '0' < base rozvinulo na ... < 10+6, což je díky prioritám (... < 10) + 6 — tedy nesmysl. Totéž platí pro (character), kam se předává '0'+4.

Celý výraz je také uzávorkován, aby is_digit(...) * 2 nebo !is_digit(...) fungovalo podle očekávání.

Parametr character se v těle vyskytuje šestkrát, takže se šestkrát vyhodnotí. Volání is_digit(16, *p++) by posunulo pointer šestkrát — přesně ten případ, před kterým varuje výklad u makra square. Makro proto nikdy nevolejte s výrazem, který má vedlejší efekt.

Jako funkce by šlo o čistší řešení bez obou nástrah:

int is_digit_fn(int base, int character)
{
	int value;
	if (character >= '0' && character <= '9')      { value = character - '0'; }
	else if (character >= 'A' && character <= 'Z') { value = character - 'A' + 10; }
	else { return 0; }
	return value < base;
}

Makro má proti funkci jedinou výhodu: výsledek pro konstantní argumenty spočítá překladač už při překladu, takže ho lze použít i tam, kde je potřeba konstantní výraz.

Úkol 2

Upravte makro LOG tak, aby tisklo i soubor a řádek, na kterém je použito.

#define LOG(fmt, ...) \
	printf("[LOG] %s:%i " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)

Použití a skutečný výstup:

LOG("zacatek");                       // [LOG] program.c:62 zacatek
LOG("hodnota %i a %s", 42, "text");   // [LOG] program.c:63 hodnota 42 a text

Makra __FILE__ a __LINE__ se rozvinou v místě volání, nikoliv v místě definice makra — proto každý řádek vypíše své vlastní číslo. (Proto potřebujeme makro, funkce by vypisovala pořád totéž místo.)

Formátovací řetězec sestavujeme spojením sousedních řetězcových literálů: "[LOG] %s:%i " fmt "\n". To funguje, protože překladač sousedící literály spojí do jednoho. Proto musí být fmt vždy literál, nikoliv proměnná.

Zápis ##__VA_ARGS__ je tu podstatný. Bez něj by volání bez nepovinných argumentů neprošlo:

#define LOG(fmt, ...) printf("[LOG] " fmt "\n", __VA_ARGS__)

LOG("zacatek");
// rozvine se na
printf("[LOG] " "zacatek" "\n", );     // syntakticka chyba

To lze ověřit pomocí gcc -E -P. Operátor ## před __VA_ARGS__ je rozšíření gcc a clang, které v tomto konkrétním případě přebytečnou čárku odstraní.

Norma C23 má pro totéž standardní řešení, které čárku naopak vloží jenom tehdy, když nějaký argument následuje:

#define LOG(fmt, ...) \
	printf("[LOG] %s:%i " fmt "\n", __FILE__, __LINE__ __VA_OPT__(,) __VA_ARGS__)

Tato varianta vyžaduje překlad s -std=c2x nebo novějším.

Úkol 3

K úkolu 7 z kapitoly Funkce doprogramujte makro EMIT_SORT(type), které expanduje na definici třídicí funkce pro type. Předpokládejte, že porovnávací funkce se jmenuje compare_type.

#define EMIT_SORT(type)                                                  \
	void type ## _sort(type *array, int n)                               \
	{                                                                    \
		generic_sort(array, (size_t)n, sizeof(type), compare_ ## type);  \
	}

Použití:

int compare_double(void *a, void *b) { /* ... */ }
int compare_int(void *a, void *b)    { /* ... */ }

EMIT_SORT(double)     // vytvori void double_sort(double *array, int n)
EMIT_SORT(int)        // vytvori void int_sort(int *array, int n)

double d[] = { 3.5, 1.5, 2.5 };
double_sort(d, 3);    // 1.5 2.5 3.5

Operátor ## použijeme dvakrát: jednou pro jméno vznikající funkce (double + _sort), podruhé pro jméno porovnávací funkce (compare_ + double).

Za makrem se nepíše středník — jeho tělo je celá definice funkce, která středníkem nekončí.

Pozor. Makro funguje jenom pro typy, jejichž jméno je jediný token. Pro unsigned int by ## spojilo compare_ pouze s tokenem unsigned a vzniklo by compare_unsigned int, tedy syntaktická chyba. Řešením je alias:

typedef unsigned int uint;

int compare_uint(void *a, void *b) { /* ... */ }
EMIT_SORT(uint)       // vytvori void uint_sort(uint *array, int n)

Za zmínku stojí, že tady žádnou mezivrstvu maker nepotřebujeme. Argumentem je jméno typu, které samo makrem není, takže se nemá co rozvíjet. Kdyby ale někdo napsal

#define MY_TYPE int
EMIT_SORT(MY_TYPE)

vznikly by funkce MY_TYPE_sort a compare_MY_TYPE, protože argument sousedící s ## se nerozvíjí. To by se opravilo právě tím dvojitým zápisem z výkladu:

#define EMIT_SORT_(type) void type ## _sort(type *array, int n) { /* ... */ }
#define EMIT_SORT(type)  EMIT_SORT_(type)

Úkol 4

Napište makra MIN(a, b) a MAX(a, b) a ověřte je na uvedených voláních.

#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define MAX(a, b) ((a) > (b) ? (a) : (b))

Můžeme ověřit, že MIN(1,2) je 1, MIN(3+1,2) je 2 a MIN(5,9) * 2 je 10.

Naivní zápis a < b ? a : b selže na dvou ze tří uvedených volání:

  • MIN(x + 1, y) se rozvine na x + 1 < y ? x + 1 : y. Operátor < má vyšší prioritu než ?:, ale nižší než +, takže tady to shodou okolností projde — zlomí se to ale například u MIN(a & 1, b), protože & má nižší prioritu než <.
  • MIN(a, b) * 2 se rozvine na a < b ? a : b * 2, což je a < b ? a : (b * 2) — tedy něco úplně jiného. Proto je nutná i vnější závorka kolem celého výrazu.

I u správně napsaného makra je

int i = 0;
int m = MAX(i++, 5);

nebezpečné. Parametr a se v těle vyskytuje dvakrát, takže se rozvine na

int m = ((i++) > (5) ? (i++) : (5));

Je-li podmínka pravdivá, i++ se provede dvakrát a i se zvýší o dvě. Tento problém text popisuje u makra square.

Vyhnout se tomu lze jedině tak, že se makrům nepředávají výrazy s vedlejším účinkem:

int tmp = i++;
int m   = MAX(tmp, 5);

Jako funkce by makro tuto vadu nemělo, ale fungovalo by jen pro jeden typ. (Makra MIN/MAX se používají, protože fungují pro více typů.)

Úkol 5

Napište makro ARRAY_SIZE(a). Vyzkoušejte ho v main a uvnitř funkce, které bylo pole předáno jako argument.

#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))

Ve funkci main nad polem int arr[10] vrátí správně 10. Uvnitř funkce

void takes_array(int a[], int n)
{
	printf("%zu\n", ARRAY_SIZE(a));   // vytiskne 2, nikoliv 10
}

vrátí 2.

Důvod je v kapitole Paměť: pole předané funkci degeneruje na pointer, takže parametr a není pole, ale int *. Výraz se proto počítá jako sizeof(int*) / sizeof(int), tedy na 64-bitovém unixu 8 / 4 == 2.

Udělat se s tím nedá nic. Informace o velikosti zaniká už při volání a makro nemá odkud ji vzít. Proto se velikost pole předává jako druhý parametr — a proto to tak dělají všechny funkce v tomto textu. Naštěstí na to gcc s -Wall upozorní:

warning: 'sizeof' on array function parameter 'a' will return size of 'int *'
   [-Wsizeof-array-argument]

Makro se tedy dá bezpečně používat jenom tam, kde je pole vidět jako pole: ve stejném bloku, kde bylo definováno, nebo na globální pole. Přesně tam je ale užitečné, protože po změně velikosti pole není potřeba opravovat žádnou konstantu.

Úkol 6

Napište makro DEBUG_PRINT(fmt, ...), které tiskne jméno souboru a číslo řádku a s definovaným NDEBUG expanduje na nic.

#if defined(NDEBUG)
	#define DEBUG_PRINT(fmt, ...) ((void)0)
#else
	#define DEBUG_PRINT(fmt, ...) \
		printf("[%s:%i] " fmt, __FILE__, __LINE__, ##__VA_ARGS__)
#endif

Ověřený výstup bez NDEBUG:

[program.c:59] bez argumentu
[program.c:60] s argumenty 42 text

S -DNDEBUG se nevypíše nic.

Proč ((void)0) a ne nic? Kdyby makro expandovalo na prázdno, zůstal by po něm v kódu osamocený středník. Ten sice většinou nevadí, ale za if bez závorek

if (x) DEBUG_PRINT("neco\n");
else   neco_jineho();

by prázdná expanze rozbila vazbu else. Výraz ((void)0) je platný příkaz, který nic nedělá, takže tvar kódu zůstane zachován. Přetypování na void navíc říká, že výsledek zahazujeme záměrně.

Proč ##__VA_ARGS__.? Bez něj by volání bez nepovinných argumentů zanechalo přebytečnou čárku:

printf("[%s:%i] " "zacatek\n", "program.c", 59, );   // syntakticka chyba

Operátor ## před __VA_ARGS__ je rozšíření gcc a clang, které čárku v tomto případě odstraní. V normě C23 je pro totéž __VA_OPT__(,):

#define DEBUG_PRINT(fmt, ...) \
	printf("[%s:%i] " fmt, __FILE__, __LINE__ __VA_OPT__(,) __VA_ARGS__)

Makra __FILE__ a __LINE__ se rozvinou v místě volání, takže každý řádek vypíše svoje číslo. Právě proto musí jít o makro a ne o funkci.

Prohlédnout si obě varianty lze pomocí gcc -E -P program.c.

Úkol 7

Napište makro STATIC_ASSERT(cond), které způsobí chybu při překladu pro nepravdivou podmínku a za běhu nic nestojí.

#define SA_CAT_(a, b) a ## b
#define SA_CAT(a, b)  SA_CAT_(a, b)

#define STATIC_ASSERT(cond) \
	typedef char SA_CAT(static_assert_line_, __LINE__)[(cond) ? 1 : -1]

Použití:

STATIC_ASSERT(sizeof(int) == 4);     // projde
STATIC_ASSERT(sizeof(char) == 1);    // projde
STATIC_ASSERT(sizeof(int) == 8);     // error: size of array is negative

Makro expanduje na typedef, tedy na deklaraci typu. Ta za běhu nic nestojí a nezabere ani bajt. Je-li podmínka pravdivá, vznikne char[1]; je-li nepravdivá, char[-1], což překladač odmítne.

Dvě vrstvy maker jsou tu nezbytné a je to přesně ta past, na kterou zadání upozorňuje. S jedinou vrstvou

#define SA_BAD_(cond, line) typedef char bad_##line[(cond) ? 1 : -1]
#define SA_BAD(cond)        SA_BAD_(cond, __LINE__)

se __LINE__ předá do SA_BAD_ jako argument, tam ale sousedí s ##, takže se nerozvine.

typedef char bad___LINE__[(sizeof(int)==4)?1:-1];
typedef char bad___LINE__[(sizeof(char)==1)?1:-1];

Obě deklarace mají totéž jméno. Náhodou to projde, dokud jsou obě podmínky pravdivé, protože opakovaný typedef téhož typu je od C11 povolen. Jakmile jedna podmínka selže, ohlásí překladač kolizi a ukáže na nesmyslný řádek.

Se správnou dvouvrstvou verzí vznikají jména static_assert_line_13, static_assert_line_14 a tak dál — ověřeno opět pomocí -E -P.

Rozdíl je v tom, že v těle vnějšího makra SA_CAT argumenty s ## nesousedí, takže se __LINE__ stihne rozvinout dřív, než se předá do SA_CAT_.

Existuje standardní řešení. Od C11 má jazyk _Static_assert(cond, zprava) a od C23 static_assert. Chybová hláška je u nich podstatně srozumitelnější, protože obsahuje zprávu, kterou napíšeme:

_Static_assert(sizeof(int) == 8, "tento kod predpoklada 64-bitovy int");

Makro výše má proti tomu jedinou výhodu: funguje i v C99.

Poznámka: použijeme-li makro uvnitř funkce, gcc s -Wall upozorní na nepoužitý lokální typedef. Na úrovni souboru žádné varování není.

Úkol 8

Určete, na co se rozvine každý z výrazů a co program vytiskne. Potom makra opravte.

Spávný výstup je 6, 5, 1.

#define HALF(x)      x / 2
#define AREA(w, h)   w * h
#define IS_ZERO(x)   x == 0
  • HALF(4 + 4) se rozvine na 4 + 4 / 2. Dělení má přednost před sčítáním, takže se počítá 4 + 2 a vyjde 6 místo 4.

  • AREA(2 + 1, 3) se rozvine na 2 + 1 * 3, tedy 2 + 3, což je 5, ne 9, jak bychom čekali.

  • 10 * IS_ZERO(0) se rozvine na 10 * 0 == 0. Násobení má přednost před porovnáním, takže se počítá (10 * 0) == 0, tedy 0 == 0, což je 1. Očekávali bychom 10 * 1, tedy 10.

Oprava spočívá v důsledném uzávorkování — kolem každého parametru i kolem celého výrazu:

#define HALF(x)      ((x) / 2)
#define AREA(w, h)   ((w) * (h))
#define IS_ZERO(x)   ((x) == 0)

Po opravě vyjde 4, 9 a 10, tedy ověřeno.

Závorky kolem parametrů řeší případy, kdy je argument složený výraz (první dva). Vnější závorka řeší případy, kdy je makro samo součástí většího výrazu (třetí). Potřeba jsou obě — každá chrání před něčím jiným.

Úkol 9

Napište makro DEFINE_GETTER(type, field).

#define DEFINE_GETTER(type, field) \
	int type ## _get_ ## field(type t) { return t.field; }

Použití — za makrem se nepíše středník, protože jeho tělo je celá definice funkce:

DEFINE_GETTER(Time, hours)
DEFINE_GETTER(Time, minutes)
DEFINE_GETTER(Time, seconds)

Time t = { 1, 2, 3 };
printf("%i:%i:%i\n", Time_get_hours(t), Time_get_minutes(t), Time_get_seconds(t));
// vytiskne 1:2:3

Operátor ## se použije dvakrát: Time + _get_ a potom + hours, takže vznikne identifikátor Time_get_hours. Jméno položky se pak dosadí ještě jednou, tentokrát za tečku, kde už se nespojuje.

Obecnější varianta makra by brala typ položky jako další parametr:

#define DEFINE_GETTER2(type, field_type, field) \
	field_type type ## _get_ ## field(type t) { return t.field; }

Další

Klíčové slovo const

Klíčovým slovem const označujeme objekty, které se po inicializaci nemají měnit. Je to slib, který dáváme překladači — a ten nám ho na oplátku pomůže dodržet.

const int max_users = 100;
 
max_users = 200;   // chyba pri prekladu

Objekt označený jako const je proto potřeba inicializovat rovnou při definici. Později už mu hodnotu nepřiřadíme.

Pozor na jednu věc, která je v C jinak než v mnoha jiných jazycích: const nevytváří konstantní výraz. Hodnota takového objektu není známa v době překladu, jenom se nesmí měnit za běhu.

Nelze ji proto použít všude tam, kde norma konstantní výraz vyžaduje.

const int size = 10;
 
int a[size];        // na urovni souboru chyba,
                    // uvnitr funkce jde o pole promenne delky
 
switch (x)
{
	case size:      // chyba, navesti case musi byt konstantni vyraz
		break;
}

Pro skutečné konstanty proto v C používáme jednu ze dvou jiných možností: makro preprocesoru (viz kapitolu Preprocesor), nebo výčtový typ (viz kapitolu Základy).

#define SIZE 10             // makro, funguje vsude, ale nema typ
 
enum { SIZE2 = 10 };        // vyctovy typ, ma typ int a je to konstanta
 
const int size3 = 10;       // ma typ, ale neni to konstantni vyraz

Pro celočíselné konstanty, které potřebujeme jako velikost pole nebo jako návěstí case, se hodí enum. Pro ostatní hodnoty #define.

U pointerů má const dvě různá místa, kam ho lze napsat, a znamenají něco jiného. Rozdíl je v tom, zda se slib týká pointeru, nebo toho, na co ukazuje.

char text[] = "ahoj";
 
const char *p = text;    // nelze menit *p, pointer p menit lze
char *const q = text;    // nelze menit q, hodnotu *q menit lze
const char *const r = text;   // nelze menit ani jedno

Co je a co není povoleno:

p[0] = 'A';       // chyba, *p je const
p = jiny_text;    // v poradku
 
q[0] = 'A';       // v poradku
q = jiny_text;    // chyba, q je const
 
r[0] = 'A';       // chyba
r = jiny_text;    // chyba

Deklaraci čteme zprava doleva od jména proměnné: char *const q je „q je konstantní pointer na char“, kdežto const char *p je „p je pointer na konstantní char“. Zápisy const char *p a char const *p znamenají totéž.

Zdaleka nejužitečnější je const u parametrů funkcí. Říká volajícímu, že funkce jeho data nezmění, a překladač na to dohlédne.

//
// funkce retezec jenom cte, proto const
//
size_t my_strlen(const char *s)
{
	size_t len = 0;
	while (s[len]) { len += 1; }
	return len;
}

Právě proto vypadají skutečné hlavičky funkcí ze standardní knihovny o něco složitěji, než jak jsme je uváděli v předchozích kapitolách.

size_t strlen(const char *str);
char  *strcpy(char *dest, const char *src);
void  *memcpy(void *dest, const void *src, size_t count);
FILE  *fopen(const char *path, const char *mode);

Na typech parametrů je hned vidět, co funkce dělá: memcpy čte z src a zapisuje do dest, nikoliv naopak. To je informace, kterou by jinak bylo nutné hledat v dokumentaci.

U parametrů typu pole platí totéž; zápisy const int array[] a const int *array znamenají u parametru funkce totéž.

Konverze

Pointer na obyčejný objekt lze na pointer na const objekt převést implicitně. Je to bezpečné, protože si tím jenom ubíráme práva.

char        text[] = "ahoj";
const char *p      = text;    // v poradku, bez pretypovani

Opačným směrem to bez explicitního přetypování nejde a je to téměř vždy chyba. Zapíšeme-li navíc do objektu, který byl skutečně definován jako const, jde o nedefinované chování.

const int x = 10;
int      *p = (int *)&x;   // pretypovani je mozne
 
*p = 20;                   // nedefinovane chovani

Nejužitečnější je toto u řetězcových literálů. Už víme, že jsou pouze ke čtení a že pokus o jejich změnu vede k nedefinovanému chování. Označíme-li pointer jako const, změní se tato past v chybu při překladu.

char *s = "ahoj";
s[0] = 'A';         // preklad probehne, ale za behu jde o nedefinovane chovani
 
const char *t = "ahoj";
t[0] = 'A';         // chyba pri prekladu, coz je presne to, co chceme

Řetězcové literály proto vždy přiřazujeme do const char *.

const neznamená, že se hodnota objektu nikdy nezmění. Znamená jenom, že ji nezměníme tímto jménem. Na tentýž objekt může existovat jiný pointer, který const označený není.

int        x = 1;
const int *p = &x;
 
x = 2;                 // v poradku, x samo const neni
 
printf("%i\n", *p);    // vytiskne 2

Nespoléhejte se proto na const jako na záruku neměnnosti. Je to nástroj na kontrolu toho, co smí dělat kód, který právě píšeme.

Projekty s více soubory

Zdrojový kód lze rozdělit do více souborů. Jeden soubor (.c) lze chápat jako samostatný modul, který může ostatním modulům poskytovat některé své funkce a globální proměnné, a ostatní si přitom ponechat soukromé.

Chceme-li, aby byla funkce nebo proměnná platná pouze v souboru, ve kterém jsou definovány, použijeme k tomu static. Toto klíčové slovo se píše před definici. Pokud se v jiném souboru nachází definice proměnné nebo funkce se stejným jménem, jedná se o jinou proměnnou (nebo funkci).

//
// proměnná private_variable a funkce private_function jsou viditelné
// pouze v souboru, kde se nachází následující definice
// 

static int private_variable;

static int private_function(int a, int b)
{
	return a + b;
}

Předtím než použíjeme globální proměnnou nebo funkci z jiného souboru, musíme je deklarovat s klíčovým slovem extern. U proměnných je povinné, u funkcí je nepovinné, ale je dobré jej tam psát.

//
// následující musí být před použitím dané proměnné nebo funkce
//

extern int variable_in_different_file;
extern int function_in_different_file(int, int);

Linkage

Každý identifikátor (jméno proměnné) má v souboru, kde je definován atribut linkage, který může nabývat tří hodnot.

  • no linkage. Tento atribut mají lokální proměnné.
  • external linkage. Identifikátor je viditelný pro kód vně modulu, ve kterém je definován. Identifikátor může být s external linkage definován nejvýše jednou v celém programu. Jinde musí být definován s internal linkage nebo bez linkage.
  • internal linkage. Identifikátor je viditelný pouze uvnitř .c souboru, kde je definován.

Vidíme tak, že obyčejné funkce a globální proměnné mají external linkage, funkce a proměnné definované s klíčovým slovem static mají internal linkage.

Je důležité si uvědomit, že external linkage není identifikátoru přidělen použitím slova extern.

// file1.c

int x;                             // external 
static int y;                      // internal 

static int sum(int a, int b)       // internal
{
	int s = a + b;                 // no linkage
	return s;
}

int product(int a, int b)          // external
{
	int p = a * b;                 // no linkage
	return p;
}
// file2.c

extern int x;                 // je to proměnná x z file1.c
extern int product(int,int);  // funkce product z file1.c

// file3.c

static int x;                 // toto je OK, má internal linkage
static int product(int a, int b)  // toto je OK, má internal linkage
{ 
	// code
}

// V souboru uz nesmime deklarovat product s extern!!!

Klasické dělení na soubory

Definice proměnných a funkcí je v .c souborech (např. code.c). Funkce a proměnné, které nemají být viditelné vně souboru, ve kterém jsou definovány, definujeme se static. Funkční prototypy a proměnné, které mají být viditelné, deklarujeme v souboru .h (např. code.h) s extern. Soubory, které tyto funkce a proměnné chtějí používat, potom tento soubor vloží pomocí #include.

Hlavičkový soubor navíc obsahuje i definice typů a maker, které jsou pro daný .c soubor veřejné nebo potřebné, např. typy, které se vyskytují ve funkcích a proměnných v daném .h souboru.

Proces překladu

Pro každý .c soubor se provede následující.

  1. Spustí se preprocessor.

    # pouze preprocesor
    gcc -E code.c      # tiskne na stdout
    
  2. Překladač přeloží kód do jazyka symbolických adres.

    # preprocesor + preklad do symbolickych adres
    gcc -S code.c  # vytvoří soubor code.s
    
  3. Assembler poté kód přeloží do objektového souboru (strojový kód a další informace, např. poskytovaná jména funkcí a proměnných.)

    # vytvori objektovy soubour code.o
    gcc -c code.c
    
  4. Linker sloučí vytvořené objektové soubory, spojí je s knihovnami (např. standardní knihovnou) a vytvoří cílený binární soubor (spustitelný soubor, knihovnu).

    # code1.o, code2.o jsou objektové soubory
    gcc code1.o code2.o   # volá linker, např. ld
    

Překlad více souborů

Program rozdělený do více souborů lze přeložit jedním příkazem, ale je to zbytečné: při každé změně se překládají znovu všechny soubory.

# funguje, ale preklada se vzdy vsechno
gcc -std=c17 -Wall -Wextra -g main.c utils.c -o program

Lepší je využít toho, co víme z předchozí sekce, a překládat každý soubor zvlášť do objektového souboru. Ty se pak slinkují dohromady.

gcc -std=c17 -Wall -Wextra -g -c main.c     # vznikne main.o
gcc -std=c17 -Wall -Wextra -g -c utils.c    # vznikne utils.o
gcc main.o utils.o -o program               # slinkovani

Změníme-li potom utils.c, stačí přeložit znovu jenom ten a slinkovat.

Psát tyto příkazy ručně je únavné a snadno se na některý zapomene. K jejich zapsání slouží soubor Makefile, který nástroj make umí přečíst a sám rozhodnout, co je potřeba přeložit znovu.

CC = gcc
CFLAGS = -std=c17 -Wall -Wextra -g -fsanitize=address,undefined
LDFLAGS = -fsanitize=address,undefined
 
program: main.o utils.o
	$(CC) $(LDFLAGS) -o program main.o utils.o
 
main.o: main.c utils.h
	$(CC) $(CFLAGS) -c main.c
 
utils.o: utils.c utils.h
	$(CC) $(CFLAGS) -c utils.c
 
clean:
	rm -f program *.o

Každé pravidlo má tvar cíl: závislosti, pod kterým je odsazený příkaz, kterým se cíl vytvoří. Nástroj make porovná časy poslední změny souborů a příkaz spustí jenom tehdy, je-li některá závislost novější než cíl.

make          # prelozi, co je potreba
make clean    # smaze vysledky prekladu

Příkazy v souboru Makefile musí být odsazeny tabulátorem, nikoliv mezerami. Je to nejčastější chyba při psaní prvního Makefile a projeví se hláškou o chybějícím oddělovači.

Všimněte si také, že přepínače se sanitizery jsou uvedeny dvakrát: v CFLAGS pro překlad a v LDFLAGS pro linkování. Bez toho druhého by linkování selhalo.

Argumenty funkce main

Funkce main může mít žádné nebo nějaké paramatry, její hlavička je potom

int main(int argc, char *argv[]);

Hodnoty parametrů je možné funkci předat při spuštění programu z příkazové řádky (terminálu, skriptu apod.). Do parametru argc se uloží jejich počet zvětšený o jedna, jejich hodnoty (jako řetězce) jsou v poli argv počínaje indexem 1; argv[0] je totiž řetězec odpovídající spuštěnému programu. Prvek argv[argc] je nulový pointer (to je základem idiomatického cyklu for (char **p = argv; *p; p+=1).

Nástroje pro kontrolu správnosti

V předchozích kapitolách jsme mnohokrát narazili na nedefinované chování: zápis za konec pole, použití uvolněné paměti, přetečení znaménkového typu, čtení neinicializované proměnné. Společné mají to, že je překladač sám od sebe neohlásí a že se program může chovat zdánlivě správně — dokud nezměníme něco úplně jiného.

Existují ale nástroje, které většinu z těchto chyb odhalí za nás. Používat je není známka nezkušenosti; je to standardní součást práce v C a jediný rozumný způsob, jak se v tomto jazyce vyhnout chybám, které se hledají velmi obtížně.

Varování překladače

Nejlevnější nástroj je překladač sám. Varování nejsou chyby a překlad nezastaví, ale téměř vždy upozorňují na něco, co jsme nemysleli vážně.

gcc -std=c17 -Wall -Wextra -g program.c -o program

Přepínač -Wall navzdory názvu nezapíná všechna varování, jenom ta nejužitečnější; -Wextra přidá další. Kromě nich se vyplatí zapnout i následující.

Prepinac                Co hlasi
------------------------------------------------------------------------
-Wpedantic              Pouziti rozsireni, ktera nejsou v norme.
 
-Wshadow                Promennou, ktera zastinuje jinou promennou
                        stejneho jmena z vnejsiho bloku.
 
-Wsign-conversion       Implicitni konverze menici znamenko,
                        tedy chyby popsane v kapitole Zaklady.
 
-Wwrite-strings         Prirazeni retezcoveho literalu do char*
                        misto const char*.
 
-Wstrict-prototypes     Deklarace int f() misto int f(void).
 
-Wvla                   Pouziti pole promenne delky.
 
-Werror                 Zmeni vsechna varovani na chyby.
------------------------------------------------------------------------

Přepínač -Werror je vhodné zapnout u vlastních projektů: donutí nás varování opravit místo toho, abychom si na ně zvykli. Během ladění cizího kódu naopak spíše překáží.

Za zmínku stojí ještě jedna věc: některá varování dokáže překladač vygenerovat až po optimalizaci, protože k jejich odhalení potřebuje analýzu toku dat. Typickým příkladem je použití neinicializované proměnné. Vyplatí se proto program občas přeložit i s přepínačem -O2.

Sanitizery

Sanitizery jsou kontroly, které překladač vloží přímo do programu. Chyba se tak neprojeví jako záhadné chování o tisíc řádků dál, ale jako srozumitelná zpráva s názvem souboru a číslem řádku, na kterém k ní došlo.

Sanitizery je nutné uvést jak při překladu, tak při linkování. Nejjednodušší je proto přeložit program jedním příkazem.

# preklad se sanitizery pameti a nedefinovaneho chovani
gcc -std=c17 -Wall -Wextra -g -fsanitize=address,undefined program.c -o program
 
./program

AddressSanitizer (ASan) (-fsanitize=address) hlídá práci s pamětí. Odhalí zápis i čtení za hranicemi pole (na haldě, na zásobníku i u globálních proměnných), použití paměti po jejím uvolnění, dvojí uvolnění téhož bloku a na Linuxu také neuvolněnou paměť na konci běhu programu.

UndefinedBehaviorSanitizer (UBSan) (-fsanitize=undefined) hlídá nedefinované chování ve výpočtech. Odhalí přetečení znaménkových typů, dělení nulou, posuv o záporný nebo příliš velký počet bitů, dereferenci nulového pointeru a přístup přes nesprávně zarovnaný pointer.

Ve výchozím nastavení UBSan chybu vypíše a program pokračuje dál. Chceme-li, aby se zastavil hned u první, přidáme následující přepínač.

gcc -std=c17 -g -fsanitize=undefined -fno-sanitize-recover=all program.c -o program

Sanitizery program zpomalí (zhruba dvojnásobně) a zvýší jeho spotřebu paměti. Pro ladění to nevadí, do finální verze programu ale nepatří. ASan navíc nelze používat současně s nástrojem valgrind — oba dělají v podstatě totéž a navzájem si překážejí.

Jedno je ale potřeba zdůraznit: sanitizery neodhalí čtení neinicializované proměnné. Právě tato chyba se přitom projevuje velmi nepříjemně, protože program často „funguje“, dokud v paměti náhodou leží vhodná hodnota.

int sum;                       // chybi inicializace
 
for (int i = 0; i < n; i += 1)
{
	sum += array[i];           // pricitame k neznamemu cislu
}

Proti tomu pomáhá jedině pořádek: proměnnou inicializujeme vždy hned při definici. Část těchto chyb navíc odhalí překladač sám, ale jenom je-li zapnutá optimalizace, protože k tomu potřebuje analýzu toku dat.

# -Wmaybe-uninitialized je soucasti -Wall, funguje ale az od -O1 vyse
gcc -std=c17 -Wall -Wextra -O2 -c program.c

Statická analýza

Sanitizery hlásí jenom chyby, ke kterým při daném běhu skutečně došlo. Chybu na řádku, který jsme netestovali, neodhalí. Doplňkem je proto statická analýza, která program zkoumá, aniž by ho spouštěla.

# vestavena analyza v gcc (od verze 10)
gcc -std=c17 -Wall -Wextra -fanalyzer -c program.c

Přepínač -fanalyzer umí najít například dvojí uvolnění paměti, použití po uvolnění, únik paměti nebo dereferenci hodnoty, o které program sám jinde předpokládá, že může být nulová. Občas hlásí i chyby, které ve skutečnosti nastat nemohou; každé hlášení je proto potřeba posoudit.

Podobnou službu odvede samostatný nástroj cppcheck nebo analyzátor zabudovaný v překladači clang.

Doporučené nastavení pro tento kurz

Během práce na úkolech doporučujeme překládat takto.

gcc -std=c17 -Wall -Wextra -Wpedantic -g \
    -fsanitize=address,undefined \
    program.c -o program

Zaručeně to odhalí většinu chyb, na které v úkolech narazíte — zejména v kapitole Paměť, kde jsou skoro všechny úkoly o ruční správě paměti.

Úkoly

  1. Pro každý z následujících řádků rozhodněte, zda jde o chybu při překladu, nedefinované chování za běhu, nebo o korektní kód. Svá rozhodnutí ověřte překladačem.

    const int x = 10;
    int *p = (int *)&x;
    *p = 20;                    // (a)
    
    int y = 10;
    const int *q = &y;
    y = 20;                     // (b)
    *q = 30;                    // (c)
    
    char *s = "ahoj";
    s[0] = 'A';                 // (d)
    
    const char *t = "ahoj";
    t = "svete";                // (e)
    
  2. Máte dva soubory:

    // util.c
    int counter;
    static int helper(int x) { return x * 2; }
    int compute(int x) { return helper(x) + counter; }
    
    // main.c
    #include <stdio.h>
    int counter;
    extern int helper(int);
    int main() { printf("%i\n", helper(21)); return 0; }
    

    Program se nepodaří slinkovat. Najděte dvě různé chyby, vysvětlete je pomocí pojmu linkage a opravte je tak, aby program vytiskl 42. Soubor util.c přitom smí zveřejnit pouze funkci compute.

  3. Vezměte řešení úkolu se zásobníkem z kapitoly Paměť a rozdělte je do tří souborů: stack.h, stack.c a main.c. Hlavičkový soubor opatřete ochranou proti vícenásobnému vložení. Napište k projektu Makefile s cíli pro překlad a pro clean a ověřte, že po změně jediného .c souboru se druhý nepřekládá znovu.

  4. Napište program pro výpočet objemu a povrchu válce, pravidelného trojbokého, čtyřbokého a šestibokého hranolu. Parametry výpočtu by mělo být možné předávat programu při spuštění z příkazové řádky. Zdrojový kód programu by měl být rozdělen do 2 modulů. Modul hlavní funkce (soubor main.c) bude zajišťovat zpracování a případně načtení chybějící parametrů výpočtu, budou z něj volány funkce zajišťující vlastní výpočet a vypisovány vypočítané hodnoty na obrazovku. Druhý modul (soubory vypocet.h a vypocet.c) pak bude zajišťovat veškeré požadované výpočty. Při řešení úlohy dbejte všech zásad zmíněných na přednášce. zdroj

  5. Napište v jazyku C jednoduchou knihovnu funkcí pro vykreslování obrázků pomocí znaků (tzv. ASCII art). Knihovna by měla mít tyto vlastnosti:

    • Obrázky se budou vykreslovat pomocí plátna - dvojrozměrné matice, která bude obsahovat jednotlivé znaky.
    • Vykreslování se tedy neprovádí přímo na výstupu, ale pouze dochází ke změně daného plátna (struktura Canvas).
    • Je možné pracovat současně s několika plátny.
    • Je možné “vykreslovat” i za hranicí kreslící plochy, tyto body se ale nebudou při zobrazení plátna vykreslovat. Jinými slovy, při pokusu o kreslení mimo plátno nedojde k vyjímce při běhu programu.

    Knihovna by měla být samostatným modulem, bude tedy tvořena jedním zdrojovým a jedním hlavičkovým souborem. V knihovně vytvořte strukturovaný datový typ Canvas a dále definujte tyto funkce:

    //
    // Inicializuje plátno na prazdné plátno široké width a vysoké height znaků
    //
    void canvas_init(Canvas *c, int width, int height);
    
    //
    // Vrátí znak nacházející ve sloupci x a na radku y
    //
    int canvas_get_point(Canvas *c, int x, int y);
    
    //
    // Nakreslí obdélník, jehož stěna je tvořena znakem ch
    //
    void canvas_draw_rect(Canvas *c, int x, int y, int width, int height, char ch);
    
    //
    //  Vyprázdní plátno
    //
    void canvas_clear(Canvas *c);
    
    //
    //  Vykreslí obsah plátna na standardní výstup/do souboru
    //
    void canvas_print(Canvas *c);
    void canvas_output(Canvas *c, FILE *f);
    
    //
    //  Uvolní vnitřní pamět plátna
    //
    void canvas_destroy(Canvas *c);
    
  6. Následující program se přeloží bez jediného varování a při běžném spuštění může i vypsat správný výsledek. Obsahuje ale tři chyby. Najděte je pomocí -fsanitize=address,undefined a opravte.

    #include <stdio.h>
    #include <stdlib.h>
    
    int main()
    {
    	int n = 5;
    	int *a = malloc(n * sizeof(int));
    
    	for (int i = 0; i <= n; i += 1) { a[i] = i * i; }
    
    	int sum;
    	for (int i = 0; i < n; i += 1) { sum += a[i]; }
    
    	printf("%i\n", sum);
    
    	free(a);
    	free(a);
    	return 0;
    }
    

    U každé chyby napište, který ze sanitizerů ji odhalil a proč ji překladač sám nenašel. Pozor: jednu z nich neodhalí ani sanitizery — která to je a jak se proti ní bránit?

  7. Napište program, který přijme libovolný počet jmen souborů a přepínač -n. Bez přepínače vypíše obsah všech souborů za sebou, s přepínačem očísluje řádky průběžně přes všechny soubory. Neexistující soubor ohlaste na stderr a pokračujte dalším. Vyzkoušejte i chování bez jakéhokoliv argumentu.

Řešení úkolů

Úkol 1

Pro každý řádek rozhodněte, zda jde o chybu při překladu, nedefinované chování za běhu, nebo o korektní kód.

Všechny případy byly ověřeny překladačem, každý zvlášť.

(a) *p = 20; — přeloží se, ale je to nedefinované chování za běhu.

const int x = 10;
int *p = (int *)&x;
*p = 20;

Explicitní přetypování const odstraní, takže překladač nemá co namítat a nevypíše ani varování. Zápis do objektu, který byl skutečně definován jako const, je ale nedefinované chování. V testu může program doběhnout bez pádu a hodnotu změnit — to je na nedefinovaném chování právě to nepříjemné. Kdyby byl x uložen v paměti jen pro čtení (například jako globální konstanta), program by spadl.

(b) y = 20; — korektní kód.

int y = 10;
const int *q = &y;
y = 20;

Objekt y sám const není. Slib se týká pouze pointeru q, tedy toho, že přes něj měnit nebudeme. Samotné y měnit smíme a *q pak vrátí novou hodnotu. To je přesně poznámka z konce sekce: const není záruka neměnnosti.

(c) *q = 30; — chyba při překladu.

error: assignment of read-only location '*q'

Typ q je const int *, tedy pointer na konstantní int. Zápis přes dereferenci překladač odmítne. Právě tohle od const chceme: chyba se objeví při překladu, ne za běhu.

(d) s[0] = 'A'; — přeloží se, ale je to nedefinované chování za běhu.

char *s = "ahoj";
s[0] = 'A';

Řetězcové literály jsou pouze ke čtení. Přiřazení literálu do char * je z historických důvodů povoleno, takže bez zvláštních přepínačů neprojde ani varování. V mém testu program skončil signálem SIGSEGV (návratový kód 139), protože literály bývají v paměti jen pro čtení.

S přepínačem -Wwrite-strings překladač upozorní:

warning: initialization discards 'const' qualifier from pointer target type

a chyba se odhalí při překladu. Proto se řetězcové literály přiřazují do const char *.

(e) t = "svete"; — korektní kód.

const char *t = "ahoj";
t = "svete";

Typ t je „pointer na konstantní char“, nikoliv „konstantní pointer“. const chrání to, na co se ukazuje, nikoliv pointer samotný, takže ho lze nasměrovat jinam. Zakázat by to šlo zápisem const char *const t = "ahoj";.

Úkol 2

Program se nepodaří slinkovat. Najděte dvě různé chyby, vysvětlete je pomocí pojmu linkage a opravte je.

Linker ohlásí přesně dvě chyby:

multiple definition of `counter'
undefined reference to `helper'
  • První chyba: counter je definován dvakrát. Zápis int counter; na úrovni souboru je definice s external linkage, a ta smí být v celém programu nejvýše jedna. V util.c i v main.c je stejná, takže linker neví, kterou vzít.

  • Druhá chyba: helper má internal linkage.** V util.c je označen jako static, takže je viditelný pouze uvnitř util.c. Deklarace extern int helper(int); v main.c slibuje, že funkce s tím jménem existuje s external linkage někde jinde — a žádná taková není. Chyba se proto projeví až při linkování, nikoliv při překladu: překladač main.c deklaraci uvěří.

Zadání navíc říká, že util.c smí zveřejnit pouze funkci compute. Obojí se opraví takto:

// util.c
static int counter = 0;                  // internal linkage, ven se nedostane
static int helper(int x) { return x * 2; }
int compute(int x) { return helper(x) + counter; }   // jedine, co je verejne
// main.c
#include <stdio.h>
extern int compute(int);                 // volame compute, nikoliv helper
int main() { printf("%i\n", compute(21)); return 0; }

Všimněte si, že counter nyní není v main.c vůbec potřeba. Kdybychom k němu opravdu potřebovali přístup, nesměl by být static a main.c by ho deklaroval jako extern int counter; — tedy deklarace bez definice.

V reálném projektu by se extern int compute(int); nepsala ručně do každého souboru, ale vložila by se z hlavičkového souboru util.h. Tím se zamezí tomu, aby se deklarace rozešla s definicí.

Úkol 3

Rozdělte řešení úkolu se zásobníkem do tří souborů a napište Makefile.

stack.h

#if !defined(STACK_H)
#define STACK_H

typedef struct
{
	int *data;
	int  top;
	int  cap;
} Stack;

Stack create_stack(void);
void  push(Stack *s, int v);
int   pop(Stack *s);
void  destroy_stack(Stack *s);

#endif

Do hlavičkového souboru patří jenom rozhraní: definice typu a prototypy. Definice typu tu být musí, protože bez ní by volající nemohl vytvořit proměnnou typu Stack.

Ochrana proti vícenásobnému vložení je povinná. Bez ní by dvojí #include "stack.h" (klidně nepřímo) vedl k dvojí definici typu.

stack.c

#include "stack.h"
#include <stdlib.h>
#include <assert.h>

#define INITIAL_CAP 16

Stack create_stack(void) { return (Stack){0}; }

void push(Stack *s, int v)
{
	if (s->top == s->cap)
	{
		int newcap = (s->cap == 0) ? INITIAL_CAP : s->cap * 2;
		int *p = realloc(s->data, (size_t)newcap * sizeof(int));
		assert(p);
		s->data = p; s->cap = newcap;
	}
	s->data[s->top] = v;
	s->top += 1;
}

int pop(Stack *s)
{
	assert(s->top > 0);
	s->top -= 1;
	int v = s->data[s->top];

	if (s->cap > INITIAL_CAP && s->top * 4 <= s->cap)
	{
		int newcap = s->cap / 2;
		int *p = realloc(s->data, (size_t)newcap * sizeof(int));
		assert(p);
		s->data = p; s->cap = newcap;
	}
	return v;
}

void destroy_stack(Stack *s) { free(s->data); s->data = 0; s->top = 0; s->cap = 0; }

Makro INITIAL_CAP je definováno v .c souboru, nikoliv v hlavičce — je to vnitřní záležitost implementace a volajícího nezajímá. Ze stejného důvodu by sem patřily i případné pomocné funkce, označené static.

Soubor stack.c vkládá vlastní hlavičku stack.h. Tím se ověří, že prototypy odpovídají definicím; kdyby se rozešly, překladač to ohlásí.

main.c

#include <stdio.h>
#include "stack.h"

int main(void)
{
	Stack s = create_stack();
	for (int i = 1; i <= 100; i += 1) { push(&s, i); }

	long sum = 0;
	while (s.top > 0) { sum += pop(&s); }

	printf("soucet=%ld, cap po vyprazdneni=%i\n", sum, s.cap);
	destroy_stack(&s);
	return 0;
}

Nakonec makefile.

CC = gcc
CFLAGS = -std=c17 -Wall -Wextra -g -fsanitize=address,undefined
LDFLAGS = -fsanitize=address,undefined

program: main.o stack.o
	$(CC) $(LDFLAGS) -o program main.o stack.o

main.o: main.c stack.h
	$(CC) $(CFLAGS) -c main.c

stack.o: stack.c stack.h
	$(CC) $(CFLAGS) -c stack.c

clean:
	rm -f program *.o

Příkazy musí být odsazeny tabulátorem, nikoliv mezerami.

Úkol 4

Program pro výpočet objemu a povrchu válce a pravidelného trojbokého, čtyřbokého a šestibokého hranolu. Parametry se předávají na příkazové řádce, kód je rozdělen do dvou modulů.

vypocet.h

#if !defined(VYPOCET_H)
#define VYPOCET_H

typedef enum
{
	SHAPE_CYLINDER,
	SHAPE_PRISM3,
	SHAPE_PRISM4,
	SHAPE_PRISM6,
	SHAPE_COUNT
} Shape;

const char *shape_name(Shape s);
int         shape_from_string(const char *s, Shape *out);
double      volume(Shape s, double a, double v);
double      surface(Shape s, double a, double v);

#endif

Hlavičkový soubor obsahuje jenom to, co má být vidět zvenčí: typ Shape a čtyři prototypy. Ochrana proti dvojímu vložení je povinná.

Všimněte si posledního prvku výčtu SHAPE_COUNT — má hodnotu rovnou počtu tvarů a použijeme ho jako velikost pole, přesně podle triku z kapitoly Základy.

vypocet.c

#include "vypocet.h"
#include <math.h>
#include <string.h>

//
// M_PI neni v norme C (je to rozsireni POSIXu), s -std=c17 nemusi byt
// definovano; zavedeme si vlastni konstantu
//
#define PI 3.14159265358979323846

static const char *names[SHAPE_COUNT] = { "valec", "hranol3", "hranol4", "hranol6" };

const char *shape_name(Shape s) { return names[s]; }

int shape_from_string(const char *s, Shape *out)
{
	for (int i = 0; i < SHAPE_COUNT; i += 1)
	{
		if (strcmp(s, names[i]) == 0) { *out = (Shape)i; return 1; }
	}
	return 0;
}

//
// pomocne funkce jsou static, ven se nedostanou
//
static double base_area(Shape s, double a)
{
	switch (s)
	{
		case SHAPE_CYLINDER: return PI * a * a;
		case SHAPE_PRISM3:   return sqrt(3.0) / 4.0 * a * a;
		case SHAPE_PRISM4:   return a * a;
		case SHAPE_PRISM6:   return 3.0 * sqrt(3.0) / 2.0 * a * a;
		default:             return 0;
	}
}

static double base_perimeter(Shape s, double a)
{
	switch (s)
	{
		case SHAPE_CYLINDER: return 2.0 * PI * a;
		case SHAPE_PRISM3:   return 3.0 * a;
		case SHAPE_PRISM4:   return 4.0 * a;
		case SHAPE_PRISM6:   return 6.0 * a;
		default:             return 0;
	}
}

double volume(Shape s, double a, double v)
{
	return base_area(s, a) * v;
}

double surface(Shape s, double a, double v)
{
	return 2.0 * base_area(s, a) + base_perimeter(s, a) * v;
}

Válec i hranol jsou kolmá tělesa: objem je vždy obsah podstavy × výška a povrch 2 × obsah podstavy + obvod podstavy × výška. Stačí tak dvě funkce závislé na tvaru a zbytek je společný.

Funkce base_area a base_perimeter jsou static, takže mají internal linkage a z jiného modulu je volat nelze. Do hlavičkového souboru nepatří.

main.c

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

static int read_double(const char *prompt, double *out)
{
	printf("%s", prompt);
	fflush(stdout);                 // vypis se jinak muze objevit az pozdeji
	return scanf("%lf", out) == 1;
}

int main(int argc, char *argv[])
{
	Shape  s;
	double a, v;

	if (argc < 2)
	{
		fprintf(stderr, "pouziti: %s <valec|hranol3|hranol4|hranol6> [rozmer] [vyska]\n", argv[0]);
		return 1;
	}

	if (!shape_from_string(argv[1], &s))
	{
		fprintf(stderr, "neznamy tvar: %s\n", argv[1]);
		return 1;
	}

	//
	// chybejici parametry doptame ze standardniho vstupu
	//
	if (argc >= 3) { a = atof(argv[2]); }
	else if (!read_double("rozmer podstavy: ", &a)) { fprintf(stderr, "chybny vstup\n"); return 1; }

	if (argc >= 4) { v = atof(argv[3]); }
	else if (!read_double("vyska: ", &v)) { fprintf(stderr, "chybny vstup\n"); return 1; }

	if (a <= 0 || v <= 0) { fprintf(stderr, "rozmery musi byt kladne\n"); return 1; }

	printf("%s: objem %.4f, povrch %.4f\n", shape_name(s), volume(s, a, v), surface(s, a, v));
	return 0;
}

Příklad použití

$ ./program valec 1 1
valec: objem 3.1416, povrch 12.5664

$ ./program hranol4 2 3
hranol4: objem 12.0000, povrch 32.0000

$ ./program hranol6 1 1
hranol6: objem 2.5981, povrch 11.1962

$ echo "1 1" | ./program valec
rozmer podstavy: vyska: valec: objem 3.1416, povrch 12.5664

Makefile

CC = gcc
CFLAGS = -std=c17 -Wall -Wextra -g
LDLIBS = -lm

program: main.o vypocet.o
	$(CC) -o program main.o vypocet.o $(LDLIBS)

main.o: main.c vypocet.h
	$(CC) $(CFLAGS) -c main.c

vypocet.o: vypocet.c vypocet.h
	$(CC) $(CFLAGS) -c vypocet.c

clean:
	rm -f program *.o

Přepínač -lm je potřeba kvůli sqrt z math.h a uvádí se až při linkování. Oba objektové soubory závisí na vypocet.h, takže se po jeho změně přeloží znovu.

Úkol 5

Knihovna pro vykreslování obrázků pomocí znaků (ASCII art).

canvas.h

#if !defined(CANVAS_H)
#define CANVAS_H

#include <stdio.h>

typedef struct
{
	int   width;
	int   height;
	char *data;      // radky za sebou, prvek (x,y) je data[y*width + x]
} Canvas;

void canvas_init(Canvas *c, int width, int height);
int  canvas_get_point(Canvas *c, int x, int y);
void canvas_draw_point(Canvas *c, int x, int y, char ch);
void canvas_draw_rect(Canvas *c, int x, int y, int width, int height, char ch);
void canvas_clear(Canvas *c);
void canvas_print(Canvas *c);
void canvas_output(Canvas *c, FILE *f);
void canvas_destroy(Canvas *c);

#endif

Struktura Canvas je v hlavičkovém souboru celá, takže si ji volající může vytvořit jako lokální proměnnou. Protože si každé plátno nese svá data, lze jich mít současně libovolně mnoho — to zadání výslovně žádá.

Do rozhraní jsme přidali canvas_draw_point, kterou zadání nepožaduje. Je to užitečný stavební kámen a zároveň jediné místo, kde se kontrolují meze.

canvas.c

#include "canvas.h"
#include <stdlib.h>
#include <assert.h>

#define CANVAS_EMPTY ' '

void canvas_init(Canvas *c, int width, int height)
{
	assert(width > 0 && height > 0);
	c->width  = width;
	c->height = height;
	c->data   = malloc((size_t)width * (size_t)height);
	assert(c->data);
	canvas_clear(c);
}

void canvas_clear(Canvas *c)
{
	for (int i = 0; i < c->width * c->height; i += 1) { c->data[i] = CANVAS_EMPTY; }
}

//
// jedine misto, kde se kontroluji meze:
// kresleni mimo platno neni chyba, jenom se neprojevi
//
void canvas_draw_point(Canvas *c, int x, int y, char ch)
{
	if (x < 0 || x >= c->width || y < 0 || y >= c->height) { return; }
	c->data[y * c->width + x] = ch;
}

int canvas_get_point(Canvas *c, int x, int y)
{
	if (x < 0 || x >= c->width || y < 0 || y >= c->height) { return CANVAS_EMPTY; }
	return c->data[y * c->width + x];
}

void canvas_draw_rect(Canvas *c, int x, int y, int width, int height, char ch)
{
	if (width <= 0 || height <= 0) { return; }

	//
	// vodorovne strany
	//
	for (int i = 0; i < width; i += 1)
	{
		canvas_draw_point(c, x + i, y, ch);
		canvas_draw_point(c, x + i, y + height - 1, ch);
	}

	//
	// svisle strany
	//
	for (int j = 0; j < height; j += 1)
	{
		canvas_draw_point(c, x, y + j, ch);
		canvas_draw_point(c, x + width - 1, y + j, ch);
	}
}

void canvas_output(Canvas *c, FILE *f)
{
	for (int y = 0; y < c->height; y += 1)
	{
		for (int x = 0; x < c->width; x += 1) { fputc(c->data[y * c->width + x], f); }
		fputc('\n', f);
	}
}

void canvas_print(Canvas *c) { canvas_output(c, stdout); }

void canvas_destroy(Canvas *c)
{
	free(c->data);
	c->data   = 0;
	c->width  = 0;
	c->height = 0;
}

Všechny kreslicí funkce jdou přes canvas_draw_point, která souřadnice mimo rozsah tiše zahodí, splnili jsme tak požadavek týkající se kreslení mimo plátno.

canvas_print je jenom obal nad canvas_output(c, stdout). Opakovat kód dvakrát by nemělo smysl.

Příklad použití

#include "canvas.h"

int main(void)
{
	Canvas a, b;
	canvas_init(&a, 20, 7);
	canvas_init(&b, 10, 3);

	canvas_draw_rect(&a, 1, 1, 8, 5, '*');
	canvas_draw_rect(&a, 6, 2, 12, 4, '#');
	canvas_draw_rect(&a, -3, -2, 6, 5, '+');    // castecne mimo platno
	canvas_draw_rect(&a, 100, 100, 4, 4, 'X');  // cele mimo platno
	canvas_draw_rect(&b, 0, 0, 10, 3, '.');

	canvas_print(&a);
	canvas_print(&b);

	canvas_destroy(&a);
	canvas_destroy(&b);
	return 0;
}

Skutečný výstup:

  +                 
 *+******           
+++   ############  
 *    # *        #  
 *    # *        #  
 *****############  
                    
..........
.        .
..........

Z obdélníku (-3,-2) je vidět jenom pravý dolní roh, obdélník (100,100) se neprojeví vůbec a program nespadl. Obě plátna existují vedle sebe nezávisle.

Makefile

CC = gcc
CFLAGS = -std=c17 -Wall -Wextra -g -fsanitize=address,undefined
LDFLAGS = -fsanitize=address,undefined

program: main.o canvas.o
	$(CC) $(LDFLAGS) -o program main.o canvas.o

main.o: main.c canvas.h
	$(CC) $(CFLAGS) -c main.c

canvas.o: canvas.c canvas.h
	$(CC) $(CFLAGS) -c canvas.c

clean:
	rm -f program *.o

Řešení je přeloženo s AddressSanitizerem a při běhu nehlásí ani přístup mimo pole, ani neuvolněnou paměť — právě proto, že se každé canvas_init páruje s canvas_destroy.

Náměty na rozšíření

Knihovnu lze snadno doplnit o další kreslicí funkce, které všechny půjdou přes canvas_draw_point:

void canvas_fill_rect(Canvas *c, int x, int y, int w, int h, char ch);
void canvas_draw_line(Canvas *c, int x1, int y1, int x2, int y2, char ch);
void canvas_draw_text(Canvas *c, int x, int y, const char *text);
void canvas_blit(Canvas *dst, Canvas *src, int x, int y);

U canvas_draw_line se hodí Bresenhamův algoritmus, canvas_blit zkopíruje jedno plátno do druhého — a díky kontrole mezí na jednom místě fungují oba i přes okraj.

Úkol 6

Program se přeloží bez varování a obsahuje tři chyby. Najděte je pomocí sanitizerů a opravte.

Chyby jsou tyto.

  1. Zápis za konec pole. Cyklus

    for (int i = 0; i <= n; i += 1) { a[i] = i * i; }
    

    používá <= místo <, takže při i == 5 zapisuje do a[5], ačkoliv pole má prvky 0 až 4. Odhalí to AddressSanitizer:

    ERROR: AddressSanitizer: heap-buffer-overflow
    SUMMARY: AddressSanitizer: heap-buffer-overflow buggy.c:7 in main
    

    Překladač to sám nenajde, protože n je proměnná a mez cyklu se vyhodnocuje až za běhu. Oprava: i < n.

  2. Dvojí uvolnění. Dvě volání free(a) za sebou. Odhalí to rovněž AddressSanitizer jako attempting double-free, ovšem až po opravě první chyby — sanitizer se zastaví hned u první nalezené. Oprava: druhé free smazat.

    Poznámka: gcc 13 tuto chybu ve skutečnosti ohlásí i sám jako -Wuse-after-free, takže tvrzení ze zadání o „ani jednom varování“ platí pro starší překladače. U složitějšího kódu, kde jsou obě volání v různých funkcích, by překladač bezradný byl.

  3. Neinicializovaná proměnná. Zápis

    int sum;
    for (int i = 0; i < n; i += 1) { sum += a[i]; }
    

    přičítá k neznámé hodnotě. Toto je chyba, kterou sanitizery neodhalí — ASan hlídá, kam se sahá, nikoliv zda tam někdo předtím zapsal. Program často „funguje“, protože na zásobníku náhodou leží nula.

    Bránit se dá dvojím způsobem.

    Zaprvé pořádkem: proměnnou inicializujeme vždy hned při definici, tedy int sum = 0;.

    Zadruhé překladačem — -Wmaybe-uninitialized je součástí -Wall, ale potřebuje analýzu toku dat, takže funguje až od -O1 výše. Ověřeno:

    $ gcc -std=c17 -Wall -Wextra -O2 -c buggy.c
    buggy.c:9:46: warning: 'sum' is used uninitialized [-Wuninitialized]
    

    To je dobrý důvod přeložit si program občas i s optimalizacemi, i když ho jinak ladíme s -O0.

    Opravený program:

    int main()
    {
    	int n = 5;
    	int *a = malloc(n * sizeof(int));
    	assert(a);                              // i navratovou hodnotu malloc kontrolujeme
    
    	for (int i = 0; i < n; i += 1) { a[i] = i * i; }
    
    	int sum = 0;
    	for (int i = 0; i < n; i += 1) { sum += a[i]; }
    
    	printf("%i\n", sum);                    // vytiskne 30
    
    	free(a);
    	return 0;
    }
    

Úkol 7

Program, který přijme libovolný počet jmen souborů a přepínač -n.

#include <stdio.h>

static void dump(FILE *in, int numbered, long *lineno)
{
	int z;
	int at_start = 1;

	while ((z = getc(in)) != EOF)
	{
		if (numbered && at_start)
		{
			*lineno += 1;
			printf("%6ld  ", *lineno);
			at_start = 0;
		}

		putchar(z);
		if (z == '\n') { at_start = 1; }
	}

	if (!at_start) { putchar('\n'); }   // soubor nekoncil novym radkem
}

int main(int argc, char *argv[])
{
	int  numbered = 0;
	long lineno   = 0;      // pocita se prubezne pres vsechny soubory
	int  files    = 0;

	for (int i = 1; i < argc; i += 1)
	{
		if (argv[i][0] == '-' && argv[i][1] == 'n' && argv[i][2] == 0)
		{
			numbered = 1;
			continue;
		}

		files += 1;

		FILE *in = fopen(argv[i], "r");
		if (!in)
		{
			fprintf(stderr, "nelze otevrit: %s\n", argv[i]);
			continue;              // pokracujeme dalsim souborem
		}

		dump(in, numbered, &lineno);
		fclose(in);
	}

	if (files == 0)
	{
		fprintf(stderr, "pouziti: %s [-n] soubor...\n", argv[0]);
		return 1;
	}
	return 0;
}

Číslování řádků je průběžné přes všechny soubory, jak zadání žádá — proto se čítač předává odkazem do dump,

Test přepínače kontroluje i argv[i][2] == 0, aby se argument -nx nebo -number nepovažoval za -n. Podle unixové konvence se přepínač projeví jen na soubory uvedené za ním; kdybychom chtěli, aby platil pro všechny, museli bychom argumenty projít dvakrát.

Chybějící soubor se ohlásí na stderr a program pokračuje dalším, takže jedna překlepnutá cesta neshodí celé zpracování. Chybějící argumenty vedou k návratovému kódu 1, aby se selhání dalo poznat ve skriptu.

Proměnná at_start řeší dvě věci najednou: číslo se tiskne až před prvním znakem řádku (takže prázdný poslední řádek nedostane číslo) a na konec se doplní chybějící odřádkování.