[Přednáška 2] [Obsah] [Přednáška 4]

Přednáška 3


Témata


Statická pole

Statické pole je homogenní datová struktura skládající se z určitého počtu (stejných) položek, např. čísel typu int. Velikost pole (počet položek) musí být známa při překladu a není možné ji měnit (proto statické pole). Pole může být jedno či vícerozměrné. Při deklaraci jakéhokoliv statického pole zadáváme do hranatých závorek počet prvků pole.

Jednorozměrná pole

Na následujícím řádku je příklad deklarace jednorozměrného pole prvků typu int, které se jmenuje A. Pole má deset prvků.
int A[10];
Konstanta může být buď zapsaná přímo (viz. číslo 10) nebo symbolická, definovaná pomocí direktivy #define, není dovoleno použít typovanou konstantu s přidělenou pamětí definovanou pomocí const:
#define MAXPOCET 10
   .
   .
   .
int A[MAXPOCET];
Po této deklaraci překladač vyhradí souvislou paměť pro deset čísel typu int, obr. 1.

Grafické znázornění pole
Obrázek 1: Grafické znázornění jednorozměrného pole

Přístup k prvkům pole se provádí pomocí indexů. Index se zapisuje do hranatých závorek, např.:
A[3] = 0; y = A[i]*3;
V jazyce C má index prvního prvku hodnotu nula, index posledního prvku je n-1, kde n je počet prvků pole. Důvodem je požadavek na rychlý výpočet mapovací funkce pole, tj. adresu indexovaného prvku. Tento způsob indexace není možno měnit! Jazyk C neprovádí žádné kontroly správnosti mezí indexů (ani při překladu, ani ve výsledném kódu), důvodem je opět snaha o co nejrychlejší výpočet adresy příslušného prvku pole. Všechny kontroly jsou ponechány v plné míře na programátorovi. Pokud se stane, že index má hodnotu mimo deklarovaný rozsah, pracuje se s daty uložené mimo pole a může se stát, že si programátor nevědomky přepíše hodnoty jiných proměnných.

Prvky pole lze inicializovat již při deklaraci pomocí tzv. konstruktoru pole:

int A[3] = { 25, 30, -5 };

Úloha:

Napište program, který uloží do statického pole násobky čísla 5 a vytiskne je. Vstupem programu (z klávesnice) je horní limit násobků: zadáme-li např. číslo 12, programu uloží násobky 0 x 5 až 12 x 5. Statické pole deklarujte o velikosti např. 50 prvků. Pokud zadaná mez překročí staticky deklarovanou velikost pole, program se ukončí s chybovým hlášením.

Řešení:

Dev C++:nasob.dev, nasob.c
CodeBlocks:nasob.cbp, nasob.c

Vícerozměrná pole

Při deklaraci vícerozměrného pole se počet prvků každé dimenze píše do samostatných hranatých závorek bez uvedení speciálního oddělovače! Deklarace dvourozměrného pole (matice 20 x 20) o 400 prvcích typu float, které se jmenuje mat je na následujícím řádku:
float mat[20][20];
Přístup k prvkům pole je opět pomocí indexů, index každé dimenze je v samostatných závorkách. O mezích indexů platí stejná pravidla jako v případě jednorozměrného pole, tedy výše deklarovanou matici mohu indexovat mat[0][0]mat[19][19].
Dvourozměrné statické pole je v jazyce C uloženo po řádcích. Uložení ukážeme na jednoduchém poli o dimenzi 2 x 2. Je dána následující deklarace čtvercového statického pole prvků typu int:
  int pole[2][2]= {{1,2},{3,4}};
Můžeme si jej představit jako matici (malá čísla 0,1 značí indexy), obr. 2:

Dvourozměrné pole
Obrázek 2: Dvourozměrné pole jako matice

Předpokládejme, že sizeof(int)=4. Uložení statického pole v paměti je znázorněno na obr. 3 (malá čísla značí hypotetické adresy v paměti):

Uložení statického dvourozměrného pole
Obrázek 3: Uložení statického dvourozměrného pole v paměti

V předmětu Úvod do programování jsme řešili úlohu součinu dvou matic. Zopakujte si a prostudujte příslušný algoritmus a jeho implementaci:

Řešení:

Dev C++:soucinmat.dev, soucinmat.c
CodeBlocks:soucinmat.cbp, soucinmat.c

Typ ukazatel, proměnná typu ukazatel

Obsahem „obyčejné“ proměnné je hodnota, tj. paměťová buňka (resp. buňky), na kterou je proměnná mapována, obsahuje tuto hodnotu. Proměnná typu ukazatel (pointer) neobsahuje přímo hodnotu, ale adresu paměťového místa, kde se hodnota nachází. Deklarace proměnných typu ukazatel se zapisuje pomocí znaku „*“. Na následujícím řádku je příklad deklarace proměnné p typu ukazatel na int:
int *p;
Tedy, proměnná p obsahuje adresu paměťového místa, které obsahuje hodnotu typu int. Takto deklarovaná proměnná však neobsahuje ještě konkrétní adresu, není totiž inicializovaná, jejím obsahem je nějaká náhodná adresa. Inicializovat ji můžeme např. přiřazením adresy jiné proměnné:
int x;
int *p;

p = &x;
Z výkladu o funkci scanf víme, že operátor „&“ vrací adresu objektu, tedy hodnotou výrazu &x je adresa, na které je umístěna proměnná x. Tuto adresu přiřadíme do proměnné p. Proměnná p nyní ukazuje na proměnnou x (obr. 4).

Schématické znázornění proměnné typu ukazatel

Obrázek 4: Proměnná typu ukazatel

Přístup k hodnotě (k paměťové buňce), na kterou proměnná typu ukazatel ukazuje, se provádí pomocí operátoru dereference, což je hvězdička „*“:

*p = 3;
Do paměťového místa, kam ukazuje proměnná, jsme nyní uložili hodnotu 3. Pokud uvažujeme předcházejí přiřazení p = &x;, změnili jsme hodnotu proměnné x.
(Poznámka: pokud bychom zapomněli do kódu napsat hvězdičku, tj. přiřazení by mělo podobu p = 3;, změnili bychom hodnotu samotného ukazatele; proměnná p by ukazovala do paměti na buňku s adresou 3.)

Shrneme si tedy ještě jednou situaci na obr. 4. Máme dvě proměnné x a p: proměnná x je typu int a v paměti je uložena na adrese 3426, druhá proměnná p je typu ukazatel na int a v paměti je uložena na adrese 2100. Proměnná p obsahuje adresu proměnné x, tedy hodnotu (číslo) 3426 (v paměťové buňce s adresou 2100 je hodnota 3426). Proměnná x obsahuje celé číslo 3 (v paměťové buňce s adresou 3426 je hodnota 3).


Dynamická alokace paměti

Alokace paměti

Nevýhodou statických polí je, že jejich velikost nejde měnit za běhu programu. Velmi často při tvorbě programu nejsme schopni odhadnout velikost zpracovávaných dat. Nadeklarujeme-li pole příliš velké, je často nevyužito a zbytečně zabírá paměť počítače. Proto existuje možnost, aby program nárokoval požadavky na paměť (alokoval paměť, třeba právě pro pole) za běhu podle aktuální potřeby (její velikost není známa při překladu). Tento mechanismus označujeme jako dynamickou alokaci paměti a může být využit nejen pro alokaci (nárokování) paměti pro pole (dynamické pole), ale i pro jiná data (struktury). V jazyce C slouží k dynamické alokaci paměti funkce malloc (memory allocation) z knihovny stdlib.h nebo alloc.h. Hlavička funkce má podobu:
(void *)malloc(size_t n)
Parametrem funkce je počet bytů paměti n, které program požaduje (typ size_t je celočíselný typ, zpravidla definován jako unsigned int). Funkce vrací ukazatel na počátek přiděleného bloku paměti, tj. adresu počátku přiděleného bloku, pokud má operační systém požadovanou velikost paměti k dispozici. Tuto adresu ukládáme právě do proměnné typu ukazatel. Ukazatel typu (void *), který funkce malloc vrací, představuje obecný ukazatel, který není vázán na konkrétní datový typ. Protože překladač kontroluje kompatibilitu typů ukazatelů při přiřazování, je nutné při volání funkce výsledek přetypovat. (viz příklad).

V souvislosti s ukazateli je nutná zmínka o konstantě NULL, která je definována jako symbolická (většinou má hodnotu 0). Znamená hodnotu „ukazatele nikam“ – je obdobou hodnoty nill v Pascalu. V případě, že operační systém nemá k dispozici dostatek volné paměti, vrací funkce malloc hodnotu NULL. Správně napsaný program by měl otestovat výsledek volání funkce malloc:

Příklad:

 
int *p;
p = (int *)malloc(512); // alokuji 512 bytu pameti
if (p == NULL)
{
  printf("Nedostatek pameti");
  return; 
}

S využítím přiřazovacího výrazu v podmínce můžeme napsat kód efektivněji:
 
int *p;
if ((p = (int *)malloc(512))== NULL)
{
  printf("Nedostatek pameti");
  return; 
}
V praxi je běžné, že potřebujeme alokovat paměť pro dynamické pole o n prvcích určitého typu. K výpočtu potřebné velikosti paměti v bytech využijeme operátor sizeof s parametrem daného typu. Konkrétně, dynamické pole o n prvcích typu int alokujeme tímto způsobem:
int *p;
p = (int *)malloc(sizeof(int)*n);
Čtenář se může ještě setkat s jimými funkcemi pro alokaci paměti - calloc a realloc.
Funkce void *calloc(size_t n, size_t size) alokuje paměť pro n položek, každou o velikosti size bytů. Odpovídá tedy volání funkce malloc(size*n), navíc celý přidělený paměťový blok nuluje.
Funkce void *realloc(void *block, size_t size) provádí realokaci paměti. Parametrem je ukazatel block na již dříve alokovanou paměť funkcemi malloc, calloc, realloc, size je nová požadovaná velikost paměti (logicky zpravidla větší než dříve alokovaná). Funkce vrátí ukazatel na nově alokovaný blok paměti o velikosti size, obsah původního bloku zkopíruje na počátek nově alokovaného bloku. Pokud má parametr block hodnotu NULL, funkce se chová jako malloc. Není-li k dispozici dostatek pamětí, vrací NULL.

Před ukončením činnosti (nebo v okamžiku, kdy ji již nepotřebuje) by měl program alokovanou paměti uvolnit (vrátit k dispozici operačnímu systému). K uvolnění paměti slouží funkce free z knihovny stdlib.h:

void free(void *block);
Parametrem je ukazatel na blok paměti, který byl dříve alokován pomocí funkce malloc (resp. realloc či calloc).

Hodnotu ukazatele je možné vytisknout pomocí specifikátoru %p: printf("%p",p) vytiskne šestnáctkově adresu, která je uložena v proměnné p.

Přístup k prvkům dynamicky alokovaného pole je stejný jako u pole statického, tedy pomocí indexů. K prvkům lze přistupovat i pomocí ukazatelové aritmetiky, kterou uvedeme později.

Nyní uvedeme jednoduchý příklad dynamické alokace pole pro uložení čísel a výpisu tohoto pole v opačném pořadí.

Příklad:

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

int main(int argc, char **argv)
{
  int *p;
  int n; // velikost pole
  int i;

  printf("Zadejte pocet zpracovavanych cisel: ");
  scanf("%d",&n);

  if ((p=(int*)malloc(sizeof(int)*n))==NULL)
  {
    printf("Neni dostatek dynamicke pameti.");
    return 1;
  }
  printf("Zadejte %d celych cisel odelenych mezerou:\n",n);

  // nacteni prvku pole
  for(i=0;i<n;i++) scanf("%d",&p[i]);
  putchar('\n');

  // vypis v opacnem poradi
  for(i=n-1;i>=0;i--) printf("%d ",p[i]);
  putchar('\n');
  free(p);
  return 0;
}
Kód je k dispozici i k přímému stažení:
Dev C++:dyn_pole1.dev, dyn_pole1.c
CodeBlocks:dyn_pole1.cbp, dyn_pole1.c

Ukazatelová aritmetika

S hodnotami ukazatelů je možné v jazyce C provádět aritmetické operace. Je možné přičítat/odečítat celočíselnou hodnotu, která představuje posuv po buňkách paměti (prvcích pole). Dále je možné hodnoty ukazatelů porovnávat a odečítat.

Mějme např. následující deklarace a dynamicky alokované pole o velikosti 10 prvků typu int :

  int *pole;
  int *uk;
  
  pole = (int *)malloc(sizeof(int)*10);
  uk = pole+5;
Ukazatel pole odkazuje na počátek dynamicky alokované paměti. Hodnotou výrazu pole + 2 je ukazatel na třetí prvek pole, tj. prvek s indexem 2 (obr. 5). Neznamená to však, že ukazatel pole + 2 ukazuje na buňku paměti, která má adresu vyšší o hodnotu dva. Při ukazatelové aritmetice se bere v úvahu velikost prvků, na které ukazatel ukazuje. Předpokládejme, že sizeof(int)=4, sizeof(char)=1 a že paměť je organizována (adresována) po slabikách - bytech (jak to ostatně dodnes počítače PC mají). Ukazatel pole+2 tedy ukazuje na adresu zvětšenou o 8, tedy na adresu ((char*)pole)+2*sizeof(int).

Ukazatelová aritmetika
Obrázek 5: Ukazatelová aritmetika

Do ukazatele uk jsme přiřadili hodnotu ukazatele pole+5, uk tedy ukazuje na šestý prvek pole a ukazatel uk+2 ukazuje na osmý prvek pole. Pokud bychom do kódu následně zapsali porovnávací výraz uk == pole+5, jeho hodnota by byla nenulová (pravda).
Přičtení záporné hodnoty k ukazateli představuje analogicky posuv „vpřed“, např. ukazatel pole-1 ukazuje na oblast paměti, která k poli již nepatří. Protože jazyk C nekontroluje meze polí, můžeme tento výraz v programu napsat (pokud budeme manipulovat s daty na této adrese, přepíšeme zpravidla hodnoty jiných proměnných).

Přístup k datům pomocí ukazatelů, statická versus dynamická pole

Z předešlé kapitoly víme, že přístup k hodnotě, na kterou ukazatel ukazuje, se provádí pomocí hvězdičky. Prvkům pole, které jsou označeny ukazateli na obr. 5, přiřadíme hodnoty tímto kódem:
  *pole = 2;
  *(pole+2) = 10;
  *uk= -4; // zde je možné napsat též *(uk+0) = -4;
  *(uk+2) = 5;
Zde je důležité poznamenat, že přístup k prvkům statického a dynamického pole je naprosto stejný. Následující dva zápisy jsou v jazyce C zcela ekvivalentní, je jedno, který z nich v programu použijeme:
   *(pole+2) = 10;
   pole[2] = 10;
Pokud máme deklarované statické pole, např. int A[30], A je konstatní ukazatel na počátek pole, lze tedy psát *(A+20)=10 a tento zápis je ekvivalentní zápisu A[20]=10. Do samotné proměnné A nelze přiřadit žádnou hodnotu, neboť jde o konstatní ukazatel inicializovaný při překladu (překladač hlásí chybu).

Ukazatelová aritmetika se efektivně využívá ve funkci scanf, kde se jako skutečné parametry uvádějí adresy paměti, na které se má uložit zkonvertovaná hodnota. Místo scanf("%d",&a[i]); je lépe psát scanf("%d",a+i);.

Využití operace rozdílu ukazatelů

Ukazatele je možné také odečítat, smysluplné je provádět rozdíl s ukazateli, kteří ukazují na stejný typ. Sčítat je nemá smysl, výsledkem by byla nesmyslná adresa. Velikost rozdílu ukazatelů je počet položek, na které ukazatelé ukazují. Předpokládejme opět, že sizeof(int)=4. Mějme následující fragment kódu:
  int *p = (int*)malloc(sizeof(int)*10);
  int *k;
  k = p + 5;
Hodnota rozdílu (k-p) je 5, tedy ((char*)k-(char*)p)/sizeof(int).

Na závěr ještě ukážeme, jak provést výpis prvků pole pouze s využitím ukazatelů, ukazatelové aritmetiky a porovnání ukazatelů. Výpis pozice hledaného prvku je proveden s využitím rozdílu ukazatelů:

  int A[5] = {1,2,3,4,5};
  int x;
  int *p,
  int *za_konec;

  za_konec = A+5;
  for(p=A;p<za_konec;p++) printf("%d ",*p);

  printf("Zadej hledane cislo: ");
  scanf("%d",&x);

  for(p=A;p<za_konec && *p != x;p++) ;  //strednik ma vyznam prazdneho prikazu
  if (p==za_konec) 
    printf("Cislo nenalezeno");
  else
    printf("Cislo je na pozici %d\n",p-A+1);
 
Kód je k dispozici ke stažení:
Dev C++: priklad_ukaz.dev, priklad_ukaz.c
CodeBlocks: priklad_ukaz.cbp, priklad_ukaz.c

[Přednáška 2] [Obsah] [Přednáška 4]