Lekce 3 - Aritmetika ukazatelů v C++
V minulé lekci, Dynamická správa paměti v C++, jsme se naučili dynamicky alokovat pamět v jazyce C++.
Dnešní díl je věnován další práci s ukazateli. Naučíme se s nimi provádět základní aritmetické operace, pracovat s nimi pomocí indexů a vytvoříme jednoduchý program pro výpočet průměru známek.
Aritmetika ukazatelů
Protože ukazatele jsou vlastně adresy do paměti, možná vás napadlo, jestli s nimi půjde nějakým způsobem počítat. Právě touto problematikou se zabývá tzv. pointerová aritmetika.
Přičítání/odečítání celého čísla k ukazateli
Mějme aplikaci z minulé lekce, která v paměti vytvoří blok pro 10 intů. Víme, že pointer pole ukazuje na první int tohoto bloku (neboli dynamického pole, chcete-li). Jak se však dostaneme např. na 5. int?
Víme, že v paměti leží jednotlivé inty bezprostředne za sebou. Adresu pátého prvku tedy vypočítáme tak, že vezmeme adresu pointeru pole (1. prvku) a přičteme k ní čtyřnásobek velikosti intu. Tím získáme adresu 5. prvku, kterou uložíme do pointeru paty_prvek.
Jazyk C++ nám celou záležitost velmi usnadňuje a to pomocí přičítání/odečítání celých čísel k pointeru. Jakmile k pointeru přičteme např. jedničku, C++ jeho adresu nezvýší o 1, ale o velikost prvku, na který pointer ukazuje. V poli se tedy posouváme dopředu nebo dozadu (pokud odčítáme) o n prvků.
// Alokace 100 intů int *pole = new int[100]; if( pole == NULL ) { cout << "Nedostatek pameti." << endl; return 1; } // Výpočet adresy pátého prvku int *paty_prvek = pole + 4; // Uložení hodnoty na pátý prvek *paty_prvek = 56; // Uvolnění paměti delete[] pole; pole = NULL;
Ačkoli to tak doteď mohlo vypadat, tak pointery nejsou jen celá čísla s adresou, ale C++ s nimi pracuje jiným způsobem. +4 ve skutečnosti způsobilo přičtení čísla 16 k adrese (protože 4 inty mají 16 bajtů).
Odečítání ukazatelů
Pokud máme 2 ukazatele, které ukazují na stejný blok paměti, můžeme jejich hodnotu odečíst. Když bude každý ukazatel ukazovat na data, která spolu vůbec nesouvisí, získáme nesmyslnou hodnotu. Pokud bude ale např. jeden ukazatel ukazovat na začátek dynamického pole intů, jako jsme vytvářeli minule, a druhý bude ukazovat např. na pátý prvek tohoto pole, získáme odečtením ukazatelů číslo 4. Zkusme si to a někam před uvolnění paměti připišme do výše uvedeného programu následující řádek:
cout << "Prvek, na ktery ukazuje paty_prvek je v poli na indexu " << paty_prvek - pole << endl;
Výsledek:
Všimněte si, že odčítáme první prvek od pátého. To proto, že pátý je v paměti dále.
Porovnávání ukazatelů
Pokud ukazují 2 ukazatele opět na stejný paměťový blok, ale třeba na jiná místa v něm, můžeme je porovnat pomocí standardních operátorů < > == <= >= a !=. Zjistíme tím zda ukazuje první ukazatel na prvek před prvkem, na který ukazuje druhý ukazatel, zda ukazují oba na stejný prvek nebo naopak první ukazuje na prvek, který je v paměti dále.
if( paty_prvek > pole ) cout << "paty_prvek je v pameti az za pole" << endl;
Výsledek:
Pointery a pole
S paměťovým blokem 100 intů, který jsme výše deklarovali, již dokážeme pracovat pomocí pointerové aritmetiky. Neměl by pro nás být příliš velký problém naplnit pole čísly, např. samými nulami (protože jsme od operátoru new dostali nějakou paměť, nemůžeme si být nikdy jistí, co je v ní uložené).
Kód pro naplnění pole nulami by vypadal asi takto:
int *p_pozice; for (p_pozice = pole; p_pozice < pole + 100; p_pozice++) { *p_pozice = 0; }
Vytvoříme si pomocný pointer, který v cyklu posouváme o 1 prvek dopředu, dokud se nedostaneme na konec bloku. Pomocí tohoto pointeru cestujeme blokem a ukládáme do prvků nuly.
S blokem ovšem můžeme pracovat úplně stejně jako s polem, protože pole v jazyce C++ není také nic jiného, než blok souvislé paměti. Úplně stejně můžeme všechny inty nastavit na 0 i tímto způsobem:
for (int i = 0; i < 100; i++) { p_i[i] = 0; }
K prvkům v bloku tedy můžeme přistupovat jako by to bylo pole, pomocí hranatých závorek a indexů. První způsob pomocí pointerové aritmetiky je rychlejší, jelikož C++ jen přičítá k adrese bajty. Při použití indexů musí C++ vynásobit velikost intu indexem a toto číslo přičíst k adrese začátku pole, což trvá o chlup déle. Rozdíly jsou většinou pro běžnou práci zanedbatelné, ale když již programujeme v C++, budeme se to snažit dělat efektivně.
sizeof()
Pokud by vás napadlo, co vrátí následující kód:
sizeof(*p_i);
Bude to velikost jednoho prvku v bloku, na který ukazuje p_i. V našem případě tedy 4 bajty (velikost intu). Počet prvků v bloku (v našem případě 100) bohužel již nikdy nezjistíme a musíme si ho po založení pamatovat nebo uložit. To je také důvod, proč se textové řetězce v C ukončují znakem '\0'.
Možná by nás ale mohlo zajímat, co udělá operace sizeof(p_i) (všimněte si chybějící hvězdičky). V tomto případě získáme obecnou velikost ukazatele. Velikost ukazatele bude stejná pro všechny typy, tedy sizeof(char*) se rovná sizeof(int*). To je tím, že z principu ukazatel pouze ukazuje na místo paměti. Pro adresaci paměti potřebujeme vždy stejně velkou hodnotu. Například pro 32-bitovou architekturu bude velikost ukazatele 4 bajty, pro 64-bitovou architekturu 8 bajtů.
Příklad: výpočet průměru z čísel
Protože jsme poměrně dlouho teoretizovali, ukažme si na závěr reálný příklad toho, co jsme se naučili. Níže uvedený program se uživatele zeptá kolik chce zadat známek, následně pro ně vytvoří v paměti pole a známky do něj postupně uloží. Na konci vypíše průměr z těchto známek.
Pozn.: Možná namítáte, že průměr bychom mohli vypočítat i úplně bez ukládání známek. když by nás však zajímal např. medián nebo jsme se známkami chtěli nějak dále pracovat, což se v programech stává v podstatě neustále, potřebujeme mít data někde uložena.
#include <iostream> using namespace std; int main( ) { cout << "Zadej pocet znamek: "; int pocet; cin >> pocet; // Alokace bloku s daným počtem intů int* data = new int[pocet]; if( data == NULL ) { cout << "Nedostatek pameti" << endl; return 1; } // Postupné načtení známek do pole for(int* pozice = data; pozice < data + pocet; pozice++ ) { cout << "Zadejte znamku: "; cin >> *pozice; } // Výpočet průměru ze známek int soucet = 0; for(int* pozice = data; pozice < data + pocet; pozice++ ) soucet += *pozice; double prumer = (double)soucet / pocet; cout << "Prumer tvych znamek je " << prumer << endl; // Uvolnění paměti delete[] data; data = NULL; cin.get(); cin.get(); return 0; }
Výsledek:
Zdrojový kód by měl být srozumitelný, jelikož je podobný jako výše uvedené příklady. Zajímavost je přetypování jedné proměnné na typ double při výpočtu průměru. Pokud totiž dělíme v C++ 2 celá čísla, výsledkem je vždy celé číslo. Pokud chceme dělit desetinně, musí být alespoň jedno číslo typu double.
Program je v příloze ke stažení se zdrojovým kódem.
Dobře, po dnešním dílu tedy dokážeme za běhu programu vytvořit libovolně velké pole. Stále však musíme specifikovat jeho velikost. Jak lze tedy vytvořit seznam zboží na skladě, který nebude velikostně vůbec omezen, a do něhož budeme moci položky stále přidávat? Nejjednodušším způsobem je hlídat si velikost pole. Při přidání dalšího prvku, které se už do pole nevejde, vytvoříme nové (větší) pole, původní prvky do něj překopírujeme a původní pole smažeme. Pro uživatele (tedy pro programátora používající takové pole), se potom zdá, že se pole dynamicky zvětšuje. Na velmi podobném principu funguje objekt string.
Příště, v lekci Reference v C++, se podíváme na reference v C++ a v průběhu lekce se s nimi naučíme pracovat.
Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkami
Staženo 66x (3.57 MB)
Aplikace je včetně zdrojových kódů v jazyce C++