Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 3 - Aritmetika ukazatelů v jazyce C

V minulé lekci, Dynamická alokace paměti v jazyce C, jsme se naučili dynamickou alokaci paměti.

Dnešní C tutoriá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ředně 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 typu prvku, na který pointer ukazuje. V poli se tedy posouváme dopředu nebo dozadu (pokud odčítáme) o n prvků.

Kód pro práci s pátým prvkem našeho dynamického pole 100 intů by vypadal následovně:

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

c_aritmetika_pointeru
Prvek, na který ukazuje p_paty je v poli na indexu 4.

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ů <, >, ==, <=, >=, and !=. 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:

c_aritmetika_pointeru
p_paty je v paměti až za p_i

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 (Ačkoli jsme od malloc() 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 typu int 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 typu int). 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 velikost ukazatele samotného, ne hodnoty, na kterou ukazuje. 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 uložení adresy do paměti potřebujeme vždy stejně velký typ. 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.

Možná namítáte, že průměr bychom mohli vypočítat i úplně bez ukládání známek. Kdyby 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:

c_prumer
Zadej počet známek: 5
Zadej známku: 1
Zadej známku: 2
Zadej známku: 3
Zadej známku: 3
Zadej známku: 5
Průměr tvých známek je: 2.800000.

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í lekci 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 kurzu :)

V příští lekci, Dynamické textové řetězce a struktury v jazyce C, se naučíme dynamicky alokovat textové řetězce požadované délky.


 

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 100x (32.41 kB)
Aplikace je včetně zdrojových kódů v jazyce C

 

Předchozí článek
Dynamická alokace paměti v jazyce C
Všechny články v sekci
Dynamická práce s pamětí v jazyce C
Přeskočit článek
(nedoporučujeme)
Dynamické textové řetězce a struktury v jazyce C
Článek pro vás napsal David Hartinger
Avatar
Uživatelské hodnocení:
24 hlasů
David je zakladatelem ITnetwork a programování se profesionálně věnuje 15 let. Má rád Nirvanu, nemovitosti a svobodu podnikání.
Unicorn university David se informační technologie naučil na Unicorn University - prestižní soukromé vysoké škole IT a ekonomie.
Aktivity