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

C++ Dynamická práce s pamětí Aritmetika ukazatelů v jazyce 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 100 intů. Víme, že pointer p_i 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 p_i (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 p_paty.

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éčko 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ů.

int *p_i, *p_paty;
// Alokace 100 krát velikosti intu
p_i = (int *) malloc(sizeof(int) * 100);
if (p_i == NULL)
{
        printf("Nedostatek paměti.\n");
        exit(1);
}

// Výpočet adresy pátého prvku
p_paty = p_i + 4;

// Uložení hodnoty na pátý prvek
*p_paty = 56;

// Uvolnění paměti
free(p_i);
p_i = NULL;

Ačkoli to tak doteď mohlo vypadat, tak pointery nejsou jen celá čísla s adresou, ale Céčko 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:

printf("Prvek, na který ukazuje p_paty je v poli na indexu %d.", p_paty - p_i);

Výsledek:

Aritmetika pointerů v jazyce 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 (p_paty > p_i)
{
        printf("p_paty je v paměti až za p_i");
}

Výsledek:

Porovnávání pointerů v jazyce 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 mallocu 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 = p_i; p_pozice < p_i + 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:

int i;
for (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éčko jen přičítá k adrese bajty. Při použití indexů musí Céčko 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éčku, 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 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 4bajty, 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 <stdio.h>
#include <stdlib.h>

int main(int argc, char** argv) {
    int pocet, *p_i, *p_pozice;
    printf("Zadej počet známek: ");
    scanf("%d", &pocet);
    // Alokace bloku s daným počtem intů
    p_i = (int *) malloc(sizeof(int) * pocet);
    if (p_i == NULL)
    {
        printf("Nedostatek paměti.\n");
        exit(1);
    }
    // Postupné načtení známek do pole
    for (p_pozice = p_i; p_pozice < p_i + pocet; p_pozice++)
    {
        printf("Zadej známku: ");
        scanf("%d", p_pozice);
    }
    // Výpočet průměru ze známek
    int soucet = 0;
    for (p_pozice = p_i; p_pozice < p_i + pocet; p_pozice++)
    {
        soucet += *p_pozice;
    }
    double prumer = (double)soucet / pocet;
    printf("Průměr tvých známek je: %lf.", prumer);
    // Uvolnění paměti
    free(p_i);
    p_i = NULL;
    return (EXIT_SUCCESS);
}

Výsledek:

Výpočet průměru známek v jazyce C

Zdrojový kód by měl být srozumitelný, jelikož je podobný jako výše uvedené příklady. Za povšimnutí stojí, že při načítání známek pomocí scanf() do p_pozice neuvádíme znak & ani *, protože pointer je sám adresou, kterou scanf() v parametru očekává. Další 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éčku 2 celá čísla, výsledkem je vždy celé číslo. Pokud chceme dělit desetinně, musí být alespoň jedno číslo reálné.

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? To se dozvíte dále v seriálu :)


 

Stáhnout

Staženo 56x (32.41 kB)
Aplikace je včetně zdrojových kódů v jazyce c

 

  Aktivity (2)

Článek pro vás napsal David Čápka
Avatar
Autor pracuje jako softwarový architekt a pedagog na projektu ITnetwork.cz (a jeho zahraničních verzích). Velmi si váží svobody podnikání v naší zemi a věří, že když se člověk neštítí práce, tak dokáže úplně cokoli.
Unicorn College Autor se informační technologie naučil na Unicorn College - prestižní soukromé vysoké škole IT a ekonomie.

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


 



 

 

Komentáře

Avatar
coells
Redaktor
Avatar
coells:
int *p_pozice;
for (p_pozice = p_i; p_pozice < p_i + 100; p_pozice++)
        *p_pozice = 0;

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

Tvrdíš, že "První způsob pomocí pointerové aritmetiky je rychlejší, jelikož Céčko jen přičítá k adrese bajty."

Můžeš to něčím podložit? Na základě disassembly je druhý způsob rychlejší v debug módu a stejně rychlý v release - oba kódy vygenerují 4 instrukce, ale druhý způsob umožní použití MMX instrukcí.

První způsob zápisu je navíc častým omylem začátečníků, kteří se snaží o premature optimalizace. Kratší a jednodušší zápis je v C vždy ten správný bez ohledu na počet instrukcí.

 
Odpovědět 19.10.2014 20:59
Avatar
David Čápka
Tým ITnetwork
Avatar
Odpovídá na coells
David Čápka:

Pokud je to stejně rychlé, tak tam udělal překladač optimalizaci, to je dost možné. Otázka je jak moc s tímhle počítat, uvádím oba způsoby, přijde mi to ok.

Odpovědět 19.10.2014 21:03
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Avatar
Odpovědět 10.11.2014 14:34
Aj tisícmíľová cesta musí začať jednoduchým krokom.
Avatar
Lukáš Hypša:

Mám pár dotazů:

p_i = (int *) malloc(sizeof(int) * 100);

1,Výrazem "(int *)" říkám metodě malloc() aby v uvolněné paměti vytvořila inty?
2,p_i je pointer na začátek uvolněného bloku paměti nebo na celý blok?
3,Pamatuje si pointer velikost uvolněného bloku?
4,Pokud ne, jak ví metoda free() kolik paměti má uvolnit?

Odpovědět 15. května 21:35
I když se programování učím jenom z interetu, velmi mě baví a doufám, že se tím jednou budu i živit.
Avatar
Jan Vargovský
Redaktor
Avatar
Odpovídá na Lukáš Hypša
Jan Vargovský:
  1. Ne
  2. Na začátek
  3. Ne
  4. Součást toho bloku je i informace, jak velký ten blok je.
 
Odpovědět 15. května 21:43
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 5 zpráv z 5.