na obsah klávesové zkratky na hlavní stránku na menu

Ukazatele

V jazyce C existují ukazatele. Jde o techniku, jak nepřímo přistupovat k datům. V tomto článku se seznámíme s ukazateli, přetypováním ukazatelů, ukazatelům na funkce a ukazatelům na ukazatele. Dále nás čeká adresová aritmetika a některé další detaily týkající se ukazatelů.

Motivace

Proč vůbec zavádět ukazatele, když doteď jsme se bez nich obešli. Odpověď je jednoduchá. Představme si, že bychom chtěli napsat funkci swap(), která přehodí hodnotu dvou proměnných. Pro jednoduchost uvažujme proměnné typu int. Napíšeme následující kód:

#include <stdio.h>
void swap (int a, int b)
{
  int c = a;
  a = b;
  b = c;
}

int main (void)
{
  int a = 3, b = 2;
  swap(a, b);
  printf("%d, %d\n", a, b);
  return 0;
}

Očekávaným výstupem by mělo být

2, 3

ale obdržíme

3, 2

Jak je to možné? Funkce swap() totiž pracuje s parametrem předávaným hodnotou, tzn., že se při volání funkce vytvoří lokální kopie všech parametrů. V našem případě se tedy vytvoří kopie parametru a a b, ty se mezi sebou přehodí a na konci funkce se zničí. Původní hodnoty proměnných zůstanou nezměněné. Abychom dosáhli svého, musíme se na data dostat nepřímo. Zde nacházejí uplatnění ukazatele.

Deklarace ukazatele

<typ> *<jméno>;

Obecná deklarace se od deklarace proměnné liší pouze použitím hvězdičky před jménem proměnné.

int a = 3;
int *ua = &a;

Zde jsme vytvořili proměnnou a s hodnotou tři a poté jsme vytvořili ukazatel na typ int, do kterého jsme uložili adresu proměnné a. Všechna data leží někde v paměti, která se dá představit jako obrovské pole. Každá buňka tohoto pole (bajt) má svoji adresu, což je pořadové číslo buňky. Ukazatel tak není nic jiného než proměnná uchovávající toto číslo. K získání adresy dat slouží operátor adresace &. Výhoda tohoto přístupu spočívá v tom, že se můžeme odkazovat na velké množství dat jen s pomocí konstantně velkého čísla a nemusíme tak data neustále kopírovat.

ua = 2; // chyba
*ua = 2;

Pokud chceme přistupovat k datům, na které ukazuje ukazatel, musíme použít operátor dereference *, jak je ukázáno na druhé řádce kódu výše. V první řádce je sémantická chyba, kde místo hodnoty proměnné a přepisujeme adresu uchovanou v ukazateli. Nyní již můžeme upravit náš kód pro přehazování hodnot proměnných:

#include <stdio.h>
void swap (int *a, int *b)
{
  int c = *a;
  *a = *b;
  *b = c;
}

int main (void)
{
  int a = 3, b = 2;
  swap(&a, &b);
  printf("%d, %d\n", a, b);
  return 0;
}

Tento kód již vrátí

2, 3

přesně, jak jsme chtěli. Všimněme si úpravy hlavičky funkce swap(). Ta nyní přejímá ukazatele na proměnné. V těle funkce používáme dereferenci, abychom se dostali k vlastním datům. Ve funkci main() používáme operátor adresace pro předání adres proměnných a a b. Tomuto postupu se říká předávání parametru ukazatelem. Jako dříve i tady dojde ke kopírování a následnému zničení lokálních parametrů. Jenže tentokrát ničíme pouze adresy, kdežto data už jsme v těle změnili.

Ukazatel bez doménového typu a neplatná adresa

Ukazatele mohou ukazovat na libovolná data. Existuje však speciální forma ukazatele, tzv. ukazatel bez doménového typu.

void *ukazatel;

Tento ukazatel může ukazovat na libovolná data a jakýkoliv ukazatel lze převést na ukazatel bez doménového typu. Takovýto ukazatel ale logicky nelze dereferencovat, neboť nevíme, na jaká data ukazuje a tudíž nevíme, jak máme odkazovaná data reprezentovat. Před přístupem k datům tak ukazatel musíme explicitně přetypovat na ukazatel s typem. Ukazatel bez doménového typu se hojně využívá v místech, která mají být dostatečně obecná a musí umět pracovat s různým typem dat.

V jazyce C je vyhrazen jeden ukazatel, který se považuje za neplatný. Je definován v různých hlavičkových souborech jako makro NULL. V jazyce C je toto makro standardně definováno takto:

#define NULL ((void*)0)

Jinými slovy neplatný ukazatel je na adrese nula. Proto můžeme testovat ukazatele na platnost v podmínkách jako každé jiné číslo. Ukazatel NULL se vyhodnotí jako nepravda.

Konstantní ukazatel a ukazatel na konstantu

K deklaraci ukazatele lze přimíchat klíčové slovo const. Podle toho, kam ho vložíme, vytvoříme konstantní ukazatel, nebo ukazatel na konstantu.

const int *u; // ukazatel na konstantu
int *const u; // konstantní ukazatel

První řádek deklaruje ukazatel na konstantu. Takový ukazatel můžeme měnit, odkazovaná data jsou ale konstantní a tedy neměnná. Při zápisu do dereferencovaného ukazatele obdržíme chybové hlášení o přístupu k datům pouze pro čtení. Druhá řádka deklaruje konstantní ukazatel. Takový ukazatel je pevně spojen s obsahem a jeho hodnotu nelze měnit. Lze ale měnit odkazovaná data.

Vztah pole a ukazatele

Pole a ukazatele mají v jazyce C souvislost. Pole jsou totiž v podstatě konstantní ukazatele na první prvek (index nula). Toto chování se liší pouze při použití operátoru sizeof(), který vrátí velikost pole a nikoliv velikost ukazatele, a při získávání adresy pole, kde má vrácený ukazatel jiný typ než ukazatel na první prvek (číselná hodnota je ale stejná). Na identifikátor pole se lze koukat jako na ukazatel a také s ním lze takto pracovat. Indexace je v jazyce C de facto použití adresové aritmetiky a ta byla také pro tento účel navržena (viz níže). Proto je možné napsat následující kód.

void pracuj_s_polem(int *pole)
{
  pole[1] = pole[2]; // pracuji s polem jako obvykle
  //...
}

int main()
{
  int pole[10] = {1, 2, 3, 4, 5, 6 ,7 ,8 ,9 ,0};
  pracuj_s_polem(pole); // předávám pole jako parametr
  //...
}

Adresová aritmetika a operátor sizeof()

Jelikož je ukazatel číslo, můžeme na něj aplikovat určité matematické operátory. Tyto operace byly navrženy pro efektivní implementaci indexace v poli. K ukazateli lze přičíst konstantu a lze spočítat rozdíl dvou ukazatelů.

int pole[3] = {1, 2, 3};
int a = pole[0]; // v a bude 1
a = *(pole + 1);  // v a bude 2

Člověk by očekával, že přičtení jedničky způsobí skok adresy o jeden bajt. Tak tomu ale není. Hodnota ukazatele poskočí o velikost jeho typu. Ukazatel bez doménového typu nemá žádný typ, tudíž je podle standardu šířka skoku nedefinovaná. Pro použití tohoto ukazatele v adresové aritmetice je nutné ho nejprve přetypovat na nějaký typový ukazatel. Překladač GCC obsahuje rozšíření, které umožní používat ukazatel bez doménového typu v adresové aritmetice tím, že ho implicitně přetypuje na ukazatel na char. To způspbí, že je možné skákat po bajtech.

Na třetí řádce výpisu tak kód pole + 1 způsobí posunutí ukazatele na druhý prvek pole. Následná dereference tedy vrátí hodnotu dvě. Přesně takto se interně převádí indexace polí v jazyce C – přičtení konstanty a následná dereference. Proto je také první prvek pole na indexu nula.

Ukazatele lze také odčítat. Musí jít o ukazatele stejného typu a měly by ukazovat na stejné pole. Výsledkem této operace je počet prvků pole mezi těmito ukazateli. U ukazatelů bez doménového typu platí to samé, co bylo uvedeno výše u přičítání konstanty. I zde je nutné ukazatel nejprve přetypovat, popř. lze využít rozšíření GCC, což ovšem není přenositelné.

K získání velikosti typu lze použít operátor sizeof(). Tento operátor přejímá název typu a vrací jeho veikost v bajtech. Vyhodnocení tohoto operátoru se provádí při překladu, protože až překladač ví, jak jsou jednotlivé typy veliké.

int velikost = sizeof(int);
int velikost2 = sizeof(int*);

Hodnoty velikost a velikost2 se budou lišit podle toho, kde kód přeložíme. Na 64 bitovém systému budou hodnoty nejpravděpodobněji čtyři a osm. Není proto dobrý nápad převádět ukazatel na číslo typu int, protože se na určitých architekturách nemusí ukazatel do čísla vejít.

Ukazatele na funkce

V jazyce C se lze odkazovat na funkce. Deklarace ukazatele na funkci vypadá následovně:

int (*funkce)(int a, int b); // je deklarace ukazatele
int *funkce(int a, int b); // je deklarace funkce

Zde jsme vytvořili ukazatel na funkci, která vrací hodnotu typu int a přejímá dva celočíselné parametry. Od deklarace funkce se liší pouze přidáním hvězdičky a závorek. Závorky jsou povinné, neboť jinak bychom deklarovali funkci, která vrací ukazatel na int. Jak ale získat adresu funkce? Na funkci nelze použít operátor adresace. Adresa funkce je přístupná přes jméno funkce bez závorek.

int funkce(void);
// ...
funkce (); // je volání funkce
funkce; // je ukazatel na funkci

Proto je při volání funkce bez parametrů důležité psát prázdné kulaté závorky. Poslední řádek výpisu ve skutečnosti nic nedělá, je to jako deklarovat proměnnou a a poté napsat a;. Takové volání se při překladu odstraní, protože je zbytečné (hodnota výrazu se zahodí). Ukazatel na funkci lze použít pro předání určité konkrétní implementace funkce do obecného algoritmu. Jako příklad poslouží funkce qsort() ze souboru stdlib.h.

void qsort(void *base, size_t n, size_t size,
int(*compar)(const void *, const void *));

Tato funkce vyžaduje tzv. komparátor, tedy funkci, která implementuje srovnání dvou prvků.

Vícenásobné ukazatele aneb ukazatele na ukazatele

Jelikož ukazatel je proměnná jako každá jiná, lze získat adresu ukazatele. Mluvíme pak o ukazateli na ukazatel. Jak takový vícenásobný ukazatel vypadá?

int a = 2;
int *uka = &a;
int **ukuka = &uka;

int b = *(*ukuka);

Ukazatel na ukazatel int je ukazatel typu int*. Proto se nám v deklaraci objeví dvě hvězdičky. S podobnou konstrukcí jsme se setkali u funkce main(), jejíž druhý parametr je typu char**. Je to tedy pole ukazatelů na char, jinými slovy tedy pole řetězců. Vícenásobné ukazatele se používají nejčastěji u vícerozměrných datových struktur, jako jsou matice nebo obecně pole polí. Z pohledu práce se nijak neliší od běžných ukazatelů, jen musíme provést vícenásobnou dereferenci, abychom se dostali k uloženým datům.