3. díl - Aritmetika ukazatelů v C++

C++ Pokročilé konstrukce v C++ Aritmetika ukazatelů v C++

V minulém tutoriálu jsme se naučili dynamickou alokaci paměti 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 + 5;

// 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:

Odcitani ukazatelů v C++

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:

Porovnávání ukazatelů v C++

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 = data; p_pozice < data + 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( ) {
        printf( "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:

Výpočet průměru v C++

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.


 

Stáhnout

Staženo 2x (3.57 MB)
Aplikace je včetně zdrojových kódů v jazyce C++

 

  Aktivity (3)

Článek pro vás napsal patrik.valkovic
Avatar
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu.

Jak se ti líbí článek?
Celkem (1 hlasů) :
4444 4


 


Miniatura
Předchozí článek
Dynamická správa paměti v C++
Miniatura
Všechny články v sekci
Pokročilé konstrukce v C++
Miniatura
Následující článek
Reference v C++

 

 

Komentáře

Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zatím nikdo nevložil komentář - buď první!