Urcite sa zhodneme na tom, ze jeden z najvacsich strasiakov jazyka C su prave smerniky ( pointre ). Tymto clankom sa pokusim co najviac detajlne a zaroven jednoducho vysvetlit tuto problematiku, aby ju pochopil ozaj kazdy a hlavne zaciatocnici, ktori so smernikmi prvy krat prisli do kontaktu.
1. ZAKLADNA TEORIA SMERNIKOV:
Co je to smernik? V prvom rade je to premenna, ako kazda ina. Je nutne podotknut, ze kazda jedna premenna v pamati zabera urcite miesto, v zavislosti od toho, akeho typu je dana premenna. Tak napriklad typ char bezne zabera 1 bajt, typ int zabera bezne 4 bajty, typ double 8, atd ( pisem bezne, pretoze je to zavisle aj od pouziteho kompilatora, operacneho systemu, a inych faktorov ). A kedze premenne nie su v pamati hociako porozhadzovane, ale kazda ma svoje miesto, musi sa aj nejako poznat, kde zacina, kde je jej adresa. Adresa je teda udaj, ktory nam presne hovori, kde je premenna v pamati umiestnena. Je to obycajne cislo, ktore dostaneme pomocou operatora ampersand & umiestnenim pred meno premennej. Mozte si ju kludne nechat vypisat, ale zrejme z toho nic mat nebudete, hodi sa vam maximalne pri debuggovani ( ladeni programu ):
Kód:
char premenna;
printf( "%x\n", &premenna ); /* %x je vypis hexadecimalneho cisla, pretoze adresy sa zvyknu oznacovat v 16-kovej sustave */
Takze teraz by ste mali vediet, co je to adresa premennej. Tento poznatok je dolezity k tomu, aby sme si dalej doplnili, co je to smernik. Je to premenna, ktora nesie adresu inej premennej. V zavislosti od toho, akeho typu je premenna, ktorej adresu pozadujeme ulozit do smernika, aj samotny smernik definujeme ako smernik na urcity typ, napr smernik na char, smernik na int, a podobne. Pre ilustraciu si prezrite nasledovny priklad:
Kód:
char znak = 'a';
int celeCislo = 10;
double desCislo = 3.14;
char *pZnak = &znak; /* pointer na typ char */
int *pCeleCislo; /* pointer na typ int */
double *pDesCislo; /* pointer na typ double */
pCeleCislo = &celeCislo; /* priradenie adresy premennej celeCislo do premennej pCeleCislo */
A pre lepsiu predstavu pomozu aj tieto obrazky:
Obr1:

Obr2:
Poznamka: tento obrazok sluzi iba ako mala pomocka pre lepsie pochopenie. Data su v pamati ukladane komlikovanejsim sposobom, hlavne co sa tyka premennych vacsich ako jeden bajt, preto to berte len ako zjednodusenu ilustraciu. Ak vas zaujimaja tato problematika, mozte sa pozriet na tieto linky:
http://en.wikipedia.org/wiki/Endianness
http://www.doc.ic.ac.uk/~eedwards/compsys/memory/index.htmlUplne v prvom riadku su nazvy premennych podla predchadzajuceho prikladu, cize znak, pZnak, celeCislo a pCeleCislo. Nazvy premennych si vyberame my, podla vlastneho vyberu. V druhom riadku, uz v tej tabulke, je adresa jednotlivych premennych. Tieto adresy prideluje operacny system, nie my, cize nad tymi cislami sa velmi nezamyslajte, ako sa tam dostali. Jeden stvorcek na obrazku predstavuje vekost jeden bajt, to znamena, ze do jedneho takehoto stvorceka mozme ulozit maximalne jeden bajt. Kedze napriklad typ int ma 4 bajty, potrebujeme na ulozenie akehokolvek cisla typu int styri taketo stvorceky v pamati. A nakoniec, v poslednom riadku, taktiez v tabulke, mozte vidiet hodnoty jednotlivych premennych, cize udaj, ktory uchovavaju tieto premenne. V pripade premennych znak a celeCislo sa nie je o com bavit. V pripade pZnak a pCeleCislo uz sa ale trocha zastavime. Vidime, ze smernik skutocne nesie adresu inej premennej ( tej, na ktoru ho priradime, ako v priklade vyssie ). A ze tato adresa ma velkost 4 bajty. Tento poznatok si zapamatajte, pretoze ci sa jedna o adresu premennej typu char alebo adresu zlozitej struktury, ktora moze mat niekolko rozne velkych poloziek, velkost adresy je stale o velkosi styroch bajtov ( aspon co sa tyka doby pisania tohto clanku ). Pevne verim, ze tieto obrazky vam pomohli lepsie si predstavit, ako to vlastne v pamati funguje.
Naco je vlastne dobre uchovavat adresy inych premennych? Odpoved je jednoducha, skusim to najprv vysvetlit na priklade zo zivota. Kazdy z nas mame svoj domov, kde byvame. Tento nas domov ma nejaku adresu. Prave pomocou tejto nasej adresy nas mozu ostatni ludia kontaktovat, posielat nam na tuto adresu nejaku postu a podobne. Rovnako ako my mame svoju adresu, kde byvame, aj premenne maju adresu, svoje umiestnenie v pamati. A rovnako, ako nas moze niekto kontaktovat pomocou nasej adresy, tak mozme aj my pristupovat k premennej skrz jej adresu ( v skutocnosti je aj nazov premennej len akysi symbol v dobe navrhu a v skutocnosti sa pracuje prave s jej adresou ). Predstavte si situaciu, ze mate funkciu, ktora ma vydelit dve cisla, ale chcete ju navrhnut tak, aby jej navratova hodnota nebola vysledok, ale aby vratila nulu v pripade, ze chcete delit nulou a jednotku v pripade, ze delenie prebehlo v poriadku. A vysledok chcete vratit v nejakej konkretnej premennej. Co v takomto pripade? Vacsina asi vie, ze nieco taketo to nevyriesi:
Kód:
#include <stdio.h>
int vydel( double delenec, double delitel, double vysledok )
{
if ( delitel == 0 )
return 0; /* nulou neviem delit, vysledok funkcie je 0 - FALSE */
vysledok = delenec / delitel; /* v opacnom pripade si ulozime vysledok a vratime 1 - TRUE */
return 1;
}
int main()
{
double vysledokDelenia = 0;
if ( vydel( 121, 11, vysledokDelenia ) == 1 )
printf( "Vysledok delenia je %f\n", vysledokDelenia ); /* vypise sa 0, ale to nie je spravne, vsak? */
else
printf( "Delenia nulou\n" );
return 0;
}
Ti, ktori neviete, preco, tak tu je vysvetlenie: Pri volani kazdej funkcie sa cez
zasobnik predaju parametre funkcie a to tak, ze sa vytvori iba ich kopia, tzn, predaju sa iba hodnoty tychto parametrov. My vo vnutri funkcie nevieme, ktorej premennej patri dana hodnota. Riesenim je pouzit smerniky. Preco? Pretoze hodnota smernika je prave adresa nejakej premennej. A teda ak predame funkcii ako parameter smernik, skopiruje sa jeho hodnota ( adresa pozadovanej premennej, ktoru nesie, na ktoru ukazuje ) a my tak mame aj vnutri funkcie pristup k premennej, ktoru potrebujeme menit. Upraveny funkcny priklad:
Kód:
#include <stdio.h>
/* premenna vysledok je uz pozmenena na smernik */
int vydel( double delenec, double delitel, double *vysledok )
{
if ( delitel == 0 )
return 0; /* nulou neviem delit, vysledok funkcie je 0 - FALSE */
*vysledok = delenec / delitel; /* na adresu, na ktoru ukazuje premenna vysledok, uloz vysledok delenia */
return 1; /* vrat 1 - TRUE */
}
int main()
{
double vysledokDelenia = 0;
if ( vydel( 121, 11, &vysledokDelenia ) == 1 )
printf( "Vysledok delenia je %f\n", vysledokDelenia ); /* vypise sa 11 */
else
printf( "Delenia nulou\n" );
return 0;
}
Smerniky sa dalej mozu vyuzivat aj na dynamicku alokaciu poli, kopirovanie blokov pamate, prechadzanie buniek pola atd. V mnohych pripadoch moze vhodne pouzitie smernikov aj usetrit pamat, alebo zrychlit program. Teraz, ked uz viete, kedy a preco pouzivat smerniky, este si povieme, ako ich spravne pouzivat, lebo aj s tym ma vela zaciatocnikov problemy. Napriklad ci pisat hviezdicku pred meno smernika alebo za meno, kedy pisat hviezdicku a kedy nie, atd.
Miesto, kde treba pisat hviezdicku, si zapamatate velmi jednoducho podla samotnej definicie smernika:
Kód:
int *smernik;
Hvezdicka je stale za typom smernika a pred nazvom smernika. Preto stale, ked pracujete s adresou, ktora je ulozena v smerniku, piste hviezdicku pred meno:
Kód:
a = b + *smernik + c;
a stale, ked pretypujete nejaky smernik na iny smernik, tak hviezdicka sa pise za typ smernika:
Kód:
smernik = (int*) inySmernik;
Hviezdicku k smerniku piseme vtedy, ak chceme pracovat s premennou, ktorej adresa je ulozena v smerniku - ci uz chceme odtial citat, alebo zapisovat. Vtedy hovorime, ze sme smernik dereferencovali ( cize *smernik ). V pripade, ze chceme narabat so samotnym smernikom, napriklad ak tam chceme vlozit novu adresu inej premennej, pripadne pracovat s adresou ktora tam uz je, napr pricitat k nej nejake cislo a posuvat sa tak v pamati, hviezdicku nepouzivame.
2. DYNAMICKA ALOKACIAV predchadzajucich prikladoch sme pracovali s pamatou, ktora bola alokovana staticky, to znamena, vsetky premenne, ktore sme pouzivali, mali uz v dobe kompilacie vyhradene svoje miesto v pamati presnej velkosti ( vid obrazky 1 a 2 ). A toto miesto im ostavalo pevne pocas celeho behu programu ( menil sa len, samozrejme, obsah tychto premennych ). Mnohokrat ale v nasom programe budeme potrebovat premenne, ktorych velkost dopredu nebudeme vediet, pripadne, ktorych velkost budeme potrebovat dynamicky menit, apod. Na tieto ucely sluzi dynamicka alokacia a spaja sa vylucne s pouzitim smernikov. Pri tejto alokacii sa pouziva uplne iny segment pamate, a ten nazyva sa
heap. Pamat je pridelovana operacnym systemom na "poziadanie" programatora. A co je dolezite, tato pamat je aj uvolnovania na poziadanie. Ak neuvolnite alokovanu pamat pred ukoncenim programu, ostane sa pouzivat aj po ukonceni programu a vzniknu tak "prazdne miesta", ktore sa nazyvaju memory leaks. Je preto dobre hned zo zaciatku sa spravne naucit narabat s touto castou pamate. Existuje skvely nastroj na odhalovanie tychto chyb programatora a nazyva sa
valgrind. Zial, tento program nie je urceny pre OS windows - aspon v sucasnosti nie. Vratme sa ale k samotnej dynamickej alokacii a ako ju pouzit. V jazyku C existuju tri zakladne funkcie pre pracu s heap-om a su to
malloc,
calloc a
realloc. Pre uvolnenie alokovanej pamate sa pouziva jedina funkcia
free. Vsetky tieto funkcie su deklarovane v hlavickovom subore
stdlib.h. Pre jednoduchost nam na prikladoch postaci pouzit funkciu malloc. Ta berie iba jeden parameter a to velkost pamate v bajtoch, ktoru chceme alokovat. Operacny system sa potom pokusi tutu velkost rezervovat a v pripade, ze nema dostatok pamate, funkcia vrati NULL ( tiez zvany nulovy smernik. Oznacuje sa nim, ze smernik neukazuje na ziaden objekt, na ziadnu pamat ). V pripade, ze potrebnej pamate je dostatok, tak tuto cast pamate alokuje ( obsadi ju a "oznaci" ako pouzitu ) a funkcia vrati smernik na prvy blok tejto pamate. Nam totiz staci vediet iba to, kde je zaciatok, pretoze jej velkost vieme. Po tomto procese uz mame v smerniku adresu na cast pamate, s ktorou uz pracujeme ako v priklade pred tym. Nasleduje jednoducha ukazka:
Kód:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int premenna;
int *sm = NULL;
sm = malloc( sizeof( int ) ); /* do smernika sm chceme priradit pamat o velkosti datoveho typu int */
if ( sm == NULL ) /* ak malloc vrati NULL, nastala chyba pri alokacii */
{
printf( "Chyba pri alokacii pamate\n" );
return 1;
}
printf( "Adresa premennej na stacku: %x\nAdresa v casti pamati zvanej heap, ktoru sme priradili do smernika sm: %x\n",
&premenna, sm );
*sm = 10;
printf( "\nSmernik = %d", *sm );
*sm = 5;
printf( "\nSmernik = %d\n", *sm );
free( sm ); /* nezabudnut uvolnit pamat pred ukoncenim programu */
return 0;
}
Chcem este upozornit na jednu vec. Funkcia malloc vracia smernik typu void*. Ak my pouzijeme iny typ smernika, ako napr v tomto priklade int*, neodporuca sa pretypovat tuto navratovu hodnotu funkcie malloc, cize nasledujuce nerobte: sm = (int*) malloc( sizeof( int ) );
V pripade, ze vas zaujima dovod, tu si o tom mozte precitat viac --->
http://c-faq.com/malloc/mallocnocast.html Ak pouzijete pretypovanie, nie je to chyba, ale je dobre sa tomu vyhnut ( zial, v sucasnosti na skolach ucia vselijaki ucitelia a na mnohych skolach sa uci aj tento nespravny postup ). Bavime sa ale iba o pretypovani navratovej funkcie z malloc. Teraz uvediem trocha komplikovanejsi priklad, kde vyuzijeme aj pretypovanie. Vyuzijem v nom aj smernik void* ktory sa neda dereferencovat, pretoze typ void nemoze niest ziaden udaj, je to tzv prazdny datovy typ. Priklad:
Kód:
#include <stdio.h>
#include <stdlib.h>
void halt( char *msg ) /* v pripade chyby zavolame tuto funkiu, kt. ukonci program */
{
printf( "%s\n", msg );
exit( EXIT_FAILURE );
}
int main()
{
void *sm;
sm = malloc( sizeof( int ) ); /* rezervujme si miesto pre jeden int */
if ( sm == NULL )
halt( "Malloc failed\n" );
*( (int*) sm ) = 10; /* vsimnite si tento zapis pozorne. Chceme zapisat cislo 10 do pamate, kam ukazuje
* smernik sm. Lenze ten je typu void* preto ho najpr musime pretypovat na typ
* int*, cize to je to (int*) sm. Teraz uz ho mozme dereferencovat a do pamate
* zapisovat hodnoty o velkosti typu int */
printf( "Na adrese, ktoru mame v sm, je naozaj hodnota %d\n", *( (int*) sm ) );
/* teraz sa rozhodneme, ze do smernika chceme ulozit znak, nie cislo, takze povodnu pamat uvolnime */
free( sm );
/* a alokujeme novu, tentokrat pre typ char */
sm = malloc( sizeof( char ) );
if ( sm == NULL )
halt( "Malloc failed\n" );
*( (char*) sm ) = 'f';
printf( "Tento krat tu mame znak '%c'\n", *( (char*) sm ) );
free( sm );
return 0;
}
V oboch prikladoch sme si ukazali iba alokaciu pamate pre jeden zakladny datovy typ. My si ale mozme pomocou funkcie malloc ( a ostatnych spomenutych ) alokovat kludne aj viac pamate a k danemu bloku pristupovat pomocou smernikov a pouzivat ho, akoby to bolo pole ( pozor - mozme ho s touto pamatou zaobchadzat, akoby to bolo pole, ale pole to nie je, viz nasledujuci bod 3 ). Najprv uvediem jednoduchy priklad, pricom predpokladam, ze pracu s poliami uz ovladate:
Kód:
#include <stdio.h>
#include <stdlib.h>
void halt( char *msg )
{
printf( "%s\n", msg );
exit( EXIT_FAILURE );
}
int main()
{
int i;
int *smBlokIntov; /* vsimnite si, ze definicia smernika je aj v pripade, ze
* alokujeme viac pamate rovnaka. Smernik totiz drzi iba adresu,
* kde zacina nasa alokovana pamat - velkost tohto bloku totiz
* pozname */
smBlokIntov = malloc( 4 * sizeof( int ) ); /* rezervujme si miesto pre 4 prvky, pricom kazdy
* je velkosti typu int ( ktory ma 4 bajty ), cize vysledna
* pamet bude velkosti 4*4 b = 16 bajtov */
if ( smBlokIntov == NULL )
halt( "Malloc failed\n" );
for ( i = 0; i < 4; i++ )
smBlokIntov[i] = i+1; /* do nasej pamate postupne ulozime cisla od 1 po 4. Snad netreba
* pripominat, ze pole sa indexuje od nuly, nie od jednotky */
for ( i = 0; i < 4; i++ )
printf( "Hodnota pola na indexe %d je %d\n", i, smBlokIntov[i] );
putchar( '\n' );
free( smBlokIntov );
return 0;
}
3. SMERNIKY VS POLIA:Pole je premenna, ktora obsahuje viacero prvkov rovnakeho datoveho typu. Velmi casto sa pouziva nespravny pojem, ze pole je vlastne smernik, co nie je tak celkom pravda, su si iba velmi podobne a smernik na dynamicky alokovanu pamat mozme pouzivat ako pole. A to je asi tak vsetko. Pozrime sa na nasledujuci priklad:
Kód:
#include <stdlib.h>
void halt( char *msg )
{
printf( "%s\n", msg );
exit( EXIT_FAILURE );
}
int main()
{
char pole[4] = { 1, 2, 3, 4 };
char *smernik;
int i;
smernik = malloc( 4 * sizeof( char ) );
if ( smernik == NULL )
halt( "Malloc failed\n" );
for ( i = 0; i < 4; i++ )
smernik[i] = i+10;
for ( i = 0; i < 4; i++ )
{
printf( "Hodnota pola na indexe %d je %d\n", i, (int) pole[i] ); /* v poli mam ulozeny typ char, preto ho pretypujeme na int */
printf( "Hodnota dynamicky alokovaneho pola na indexe %d je %d\n", i, (int ) smernik[i] );
}
putchar( '\n' );
free( smernik );
return 0;
}
Cisla sa vypisali presne take, ake sme ocakavali, takze by ste asi povedali, ze aj premenna pole aj premenna smernik su klasicke polia, vsak? A predsa tomu tak nie je. Pozrite sa na tento obrazok a hned uvidite rozdiel,
Obrazok3:
Je krasne vidiet, ze smernik, tak ako som spominal na zaciatku, nesie iba adresu, kde blok s datami zacina. Pouzivanie poli a smernikov je ale rovnake. Ci uz zavolame pole[2], alebo *(pole + 2), stale sa dostaneme na pamat, kde je ulozene cislo 3. A takisto aj zapisom smernik[2] a *(smernik + 2 ) sa dostaneme na pamat, kde je ulozene cislo 12.
Problem pri zameneni smernika s polom nastane hlavne pri dvoj a viac rozmernych poliach a tento problem je dost casty a mnohi zaciatocnici nechapu, aky je rozdiel medzi double **premenna a double premenna[10][5]. Ten rozdiel je dost velky, ale o tom napisem az v dalsej casti, ak o nu bude zaujem.
V pripade, ze ste niecomu aj tak nepochopili, pripadne mam niekde chybu, tak len smelo piste.