Kurz C++ (6.)

V této lekci dostáváme možná k nejobtížnější problematice programování a to k ukazatelům. Čtenář se za prvé dozví, co to je ukazatel, dále se dozví, jak ukazatel získá a jak s ním pracuje.

Ukazatele

Každá proměnná, kterou v programu deklarujeme, se nachází v paměti počítače, která je rozdělena na buňky o velikosti jednoho bytu, tedy 8 bitů. Každá buňka má nějaké číslo, pomocí kterého ji lze jednoznačně určit. Tomuto číslu se říká adresa.

Ukazatel (angl. pointer) je proměnná nebo konstanta, která v sobě udržuje adresu nějaké jiné proměnné (říká se, že ukazatel "ukazuje na proměnnou"), nebo i adresu nějaké libovolné buňky v paměti. Ukazatelem je možné číst nebo měnit hodnotu na adrese, kam ukazuje. Jako vždy, ukážeme si příklad:

int a;        	 
int *pInt;          // pInt je ukazatel na proměnnou typu int

pInt = &a;          // ukazateli přiřadíme adresu proměnné a
*pInt = 2;          // měníme hodnotu na adrese, kam ukazatel ukazuje

cout << *pInt;      // vypíše se hodnota 2

Nejdříve jsme deklarovali ukazatel pInt, což se provádí operátorem * s uvedením "na jaký datový typ má ukazatel ukazovat". Situace v paměti vypadá asi takto:

Dále jsme ukazatel nastavili na adresu proměnné a, kterou jsme získali operátorem &:

Na dalším řádku jsme nepřímo přiřadili proměnné a hodnotu 2. Zápis *pInt = 2 znamená: obsah adresy, na kterou ukazuje pInt, interpretuj jako proměnnou typu int a proveď vyžádanou operaci (v našem případě přiřazení čísla 2). Protože ukazatel ukazuje na adresu proměnné a, dojde k přiřazení čísla 2 této proměnné. Operace kterou získáme obsah adresy, na kterou ukazuje ukazatel,se nazývá dereference.

Ukazatel může být pevného datového typu, to znamená, že ukazatel "ví", na jaký datový typ ukazuje, a při dereferenci vrátí přesně ten datový typ. Existuje ještě jeden typ ukazatele, tzv. obecný, který může ukazovat na proměnnou jakéhokoli datového typu, ale datový typ "si nepamatuje", a z tohoto důvodu nemůže být na něm provedena dereference. Obecný ukazatel se deklaruje klíčovým slovem void:

int a = 2;
void *p;            // toto je obecný ukazatel

p = &a;             // přiřadit adresu můžeme
cout << *p;         // kompilátor ohlásí chybu

Kompilátor ohlásí chybu na řádku cout << *p, protože obecný ukazatel je beztypový a neví se jakého datového typu je proměnná, na kterou ukazuje. To ovšem znamená, že to víme my, programátoři, takže obsah adresy, na kterou ukazuje obecný ukazatel, přece jen lze získat, a to přetypováním:

int a = 2;
void *p1;
int *p2;

p1 = &a;
p2 = (int*)p1;             // přetypujeme obecný ukazatel (p1) na ukazatel na proměnnou typu int (p2)
cout << *p2;               // dereference ukazatele p2 již možná je
Poslední dva řádky můžeme spojit do jednoho, je to rozumnější zápis, a určitě elegantnější, protože se vyhneme deklaraci zbytečného ukazatele p2:
cout << *(int*)p1;
Pozor, obecný ukazatel můžeme přetypovat na ukazatel na libovolný datový typ, ale výsledek dereference nebude správný.

Ukazatel může být i prázdný, tedy neukazuje na žádnou adresu. K tomu se používá zvláštní hodnota NULL, v C++ lze používat i 0.

*pInt = NULL;       // v C
*pInt = 0;          // v C++

Možná se ptáte, k čemu vlastně ukazatele jsou. Je pravda, že jsme si neukázali žádné jejich smysluplné použití. Dále vysvětlím souvislost mezi ukazateli a poli. Až si vysvětlíme záznamy ukážeme si další použití významné ukazatelů a záznamů. Dále v kurzu probereme třídy a tam také uvidíte, jak jsou ukazatele důležité.

Ukazatele a pole

Pole a ukazatele mají v jazycích C a C++ k sobě velice blízko. Bez ohledu na rozdílnou syntaxi se tyto jazyky dívají na pole a ukazatele stejně: pole je vlastně ukazatel někam do paměti, kde se nachází seznam proměnných stejného typu těsně za sebou. Řekli jsme si, že se meze polí nekontrolují. Teď chápeme, proč tomu tak je: pole je ukazatel, který pouze ukazuje na proměnnou na nějaké adrese, ale nelze říci, kolik je za ní dalších proměnných stejného typu, to ví pouze programátor.

Také jsme si řekli, že velikost prvku pole získáme zápisem sizeof *pole. Teď chápeme proč: je to dereference prvního prvku pole, která "vrací" přímo ten prvek.

Ukazovali jsme si, že řetězce lze deklarovat zápisem: char *str = "Ahoj";. Deklarovali jsme vlastně ukazatel na proměnnou char. Kompilátor zároveň uložil řetězec "Ahoj" do paměti, a do ukazatele uložil adresu znaku "A".

Ze skutečností výše uvedených vyplývá i to, že s ukazatelem můžeme zacházet jako s polem, například:

char *str = "abc";

cout << str[0] << "\n";
cout << str[1] << "\n";
cout << str[2] << "\n";

Existují i případy, kdy pole a ukazatel nejsou totéž. Typický případ je použití operátoru sizeof:

int pole[5];
int *p;

cout << sizeof pole << "\n";
cout << sizeof p << "\n";
Velikost pole je 20 (5 * 4), ale velikost ukazatele je 4 (ukazatel je 32bitová proměnná).

Také je nutné si uvědomit rozdíl mezi deklarací pole (popř. řetězce) a deklarací ukazatele.

int pole[5];                // deklarace pole - alokuje se místo pro 5 proměnných typu int
int *p;                     // deklarace ukazatele - místo pro proměnnou int se nealokuje
                            // alokuje se pouze místo kam se ukládá adresa (32 bitů = 4 byty)

char str[20] = "Kurz C++";  // deklarace retezce (alokuje se místo pro 20 znaků, prvních 9 z nich se incializuje)
char *str;                  // nealokuje se žádné místo pro řetězec

Aritmetika ukazatelů

Jednou z předností jazyka C a C++ je aritmetika ukazatelů. To nám dovoluje zacházet s ukazatelem jako s číselnou proměnnou: můžeme k němu přičítat čísla, odečítat, zvětšit a zmenšit, porovnávat s jiným ukazatelem a tak podobně. Například aritmetikou ukazatelů můžeme nahradit použití hranatých závorek (to většinou neděláme, ale někdy se nám to může hodit):

int pole[3] = { 10, 20, 30 };
int *p;

p = pole;                            // ukazuje na začátek pole, tedy na první prvek
p = p + 2;                           // posuv ukazatele o dva prvky

cout << *p << "\n";                  // vypíše se třetí prvek pole - *p je nyní to to samé, jako pole[2]
cout << *(p - 1) << "\n";            // vypíše se druhy prvek pole
Další použití je posuv v řetězci:
char *str = "Kurz C++";

cout << str + 5;                     // vypíše se C++
Typické použití je procházení řetězce za účelem nějakého zpracování. Představte si, že bychom potřebovali vypsat řetězec tak, že přeskakujeme nadbytečné mezery (tj. když jich je více za sebou, vypíšeme jen jednu). Nejdříve musíme vymyslet algoritmus (jak to budeme provádět): projdeme všechny znaky pole a pokud znak není mezera vypíšeme ho, jinak ho vypíšeme pouze pokud předchozí zpracovaný znak nebyl mezera:
#include <iostream.h>

// deklarace konstanty - řetězec deklarovaný takto není možné přímo měnit
const char *str = "Píše    se rok    2002.";

void vypis(char *s) {
    char last = 0;                // uchovává posledně zpracovaný znak
    
    while (*s != 0) {             // opakujeme dokud znak není nula (řetězec končí nulovým znakem)
        if (*s != ' ')            // pokud pravě zpracovaný znak není mezera
            cout << *s;           // vypíšeme
        else                      // pravě zpracovaný znak je mezera
            if (last != ' ')      // pokud posledně zpracovaný není mezera
                cout << *s;       // vypíšeme
        last = *s;                // nyní zpracovaný znak bude pro další průchod cyklem while předchozí zpracovaný znak - 
			       // nastavujeme proměnnou last pro další cyklus
        s = s + 1;                // aritmetika ukazatelů - posuneme se na další znak
                                  // zvětšení o jedničku znamená posun na další znak
    }
}

void main() {
    vypis(str);
    cout << "\n";
}


Poznámka: snažil jsem se napsat funkci vypis() tak, aby byla pochopitelná. Ovšem jazyk C++ je známý pro svou stručnost a eleganci, takže si ukážeme, jak naši funkci napsat v tomto duchu. Podmínka cyklu while (*s != 0) bude mít hodnotu false (0) pokud hodnota *s bude 0, a hodnotu true (1, ale také jakákoli nenulová hodnota) pokud hodnota *s bude různá od 0. To znamená, že hodnota podmínky je stejná s hodnotou *s. To využijeme k tomu, abychom napsali cyklus while takto: while (*s). Je to naprosto totéž.

Dále zaměříme svou pozornost na příkazy if. Trochu vadí, že příkaz pro vypsání se opakuje dvakrát, takže zkusíme napsat podmínku pro if tak, abychom si vystačili s jedním if. Platí, že se znak vypíše pokud není mezera nebo pokud mezera je a současně posledně zpracovaný není mezera. Příkaz if, který tomu odpovídá, je

    if (*s != ' ' || (*s == ' ' && last != ' '))
Jsme skoro u cíle, ale není to úplně ono. Výraz *s == ' ' za druhou závorkou bude vždy pravdivý. Do jeho závorky se totiž dostaneme pouze pokud první výraz (*s != ' ') je nepravdivý, a to znamená, že výraz *s == ' ' je pravdivý. Můžeme nadbytečnou podmínku vypustit, a dostaneme:
    if (*s != ' ' || last != ' ')
Poslední "fígl", který Vám chci ukázat, se týká obou dvou posledních řádků funkce vypis(). Z předchozích částí víte, ze v C a C++ existuje operátor ++, který zvětší proměnnou o jedničku. Místo s = s + 1 budeme psát s++. Řekněte, není to hezčí? A teď ta nejlepší část: operátor ++ psaný za měněnou hodnotou funguje tak, že ještě před zvětšením vrací původní (nezvětšenou) hodnotu - v našem případě původní ukazatel (výsledek s++ bude s). To využijeme k nastavení proměnné last, a napíšeme: last = *s++. Takže naše upravená funkce vypadá takto:
void vypis(const char *s) {
    char last = 0;

    while (*s) {
        if (*s != ' ' || last != ' ')
            cout << *s;
        last = *s++;
    }
}
Pokud nerozumíte všem úpravám, nic si z toho nedělejte. Jste přece jen na začátku a C++ je dost složitý jazyk. Pokud se budete alespoň trochu zabývat programováním, přijdete na tyto skutečnosti sami. Měli byste alespoň pochopit zjednodušení podmínky cyklu while, je to v C a C++ opravdu používaný zápis.

Ukazatele a funkce

Když jsme si ukázali funkce pro práci s řetězci uvedl jsem jejich syntaxi, ale značně zjednodušeně. Nyní, vyzbrojení znalostmi o ukazatelích, si můžeme ukázat hlavičky těchto funkcí. Funkce pro zpracování řetězců mohou dostat jako parametry i řetězce značné velikosti. Kdyby se musel funkci předat takový řetězec tak, že by se "do funkce" zkopíroval celý, mohlo by to trvat dost dlouho. Proto existuje mnohem lepší způsob předávání řetězců (a nejenom, ale jakýchkoliv proměnných velkých datových typů): funkci se předá pouze ukazatel na řetězec. Například:

int strcmp(const char *string1, const char *string2);
char* strcpy(char *strDestination, const char *strSource);          // funkce strcpy řetězec i vrací
Předávání parametrů tak, že se předá pouze ukazatel, je také předávání odkazem. V minulém dílu jsme si ukázali předávání odkazem operátorem &. Je to možné i ukazatelem:
void prumery(double a, double b, double *aritm, double *geom) {
    *aritm = (a + b) / 2;
    *geom = sqrt(a * b);
}
Jako parametry aritm a geom samozřejmě předáme adresy proměnných, do kterých se průměry mají uložit, ty získáme operátorem &.

To byl v jazyce C jediný způsob předávání odkazem. V jazyce C++ máme lepší způsob, a to je operátor & (operátor reference - angl. reference operator), jak jsme si ukázali minule. Reference v sobě udržuje adresu nějaké proměnné, ale syntakticky se chová jako ta proměnná. Ale jaký je rozdíl mezi předávání parametru ukazatelem a referencí? Je to jednoduché, nulová reference neexistuje, nulový ukazatel ano. Tedy předávání ukazatelem můžeme použít tam, kde chceme mít možnost v parametru nepředat nic. Jako příklad vylepšíme funkci prumery():

void prumery(double a, double b, double *aritm, double *geom) {
    if (aritm != 0)                 // lépe: if (aritm)
        *aritm = (a + b) / 2;
    if (geom != 0)                  // lépe: if (geom)
        *geom = sqrt(a * b);
}
Funkce prumery() nyní umí počítat pouze aritmetický průměr, nebo pouze geometrický, nebo dokonce ani jeden z nich. Budeme-li mít zájem pouze o aritmetický průměr, zavoláme funkci takto:
double aritm;
prumery(1, 2, &aritm, 0);
Toto bychom referencí neudělali. Ale reference se také hodí, například když naopak chcete mít jistotu, že se skutečně předala nějaká proměnná.


Poznámka: význam klíčového slova const u parametrů string2 a strSource v deklaraci výše uvedených funkcí znamená, že funkce nemění řetězce předané v těchto parametrech, takže lze předat i konstantní řetězec (deklarovaný také klíčovým slovem const - const char *str = "Ahoj";). Bez klíčového slova const by to možné nebylo.
 

To je pro tento měsíc všechno. Pilujte, zkoušejte, nebojte se experimentovat, za měsíc nashledanou.
 

Andrei Badea