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