Pole a ukazatele

Datové pole a řetězce

O datových polích jsem se již stručně zmínil v souvislosti s řetězci. K řetězcům snad není nutné říkat nic jiného, než že jsou to vlastně pole typu char zakončené nulovým znakem '\0'.

Datové pole je nějaké spojité místo vyhrazené kdesi v paměti pro několik objektů stejného datového typu. K tomu, abyste mohli k těmto objektům přistupovat, slouží ukazatel na onen datový typ.

Pole může mít různou velikost. Kde začíná je dáno právě ukazatelem, jeho konec si však musíte ohlídat. U řetězců je konec řetězce dán znakem '\0', což není nic jiného než nulový bajt. Je tak ukončen řetězec, nikoliv pole. Konec pole nijak označen není, proto se často stává ta chyba, že se program pokouší číst mimo rozsah pole, což vede ke zhroucení programu.

Rozdíl mezi polem typu char a mezi řetězcem je pouze v tom, že řetezec má na svém konci uložen nulový znak. Vlastně je to jen taková dohoda, na kterou spoléhají všechny funkce pracující s řetězci.

Pole se deklaruje následovným způsobem:

datovy_typ nazev [rozsah];

Například:

char znaky[4]; /* pole pro 4 znaky */
int cisla[1];  /* pole pro jedno cislo */

Proměnné znaky a cisla jsou proměnné typu array, které se chovají jako konstantní ukazatele, které ukazují na první prvek z pole. Jednotlivé položky pole se adresují od nuly pomocí hranatých závorek. Například pole cisla obsahuje jen jedno místo pro číslo typu int a s tím lze pracovat následovně (připomínám, že v jazyku C se indexuje od nuly, nikoliv od jedničky, narozdíl od Pascalu):

int x;
cisla[0] = 5;
x = cisla[0];

Chybné by bylo přiřazení cisla=5;, protože tím byste přiřazovali číslo 5 do proměnné cisla, čili byste se pokoušeli změnit „konstantní ukazatel“, který obsahuje adresu začátku pole.

Konstantní ukazatel na pole obsahuje adresu paměti, kde začíná pole. Tuto hodnotu změnit nelze, můžete měnit jen hodnoty v poli.

Číslo v hranatých závorkách při definici pole určuje jeho velikost. V jazyce C to musí být skutečné číslo, nelze použít proměnnou, ani konstantu (číslo musí překladač znát už v době překladu, aby pro pole dokázal vyhradit dostatek paměti).
V jazyce C++ lze použít i konstantu (její hodnota je překladači známa, protože se nemění).

Číslo v hranatých závorkách za ukazatelem při použití v kódu se nazývá index pole. Indexem pole se odkazuje na n-tou položku pole.

Používání indexu pole osvětlím na následujícím příkladu. Definuji pole znaky.

char znaky[4];

V tabulce je ukázáno, jak se uloží pole znaky do paměti.

ukázka uložení prázdného čtyřznakového pole v paměti
skutečná adresa 1534 1535 1536 1537 1538 1539
pozice (index) ... 0 1 2 3 ...
bity ??? ???????? ???????? ???????? ???????? ???

Momentálně pole znaky obsahuje náhodné byty. Ukazatel znaky ukazuje na adresu 1535 (to číslo je samozřejmě smyšlené). Pokud chcete vložit do pole znaky a vytvořit řetězec, nesmíte zapomenout na zarážku (nulový byte).

znaky[0] = 'j';
znaky[1] = 'e';
znaky[2] = '\0';

V tabulce můžete vidět, jak se pole přiřazením hodnot vyplnilo.

ukázka uložení dvouznakového řetězce v paměti
skutečná adresa 1534 1535 1536 1537 1538 1539
pozice (index) ... 0 1 2 3 ...
bity ??? 01101010 01100101 00000000 ???????? ???

Všiměte si, že poslední bajt pole je nevyužitý (znak[3] na adrese 1538). Taky si všiměte, že pokud byste se pokusili přiřadit hodnotu do znak[4], odstanete se na adresu 1539, která už zasahuje mimo rozsah pole. To může vést k chybě „neoprávněný přístup do paměti“ a následnému odstřelení programu operačním systémem. Ale taky nemusí. Můžete si přepsat nějakou jinou proměnnou, která v této části paměti sídlí a tím vytvořit nějakou jinou, nepředvídatelnou, těžko odhalitelnou chybu.

Operační systém nedovolí vašemu programu sahat do paměti, která mu nebyla vyhrazena. Je to z bezepčnostních důvodů, aby jeden program nemohl škodit druhému.

Pokud pole během jeho definice rovnou inicializujete, nemusíte uvádět jeho délku. Délka pole bude taková, kolik prvků předáte při inicializaci.

int cisla[] = { 5, -1, 3, 8};                /* pole pro 4 cisla */
char pole1[] = {'a', 'h', 'o', 'j', '\0'};   /* pole pro 5 znaku */
char pole2[] = "ahoj";                       /* pole pro 5 znaku */

Inicializace pole1 a pole2 je ekvivalentní. Všiměte si, že v druhém případě je přidán nulový znak na konec automaticky. Nyní můžete přiřadit: pole2[4] = 'x';.

Tím se však připravíte o zarážku (nulový znak) a již nemůžete s polem pracovat jako s řetězcem (například jej předat funkci printf(), protože by po vytištění ahojx funkce printf() tiskla další byty z paměti, dokud by nenarazila na '\0' (nebo ji operační systém nesestřelil).

Podívejte se ještě na tento příklad:

char pole1[] = "ahoj";
char *pole2 = "zdar";

Proměnné pole1 i pole2 jsou ukazatele na typ char. Rozdíl v těchto definicích je ale v tom, že řetězec zdar je řetězcový literál, který nemůžete měnit, zatímco ahoj se uloží do pole 5 znaků, jehož hodnoty můžete libovolně měnit.
Na definici s pole1[] prostě pohlížejte jako na char pole1[5] = {'a', 'h', 'o', 'j', '\0'}; a nebudete si to plést.

Další rozdíl je v tom, že proměnná pole1 je konstantní (já vím, konstantní proměnná zní divně :D), takže její hodnotu nemůžete měnit, ale hodnotu proměnné pole2 ano.

char pole1[] = "ahoj";
char *pole2 = "zdar";
char pole1[0] = 'A'; // ok, měním hodnotu v poli na které pole1 ukazuje
char pole2[0] = 'Z'; // ale fuj
pole1 = NULL; //syntax error
pole2 = NULL; // ok, no problemo

Řetězcový literál "zdar" nelze měnit v žádném případě. Pokud byste se pokusili o něco jako pole2[0] = 'Z';, program se zhroutí. Na druhou stranu, pro pole1 se alokuje paměť v místě, kde ji lze měnit (a do tohoto místa se "ahoj" nakopíruje).

Vícerozměrná pole

Když se pdíváte jakým způsobem je v paměti pole uloženo, pochopíte, že v paměti můžou být jen jednorozměrná pole (prvky pole můžou být řazeny v paměti počítače pouze za sebou). Nic vám však nebrání vytvořit pole, jehož prvky budou jiná pole a tak vytvářet pole libovolných dimenzí (rozměrů). Podívejte se na definici dvourozměrného pole a jeho uložení v paměti.

char pole[4][2] = { {0,0}, {1,1}, {3,4}, {0xff,0xff} };

Jedná se o čtyřprvkové pole, jehož prvky jsou dvouprvková pole typu char.

Uložení pole 4x2 v paměti
skutečná adresa 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543
index ... [0][0] [0][1] [1][0] [1][1] [2][0] [2][1] [3][0] [3][1] ...
hodnota ??? 0 0 1 1 3 4 255 255 ???

Všimněte si, že při putování pamětí se mění vždy nejdříve poslední index pole a pak ty předešlé.

Můžete vytvářet pole libovolné dimenze a velikosti, ale dejte pozor na to, kolik takové pole zabírá místa v paměti. Např. pole long double nazev[50][7][2][8] je velké 10*50*7*2*8 = 56000 bytů (long double je veliký 10 bytů)!

Vícerozměrná pole se využívají poměrně často. Například kalendar[12][31], sachovnice[8][8] atp. Práce s takovými poli je daleko snazší a přehlednější než s mnoha jednorozměrnými poli, přestože např. v kalendáři nevyužijete všechny prvky pole (ne každý měsíc má 31 dní).

Ukazatele a pole

Když si zopakujete kapitolu o ukazatelích, pak spolu s předcházejícími odstavci o polích by vám mělo být již vše jasné. Přesto si můžeme ještě ukázat zajímavá „kouzla“, která lze s ukazateli provádět. Začnu s řetězci. Víte například to, jaký je rozdíl v následujících definicích?

char *retezec1 = "Ahoj";
char retezec2[] = "Ahoj";

Rozdíl je v tom, že retezec2 je konstantní ukazatel (pole), kdežto retezec1 je ukazatel inicializovaný hodnotou adresy řetězcového literálu "Ahoj".

Při obou definicích se vytvořil řetězec "Ahoj", což je pole znaků (se zarážkou '\0' na konci). První je však uložen v části paměti, která je jen pro čtení, druhý je uložen v poli, se kterým můžete manipulovat.

V následujícím příkladě se podívejte, jakým způsobem lze adresovat jednorozměrné pole. Když si představíte, jakým způsobem je pole v paměti uloženo a uvědomíte si, že ukazatel obsahuje číselnou hodnotu která je adresou, neměl by být pro vás problém příklad pochopit.

/*------------------------------------------------*/
/* c09/pole1.c                                    */

#include <stdio.h>

int main(void)
{

    float pole[] = { 5.0, 6.0, 7.0, 8.0, 9.0 };
    float *ukazatel;

    ukazatel = pole;

    /* prvni cast */
    printf("%.1f ", pole[1]);
    printf("%.1f ", pole[1] + 10.0);
    printf("%.1f\n", (pole + 1)[2]);

    /* druha cast */
    printf("%.1f ", *ukazatel);
    printf("%.1f ", *ukazatel + 10);
    printf("%.1f ", *(ukazatel + 1));
    printf("%.1f\n", *(ukazatel + 1) + 10);

    return 0;
}

/*------------------------------------------------*/

Výstup z programu:

6.0 16.0 8.0
5.0 15.0 6.0 16.0

Všimněte si, jakou roli hrají závorky! Také si všimněte, jak chytře pracuje jazyk C s aritmetikou ukazatelů. Výraz (ukazatel + 1) ukazuje na další prvek pole, přestože typ float je dlouhý hned 4 bajty a ne jeden. To je jeden z důvodů, proč se při deklaraci ukazatele určuje, jakého je typu. Nemusíte si tak lámat hlavu, o kolik bytů byste měli zvýšit jeho hodnotu, aby se ukazoval na další prvek příslušného datového typu. Překladač pak zvýší hodnotu ukazatele o správný počet bytů tak, aby ukazoval na další prvek v poli.

Následující výrazy jsou ekvivalentní:

&pole[n] je totéž co (pole + n) (tj adresa n-tého prvku v paměti)
a
pole[n] je totéž co *(pole + n) (tj hodnota n-tého prvku v paměti)

Abych vás ještě trochu potrápil, ukáži vám příklad s dalším možným zápisem adresy pole:

/*------------------------------------------------*/
/* c09/pole2.c                                    */

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char** argv) {
     char array[5] = {'a', 'b', 'c', 'd', 'e'};

     // array[2] je totez jako 2[array]
     printf("array[2] = '%c'\n", 2[array]);

    return EXIT_SUCCESS;
}

/*------------------------------------------------*/

Tento způsob indexace pole není ukázkou dobrého stylu psaní a měli byste ho používat jen k mučení učitele programování. Ukázka jen ukazuje to, že je jedno, jestli se adresa paměti vypočte jako array + 2, nebo (jako v příkladu) 2 + array.

array[2] = 'c'

Ukazatele na ukazatele

V jazyku C můžete vytvořit ukazatel na libovolný datový typ (třeba i na vlastní strukturu – o těch později). Ukazatel tedy může ukazovat i na ukazatel. Například můžete vytvořit ukazatel, který může ukazovat na „ukazatel na typ char“:

char **ukazatel;

Můžete taktéž vytvořit pole ukazatelů (včetně vícerozměrných polí ukazatelů…).

char * ukazatel[10]; /* 10-prvkove pole ukazatelu na typ char */

Takto definované pole má velikost 10*sizeof(char *), tj. 10x velikost ukazatele, tj. např. 40 bajtů (pro 32-bitové adresování).

Příklad:

/*------------------------------------------------*/
/* c09/unau1.c                                    */
#include <stdio.h>

int main(void)
{
    int x;
    int *ukazatel_na_int;
    int **ukazatel_na_ukazatel;

    int pole[] = { 5, 7, 9, 11, 13 };

    /* inicializace promennych */
    x = 25;
    ukazatel_na_int = &x;
    ukazatel_na_ukazatel = &ukazatel_na_int;

    /* pristup k promenne x */
    printf("%2d = %2d = %2d\n", x, *ukazatel_na_int,
           *(*ukazatel_na_ukazatel));

    /* inicializace */
    ukazatel_na_int = pole;
    /*  ukazatel_na_ukazatel = &ukazatel_na_int; ... toto prirazeni
     *  je o nekolik radek vyse a hodnota ukazatel_na_ukazatel ukazuje
     *  stale na ukazatel_na_int */


    /* pristup k poli */
    printf("%2d = %2d = %2d\n", pole[0], *ukazatel_na_int,
           **ukazatel_na_ukazatel);
    printf("%2d = %2d = %2d\n", pole[1], *(ukazatel_na_int + 1),
           *((*ukazatel_na_ukazatel) + 1));
    return 0;
}

/*------------------------------------------------*/

Výstup z programu:

25 = 25 = 25
 5 =  5 =  5
 7 =  7 =  7

Podívejte se na vyhodnocení výrazu *(*ukazatel_na_ukazatel). Závorky v tomto výrazu nejsou podstatné a jsou zde jen pro ilustraci. První, co se provede, je vyhodnocení výrazu *ukazatel_na_ukazatel. Hvězdička je dereferencí adresy, jejíž výsledkem je hodnota proměnné, na kterou ukazuje proměnná ukazatel_na_ukazatel. V příkladě to znamená, že *ukazatel_na_ukazatel je hodnota proměnné ukazatel_na_int (to je adresa proměnné x).

Dejme tomu, že adresa x je 123456. Potom *(*ukazatel_na_ukazatel) je *(123456) a to je hodnota v paměti na adrese 123456, tedy hodnota proměnné x.

Teď už jen v rychlosti proberu výraz *((*ukazatel_na_ukazatel) + 1).
Dejme tomu, že pole začíná na adrese 12340. Potom ukazatel_na_int bude po druhé inicializaci v příkladu 12340, (*ukazatel_na_ukazatel) je tedy také 12340.
A teď pozor. Výraz (*ukazatel_na_ukazatel + 1) je (12340+1*n) a výraz *((*ukazatel_na_ukazatel) + 1) je *(12340+1*n).
Číslo n je počet bajtů, které zabírá typ int, protože jedničku přičítáme k ukazateli na int. Tak tomu se říká aritmetika ukazatelů. Výsledkem pak bude hodnota (kvůli hvězdičce) na adrese 12344 (pokud int zabírá 4 bajty). Uff.

Ovšem pozor. Ukazatele a pole nelze libovolně zaměňovat. Tak například **pole1 a pole5[][5] a pole6[][6] jsou tři různé typy. S vědomostmi které máte o aritmetice ukazatelů vás jistě napadne, jaké chyby by se mohli stát, kdybyste použití těchto proměnných libovolně zaměňovali. Mám na mysli hlavně aplikaci pole1 na některé z dvourozměrných polí. Správně by se mělo použít např.: int pole5[N][5]; int (*upole5)[5]; upole5 = pole5; atp.

Pokud jste se ve zdraví prokousali až sem, pak vám gratuluji. Váš mozek právě začal mutovat v „mozek programátora jazyka C“. Tento proces je, bohužel, nevratný.

Datový typ pro indexaci pole

Standard jazyka C definuje datový typ size_t pro určování velikostí oběktů (a indexaci polí). Máte zaručeno, že datový typ size_t je dost velký na to, abyste mohli indexovat „libovolně“ velké pole. (Pravděpodobně to bude ve skutečnosti něco jako unsigned int nebo unsigned long.)

Datový typ ptrdiff_t se zase používá pro výsledek rozdílu pointerů.

Oba datové typy jsou definovány v knihovně <stddef.h>. Mohou být definovány i v jiných knihovnách (například knihovnách, které <stddef.h> sami využíají), takže není vždy nutné <stddef.h> includovat.

/*------------------------------------------------*/
/* c09/typy.c                                     */

#include <stdio.h>
#include <stddef.h>

int main(void)
{

    char *text = "Hello world!";
    char *u1 = text, *u2;
    size_t i = 0;
    ptrdiff_t diff;

    while(text[i]) i++;
    u2 = text+i;
    diff = u1 - u2;

    /* vypíše délku textu */
#ifdef _MSC_BUILD
    printf("size_t i = %Iu, ptrdiff_t diff = %Ii\n", i, diff);
#else
    printf("size_t i = %zu, ptrdiff_t diff = %ti\n", i, diff);
#endif

    return 0;
}

/*------------------------------------------------*/
Visual Studio

Ve zdrojovém kódu jsou 2 řádky s funkcí printf(). První řádka se použije ve Visual Studiu, druhá všude jinde. To je díky podmíněnému překladu, který budu probírat později.
Důvodem je, jaké "%" sekvence používá printf() pro výpis těchto datových typů. Na druhé řádce je to podle standardu C99, na první řádce je to podle standardu Microsoftu.
Pro datový typ size_t používá printf() dle standardu C99 %zu, ale Microsoft %Iu.

Pokud používáte Dev-C++, musíte si nastavit makro -D__USE_MINGW_ANSI_STDIO=1, jinak vám sekvence %zu a podobné nebudou fungovat.

Výstup z programu:

size_t i = 12, ptrdiff_t diff = -12

Načtení řetězce pomocí funkce scanf()

V kapitole o Vstupu a výstupu jste se dozvěděli, že funkce scanf() používá pro řetězce typ s. A taky že maximální počet vstupních znků se dá omezit pomocí „width“. Z toho vychází následující příklad:

/*------------------------------------------------*/
/* c09/scanstring.c                                */
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main(void)
{
        char znaky[10];
        printf("Zadejte text: ");
        fflush(stdout);
        scanf("%9s", znaky);
        printf("Nacteno: '%s'\n", znaky);

    return 0;
}

/*------------------------------------------------*/

Bez fflush(stdout); by mohl zůstat text tištěný funkcí printf() v bufferu, protože není ukončen novým řádkem '\n'.

Možný výstup z programu:

Zadejte text: 1234567890
Nacteno: '123456789'

Načetlo se 9 znaků (jak jsem chtěl). Desátý znak se využije pro označení konce řetězce '\0'.

A ještě jedna ukázka:

Zadejte text: Ahoj světe!
Nacteno: 'Ahoj'

Funkce scanf() čte řetězec až k oddělovači (bílým znakům), takže načte jen Ahoj.

Existují i jiné funkce pro načítání vstupu od uživatele, šikovnější než scanf() (třeba v tom, jak se jim předává maximální délka řetězce), které načtou řetězec až ke konci řádku ('\n'). S těmi vás seznámím později.

Komentář Hlášení chyby
Created: 29.8.2003
Last updated: 20.8.2017
Tato stánka používá ke svému běhu cookies, díky kterým je možné monitorovat, co tu provádíte (ne že bych to bez cookies nezvládl). Také vás tu bude špehovat google analytics. Jestli si myslíte, že je to problém, vypněte si cookies ve vašem prohlížeči, nebo odejděte a už se nevracejte :-). Prohlížením tohoto webu souhlasíte s používáním cookies. Dozvědět se více..