Wprowadzenie do programowania w języku C Część szósta Zmienne wskaźnikowe koncepcja, zastosowania, sztuczki i triki Autor Roman Simiński Kontakt siminski@us.edu.pl www.us.edu.pl/~siminski Niniejsze opracowanie zawiera skrót treści wykładu, lektura tych materiałów nie zastąpi uważnego w nim uczestnictwa. Opracowanie to jest chronione prawem autorskim. Wykorzystywanie jakiegokolwiek fragmentu w celach innych niż nauka własna jest nielegalne. Dystrybuowanie tego opracowania lub jakiejkolwiek jego części oraz wykorzystywanie zarobkowe bez zgody autora jest zabronione.
Zanim poznamy zmienne wskaźnikowe... Co to jest zmienna? Zmienna jest obiektem w programie przeznaczonym do przechowywania wartości. int i; i = 10; Pamięć operacyjna i Dziwne pojęcia l-wartość i r-wartość 10 Każda zmienna ma swoją nazwę, oraz typ wartości. Zmienne są przechowywane w pamięci operacyjnej, liczba zajętych bajtów zależy od typu zmiennej. Nazwa zmiennej identyfikuje zmienną w programie zwalniając programistę od zastanawiania się, pod jakim adresem w pamięci zmienna jest zlokalizowana. int i; int j; j = 5; i = j; 5 = i; j to l-wartość 5 to r-wartość i to l-wartość j to r-wartość Zmienna może występować po lewej stronie operatora przypisania, mówi się, że jest wtedy l-wartością. Wszystko co może występować po prawej stronie operatora przypisania jest r-wartością. Nie każda r-wartość to l-wartość Obiekt jest nazwanym obszarem pamięci. l-wartość jest wyrażeniem lokalizującym ten obiekt w pamięci Copyright Roman Simiński Strona : 2
Zanim poznamy zmienne wskaźnikowe... Motywacja W języku C intensywnie wykorzystuje się l-wartości oparte na zmiennych wskaźnikowych oraz na wyrażeniach te zmienne zawierających. Dokładne opanowanie zasad posługiwania się wskaźnikami jest niezbędne do efektywnego i sprawnego programowania w C i C++. Tej umiejętności nie można pominąć, przeskoczyć lub zostawić na później. Nie oszukujmy się ten, kto nie opanuje zasad posługiwania się wskaźnikami nigdy nie będzie prawdziwym, profesjonalnym programistą wykorzystującym język C lub C++. Copyright Roman Simiński Strona : 3
Zmienne wskaźnikowe koncepcja Po co są zmienne wskaźnikowe? Zmienna wskaźnikowa przeznaczona jest do lokalizowania (wskazywania) obiektów w pamięci operacyjnej. Jedyną rolą zmiennej wskaźnikowej jest umożliwienie odwoływania się do obiektów wskazywanych. Naiwna interpretacja graficzna Zmienna wskaźnikowa Obiekt wskazywany Zmienne wskaźnikowe też żyją w pamięci operacyjnej Zmienna wskaźnikowa Obiekt wskazywany Copyright Roman Simiński Strona : 4
Zmienne wskaźnikowe koncepcja Trzy stany zmiennej wskaźnikowej Zmienna wskaźnikowa wskazuje na konkretny obiekt w pamięci Zmienna wskaźnikowa Obiekt wskazywany OK Zmienna wskaźnikowa nie wskazuje na żaden obiekt Zmienna wskaźnikowa OK Zmienna wskaźnikowa wskazuje na nie wiadomo co Zmienna wskaźnikowa? Kiepsko Copyright Roman Simiński Strona : 5
Zmienne wskaźnikowe koncepcja Co zawiera zmienna wskaźnikowa? Zwykle przyjmuje się, że zmienna wskaźnikowa zawiera w sobie adres obiektu wskazywanego. Zmienna wskaźnikowa 345fa012h Obiekt wskazywany Adres: 345fa012h Jednak zmienna wskaźnikowa nie musi w sobie zawierać adresu bezpośredniego, jej zawartość może, w pewnej realizacji kompilatora, zawierać inną informację, pozwalającą na precyzyjne i jednoznaczne zidentyfikowanie położenia obiektu w pamięci. Przykład W 16-to bitowych realizacjach kompilatorów firmy Borland: zmienna wskaźnikowa zawiera przesunięcie (ang. offset) obiektu względem początku segmentu gdy wskaźniki są krótkie (odwołania wewnątrz segmentu), zmienna wskaźnikowa zawiera adres segmentu i przesunięcie obiektu gdy wskaźniki są długie (odwołania międzysegmentowe). Copyright Roman Simiński Strona : 6
Zmienne wskaźnikowe deklaracje W deklaracjach zmiennych wskaźnikowych występuje * int i = 10; int * pi; i Pamięć operacyjna 10 int i = 10; int * pi = NULL; i Pamięć operacyjna 10 pi? pi Deklaracja pod lupą Nieznany obiekt wskazywany Wskaźnik pusty oznacza brak obiektu wskazywanego int * pi ; To oznacza, że deklarowana zmienna wskaźnikowa będzie przeznaczona do lokalizowania w pamięci obiektów typu int. To oznacza, że deklarowana zmienna będzie wskaźnikiem, kompilator wie, ile dla niej zarezerwować pamięci. Nazwa deklarowanej zmiennej wskaźnikowej. Podlega takim samym regułom jak nazwy innych zmiennych. Często zawiera p lub ptr od pointer. Copyright Roman Simiński Strona : 7
Zmienne wskaźnikowe deklaracje Rola wskaźnika pustego NULL Tak zdefiniowana zmienna wskaźnikowa: int * pi; ma wartość początkową zależną od kontekstu deklaracji. Jeżeli ta zmienna jest klasy auto, to jej wartość jest przypadkowa zmienna wskazuje zatem na bliżej nieznany obiekt w pamięci. W pliku nagłówkowym stddef.h zdefiniowana stałą NULL, reprezentującą wskaźnik pusty, niezależny od platformy i implementacji. Tak zdefiniowana zmienna: int * pi = NULL; jest wskaźnikiem pustym, a więc nie wskazuje żadnego obiektu w pamięci. To, czy zmienna wskaźnikowa jest wskaźnikiem pustym można sprawdzić: if( pi!= NULL ) if( pi == NULL ) W języku C jawnie inicjowanie zmiennych wskaźnikowych oraz posługiwanie się wartością NULL (a nie wartością 0) jest dobrą praktyką programistyczną. Copyright Roman Simiński Strona : 8
Zmienne wskaźnikowe podstawowe operacje Przypisywanie wartości zmiennym wskaźnikowym int i = 10; int * pi = NULL; pi = &i; i Pamięć operacyjna 10 O biekt ws kazywany pi Przypisanie pod lupą pi = & i ; Od momentu tego przypisania, pi wskazuje zmienną i, umożliwiając realizację dowolnych operacji na tej zmiennej. Jednoargumentowy operator & buduje wyrażenie wskaźnikowe lokalizujące zmienną w pamięci operacyjnej. Argument musi być l-wartością nie odnoszącą się do obiektu register ani pola bitowego. Wyrażenie wskaźnikowe lokalizujące zmienną i w pamięci. Copyright Roman Simiński Strona : 9
Zmienne wskaźnikowe podstawowe operacje Odwoływanie sie do obiektu wskazywanego int i = 10; int * pi = NULL; pi = &i; *pi = 20; i pi Pamięć operacyjna 20 *pi Odwołanie pod lupą * pi = 20 ; Ten zapis oznacza obiekt wskazywany przez pi. Zapis *pi może wystąpić wszędzie tam, gdzie może wystąpić i. Zmienna pi jest aliasem, linikiem do obiektu i. Jednoargumentowy operator adresowania pośredniego * daje w wyniku obiekt wskazywany przez argument pi. Dowolne wyrażenie typu zgodnego z typem obiektu wskazywanego. Copyright Roman Simiński Strona : 10
Zmienne wskaźnikowe podstawowe operacje Odwoływanie sie do obiektu wskazywanego Po przypisaniu: pi = &i; te fragmenty są równoważne: char s[] = 123 ; *pi = atoi( s ); *pi += 10; printf( %d, *pi ); char s[] = 123 ; i = atoi( s ); i += 10; printf( %d, i ); Jeżeli wskaźnik pi wskazuje na zmienną i, to *pi może wystąpić wszędzie tam, gdzie może wystąpić i. Zmienna pi jest aliasem, linikiem do i. Copyright Roman Simiński Strona : 11
Zmienne wskaźnikowe zastosowania Realizacja przekazywania parametrów przez zmienną z użyciem wskaźników void inc( int * pi ) *pi = *pi + 1; } int i = 10; inc( &i ); i pi Pamięć operacyjna 11 10 *pi=*pi+1 void getint( int * ptr ) char s[ 80 ]; gets( s ); *ptr = atoi( s ); } int liczba; getint( &liczba ); liczba ptr Pamięć operacyjna 123 W języku C wykorzystuje się parametry będące wskaźnikami do realizacji przekazywania parametrów działającego podobnie do przekazywania przez zmienną. void zamien( int * pierwszy, int * drugi ) int s; /* Schowek */ } s = *pierwszy; *pierwszy = *drugi; *drugi = s; a 5 b 10 s 5 int a = 5, b = 10; printf( "a=%d b=%d", a, b ); zamien( &a, &b ); printf( "a=%d b=%d", a, b ); a=5 b=10 a=10 b=5 Copyright Roman Simiński Strona : 12
Zmienne wskaźnikowe zastosowania Dynamiczny przydział pamięci Dynamiczny przydział pamięci polega na zarezerwowaniu fragmentu pamięci w obszarze pamięci wolnej zwanej stertą, dla obiektu pamięciowego zwanego dynamicznym. p Sterta a int * p = NULL; p = malloc( sizeof( int ) ); *p = 10; free( p ); p p 10 Sterta a Sterta a Przydziela się fragment o określonym rozmiarze, jedyny sposób na odwoływanie się do takiego obiektu to wykorzystanie wskaźnika. p Sterta a Strona : Copyright Roman Simiński 13
Zmienne wskaźnikowe zastosowania Dynamiczny przydział pamięci double * pole; pole = malloc( sizeof( double ) ); if( pole!= NULL ) *pole = PI * r * r; printf( "Pole kola: %f", *pole ); free( pole ); } Rezultatem funkcji malloc jest wskaźnik do przydzielonego dynamicznie obszaru pamięci przeznaczonego dla obiektu o rozmiarze sizeof(double). Wskaźnik będzie miał wartość NULL jeżeli pamięć nie została przydzielona. Zwalnia obszar pamięci wskazywany przez wskaźnik pole. W tym przykładzie przydział pamięci wydaje sie bezsensowny. Nie lepiej posłużyć się zwykłą zmienną typu double? Tak, dynamiczny przydział sprawdza się w innych sytuacjach, ale o tym później.... Copyright Roman Simiński Strona : 14
Zmienne wskaźnikowe zastosowania w przetwarzaniu tablic Tablice a wskaźniki Nazwa tablicy jest interpretowana jako ustalony wskaźnik na jej początek (pierwszy element). tab int tab[ 10 ]; 10 elementów 0 1 2 3 4 5 6 7 8 9 int tab[ 10 ]; int * p; p = tab; /* lub p = &tab[ 0 ]*/ 10 elementów tab p 0 1 2 3 4 5 6 7 8 9 Copyright Roman Simiński Strona : 15
Zmienne wskaźnikowe zastosowania w przetwarzaniu tablic Tablice a wskaźniki, cd.... tab[ 0 ] = 5 tab[ 1 ] = 1 tab[ 2 ] = 10 tab[ i ] = 22 Odwołania równoważne *p = 5 *( p + 1 ) = 1 *( p + 2 ) = 10 *( p + i ) = 22 tab p p + 0 p + 1 p + 2 p + i 0 1 2 i 7 8 9 tablica + indeks Odwołania równoważne wskaźnik + przesunięcie Wyrażenie p + i jest wyrażeniem wskaźnikowym, wskazuje ono na obiekt oddalony o i obiektów od p. Wartość dodawana do wskaźnika jest skalowana rozmiarem typu obiektu wskazywanego. Copyright Roman Simiński Strona : 16
Zmienne wskaźnikowe zastosowania w przetwarzaniu tablic Tablice a wskaźniki, cd.... Każde odwołanie: tab[ i ] można zapisać tak: *( tab + i ) Oraz każde odwołanie: *( p + i ) można zapisać tak: p[ i ] Wskaźniki to nie to samo, co tablice int tab[ 10 ]; int * p = tab; tab = p; tab++; p = tab + 8; p++; tab p Źle OK 0 1 2 3 4 5 6 7 8 9 int tab[ 10 ] wskaźnik + obszar danych int * p = tab zakotwiczony wskaźnik Nazwa tablicy jest ustalonym (niemodyfikowalnym) wskaźnikiem na pierwszy jej element. Nazw tablic nie wolno modyfikować! Wskaźniki można. Copyright Roman Simiński Strona : 17
Zmienne wskaźnikowe zastosowania w przetwarzaniu tablic Ciekawostka Wiemy, że: tab[ i ] można zapisać tak: *( tab + i ) Skoro dodawanie jest przemienne: *( i + tab ) czy można zapisać tak?: i[ tab ] Przykład: char napis[] = "język c"; puts( napis ); 0[ napis ] = 'J'; /* Zamiast napis[ 0 ] */ 6[ napis ] = 'C'; /* Zamiast napis[ 6 ] */ język c puts( napis ); Copyright Roman Simiński Strona : 18
Zmienne wskaźnikowe zastosowania w przetwarzaniu tablic Dlaczego nie wolno przypisywać tablic? int a[ 10 ]; int b[ 10 ]; b = a; /* Nie wolno przypisywać do siebie tablic */ Gdyby przypisywanie było możliwe... a b Po wykonaniu tej linii: 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 b = a; gubimy obszar danych tablicy b! a 0 1 2 3 4 5 6 7 8 9 b 0 1 2 3 4 5 6 7 8 9 Copyright Roman Simiński Strona : 19
Zmienne wskaźnikowe zastosowania w przetwarzaniu tablic Arytmetyka na wskaźnikach nie tylko dla typu char Załóżmy, że zmienne wskaźnikowe zawierają bezpośrednio adresy komórek pamięci. char s[ 6 ] = "Napis"; char * p1 = s; char * p2 = &s[ 4 ] int n = 2; Adresy 200341010 200341011 200341012 200341013 200341014 200341015 200341016 200341017 N a p i s \0 200341011 s 200341011 p1 p2-2 200341015 p2 p2 - p1 + 1 = 200341015-200341011 + 1 = 5 Wskaźnik i wartość całkowita mogą być dodawane i odejmowane, wyrażenie: s + n oznacza adres n-tego elementu od miejsca na które wskazuje s Wskaźniki związane z jedną tablicą można odejmować, jeżeli p1 < p2 to wyrażenie: p2 p1 + 1 określa liczbę elementów pomiędzy p1 a p2 wliczając w to p2 Copyright Roman Simiński Strona : 20
Zmienne wskaźnikowe zastosowania w przetwarzaniu tablic Arytmetyka na wskaźnikach podsumowanie Dozwolone operacje wskaźnikowe to: przypisywanie wskaźników do obiektów tego samego typu, dodawanie lub odejmowanie wskaźnika i liczby całkowitej, odejmowanie lub porównanie dwóch wskaźników związanych z ta samą tablicą, przypisanie wskaźnikowi wartości zero (wskazanie puste NULL) lub porównanie ze wskazaniem pustym. Copyright Roman Simiński Strona : 21
Zmienne wskaźnikowe zastosowania w przetwarzaniu tablic Wskaźniki w akcji metamorfoza funkcji put_string void put_string( char s[] ) int i; for( i = 0; s[ i ]!= '\0'; i++ ) putchar( s[ i ] ); } Wersja początkowa void put_string2( char * s ) for( ; *s!= '\0'; s++ ) putchar( *s ); } Eliminujemy zmienną i void put_string3( char * s ) for( ; *s!= '\0' ; putchar( *s++ ) ) ; } Kompresja iteracji for Najpierw pobierz znak wskazywany przez s, użyj go. *s ++ Potem zwiększ o jeden wartość wskaźnika s będzie on wtedy wskazywał na następny element tablicy. Copyright Roman Simiński Strona : 22
Zmienne wskaźnikowe zastosowania w przetwarzaniu tablic Wskaźniki pod lupą jak działa funkcja put_string4 void put_string4( char * s ) while( *s ) putchar( *s++ ); } Iteracja while nie jest taka zła... Znak '\0' to bajt o wartości 0 Najpierw pobierz znak wskazywany przez s, użyj go. *s ++ Potem zwiększ o jeden wartość wskaźnika s będzie on wtedy wskazywał na następny element tablicy. s Numer przebiegu iteracji while 1 2 3 s++ s++ s++ s++ char imie[ 80 ] = Aga ; put_string4( imie ); imie A g a \0 *s *s *s *s Aga_ Koniec iteracji while Copyright Roman Simiński Strona : 23
Zmienne wskaźnikowe zastosowania w przetwarzaniu tablic Wskaźniki pod lupą metamorfoza funkcji strcpy char s1[ 80 ] = ""; char s2[ 20 ]; strcpy( s2, s1 ); Wersja początkowa Przypomnienie jak to działa s i++ s1 J ę z y k C \0 0 1 2 3 4 5 6 79 d s2 J ę z y k C \0 0 1 2 3 4 5 void strcpy( char d[], char s[] ) int i; for( i = 0; s[ i ]!= '\0'; i++ ) d[ i ] = s[ i ]; d[ i ] = '\0'; } 6 19 Copyright Roman Simiński Strona : 24
Zmienne wskaźnikowe zastosowania w przetwarzaniu tablic Wskaźniki pod lupą metamorfoza funkcji strcpy void strcpy1( char * d, char * s ) while( *s!= '\0' ) *d = *s; d++; s++; } *d = '\0'; } Odwołania wskaźnikowe To właściwie nie wiele zmienia, poza wyeliminowaniem zmiennej i void strcpy2( char * d, char * s ) while( *s!= '\0' ) *d++ = *s++; *d = '\0'; } Kompresja krok pierwszy Copyright Roman Simiński Strona : 25
Zmienne wskaźnikowe zastosowania w przetwarzaniu tablic Wskaźniki pod lupą metamorfoza funkcji strcpy void strcpy3( char * d, char * s ) while( ( *d++ = *s++ )!= '\0' ) ; } Kompresja krok drugi Wartością tego wyrażenia jest znak (bajt) przepisany z obszaru wskazywanego przez s do obszaru wskazywanego przez d. Operator = jest lewostronnie łączny ( *d++ = *s++ ) Pobierz znak wskazywany, wykorzystaj go, zwiększ wskaźnik tak, by pokazywał na następny element tablicy.!= '\0' void strcpy4( char * d, char * s ) while( *d++ = *s++ ) ; } Kompresja krok trzeci Znak '\0' to bajt o wartości 0 Copyright Roman Simiński Strona : 26
Zmienne wskaźnikowe zastosowania w przetwarzaniu tablic Wskaźniki pod lupą metamorfoza funkcji strcpy Często spotykaną praktyką w funkcjach bibliotecznych jest udostępnianie wskaźnika do tablicy (jednej z tablic) będącej parametrem: char * strcpy5( char * d, char * s ) while( *d++ = *s++ ) ; return d; } Tablica d jako rezultat funkcji Pozwala to na skrócenie kodu, załóżmy następujące definicje tablic s1, s2, s3: char s1[ 80 ] = ""; char s2[ 80 ]; char s3[ 80 ]; Następujący fragment kodu: strcpy5( s2, s1 ); strcpy5( s3, s2 ); puts( s3 ); Można zapisać krócej: puts( strcpy5( s3, strcpy5( s2, s1 ) ) ); Copyright Roman Simiński Strona : 27
Zmienne wskaźnikowe zastosowania w przetwarzaniu tablic Wskaźniki pod lupą metamorfoza funkcji strcpy W dotychczasowych realizacjach funkcji strcpyx, funkcja może modyfikować zawartość tablicy źródłowej: char * strcpy5( char * d, char * s ) *s = 'A'; }` Modyfikacja tablicy źródłowej dozwolona, choć merytorycznie niepoprawna Aby temu zaradzić, można zadeklarować parametr reprezentujący tablicę źródłową w specyficzny sposób: char * strcpy6( char * d, const char * s ) *s = 'A'; } Aby funkcja nie mogła zmodyfikować parametru przekazanego za pośrednictwem wskaźnika, należy w deklaracji użyć słowa const. Deklaracja: const char * s; oznacza, że s jest wskaźnikiem na stały (niemodyfikowalny) obiekt typu char. Copyright Roman Simiński Strona : 28
Zmienne wskaźnikowe uwagi Wskaźniki a kwalifikator const Można wyróżnić następujące kombinacje definicji wskaźnika z/bez const: const int * const p; /* Ustalony wskaźnika na niemodyfikowalny obiekt */ int * const p; /* Ustalony wskaźnika na modyfikowalny obiekt */ const int * p; /* Zwykły wskaźnika na niemodyfikowalny obiekt */ int * p; /* Zwykły wskaźnik na zwykły obiekt */ Wersja najbardziej restrykcyjna pod lupą const int * const p; To się nie uda, ustalony wskaźnik należy zainicjować! int i = 10; const int * const p = &i; j = *p + 10; *p = 20; p = &j; To jest OK, odwołanie nie modyfikujące obiektu Niedozwolone, odwołanie modyfikujące obiekt Niedozwolone, odwołanie modyfikujące wskaźnik Copyright Roman Simiński Strona : 29
Zmienne wskaźnikowe uwagi Ważna sprawa ostrożnie z parametrami wskaźnikowymi! W funkcjach bibliotecznych języka C stałą praktyką jest deklarowanie parametrów tablicowych z wykorzystaniem wskaźników, np: int strlen( char * s ); zamiast int strlen( char s[] ); Wymaga to dokładnego przeczytania dokumentacji, bowiem programiści często się mylą. Rozważmy następujący przykład (fragment systemu pomocy firmy Borland): Prototype char *gets(char *s); Description Gets a string from stdin. gets collects a string of characters terminated by a new line from the standard input stream stdin and puts it into s. The new line is replaced by a null character (\0) in s. gets allows input strings to contain certain whitespace characters (spaces, tabs). gets returns when it encounters a new line; everything up to the new line is copied into s. Copyright Roman Simiński Strona : 30
Zmienne wskaźnikowe uwagi Ważna sprawa ostrożnie z parametrami wskźnikowymi, cd... Niedokładna lektura dokumentacji może sugerować, że funkcji należy użyć tak: char * imie; printf( "Podaj imie: " ); gets( imie ); imie??? Aga gets( imie ) A trzeba np. tak: char imie[ 80 ]; printf( "Podaj imie: " ); gets( imie ); imie Aga gets( imie ) Copyright Roman Simiński Strona : 31
Dynamiczny przydział pamięci Dynamiczna alokacja tablic Dynamiczny przydział pamięci polega na zarezerwowaniu fragmentu pamięci w obszarze pamięci wolnej zwanej stertą, dla obiektu pamięciowego zwanego dynamicznym. char * s = NULL; int n = 30; s = malloc( n * sizeof( char ) ); strcpy( s, " " ); strcat( s, "fajny jest!" ); puts( s ); free( s ); Na tablicach alokowanych dynamicznie na stercie, można wykonywać takie same operacje, jak na tablicach statycznych. Należy tylko uważnie przydzielać i zwalniać pamięć. s s s s Sterta a Sterta a Sterta a fajny Pamięć jest! operacyjna Sterta a fajny Pamięć jest! operacyjna Copyright Roman Simiński Strona : 32