Kurz C++ (4.)
V této, již čtvrté lekci, se podíváme na typovou konverzi typů, což
je velice důležitá věc, když chceme převádět proměnnou jistého typu na typ
jiný. Druhá část kurzu se zabývá preprocesorem jazyka C. Vysvětlím Vám,
jak se vytvářejí makra, jak se definují symbolické konstanty atd.
4.1. Typová konverze
Nejdříve se podíváme na převody proměnných určitého typu na jiný typ
(například z int na double). Existují dva druhy typové konverze:
- implicitní čili automatická konverze
- explicitní čili vynucená konverze
4.1.1. Implicitní typová konverze
Pravidla:
- Před vykonáním operace se typy char a
short konvertují na int. Zde
není žádný problém, protože char i
short jsou celočíselné hodnoty a
maximální hodnoty jsou menší než u typu int.
Typy
unsigned char
(BYTE) a unsigned short (WORD) se automaticky konvertují na
int jen
pokud se hodnota proměnné vejde do maximální hodnoty
int jinak se
konvertuje na unsigned int (UINT).
- U operací s různými typy operandů se konvertuje podle priority typu.
Implicitní konverze vždy probíhá jen na typy s vyšší prioritou.
Hierarchie priorit jednotlivých typů (typ int
má nejnižší prioritu):
int
| ->
| unsigned int (UINT) |
unsigned int (UINT)
| ->
| long |
long
| ->
| unsigned long (DWORD) |
unsigned long (DWORD)
| ->
| float (FLOAT) |
float (FLOAT)
| ->
| double |
double
| ->
| long double (nejvyšší priorita)
| V závorkách jsou uvedeny typy, které se
používají ve Visual C++.
Příklad:
int
i = 5; double d;
// // Toto je mozne diky
implicitni konverzi // Typ int (i) se automaticky prevede na typ
float (d)
d = i;
- V uvedeném příkladu vidíte přiřazení. V přiřazení je pravý operand
konvertován na typ levého operandu čili výsledek má typ levého operandu.
4.1.2. Explicitní typová konverze
Tuto konverzi plně řídí programátor. Může tak prakticky převést cokoliv
na cokoliv, ale nezaručí vždy přesné nebo očekávané výsledky (např.
konverze z double na int zaokrouhlí reálné číslo na celé číslo -
3.14 ->
3).
Explicitní typová konverze má tvar:
(typ)
vyraz
a znamená to, že vyraz je v čase
překladu konvertován na typ .
Můžeme také
použít explicitní konverzi tam, kde mi normálně proběhla implicitní, ale
programátor tak může vyjádřit, že konverzi chtěl.
Seznam často
používaných explicitních konverzí:
(int) char_vyraz
| - převod znaku na ordinální číslo (pořadí v ASCII tabulce) |
(char) int_vyraz
| - převod ordinálního čísla na znak |
(int) float_vyraz
| - zaokrouhlení reálného čísla (zaokrouhluje se vždy dolu)
| Pokud se pokusíte o implicitní konverzi,
která nějakým způsobem zhoršuje přesnost čísla, kompilátor vás na to
upozorní. Toto varovné hlášení odstraníte explicitní
konverzí.
Příklad:
double d =
3.14; int i;
// // Explicitni konverze odrizne
desetinnou cast // takze promenne i se priradi hodnota 3
i =
(int) d;
4.2. Preprocesor jazyka C
Preprocesor zpracovává kód ještě před vlastním překladem. Zatím
jsme používali jen příkaz include , který nám
umožňoval používat nějaké další užitečné funkce v našich programech. V
této lekci si rozšíříme znalosti příkazů preprocesoru. Preprocesor
připraví Váš kód k překladu.
Preprocesor provádí:
- Nahrazuje symbolické konstanty za správné číselné hodnoty
- Nahrazuje všechny makra vlastním kódem makra
- Vypustí z kódu všechny komentáře
- Provádí podmíněný překlad
Jistě jste si všimli, že příkaz
include musí mít před sebou znak # . To je spravný postřeh a neplatí jen pro příkaz include , ale pro všechny ostatní příkazy preprocesoru.
Takže znakem # uvozujeme všechny příkazy určené pro
preprocesor.
4.2.1. Symbolické konstanty
Symbolickým konstantám se někdy též říká makra bez parametrů. Pomocí
těchto konstant zbavíte program "magických čísel" tzn. konstant, které
používáte v programu. Když pak někdo čte váš program a vidí konstantu
PI místo čísla 3.141592654 ,
velmi ho to potěší.
Navíc definováním takových konstant můžete
snadno měnit parametry programu. Když například vypisujete 100 řádek na
monitor, ale náhle rozhodnete, že chcete aby program vypisoval jen 50
řádek, stačí změnit konstantu na začátku programu a nemusíte přepisovat
všechny konstanty v programu.
Syntaxe:
#define
JMENO_KONSTANTY hodnota
Pro symbolické konstanty platí tyto
pravidla:
- Jméno konstant se píší velkými písmeny (je to pouze doporučení)
- Jméno konstanty je odděleno od vlastní hodnoty nejméně jednou
mezerou
- Za hodnotou by měl být komentář
- Nové konstanty mohou využívat existující konstanty
- Pokud je hodnota konstanty dlouhá (např. řetězec) a nevejde se na
jednu řádku, musí být na konci znak
"\" .
Příklady:
#define PI
3.141592654 // Presne Ludolfovo cislo #define
DATA_TXT "DATA.TXT" // Jmeno souboru #define EOL
'\n' // Odrakovani - End of
Line #define DLOUHY_RETEZEC "Tohle je strasne dlouhy retezec,
\
takze bacha."
A
teď můžete psát definované konstanty místo konkrétních číselných hodnot a
preprocesor je nahradí správnými hodnotami při překladu.
Poznámka: Makro se v programu nerozvine (nebude nahrazeno), pokud
je uzavřeno v uvozovkách. Například:
printf("Ludolfovo cislo je PI\n");
Toto je špatně,
konstanta PI nebude nahrazena. Řešením může být třeba toto: printf("Ludolfovo cislo je %f\n", PI);
Platnost definice konstanty
Pokud nadefinujete již definovanou
konstantu a přitom změníte její hodnotu, kompilátor vypíše varovné
hlášení. Pokud chcete v průběhu programu konstanty měnit, musíte ji
nejdříve "oddefinovat" a teprve poté ji nadefinovat znovu.
Příklad:
#define MAX_POLE 50
// prvni definice MAX_POLE .. .. .. #undef MAX_POLE
// Oddefinovani stare definice #define MAX_POLE
75 // Definice nove hodnoty
Toto
platí obecně pro všechny makra tzn. symbolické konstanty i makra s
parametrem.
4.2.2. Makra s parametrem
Tyto makra fungují podobně jako funkce. Na začátku programu si
nadefinujete určité makra, které pak použijete v programu, preprocesor
opět nahradí makro konkrétním kódem v době překladu.
Použití makra je rychlejší než funkce, protože se nic nevolá, jen se v
kódu nahrazují kousky kódu makra, ale výsledný program je větší, protože s
každým výskytem makra, se kód makra opakuje narozdíl od funkce.
Syntaxe makra:
#define jmeno_makra(arg1,....,argN)
telo_makra
Příklad:
#define
je_velke(c) ((c) >= 'A' && (c) <= 'Z')
V
programu pak makro voláte takto:
ch = je_velke(ch) ? ch
+ ('a' - 'A') : ch;
Těsně před překladem se makro rozvine
takto: ch = ((ch) >= 'A' && (ch) <=
'Z')) ? ch + ('a' - 'A') : ch;
Dobré rady:
- Argument použitý v makru (v našem případě c) by měl být v definici
makra v závorkách. Předejdete tak zbytečným chybám, když jako parametr
makra použijete výraz.
- Doporučuji celé makro též uzavřít do kulatých závorek. Opět se
vyvarujete chyb, když makro použijete ve výrazu.
4.2.3. Předdefinované makra
Soubor stdio.h obsahuje několik maker, které
jsme již využívali: putchar(c) a getchar()
My si uvedeme další hlavičkový soubor
ctype.h , který obsahuje definice dalších užitečných
maker. Makra jsou zde rozdělena do dvou skupin, z nichž první skupina
nemění hodnotu parametrů, ale jen zjišťují vlastnosti parametru:
Jméno
| Použití |
isalnum
| Vrací argument, pokud je argument číslice, malé či velké
písmeno, jinak vrátí 0 (FALSE) |
isalpha
| Vrací argument, pokud je argument malé či velké písmeno, jinak
vrátí 0 (FALSE) |
isascii
| Vrací 1 (TRUE), pokud je argument z ASCII tabulky, jinak vrátí 0
(FALSE) |
isdigit
| Vrací znak (číslici), pokud je argument číslice, jinak vrátí 0
(FALSE) |
islower
| Vrací znak, pokud je argument malé písmenko, jinak vrátí 0
(FALSE) |
isspace
| Vrací znak, pokud je argument neviditelný znak (mezera,
tabulátor), jinak vrátí 0 (FALSE) |
isupper
| Vrací znak, pokud je argument velké písmenko, jinak vrátí 0
(FALSE) | Narozdíl makra druhé skupiny mění
hodnotu parametru:
Jméno
| Použití |
tolower
| Převede argument (velké písmenko) na malé písmenko |
toupper
| Převede argument (malé písmenko) na velké písmenko
|
4.3. Pole
Velice užitečnou součástí programovacího jazyka jsou pole. Představte
si, ze byste chtěli napsat jednoduchý telefonní seznam. Určitě Vás napadá,
že ukládat jména Vašich kamarádů tak, že pro každé jméno budete mít jednou
proměnnou, není moc dobrý nápad. To byste museli program zkompilovat znovu
pokaždé, když chcete někoho přidat. A to je jen ta nejmenší nevýhoda. Pole
Vám dovolí používat více proměnných stejného typu, jedna vedle druhé, pod
stejným jménem: void main(int argc, char *argv[]) {
int pole[10]; // deklarujeme pole 10 proměnných int
pole[5] = 1; // měníme jedno z prvků pole
cout << pole[5]; // a vypisujeme ho
}
V příkladu jsme deklarovali pole deseti prvků typu int , a ukázali jsme si, jak se k jednomu
z prvků pole přistupuje, totiž pomocí stejného operátoru, kterým se pole
deklarují - hranatých závorek. Je důležité si pamatovat, že počítání prvků
pole vždy začíná nulou, takže můžeme používat čísla prvků (správně se jim
říká indexy) 0 až 9 (v dalších příkladech budu vynechávat funkci
main() ,
proto ji nezapomínejte pokaždé přidat):
int pole[10];
for (int i = 0; i < 10; i++)
pole[i] = i; // procházení polem cyklem for
for (int i = 0; i < 10; i++)
cout << pole[i] << ' ';
Dalším důležitým faktem je, že kompilátor nekontroluje, zda jsme
nepřekročili meze pole, takže když napíšeme něco jako: int pole[10];
int dalsi;
pole[10] = 1; // 10 není platný index
přepíšeme si tímto jinou proměnnou, zde zrovna proměnnou další , a program nám určitě nebude
fungovat správně. Musíme si tedy pamatovat, že nejvyšší index, který
můžeme používat, je o jedničku menší, než deklarovaná velikost pole.
Nejmenší index je 0, ačkoli jestli se pokusíte použít záporné číslo
kompilátor Vám to klidně dovolí.
Jazyky C a C++ obsahují i prostředek, kterým se zjišťuje velikost pole,
je jím operátor sizeof :
char pole_char[10];
int pole_int[10];
cout << sizeof pole_char; // vypíše se 10
cout << sizeof pole_int; // vypíše se 40
Výsledek druhého řádku cout
možná překvapí. Je to tím, že operátor sizeof vrací skutečnou velikost pole v
bajtech, ne počet jeho prvků. U pole prvků typu char je to jedno, ale to jen protože char je velký jeden byte. U prvků int to už jedno není. Kdybychom chtěli zjistit počet
prvků, musíme používat jeden z těchto zápisů: cout << sizeof pole / sizeof int;
cout << sizeof pole / sizeof pole[0];
cout << sizeof pole / sizeof *pole; // tento zápis pochopíte později
Podle prvního způsobu zjišťujeme počet prvků "napevno" - víme, že se
jedná o pole "intů", takže dělíme přímo velikostí datového typu int (zjišťujeme-li velikost datového
typu, musí tento být uveden v závorkách, u proměnných jsou závorky
nepovinné).
Podle druhého a třetího způsobu zjišťujeme přímo velikost prvního prvku
pole, a nezáleží na tom, jakého je typu. Druhému způsobu byste měli
rozumět hned, a třetí způsob, který se mi zdá ze všech nejelegantnější,
pochopíte později, až budeme probírat ukazatele, zatím stačí když budete
vědět, že zápis *pole vrací první
prvek pole, má tedy stejný význam jako pole[0] .
Výhoda druhého a třetího způsobu spočívá v tom, že kdybyste v budoucnu
změnili datový typ prvků pole na jiný, stačí, když ho upravíte pouze v
deklaraci pole, ale nemusíte ho měnit i v kódu sizeof . Doporucuji vždy používat
operátor sizeof tam, kde to je
možné, místo pevně zadaného rozměru pole.
Třetí způsob, který se mi zdá ze všech nejelegantnější, úplně pochopíte
později, až budeme probírat ukazatele, zatím stačí když budete vědět, že
zápis *pole vrací první prvek
pole, má tedy stejný význam jako pole[0] .
Při deklaraci pole ho můžeme hned inicializovat. Například: int pole1[4] = { 0, 1, 2, 3 };
int pole2[] = { 0, 1, 3, 3, 4, 5 };
Jak vidíte, v deklaraci pole2 chybí počet prvků. To
protože z počtu konstant ve složených závorkách (inicializátorů)
kompilátor pozná, jak velké musí alokovat pole. Tento zápis je výhodnější,
protože umožňuje určit velikost pole nepřímo, podle počtu inicializátorů.
Uvedení jak rozměru pole, tak i inicializátorů je jistá redundance, jeden
z těchto údajů je nadbytečný, můžeme ho tedy vynechat. Tady je také vidět
důležitost operátoru sizeof - bez
něj bychom velikost pole pole2
nemohli zjistit. Tato vlastnost se nám také bude hodit při
deklarování řetězců, jak uvidíme později.
4.4. Vícerozměrná pole
Jazyky C a C++ umožňují používat i pole vícerozměrná. Deklarují se
obdobně jako jednorozměrná pole:
float matice[3][4];
for (i = 0; i < 3; i++)
for (j = 0; j < 4; j++)
matice[i][j] = i * j; // postupné procházení a naplnění pole
Tady jsme deklarovali dvourozměrné pole, první rozměr je 3, druhý je 4.
Je to vlastně matice o třech řádcích a čtyřech sloupcích. Na tuto
deklaraci je také možno hledět jako na pole tří polí, z nichž každé má
čtyři prvky typu float. Možná to zní složitě,
ale skutečně nic na tom není a určitě si rychle zvyknete. Z tohoto pohledu
vyplývá také použití operátoru sizeof : cout << sizeof matice; // vypisuje se velikost v bajtech, tedy 12 * 4 = 48 //(velikost typu float je 4 byty)
cout << sizeof matice[0]; // vypisuje se velikost "prvního řádku", tedy 4 * 4 = 16
Inicializace vícerozměrných polí se provádí obdobně jako
jednorozměrných: int pole[2][3] = {
{0, 1, 2},
{3, 4, 5}
};
Všimněte si, jak tato deklarace opravu připomíná to, co je výše řekl o
"poli polí". Vnější složené závorky jako by začínají inicializaci pole
dvou prvků, tyto jsou ale zase pole. Toto uspořádání kódu nemusíte
dodržovat a můžete vše napsat na jednom řádku, já si myslím, že je
přehledný, protože je v něm vidět ta matice.
Samozřejmě není problém deklarovat pole i více než dvourozměrná, prostě
přidáme další pár hranatých závorek s dalším rozměrem.
4.5. Řetězce
Pole a řetězce mají v C a C++ k sobě velmi blízko. Tyto jazyky totiž
pohlížejí na řetězce jako na pole prvků char: char retezec[100];
Takto jsme deklarovali řetězec, který může obsahovat maximálně 99
znaků. Proč 99, když jsme deklarovali pole o 100 prvcích? To protože jazyk
C++ vyžaduje, aby poslední znak řetězce byl speciální ASCII znak s kódem
0. Tímto pozná, kde řetězec končí, například při jeho vypisování. To
znamená, že když deklarujeme řetězec musíme uvést počet znaků o jednu
větší než maximální počet znaků, který zamýšlíme do řetězce ukládat.
Výše deklarovaný řetězec nám zatím není k ničemu, zkusíme si tedy ho
rovnou inicializovat. Tady se nám bude hodit možnost inicializace bez
uvedení počtu znaků. Kdybychom museli ten počet uvést museli bychom znaky
řetězce počítat... no, nebylo by to nic pěkného: char str[] = "Ahoj";
Kompilátor poznal, že deklarujeme řetězec, a alokoval pro řetězec 5
bytů, což uvidíte, když si necháte vypsat velikost str operátorem sizeof. Existuje ještě jeden způsob, elegantnější,
úplně ho zase pochopíte až budeme brát ukazatele, zatím musíte jen vědět,
že to znamená úplně to samé: char *str = "Ahoj";
Na druhou stranu se nám ale může hodit i inicializace uvedením počtu
prvků, například kdybychom chtěli mít možnost ukládat do proměnné i delší
řetězce než je ten uvedený při deklaraci. Musíme ovšem dávat pozor na to,
abychom deklarovali alespoň tolik znaků, kolik má inicializátor
(samozřejmě plus jedna), jinak kompilace skončí chybou. char str[20] = "Další řetězec.";
char str[5] = "Toto skončí chybou";
Jazyk C++ nabízí pro práci s řetězci více funkcí. Všechny jsou uloženy
v takzvané run-time knihovně, což je soubor funkcí, který se přidá
k vašemu programu při jeho sestavení (build). K nejdůležitějším funkcím
pro práci s řetězci patří: strlen ,
strcpy , strcat , strchr ,
strcmp a jiné (je jich
skutečně spousta, uvedl jsem jen nejpoužívanější):
Funkce strlen vrací délku
řetězce, volá se strlen(řetězec) : char str1[30] = "Já jsem krátký řetězec.";
cout << strlen(str1);
Pozor, neplést si funkci strlen
a operátor sizeof , jsou to dvě
různé věci: sizeof vrací velikost
řetězce, tedy kolik znaků se do něj maximálně vejde (včetně nulového znaku
na konci) - podle našeho příkladu by to bylo 30, kdežto strlen vrací skutečný počet znaků, který
řetězec obsahuje (to poznává podle nulového ukončovacího znaku, který se v
našem případě nachází hned za tečkou).
Funkce strcpy kopíruje řetězec
do jiného včetně nulového ukončovacího znaku, volá se strcpy(kam, odkud) . Například: char str1[] = "Zkopíruj me!";
char str2[20];
strcpy(str2, str1);
cout << str2; // nyní str2 obsahuje stejný text jako str1
Při používaní této funkce je třeba dávat pozor na to, že nekontroluje
velikost cílového řetězce. Jestliže se pokusíme zkopírovat 100znakový
řetězec do řetězce, jehož velikost je 10, funkce strcpy si nebude stěžovat, velmi ochotně
to provede, ale na funkčnosti programu se toto asi projeví katastrofálním
způsobem, protože nám určitě přepíše část jiných dat.
Teď, když známe funkci strcpy
můžeme si ukázat další možnost inicializace řetězců, a to řetězcovou
konstantou v programu: char str1[20];
strcpy(str1, "Konstanta"); // toto je řetězcová konstanta
cout << str1;
Jde o to, že v v programu můžeme kdykoli používat řetězec, který nemá
jméno a který jsme předtím nedeklarovali. Je to totéž jako když napíšeme
a = 3 . Tu trojku jsme přece nikde
nedeklarovali, a s řetězci je to to samé. Nevýhoda tohoto postupu ale je,
že k jednou použitému řetězci se nemůžete vrátit, nemůžete se k němu znovu
odkazovat, prostě ho musíte napsat znovu. A až Váš program bude slavný a
budete ho chtít přeložit do jiného jazyka, budete muset projít celý kód a
hledat kde všude máte řetězcové konstanty. Je to dřina, a tak Vám tuto
praktiku příliš nedoporučuji.
Chceme-li spojit dvě řetězce do jednoho, použijeme funkci strcat(kam, odkud) . Například: char str1[20];
strcpy(str1, "První ");
strcat(str1, "Druhý");
cout << str2; // vypíše se První Druhý
Další užitečnou funkcí je strcmp . Jak jste možná poznali z jejího
jména (cmp je zkratka anglického compare), slouží k
porovnávání řetězců. Funkce se volá strcmp(první_řetězec, druhý_řetězec) , a
vrací hodnotu int, která má tento význam: je-li
menší než nula, první_řetězec by
byl ve slovníku před druhý_řetězec , je-li větší než nula bylo
by to naopak, a je-li nula řetězce jsou stejné. Tato funkce má variantu
stricmp , která porovnává bez
ohledu na velikost písmen (vnitřně převádí všechna písmena na
malá). char str1 = "abcd";
char str2 = "bcde";
int vysledek;
vysledek = strcmp(str1, str2);
cout << vysledek;
Těšíme se příště nashledanou.
|