Wskaźniki w C Anna Gogolińska
Zmienne Zmienną w C można traktować jako obszar w pamięci etykietowany nazwą zmiennej i zawierający jej wartość. Przykład: kod graficznie int a; a a = 3; a 3
Wskaźniki Wskaźnik to tylko adres w pamięci czyli wskazanie na określone miejsce w pamięci. Można sobie go wyobrażać jako strzełkę. Wskaźnik tworzymy poprzed dodanie przez nazwą zmiennej *. int *p; p
WAŻNE!!! Jeśli utworzyliśmy wskaźnik np. int *p; to: p to adres w pamięci! Wszelkie operacje na samym p (np. p++) to operacje na adresach pamięci! *p to element na który wskazuje wskaźnik! Zazwyczaj to jego chcemy modyfikować (a nie sam adres). Na przykład (*p)++ powoduje zwiększenie elementu wskazywanego przez p. Elementem zerowym ( zerem dla wskaźników ) jest wartość NULL, nazywana wskaźnikiem pustym. Wyzerowanie wskaźnika: p = NULL; /*dlaczego nie *p?*/
Wskaźniki Nowo utworzony wskaźnik wskazuje na nieokreślone miejsce w pamięci, często obszar do którego program nie ma prawa pisać. Wskaźnik który na razie na nic nie wskazuje dobrze jest wyzerować (przypisać NULL). Aby móc używać wskaźnika trzeba wyznaczyć/zarezerwać obszar pamięci na który on będzie wskazywać. Można to zrobić na dwa sposoby.
I - Przypisanie pod adres zmiennej Pierwszym sposobem przydzielenia pamięci dla wskaźnika jest przypisanie go pod adres zmiennej. int a; a a = 3; int *p; p = &a; p a 3 p a 3 Teraz zarówno pod a jak i pod *p mamy wartość 3.
Przypisanie pod adres zmiennej int a; a = 3; int *p; p = &a; a++; *p+=2; p p a 4 a 6
Przypisanie pod adres zmiennej W przykładzie wyżej na koniec zmienna ma wartość 6. Powyższa konstrukcja wydaje się nieprzydatna po co nam dwa obiekty, które wskazują na tą samą wartość? Użyteczne jest to jednak w przekazywaniu argumentów do funkcji. Normalnie do funkcji przekazywane są kopie wartości argumentów, operacje na tych wartościach nie są widziane poza funkcją. Aby rzeczywiście modyfikować wartości argumentów należy je przekazać przez wskaźnik (a w zasadzie do funkcji przekazać adres argumentu).
Funkcje i wskaźniki void skracanie(int* licz, int * mian) { int a = nwd(*licz, *mian); /*nwd() to inna funkcja zdefiniowana wyżej w kodzie*/ *licz = *licz / a; /*dlaczego nie licz = licz/a?*/ *mian = *mian / a; return; int main(){ int a = 10, b = 45; skracanie(&a, &b); return 0;
Funkcje i wskaźniki Funkcja z poprzedniego slajdu przyjmuje dwa argumenty, z których jeden jest licznikiem ułamka, a drugi mianownikiem ułamka i skraca je. Ponieważ operuje na wskaźnikach zmienione zastoną rzeczywiste wartości a i b, a nie ich kopie. Aby funkcja działała należy przekazać do niej adresy zmiennych a i b - poprzez operator &. W tym miejscu dochodzi do przypisania licz = &a i mian = &b. dlaczego nie licz = licz/a? taki zapis oznaczałby podzielenie adresu zmiennej, czyli numeru określającego blok pamięci i przypisanie go pod adres zmiennej. Wskaźnik licz wskazywałby wtedy jakiś zupełnie inny obszar pamięci.
II - Przydzielenie pamięci Innym sposobem przydzielenia pamięci dla wskaźnika jest wykonanie tego bezpośrednio (bez pośrednictwa zwykłej zmiennej). Służy do tego funkcja malloc i calloc z biblioteki stdlib.h. Przykład (w pierwszym przykładzie od razu przydzielamy pamięć, w drugim wpierw tworzymy wskaźnik, a pamięć przydzielamy później między tymi czynnościami mogą występować inne instrukcje): int * p = (int *)malloc(sizeof(int)); char * q; /*opcjonalnie inne instrukcje*/ q = (char *)malloc(sizeof(char));
malloc p = (int *)malloc(sizeof(int)) - na początku należy zrobić rzutowanie na typ wskaźnika dla jakiego przydzielmy pamięć, ponieważ funkcja malloc zwraca zawsze void*. p = (int *)malloc(sizeof(int)) argumentem dla funkcji malloc jest wielkość obszaru pamięci jaki chcemy przydzielić. Tu jest to rozmiar jednego inta. Wskazane jest użycie operatowa sizeof. Do obszaru przydzielonego przez funkcję malloc dostęp jest jedynie za pośrednictwem wskaźnika, np: int * p = (int *)malloc(sizeof(int)); *p = 10;
malloc i tablice Funkcja malloc nie musi zwracać obszaru pamięci równego wielkości jednej zmiennej. Można zwracać wielokrotności rozmiarów zmiennych. W ten sposób można deklarować tablice (w przykładzie niżej tablica długości n): int *t; /*opcjonalnie inne instrukcje*/ t = (int*)malloc(sizeof(int) * n)); Używjąc takiej definicji można tworzyć tablice dynamicznie wielkość tablicy jest ustalana w trakcie działania programu, np. podawana przez użytkownia (w przykładzie wyżej wartość n może być zczytywana między deklaracją wskaźnika, a wywołaniem funkcji malloc).
Calloc Funkcja calloc jest bardzo podobna do funkcji malloc, ma tylko nieco inną składnię. W przykładach dalej pojawiać się będzie malloc ale można go zastąpić również przez calloc. int * tab = (int*) calloc (i,sizeof(int)); Pierwszym argumentem funkcji jest ilość komórek pamięci które chcemy przydzielić (np. wielkość tablicy, albo 1 w przypadku pojedyńczej zmiennej). Drugim jest wielkość pojedyńczego elementu. W przykładzie powyżej przydzielamy pamięć dla tablicy intów wielkości i.
Tablice i wskaźniki Tablica w C jest wskaźnikiem. A dokładniej identyfikator tablicy jest wskaźnikiem na pierwszy jej element. Jeśli mamy tablice int tab[10] to tab jest adresem pierwszego (zerowego) elementu. Możemy się do tego elemetu odwołać używając tab[0] lub *tab. Oba sposoby operowania na tablicach: z użyciem [] czy z użyciem * są poprawne i równoważne. Można nawet je mieszać. Ponieważ tablica jest wskaźnikiem, nie stosuje się operatowa & w funkcji scanf, podczas wczytywania łańcucha znaków: char napis[20]; scanf( %s, napis);
Tablice i wskaźniki Zapis z użyciem * zamiast z [] stosuje się, kiedy funkcja ma przyjmować tablice jako argument lub ją zwracać, na przykład: char* funkcja(int* tab1, double* tab2) Na wskaźnikach można używać operatorów +, -, +=, -=, ++, --. Powodują one przesunięcie wskaźnika na następną komórkę pamięci (wielkości odpowiadającej typowi wskaźnika). Dzięki użyciu tych operatorów można iterować po tablicach po prostu przesuwając wskaźnik.
Tablice przykład 1 #include <stdio.h> int main(){ int tab[5], i, maks; printf( podaj elementy tablicy\n ); for(i = 0; i < 5; i++) scanf( %d, &tab[i]); maks = tab[0]; for(i = 1; i < 5; i++) if(maks < tab[i]) maks = tab[i]; printf( element najwiekszy: %d\n, maks); return 0;
Tablice przykład 2 #include <stdio.h> #include <stdlib.h> int main(){ int *tab, *pom, i, maks; tab = (int*)malloc(sizeof(int) * 5); pom = tab; printf( podaj elementy tablicy\n ); for(i = 0; i < 5; i++) { scanf( %d, tab); tab = pom; maks = *tab; for(i = 1; i < 5; i++){ if(maks < *tab) maks = *tab; printf( element najwiekszy: %d\n, maks); return 0; tab
Tablice przykład 2 #include <stdio.h> #include <stdlib.h> int main(){ int *tab, *pom, i, maks; tab = (int*)malloc(sizeof(int) * 5); pom = tab; printf( podaj elementy tablicy\n ); for(i = 0; i < 5; i++) { scanf( %d, tab); tab = pom; maks = *tab; for(i = 1; i < 5; i++){ if(maks < *tab) maks = *tab; printf( element najwiekszy: %d\n, maks); return 0; Zmienna pomocnicza w której przechowywany i zachowywany jest wskaźnik na początek tablicy, ponieważ wskaźnik tab jest później przesuwany na kolejne elemety. tab pom
Tablice przykład 2 #include <stdio.h> #include <stdlib.h> int main(){ int *tab, *pom, i, maks; tab = (int*)malloc(sizeof(int) * 5); pom = tab; printf( podaj elementy tablicy\n ); for(i = 0; i < 5; i++) { scanf( %d, tab); tab = pom; maks = *tab; for(i = 1; i < 5; i++){ if(maks < *tab) maks = *tab; printf( element najwiekszy: %d\n, maks); return 0; W pierwszej iteracji pętli tab wskazuje na zerowy element tablicy. Pod element ten wspisywana jest wartość podana przez użytkownia. Ponieważ tab jest wskaźnikiem nie jest potrzebne użycie &.
Tablice przykład 2 #include <stdio.h> #include <stdlib.h> int main(){ int *tab, *pom, i, maks; tab = (int*)malloc(sizeof(int) * 5); pom = tab; printf( podaj elementy tablicy\n ); for(i = 0; i < 5; i++) { scanf( %d, tab); tab = pom; maks = *tab; for(i = 1; i < 5; i++){ if(maks < *tab) maks = *tab; printf( element najwiekszy: %d\n, maks); return 0; pom tab Wskaźnik tab jest przesuwany o długość zmiennej int (ponieważ tab jest takiego typu) i wskazuje na kolejną wartość int w pamięci czyli na kolejny element tablicy. W ten sposób w pętli for wypełni/przejrzy się wszystkie elementy tablicy.
Tablice przykład 2 #include <stdio.h> #include <stdlib.h> int main(){ int *tab, *pom, i, maks; tab = (int*)malloc(sizeof(int) * 5); pom = tab; printf( podaj elementy tablicy\n ); for(i = 0; i < 5; i++) { scanf( %d, tab); tab = pom; maks = *tab; for(i = 1; i < 5; i++){ if(maks < *tab) maks = *tab; printf( element najwiekszy: %d\n, maks); return 0; W pom zapamiętany jest wskaźnik na początek tablicy. Teraz również zmienna tab wskazuje na ten początek. tab pom
Tablice przykład 2 #include <stdio.h> #include <stdlib.h> int main(){ int *tab, *pom, i, maks; tab = (int*)malloc(sizeof(int) * 5); pom = tab; printf( podaj elementy tablicy\n ); for(i = 0; i < 5; i++) { scanf( %d, tab); tab = pom; maks = *tab; for(i = 1; i < 5; i++){ if(maks < *tab) maks = *tab; printf( element najwiekszy: %d\n, maks); return 0; Do elementu tablicy można się odwołać jak do elementu wskazywanego przez wskaźnik, czyli z użyciem *.
Tablice - przykłady Oba przykłady działają tak samo. Pierwszy jest prostszy i krótszy w zapisie. Drugi jednak ma nad nim znaczną przewagę zamiast deklarować od razy wielkość tablicy można o nią zapytać użytkownika i stworzyć tablicę dynamicznie. Ponieważ oba podejścia do tablic można mieszać, w praktycznych zastosowaniach można utworzyć tablicę jako wskaźnik, a później traktować ją w sposób klasyczny (z użyciem []).
Częste błędy Odwoływanie się do wskaźnika do którego nie przydzielono pamięci. Na przykład: int *p; *p = 10; Mylenie samego wskaźnika (czyli adres) i elementu przez niego wskazywanego, np. w wywołaniach funkcji, iteracji po tablicy z użyciem wskaźnika. Przekroczenie wielkości tablicy wyjechanie wskaźnikiem za tablicę. Nie jest to kontrolowane przez kompilator.