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