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:

  1. 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).
  2. 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;

  3. 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.

Jiří Formánek a Andrei Badea