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

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.