Python týden Geek tričko zdarma
Tričko zdarma! Stačí před dobitím bodů použít kód TRIKO15. Více informací zde
Pouze tento sleva až 80% na kurzy Python

Lekce 3 - Aritmetika ukazatelů v C++

Unicorn College Tento obsah je dostupný zdarma v rámci projektu IT lidem.
Vydávání, hosting a aktualizace umožňují jeho sponzoři.

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

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

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 34x (3.57 MB)
Aplikace je včetně zdrojových kódů v jazyce C++

 

 

Článek pro vás napsal patrik.valkovic
Avatar
Jak se ti líbí článek?
5 hlasů
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu (NodeJS).
Předchozí článek
Dynamická správa paměti v C++
Všechny články v sekci
Pokročilé konstrukce C++
Miniatura
Následující článek
Reference v C++
Aktivity (5)

 

 

Komentáře
Zobrazit starší komentáře (1)

Avatar
patrik.valkovic
Šéfredaktor
Avatar
patrik.valkovic:7.1.2017 18:39

Zřejmě jsem na to zapoměl, když jsem ten článek přepisoval z C. Je to opraveno.

Odpovědět 7.1.2017 18:39
Nikdy neumíme dost na to, abychom se nemohli něco nového naučit.
Avatar
Odpovídá na patrik.valkovic
Libor Šimo (libcosenior):7.1.2017 20:04

Myslim, ze by bolo vhodne, keby si mu vysvetlil rozdiel medzi << a printf(). Co ty na to? ;-)

Odpovědět 7.1.2017 20:04
Aj tisícmíľová cesta musí začať jednoduchým krokom.
Avatar
Libor Šimo (libcosenior):7.1.2017 20:30

Radsej to trochu spresnim.
Medzi

string s = "fajn";
cout << s << endl;

char s[] = "fajn";
printf("%s\n", s):
Odpovědět 7.1.2017 20:30
Aj tisícmíľová cesta musí začať jednoduchým krokom.
Avatar
Odpovídá na Libor Šimo (libcosenior)
Tomáš Pařízek:7.1.2017 20:40

Teda nebýt Pythonu tak jsem to nikdy nepochopil. Ale díky.

 
Odpovědět 7.1.2017 20:40
Avatar
Tomáš Rabel:29.10.2018 17:37

Jsem trošku zmatený, ale snad to pochopím :-)

 
Odpovědět 29.10.2018 17:37
Avatar
patrik.valkovic
Šéfredaktor
Avatar
Odpovídá na Tomáš Rabel
patrik.valkovic:29.10.2018 19:01

Co ti prosím není jasné?
Jinak rozdíl mezi cout a printf: viz články na textové soubory v C a v C++.

Editováno 29.10.2018 19:03
Odpovědět 29.10.2018 19:01
Nikdy neumíme dost na to, abychom se nemohli něco nového naučit.
Avatar
Odpovídá na patrik.valkovic
Tomáš Rabel:30.10.2018 15:53

Děkuji, už chápu.

 
Odpovědět 30.10.2018 15:53
Avatar
Jan Michálek:8. ledna 10:48

Píše se zde"

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.

Výsledek:

"Prvek, na ktery ukazuje paty_prvek je v poli na indexu 5. "

Nemá náhodou být pátý prvek v poli na indexu 4?

Takže by to mělo být napsané takhle?

int *paty_prvek = pole + 4

Možná se pletu, teprve se učím. Jen bych to chtěl uvést na pravou míru :-)

Odpovědět 8. ledna 10:48
Nemá cenu nic programovat, pokud se to neprogramuje geniálně.
Avatar
Jan Michálek:8. ledna 11:32

Ještě jsem si všiml maličkosti, která mě ale dost zmátla. V odstavci Pointery a pole píšete.

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

A zde jste napsali kod:

int *p_pozice;
for (p_pozice = data; p_pozice < data + 100; p_pozice++)
{
        *p_pozice = 0;
}

Pokud, jsem to pochopil správně tak se odkazujete na výše zadaný kod. Neměl by kod vypadat tedy takhle?

int *p_pozice;
for (p_pozice = pole; p_pozice < pole + 100; p_pozice++)
{
        *p_pozice = 0;
}

Nechci být tzv. "rypálek", ale mne osobně toto zmátlo :-) :-) , ale možná se pletu a autor článku to myslel jinak. O:-)

Odpovědět 8. ledna 11:32
Nemá cenu nic programovat, pokud se to neprogramuje geniálně.
Avatar
patrik.valkovic
Šéfredaktor
Avatar
Odpovídá na Jan Michálek
patrik.valkovic:8. ledna 15:50

Opravil jsem překlepy, které v článku byly. Poslední ukázku jsem nechal jak je, protože tam je deklarované pole jako data.

Odpovědět  +1 8. ledna 15:50
Nikdy neumíme dost na to, abychom se nemohli něco nového naučit.
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.

Zobrazeno 10 zpráv z 11. Zobrazit vše