Skočiť na obsah


Programovanie v jazyku C a C#

Návody pre začiatočníkov a pokročilých



Fórum: Škôlka jazyka C
- - - - -
Fotografia

5. lekcia C (Pointery) základná škola


5 Pointery

Pointer predstavuje adresu v pamäti a až na tejto adrese je ukrývana príslušná hodnota, na ktorú sme boli doposiaľ zvyknutí. Táto odlišná interpretácia obsahu premenej nás stavia pred nutnosť ako nejakým vhodným spôsobom prekladaču povedať, že hodnota premennej je adresa a nie už cieľová hodnota. To sa prevedie pomocou operátoru *.
Poznámka:
  • Operátor * sa označuje ako dereferenčný operátor. V literatúre sa občas vyskytuje i pojem opačného významu - referenčný operátor, ktorý je predstavovaný operátorom &.
Pomocou operátoru * možeme jednak získat obsah na adrese, na ktorú ukazuje pointer (napr. i = *poi; ), ale je možná aj opačná akcia - zapísanie hodnoty na túto adresu (napr. *poi = 5;).

5.1 Základy práce s pointermi

Poznámka:
  • Pointer je vždy zviazaný s nejakým dátovym typom. Správne by sa namiesto termínu "pointer" malo vždy uvádzať "pointer na typ ..."
5.1.1 Definícia údajov typu pointer na typ

Poznámky:
  • Pre ukážku budeme definovať pointer na typ int. Definícia pointerov na iné dátove typy je analogická.
  • Všetky identifikátory pointerov budú ďalej začínat jednotne znakmi p_.
Definícia premennej typu pointer na typ int je nasledujúca:

int *p_i;

Je možné (a často sa to robí) uviesť definíciu premennej typu int a pointeru na typ int naraz, napr.:

int *p_i, i;

Z tejto možnosti vyplýva častá chyba pri definícii viac pointerov naraz:

int *p_i, p_j;

kde iba p_i je pointer na int a p_j je premenná typu int.


5.1.2 Práca s adresovými operátormi

Zatiaľ sme sa dozvedeli, ako získat hodnotu premennej, ktorej adresu (teda pointer na túto premennú) poznáme. K úplnej znalosti je teda potrebné sa naučit aj opačný spôsob, t. j. ako získat adresu premennej, ktorá uz existuje. Opät to nie je nič tažké, pretože adresa ľubovoľnej premennej sa dá získat pomocou referenčného operátora &, teda:

int i, *p_i = &i;

čo je definícia p_i a jeho súčasná inicializácia adresou premennej i alebo:

p_i = &i;

čo je priraďovací príkaz, ktorý v programe uskutoční priradenie adresy premennej i do premennej (pointeru) p_i.

Poznámka:
  • Premená p_i má samozrejme taktiež adresu, ktorá sa ale veľmi nevyužíva.
5.1.3 Priradenie hodnoty pointerom a pomocou pointerov

Pretože každá pointerova premenná je l-hodnota, je teda možné napísať:

*p_i = 1; // celkom v poriadku
*(p_i + 3) = 5; // podozrive, pokial p_i neukazuje na pole.
*(3 + k) = 6; // je to mozne, ale nespravne, pretoze zapisujeme na neznamu adresu v pamati

Operátor & je ale možné použit iba na premenne. Tie totiž majú vždy adresu, na rozdiel od konštant a výrazov, ktoré ju nemajú.

p_i = &i; /* spravne */
p_i = &(i + 3); /* chyba */
p_i = &15; /* chyba */


Poznámka:
  • U mikorprocesorov sa často používaju pointery pre priamy prístup do pamäte. Potom má zmysel priradiť pointeru absolútnu adresu, teda napr:
    typedef unsigned char BYTE;
    BYTE *p_mem;
    p_mem = (BYTE *) 0x80;
5.1.4 Použitie pointerov v priraďovacích príkazoch

Tu sa situácia trochu komplikuje, pretože je potrebné rozoznávať dva typy správnosti priradenia:
  • statické - priradenie je správne v dobe prekladu
  • dynamické - priradenie je správne v dobe prekladu aj pri behu programu
pravidlá:
  • ľavá strana by mala byť rovnakého typu ako pravá strana, tzn. nemiešať pointery na rôzne typy
  • priradzovať len cez inicializované alebo správne nastavené pointery
Príklady:

Pre všetky príklady platí definícia: int i, *p_i;
na adresu v p_i musí byť p_i inicializovaná.
  • Staticky správne:
    i = 3; /* do i dá hodnotu 3 */
    *p_i = 4; /* na adresu v p_i (kde je uložený int) dá hodnotu 4 */
    i = *p_i; /* do i dá obsah z adresy v p_i */
    *p_i = i; /* na adresu v p_i dá obsah i */
    p_i = &i; /* naplní p_i adresou i */
  • Staticky nesprávne:
    p_i = 3; /* namiesto adresy je do p_i daná bez
    pretypovania hodnota 3, teda (absolútna) adresa 3 */
    i = p_i; /* do i sa dá obsah p_i, teda adresa namiesto int hodnoty */
    i = &p_i; /* do i sa dá adresa p_i */
  • Dynamicky správne:
    p_i = &i; *p_i = 4; /* je to to isté ako: i = 4; */ pred priradením hodnoty
  • Dynamicky nesprávne:
    *p_i = 4; /* 4 je priradená na náhodnú adresu, ktorá
    je v p_i. Toto je najčastejšia chyba! */
Príklad:
Na nasledujúcich priradzovacích príkazoch budú ukázané niektoré možnosti práce s pointermi. Predpokladajme definíciu:
int i, *p_i1, *p_i2;
a ďalej predpokladajme, že premenná i leží na absolútnej adrese 10, p_i1 na adrese 20 a p_i2 na adrese 30.

Obrázok

Poznámky:
  • Operátor * má vyššiu prioritu než operátor +, takže príkaz:
    i = *p_i1 + 1; je v skutočnosti príkaz:
    i = (*p_i1) + 1;
  • Operátor ++ ma rovnakú prioritu ako operátor *, ale je použity ako postfix, takže príkaz:
    i = (*p_i1) ++; ma význam dvoch príkazov:
    i = *p_i1; p_i1++; teda do i sa dá obsah adresy, na ktorú ukazuje p_i1 a potom sa pointer p_i1 inkrementuje. Bude teda ukazovať na bezprostredne nasledujúci prvok (adresu) za i. Tento trik sa často používa pri operáciach s reťazcami.
Príklad:
Program číta dve celé čísla a zobrazí väčšie z nich.

#include <stdio.h>



main()

{

	int i, j, *p_i;



	scanf("%d %d", &i, &j);

	p_i = ( i > j) ? &i : &j;

	printf("Vacsie je %d \n", *p_i);

}



Poznámka:
  • Ak potrebujeme niekedy vytlačiť adresu na ktorú ukazuje pointer, alebo hodnotu pointeru, potom použijeme:
int i, *p_i = &i;
printf("Adresa i je %p, hodnota p_i je %p \n", &i, p_i);

Výpis adresy, na ktorú ukazuje pointer, použijeme najčastejšie pri ladení, keď si nie sme istí, či pointer ukazuje tam, kam má.

Pozor:
Znovu upozornujeme, že je treba dať si veľky pozor na tieto dve odlišnosti:

int i, *p_i = &i;

a

int i, *p_i;
p_i = &i;
  • V prípade vyššie sa jedná o definíciu p_i ako pointeru na typ int (preto tam musi byt * a súčasne o jeho inicializáciu na adresu skôr definovanej premennej i.
  • V pripade nižšie je to opät definícia p_i ako pointeru na typ int. Avšak v priradzovacom príkaze (už to nie je inicializácia) nesmie byť *, pretože p_i priraďujeme adresu premennej i - viď tiež príklady statickej správnosti a nesprávnosti v predchadzajúcom texte.
Vlastne obidva výrazy majú ten istý výsledok, len zápis je rozdielny.


5.1.5 Nulový pointer NULL

Tak ako v Pascale konštanta NIL, je aj v C podobná konštanta NULL. Je to symbolická konštanta definovaná v stdio.h ako napr.:
#define NULL 0 /* alebo ako #define NULL ((void *) 0) */
NULL je možné priradiť bez pretypovania všetkým typom pointerov (pointerom na ľubovolný typ) a používa sa na označenie, že tento pointer neukazuje na nič.


5.1.6 Konverzia pointerov

Všeobecne sa jej snažíme vyhýbať, pretože prináša množstvo problémov, napr. s pointerovou aritmetikou alebo s ukladaním niektorých dátovych typov na určité (párne) adresy v pamäti. Sú však standardné situácie - typicky prideľovanie dynamickej pamäti - pri ktorej je nutné konverziu pointerov používat. Pokiaľ sa nedá v iných než obvyklých situáciach konverzii pointerov vyhnúť, potom je dobré použiť explicitné pretypovanie. Spoľahnúť sa na implicitné pretypovanie je síce možne, ale nie je to vhodné.

Priklad:

char *p_c;
int *p_i;
p_c = p_i; /* nevhodne */
p_c = (char *) p_i; /* lepsie */


5.1.7 Zarovnavanie v pamäti

Pokiaľ pri konverzii pointerov dochádza k neočakávanej chybe, je vhodné skúsiť pretypovanie tam a späť a vypísať hodnoty pointerov, napr.:

printf("p_c pred konverziou %p \n", p_c);
p_i = (int *) p_c;
p_c = (char *) p_i;
printf("p_c po konverzii %p \n", p_c);

Odlišné výsledky, ktoré môžeme dostať, sú sposobené vďaka tomu, že niektoré kompilátory používajú taktiku, že určité dátove typy, napr. int sú uložené v pamäti od párnych adries. Pri pretypovaní pointeru s tým prekladač počíta a zaokrúhľuje prípadnú nepárnu adresu na najbližšiu nižšiu párnu. To má potom výhodu rýchlejšieho prístupu k týmto údajom, ale nevýhodu práve pri pointerovej konverii. Ďalšou nevýhodou je horšie využitie pamäte. Všeobecne sa dá povedať len to, že bez problémov je pointerová konverzia iba z vyšších (dlhších) dátovych typov na nižšie (kratšie), teda napr. pointer na long je možne bez problémov pretypovať na pointer na int alebo na char. Opačne to ale nemusí výjsť správne.


5.2 Pointery a funkcie

5.2.1 Volanie odkazom

Jednou z veľmi užitočných vlastností pointerov je, že umožňujú volanie parametrov "odkazom". Pointery v tomto prípade použijeme, keď chceme vo funkcii trvale zmeniť hodnotu skutočného parametra. V praxi to znamená, že nepredávame hodnotu premennej, ale adresu tejto premennej.

V C je toto "volanie odkazom" opäť volanie hodnotou, keď sa v stacku vytvorí lokálna kópia pre uloženie parametra - adresy skutočného parametra. Táto lokálna premenná síce zanikne s ukončením prislušnej funkcie, ale má tú vlastnosť, že je v nej uložený pointer, pomocou ktorého sa nepriamo zmenia údaje, ktoré nemajú s touto funkciou nič spoločného - boli definované vo vnútri tejto funkcie a nezanikajú s jej koncom. Teda výsledok je rovnaký ako pri skutočnom volaní odkazom v Pascale, ale postup spracovania iný. To čo v Pascale uskutočňoval automaticky kompilátor vďaka kľúčovému slovu VAR, to musíme v C urobiť sami - teda nepracovať s formálnym parametrom ako s normálnou premennou, ale ako s pointerom. Pre zjednodušenie bude však ďalej používaný termín volanie odkazom.

Poznámka:
Nejasnosti môžu vzniknúť až pri štúdiu jazyka C++ - objektovo orientovaného C - ktorý umožňuje skutočné volanie odkazom.

Príklad:
Toto je klasický príklad funkcie pre výmenu obsahov dvoch premenných.


void vymen(int *p_x, int *p_y)

{

	int pom;



	pom = *p_x;

	*p_x = *p_y;

	*p_y = pom;

}

Funkciu vymen() potom voláme so skutočnými parametrami i a j takto:

int i = 5, j = 3;
vymen(&i, &j);

Pozor:
  • Veľmi častá chyba pri volaní je:
    vymen(i, j); ktorá sposobí, že sa bude zapisovať na adresy dané obsahom premenných i a j, teda na absolútnu adresu 3 a 5, čo vedie vo väčšine prípadov k zrúteniu programu.
  • Druhou častou chybou je volanie:
    vymen(*i, *j); kde bude zápis prevedený na adresy adries z obsahov i a j, t. j. napr. z absolútnej adresy 3 sa vezme hodnota, ktorá sa použije ako adresa, na ktorej sa niečo zmeni. Výsledkom je opät najčastejšie zruúenie programu.
Príklad:
Program číta pomocou funkcie citaj_riadok() riadky z klávesnice a počita, koľko na ňom bolo medzier a malých pismen. Funkcia citaj_riadok() vracia hodnotu 1, keď na riadku boli nejaké znaky a hodnotu 0, ak bol riadok prázdny. Program skončí, ak prečíta prazdny riadok.

#include <stdio.h>



int citaj_riadok(int *p_medzery, int *p_male)

{

	int c, pocet = 0;



	*p_medzery = 0; *p_male = 0;



	while ((c = getchar()) != '\n') {

		pocet++;

		if (c == ' ')

			(*p_medzery)++;				 /* zatvorky nutne */

		else if (c >= 'a' && c <= 'z')

			(*p_male)++;					 /* zatvorky nutne */

	}



	return ((pocet == 0) ? 0 : 1);

}



main()

{

	int medzery, male;



	while (citaj_riadok(&medzery, &male) == 1) {

		printf("Na riadku bolo %d medzier a %d malych pismen. \n", medzery, male);

	}

}

Poznámka:
  • Pokiaľ by boli premenné medzery a male definované ako:
    int *medzery, *male; potom by nebolo možne volať funkciu citaj_riadok() ako:
    citaj_riadok(medzery, male); pretože nebola pridelená pamät, na ktorú ukazujú tieto pointery.
5.2.2 Pointer na typ void

Možnosť, ako pouziť typ void, je definovať pointer na void, napr.

void *p_void;

Pointer p_void neukazuje na žiadny konkrétny typ, teda dá sa využiť pre ukazovanie na celkom ľubovolný typ, avšak po dôslednom pretypovaní. Občas sa preň použiva výraz generický pointer.

Sú zhruba dve oblasti použitia:

void ako pointer na niekoľko rôznych typov


int i;

float f;

void *p_void = &i;				 /* p_void ukazuje na i */



main()

{

*(int *) p_void = 2;			 /* nastavi i na 2	 */

p_void = &f;					 /* p_void ukazuje na f */

*(float *) p_void = 1.1;		 /* nastavenie f na 1.1 */

}

void ako formálny parameter funkcie


#include <stdio.h>



void vymen_pointery(void **p_x, void **p_y)

{

void *p_pom;



p_pom = *p_x;

*p_x = *p_y;

*p_y = p_pom;

}



main()

{

	char c = 1, *p_c = &c, d = 2, *p_c = &d;

	FILE *fin = stdout,					/* zamerna chyba */

	FILE *fout = stdin;



	fprint(fin, "c = %d, d = %d \n", *p_c, *p_d);

	vymen_pointery((void *) &p_c, (void *) &p_d);

	vymen_pointery(&fin, &fout);

	fprintf(fout, "c = %d, d = %d \n", *p_c, *p_d);

}

Poznámka:
  • Pretypovanie na (void **) pri prvom volaní funkcie vymen_pointery() je len z dôvodu zamedzenia varovného hlásenia o nerovnakých typoch parametrov. Program funguje i bez neho, ale takto je to čistejšie.
5.2.3 Pointery na funkcie a funkcie ako parametre funkcií

Ak si spomenieme na funkciu fopen(), vieme, že funkcia môže vracat taktiež pointer na nejaký dátovy typ. Táto možnosť sa v C využiva pomerne často, napr. funkcia:

char *najdi_adresu_znaku(char c)

Táto funkcia by hľadala v pamäti od nejakej adresy zadaný znak a vracala by pointer na tento znak.

často sa tiež využíva možnosť definovať premennú ako pointer na funkciu vracajúcu nejaký typ, napr:

double (*p_fd) ();

Poznámky:
  • Prázdne zátvorky () pred ukončovacou bodkočiarkou sú nevyhnutné, pretože
    double (*p_fd); by znamenalo to iste, co:
    double *p_fd; teda že pre p_fd je pointer na double a nie pointer na funkciu vracajúcu double.
  • Zátvorky okolo mena premennej sú nevyhnutné, pretože:
    double *p_fd(); by znamenalo, že tento riadok je deklarácia funkcie pomenovanej p_fd, ktorá vracia pointer na double.
Ak máme definovanú funkciu:

double secti_dbl(double f, double g)

potom je možné napísať priradenie:

p_fd = secti_dbl; /* Pozor! ziadny & /*

ktoré priradí pointeru p_fd adresu funkcie secti_dbl().

Z toho, čo už o funkciách vieme, vyplýva, že meno funkcie sa môže v programe objaviť v týchto prípadoch:
  • Definícia funkcie:
    double secti_dbl(double f, double g) {return (f + d);}
  • Deklarácia funkcie:
    double secti_dbl();
  • Úplný funkčný prototyp:
    double secti_dbl(double f, double g);
  • Neúplný funkčný prototyp:
    double secti_dbl(double, double);
  • Volanie funkcie:
    w = secti_dbl(f1, f2);
  • Priradenie adresy funkcie do premennej, ktorá je typu pointer na zodpovedajúci typ:
    p_fd = secti_dbl;
Poznámka:
  • Pomocou pointeru p_fd, je potom možné volať funkciu secti_dbl() ako:
    w = (*p_fd) (f1, f2); alebo úplne rovnako ako:
    w = p_fd(f1, f2); pričom prvý spôsob bol ako jediný možný v K&R verzii C, a obidva sú možné v ANSI C.
Príklad:
Nasledujúci program vypíše tabuľku hodnôt polynomov p(x) = x * x + 8 a q(x) = x * x * x - 3 v intervale <-1;+1> s krokom 0,2

#include <stdio.h>



#define DOLNY	(-1)

#define HORNY	 1

#define KROK	 0.2



double pol1(double x)

{

return(x * x + 8);

}



double pol2(double x)

{

return(x * x * x - 3);

}



main()

{

	int i;

	double x;

	double (*p_fd) ();				 /* pointer na funkciu vracajucu double */



	for (i = 0; i < 2; i++) {

		/* priradenie adresy funkcie pointeru fd */

		p_fd = (i == 0) ? pol1 : pol2;

		/* tabulacia */

		for (x = DOLNY; x <=HORNY; x +=KROK)

			printf("%15.5lf \t %15.5lf \n", x, (*p_fd) (x));

	}

}

Keď sa nad predchádzajúcim príkladom zamyslíme, zistíme, že by bolo vhodné napísať funkciu tabulacia(), takto:

void tabulacia(double d, double h, double k, double (*p_fd) ())

{

	double x;



	for (x = d; x <= h; x += h)

		printf("%15.5lf \t %15.5lf\n", x, (*p_fd) (x));

}

a funkcia main() by vyzerala napr. takto:

main()

{

tabulacia(DOLNY, HORNY, KROK, pol1);

tabulacia(-2.0, 2.0, 0.05, pol2);

}

5.3 Ako čítať komplikované definície - I

Zatiaľ sme nemali problémy s tým, ako rozlúštiť, čo ktorá definícia znamená, pretože tieto definície boli veľmi jednoduché.
S príchodom pointerov sa však situácia rapídne mení, pretože je možné definovať pointer na veľmi komplikovaný typ.

Príklady zápisu definícií pomocou pointerov:

int x; /* x je typu int */
float *y; /* y je pointer na typ float */
double *z(); /* z je funkcia vracajuca pointer na double */
int *(*v) (); /* v je pointer na funkciu vracajucu pointer na int */


Pre túto ťažko pochopiteľnú zložitosť našťastie funguje jednoduché mnemotechnické pravidlo, ako prečítat ľubovolne komplikovaný zápis.
  • Nájdeme identifikátor a od neho čítame doprava.
  • Pokiaľ narazíme na samostatnú pravú okrúhlu zátvorku ")" (nie na dvojicu "())", vraciame sa na zodpovedajúcu ľavú okrúhlu zátvorku "(" a od nej čítame opäť doprava a samozrejme preskakujeme všetky už prečítané.
  • Ak narazime na ukončovaciu bodkočiarku, potom sa vraciame na najľavejšie, doposiaľ nespracované miesto a od neho čítame dolava.
Čítanie si prakticky ukážeme na naposledy uvedenom príklade definície premennej v:

int *(*v) ();

Tento zápis definície čítame nasledujúcim spôsobom:
  • V spleti zátvoriek a hviezdičiek si nájdeme identifikátor, teda "v" a povieme: v je ...
  • Od neho sa číta doprava, pokiaľ nenarazíme na ")". Pravá okrúhla zátvorka nás vracia doľava až na zodpovedajúcu ľavú okruhlu zátvorku "(" a od nej sa číta zase doprava, teda znak "*" a pridame: ...pointer na...
  • Preskakujeme meno premennej "v" a zátvorku ")", ktoré nám už poslúžili, a čítame stale doprava, pokiaľ nenarazíme na ďalšiu samostatnú pravú ")" alebo na ukončovaciu bodkočiarku, v našom prípade teda "()" a pridáme: ...funkciu vracajúcu ...
  • Ukončovacia bodkočiarka nás vracia doľava pred už spracovanú ľavú "(" a pretože sme vpravo už všetko prečítali, čítame teraz opačne - doľava - teda "*" a pridáme: ...pointer na...
  • Čítame stále doľava, teda "int" a dodáme: ...int a sme hotoví.
Výsledok "prieskumu" teda dohromady znie: v je pointer na funkciu vracajúcu pointer na int
Týmto jednoduchým spôsobom sa dá kedykoľvek prečítat ľubovolná definícia. Len je nutné trochu si to vyskúšat na niekoľkých príkladoch.


5.4 Definícia s využitím operátora typedef

Pomocou operatora typedef je možné vytvoriť nový dátovy typ. Pri definovaní premenných jednoduchých dátových typov sa typedef príliš nepoužíva, zatiaľ čo pri použití pointerov a štruktúr sa využíva veľmi často.

Príkaz:

typedef float *P_FLOAT;

vytvorí nový typ ako pointer na float a pomenuje tento typ identifikátorom P_FLOAT.

Pozor: Nie je to definícia premennej, ktorá vyhradzuje pamät, ale definícia nového typu, ktorá iba určuje vzorec (šablónu) pre ďalšie akcie.
Identifikátor P_FLOAT sa stáva synonymom typu float a je možné ho ďalej v programe použit pre definície, deklarácie, pretypovanie, atď.
Ďalšia možná definícia pomocou typedef, ktorá sa u pointerov často používa:
typedef int *P_INT;
P_INT p_i, p_j; /* spravna definicia dvoch pointerov na int */


Ak ukazujú pointery na ďalšie pointery, je to možné zapísat taktiež pomocou typedef napr.:
int *p_i; **p_p_i;

alebo
typedef int *P_INT;
typedef P_INT *P_P_INT;
P_INT p_i;
P_P_INT p_p_i;

kde p_i je pointer na int a p_p_i je pointer na pointer na int, alebo inak - pointer na typ pointer na typ int.

Príklad:
Definícia nového typu:
typedef double (*P_FD) ();
kde P_FD je typ pointer na funkciu vracajúcu double. Potom sú možné nasledujúce definície:
  • Definovanie premennej:
    P_FD p_fd;definuje p_fd ako pointer na funkciu vracajúcu double
  • Definovanie navratového typu funkcie:
    P_FD g(void) {
    return(sqrt);
    }kde g je funkcia vracajúca pointer na štandartnú funkciu sqrt( ) odmocnica z 10 by sa potom vypočítala ako:
    (*(g())) (10.0)
5.5 Pointerova aritmetika

S pointermi je možné uskutočňovať niektoré aritmetické operácie. Je však nutné poznamenať, že je ich oveľa menej, než aritmetických operácií s normálnou premennou.

Platné operácie s pointermi sú:
  • súcet pointerov a celého čísla
  • rozdiel pointeru a celého čísla
  • porovnavanie pointerov rovnakých typov
  • rozdiel dvoch pointerov rovnakých typov
Ostatné aritmetické operácie s pointermi sú sice mnohokrát uskutočniteľné, ale nemajú žiadny zmysel a stávaju sa zdrojom chýb. Predtým, než sa budeme aritmetickými operátormi s pointermi hlbšie zaoberať, je nutné vysvetliť ešte jeden operátor.

5.5.1 Operátor sizeof

Operátor sizeof zistí veľkosť skúmaného dátového typu v Bytoch. Občas sa použiva aj pri práci s jednoduchými dátovými typmi, ale ťažisko jeho práce je práve v spolupráci s pointermi a so zloženými dátovými typmi. Často je totiž nutné zistit veľkost objektov, na ktoré pointery ukazujú alebo majú ukazovať
.
Poznámka:
  • Pre tých, čo sa zaujímajú aj o efektivitu programu, dodávame, že sizeof nepracuje po spustení programu, ale je vyhodnotený v čase prekladu, takže vlastný beh programu nijako nezdržuje. Je teda veľmi vhodné ho použivať.
Majme definície:
int i, *p_i;
  • po príkaze:
    i = sizeof(p_i); bude v i počet Bytov nutných pre uloženie pointeru na typ int - tento príkaz sa používa málokedy.
  • Po príkaze:
    i = sizeof(*p_i); bude v i počet Bytov nutných pre uloženie objektu, na ktorý ukazuje p_i, teda objektu typu int - tento príkaz sa naopak používa veľmi často.
Poznámka:
  • Typ hodnoty, ktorú vracia sizeof, nie je určený, ale väčšinou to býva unsigned int alebo unsigned long.
Teraz sa môžeme vrátiť k sľubovanej pointerovej aritmetike.

5.5.2 Súčet pointeru a celého čísla

Výraz:
p + n
znamená, že sa odkazujeme na n-tý prvok za prvkom, na ktorý práve ukazuje pointer p. Hodnota adresy tohoto prvku je potom:
(char *) p + sizeof(*p) * n
teda k pointeru sa nepripočíta príslušné celé číslo, ale násobok tohoto čísla a veľkosti typu, na ktorý pointer ukazuje.

Majme definície a predpokladajme, že pre tieto typy plati:
char *p_c = 10; /* sizeof(char) == 1 */
int *p_i = 10; /* sizeof(int) == 2 */
float *p_f = 10; /* sizeof(float) == 4 */

Spočiatku všetky pointery ukazujú na adresu 10. Po inkrementácii ale platí:
p_c + 1 == 11
p_i + 1 == 12
p_f + 1 == 14

ale:
(char *) p_i + 1 ==11
rovnako ako:
(char *) p_f + 1 == 11
pretože pretypovanie na (char *) zmenilo veľkost objektu.

Pretože súčet celého čísla a pointeru je opäť pointer, je možné písať výrazy typu:
p_i = p_i + 5;
kde p_i bude ukazovať na 5-ty prvok za pôvodným prvkom.

Príklad:
Program prečíta double číslo a zobrazí zodpovedajúce Byty z adresy v pamäti, na ktorej je toto číslo uložené.

#include <stdio.h>



typedef unsigned char *P_BYTE;



main() {

	double f;

	P_BYTE p_byte;

	int i;



	printf("Zadaj realne cislo : ");

	scanf("%f", &f);

	p_byte = (P_BYTE) &f;

	for (i = 0; i < sizeof(double); p_byte++, i++)

		printf("%d. byte = %02Xh \n", *p_byte);

}

Poznámky:
  • Pretypovanie p_byte = (P_BYTE) &f; je nutné, pretože &f je adresa typu double a p_byte je typu pointer na unsigned char.
  • Ak sa odkazujeme do pamäte, používame explicitne typ unsigned char a pretože char môže byť ako signed, tak aj unsigned (čo záleží na implementácii). Ale my väčšinou chceme prečítat z pamäti Byte vo význame neznamienkovaného celého čísla, teda unsigned char.
5.5.3 Odčítanie celého čísla od pointeru

Táto aritmetická operácia má úplne rovnakú filozofiu ako pričítanie, teda výraz:
p - n
znamená, že sa odkazujeme na n-tý prvok pred prvkom, na ktorý práve ukazuje pointer p. Hodnota adresy tohoto prvku je potom:
(char *) p - sizeof(*p) * n


5.5.4 Porovnávanie pointerov

Pre porovnávanie veľkosti dvoch pointerov je možné použiť operátory:
< <= > >= == !=
Výrazy typu:
p_i < p_j
majú zmysel iba vtedy, ak sú obidva pointery rovnakého typu a obidva ukazujú na ten istý úsek pamäti, napr. na jedno pole. Potom má tento výraz hodnotu 1 (TRUE) ak je p_i (ako adresa) menšia než p_j (taktiež ako adresa) alebo 0 (FALSE) v ostatných prípadoch.

Poznámka:
  • Obmedzenie, že obidva pointery musia ukazovať na ten istý úsek pamäti, sa zavádza preto, že mnoho systémov má pamät segmentovanú a porovnavanie pointerov z rôznych segmentov nedáva žiadne rozumné výsledky.
Pozor:
Bez explicitného pretypovania sa nedajú porovnávat pointery rôznych typov okrem porovnávania pointerov s NULL pointerom.
Napríklad, ak sú p_c a p_d pointery na char, pričom p_c ukazuje na začiatok bloku údajov dĺžky MAX, potom je možné zistiť, či p_d ukazuje do vnútra tohto bloku takto:
(p_d >= p_c && p_d <= p_c + MAX)
Znaky z tohto poľa by sme tlačili:
for (p_d = p_c; p_d < p_c + MAX; p_d++)
printf("%c", *p_d);

Pointerovu aritmetiku možeme využit pre rýchle kopírovanie pamäti. S využitím predchádzajúceho príkladu budeme kopírovať blok dĺžky MAX z adresy p_c na adresu p_d. Využijeme pomocnú premennú p_t, ktorá je tiež pointer na char.
for (p_t = p_c; p_t < p_c + MAX; *p_d++ = *p_t++)
tu je základný trik:
*p_d++ = *p_t++
Ten sa dá rozpísat do troch príkazov:
  • Najprv sa uskutoční:
    *p_d = *p_t; teda kopírovanie jedneho Bytu
  • V druhej časti sa uskutočnia dva príkazy:
    p_d++; p_t++; teda inkrementácia obidvoch pointerov.
Po ukončení kopírovania ukazuje p_d na prvý Byte za novo skopírovaným blokom, takže potom je vhodný príkaz:
p_d -= MAX;
ktorý nastaví p_d na začiatok nového bloku dát.

5.5.5 Odčítavanie pointerov

Výraz:
p_d - p_c
má zmysel iba ak pointery ukazujú na rovnaké pole údajov. V tomto prípade slúži k zisteniu počtu položek poľa medzi týmito pointermi, pričom položkou poľa je dátový typ, na ktorý sú obidva pointery definované.

Výraz:
p_d - p_c
sa dá prepísať ako:
((char *) p_d - (char *) p_c) / sizeof(*p_d)

Predpokladajme opäť predchádzajúci príklad bloku údajov. Nasledujúca časť programu nájde v tomto bloku znak "?" a vytlačí jeho pozíciu. Pokiaľ sa v bloku údajov nenachádza znak "?", vytlačí sa -1.
for (p_d = p_c; *p_d != '?' && p_d < p_c + MAX; p_d++)
printf("%d \n", (p_d < p_c + MAX) ? p_d - p_c + 1 : -1);

Pozor:
Sčítať pointery sa nedá. Výsledkom sčítania by bola nezmyselná hodnota.



5.6 Dynamické prideľovanie a navracanie pamäti

Prideľovanie pamäti za chodu programu tak, aby nedošlo ku kolízii s ostatnými údajmi, je dosť zákerný problém. Našťastie programovacie jazyky poskytujú pre túto zložitú a citlivú operáciu štandartné procedúry a funkcie, ktoré ju uskutočnia za nás, takže sa možnosť omylu výrazne znižuje. Nám potom stačí tieto run-time procedúry a funkcie iba zavolať.
Ich označenie ako run-time, znamená, že tieto rutiny uskutočňujú operácie za behu programu. Sú to operácie, ktorých požiadavky (parametre) nemôžu byť určené v čase prekladu. V C je to funkcia malloc(). Tá pridelí z heapu blok pamäti potrebnej veľkosti
a vráti jeho adresu. Na strojovej úrovni je to volanie funkcii, ktoré manipulujú s pamäťou - uskutočňujú správu pamäti, čo je operácia pomerne zložitá. Veľkosť pamäti, ktorú pridelí, si v C musíme sami priamo určiť.
Všeobecne platí, že veľkosť pridelenej dynamickej pamäti je zavislá na veľkosti objektu, na ktorý príslusný pointer ukazuje. V súvislosti s dynamickým prideľovaním pamäti sa hovorí o tzv. životnosti údajov, čo znamená, ako dlho novo alokovaný objekt v pamäti existuje.
Spomeňme si napr. na automatické premenné, ktoré majú dobu životnosti určenú dobou uskutočňovania funkcie, v ktorej sú definované. Životnosť údajov v heape je od doby pridelenia pamäti až do jej uvolnenia alebo do skončenia programu. V C sa uvolnenie pamäti uskutoční napr. volaním funkcie free().

Poznámka:
  • Z predchádzajúcich riadkov je už asi jasné, že nie je dobré miešať údaje v stacku a v heape - napr. definovať pointer na údaje v stacku.
Pozor:
Funkčné prototypy ďalej popisovaných funkcií sú uvedené v súboroch stdlib.h (alebo niekedy tiež v alloc.h), ktorý je nutné do programu pripojiť pomocou prikazu:
#include <stdlib.h>


5.6.1 Pridelovanie pamäti

Štandardnou a najčastejšie používanou funkciou pre pridelenie pamäti je funkcia malloc(), ktorej jediný parameter je typu unsigned int. Tento parameter udáva počet Bytov, ktoré chceme alokovat. Funkcia malloc() vracia pointer na void, ktorý predstavuje adresu prvého prideleného prvku. Tento pointer je veľmi vhodné pretypovať na pointer na zodpovedajuci typ. Ak nie je v pamäti dostatok miesta pre pridelenie pozadovaného úseku, vracia malloc() hodnotu NULL.

Poznámky:
  • Je dobrým zvykom pri každom pridelovaní pamäti testovať návratovú hodnotu na NULL a nespoliehať sa na pocit, že pamäti musí byť dosť. Predídeme tým mnohým problémom. Ladíme totiž najčastejšie programy s malými údajmi, pre ktoré pamät väčšinou stačí. V reálnej prevádzke bude ale program použitý pre skutočné údaje, ktorých je väčšinou oveľa viac.
  • V tomto prípade sa nemusíme starať o problémy so zarovnávaním na určité adresy v pamäti, pretože malloc() na tieto problémy pamätá.
Priklad:
Ukážka použitia malloc() vrátane reakcie na prípadny neúspech.

int *p_i;



if ((p_i = (int *) malloc(1000) == NULL) {

	printf("Malo pamati \n");

	exit(1);

}

Poznámka:
  • Pri pridelovaní dynamickej pamäti je potrebné si uvedomiť, že síce žiadame o pridelenie určitého počtu Bytov, ale je iba vecou operačného systému, koľko pamäti najviac sa skutočne z heapu pridelí. Napríklad v MS-DOSe sa prideluje pamät po tzv. paragrafoch, čo sú násobky 16-tich Bytov. V praxi to teda znamená, že ak žiadame príkazom:
    p_c = malloc(1);o pridelenie jedného jediného Bytu, systém ich v skutočnosti pridelí 16. Dôvod tohto "plýtvania" pamäťou je, že systém musí mať pre každý pridelený blok dynamickej pamäti nejakú administratívu, aby napr. vedel, že je práve tento kúsok pamäti obsadený. Túto skutočnosť je treba si uvedomiť, práve v programoch, ktoré sa snažia prideliť väčšie množstvo kratších úsekov pamäti. Potom pamät dojde skôr, než keby program žiadal o pridelenie jedného veľkého useku.
5.6.2 Uvolnovanie pamäti

Uvolnovanie alebo navracanie pamäti je opačná akcia než pridelovanie. Platí všeobecná zásada, že už nepotrebnú pamät je dobre okamžite vrátit, a nečakať až na koniec programu. Pre uvolnenie pamäti sa využíva funkcia free(), ktorej parametrom je pointer na typ void, ktorý ukazuje na začiatok skôr prideleného bloku.

Poznamka:
  • Funkcia free() vracia už nepotrebnú pamät spät do heapu, teda uvolní ju pre ďalšie použitie. Dôležité ale je, že free() nemení hodnotu svojho parametra. To znamená, že pointer stále ukazuje na to isté miesto v pamäti. S touto pamäťou sa dá teda ďalej pracovať, ale v skutočnosti už programu nepatrí! Takéto využívanie uvolnenej pamäti môže spôsobiť množstvo problémov.
Po príkaze: free((void *) p_c);
je teda vhodné uviesť bezprostredne aj príkaz: p_c = NULL;
čím zabránime možnému prístupu do uvolnenej pamäti.


5.6.3 Príklady pridelovania pamäti

Majme definície:
char *p_c;
int *p_i, i = 0;
Potom príkaz:
*p_c = 'a';
nie je celkom v poriadku, pretože *p_c ukazuje niekam do pamäti, ktorú nemáme pridelenú. Pred týmto príkazom je teda treba uviesť príkaz:
p_c = malloc(1);
Aby sme dodržiavali dobré návyky, je potrebné naviac zistiť, či sa nám požadovanú pamät podarilo priradiť,
príkaz teda bude:
if ((p_c = malloc(1) == NULL) {
printf("Nie je volna pamat \n");
return;
}

Ak chceme v nasledujúcom kroku výpočtu alokovať 20 Bytov pomocou rovnakého pointeru p_c, potom príkaz:
p_c = malloc(20);
nie je úplne najvhodnejší, pretože sme stratili pointer na skôr alokovanú pamät - je v nej znak 'a' - túto pamät sa nám už nikdy nepodarí uvolniť a do konca programu bude znak 'a' "visieť" niekde v pamäti. Ak potrebujeme uvolniť pamať pre uloženie int hodnoty, potom príkaz:
p_i = malloc(2);
nie je opäť najvhodnejší, pretože je systémovo závisly - typ int nemusí nutne využívat len 2 Byty. Lepší spôsob je:
p_i = malloc(sizeof(int));
Ale ani táto varianta však nie je optimálna, pretože sme p_i priradili pointer na typ void. Bezchybná varianta je:
p_i = (int *) malloc(sizeof(int));


Ak použivame pri alokovaní pamäti operátor typedef, potom napr.:
typedef int *P_INT;
P_INT p_i;
p_i = (P_INT) malloc(sizeof(int));
je absolútne korektný, na rozdiel od:
p_i = (P_INT) malloc(sizeof(P_INT));
pretože veľkosť pointeru na int môže byť odlišná od veľkosti dátového typu int.

5.6.4 Funkcia calloc()

V mnohých prípadoch je nutné alokovať pamäť pre n prvkov, z ktorých každý má veľkosť size.
Pre tento prípad slúži funkcia calloc(n, size), ktorá alokuje toto pole prvkov rovnako ako príkaz:
malloc(n * size)
a naviac ho vynuluje.
Funkcia calloc() opäť vracia pointer na začiatok alokovanej oblasti alebo NULL, ak sa nepodarilo požadovanú pamäť prideliť.

Pozor:
V niektorých systémoch je nutné pamät pridelenú pomocou calloc() uvolniť pomocou funkcie cfree() a nie free()!


5.7 Pointer ako skutočný parameter funkcie

Ak potrebujeme vo funkcii zmeniť nie hodnotu premennej jednoduchého typu, ale hodnotu pointeru, je to samozrejme možné.
Ako na to, to nám ukáže druhá časť nasledujúceho programu.
Program prečíta z klávesnice 10 double čísiel, uloží ich do pamäti a vypočíta ich súčin. Program je rozdelený do troch pomocných funkcií a funkcie main(). Prvá funkcia alokuje blok památi a vráti pointer na jeho začiatok, alebo vráti NULL, ak nie je dostatok pamäti.
#include <stdio.h>

#include <stdlib.h>



#define SIZE	10



double *init(void) {

	return((double *) malloc(SIZE * sizeof(double)));

}

Druhá funkcia prečíta čísla z klávesnice a uloží ich do pamäti. Jej formálny parameter je pointer na začiatok alokovanej oblasti. Hodnota tohoto parametru sa nemeni.

void citanie(double *p_f) {

	int i;



	for (i = 0; i < SIZE; i++) {

		printf("Zadajte %d. cislo : ", i + 1);

		scanf("%lf", p_f + i);							/* tu nie je & !!! */

	}

}

Tretia funkcia uskutočňuje súčin všetkých prečítanych čísiel tak, že sa posledným číslom naplní formálny parameter sucin a ostatnými číslami sa násobí. Parameter sucin je spracovaný pomocou pointeru, pretože sa jeho hodnota bude meniť.

Poznámka:
  • v skutočnosti by bolo vhodnejšie, keby funkcia nasob() vracala typ double ako výsledok násobenia. Pointer na parameter sucin je použitý iba z pedagogických dôvodov.

void nasob(double *p_f, int size, double *sucin) {

for (size--, *sucin = *(p_f + size); size >= 0; size--)

	 *sucin *= *(p_f + size);

}

Hlavná funkcia main() iba volá predchádzajuce funkcie.

main() {

	double *p_dbl, suc;



	if ((p_dbl = init()) == NULL)

		return 1;							/* nedostatok pamati - koniec */



	citanie(p_dbl);

	nasob(p_dbl, SIZE, &suc);

	printf("Sucin cisel je: %12.3f \n", suc);

}

Poznámka:
  • Pri volaní funkcie nasob() sa prvý parameter p_dbl uvádza bez &, pretože je to pointer na double, kdežto tretí parameter suc je uvedený s &, pretože je to premenná typu double.
Funkciu init() je možné prepísať tak, aby adresu alokovanej pamäti nevracala, ale aby ju uložila do svojho parametra.

void init(double **p_f) {

	*p_f = ((double *) malloc(SIZE * sizeof(double)));

}

a v hlavnom programe by bola volaná ako:

main() {

	double *p_dbl;

	init(&p_dbl);

	...

}

Situacia v pamati bude nasledujuca:

Obrázok


Časté chyby:

int *p_i = 2;		 // inicializácia p_i na adresu 2

*p_i = 3;			 // zápis hodnoty 3 do pamäti na adresu 2





void nastav(int *i)

{

*i = 5;

}

int main (void)

{

int j, *p_j;

nastav(j);		 // má byť: nastav(&j);

nastav(*j);		 // má byť: nastav(&j);

nastav(*p_j);	 // má byť: nastav(p_j);

nastav(&p_j);	 // má byt: nastav(p_j);

}

V obidvoch prípadoch s p_j musí byť najskôr niekde alokovaná pamäť, teda:
p_j = (int *) malloc(sizeof(int));
alebo spravené priradenie:
p_j = &j;

int main(void) 1. chýba include <stdlib.h>
{
int *p_i;
p_i = maloc(5); 2. správne má byť:
} p_i = (int *) malloc(sizeof(int));
3. chýba test na NUL, teda úplne správne má byť:
if ((p_i = (int *) malloc(5) == NULL) {
printf("Malo pamati\n");
exit(1);
}
4. nakoniec, keď už pamäť nebude potrebná, nezabudnúť na jej uvolnenie:
free((void *) p_i);
p_i = NULL;


Čo je dobré si uvedomiť:
  • Pointery nie sú celé čísla.
  • Vždy používajte pretypovanie na správny typ pointeru.
  • Veľkosť objektov pre alokáciu pamäti určujte zásadne pomocou sizeof.
  • Vždy testujte, či sa podarilo požadovanú pamät prideliť.
  • Nezabudnite na príkaz:
    #include <stdlib.h>
    alebo:
    #include <alloc.h>
  • Uvolnujte iba pridelenú pamät.
  • Funkcia free() uvolní pamät, ale nezmení hodnotu pointeru. Použitie tejto hodnoty má za následok nepredvídatelné správanie sa programu.
  • Pre predávanie parametrov odkazom je nutné formálny parameter definovať ako pointer a skutočny parameter uvádzat s operátorom &.
  • Ak sa má zmeniť hodnota skutočného parametra, ktorý je pointer, je nutné použiť formálny parameter ako pointer na pointer.
  • Pokiaľ je skutočným parametrom NULL pointer, urobte explicitné pretypovanie na typ formálneho parametru.
Úlohy:

1. Zisti veľkosť všetkých základných dátových typov (int, float, ...) v bajtoch.
Spoiler

2. Napíš funkciu int set(char a, char *p_a) s jedným vstupným a druhým výstupným parametrom. Funkcia ako svoju návratovú hodnotu vracia 1, ak bolo vo vstupnom parametre písmeno a 0 v ostatných prípadoch. Do výstupného parametru uloží funkcia opačný typ písmena (veľké prevedie na malé a naopak), ak bol vstupný znak písmeno, alebo ho nezmení, ak znak nebol písmeno.
Otestuj v programe. Vo funkcii main() bude výpis znaku z výstupného parametru funkcie set() na základe vyhodnotenia jej výstupnej hodnoty za pomoci if-else.
Celý program náležite okomentuj.
Spoiler

3. Napíš funkciu long citaj_znak(FILE *fr, int *p_c), ktorá prečíta jeden znak zo súboru TEST.TXT a vráti ho pomocou druhého parametru. Návratovou hodnotou funkcie bude počet volaní tejto funkcie (využitie lokálnej statickej premennej). Hlavný program vypíše pomocou tejto funkcie súbor a nakoniec aj počet prečítaných znakov.
Použi tento súbor TEST.TXT!
Spoiler

4. Napíš funkciu, ktoré usporiada hodnoty 3 premenných int a, b, c tak, aby po skončení kódu funkcie platilo a <= b <= c. Uvedené premenné budú definované vo funkcii main. Vstupné parametre funkcie budú adresy tých premenných. Hodnoty premenných budú zadané z klávesnice (main) a výpis bude vyzerať nejak takto:
vstup: a = 5, b = 58, c = 3
výstup: a = 3, b = 5, c = 58
Spoiler
  • 0

Popis: Pointery

- Definicia udajov typu pointer na typ
- Praca s adresovymi operatormi
- Priradenie hodnoty pointerom a pomocou pointerov
- Pouzitie pointerov v priradovacich prikazoch
- Nulovy pointer NULL
- Konverzia pointerov
- Zarovnavanie v pamati
- Pointery a funkcie
- Volanie odkazom
- Pointer na typ void
- void ako pointer na niekolko roznych typov
- void ako formalny parameter funkcie
- Pointery na funkcie a funkcie ako parametre funkcii
- Ako citat komplikovane definicie - I
- Definicia s vyuzitim operatoru typedef
- Pointerova aritmetika
- Operator sizeof
- Sucet pointeru a celeho cisla
- Odcitanie celeho cisla od pointeru
- Porovnavanie pointerov
- Odcitavanie pointerov
- Dynamicke pridelovanie a navracovanie pamati
- Pridelovanie pamati
- Uvolnovanie pamati
- Priklady pridelovania pamati
- Funkcia calloc()
- Pointer ako skutocny parameter funkcie
- úlohy k lekcii
Poznámky:
Odkazy:


0 Komentárov


Najlepšie lekcie


Najnovšie pridané lekcie


Najnovšie komentáre


Najviac komentované lekcie


Najviac zobrazené lekcie


Náhodné lekcie


Na tejto stránke bolo užívateľ(ov) za posledných 30 minút

členov, návštevníkov