Ukazatele #1, struktury BI-PA1 Programování a Algoritmizace 1 Miroslav Baĺık, Ladislav Vagner a Josef Vogel Katedra teoretické informatiky a Katedra softwarového inženýrství Fakulta informačních technologíı České vysoké učení technické v Praze xvagner@fit.cvut.cz, vogeljos@fit.cvut.cz 14., 23. a 24. listopadu 2017
Přehled Datové typy ukazatel. Ukazatele reference, dereference. Struktura jako datový typ. Pamět ová reprezentace struktury. Pole struktur, ukazatele na struktury. M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 2/35
Ukazatele motivace Chceme pracovat s daty uvnitř nějaké funkce. Data si do funkce můžeme předat zkopírováním: void foo ( int x )... int data =...; foo ( data ); funkce má k dispozici vlastní proměnnou x, tato proměnná obsahuje kopii hodnoty z proměnné data, proměnná data se jednorázově zkopíruje do x, od tohoto okamžiku nemají proměnné nic společného, funkce foo může hodnotu proměnné x zničit (přepsat), tím nezmění data. Kopírování proměnné nemusí být vždy výhodné. M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 3/35
Ukazatele motivace Kdy je kopírování nevýhodné: kopírování může trvat dlouho, pokud je proměnná velká. Kopírování proměnné typu int je velmi rychlé, režie kopírování by se uplatnila pro proměnné od velikosti řádově stovek bajtů, změny v proměnné x uvnitř foo nejsou vidět ven, toto požadujeme např. u výstupních parametrů. Místo kopie hodnoty proměnné bychom mohli funkci předat informaci o umístění proměnné, se kterou má pracovat. Informace o umístění proměnné je její adresa v paměti. void foo ( int * x )... int data =...; foo ( &data ); M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 4/35
Ukazatele Ukazatele abstrakce adres ve vyšších programovacích jazycích. Hodnota ukazatele na typ T reprezentuje adresu v paměti, kde je hodnota typu T uložena. Deklarace ukazatele: T * ptr Proměnná ptr reprezentuje adresu. Stejně jako u jiných (lokálních) proměnných, je počáteční hodnota nedefinována. Proměnná může být inicializována adresou: T x; /* proměnná typu T, kdekoli deklarovaná */... T * p = & x; /* alternativně mimo deklaraci: */ p = & x; Unární prefixový operátor & se nazývá reference (odkaz). Výsledkem operace &x je ukazatel (adresa) proměnné x. Ukazatel je datového typu T* ukazatel na hodnoty typu T. M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 5/35
Ukazatele reference Vlastní adresa je celé číslo udávající pozici proměnné v paměti (počítáno v bajtech od počátku pamět ového prostoru). Zjednodušeně pokud tuto kombinaci bitů pošle CPU na adresní sběrnici, pamět ový čip zpřístupní odpovídající obsah. Skutečné hodnoty adres jsou jen málokdy důležité: adresy proměnných jsou přiděleny kompilátorem, adresy proměnných se mohou lišit, závisí na kompilátoru, OS, nebo na okamžitém využití paměti. Je obvyklé označovat adresy šipkou vedoucí od ukazatele k referencované proměnné. int x = 10, *iptr = &x; char c = a, *cptr; cptr = &c; x iptr c cptr 10 'a' M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 6/35
Ukazatele dereference Pro přístup k proměnné, jež je referencována ukazatelem, musí být použit operátor dereference *. Když ptr je ukazatel typu T*, pak výraz *ptr označuje hodnotu referencovanou tímto ukazatelem ptr. int x, *iptr = &x; char c, *cptr; cptr = &c; *iptr = 20; *cptr = Z ; x iptr c cptr 20 'Z' M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 7/35
Ukazatele kompatibilita Ukazatele stejného typu je možno přiřazovat: int x, *p1 = &x, *p2; p2 = p1; /* p1 a p2 nyní referencují proměnnou - x */ *p2 = 10; /* x má nyní hodnotu 10 */ Když přiřazujeme adresy různých typů (např.. T1* a T2*), kompilátor hlásí varovnou zprávu, ale kód je vytvořen. Výsledek dereferencování takové adresy je podivný (špatná interpretace hodnoty, data přepsána,... ). M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 8/35
Ukazatele kompatibilita int x, *iptr; char c, *cptr; iptr = &c; /*!!! */ cptr = &x; /*!!! */ *iptr = 0; /* Čtyři bajty od adresy proměnné c jsou naplněny nulami. Možná je nějaká proměnná přepsána (třeba zrovna adresa v cptr). */ *cptr = 0; /* První bajt proměnné x je vynulován. Zbylé tři bajty jsou nezměněny. Hodnota proměnné x nemusí být 0. */ M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 9/35
Ukazatele poznámka k pořadí bajtů v paměti Jak je uložena např. hodnota x = 305419896 = 0x12345678? Hodnota (32 bitů) bude zaujímat 4 bajty paměti: little-endian (Intel): 0x78 0x56 0x34 0x12 big-endian (Motorola, SPARC): 0x12 0x34 0x56 0x78 bi-endian (can switch in boot time): ARM, PowerPC, MIPS, IA64 Efekt posledního příkladu (*cptr=0;) velmi závisí na architektuře procesoru. Vynulován bude nejnižší nebo nejvyšší bajt proměnné x. Tedy hodnota se změní bud na 0x00345678 = 3430008, nebo na 0x12345600 = 305419776. M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 10/35
Ukazatele ukazatel NULL Ukazatel NULL speciální hodnota adresy, která nic nereferencuje. Hodnota NULL ukazatele je používána pro signalizaci speciálních případů (např. operace selhala). V jazyce C je hodnota NULL definována: #define NULL ((void*)0) Pokus o dereferenci ukazatele NULL vede k pádu programu: int main(void) { int *p = NULL; *p = 10; /* zde spadne */ printf ( "Hello world!\n" ); return 0; M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 11/35
Ukazatele (de)motivace Za obrázek děkujeme Randallu Munroe, XKCD comics #138 (xkcd.com). M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 12/35
Ukazatele (de)motivace Za obrázek děkujeme Randallu Munroe, XKCD comics #138 (xkcd.com). Pointers will be back next week! M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 12/35
Struktury motivace Problém: přečíst jméno studenta, příjmení studenta a jeho průměrný prospěch, vstupní data jsou na std. vstupu, vypsat studenta s nejlepším průměrem. Příklad: John Smith 2.45 George Peterson 1.32 Peter O Brien 1.93 Michael Corleone 2.21 Výsledek: George Peterson 1.32 M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 13/35
Struktury motivace Nástin řešení: inicializace proměnné(ých) nejlepší_student while ( další student přečten ) porovnání do ted nejlepšího průměru s průměrem právě přečteného if je lepší then uložení jména, příjmení a průměru do nejlepší_student výpis nejlepšího_studenta M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 14/35
Struktury motivace #define NAME_MAX 32 int main ( void ) { char curname[name_max], cursurname[name_max]; char bestname[name_max], bestsurname[name_max]; double bestavg = 5, curavg; while ( scanf ( "%31s%31s%lf", curname, cursurname, &curavg ) == 3 ) { printf ( "%s %s: %f\n", curname, cursurname, curavg ); if ( curavg < bestavg ) { /* ok, přiřadit */ bestavg = curavg; strncpy ( bestname, curname, sizeof(bestname) ); strncpy ( bestsurname, cursurname, sizeof(bestsurname) ); printf ( "Nejlepsi prumer:\n" ); printf ( "%s %s: %f\n", bestname, bestsurname, bestavg ); return 0; M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 15/35
Struktury Nevýhody řešení: příliš mnoho proměnných, související proměnné jsou jen slabě propojeny (podobné pojmenování, případně stejný index v poĺıch), když přidáme novou informaci (třeba datum narození), bude třeba program modifikovat na mnoha místech. Lepší řešení použití struktury. Struktura je složený datový typ, který spojuje související informace (proměnné) do jedné větší jednotky. Struktury lze přirovnat k funkcím: funkce spojují související akce (výpočty) do jedné větší jednotky, struktury spojují související data (proměnné) do jedné větší jednotky. M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 16/35
Struktury #define NAME_MAX 32 struct TStudent { /* jméno datového typu */ char name[name_max]; /* složky (položky struktury) */ char surname[name_max]; double avg; ; int main ( void ) { struct TStudent cur, best; best. avg = 5; /* přístup ke složce */... M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 17/35
Struktury struct TStudent {... ; int main ( void ) { struct TStudent cur, best; best.avg = 5; while ( scanf ( "%31s%31s%lf", cur.name, cur.surname, &cur.avg ) == 3 ) { printf ( "%s %s: %f\n", cur.name, cur.surname, cur.avg ); if ( cur.avg < best.avg ) best = cur; /* ok, přiřadit */ printf ( "Nejlepsi prumer:\n" ); printf ( "%s %s: %f\n", best.name, best.surname, best.avg ); return 0; M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 18/35
Struktury Jméno typu struct TStudent je poněkud zdlouhavé. Je možné používat kratší jméno, když definici struktury modifikujeme: typedef struct TStudent { /* typedef deklaruje */ char name[name_max]; /* nové jméno typu */ char surname[name_max]; double avg; TSTUDENT; /* TSTUDENT je nové, jednoduché jméno typu struct TStudent */ int main ( void ) { TSTUDENT cur, best; /* jméno typu */... /* bez klíčového slova struct */ M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 19/35
Struktury deklarace detailněji struct TStudent { char name[name_max]; char surname[name_max]; double avg; foo; /* struct TStudent je jméno datového typu: */ struct TStudent a; /* a je proměnná */ /* foo je proměnná typu struct TStudent: */ printf ( "%s\n", foo. name ); M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 20/35
Struktury deklarace detailněji typedef struct TStudent { char name[name_max]; char surname[name_max]; double avg; TSTUDENT; /* struct TStudent je jméno datového typu: */ struct TStudent a; /* a je proměnná */ /* TSTUDENT je jméno datového typu: */ TSTUDENT b; /* b je proměnná */ a = b; /* stejný datový typ, tedy lze přiřazovat */ M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 21/35
Struktury deklarace detailněji typedef struct { char name[name_max]; char surname[name_max]; double avg; TSTUDENT; typedef struct { char name[name_max]; char surname[name_max]; double avg; XSTUDENT; TSTUDENT a; XSTUDENT b; a = b; /* nelze přiřadit, odlišný datový typ */ M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 22/35
Struktury reprezentace v paměti #define NAME_MAX 32 typedef struct TStudent { char name[name_max]; char surname[name_max]; double avg; TSTUDENT; TSTUDENT student = {"John", "Smith", 1.35 ; /* init */ +0 'J' 'o' 'h' 'n' '\0'... +32 'S' 'm' 'i' 't' 'h' '\0'... +64 1.35 (as a double) +72 size of the structure M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 23/35
Struktury reprezentace v paměti #define NAME_MAX 32 typedef struct TStudent { char name[name_max]; char surname[name_max]; double avg; TSTUDENT; int main ( void ) { TSTUDENT x; printf ( "name: %d\n", (int)((char*)&x.name - (char*)&x) ); printf ( "surname: %d\n", (int)((char*)&x.surname - (char*)&x) ); printf ( "avg: %d\n", (int)((char*)&x.avg - (char*)&x) ); printf ( "sizeof: %d\n", (int)sizeof ( x ) ); return 0; M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 24/35
Struktury reprezentace v paměti Při spuštění program vypíše očekávané hodnoty: 0, 32, 64, a 72. Co se stane, když deklaraci modifikujeme? #define NAME_MAX 33 Modifikovaný program nevypíše: 0, 33, 66, 74, ale místo toho 0, 33, 72, 80. Složka avg má délku 8 bajtů. Nejrychlejší přístup k této položce vyžaduje, aby počáteční adresa byla dělitelná 8. Když není předchozí podmínka splněna, procesor čte položku více cykly sběrnice (Intel, AMD) nebo odmítne položku přečíst vůbec a dojde ke pádu programu (SPARC, MIPS). M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 25/35
Struktury reprezentace v paměti Kompilátor provádí tedy tzv. zarovnání (aligning): položky struktur nebo proměnné o velikosti 2 k bajtů jsou alokovány na adresách začínajících na násobku 2 k, v našem příkladu kompilátor vložil 6 bajtů navíc mezi položky surname a avg, když je program napsán korektně, tak tyto bajty nejsou vůbec používány, kvůli rychlému přístupu je položka avg je zarovnána. Zarovnání proměnných je významně platformně závislé. Přenositelná aplikace nesmí záviset na platformě specifických velikostech/zarovnání proměnných. M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 26/35
Struktury a pole Příklad: Program čte studenty, ukládá je do paměti, řadí je podle průměrného prospěchu a pak je vypíše. Uvedeme funkce, které vedou na lépe přehledné a čitelné řešení: void printstudent ( TSTUDENT s ); int readstudent ( TSTUDENT * ps ); void sortstudents ( TSTUDENT a[], int n ); M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 27/35
Struktury a pole int main( void ) { TSTUDENT students[max]; int cnt = 0, i; while ( cnt < MAX && readstudent ( &students[cnt] ) ) cnt ++; sortstudents ( students, cnt ); for ( i = 0; i < cnt; i ++ ) printstudent ( students[i] ); return 0; M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 28/35
Struktury a pole Seřazení pole algoritmem select sort: V poli nalezneme nejmenší prvek a prohodíme jej s prvkem na indexu 0. Ve zbytku pole (od indexu 1) nalezneme nejmenší prvek a prohodíme jej s prvkem na indexu 1. To samé se zbytku počínaje indexy 2, 3,.... Výsledkem je seřazené pole. M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 29/35
Struktury a pole void sortstudents ( TSTUDENT a[], int n ) { int i, j, imin; for ( i = 0; i < n - 1; i ++ ) { imin = i; /* umístění nejmenšího prvku */ for ( j = i + 1; j < n; j ++ ) if ( a[j].avg < a[imin].avg ) imin = j; /* na pozici j je menší */ if ( imin!= i ) { TSTUDENT tmp = a[imin]; /* prohození obsahu */ a[imin] = a[i]; /* všech složek zároveň */ a[i] = tmp M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 30/35
Struktury a pole void printstudent ( TSTUDENT s ) { printf ( "%s %s %f\n", s.name, s.surname, s.avg ); int readstudent ( TSTUDENT * ps ) { /* ps je výstupní parametr, předáme jej jako ukazatel */ TSTUDENT s; if ( scanf ( "%31s%31s%lf", s.name, s.surname, &s.avg )!= 3 ) return 0; *ps = s; return 1; M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 31/35
Struktury a pole void printstudent ( TSTUDENT s ) { printf ( "%s %s %f\n", s.name, s.surname, s.avg ); int readstudent ( TSTUDENT * ps ) { /* ps je výstupní parametr, předáme jej jako ukazatel */ TSTUDENT s; if ( scanf ( "%31s%31s%lf", s.name, s.surname, &s.avg )!= 3 ) return 0; *ps = s; return 1; Struktura TSTUDENT je dost velká. Vyplatí se její kopírování při každém volání printstudent? M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 31/35
Struktury a pole void printstudent ( TSTUDENT s ) { printf ( "%s %s %f\n", s.name, s.surname, s.avg ); int readstudent ( TSTUDENT * ps ) { /* ps je výstupní parametr, předáme jej jako ukazatel */ TSTUDENT s; if ( scanf ( "%31s%31s%lf", s.name, s.surname, &s.avg )!= 3 ) return 0; *ps = s; return 1; Struktura TSTUDENT je dost velká. Vyplatí se její kopírování při každém volání printstudent? Je potřeba při načítání zapsat data do lokální proměnné a pak je zkopírovat do parametru? Nešlo by to přímo? M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 31/35
Struktury a pole void printstudent ( TSTUDENT * s ) { /* s je dost velký (~100 B) vstupní parametr, který funkce * pouze čte. Proto jej z výkonnostních důvodů předáme * odkazem (jako ukazatel). Volající použije: * printstudent( &students[i] ); */ printf ( "%s %s %f\n", (*s).name, (*s).surname, (*s).avg ); int readstudent ( TSTUDENT * s ) { /* s je výstupní parametr, předáme jej jako ukazatel */ if ( scanf ( "%31s%31s%lf", (*s).name, (*s).surname, &(*s).avg )!= 3 ) return 0; return 1; M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 32/35
Struktury a pole void printstudent ( TSTUDENT * s ) { /* s je dost velký (~100 B) vstupní parametr, který funkce * pouze čte. Proto jej z výkonnostních důvodů předáme * odkazem (jako ukazatel). Volající použije: * printstudent( &students[i] ); */ printf ( "%s %s %f\n", (*s).name, (*s).surname, (*s).avg ); int readstudent ( TSTUDENT * s ) { /* s je výstupní parametr, předáme jej jako ukazatel */ if ( scanf ( "%31s%31s%lf", (*s).name, (*s).surname, &(*s).avg )!= 3 ) return 0; return 1; Zápis (*s).name je zdlouhavý a nepřehledný. Lze jej nahradit operátorem ->. M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 32/35
Ukazatele a struktury void printstudent ( TSTUDENT * s ) { /* s je dost velký (~100 B) vstupní parametr, který funkce * pouze čte. Proto jej z výkonnostních důvodů předáme * odkazem (jako ukazatel). Volající použije: * printstudent( &students[i] ); */ printf ( "%s %s %f\n", s->name, s->surname, s->avg ); int readstudent ( TSTUDENT * s ) { /* s je výstupní parametr, předáme jej jako ukazatel */ if ( scanf ( "%31s%31s%lf", s->name, s->surname, &s->avg )!= 3 ) return 0; return 1; M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 33/35
Ukazatele a struktury Při práci s ukazateli na strukturu často potřebujeme ukazatel dereferencovat a následně zpřístupnit složku struktury. Např.: TSTUDENT *p;... (*p).name... Tuto operaci lze nahradit jediným operátorem šipka. Kód se lépe píše a je přehlednější: TSTUDENT *p;... p->name... Následující zápisy jsou rovnocenné: TSTUDENT *p; TSTUDENT s;... s. name <=> (&s) -> name p -> name <=> (*p). name M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 34/35
Otázky a odpovědi Otázky... M. Baĺık, L. Vagner a J. Vogel, ČVUT FIT Ukazatele, BI-PA1 35/35