Slevový týden - Srpen
30 % bodů zdarma na online výuku díky naší Slevové akci!
Pouze tento týden sleva až 80 % na e-learning týkající se Javy.

3. diel - Aritmetika ukazovateľov v C ++

V minulom tutoriále sme sa naučili dynamickú alokáciu pamäte v jazyku C ++. Dnešný diel je venovaný ďalšiu prácu s ukazovateľmi. Naučíme sa s nimi vykonávať základné aritmetické operácie, pracovať s nimi pomocou indexov a vytvoríme jednoduchý program pre výpočet priemeru známok.

Aritmetika ukazovateľov

Pretože ukazovatele sú vlastne adresy do pamäti, možno vás napadlo, či s nimi pôjde nejakým spôsobom počítať. Práve touto problematikou sa zaoberá tzv. Pointerová aritmetika.

Pričítanie / odčítanie celého čísla k ukazovateľu

Majme aplikáciu z minulej lekcie, ktorá v pamäti vytvorí blok pre 10 intů. Vieme, že pointer pole ukazuje na prvý int tohto bloku (čiže dynamického poľa, ak chcete). Ako sa však dostaneme napr. Na 5. int?

Vieme, že v pamäti leží jednotlivé inty hneď za sebou. Adresu piateho prvku teda vypočítame tak, že vezmeme adresu Pointer pole (1. prvku) a pripočítame k nej štvornásobok veľkosti intu. Tým získame adresu 5. prvku, ktorú uložíme do pointera paty_prvek.

Jazyk C ++ nám celú záležitosť veľmi uľahčuje a to pomocou pričítanie / odčítanie celých čísel k pointera. Akonáhle k Pointer pripočítame napr. Jedničku, C ++ jeho adresu nezvýši o 1, ale o veľkosť prvku, na ktorý pointer ukazuje. V poli sa teda posúvame dopredu alebo dozadu (ak odčítame) on prvkov.

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

Hoci to tak doteraz mohlo vyzerať, tak Pointer nie sú len celé čísla s adresou, ale C ++ s nimi pracuje iným spôsobom. +4 v skutočnosti spôsobilo pripočítanie čísla 16 k adrese (pretože 4 inty majú 16 bajtov).

Odčítanie ukazovateľov

Ak máme 2 ukazovatele, ktoré ukazujú na rovnaký blok pamäti, môžeme ich hodnotu odčítať. Keď bude každý ukazovateľ ukazovať na dáta, ktoré spolu vôbec nesúvisí, získame nezmyselnú hodnotu. Pokiaľ bude ale napr. Jeden ukazovateľ ukazovať na začiatok dynamického poľa intů, ako sme vytvárali minule, a druhý bude ukazovať napr. Na piaty prvok tohto poľa, získame odpočítaním ukazovateľov číslo 4. Skúsme si to a niekam pred uvoľnenie pamäti pripíšte do vyššie uvedeného programu nasledujúci riadok:

cout << "Prvek, na ktery ukazuje paty_prvek je v poli na indexu " << paty_prvek - pole << endl;

výsledok:

Odčítanie ukazovateľov v C ++
Tento výukový obsah pomáhajú rozvíjať nasledujúce firmy, ktoré možno hľadajú práve teba!

Všimnite si, že odčítame prvý prvok od piateho. To preto, že piaty je v pamäti ďalej.

Porovnávanie ukazovateľov

Ak ukazujú 2 ukazovatele opäť na rovnaký pamäťový blok, ale napríklad na iné miesta v ňom, môžeme ich porovnať pomocou štandardných operátorov <> == <=> = a! =. Zistíme tým či ukazuje prvý ukazovateľ na prvok pred prvkom, na ktorý ukazuje druhý ukazovateľ, či ukazujú obaja na rovnaký prvok alebo naopak prvý ukazuje na prvok, ktorý je v pamäti ďalej.

if( paty_prvek > pole )
    cout << "paty_prvek je v pameti az za pole" << endl;

výsledok:

Porovnávanie ukazovateľov v C ++

Pointer a polia

S pamäťovým blokom 100 intů, ktorý sme vyššie deklarovali, už dokážeme pracovať pomocou Pointerova aritmetiky. Nemal by pre nás byť príliš veľký problém naplniť poľa číslami, napr. Samými nulami (pretože sme od operátora new dostali nejakú pamäť, nemôžeme si byť nikdy istí, čo je v nej uložené).

Kód pre naplnenie poľa nulami by vyzeral asi takto:

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

Vytvoríme si pomocný pointer, ktorý v cykle posúvame o 1 prvok dopredu, kým sa nedostaneme na koniec bloku. Pomocou tohto Pointer cestujeme blokom a ukladáme do prvkov nuly.

S blokom však môžeme pracovať úplne rovnako ako s poľom, pretože pole v jazyku C ++ nie je tiež nič iné, než blok súvislé pamäte. Úplne rovnako môžeme všetky inty nastaviť na 0 aj týmto spôsobom:

for (int i = 0; i < 100; i++)
{
    p_i[i] = 0;
}

K prvkom v bloku teda môžeme pristupovať ako by to bolo pole, pomocou hranatých zátvoriek a indexov. Prvý spôsob pomocou Pointerova aritmetiky je rýchlejší, pretože C ++ len pripočíta k adrese bajty. Pri použití indexov musí C ++ vynásobiť veľkosť intu indexom a toto číslo pripočítať k adrese začiatku poľa, čo trvá o chlp dlhšie. Rozdiely sú väčšinou pre bežnú prácu zanedbateľné, ale keď už programujeme v C ++, budeme sa to snažiť robiť efektívne.

Sizeof ()

Ak by vás napadlo, čo vráti nasledujúci kód:

sizeof(*p_i);

Bude to veľkosť jedného prvku v bloku, na ktorý ukazuje Ak chcete nájst. V našom prípade teda 4 bajty (veľkosť intu). Počet prvkov v bloku (v našom prípade 100) bohužiaľ už nikdy nezistíme a musíme si ho po založení pamätať alebo uložiť. To je tiež dôvod, prečo sa textové reťazce v C ukončujú znakom '\ 0'.

Možno by nás ale mohlo zaujímať, čo urobí operácie sizeof (Optimálna) (všimnite si chýbajúce hviezdičky). V tomto prípade získame všeobecnú veľkosť ukazovatele. Veľkosť ukazovatele bude rovnaká pre všetky typy, teda sizeof (char ) sa rovná *sizeof (int *). To je tým, že z princípu ukazovateľ iba ukazuje na miesto pamäti. Pre adresáciu pamäte potrebujeme vždy rovnako veľkú hodnotu. Napríklad pre 32-bitovú architektúru bude veľkosť ukazovatele 4 bajty, pre 64-bitovú architektúru 8 bajtov.

Príklad: výpočet priemeru z čísel

Pretože sme pomerne dlho teoretizovali, ukážme si na záver reálny príklad toho, čo sme sa naučili. Nižšie uvedený program sa užívateľa opýta koľko chce zadať známok, následne pre nich vytvorí v pamäti poľa a známky do neho postupne uloží. Na konci vypíše priemer z týchto známok.

Pozn .: Možno namietate, že priemer by sme mohli vypočítať aj úplne bez ukladania známok. keď by nás však zaujímal napr. medián alebo sme sa známkami chceli nejako ďalej pracovať, čo sa v programoch stáva v podstate neustále, potrebujeme mať dáta niekde uložené.

#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ýsledok:

Výpočet priemeru v C ++

Zdrojový kód by mal byť zrozumiteľný, pretože je podobný ako vyššie uvedené príklady. Zaujímavosť je pretypovanie jednej premennej na typ double pri výpočte priemeru. Ak totiž delíme v C ++ 2 celé čísla, výsledkom je vždy celé číslo. Ak chceme deliť desatinné, musí byť aspoň jedno číslo typu double.

Program je v prílohe k stiahnutiu so zdrojovým kódom.

Dobre, po dnešnom dieli teda dokážeme za behu programu vytvoriť ľubovoľne veľké pole. Stále však musíme špecifikovať jeho veľkosť. Ako možno teda vytvoriť zoznam tovaru na sklade, ktorý nebude veľkostne vôbec obmedzený, a do ktorého budeme môcť položky stále pridávať? Najjednoduchším spôsobom je strážiť si veľkosť poľa. Pri pridanie ďalšieho prvku, ktoré sa už do poľa nevojde, vytvoríme nové (väčšie) pole, pôvodné prvky doň prekopíruje a pôvodné polia zmažeme. Pre užívateľov (teda pre programátora používajúce také pole), sa potom zdá, že sa pole dynamicky zväčšuje. Na veľmi podobnom princípe funguje objekt string.


 

Stiahnuť

Stiahnuté 47x (3.57 MB)
Aplikácia je vrátane zdrojových kódov v jazyku C++

 

Predchádzajúci článok
Dynamická správa pamäte v C ++
Všetky články v sekcii
Pokročilé konštrukcia C ++
Článok pre vás napísal Patrik Valkovič
Avatar
Ako sa ti páči článok?
Ešte nikto nehodnotil, buď prvý!
Věnuji se programování v C++ a C#. Kromě toho také programuji v PHP (Nette) a JavaScriptu (NodeJS).
Aktivity (1)

 

 

Komentáre

Avatar
Jan Michálek:8.1.2019 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 :-)

Odpovedať
8.1.2019 10:48
Nemá cenu nic programovat, pokud se to neprogramuje geniálně.
Avatar
Jan Michálek:8.1.2019 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:-)

Odpovedať
8.1.2019 11:32
Nemá cenu nic programovat, pokud se to neprogramuje geniálně.
Avatar
Odpovedá na Jan Michálek
Patrik Valkovič:8.1.2019 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.

Odpovedať
8.1.2019 15:50
Nikdy neumíme dost na to, abychom se nemohli něco nového naučit.
Avatar
Lukáš Taragel:2. januára 11:53

Prosim, pomocí pointeru se snažím naplnit pole hodnotami zvyšující se o +1, ale nedaří se mi to. Tedy chci naplnit pole pomocí pointeru hodnotami 1, 2, 3, ..., 100.

Chápu, že pomocí bloku by se to udělalo např. takhle:

int *pole = new int[100];

for (int i = 0; i < 100; i++)
{
    pole[i] = i + 1;
}

Ale jak to udělat pomocí pointeru? Snažím se o něco podobného a háže mi to chybu:

int *p_pozice;
*p_pozice = 0;

for (p_pozice = pole; p_pozice < pole + 100; p_pozice++)
{
    *p_pozice++;
}
 
Odpovedať
2. januára 11:53
Avatar
Odpovedá na Lukáš Taragel
Patrik Valkovič:2. januára 16:25

Ahoj,
problém je, že ukazateli nenastavuješ v cyklu hodnotu. Navíc nastavuješ hodnotu pointeru, který nikam neukazuje (na druhhém řádku). Správně by to bylo takhle.

int* pole = new int[100];
for(int* pointer = pole; pointer < pole + 100; pointer++)
    *pointer = pointer - pole;
Odpovedať
2. januára 16:25
Nikdy neumíme dost na to, abychom se nemohli něco nového naučit.
Tento výukový obsah pomáhajú rozvíjať nasledujúce firmy, ktoré možno hľadajú práve teba!
Avatar
DarkCoder
Člen
Avatar
Odpovedá na Patrik Valkovič
DarkCoder:2. januára 17:49

Ještě posun:

*pointer = (pointer - pole) + 1;                // pro 1 až 100
Odpovedať
2. januára 17:49
"„Učíš-li se proto, aby sis zapamatoval, zapomeneš. Učíš-li se proto, abys porozuměl, zapamatuješ si."
Avatar
Odpovedá na Patrik Valkovič
Lukáš Taragel:3. januára 13:53

Ahoj, už chápu, díky moc!

 
Odpovedať
3. januára 13:53
Avatar
dvorkam
Člen
Avatar
dvorkam:31. júla 14:53

V tomhle trochu plavu (proto opakuji), ale není namísto makra NULL

int *data = NULL

lepší používat v C++

int *data = nullptr

?

 
Odpovedať
31. júla 14:53
Avatar
DarkCoder
Člen
Avatar
Odpovedá na dvorkam
DarkCoder:1. augusta 6:37

Ano, v C++ je preferováno používat nullptr před NULL.

Jedním z důvodu je využití u přetěžovaných funkcí.

Podívej na následující definice přetěžované funkce myfunc().

void myfunc(char const *ptr);
void myfunc(int i);

Bude-li funkce volána takto:

myfunc(NULL);

Která funkce bude volána?

Překvapivě bude volána tato přetěžovaná funkce:

void myfunc(int i);

Důvodem je to že v C++ je makro NULL definováno jako:

#define NULL 0

což je celočíselná hodnota. To je důvod, proč je volána přetěžovaná varianta funkce myfunc(int). A to je problém.

Řešením je použití nullptr

Bude-li funkce volána takto:

myfunc(nullptr);

Pak bude volána tato přetěžovaná funkce:

void myfunc(char const *ptr);
Odpovedať
1. augusta 6:37
"„Učíš-li se proto, aby sis zapamatoval, zapomeneš. Učíš-li se proto, abys porozuměl, zapamatuješ si."
Avatar
dvorkam
Člen
Avatar
Odpovedá na DarkCoder
dvorkam:1. augusta 7:32

Děkuji za odpověd. To s tim přeťežováním mě ani nenapadlo, velmi zajímavý poznatek. Děkuji.

 
Odpovedať
1. augusta 7:32
Robíme čo je v našich silách, aby bola tunajšia diskusia čo najkvalitnejšia. Preto do nej tiež môžu prispievať len registrovaní členovia. Pre zapojenie sa do diskusie sa zaloguj. Ak ešte nemáš účet, zaregistruj sa, je to zadarmo.

Zatiaľ nikto nevložil komentár - buď prvý!