Wskaźniki
Transkrypt
Wskaźniki
Wskaźniki nie są konieczne, ale dają językowi siłę i elastyczność są języki w których nie używa się wskaźników typ wskaźnikowy – typ pochodny: typ nw; /* definicja zmiennej nw typu typ */ typ *w_nw; /* definicja wskaźnika w_nw do typu typ */ wskaźnik – zmienna która zawiera adres innej zmiennej wskaźnik niesie dwojakiego rodzaju informację: o adres obiektu, który wskazuje (gdzie) o rodzaj obiektu, który wskazuje (co) rola operatora odwołania pośredniego (wyłuskiwania) Główne obszary zastosowanie wskaźników w funkcjach, które mogą zmieniać wartość wskazywanych argumentów usprawnienie pracy z tablicami dostęp do specjalnych komórek pamięci rezerwacja obszarów pamięci (dynamiczna alokacja pamięci) Funkcje zmieniające wartość wskazywanych argumentów zwykłe przekazanie argumentów – przez wartość przekazujemy kopię zmiennej; zmiany dokonane wewnątrz funkcji nie przenoszą się na oryginał inne rozwiązanie – zmienne globalne; problem – niejawne (nieoczywiste) powiązania między danymi najlepsze rozwiązanie – użycie wskaźników zasada: parametr aktualny – adres obiektu; parametr formalny – wskaźnik; zmiany – poprzez operator odwołania pośredniego Tablice a wskaźniki bardzo bliski związek między tablicami a wskaźnikami każda operacja wykonana przez indeksowanie tablicy można wykonać przez działanie na wskaźnikach (wersja wskaźnikowa zwykle szybsza) int a[10]; int *pa; /* rezerwacja tablicy o 10 elem. typu int */ /* pa – wskaźnik do pokazywania int */ pa a[0] a[1] a[2] a[9] Po przypisaniu: pa=&a[0]; /* lub pa=a */ pa a[0] a[1] a[2] a[9] kluczowy punkt: jeżeli pa wskazuje na pewien element tablicy, to pa+1 wskazuje na następny element tablicy (bez względu na rodzaj tablicy) pa pa+1 pa+2 a[0] a[1] a[2] a[9] pa wskazuje na a[0] pa+k wskazuje na a[k] *pa == a[0] *(pa+k) == a[k] Wniosek: wyrażenie postaci ”tablica i indeks” jest równoważne wyrażeniu postaci: ”wskaźnik i przesunięcie” a – stała, adres początku tablicy; dlatego a[k] == *(a+k) różnica pomiędzy wskaźnikiem a nazwą tablicy – a to nie jest l-wartość; wyrażenia a++ niedozwolone #include <iostream> using namespace std; int main() { int a[5]={1, 2, 3, 4, 5}; int w1, w2, w3, s, k, *pa; /* metoda tablica + indeks */ for (s=0, k=0; k<5; k++) s += a[k]; w1=s; /* metoda nazwa tablicy + przesuniecie */ for (s=0, k=0; k<5; k++) s += *(a+k); w2=s; /* metoda wskaźnik + przesuniecie */ pa=a; for (s=0, k=0; k<5; k++) s += *pa++; w3=s; cout<<”w1= << w1 << ” w2=” << w2 <<” w3=” << w3 << endl; } w1=15 w2=15 w3=15 Arytmetyka wskaźników dodawanie/odejmowanie liczb całkowitych do/od wskaźnika o wskaźnik + liczba całkowita k przesuwa wskaźnik o k pozycji wzdłuż tablicy o wskaźnik - liczba całkowita k cofa wskaźnik o k pozycji wzdłuż tablicy gdy p1, p2 – dwa wskaźniki pokazujące elementy tej samej tablicy to p1-p2 ilość pozycji w tablicy o które różnią się elementy wskazywane przez oba wskaźniki inne operacje na wskaźnikach o porównywanie wskaźników między sobą; dozwolone operacje: == , != czy obiekty wskazywane są tożsame, czy też różne < , > , <= , >= - dla wskaźników wskazujących elementy tej samej tablicy < , > , <= , >= - dla wskaźników nie wskazujących elementów tej samej tablicy, ale tego samego typu – sens: porównanie położenia obiektów w pamięci porównanie wskaźnika z adresem 0 o oparte na założeniu, że adres 0 nie może być adresem żadnego obiektu w C/C++ o gdy dostaniemy 0 jako wartość wskaźnika zwracanego przez pewien proces, to oznaczało to, że ten proces zakończył się niepowodzeniem – metoda sygnalizacji błędu o konwencja – gdy wskaźnik 0 – nic z nim nie rób Równoważność wskaźnik – tablica: jak to działa wewnątrz funkcji? Jak przekazywać i odbierać tablice jako argumenty funkcji? Przykład – funkcja konwertująca temperatury: void konwertuj_t(float t[ ], int ile) /* wersja tablicowa */ { int k; for (k=0; k<ile; k++) t[k]=5.0*(t[k] – 32.0)/9.0; } main() { int m; float temp[101]; for (m=0; m<101; m++) temp[m]=m; konwertuj_t(temp, 101); } Wersja wskaźnikowa funkcji konwertuj_t: void konwertuj_t(float *t, int ile) /* wersja wskaźnikowa */ { int k; for (k=0; k<ile; k++, t++) *t =5.0*(*t – 32.0)/9.0; } adres tablicy odbieramy jako wskaźnik przesuwamy wskaźnik wzdłuż obszaru pamięci zajmowanego przez tablicę tak, by w obliczeniach wziąć odpowiedni element przewaga wersji wskaźnikowej – program nie oblicza adresów odpowiadających poszczególnym indeksom (tylko przesuwa wskaźnik) możliwa wersja mieszana – odbieramy adres jako wskaźnik, używamy jak tablicy void konwertuj_t(float *t, int ile) /* wersja mieszana */ { int k; for (k=0; k<ile; k++) t[k]=5.0*(t[k] – 32.0)/9.0; } Funkcje znakowe – wersje wskaźnikowe Przykład 1 – kopiowanie stringów: a. wersja tablicowa: void strcpy(char kopia[ ], char wzor[ ]) { int k=0; while (kopia[k]=wzor[k]) k++; } b. wersja wskaźnikowa void strcpy(char *kopia, char *wzor) { while (*kopia++ = *wzor++) ; } Przykład 2 – sumowanie stringów (strcat) Do końca stringu s1 dodaje string s2, zwraca wskaźnik do stringu zawierającego sumę obu. #include <iostream> using namespace std; char *strcat(char *suma, char* zrodlo) { char *start=suma; /* znalezenie konca stringu suma */ while (*suma++) ; suma--; /* skopiowanie stringu zrodlo */ while (*suma++ = *zrodlo++) ; return start; } int main() { char sss[80]={"Ala ma "}; cout << sss << strcat(sss,"kota !!!\n"); return 0; } Ala ma kota !!! Ala ma kota !!! Dostęp do wyróżnionych obszarów pamięci stosowane, gdy chcemy sięgnąć do konkretnej komórki pamięci (np. o adresie 93960) ważne dla komórek wyróżnionych (np. sprzężonych z zewnętrznym miernikiem temperatury przy normalnej definicji zmiennej nie mamy wpływ na alokację pamięci rozwiązanie – użycie wskaźnika: float *p; p= (float*) 93960; cout <<“Aktualna temperatura wynosi: ”<< *p << endl; Dynamiczna alokacja pamięci normalna (statyczna) definicja tablicy – nie możemy określić rozmiaru tablicy po rozpoczęciu pracy programu konieczność deklaracji nadmiarowej (nieefektywność) inna możliwość: dynamiczny przydział pamięci Przykład 1: utworzenie pojedynczego obiektu typu int: int *wint; wint= new int; *wint = 1000; cout << *wint; delete wint; // powołanie do życia // nadanie wartości // wypisanie wartości // likwidacja obiektu Przykład 2: utworzenie tablicy o rozmiarze określonym przez użytkownika: #include <iostream> #include <cstdlib> using namespace std; int main() { float *wf, *wfb; int rozmiar,k; cout << "Podaj rozmiar tablicy: "; cin >> rozmiar; cout << rozmiar << endl; wf=new float[rozmiar]; // utworzenie tablicy wfb=wf; for (k=0; k<rozmiar; k++) // obsługa tablicy *wfb++=k/100.0; cout << "Ostatni element = " << *(wf+rozmiar-1) << endl; delete [ ] wf; // usunięcie tablicy } tworzenie obiektów – operatory new oraz new [ ] – zdefiniowane w C++ obiekty tworzone na stercie – własności różne od zmiennych automatycznych obszar sterty nie jest nieograniczony – konieczność likwidacji obiektów już niepotrzebnych: operatory delete oraz delete [] Cechy obiektów tworzonych dynamicznie: istnieją od momentu utworzenia do momentu ich skasowania – my decydujemy o ich czasie życia tak utworzone obiekty nie mają nazwy – są dostępne tylko przez wskaźnik obiektów tych nie obowiązują zwykłe zasady dotyczące zakresu ważności – jeżeli tylko dostępny jest wskaźnik wskazujący na dany obiekt, to mamy do niego pełny dostęp po utworzeniu zawiera śmiecie Uwaga: w C – do dynamicznej alokacji pamięci stosuje się funkcje biblioteki standardowej: malloc, calloc, realloc, free. void* malloc(size_t size) -- zwraca wskaźnik do obszaru pamięci przeznaczonego dla obiektu o razmiarze size, lub NULL gdy alokacja jest niemożliwa void *calloc(size_t ile, size_t size) - zwraca wskaźnik do obszaru pamięci przeznaczonego dla tablicy o ile elementów o długości size każdy (NULL gdy alokacja niemożliwa) void *realloc(void *p, size_t size) – zmienia rozmiar obiektu wskazywanego przez p na wartość określoną przez size. Zawartość początkowej części obiektu o rozmiarze równym min(nowy rozmiar, stary rozmiar). Zwraca wskaźnik do nowego obszaru (NULL gdy polecenie nie może być wykonane – wtedy zawartość *p się nie zmienia). void free(void *p) – zwalnia obszar pamięci wskazywany przez p. Nie robi nic, gdy p=NULL. Argument p musi być wskaźnikiem zwróconym wcześniej przez malloc, calloc lub realloc. Użycie tych funkcji – konieczność rzutowania wskaźników oraz podawania rozmiarów obiektów Przykład 2a #include <iostream> #include <cstdlib> using namespace std; int main() { float *wf, *wfb; int rozmiar,k; cout << "Podaj rozmiar tablicy: "; cin >> rozmiar; cout << rozmiar << endl; if ((wf=(float*)malloc(rozmiar*sizeof(float)))==NULL) { printf("Blad alokacji pamieci !!!\n"); return 1; } wfb=wf; for (k=0; k<rozmiar; k++) // obsługa tablicy *wfb++=k/100.0; cout << "Ostatni element = " << *(wf+rozmiar-1) << endl; free(wf) // usunięcie tablicy }