Aktuálně: Postihly zákazy tvou profesi? Poptávka po ajťácích prudce roste, využij slevové akce 30% výuky zdarma!
Discount week - April - 30

Lekce 1 - Úvod do ukazatelů (pointerů) v jazyce C

Vítejte u první lekce pokročilého kurzu o programování v jazyce C. V tomto kurzu se naučíme pracovat s dynamicky alokovanou pamětí v jazyce C a dostaneme se také k práci se soubory. Asi vás nepřekvapí, že předpokladem ke zdolání seriálu je znalost základních konstrukcí jazyka C.

Adresy v paměti

Když jsme se poprvé zmiňovali o proměnných, říkali jsme si, že proměnná je "místo v paměti", kam si můžeme uložit nějakou hodnotu. Také víme, že proměnné mají různé datové typy (např. int) a ty zabírají v paměti různě místa (např. int zabírá 32 bitů, tedy 32 nul a jedniček).

Paměť počítače si můžeme představit jako dlouhou (téměř nekonečnou :) ) řadu nul a jedniček. Některé části paměti jsou obsazené jinými aplikacemi a některé jsou operačním systémem chápány jako volné místo. Aby se dalo s pamětí rozumně pracovat, je adresována, jako jsou např. domy v ulici. Adresy se většinou zapisují v šestnáctkové soustavě, ale stále se jedná o obyčejná čísla. Adresy jdou chronologicky za sebou a na každé adrese se nachází 1 bajt (tedy 8 bitů, protože adresování po drobných bitech by bylo nepraktické).

Jakmile v céčku deklarujeme nějakou proměnnou ve zdrojovém kódu a aplikaci spustíme, Céčko si řekne operačnímu systému o tolik paměti, kolik je pro tuto proměnnou třeba. Od systému získá přidělenou adresu do paměti, na kterou může hodnotu proměnné uložit (zjednodušeně řečeno). Tento proces nazýváme alokace paměti.

Získání adresy proměnné

Jazyk C nás od adres zatím plně odsťiňoval, paměť alokoval za nás a s našimi proměnnými jsme pracovali jednoduše pomocí jejich jmen. Vytvořme si nyní jednoduchý program, který založí proměnnou typu int a do ní uloží hodnotu 56. Adresu této proměnné si získáme pomocí tzv. referenčního operátoru & (ampersand) a vypíšeme ji do konzole. Ve formátovacím řetězci použijeme %p, což ji vypíše v šestnáctkové soustavě tak, jak se na paměťovou adresu sluší a patří.

int main(int argc, char** argv) {
    int a;
    a = 56;
    printf("Proměnná a s hodnotou %d je v paměti uložená na adrese %p", a, &a);
    return (EXIT_SUCCESS);
}

Výsledek:

c_pointery
Proměnná a s hodnotou 56 je v paměti uložená na adrese 0x23aadc

Vidíte, že na mém počítači si systém vybral adresu 0x23aadc. Vy tam budete mít jiné číslo. Situace v paměti počítače bude vypadat takto:

Paměť počítače

(Datový typ int má 32 bitů, proto tedy zabírá 4 osmice bitů na 4 adresách. Udáváme vždy adresu začátku hodnoty.)

Ukazatelé (pointery)

Získat číslo adresy je sice hezké, ale pokud bychom s pamětí takto pracovali, bylo by to poněkud nepraktické. Z toho důvodu jazyk C podporuje tzv. ukazatele (anglicky pointery). Ukazatel je proměnná, jejíž hodnotou je adresa někam do paměti. Céčko ukazatel však nebere jako pouhé číslo, ale ví, že ho má používat jako adresu. Když do ukazatele tedy něco uložíme nebo naopak vypíšeme jeho hodnotu, nevypisuje se adresa (hodnota ukazatele), ale použije se hodnota, na kterou ukazatel ukazuje.

Tento výukový obsah pomáhají rozvíjet následující firmy, které dost možná hledají právě tebe!

Vraťme se opět k našemu programu. Tentokrát si kromě proměnné a definujeme i ukazatel na proměnnou a. Ten bude také typu int, avšak před jeho názvem bude tzv. dereferenční operátor * (hvězdička). Zvykněte si pointery pojmenovávat vždy tak, aby začínaly na p_. Vyhnete se tak v budoucnu velkým problémům, protože pointery jsou poměrně nebezpečné, jak dále zjistíme, a měli bychom si srozumitelně označit, zda je proměnná pointerem či nikoli.

int main(int argc, char** argv) {
    int a, *p_a;
    a = 56;
    p_a = &a; // Uloží do p_a adresu proměnné a
    *p_a = 15; // Uloží hodnotu 15 na adresu v p_a
    printf("Ukazatel p_a má hodnotu %d ukazuje na hodnotu %d", p_a, *p_a);
    return (EXIT_SUCCESS);
}

Aplikace si vytvoří proměnnou typu int a dále ukazatel na int. Ukazatelé také mají vždy svůj datový typ podle toho, na hodnotu jakého typu ukazují. Do proměnné a se uloží hodnota 56.

Do ukazatele p_a (zatím bez hvězdičky) se uloží adresa proměnné a, kterou získáme pomocí referenčního operátoru &. Nyní chceme tam, kam ukazuje pointer p_a, uložit číslo 15. Použijeme dereferenční operátor (*) a tím neuložíme hodnotu do ukazatele, ale tam, kam ukazatel ukazuje.

Následně vypíšeme hodnotu ukazatele (což je nějaká adresa v paměti, obvykle vysoké číslo, zde ho vypisujeme v desítkové soustavě) a dále vypíšeme hodnotu, na kterou ukazatel ukazuje. Kdykoli pracujeme s hodnotou ukazatele (ne adresou), používáme operátor *.

Výsledek:

c_pointery
Ukazatel p_a má hodnotu 23374500 ukazuje na hodnotu 15

Opět si ukažme i situaci v paměti:

Paměť počítače

Předávání referencí

Umíme tedy na proměnnou vytvořit ukazatel. K čemu je to ale dobré? Do proměnné jsme přeci uměli ukládat i předtím. Jednou z výhod pointerů je tzv. předávání referencí. Vytvořme si funkci, které přijdou v parametru 2 čísla a my budeme chtít, aby jejich hodnoty prohodila (této funkci se anglicky říká swap). Naivně bychom mohli napsat následující kód:

// Tento kód nefunguje
void prohod(int a, int b)
{
    int pomocna = a;
    a = b;
    b = pomocna;
}

int main(int argc, char** argv) {
    int cislo1 = 15;
    int cislo2 = 8;
    prohod(cislo1, cislo2);
    printf("V cislo1 je číslo %d a v cislo2 je číslo %d.", cislo1, cislo2);
    return (EXIT_SUCCESS);
}

Výsledek:

c_pointery
V cislo1 je číslo 15 a v cislo2 je číslo 8.

Proč že aplikace nefunguje? Při volání funkce prohod() ve funkci main() se vezmou hodnoty proměnných cislo1 a cislo2 a ty se zkopírují do proměnných a a b v definici funkce. Funkce dále změní tyto proměnné a a b, avšak původní proměnné cislo1 a cislo2 zůstanou beze změny. Tomuto způsobu, kdy se hodnota proměnné do parametru funkce zkopíruje, říkáme předávání hodnodnou.

Všimněte si, že k prohození 2 čísel potřebujeme pomocnou proměnnou. Kdybychom ve funkci prohod() napsali jen a = b; b = a;, byla by v obou proměnných hodnota b, protože hodnota a se prvním příkazem přepsala.

Libovolnou proměnnou můžeme předat referencí a to tak, že funkci upravíme aby přijímala v parametrech pointery. Při volání takové funkce potom použijeme referenční operátor &:

void prohod(int *p_a, int *p_b)
{
    int pomocna = *p_a;
    *p_a = *p_b;
    *p_b = pomocna;
}

int main(int argc, char** argv) {
    int cislo1 = 15;
    int cislo2 = 8;
    prohod(&cislo1, &cislo2);
    printf("V a je číslo %d a v b je číslo %d.", cislo1, cislo2);
    return (EXIT_SUCCESS);
}

Výsledek:

c_pointery
V cislo1 je číslo 8 a v cislo2 je číslo 15.

Jelikož funkci nyní předáváme adresu, je schopna změnit původní proměnné.

Někteří programátoři v jazyce C používají často parametry funkcí k vracení hodnoty. To ovšem není příliš přehledné a pokud nás netlačí výpočetní čas a je to jen trochu možné, měla by funkce vždy vracet jen jednu hodnotu pomocí příkazu return, případně může vracet strukturu nebo ukazatel na strukturu/pole.

Možná vás napadlo, že konečně rozumíte funkci scanf(), která ukládá hodnoty do proměnných předaných parametry. Operátor & zde používáme proto, abychom funkci předali adresu, na kterou má data uložit:

int a;
scanf("%d", &a);

Předávání pole

Pole a pointery mají v céčku mnoho společného. Proto když předáme pole do parametru nějaké funkce a pole v ní změníme, změny se v původním poli projeví. Pole je na rozdíl od ostatních typů vždy předáváno referencí aniž bychom se o to museli snažit.

void napln_pole(int pole[], int delka)
{
    int i;
    for (i = 0; i < delka; i++)
    {
        pole[i] = i + 1;
    }
}

int main(int argc, char** argv) {
    int cisla[10];
    napln_pole(cisla, 10);
    printf("%d", cisla[5]); // Vypíše číslo 6
    return (EXIT_SUCCESS);
}

Jak jsme si řekli dříve, pole je vlastně spojité místo v paměti. Ale takové místo musíme umět nějak adresovat. Adresujeme ho právě pomocí ukazatele. Proměnná typu pole totiž není nic jiného než ukazatel. To znamená, že nám bez problémů projde následující operace přiřazení:

int pole[10];
int* p_pole = pole;

V kapitole Aritmetika ukazatelů si ukážeme, že je úplně jedno, zda máme pole nebo ukazatel.

NULL

Všem pointerům libovolného typu můžeme přiřadit konstantu NULL. Ta udává, že je pointer prázdný a že zrovna na nic neukazuje. Na většině platforem se NULL rovná hodnotě 0 a tak se v některých kódech můžete setkat s přiřazením 0 místo NULL. To se obecně nedoporučuje kvůli kompatibilitě mezi různými platformami. Tuto hodnotu budeme v budoucnu hojně používat.

Co si zapamatovat: Pointer je proměnná, ve které je uložena adresa do paměti. Můžeme pracovat buď s touto adresou nebo s hodnotou na této adrese a to pomocí operátoru *. Adresu libovolné proměnné získáme pomocí operátoru &.

Ačkoli jsme si pointery poměrně slušně uvedli, jejich pravým účelem je zejména dynamické alokování paměti. Na které se podíváme hned v příští lekci, Dynamická alokace paměti v jazyce C.


 

Všechny články v sekci
Dynamická práce s pamětí v jazyce C
Článek pro vás napsal David Čápka
Avatar
Jak se ti líbí článek?
9 hlasů
David je zakladatelem ITnetwork a programování se profesionálně věnuje 13 let. Má rád Nirvanu, sushi a svobodu podnikání.
Unicorn university David se informační technologie naučil na Unicorn University - prestižní soukromé vysoké škole IT a ekonomie.
Aktivity (8)

 

 

Komentáře
Zobrazit starší komentáře (32)

Avatar
Jozef Vendel
Člen
Avatar
Jozef Vendel:12. ledna 21:47

Ešte som si neprešiel všetky lekcie z tohto kurzu, ak si ich prejdem poctivo tak sa ti ozvem a tvoju myšlienku môžeme zrealizovať ;)

 
Odpovědět
12. ledna 21:47
Avatar
DarkCoder
Člen
Avatar
Odpovídá na Jozef Vendel
DarkCoder:12. ledna 22:12

Nemusíš procházet všechny články, otázky budou z naprostých základů o ukazatelích. Nicméně čím více si toho projdeš, tím více Ti to může začít dávat smysl. Kdykoli se ozvi, příspěvky v sekci C pročítám pravidelně.

Editováno 12. ledna 22:12
Odpovědět
12. ledna 22:12
"„Učíš-li se proto, aby sis zapamatoval, zapomeneš. Učíš-li se proto, abys porozuměl, zapamatuješ si."
Avatar
Jozef Vendel
Člen
Avatar
Odpovídá na DarkCoder
Jozef Vendel:14. února 15:21

AhojDarkCoder, prešiel som si celý kurz "Jazyk C a ukazatele" aj s doplnením teoretických informácií z knižky Učebnica jazyka C od P.Herouta. Momentálne si hľadám praktické príklady, kde by som si precvičoval prácu s pointermi. Môžeš mi položiť otázky, ak máš záujem. Poprípade, ak poznáš dobrú stránku s praktickými príkladmi jazyka C(momentálne stačí práca s pointermi) môžeš sa o ňu podeliť.
Ďakujem.
J.V.

 
Odpovědět
14. února 15:21
Avatar
DarkCoder
Člen
Avatar
Odpovídá na Jozef Vendel
DarkCoder:14. února 19:46

Môžeš mi položiť otázky, ak máš záujem.

Dobrá. Jak jsem slíbil, zde je pár otázek a cvičení ohledně základů ukazatelů v C,
do kterých se můžeš pustit, zodpovědět je a zpracovat.

1. Jaké má jazyk C ukazatelové operátory, jak se nazývají a jaký je jejich význam?
Uveď příklad.
2. Jak vypadá inicializace ukazatele na typ float, který na nic neukazuje.
Uveď příklad.
3. Máš proměnou q typu int. Přiřaď ji nějakou hodnotu. Vypiš její adresu a hodnotu pomocí ukazatele.
Uveď příklad.
4. Je následující úryvek kódu správný? Pokud ne, proč?

int *p;
*p = 100;

5. Je následující úryvek kódu správný? Pokud ne, proč?

int *p;
double q, val = 1234.56;

p = &val;
q = *p;
printf("%f", q);

6. Jaké operátory lze použít pro práci s ukazateli?
7. Máš ukazatel p typu int, který obsahuje hodnotu 100.
Představuje tato hodnota adresu? Jaká hodnota ukazatele by byla po provedení následujícího příkazu?

p = p + 4;

8. Máš proměnnou q typu int, jejíž hodnota je 100.
Jak by vypadal zápis, abychom inkrementovali její hodnotu pomocí ukazatele.
Uveď příklad.
9. Máš 5 prvkové pole celých čísel. Vypiš hodnotu čtvrtého prvku pomocí ukazatele.
Uveď příklad.
10.Lze indexovat ukazatele jako by to bylo pole?
11. Hodnotu i-tého prvku pole pomocí indexů lze získat takto: pole[i]
Jak vypadá zápis pomocí kterého lze získat hodnotu i-tého prvku pomocí ukazatele?
Uveď příklad.
12. Co představuje jméno pole bez indexu? V čem se liší od klasického ukazatele?
13. kdy je třeba použít ukazatel jako parametr?
Uveď příklad.
14. Jak vypadá deklarace deklarace pětiprvkového pole ukazatelů na int a v jak vypadá
deklarace ukazatele na pětiprvkové pole typu int.
15. Je následující úryvek kódu správný? Pokud ne, proč?

char *p = "Ucim se ukazatele";
pritnf(p);

16. Napiš alespoň dva způsoby, jak lze předat jednorozměrné pole typu float jako parametr funkce.
17. Napiš funkci, která vrátí počet znaků v řetězci zadaného, předaného jako argument funkce.
18. Máš následující inicializaci pole:

char str[] = "Ucim se ukazatele";

Vytvoř dva ukazatele p_zacatek a p_konec a ukazatel p_zacatek nastav na začátek str
a ukazatel p_konec nastav na konec str. (Nápověda: strlen())
19. Napiš funkci která vrátí obvod a obsah kruhu, funkce přebírá poloměr jako argument funkce.
20. Napiš funkci, která zamění v řetězci str, původní znak c_puvodni za nový znak c_novy.
Funkce vrátí ukazatel na tento upravený řetězec.

Odpovědět
14. února 19:46
"„Učíš-li se proto, aby sis zapamatoval, zapomeneš. Učíš-li se proto, abys porozuměl, zapamatuješ si."
Avatar
Jozef Vendel
Člen
Avatar
Odpovídá na DarkCoder
Jozef Vendel:18. února 18:29

1.Tak operator „*“ v kontexte *p_i(inak to znamena nasobenie), ktory vyuzivame na oznacenie kompilatoru, ze premena p_i je typu poiner na nejaky datovy typ. Dalej pri pointeroch vyuzívame referencny operator „&“ na ziskanie adresy premennej v pameti. Napr... int *p_i, i; i = 5; /*Ak by som pouzil nasledovny prikaz *p_i = 5 pri dynamickom pridelovani; .. bolo by to chybne, pretoze pointer p_i ukazuje na nahodnu adresu v pameti, preto vyuzivame referencny operator &*/ p_i = &i; /*prikazom priradenia sme adresu premenej i ulozili do premenej p_i, cize hodnotu ktoru ma premenna p_i je hodnota, ktoru ma aj premena i a to je 5, a teraz pomocou premenej p_i vieme upravovat hodnotu, ktora lezi v premenej i.....*/
2.

float *p_f;
p_f = (float *) malloc(sizeof(float));
p_f  = NULL;

3.

int *p_q, q; q = 5; p_q = &q; printf("adresa premenej q je %p a hodnota ulozena v premenej je %d", p_q, *p_q);

4. V knizke od P.Heourta som cital, ze pokial je to staticke priradenie tak je to spravne ale pokial je to dynamicke priradenie tak nie. Pointer p na int ukazuje na nahodne miesto v pamäti takže sme na náhodnej adrese v pamäti nainicializovali hodnotu 100. Tu som sa trosku stratil chapem co je staticke a co dynamicke ale mohol by si mi to vysvetlit este raz podla seba.
5.Nie lebo premenne q, val su typu double a pointer p je typu int, nie je spravne miesat rozne datove typy na oboch stranach, je potrebne pretypovanie.
6. <, <=, >, >=, !=, ==, +, -, ++, --
7.Nie, na adresu v p(kde je uložený int) dá hodnotu 4.Prikaz zobrazený nižšie môžeme použiť napríklad ak mame pole 10-in int prvkov a chceme zistit adresu 5teho prvku tak k prvemu prvku v poli p pripocitame +4( zalezi od kompilatora a od OS ak som spravne pochopil ale vacsinou je int ukaldany ako 4B) to znamena že 4*4=16B, čiže nasledujúci 20B(jeho adresa) bude zapísaná v pointri p.
8.

int *p_q, q;
q = 100;
p_q = &q;
*p_q += 1;
printf("Nova hodnota *p_q je:%d", *p_q);

9.

int *p_q;
    int pole[5] = {2, 5, 7, 10, 11};
    p_q = &pole[0];
    printf("Hodnota 4.prvku je %d", *(p_q + 3));

10.Nie.V priklade 9 som ukazal ako je mozne pristupovat k jednotlivym prvkom pola.
11.*(p_i + i);
12.Staticky pridelena pamät pre prvky v poli. Ukazatele maju pridelenu pamäť dynamický.
13.Keď chceme vo funkcií trvalo zmeniť hodnotu skutočného parametru obyčajného dátového typu alebo hodnotu pointeru. Napr ak chceme zemnit hodnotu skutočného parametru obyčajneho datoveho typu: ..

void vymen(int *a, int *b);
void vymen(int *a, int *b){
    int pomocne;
    pomocne = *a;
    *a = *b;
    *b = pomocne;
  }
int main(){
    int o = 3, v = 4;
    vymen(&o, &v);
printf("Hodnoty po vymene o:%d v:%d", o, v); }

14. int** pole = (int**)malloc(5 * sizeof(int));
Int pole = (init) malloc (5*sizeof(int));
15.Spravny, lebo p nepredstavuje momentalne dynamicky retazec, ale pointer na ryp char. A tento pointer je inicializovany adresou retazovej konstanty, ktora ma obsah Ucim se ukazatele. Len akurat som sa este nikde nestretol s vypisom takeho printf(p), co asi je dobre, lebo printf ma parameter retaca ktory ma vypisat na obrazku, ale rozmyslal nad tym, ci je v retazci ulozeny ukoncovaci znak ‚\0‘.
16. void vymen(float a[], int pocet) alebo void vymen(float *a, int pocet); ..Pridal som este parameter pocet lebo, ak pracujeme vo funkcii s polom, poterbujeme poznat jeho velkost a ak pracujeme s poliami, ktorych velkost nie je konstantna, musime ju funkcii oznamit pouzitim dalsieho parametra. Vo funkcii main funkciu vymen zavolam takto .. vymen(a, pocet);... Na tom ci a je pole staticke alebo dynamicke nezalezi. Ak by som chcel napr. urobit vymenu v poli float f[10] medzi stvrtym a desiatym prvkom, tak by som mohol zavolat takto vymen(f+3, 6);
17.

int pocet(char *retazec1);
int pocet(char *retazec1){
   int vysledok = strlen(retazec1);
    return vysledok;
}
int main(){
    int v;
    char *retazec = (char*) malloc(sizeof(char));
    printf("Zadaj retazec\n");
    scanf("%[^\n]s", retazec);
    v = pocet(retazec);
    printf("\nDlzka retazca je:%d", v);
}

Urobil som to takto ale viem, ze je to zle. Nevedel som urobit funkciu aby som v nej predaval dalsi formalny parameter a to velkost pola.
18.

int dlzka;
   char str[] = "Ucim se ukazatele";
   char *p_zacatek, *p_konec;
   dlzka = strlen(str);
   p_zacatek = &str[0];
   p_konec = &str[dlzka - 1];
   printf("Vypisujem prvy znak %c a posledny znak retazca %c ", *p_zacatek, *p_konec);

19.

#define PI 3.1415927
float *vypocet(float r);


void main() {
 float *a, r;
 printf("Zadaj polomer v cm: ");
 scanf("%f", &r);
 a = vypocet(r);
 printf("Obvod je %.2f a obsah je %.2f", *a, *(a + 1));

}

float *vypocet(float r) {
  float *b = (float*) malloc(2 * sizeof(float));
  float o, s;
  o =  PI * r * r;
  s =  2 * PI * r;
  *b = o;
  *(b + 1) = s;
  return (b);

  free(b);
}

20.

#define VELKOST 17

char *zmen(char c_znak, char c_znak2);

int main() {
  char *a, c_znak, c_znak2;
//  a = (char*) malloc(sizeof(char));
  printf("Zadaj znak, ktory chces zmenit\n");
  scanf(" %c", &c_znak2);
  printf("Zadaj novy znak\n");
  scanf(" %c", &c_znak);
  a = zmen(c_znak, c_znak2);
  printf("Retazec po zmene: %s\n", a);

  return 0;
  // free (a);
  // a = NULL;
}


char *zmen(char c_novy, char c_stari) {
  static char str[] = "Ucim se ukazatele";
  int i, skuska = 0;
  printf("Retazec pred zmenou: %s\n", str);

  for (i = 0; i < VELKOST; i++){
      if (str[i] == c_stari){
          str[i] = c_novy;
          skuska++;
      }
  }

  if (skuska == 0){
      printf("Zadal si znak, ktory sa nenachadza v retazci, nenastala zmena\n");
  }

   return (str);
}
 
Odpovědět
18. února 18:29
Tento výukový obsah pomáhají rozvíjet následující firmy, které dost možná hledají právě tebe!
Avatar
DarkCoder
Člen
Avatar
Odpovídá na Jozef Vendel
DarkCoder:19. února 5:43

Zde je vyhodnocení toho, jak si se vypořádal s otázkami a příklady k ukazatelům v C.
Ke každé otázce doplním i svůj komentář a řešení, aby si měl zpětnou vazbu.

1. Pro práci s ukazateli se používají dva speciální operátory (* a &). Operátor * má dva významy. První význam je ten, že se používá pro deklarování ukazatelové proměnné. Druhý význam je, že vrací hodnotu uloženou na adrese před kterou stojí. Tento operátor se nazývá operátor dereference nebo operátor indirekce. Druhý operátor & slouží pro získání adresy proměnné před kterou stojí. Tento operátor se nazývá operátor reference nebo adresový operátor.

Ukázkový příklad:

int i = 10, *pi = &i; // ukazatel pi je inicializovan adresou i
printf("%d", *pi); // vypis hodnotu i pomoci ukazatele

Operátor & lze vyjádřit slovy "adresa něčeho".
Operátor * lze vyjádřit slovy "na adrese"

Proto můžeš zápisy v příkladu číst následovně:
Přiřaď ukazateli pi adresu proměnné i
Vypiš hodnotu na adrese pi

Je-li hodnota proměnné získána pomocí ukazatele, říkáme, že je získána odkazem (dereferencováním, nepřímo).

Tedy správně si určil první význam operátoru *. Druhý chyběl. Význam operátoru & jsi uvedl správně. Avšak:

cize hodnotu ktoru ma premenna p_i je hodnota, ktoru ma aj premena i a to je 5

Ne. Hodnota pi není shodná s hodnotou i. p_i obsahuje adresu i, kdežto i obsahuje hodnotu 5.
Pomatuj: Ukazatel je proměnná, která obsahuje adresu objektu.

2. Inicializace ukazatelové proměnné na typ float, která na nic neukazuje, vypadá následovně:

float *p = NULL;

NULL je makro definované v mnoha hlavičkových souborech (stdio.h, string.h, ..) A je definováno jako:

#define NULL ((void*)0)

K následujícímu příkazu:

pf = NULL;

Toto není inicializace ale přiřazovací příkaz. Inicializace znamená přiřazení v definici.
Na dynamickou alokaci nebyly otázky ani cvičení mířeny, nikde není třeba.
Zápis který si uvedl je syntakticky správně, ovšem sémanticky chybně. Za prvé by bylo třeba kontrolovat, zda-li se podařilo alokovat paměť. Za druhé tuto paměť neuvolňuješ pomocí funkce free(), čímž vytváříš memory leak a za třetí dokonce přepisuješ ukazatel na neplatný a nikdy už se tak k alokované paměti nedostaneš. Tato paměť se uvolní až po skončení programu. Takže na to pozor..

3. V pořádku. Základní principy práce s referenčním a dereferenčním operátorem. Zde jen znovu připomínám. Výraz *p_q lze číst - hodnotu na adrese p_q. Všimni si, jak sufix proměnné napomáhá čtení. Pak lze číst výraz i takto - hodnotu q pomocí ukazatele.

4. Úryvek kódu pochopitelně není správný. Neboť ukazatel p neukazuje na žádný známý objekt. Proto je pokus o nepřímé přiřazení hodnoty pomocí p nesmyslný a nebezpečný.

5. Další úryvek kódu který není správný. Příklad ukazuje, proč je důležitý základní typ ukazatele a proč je třeba zajistit, aby byl typ ukazatele shodný s typem objektu na který ukazuje. Při přiřazení:

q = *p;

nedojde ke zkopírování čísla, neboť se přenesou pouze sizeof(int) bytů. Jelikož je p ukazatel na int, nelze jej použít pro přenos hodnoty odpovídající velikosti typu double. Toto je důvod, proč program nebude pracovat správně.

Obvykle míchání odlišných typů levé a pravé strany není vhodné. Někdy však to může být vhodné, jelikož jedním z pravidel je pravidlo při přiřazení, které říká, že typ pravé strany se převádí na typ levé strany.

6. Ano, všechny tyto operátory lze použít pro práci s ukazateli. Pouze doplním, že k nim samozřejmě patří i operátory * a & a také -> přístup k členům a [] přístup k prvkům.

7. Hodnota ukazatelové proměnné je adresa objektu na který ukazatel ukazuje. Tedy správná odpověď je že ano. Pokud ukazatel p bude obsahovat adresu 100, pak příkaz

p = p + 4;

Bude podle ukazatelové aritmetiky ukazovat na 4tý prvek za prvkem na, který ukazoval. Je-li tedy ukazatel p ukazatelem na int, pak adresa, na kterou bude ukazovat bude:

p = p + 4 * sizeof(int); // 100 + 4 * sizeof(int)

Tedy v p bude uložena hodnota 116 (ve 32bit prostředí, kde sizeof(int) == 4).
Velikost typu se určuje zásadně pomocí operátoru sizeof.

8. Je to správně. Snad jen že jazyk C má pro zvyšování o 1 speciální operátor inkrementace ++. Tedy příkaz pro zvyšování proměnné o 1 pomocí ukazatele by vypadal následovně:

(*p_q)++;

Smyslem tohoto příkladu bylo si uvědomit, jaký je rozdíl mezi

(*p_q)++;
// a
*p_q++;

V prvním případě se inkrementuje hodnota prvku, ve druhém případě se inkrementuje nejprve ukazatel a poté se bere hodnota na nové adrese. Příklad ukazuje důležitost použití závorek z důvodu priorit operátorů.

9. V pořádku. Správně si použil bázovou adresu pole a posun. Výsledkem součtu ukazatele a celého čísla je opět ukazatel. Výrazy *(p + i) a p[i] vyjadřují totéž. Výraz, kde je použita pointerová aritmetika je o něco efektivnější. Nicméně moderní kompilátory nahrazují indexaci pole právě adekvátním výrazem pointerové aritmetiky.

Ještě jedna drobnost. Příkaz:

p_q = &pole[0];

// je ekvivalentni prikazu

p_q = pole;

Často se vyskytuje a zapisuje ukazatel na pole druhou formou.
Pokud je vytvářen ukazatel na pole, nepíše se &. Jméno pole adresa na začátek pole.

10. Jde. Následující zápisy výpisu třetího prvku pole jsou totožné

int  pole[] = {1, 2, 3, 4, 5};
int p = pole;

printf("%d", *(pi + 2)); // ukazatelova aritmetika ukazatele
printf("%d", *(pole + 2)); // ukazatelova aritmetika pole
printf("%d", pole[2]); // indexace pole
printf("%d", pi[2]); // indexace ukazatele

Indexovat ukazatel nejspíš dělat nebudeš, neboť ukazatelová aritmetika je vhodnější.

11. Ano. Je třeba mít stále v paměti souvislost mezi *(p + i) a p[i].

12. Jméno pole bez indexu představuje ukazatel na začátek pole. Oproti klasickému ukazateli se liší v tom, že ukazatel na začátek pole je konstantní. Nelze jej tedy měnit. Pokud tedy chceme traversovat pole pomocí ukazatele, přiřadí se ukazatel na toto pole jinému ukazateli, který již lze modifikovat.

Pokud tedy máme pole

int  pole[] = {1, 2, 3, 4, 5};
int p = pole;

pak v programu je:

pole++; // neplatný příkaz
p++; platný příkaz

13. Funkci lze předat argument hodnotou nebo odkazem. Při volání hodnotou se předává kopie proměnné což má za následek, že změna proměnné se neprojeví vně funkce. Pokud se ovšem provádí volání odkazem, předává se adresa na tuto proměnnou a změny se pak projeví vně funkce. Otázkou může být, kdy použít volání hodnotou a kdy volání odkazem. Pomůcka je jednoduchá - volání odkazem, tedy kdy funkce přebírá ukazatel, se vytváří tehdy, chceme-li měnit hodnotu argumentu předaného jako parametr funkce.

Následující příklad ukazuje tuto problematiku:

#include <stdio.h>

void func1(int i);
void func2(int* i);

int main(void) {
        int val = 0;
        func1(val);
        printf("vne func1 %d\n", val);
        func2(&val);
        printf("vne func2 %d\n", val);
        return 0;
}

void func1(int i) {
        i = 10;
        printf("uvnitr func1 %d\n", i);
}

void func2(int *i) {
        *i = 10;
        printf("uvnitr func2 %d\n", *i);
}

Tedy ukazatel jako argument funkce použijeme tehdy, chceme-li, aby se změna proměnné promítla i mimo funkci nikoli jen uvnitř funkce.

14. Deklarace pětiprvkového pole ukazatelů na int vypadá následovně:

int *p[5];

Kdežto deklarace ukazatele na pětiprvkové pole typu int vypadá následovně:

int (*p)[5];

Smyslem příkladu bylo opět ukázat, jak důležité jsou závorky při práci s ukazateli. Jak priorita operátorů mění smysl. Toto bývá častá chyba.

Opět zde není důvod používat dynamickou alokaci. Navíc druhý zápis je chybný (neberu v potaz přepsání se v přetypování na int*. Funkce malloc() vrací ukazatel na void a návratovou hodnotu je třeba přiřadit ukazatelové proměnné nikoli klasické proměnné.

15. Ano, úryvek kódu je správný. Správně jsi zodpověděl, že ukazatel je inicializován adresou řetězcové konstanty. Funkce printf() přebírá jako první argument řetězec a je zcela v pořádku, pokud je tímto argumentem ukazatel na řetězec. Důležité je neměnit tento ukazatel, jinak se ztratí přístup k tomuto řetězci. Ano, nulový znak je přidáván na konec automaticky.

16. Tuto otázku si zodpověděl správně a dodal si i informaci o nutnosti předávání informace o velikosti pole a to, že není nutné předávat pole od začátku ale od ukazatele který je vytvořen s posunem. Pouze upřesním informaci o předávání velikosti pole. Toto není třeba u polí které jsou řetězci. Důvodem je to, že řetězec je ukončen vždy nulovým znakem. tedy lze určit, kdy řetězec končí.

17. Zde jsem nespecifikoval zadání úplně přesně a došlo tak nedorozumění v tom, co jsem požadoval a co jsi vypracoval. Pokud bych totiž chtěl pouze délku řetězce, bylo by neefektivní tuto funkci vytvářet a zaobalovat tak již existující funkci strlen(), která již toto dělá. Úkolem bylo vytvořit funkci, která převezme řetězec zadaný jako argument funkce spolu se znakem zadaným jako druhý argument. Funkce tedy bude mít dva parametry a bude vracet počet nalezených znaků v řetězci. Druhý parametr určuje právě hledaný znak. Abych Tě o tuto úlohu neochudil, dostaneš ji za úkol zpracovat v tomto upřesněném zadání. Opět, uvědom si, kdy je třeba předávat funkci argument odkazem a kdy hodnotou a jak je to s určení velikosti řetězcových polí. A opravdu není třeba použití dynamické alokace.

18. Je to správně. Pouze menší efektivita a zbytečné použití pomocné proměnné. Ale to není někdy na škodu. Následující úryvek kódu ukazuje, jak to lze napsat lépe. Ukazatele p_zacatek a p_konec jsou inicializovány přímo.

char str[] = "Ucim se ukazatele";
char *p_zacatek = str;
char *p_konec = str + strlen(str) - 1;

Je třeba mít stále na paměti, že ukazatel + celočíselná konstanta dává ukazatel.

19. Smyslem úlohy bylo uvědomit si, že vracet hodnoty z funkce lze i přes parametry ne jen přes návratovou hodnotu funkce. Opět, pokud je předáván ukazatel, lze měnit hodnoty argumentů a změny se projeví vně funkce. Stačila mi funkce a nebylo třeba v tom hledat složitosti. Využije se zde přístup do proměnné nepřímo pomocí ukazatele. Funkce vypadá následovně:

void vypocet(double r, double* o, double* s) {
        *o = 2 * PI * r;
        *s = PI * r * r;
}

celý program pak například takto:

#define PI 3.141592653
#include <stdio.h>

void vypocet(double r, double *o, double *s);

int main(void) {
        double obvod, obsah;

        vypocet(10.0, &obvod, &obsah);
        printf("obvod=%f\nobsah=%f\n", obvod, obsah);
        return 0;
}

void vypocet(double r, double* o, double* s) {
        *o = 2 * PI * r;
        *s = PI * r * r;
}

Způsobů, jak vracet vícero hodnot z funkce je více. Lze využít pole popř. struktury. Zde šlo pouze o princip a to, že je třeba předat ukazatel na proměnnou.

Ještě malá odbočka k dynamické alokaci. Funkce free() pro uvolnění alokované paměti se nikdy nezavolá, jelikož funkce je ukončena dříve pomocí příkazu return. Často se způsob alokace ve funkci využívá spolu s tím, že paměť je uvolněna později někde v programu. Takové funkce pak vrací ukazatel aby se mohla tato paměť později, když je třeba, uvolnit.

20. Nejtěžší na konec. Nic však, co by se nedalo vyřešit. Pořád je to o tom, kdy je třeba argument předat voláním odkazem nebo hodnotou. Tedy zda se bude měnit či nikoli. Aby funkce byla soběstačná, je třeba, aby byl řetězec předáván jako argument, spolu s oběma znaky. První je třeba mít představu o tom jak bude vypadat takový prototyp funkce. Mohl by vypadat např. následovně:

char *zamena_znaku(char *str, char c_puvodni, char c_novy);

A celý program pak např. takto:

#include <stdio.h>

char *zamena_znaku(char *str, char c_puvodni, char c_novy);

int main(void) {
        char text[] = "Ucim se ukazatele";
        printf(zamena_znaku(text, 'e', 'x'));

        return 0;
}

char* zamena_znaku(char* str, char c_puvodni, char c_novy) {
        char* s = str;
        while (*str) {
                if (*str == c_puvodni) *str = c_novy;
                str++;
        }
        return s;
}

Pole se vždy předává pomocí ukazatele. Zbylé dva znaky není třeba předávat voláním odkazem. Ukazatel s uvnitř funkce slouží pro uchování adresy začátku pole. Postupně procházím všechny znaky a testuji, zda znak je původním. Pokud ano, přepíšu ho novým. Pro ukončení cyklu se využívá faktu, že řetězec končí nulovou hodnotou (nepravdivou). Podmínka ve while cyklu zjišťuje, zda-li znak který testuji není znakem, který ukončuje řetězec. Aby se prošly všechny znaky, je třeba inkrementace ukazatele. Cyklus while má na starosti změny. A abych předal zpět tento upravený řetězec, využívám lokalní ukazatel s, který mi uchovává adresu začátku tohoto pole. Tento ukazatel s je nutný, neboť pro traversování pole používám ukazatel. Pokud bych používal konstantu posunu, měl bych uchovaný začátek tohoto pole stále v ukazateli str. Nakonec, co stojí za povšimnutí je to jak užitečné je vracet ukazatel. Návratovou hodnotu funkce, tedy ukazatel na tento řetězec mohu použít jako argument funkce printf(). Díky tomu mám definici řetězce, provedení změn a výpisu pole na pouhých dvou řádcích.

Tolik k tvému zpracování otázek a cvičení na téma - Základy ukazatelů v C. Doufám, že Ti vše doplněné o předchozí výklad více přiblížilo pochopení práce s ukazateli a porozuměl si všemu, co si dělal chybně. Pokud Ti cokoli z toho co jsem napsal není jasné, ptej se. A nezapomeň, úloha 17. na tebe stále čeká k vyřešení :-)

Odpovědět
19. února 5:43
"„Učíš-li se proto, aby sis zapamatoval, zapomeneš. Učíš-li se proto, abys porozuměl, zapamatuješ si."
Avatar
Jozef Vendel
Člen
Avatar
Odpovídá na DarkCoder
Jozef Vendel:19. února 11:19

Dakujem velmi pekne za snahu a ochotu. Idem si to prestudovat, zlepsovat si vedomosti a zrucnosti v tejto teme.

 
Odpovědět
19. února 11:19
Avatar
Jozef Vendel
Člen
Avatar
Odpovídá na DarkCoder
Jozef Vendel:19. února 11:23

Inac myslim si, ze by si mohol kludne tento 20 otazkovy test k ukazovatelom urobit ako 12. cvicenie k tomuto kurzu, ktore by sa nieslo v duchu, ze precvic si ako si pochopil ukazovatelom.­..nieco v tom zmysle ak ma chapes, co by bolo fajn lebo ukazovatelia su dost zlozita tema a treba sa jej venovat a taketo precvicenie je urcite vhodne.

 
Odpovědět
19. února 11:23
Avatar
Jozef Vendel
Člen
Avatar
Odpovídá na DarkCoder
Jozef Vendel:19. února 15:21
#include <stdio.h>
#include <stdlib.h>

int spocitaj(char *retazec, char znak){

    retazec = (char*) malloc(20 * sizeof(char));
    int a = 0;
    printf("Zadaj rezatec\n");
    scanf("%[^\n]", retazec);
    printf("Zadaj skumany znak\n");
    scanf(" %c", &znak);

     while (*retazec) {
                if (*retazec == znak) a++;
                retazec++;
            }

   // free(retazec);
    return a;
}

int main(){
    char *str, c;

   printf("Pocet znakov v retazci je %d", spocitaj(str, c));

}

Viem, ze si hovoril ze sa to da aj bez dynamickej alokacie, ale chcel som robit s retazcom zadanym od uzivatela.
Mam na teba 2 otazky. Prva je, ze kde v tomto kode by som mal pouzit funkciu free(retazec) a druha je, ze chcel by som alokovat pomocou funkcie malloc presny pocet B pre retazec zadany uzivatelom, ked rozmyslam mohol by som to reisit tak, ze by som si vytvoril pomocne staticke pole char str[128], tam by som si ulozil vstupny retazec, zistil jeho velkost a podla toho uz alokoval konkretny pocet B. Bola by to tiez moznost nie ?

 
Odpovědět
19. února 15:21
Avatar
DarkCoder
Člen
Avatar
Odpovídá na Jozef Vendel
DarkCoder:19. února 15:49

Příliš přezskakuješ. Funkce free() se volá tehdy, když se ví, že už se nebude s alokovanou pamětí pracovat. Bylo by to za while cyklem ale před return. Pokud chceš pracovat s řetězcem zadaným z klávesnice, musíš se rozhodnout, kam načítací sekci umístíš. Pokud mimo funkci, pak funkci zůstanou parametry, pok dovnitř, pak je funkce nebude mít, ale pak vše musíš ošetřit uvnitř funkce. Pro načítání řetězců používej funkci fgets() s ukazatelem na soubor stdin. Aby se vyhradil přesný počet bytů pro řetězec, je třeba alokované pole zmenšovat či zvětšovat dle potřeby během načítání znaků. Jde to ale je to dosti komplikovaným proces. Dále pokud měníš ukazatel předány jako argument funkce, je třeba předávat na něj ukazatel. Tím se dostáváš do vícenásobné dereference, kterou můžeš řešit až tehdy, znáš-li základy ukazatelů. Pokud chceš používat buffer pro načítání řetězce z klávesnice, použij staticky alokované pole. Ale znovu opakuji, postupuj postupně

Odpovědět
19. února 15:49
"„Učíš-li se proto, aby sis zapamatoval, zapomeneš. Učíš-li se proto, abys porozuměl, zapamatuješ si."
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 10 zpráv z 42. Zobrazit vše