Ukazatele dělíme do dvou základních kategorií: ukazatele na data (objekty)
a ukazatele na funkce. Oba dva typy ukazatelů jsou speciální objekty, které
obsahují adresy v paměti. Oba dva typy ukazatelů mají různé vlastnosti,
účely použití a pravidla zacházení. Obecně řečeno, ukazatelé na funkce
se používají pro přístup k funkcím a pro předávání funkcí jako parametrů
jiným funkcím. S těmito ukazateli není možné provádět žádné aritmetické
operace. Ukazatele na objekty můžeme inkrementovat a dekrementovat tak,
jak je to při prohlížení polí nebo složitějších struktur potřeba.
Na ukazatel na funkci se můžeme dívat jako na adresu, kde je uchován
proveditelný kód funkce, tj. na adresu, kam je při volání funkce předáno
řízení. Ukazatel na funkci má typ "ukazatel na funkci s určitým počtem
a typy parametrů a vracející jistý typ" (v jazyce C na parametrech
nezáleží). Např. po deklaraci
void (*fce)(int);
je fce ukazatel na funkci s parametrem typu int, která
nic nevrací.
Následující konzolová aplikace ukazuje použití ukazatele na funkci. Program
se nejprve zeptá, zda se má provádět sčítání nebo násobení. Podle této
odpovědi vloží do proměnné operace ukazatel na funkci sečti
nebo na funkci nasob. Dále zadáme dvě čísla, která se použijí jako
parametry vybrané funkce.
int secti(int a,
int b)
{
return a+b;
}
int nasob(int a,
int b)
{
return a*b;
}
int main(int argc,
char **argv)
{
int (*operace)
(int, int);
int x, y,
volba;
cout <<
"Budeme Sčítat (1) nebo Násobit (2) ?";
do {
cin >> volba;
} while (volba
!= 1 && volba != 2);
if (volba
== 1) operace = secti;
if (volba
== 2) operace = nasob;
cout <<
"Zadej dvě celá čísla: ";
cin >> x >>
y;
cout <<
"Výsledek je " << operace(x, y) << endl;;
getch();
return 0;
}
S použitím ukazatele na funkci vytvořte program vypisující tabulku
malé násobilky nebo obdobnou tabulku sčítání. Výpis celé tabulky řešte
jako funkci (ukazatel na funkci provádějící operaci předávejte jako parametr
funkce).
Většinou se budeme ale zabývat ukazateli na data. Ukazatel na data musí
být deklarován tak, aby ukazoval na nějaký konkrétní typ, a to i v případě,
že tento typ je void (což vlastně znamená ukazatel na cokoliv).
Je-li typ libovolný předdefinovaný nebo uživatelem definovaný typ
(včetně void), potom deklarace
typ *ptr;
// pozor - neinicializovaný ukazatel
deklaruje objekt ptr typu "ukazatel na typ". Než začneme
ukazatel používat, musíme jej nejprve inicializovat. Prázdný ukazatel (ukazatel,
který na nic neukazuje) by měl obsahovat adresu, u které je zaručeno, že
se bude lišit ode všech platných adres v daném programu (adresa 0). Pro
lepší čitelnost programů je tato adresa (prázdný ukazatel) označena symbolickou
konstantou NULL (je definována např. v stdlib.h). Všechny ukazatele
je možné testovat na rovnost či nerovnost s hodnotou NULL.
Ukazatel typu "ukazatel na void" nesmí být zaměňován s prázdným
ukazatelem. Při přiřazování hodnot ukazatelů je nutno, aby ukazatelé byly
stejného typu, nebo aby jeden z nich byl typu "ukazatel na void".
Jinak je nutno provést přetypování.
Podívejme se na příklad. Předpokládejme, že máme pole prvků typu int.
Jednotlivé prvky pole můžeme zpřístupňovat pomocí operátoru indexace. To
již známe z dřívějška.
int pole[] = {5,
10, 15, 20, 25};
int promenna = pole[3];
// hodnota 20
Totéž můžeme provést pomocí ukazatele:
int pole[] = {5,
10, 15, 20, 25};
int* ptr = pole;
int promenna = ptr[3];
V tomto příkladě adresa paměti začátku pole je přiřazena ukazateli
ptr.
Tento ukazatel je ukazatelem na datový typ int (při deklaraci je
použit symbol *). Můžeme deklarovat ukazatel na libovolný celočíselný datový
typ, stejně jako na objekty (struktury nebo třídy). Po přiřazení, ukazatel
obsahuje paměťovou adresu začátku pole a tedy ukazuje na pole (jméno proměnné
pole použité bez operátoru indexace, vrací adresu prvního prvku pole).
V tomto případě můžeme pro přístup k poli používat ukazatel i jméno
pole. U dynamických objektů je možno použít pouze ukazatel (viz dále).
Aritmetika ukazatelů je omezena na sčítání, odčítání a porovnávání ukazatelů.
Aritmetické operace na ukazatelích typu "ukazatel na typ" berou
v úvahu délku typu typ, tzn. počet slabik potřebných na uchování
objektu typu typ. Při provádění aritmetických operací se předpokládá,
že ukazatel ukazuje do pole objektů. Je-li tedy ukazatel deklarován jako
ukazatel na objekt typu typ, potom přičtení celočíselné hodnoty
k tomuto ukazateli jej posune o tento počet objektů typu typ. Má-li
typ typ velikost 10 slabik, potom přičtením hodnoty 5 k tomuto ukazateli
jej posuneme o 50 slabik v paměti dále. Rozdílem dvou ukazatelů je počet
prvků pole, které navzájem oddělují tyto dva ukazatele. Rozdíl ukazatelů
má smysl pouze v případě, kdy oba ukazují do stejného pole.
Hodnotu ukazatele je možné převést na hodnotu jiného typu ukazatele
za pomocí mechanismu přetypování:
char *str;
int *ip;
str = (char *) ip;
Obecně platí, že operátor přetypování (typ *) převádí daný ukazatel
na typ "ukazatel na typ".
Všechny příklady konzolových aplikací, se kterými jsme se zatím seznámili
používají lokální alokaci objektů. Tzn. paměť požadovaná pro proměnnou
nebo objekt je získána ze zásobníku programu. Všechnu paměť, kterou program
potřebuje pro lokální proměnné, volání funkcí apod. bere ze zásobníku.
Tato paměť je alokována, když je zapotřebí a uvolňována, když již zapotřebí
není. To obvykle nastává, když program vstupuje do funkce nebo jiného lokálního
bloku kódu. Paměť pro všechny lokální proměnné funkce je alokována při
vstupu do funkce. Při výstupu z funkce, je všechna paměť alokovaná funkcí
uvolněna. Toto probíhá automaticky a nemáme možnost určit jak a kdy paměť
bude uvolněna.
Lokální alokace má výhody a nevýhody. Výhodou je, že paměť může být
v zásobníku alokována velmi rychle. Nevýhodou je, že zásobník má pevnou
velikost a nemůže být změněn za běhu programu. Pokud náš běžící program
přečerpá kapacitu zásobníku, pak je program ukončen chybou.
Pro proměnné standardních datových typů a malá pole je lokální alokace
vhodným řešením. Když ale začneme používat velká pole, struktury a třídy,
pak budeme potřebovat dynamickou alokaci v hromadě.
Hromada zahrnuje všechnu volnou operační paměť počítače a volné místo
na disku (použité pro odkládací soubory). Velikost této paměti bývá obvykle
asi 100 Mslabik. Hromada je tedy podstatně větší než zásobník. S dynamicky
alokovanou pamětí se ale hůře pracuje a je to také pomalejší. Neobejdeme
se ale bez ní.
V předchozích kapitolách jsme se zabývali konzolovou aplikací udržující
adresář našich známých. Při lokální alokaci struktury jsme používali příkazy:
adresar zaznam;
strcpy(zaznam.jmeno
= "Karel");
strcpy(zaznam.prijmeni
= "Novak");
// atd.
Při dynamické alokaci používáme operátor new a v tomto případě
bychom zapsali:
adresar* zaznam;
zaznam = new adresar;
strcpy(zaznam->jmeno
= "Karel");
strcpy(zaznam->prijmeni
= "Novak");
// atd.
První řádek deklaruje ukazatel na strukturu adresar. Další řádek
inicializuje tento ukazatel vytvořením nové dynamické instance struktury
adresar.
Takto vytváříme a zpřístupňujeme objekty. V dalších příkazech vidíme nahrazení
operátoru přímého selektoru složky (.) operátorem nepřímého selektoru složky
(->).
Dynamicky vytvářené pole struktur vyžaduje více práce. V lokální verzi
použijeme např.
adresar seznam[3];
seznam[0].pcs = 53002;
zatímco v dynamické verzi je nutmo postupovat takto:
adresar* seznam[3];
for (int i = 0; i
< 3; i++)
seznam[i]
= new adresar;
seznam[0]->pcs =
53002;
Vidíme, že musíme vytvořit novou instanci struktury samostatně pro
každý prvek pole. Přístup k datovým složkám pole provádíme operátorem indexace
a operátorem nepřímého selektoru složky.
Operátory & a * jsou operátory odkazu (reference) a dereference. Např.
&typový_výraz
převádí typový_výraz na ukazatel na typový_výraz. Všimněte
si, že identifikátory některých objektů (např. jména funkcí a jména polí)
jsou v jistém kontextu automaticky převedeny na typ "ukazatel na objekt".
Operátor & je možné s takovýmito objekty používat, ale jeho výskyt
je vlastně zbytečný (a matoucí). Operátor & říká překladači "Dej mě
adresu proměnné a ne obsah proměnné".
Ve výrazu
* typový_výraz
musí být operand typový_výraz typu "ukazatel na typ",
kde typ je libovolný typ. Výsledek dereference je typ typ.
Pokuste se určit co dělají následující příkazy:
int i, *ukazatel
= &i;
cin >> i;
cout << ukazatel
<< endl << *ukazatel << endl;
Předpokládejte deklarace:
int promenna;
int *ukazatel = &promenna;
int **ukazatel_na_ukazatel
= &ukazatel;
Co provádějí následující příkazy:
**ukazatel_na_ukazatel
= 10;
*ukazatel = 10;
promenna = 10;
*ukazatel_na_ukazatel
= &promenna;
Neinicializovaný ukazatel obsahuje, stejně jako jiná neinicializovaná proměnná,
náhodnou hodnotu. Pokus o použití neinicializovaného ukazatele může způsobit
havárii programu. V mnoha případech provádíme současnou deklaraci a inicializaci
ukazatele. Např.
adresar* pom = 0;
Pokud se pokusíme použít ukazatel NULL (ukazatel nastavený na NULL
nebo nulu), je automaticky procesorem detekován pokus o nedovolný přístup
k paměti (program je ukončen, ale nemohou nastat různé náhodné chyby).
Povšimněte si ještě zápisu operátoru * (přesněji řečeno používání mezer
před a za *). Je jedno zda zapisujeme
int* i;
int *i;
int * i;
Všechny tyto zápisy jsou ekvivalentní a je jedno, kterou možnost zvolíme.
Je ale vhodné jednu z těchto možností si vybrat a dodržovat ji.
Zadání 4 z kapitoly 15 nyní změníme na použití dynamické alokace. Náš program
se změní takto (hlavičkový soubor zůstane beze změny):
#include <iostream.h>
#include <conio.h>
#include <stdlib.h>
#pragma hdrstop
#include "structur.h"
//---------------------------------------------------------------------
#pragma argsused
void zobrazZaznam(int,
adresar adrZaz);
int main(int argc,
char **argv)
{
adresar* seznam[3];
for (int i
= 0; i < 3; i++)
seznam[i] = new adresar;
cout <<
endl;
int index
= 0;
do {
cout << "Jméno: ";
cin.getline(seznam[index]->jmeno, sizeof(seznam[index]->jmeno)-1);
cout << "Příjmení: ";
cin.getline(seznam[index]->prijmeni,
sizeof(seznam[index]->prijmeni)-1);
cout << "Ulice: ";
cin.getline(seznam[index]->ulice, sizeof(seznam[index]->ulice)-1);
cout << "Město: ";
cin.getline(seznam[index]->mesto, sizeof(seznam[index]->mesto)-1);
cout << "Psč: ";
char buff[10];
cin.getline(buff, sizeof(buff)-1);
seznam[index]->psc = atoi(buff);
index++;
cout << endl;
} while (index
< 3);
clrscr();
for(int i
= 0; i < 3; i++) {
zobrazZaznam(i, *seznam[i]);
}
cout <<
"Zadej číslo záznamu: ";
int zaz;
do {
zaz = getch();
zaz -= 49;
} while (zaz
< 0 || zaz > 2);
adresar pom
= *seznam[zaz];
clrscr();
zobrazZaznam(zaz,
pom);
getch();
return 0;
}
void zobrazZaznam(int
cis, adresar adrZaz)
{
cout <<
"Záznam " << (cis + 1) << ":" << endl;
cout <<
"Jméno: " << adrZaz.jmeno << " " << adrZaz.prijmeni <<
endl;
cout <<
"Adresa: " << adrZaz.ulice << endl;
cout <<
" " << adrZaz.mesto <<
endl;
cout <<
" " << adrZaz.psc <<
endl << endl;
}
Změněné řádky jsou v předchozím výpisu zobrazeny červeně. Je deklarováno
pole ukazatelů, pro každý prvek pole je vytvořena instance, operátory přímého
selektoru složky jsou v cyklu nahrazeny operátory nepřímého selektoru složky
a dvakrát byl použit operátor dereference. Funkce zobrazZaznam zůstala
beze změny.
Toto řešení není ideální, bude ještě vylepšeno.
Parametry funkcí v jazyku C jsou volány hodnotou. To znemožňuje funkci
měnit hodnotu parametru tak, aby změněná hodnota byla použitelná mimo funkci,
tzn. jazyk C nepoužívá parametry volané odkazem. Tento problém lze vyřešit
pomocí ukazatelů. Funkci předáme jako parametr adresu proměnné, s jejiž
hodnotou pak budeme pracovat. Např. následující funkce zaměňuje hodnoty
svých parametrů:
void zamen(int *a,
int *b)
{
int c = *a;
*a = *b;
*b = *c;
}
Funkci pak vyvoláme např. takto:
int x = 10, y = 20;
zamen(&x, &y);
Upravte tuto funkci tak, aby výměna prvků proběhla pouze když a
> b a aby funkční hodnota informovala zda k výměně došlo. Vyzkoušejte
v nějakém programu.
Hlavičkový soubor stdlib.h obsahuje řadu různých funkcí. Jsou zde
např. funkce provádějící celočíselné dělení a určující maximální a minimální
hodnotu ze dvou hodnot (div, ldiv, max a min).
Dále tu jsou funkce provádějící převody typů (atoi, atol,
ecvt,
strtod
a řada dalších). Jsou zde také funkce pro generování náhodných čísel, s
kterými jsme se již seznámili. Seznamte se s těmito funkcemi pomocí ukázkových
programů v nápovědě.
Nové pojmy:
Ukazatel je proměnná, která obsahuje adresu jiné proměnné (ukazuje
na jinou proměnnou). Protože ukazatel nemá přímý vztah k aktuálním datům,
používáme pojem nepřímý přístup, když se tímto způsobem odkazujeme na data.
Lokální alokace znamená, že paměť potřebná pro proměnnou nebo objekt
je získána ze zásobníku.
Zásobník je oblast pracovní paměti nastavená programem při spuštění
programu.
Dynamická alokace znamená, že paměť potřebná pro objekt je alokována
v hromadě.
Hromada je všechna virtuální paměť našeho počítače.
Paměť alokujeme dynamicky pomocí operátoru new.
Dereference ukazatele znamená získání obsahu objektu, na který ukazatel
ukazuje.