Autor zadání | Miroslav Jaroš |
---|
V tomto cvičení již začneme programovat v jazyce C.
Připomenutí z minulého cvičení
V minulém cvičení jsme si zkusili vytvoření prvního programu, který
vypisuje řetězec Hello World!
na standardní výstup.
#include <stdio.h>
/* Declaration of the main function, returns an integer, doesn't accept any arguments */
int main(void)
{ /* Block begins here */
printf("Hello World!\n"); /* Call of printf(3) to print formatted string */
return 0; /* Ends the execution of the function and returns a value */
} /* Block ends here -- definition of the function */
V tomto programu se objevují následující prvky:
-
#include
direktiva preprocesoru, která vám zpřístupní funkce definované v hlavičkovém souboru, -
stdio.h
hlavičkový soubor, v němž se nacházejí funkce pro práci se vstupem a výstupem, -
int
datový typ definující celé číslo, -
main
jméno funkce – v případě jazyka C je nezbytné, aby existovala právě jednou pro každý spustitelný program, -
(void)
vstupní argumenty funkcemain
, v tomto případě žádné nejsou, -
{}
složené závorky definují blok, v tomto případě tělo celé funkcemain
, -
printf("Hello World!\n")
volání funkce, která vypisuje na standardní výstup předaný formátovací řetězec a další argumenty, -
return
ukončuje průběh funkce a vrátí volajícímu specifikovaný návratový kód, který musí být typu návratové hodnoty funkce. V případě funkcemain
se návratová hodnota propaguje do volajícího shellu jako návratová hodnota procesu.
Funkce scanf(3)
Před samotnými úkoly si připomeňme minulé cvičení a formátovací značky
pro funkci printf(3)
. Tyto značky jsou univerzální pro celou standardní knihovnu jazyka C a jsou
používané i ve funkcích pro načítání vstupu. Jednou z takových funkcí je funkce scanf(3)
, jejíž
prototyp je velice podobný funkci printf(3)
int scanf(char *fmt_str, ...);
-
fmt_str
je formátovací řetězec (prozatím budeme řetězec uvažovat jako sekvenci znaků uzavřených v dvojitých uvozovkách"
), který říká, jaké elementy mají být načteny ze standardního vstupu, -
…
je výpustka (ellipsis), která značí libovolný počet dalších argumentů libovolného typu (typ se odvodí z formátovacího řetězce), -
návratová hodnota je počet úspěšných konverzí, tedy počet úspěšně zpracovaných formátovacích značek z řetězce
fmt_str
.
Funkce scanf(3)
se následně požívá stejně jako funkce printf(3)
, tedy jako první argument jí je předán
formátovací řetězec a následně další proměnné, které budou naplněny hodnotami načtenými ze
standardního vstupu. Pokud například budeme chtít načíst číslo, bude volání vypadat následovně
int x = 0;
scanf("%d", &x);
Po tomto volání bude v proměnné x
uloženo číslo přečtené ze standardního vstupu, které bylo zadáno
v dekadickém formátu. Nicméně, toto volání předpokládá, že funkce scanf(3)
neselže. Abychom ověřili,
že se čtení podařilo, musíme ověřit návratovou hodnotu funkce scanf(3)
.
int x = 0;
if (scanf("%d", &x) != 1) {
printf("Read failed\n");
}
Pro toto cvičení budeme uvažovat pouze dvě formátovací značky:
-
%d
načtení celého čísla v dekadickém zápisu, -
%c
načtení právě jednoho znaku.
Pro načtení znaku je potřeba ve formátovacím řetězci správně použít znak mezery před specifikací
formátovací značky. Pokud bychom zadali formátovací řetězec Abychom tomuto chování předešli, budeme při načítání znaku zadávat formátovací řetězec jako |
Všimněte si znaku ampersand
Pokud vám ale ampersand stále leží v hlavě, můžete si zabrousit na konec tohoto cvičení, kde je tento operátor rámcově vysvětlen. Nicméně vám doporučujeme vyhnout se cestování v čase a počkat dva týdny, kdy bude tento koncept řádně vysvětlen na přednášce. |
Úkol 1. Vykreslení čáry
|
Od tohoto úkolu dále budeme pracovat v souboru drawer.c
, pokud zadání neřekne jinak.
Vaším úkolem bude napsat funkci
void draw_line(int length);
která na standardní výstup vypíše length
znaků #
.
-
Pomocí cyklu
while
vypisujte, dokudlength != 0
, a v každé iteraci snižtelength
. -
Vypsat znak můžete například pomocí funkce
putchar(3)
, která přijímá právě jeden znak typuchar
. -
Po vypsání křížků vypíše znak
'\n'
, tedy znak konce řádku.
Tuto funkci zkuste zavolat z funkce main()
několikrát s různými argumenty.
Jak se bude funkce chovat, pokud jí předáte záporné číslo?
V případě zacyklení programu je možné v linuxovém terminálu program ukončit pomocí kombinace Ctrl + C.
Úkol 2. Testování
Nedílnou součástí programování je také testování. Umožňuje vám specifikovat očekávané chování vašeho programu a taky zachycovat chyby, které můžete prostou nepozorností při úpravách kódu vytvořit.
V předmětu PB071 budeme používat knihovnu CUT. Začneme jednoduchými testy, které
budou kontrolovat standardní výstup vašeho programu. Pro tento účel budeme používat
makro ASSERT_FILE(file, expected)
, které zkontroluje zdali soubor file
obsahuje
expected
. V našem případě budeme kontrolovat obsah „speciálního“ souboru stdout
,
tedy standardní výstup programu. Příklad použití:
ASSERT_FILE(stdout, "Hello World!\n");
Jestli váš program (nebo volání funkcí v testu) vypíše na výstup Hello World!
následován znakem nového řádku, tak test projde, jinak selže.
Pro testování funkce, která čte vstup, se může hodit taky makro INPUT_STRING(string)
,
které simuluje vaše psaní do terminálu. Pro testování funkce main()
neboli
celého programu však doporučujeme sáhnout pro jiném nástroji. Jeden z takových
nástrojů, CLI, budete mít k dispozici i v domácích úkolech.
Několik jednoduchých testů můžete vidět v připravené kostře cvičení v souboru tests.c
.
Vaším úkolem je otestovat další případ chování funkce draw_line()
, který je poněkud
zajímavější, a to zápornou vstupní délku. Jak by se funkce v takovém případě měla chovat?
Jestli váš test neprošel, je nutné funkci opravit. Jak na to se podíváme v dalším úkolu.
Úkol 3. První vstup
Pokud již máte napsanou funkci draw_line()
, můžeme nyní náš program
nechat spolupracovat s uživatelem. Vaším úkolem bude upravit funkci
main()
tak, aby načetla celé číslo ze vstupu a následně toto číslo
použila jako argument funkce draw_line()
. Protože se jedná o
uživatelský vstup, je potřeba ošetřit jeho validitu, aby určité vstupy
nemohly způsobit například pád programu nebo nevalidní výsledek:
-
Použijte funkci
scanf(3)
a načtené číslo předejte funkcidraw_line()
. -
Rozšiřte funkci
draw_line()
o kontrolu validity vstupu. -
Použijte konstrukci
if (condition) { commands; }
pro ověření validity. -
Vstup je nevalidní, pokud je menší než
0
, řádek nulové délky validní je.
Výstup vašeho programu by měl vypadat takto:
$ ./drawer
10
##########
kde první řádek specifikuje spuštění programu, druhý načtení čísla a třetí výstup funkce draw_line()
.
Úkol 4. Bitové operace
Součástí nízkoúrovňového programování jsou taky bitové operace, které vám mohou některé činnosti ulehčit (třeba i implementovat jednoduchou množinu bez potřeby pokročilejších datových struktur).
V jazyku C máte k dispozici následující bitové operace (příklady používají
|
Určitě jste již někdy viděli binární hodiny, vaším úkolem bude napsat funkci
void draw_time(int hours, int minutes);
která přijme čas (v hodinách a minutách) a vypíše jej ve formě binárních hodin. Formát je ponechán na vás. Ukázka níže vypisuje hodiny a minuty v celku, v odkazu výše si můžete všimnout i hodin, které vypisují jednotky a desítky v hodinách (resp. minutách) odděleně.
Výstup vaší funkce může vypadat třeba nasledovně (pro účely testování vám dáváme k disposici více časů):
06:32 ..##. #.....
07:30 ..### .####.
13:37 .##.# #..#.#
17:15 #...# ..####
20:16 #.#.. .#....
23:57 #.### ###..#
03:14 ...## ..###.
Při kontrole validity argumentů předaných do funkce můžete být v pokušení psát kód, který vypadá třeba takto:
Kdyby jste navíc chtěli ošetřovat jednotlivé chyby, tak budete muset přidat spoustu
Takto dostaneme kód, který vypadá následovně:
Můžete vidět, že kód je více čitelný a zbavili jsme se zbytečného odsazení. |
Úkol 5. Výprava do nové dimenze
Vaším úkolem bude vytvořit funkci:
void draw_square(int size);
Tato funkce vykreslí čtverec o velikosti strany size
.
-
Kvůli velikosti fontu v terminálech, budeme jedno políčko čtverce počítat jako dva znaky. Pokud bychom použili jeden, bude náš čtverec spíše obdélníkového tvaru. Tedy výpis jednoho políčka provedeme jako
putchar('#'); putchar('#');
. -
Výsledný obrazec tedy bude mít na každém řádku
2 * size
křížků#
asize
řádků. -
Stále platí, že čtverec se zápornou velikostí strany nelze vykreslit.
-
Ve funkci
main()
upravte volání zdraw_line()
nadraw_square()
. -
Pro vykreslení čtverce bude vhodné použít cyklus
for
.
Program bude po implementaci vypisovat:
$ ./drawer
5
##########
##########
##########
##########
##########
Úkol 6. Obdélník
Nyní svoji implementaci zobecníme a rozšíříme na kreslení obdélníků.
void draw_rectangle(int a, int b, char fill);
-
Vykreslí obdélník o délkách stran
a
ab
. -
a
značí délku strany odpovídající řádku. -
b
značí délku strany odpovídající sloupci. -
fill
je znak, kterým bude obdélník nakreslený. -
Upravte funkci
main()
, tak aby nejdříve načítala dvě čísla a následně načetla jeden znak. -
Při načítání znaku si dejte pozor na přidání mezery do formátovacího řetězce.
-
Ve funkci
main()
upravte volání zdraw_square()
nadraw_rectangle()
.
Program bude po implementaci vypisovat:
$ ./drawer
5
3
$
$$$$$
$$$$$
$$$$$
Bonusový úkol 1. — Kruh
Jako první bonusový úkol implementujeme vykreslení kruhu do terminálu.
void draw_circle(int radius, char fill, char space);
Protože kruh vám nevyplní místa v rozích, bude potřeba přidat další znak, který označí, co je na výsledném obrázku prázdným místem.
-
radius
značí poloměr kruhu. -
Obrázek tedy bude vysoký
2 * radius
řádek. -
Šířka obrázku bude
4 * radius
znaků. -
Stejně jako u čtverce použijeme jako jedno políčko dva znaky.
-
Při vykreslování musíte vypočítat, zda na dané souřadnici
[x, y]
bude kruh, nebo prázdné místo. -
Pro připomenutí analytické geometrie: středová rovnice kružnice má tvar $(x-x_0)^2 + (y - y_0)^2 = r^2$, kde $x_0$ a $y_0$ jsou souřadnicemi středu kružnice.
-
Kruh je množina bodů, jejichž vzdálenost je od obepínající kružnice menší nebo rovna poloměru.
-
Upravte
main()
, tak aby načítal nejdříve jedno číslo a potom dva znaky. -
I zde bude potřeba dát pozor na správné použití mezery před formátovací značkou
-
Ve funkci
main()
upravte volání zdraw_rectangle()
nadraw_circle()
. -
Protože to, co funkce vykreslí, se může lišit na základě podmínek, zkuste postupně nahrazovat porovnání
<
a<=
a nalezněte nejlepší kombinaci, aby váš výtvor vypadal co nejpodobněji kruhu.
Váš program by měl vypsat:
$ ./drawer
10
#
^
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^################^^^^^^^^^^^^
^^^^^^^^########################^^^^^^^^
^^^^^^############################^^^^^^
^^^^################################^^^^
^^^^################################^^^^
^^####################################^^
^^####################################^^
^^####################################^^
^^####################################^^
^^####################################^^
^^####################################^^
^^####################################^^
^^####################################^^
^^^^################################^^^^
^^^^################################^^^^
^^^^^^############################^^^^^^
^^^^^^^^########################^^^^^^^^
^^^^^^^^^^^^################^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Není od vás vyžadováno, aby jste zreplikovali daný výstup 1:1 vůči vzorovému výstupu, přibližné řešení, z kterého je na pohled možné říct, že se jedná o kruh je zcela postačující. |
Bonusový úkol 2. — Elipsa
Jako druhý bonusový úkol vytvořte funkci
void draw_ellipse(int a_axis, int b_axis, char fill, char space);
která vykreslí na standardní výstup elipsu, kde
-
a_axis
určuje šířku poloosy na ose x. -
b_axis
určuje šířku poloosy na ose y. -
fill
je znak, kterým bude elipsa vyplněna. -
space
je znak použitý pro prázdné místo. -
Stejně jako v případě kruhu, nelze vykreslit elipsu se zápornými poloosami.
-
Pokud je vstup nevalidní, funkce vypíše chybovou hlášku a ukončí se.
-
Podobně jako u kruhu, připomeňme si středovou rovnici elipsy $\left(\frac{x-x_0}{a}\right)^2 + \left(\frac{y - y_0}{b}\right)^2 = 1$ , kde
-
$x$ a $y$ jsou souřadnice bodu na elipse,
-
$x_0$ a $y_0$ jsou souřadnice středu elipsy,
-
$a$ je délka poloosy na ose x,
-
$b$ je délka poloosy na ose y.
-
Po implementaci funkce draw_ellipse()
upravte funkci main()
tak, aby akceptovala
na vstupu
-
celé číslo
a_axis
, -
celé číslo
b_axis
, -
znak pro výplň,
-
znak pro mezeru.
Výstup by měl vypadat následovně:
$ ./drawer
15
10
#
^
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^##########################^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^##################################^^^^^^^^^^^^^^
^^^^^^^^^^##########################################^^^^^^^^^^
^^^^^^^^##############################################^^^^^^^^
^^^^^^##################################################^^^^^^
^^^^######################################################^^^^
^^##########################################################^^
^^##########################################################^^
^^##########################################################^^
^^##########################################################^^
^^##########################################################^^
^^##########################################################^^
^^##########################################################^^
^^^^######################################################^^^^
^^^^^^##################################################^^^^^^
^^^^^^^^##############################################^^^^^^^^
^^^^^^^^^^##########################################^^^^^^^^^^
^^^^^^^^^^^^^^##################################^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^##########################^^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Ačkoliv vzorec pro výpočet elipsy obsahuje podíl, zkuste se zamyslet, zda by se tento výpočet nedal přepsat do celočíselné aritmetiky. |
Doplňkové informace: Aneb cesta do budoucnosti a zase zpátky
Nechci se do toho zabrušovat, abych se pak z toho nemusel nějak vybrušovat a neřekl něco špatně.
Poslanec parlamentu České Republiky
Proč píšeme ve funkci scanf(3)
ampersand před proměnnou?
Při volání funkce zadáváme seznam argumentů, které jsou následně funkcí využity. Nicméně v jazyce
C se předání argumentu provede kopií, tedy pokud například zavoláme funkci draw_line()
následujícím
způsobem
int k = 10;
draw_line(k);
tak je hodnota proměnné k
překopírována do proměnné size
(vstupní argumenty jsou proměnné)
existující pouze po dobu běhu funkce draw_line()
. Z toho můžete odvodit, že pokud bychom funkci
scanf(3)
předali proměnnou jako takovou, potom bychom ji vlastně předali hodnotu, která je zcela irelevantní.
Operátor &
v tomto kontextu vrací adresu, na které je v paměti proměnná uložena. Pokud tedy zapíšeme
int k = 10;
scanf("%d", &k);
tak je funkci scanf(3)
předána adresa místo hodnoty, což je funkcí očekáváno, a scanf(3)
po přečtení
hodnoty ze standardního vstupu zapíše tuto hodnotu přímo na předanou adresu. Proto, pokud při
volání scanf(3)
zapomenete před proměnnou použít &
, vyhodnotí váš program zadaný argument jako
adresu v paměti a pokusí se na ní zapsat, což může způsobit i pád programu.