-
Odkazy jsou speciálním typem ukazatelů, které umožňují pracovat s ukazateli
jako s normálními objekty. Odkazy jsou deklarovány pomocí operátoru &.
Např.
mojeStruktura* ukStrukt
= new mojeStruktura;
mojeStruktura&
odkaz = *ukStrukt;
odkaz.X = 100;
Povšimněte si, že při přístupu ke složkám struktury jsou použity přímé
selektory složky. V reálných programech obvykle není zapotřebí udržovat
při použití odkazu ukazatel na strukturu a předchozí zápis lze zkrátit
takto:
mojeStruktura&
odkaz = *new mojeStruktura;
odkaz.X = 100;
Nyní se opět vrátíme k naší konzolové aplikaci adresáře našich známých.
Řešení z konce předchozí kapitoly můžeme změnit takto:
#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 {
adresar& zaznam = *seznam[index];
cout << "Jméno: ";
cin.getline(zaznam.jmeno, sizeof(zaznam.jmeno)-1);
cout << "Příjmení: ";
cin.getline(zaznam.prijmeni, sizeof(zaznam.prijmeni)-1);
cout << "Ulice: ";
cin.getline(zaznam.ulice, sizeof(zaznam.ulice)-1);
cout << "Město: ";
cin.getline(zaznam.mesto, sizeof(zaznam.mesto)-1);
cout << "Psč: ";
char buff[10];
cin.getline(buff, sizeof(buff)-1);
zaznam.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 opět zobrazeny červeně. Povšimněte si deklarace
odkazu na strukturu adresar. Při každém průchodu cyklem je odkazu
přiřazen jiný objekt (následující prvek v poli). Pro přístup k prvkům struktury
nyní používáme operátor přímého selektoru složky. Jak uvidíme pozdějí,
odkaz umožňuje chápat ukazatel jako objekt. Výsledkem používání odkazu
dostáváme kratší a čitelnější kód.
Přestože odkazy jsou preferované před ukazateli, není tomu tak vždy.
Odkazy nelze použít ve všech případech. Např. odkaz nemůže být deklarován
bez přiřazení hodnoty. Musí být inicializován při deklaraci. Následující
kód způsobí chybu překladu:
mojeStruktura* ukStruktura
= new mojeStruktura;
mojeStruktura&
odkaz;
odkaz = *ukStruktura;
odkaz.X = 100;
Další problém s odkazem je ten, že jej nelze nastavit na NULL nebo
0, což u ukazalele lze.
-
Parametry u funkcí jsme zatím předávali hodnotou (v předchozí kapitole
byla ukázka předávání parametrů odkazem). V případě struktur a tříd je
ale lepší předávat tyto objekty odkazem. Odkazem může být předán libovolný
objekt (standardní datové typy jako je int nebo char a také
instance struktur nebo tříd). Při předávání parametru funkce hodnotou je
vytvořena kopie objektu a funkce pracuje s touto kopií. Když předáváme
parametr odkazem, pak je předán ukazatel na objekt a ne objekt samotný.
To má dvě výhody. Objekt předaný funkci může být funkcí modifikován a předávání
odkazem eliminuje režijní náklady na vytvoření kopie objektu.
Možnost modifikace objektu je důležitým aspektem při předávání parametrů
odkazem. Podívejte se na následující kód:
void Inkrementace(int&
xPos, int& yPos)
{
xPos++;
yPos++;
}
int x = 20;
int y = 40;
Inkrementace(x, y);
// x je nyní rovno
21 a y 41
Povšimněte si, že po návratu z funkce jsou oba předané parametry zvětšeny
o 1. To je z důvodu modifikace aktuálního objektu funkcí prostřednictvím
ukazatele (nezapomeňte, že odkaz je typ ukazatele).
Funkce může vracet pouze jednu hodnotu. Pomocí parametrů předávaných
odkazem, můžeme dosáhnout efektu návratu více hodnot. Funkce stále vrací
pouze jednu hodnotu, ale objekty předané odkazem mohou být aktualizovány
a funkce tak zdánlivě vrací více hodnot.
Dalším důvodem pro předávání parametrů odkazem je eliminace režijních
nákladů spojených s vytvářením kopie objektů při volání funkce. Při používání
standardních datových typů jsou režijní náklady na vytvoření kopie zanedbatelné.
Při práci se strukturami a třídami mohou být značné. Struktury v libovolné
situaci lze předat odkazem (viz následující ukázka):
void nejakaFunkce(mojeStruktura&
s)
{
// uděláme
něco se s
return;
}
mojeStruktura mojeStr;
....
nejakaFunkce(mojeStr);
Použití odkazu umožňuje modifikaci objektu předaného funkci. Někdy
se ale můžeme dostat do situace, kdy této modifikaci chceme zabránit.
-
Jestliže proměnnou deklarujeme s klíčovým slovem const, pak nemůžeme
změnit její hodnotu. Stejné řešení lze použít i při předávání parametru
funkcí odkazem a udělat tak konstantní objekt:
void nejakaFunkce(const
mojeStruktura& s)
{
// uděláme
něco se s
return;
}
mojeStruktura mojeStr;
....
nejakaFunkce(mojeStr);
Nyní můžeme funkci předávat objekt a nemusíme se obávat, že jej funkce
změní. Pokus o modifikaci konstantního objektu uvnitř funkce způsobí při
překladu chybu. Např.
void nejakaFunkce(const
mojeStruktura& s)
{
s.slozka =
100; // chyba, konstantní objekt nelze modifikovat
return;
}
Takovýto objekt je konstantní pouze uvnitř funkce. Před voláním funkce
a po návratu z funkce jej lze modifikovat (pokud není původně deklarován
jako konstantní).
Když si máme vybrat, zda parametr budeme předávat odkazem nebo ukazatelem,
pak většinou dáváme přednost předávání odkazem. Při předávání znakového
pole je ale výhodnější použít předávání parametru ukazatelem (ukazatel
na znakové pole a jméno pole jsou zaměnitelné).
-
Změňte naši aplikaci s adresářem našich známých tak, že u funkce zobrazZaznam
budete strukturu předávat odkazem.
-
Při vytváření a rušení dynamických proměnných se používají operátory new
a delete. S operátorem new jsme se již seznámili. K uvolnění
paměti používáme operátor delete.
Jak již bylo uvedeno dříve, paměť můžeme alokovat lokálně (v zásobníku)
nebo dynamicky (v hromadě). Následující ukázka kódu alokuje dvě pole. Jedno
je alokováno v zásobníku (lokální alokace) a druhé v hromadě (dynamická
alokace):
char buff[80];
char* velkyBuff =
new char[4096];
V prvním případě velikost alokované paměti je malá a není podstatné,
zda bude použit zásobník nebo hromada. V druhém případě se jedná o velké
pole a je tedy vhodné jej alokovat v hromadě (šetříme místem v zásobníku).
V případě polí (řetězec je pole typu char), dynamické a lokální
instance jsou zaměnitelné. Tzn. používají stejnou syntaxi:
strcpy(buff, "Ahoj");
strcpy(velkyBuff,
"Hodně dlouhý řetězec ......");
// a později
strcpy(velkyBuff,
buff);
Jméno pole bez operátoru indexace ukazuje na začátek pole. Ukazatel
také ukazuje na začátek pole a je tedy jedno zda použijeme jméno pole nebo
ukazatel na pole.
Pokud operátor new nemůže alokovat požadovanou paměť, pak vrací
NULL. To můžeme testovat např. takto:
char* buff = new
char[1024];
if (buff) strcpy(buff,
"Nějaký text");
else SignalizaceChyby();
Jestliže alokujeme velmi velkou oblast paměti nebo alokujeme paměť
v kritické oblasti programu, pak je vhodné testovat ukazatel na přípustnost
(úspěšnost alokace). Běžné alokace lze ponechat bez testování.
-
Všechna alokovaná paměť musí být dealokována (uvolněna), když ji již nepotřebujeme.
U lokálních objektů to probíhá automaticky. Při použití dynamické alokace,
je za uvolňování zodpovědný programátor a k uvolňování paměti používá operátor
delete.
Všechna volání new mají své odpovídající delete. Operátory
new a delete tvoří dvojici.
Použití operátoru delete je snadné:
nejakyObjekt* mujObjekt
= new nejakyObjekt;
// nějaká činnost
s objektem
delete mujObjekt;
// objekt je zrušen
Při používání samotného operátoru delete nejsou žádné problémy,
ale je několik věcí spojených s ukazateli a delete, na které musíme
dávat pozor. Za prvé nesmíme zrušit ukazatel (přesněji řečeno objekt na
který ukazatel ukazuje), který již byl zrušen (vznikne chyba přístupu a
řada dalších problémů). Za druhé můžeme zrušit ukazatel, který byl nastaven
na 0.
Někdy deklarujeme ukazatel pro případ, kdy by mohl být použit, ale
nevíme jistě zda v dané instanci našeho programu byl skutečně použit. Např.
můžeme mít objekt, který je vytvářen, když uživatel provede jistou volbu
v nabídce. Pokud uživatel nikdy tuto volbu neprovede, pak objekt nebude
vytvořen. Problémem je, že v případě vytvoření objektu je zapotřebí zrušit
ukazatel a nerušit jej, když objekt nebyl vytvořen. Zrušením neinicializovaného
ukazatele si způsobíme problémy, protože nevíme na kterou část paměti ukazuje.
Jak již bylo uvedeno dříve je vhodné inicializovat ukazatel nulou,
pokud jej přímo neinicializujeme normální hodnotou. To je ze dvou důvodů
vhodný způsob. První důvod byl popsán výše; neinicializovaný ukazatel obsahuje
náhodnou hodnotu, což je nežádoucí. Druhým důvodem je to, že zrušení ukazatele
NULL je přípustné (můžeme zrušit tento ukazatel a nemusíme se zabývat tím,
zda jej uživatel použil):
ukazatel* nekdy =
0;
// možné použití
nekdy
delete nekdy;
Zrušení ukazatele v tomto případě je v pořádku a ať již ukazatel ukazuje
na objekt nebo je NULL.
Můžeme se také dostat do situace, kdy vytvoříme objekt v jedné části
programu a zrušíme jej v jiné části programu. Přitom část kódu rušící objekt
nemusí být nikdy provedena. V tomto případě by bylo vhodné zrušit objekt
při ukončení programu (v některých případech ale objekt již mohl být zrušen).
K zabránění dvojnásobného zrušení je vhodné ukazatel po zrušení nastavit
na NULL (nebo 0):
ukazatel* vytvoren
= new ukazatel;
// později
delete vytvoren;
vytvoren = 0;
Pokud nyní použijeme na tento objekt dvakrát operátor delete,
pak se nic nestane, neboť není chybou zrušit ukazatel NULL.
Jinou možností, jak zabránit problému dvojnásobného zrušení je testování
ukazatele na nenulovou hodnotu před použitím delete:
if (nekdy) delete
nekdy;
To ale také předpokládá nastavení ukazatele na 0 při jeho zrušení.
Pokud při dynamickém vytvoření objektu použijeme odkaz, pak syntaxe
rušení je jiná. Následující příklad ukazuje tento problém:
mojeStruktura&
odkaz = *new mojeStruktura;
odkaz.X = 100;
delete &odkaz;
Vidíme, že rušení ukazatele v případě odkazu vyžaduje použít adresový
operátor. Odkaz nelze nastavit na 0 a tedy musíme být opatrní a nerušit
odkaz dvakrát.
-
Když se vrátíme k našemu programu adresáře našich známých, tak vidíme,
že program je chybný (ve verzi, kde pracujeme s ukazateli a verzi s odkazy).
Dynamicky alokovaná paměť není uvolňována. Vytvořené pole struktur alokovaných
v hromadě není nikdy uvolněno z paměti. Na konec našeho programu (za řádek
getch();)
je nutno vložit:
for (int i = 0; i
< 3; i++)
delete seznam[i];
Nyní již máme spráný program. Projdeme pole ukazatelů a zrušíme každý
z nich samostatně.
-
Když voláme new k vytvoření pole, pak můžeme použít verzi new[]
tohoto operátoru. Není důležité znát jak toto pracuje, ale je nutno vědět
jak probíhá rušení dynamicky alokovaných polí. Použijeme jednu z předchozích
ukázek, na jejíž konec přidáme příkaz delete[]:
char buff[80];
char* velkyBuff =
new char[4096];
strcpy(buff, "Ahoj");
strcpy(velkyBuff,
"Hodně dlouhý řetězec ......");
// a později
delete[] velkyBuff;
Je zde použit operátor delete[]. Nebudeme se zde zabývat technickým
popisem, ale můžeme si být jisti, že všechny prvky pole jsou zrušeny.
-
Následující konzolová aplikace deklaruje dvourozměrné pole dynamicky. V
našem případě má pole tři řádky a pět sloupců, ale jeho rozměry můžeme
snadno modifikovat. Dvourozměrné pole je tvořeno jednorozměrným polem s
ukazateli na jednotlivé řádky, tj. s ukazateli opět na jednorozměrná pole.
#include <iostream.h>
#include <conio.h>
void display(long
double **);
void de_allocate(long
double **);
int m = 3;
// Počet řádků.
int n = 5;
// Počet sloupců.
int main(int argc,
char **argv) {
long
double **data;
data
= new long double*[m];
// Krok 1: Nastavení řádků.
for
(int j = 0; j < m; j++)
data[j] = new long double[n]; // Krok 2:
Nastavení sloupců
for
(int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
data[i][j] = i + j;
// Inicializace pole
display(data);
de_allocate(data);
getch();
return
0;
}
void display(long
double **data) {
for
(int i = 0; i < m; i++) {
for (int j = 0; j < n; j++)
cout << data[i][j] << " ";
cout << "\n" << endl;
}
}
void de_allocate(long
double **data) {
for
(int i = 0; i < m; i++)
delete[] data[i];
// Krok 1: Zrušení sloupců
delete[]
data;
// Krok 2: Zrušení řádků
}
Snažte se pochopit, jak tento program pracuje. Změňte tento program
tak, aby rozměry pole bylo možno zadat až při spuštění programu.
-
Hlavičkový soubor ctype.h obsahuje řadu funkcí testujících znaky
(isalnum, isalpha, isdigit, islower a další)
a funkcí převádějících znaky (toascii, tolower, toupper,
atd.). Seznamte se s jejich používáním (pomocí nápovědy).
Základní pravidla pro používání ukazatelů a dynamické alokace paměti:
-
Inicializujte ukazatele nulou, pokud jim přímo nepřiřazujete hodnotu.
-
Zabraňte dvojnásobnému zrušení ukazatelů.
-
Není chybou rušit ukazatele nastavené na NULL nebo 0.
-
Po zrušení ukazatele jej nastavte na NULL nebo 0.
-
Dereferencí ukazatelů získáme objekt, na který ukazatel ukazuje.