Wykład 1: Wskaźniki Podstawy programowania Programowanie w C Dr inż. Zdzisława Rowińska III p. KIS, pokój 314, zrow@kis.p.lodz.pl
Gdzie wykorzystywane są wskaźniki? Praca z tablicami Funkcje – zmiana wartości przesyłanych argumentów Dostęp do specjalnych komórek pamięci Rezerwacja obszarów pamięci
Definiowanie wskaźników typ_obiektu *nazwa_wskaźnika; nazwa_wskaźnika wskazuje na obiektu typu typ_obiektu. Przykłady Można definiować wskaźniki do obiektów różnych typów: int *wi; char *wch; float *wf; Uwaga: Z definicji wskaźnika wynika, że wskaźnik pokazuje na obiekt. Referencja nie jest obiektem, dlatego nie można definiować wskaźników do referencji. Wskaźnik, który pokazuje na obiekt jednego typu, nie może być wykorzystany do pokazywania na obiekt innego typu.
Wskaźniki Sama definicja wskaźnika nie powoduje, że wskaźnik wskazuje na konkretny obiekt. Nadanie wartości obiektowi (wskaźnik wskazuje na istniejący obiekt): int *wi; int i; wi=&i; //ustawienie wskaźnika w na obiekt i Jeśli wskaźnik wskazuje na konkretny obiekt, to można odnosić się do tego obiektu za pomocą wskaźnika. Do operacji tej służy jednoargumentowy operator odniesienia (wyłuskania, dereferencji) * *nazwa_wskaźnika; Przykład: int *wi; //definicja wskaźnika int i; //definicja zmiennej i wi=&i; //ustawienie wskaźnika *wi=5; //przypisanie wartości 5 zmiennej i
Wskaźniki – cd Przykłady: int *wi,*wj; //definicja wskaźnika int i,j; //definicja zmiennych i,j wi=&i; i=5; *wi=5; j=7; j=*wi; wi=&j; wj=wi;
Wskaźniki typu void void *wv; Definicja wskaźnika bez podania typu obiektu, na jaki wskazuje. Może być użyty do wskazywania na obiekty dowolnego typu. Przykłady: void *wv; int *wi; float *wf; wv=wi; //teraz wskaźnik wv wskazuje na ten sam obiekt (typu int), //na który wskazuje wskaźnik wi wv=wf; wi=wf //kompilator zasygnalizuje błąd!! wi=(int *)wf; //wykorzystanie rzutowania wf=(float *)wv; //wykorzystanie rzutowania Wskaźnikowi typu void można przypisać wskaźnik dowolnego (niestałego) typu. Działanie odwrotne wymaga operatora rzutowania.
Zastosowanie wskaźników do tablic int *wsk; //definicja wskaźnika int tab[10]; //definicja tablicy wsk=&tab[indeks]; //ustawienie wskaźnika na elemencie tablicy //o indeksie indeks wsk=&tab[0]; wsk=tab; wsk=&tab[indeks]; wsk=wsk+ind; wsk += ind; //przesunięcie wskaźnika o ind pozycji instrukcje równoważne Dodanie do wskaźnika liczby całkowitej ind powoduje, że wskaźnik pokazuje o ind elementów dalej w tablicy; niezależnie od tego, jakiego typu są elementy tablicy.
Zastosowanie wskaźników do tablic Przykład: #include <iostream.h> void main(){ float *wf; float tab[10]; wf=tab; //lub wf=&tab[0]; for(int i=0; i<10; i++){ *(wf++)=i/10.0; } for(i=0,wf=tab;i<10;i++,wf++) printf(”%f\n”,*wf);
Nazwa tablicy i wskaźnik Nazwa tablicy jest jednocześnie adresem jej zerowego elementu. Nazwa tablicy jest stałym wskaźnikiem do jej zerowego elementu. float *wsk; float tab[10]; wsk=tab; //wsk=&tab[0]; //możliwe jest przypisanie wsk++; //niemożliwa jest instrukcja tab++; tab[ind]; *(tab+ind); Wskaźnik jest pewnym obiektem w pamięci (to znaczy posiada swój adres). Nazwa (również tablicy) nie jest obiektem (nie ma więc adresu). float *wsk; &wsk; //adres wskaźnika – odwołanie poprawne
Wskaźniki w argumentach funkcji void fun(float f) { f /= 3.3; } float fun(float *f) { *f /= 3.3; Przesyłanie argumentów przez wartość. Funkcja nie zmienia wartości przesyłanego argumentu. void main() { float fvar = 13.3; fun(fvar); printf(”%f\n”, fvar); // 13.3 fun(&fvar); printf(”%f\n”, fvar); // 4.03 } Przesyłanie argumentów przez wskaźnik. Zmiana wewnątrz funkcji wartości przesyłanego argumentu. 10
Przesyłanie tablic do funkcji #include <iostream.h> void f1(int *pi, int rozm); void f2(int *pi, int rozm); void f3(int t[], int rozm); void main(){ int tab[5]={1,2,3,4,5}; f1(tab,5); f2(tab,5); f3(tab,5); } void f1(int *pi, int rozm){ printf("\nfunkcja f1\t”); for(int i=0;i<rozm;i++) printf(”%f\t”,*(pi++)); void f2(int *pi, int rozm){ print(”%f\t”,pi[i]); void f3(int t[], int rozm){ printf("\nfunkcja f3\t”); print(”%f\t”,t[i])); Przesyłanie tablic do funkcji Funkcje są wywoływane przez podanie nazwy tablicy Przesłany adres tablicy inicjalizuje lokalny wskaźnik pi. Wewnątrz funkcji - zapis wskaźnikowy Przesłany adres tablicy inicjalizuje lokalny wskaźnik pi. Wewnątrz funkcji - zapis tablicowy Przesłany adres tablicy odebrany jako tablica 11
Przesyłanie tablic do funkcji – cd Odebranie tablicy jako tablicy – czytelność funkcji Odebranie tablicy jako adresu inicjującego wskaźnik – funkcja działa szybciej Odebranie tablicy jako adresu – łatwiejsze przekazywanie tablic wielowymiarowych (rozmiary tablicy nie muszą być znane w momencie wywołania funkcji) 12
Wskaźniki do stałych Wskaźniki do stałych mogą zawierać adresy dowolnych zmiennych i mogą być modyfikowane w programie, ale nie można za ich pomocą modyfikować zmiennych wskazywanych. const int stala = 10; // definicja stałej typu int const int *wsk_st; // wskaźnik do stałej typu int void main() { int i = 5; const int *w = &i; // wskaźnik zainicjowany wsk_st = &i; // inicjacja adresem zmiennej automatycznej i cout << *wsk_st << endl; // 5 cout << *w << endl; // 5 // *w = *w + 5; // błąd kompilatora ! // *wsk_st+= 7; // nie można modyfikować stałej wsk_st = &stala; // wskaźnik inicjowany adresem stałej w = &stala; // wskaźnik inicjowany adresem stałej cout << *wsk_st << endl; // 10 cout << *w << endl; // 10 } 13
Stałe wskaźniki Stały wskaźnik to wskaźnik, który zawsze pokazuje na to samo. Wskaźnik tego typu musi być zainicjowany w miejscu definicji - tak jak każda stała. W programie nie można już modyfikować jego wartości. Za pomocą wskaźnika stałego można jednak modyfikować zawartość zmiennej wskazywanej, ale tylko tej, której adresem wskaźnik został zainicjowany. const int st = 4; // stała int zm = 10; // definicja zmiennej int * const w = &zm; // stały wskaźnik do zmiennej zm // int * const x = &st; // błąd – wskaźnik musi wskazywać na zmienną const int * const x = &st; // dobrze - stały wskaźnik do stałej int i = 7; int * const wi = &i; // stały wskaźnik do zmiennej lokalnej i int * const pz = &zm; // stały wskaźnik do zmiennej zm cout << *wi << endl; // i=7 = i cout << *pz << *w << endl; // 10 i 10; zm=10 *wi +=8; // i = 7 + 8 = 15 cout << i << endl; // i =15 // wi = &zm; // błąd – nie wolno zmieniać stałej wi *pz += 2; // zm = 10 + 2 cout << zm << endl; // zm = 12 14
Wskaźniki do funkcji typ_zwracany (*wsk_do_funkcji)([argumenty_funkcji]); #include <iostream.h> void f1(); void f2(); void blad(); void main(){ void (*pf)()=blad; int i; cin>>i; if(i==1) pf=f1; else if(i==2) pf=f2; (*pf)(); } void f1(){ cout<<"funkcja f1\n"; void f2(){ cout<<"funkcja f2\n"; void blad(){ cout<<"blad\n"; 15
Wskaźniki do funkcji Nazwa funkcji jest adresem jej początku (adresem miejsca w pamięci, gdzie zaczyna się kod tej funkcji). Zastosowanie wskaźników do funkcji: Przesyłanie argumentów do funkcji. Adres funkcji można wysłać jako argument. Tworzenie tablic ze wskaźników do funkcji. 16
Przesyłanie argumentów do funkcji #include <iostream.h> void f1(); void f2(); void blad(); void funkcja(int i, void (*pf)()); void main(){ void (*pf)()=NULL; int i; cin>>i; funkcja(i,pf); } void funkcja(int i, void (*pf)()){ pf=blad; if(i==1) pf=f1; else if(i==2) pf=f2; (*pf)(); void f1(){ cout<<"funkcja f1\n"; void f2(){ cout<<"funkcja f2\n"; void blad(){ cout<<"blad\n"; Przesyłanie argumentów do funkcji 17
Tablice wskaźników typ_wskazywany *tablica[rozmiar]; //typ_wskazywany *(tablica[rozmiar]); zapis równoważny double *tablica[10]; 18
Tablice wskaźników - cd #include <iostream.h> void f1(); void f2(); void blad(); void main(){ void (*pt[3])()={f1,f2,blad}; for(int i=0;i<3;i++) (*pt[i])(); } void f1(){ cout<<"funkcja f1\n"; void f2(){ cout<<"funkcja f2\n"; void blad(){ cout<<"blad\n"; Tablica wskaźników do funkcji 19
Pamięć a zmienne w programie Ze względu na czas życia wyróżnia się: - obiekty statyczne - istniejące od chwili rozpoczęcia działania programu aż do jego zakończenia - obiekty dynamiczne - tworzone i usuwane z pamięci w trakcie wykonania programu: - automatycznie, czyli bez udziału programisty - kontrolowane przez programistę
Pamięć a zmienne w programie O tym czy obiekt jest statyczny czy dynamiczny decyduje miejsce deklaracji oraz klasa pamięci (przypisywana jawnie w deklaracji lub przyjmowana domyślnie). Klasę pamięci określają słowa kluczowe: auto, static, register, umieszczane przed deklaracją zmiennej, np. static int x; wszystkie zmienne globalne są statyczne wszystkie zmienne lokalne zadeklarowane bez jawnego specyfikowania klasy pamięci są automatyczne (auto). Zmienną lokalną można uczynić statyczną dodając przed deklaracją static (czasami stosowane w funkcjach) zmienne klasy register są automatyczne
Funkcje malloc, calloc, free Dynamiczna alokacja pamięci Funkcje malloc, calloc, free W języku C/C++ istnieją standardowe funkcje umożliwiające dynamiczną alokację pamięci (malloc i calloc) oraz funkcja zwalniająca przydzieloną pamięć (free). Prototypy funkcji: void *malloc(size_t K); // alokacja K bajtów void *calloc(size_t N, size_t K); // alokacja N razy po K bajtów void free(void *x); // zwolnienie obszaru Funkcje malloc i calloc przydzielają spójne obszary pamięci, których rozmiary nie przekraczają 64 KB (size_t jest zdefiniowany jako unsigned) i zwracają wskazanie do przydzielonego obszaru. Jeżeli alokacja nie jest możliwa, to zwracany jest wskaźnik NULL. Funkcja calloc dodatkowo zeruje przydzieloną pamięć. Funkcja free zwraca do systemu przydzieloną pamięć. Funkcje malloc i calloc zwracają wskaźniki do typu void dlatego niezbędne są konwersje typu przy podstawieniach do wskaźników innych typów. Należy uważać, aby za pomocą funkcji free nie zwalniać pamięci, która nie została przydzielona. 22
Funkcje malloc, calloc, free Dynamiczna alokacja pamięci Funkcje malloc, calloc, free Przykład: void main() { int i; char* ps = NULL; //wskaźnik do char ps = (char*) malloc(5 * sizeof(char)); //alokacja pamięci na //elementy typu char if (ps) strcpy(ps, "Ola"); for (i = 0; i < strlen(ps); i++) printf("%c\n", *(ps + i)); //wypisanie i-tego //elementu tablicy free(ps); //zwolnienie pamięci ps = NULL; } 23
Dynamiczny przydział pamięci - przykład Przydział pamięci dla n-liczb typu int, wczytanie liczb, obliczenie średniej #include <stdio.h> #include <stdlib.h> int main() { int *tab, i, n, x; float suma = 0.0; printf("Podaj ilosc liczb: "); scanf("%d",&n); tab = (int *) calloc(n,sizeof(int)); if (tab == NULL) printf("Nie mozna przydzielic pamieci.\n"); exit(-1); } for (i=0; i<n; i++) /* wczytanie liczb */ printf("Podaj liczbe nr %d: ",i+1); scanf("%d",&x); tab[i] = x; for (i=0; i<n; i++) suma = suma + tab[i]; printf("Srednia %d liczb wynosi %f\n",n,suma/n); free(tab); return 0;
Dynamiczny przydział pamięci na tablicę dwuwymiarową (macierz): Zamiast standardowego odwołania do elementów macierzy: tab[i][j] stosujemy odwołania do odpowiednich elementów wektora: *(tab+M*i+j) lub tab[M*i+j], gdzie M –liczba kolumn Przykład: tablica tab, dla której N=3, M=4 odwołujemy się do elementu tab[2][2]: *(tab+4*2+2)
Przydział pamięci na tablicę NxM, wygenerowanie i wyświetlenie liczb #include <stdio.h> #include <stdlib.h> #include <time.h> #define N 4 #define M 6 int main() { int i,j,*tab; tab = (int *) calloc(N*M,sizeof(int)); srand(time(NULL)); for (i=0;i<N;i++) for (j=0;j<M;j++) *(tab+M*i+j) = rand()%100; printf("%4d",tab[M*i+j]); printf("\n"); } free(tab); return 0;
Metoda 2 (wskaźnik na tablicę wskaźników): Przydzielamy pamięć na N-elementowy wektor wskaźników na typ int, a następnie do kolejnych elementów tego wektora zapisujemy adresy M-elementowych wektorów liczb typu int (pamięć na wektory jest także przydzielana dynamicznie) int **tab; tab = (int**) calloc(N,sizeof(int *)); for (i=0; i<N; i++) tab[i] = (int*) calloc(M,sizeof(int));
Przydział pamięci na tablicę NxM, wygenerowanie i wyświetlenie liczb #include <stdio.h> #include <stdlib.h> #include <time.h> #define N 4 #define M 6 int main() { int i, j, **tab; tab = (int **) calloc(N,sizeof(int *)); for (i=0;i<N;i++) tab[i] = (int*) calloc(M,sizeof(int)); srand(time(NULL)); for (j=0;j<M;j++) tab[i][j] = rand()%100; printf("%4d",tab[i][j]); printf("\n"); } free(tab[i]); /*w pierwszej kolejności zwalniamy pamięć przydzieloną na N wektorów, każdy o rozmiarze M */ free(tab); /*zwalniamy pamięć przydzieloną na N-elementowy wektor wskaźnikow na typ int */ return 0;