WYKŁAD 6
Wskaźniki
Każda zmienna ma unikalny adres wskazujący początkowy obszar pamięci zajmowany przez tą zmienną. Ilość pamięci zajmowanej przez zmienną zależy od typu zmiennej.
Adres można przechowywać, zmienna która przechowuje adres do obiektu nazywa się wskaźnikiem.
Wskaźniki deklarujemy używając następującej składni:
typ_zmiennej * nazwa_zmiennej
Operator * informuje nas, że mamy do czynienia ze wskaźnikiem.
Wskaźnik nazywa się tak jak zmienna .
float pi = 3.14;
float *wsk;
wsk = π
float pi = 3.14;
float *wsk; - wsk � przechowuje adres
zmiennej typu float
wsk=π
pi = 3.14
wsk = FFF0
wsk
� przechowuje adres zmiennej pi
&pi
� adres zmiennej pi
printf("pi = %f", pi); printf("adres zmiennej pi = %p", wsk); EKRAN: pi = 3.14 EKRAN: adres zmiennej pi = FFF0; Przykłady:
char *wsk; - wsk jest wskaźnikiem do pokazywania na obiekty typu char
int *wsk; - wsk jest wskaźnikiem do pokazywania na obiekty typu int
float *wsk; - wsk jest wskaźnikiem do pokazywania na obiekty typu float
void *wsk; - wsk jest wskaźnikiem do pokazywania na obiektu nieznanego typu float *wsk_tab[10]; - tablica 10 wskaźników do liczb rzeczywistych
float (*wsk_tab)[10]; - wskaźnik do tablicy 10 liczb rzeczywistych
Treścią wskaźnika jest informacja, gdzie wskazany obiekt się znajduje.
Przed użyciem musimy wskaźnikowi nadać wartość początkową, czyli przypisać go do konkretnego obiektu.
Do inicjowania wskaźnika może służyć operator adresu & i może być stosowany tylko do obiektów zajmujących pamięć: zmienne, elementy tablic. Nie można go stosować do wyrażeń stałych i zmiennych typu register.
int *wsk;
int *wsk;
float a;
int i;
wsk = &a; //nieprawidłowo.
wsk = &i; //prawidłowo.
Chcemy wskaźnikiem do int pokazywać na float
Kiedy wskaźnik pokazuje już na konkretnie miejsce możemy odnieść się do tego obiektu na który on wskazuje, odczytać jego wartość, lub wpisać wartość pod wskazany adres.
Podstawową operacją na wskaźniku jest wyłuskanie, czyli odwołanie się do obiektu wskazywanego przez wskaźnik. Operacja ta nazywa się adresowaniem pośrednim.
Operatorem adresowania pośredniego jest jednoargumentowa * zapisywana jako przedrostek.
Zastosowana do wskaźnika daje zawartość obiektu wskazanego przez ten wskaźnik np.: int *wsk ;
int j, i=3;
wsk = &i;
wsk przechowuje adres zmiennej i.
cout<<"wartość zmiennej i wynosi "<<*wsk;
Ekran: wartość zmiennej i wynosi 3
int i, j;
int *wsk;
i=3;
wsk=&i;
j =*wsk;
=*wsk - pobierz wartość spod adresu
(1) j =*wsk - pobranie wartości spod adresu i przypisanie tej wartośći zmiennej j po wykonaniu intsrukcji (1) j=3;
(2) *wsk = 200; // wstaw wartość 200 pod adres wskazywany przez zmienną wsk po wykonaniu intsrukcji (2) i = 200;
Zmieni się wartość zmiennej i z 3 na 200.
Jeśli wsk wskazuje na zmienną całkowitą, to *wsk może wystąpić wszędzie tam gdzie może wystąpić zmienna, np:
int *wsk, x, zm;
zm=10;
wsk = &zm;
zm=zm+2; //zm=12
zm=10;
*wsk = *wsk + 2; // zm = 12;
x = *wsk + 1; //x=13;
Ponieważ operatory * oraz & są silniejsze niż operatory arytmetyczne, to dla wyrażeń:
*wsk = *wsk + 2;
x = *wsk + 1;
najpierw pobierana jest zawartość spod adresu wsk i zwiększona o 1 zostaje przypisana zmiennej x.
int i, j;
int *wsk=&i, *wsk1;
wsk1 = wsk;
Zapis ten mówi, że teraz wsk będzie wskazywał na to samo co wsk1, czyli na zmienną i.
wsk=wsk+1; wskaźnik zostaje powiększony o rozmiar typu obiektu na który wskazuje, w naszym przypadku o sizeof (int) (czyli o dwa bajty) ponieważ wskazuje na obiekt typu int.
wsk=wsk+1; lub wsk++;
Wskaźniki a tablice
W C++ wskażniki i tablice są ze sobą ścisle związane. Nazwa tablicy może byc używana jako wskaźnik do jej pierwszego elementu.
Zadeklarujemy wskaźnik i tablicę:
int *wsk;
int tab[10];
instrukcja: wsk = &tab[n];
ustawia wskaźnik na n-tym elemencie tablicy. Typ wskaźnika musi się zgadzać z typem tablicy.
instrukcja: wsk = &tab[0]; jest równoważna instrukcji: wsk = tab;
i oznacza ustawienie wskaźnika na pierwszy element tablicy czyli na jej początek. Zapisy są równoważne, ponieważ jak już wspomnieliśmy, nazwa tablicy stanowi adres jej zerowego elementu.
int *wsk; int tab[4]={-1,-20, 3, 5};
wsk = tab;
2000 2002 2004 2006
tab:
-1
-20
3
5
wsk
wsk +1
wsk +2
wsk +3
cout<<*wsk << " to wyświetlenie wartości tab[0]\n";
cout<<*(wsk+1) << " to wyświetlwnie wartości tab[1]\n"; cout<<*(wsk+2) << " to wyświetlwnie wartości tab[2]\n"; cout<<wsk[0]<< "\n"<<wsk[1] <<"\n"<<wsk[2]; Możemy również zmieniać adres dodając do wskaźnika liczbę całkowitą.
Przejście do następnego elementu tablicy umożliwia instrukcja :
wsk= wsk + 1; lub wsk ++; //gubimy adres początkowy
Aby przesunąć się o n elementów w tablicy piszemy
instrukcja: wsk = wsk+n;
Z definicji wskaźnika wynika że wsk jest wskaźnikiem do int. Stąd kompilator wnioskuje, że aby odnaleźć następny element typu int należy przesunąć się o sizeof (int).
cout<<*wsk << "to wyświetlwnie wartości tab[0]";
wsk++; //wsk=wsk+1;
cout<<*wsk<< \
� n <
� < "to wyświetlwnie wartości tab[1]";
wsk++; //wsk=wsk+2;
cout<<*wsk<< \
� n <
� < "to wyświetlwnie wartości tab[2]";
int *wsk;
int tab[10]={1, 3, 5, 6, 7, 8, 9}, i;
wsk = tab; //inicjowanie wskaźnika
for(i = 0; i<10; i++) printf("%d\n", *wsk++);
Uwaga!!!
Mimo, że możemy zapisać:
wsk = tab;
to o ile możemy zapisać:
wsk++;
to nie możemy napisać:
tab++;
Różnicą między wskaźnikiem a nazwą tablicy jest taka, że na wskaźniku możemy dokonywać operacji arytmetycznych, a na adresie tablicy nie, jest ona traktowana jak wielkość stała.
Dla tak zadeklarowanego wskaźnika:
int *wsk;
adresem tego wskaźnika jest wartość wyrażenia:
&wsk;
Wskaźnik void
Deklaracja wskaźnika niesie w sobie dwie informacje: adres miejsca w pamięci, oraz typ obiektu na który te adres wskazuje.
Przy deklaracji
void *wsk;
wskaźnik wsk wskazuje jedynie na konkretne miejsce w pamięci nie informując o typie obiektów tam przechowywanych.
void *adres;
char *wsk;
int *wsk1;
//inicjalizowanie zmiennych wsk i wsk1.
.................................
adres = wsk; //lub adres = wsk1;
Zapisy te oznaczają, że teraz wskaźnik typu void wskazuje na to samo, na co wskazuje wskaźnik typu char //int.
Wskaźnik każdego typu można przypisać wskaźnikowi typu void, bez konieczności konwersji.
Odwrotne przypisanie wymaga stosowania operatora konwersji.
void *wsk1;
....................
wsk = wsk1; //błąd
wsk = (float *) wsk1; //poprawnie z operatorem rzutowania
Ary
tmetyka wskaźników
1. Możemy dodawać i odejmować liczby całkowite od wskaźników tak, aby w potrzebny sposób przesuwać je po tablicy. Operacje te nie są sprawdzane przez kompilator, i możemy przesunąć wskaźnik poza zadeklarowany obszar tablicy i zniszczyć istniejące dane. Takie błędy są trudne do wykrycia.
int *wsk; int tab[4]={-1,-20, 3, 5};
wsk = tab;
wsk=wsk+10; lub wsk=wsk-10;
co najwyżej wsk=wsk+3 lub wsk=wsk-3
2. Możemy odjąć dwa wskaźniki od siebie:
wsk_a - wsk_b
Gdy pokazują one na różne elementy tej samej tablicy to wynikiem takiej operacji jest liczba dzielących je elementów. Liczba może być ze znakiem - lub +.
int tab[12], *wsk;
wsk= &tab[0]
� &tab[2]; //wsk = -2
3. Wskaźniki można ze sobą porównać. Do tego celu służą nam operatory:
== != < > <= >=
Dla dwóch wskaźników:
int *wsk1, *wsk2;
przypisanie: wsk1 = wsk2 oznacza, że wskazują one na ten sam obiekt.
if (wsk1 = = wsk2)
cout<<"Oba wskaźniki pokazują na ten sam obiekt";
Jeśli wskaźniki wskazują na elementy tej samej tablicy, to wyrażenie wsk1 < wsk2 oznacza, że wsk1 wskazuje na element tablicy o mniejszym indeksie.
4. Każdy wskaźnik można porównać z adresem 0 zwanym NULL. Takie ustawienie wskaźnika: wsk = 0; //lub wsk = NULL
informuje, że wskaźnik nie pokazuje na nic konkretnego, niektóre funkcje biblioteczne zwracają wskaźnik NULL (null pointer), możemy go użyć do kontroli np: if (wsk = = 0) if(wsk = =NULL) if(!wsk)
Inicjo
wanie wskaźników
W tym punkcie zbierzemy wszystkie sposoby inicjowania wskaźników:
1. można przypisać adres konkretnego obiektu:
int *wsk, obiekt, tab[12];
wsk = &obiekt;
wsk = tab; // wsk = &tab[0]; wsk = &tab[2];
2. zarezerwować obszar dynamicznie
char *wsk;
wsk = new char[12];
wsk = (char*) malloc (12 * sizeof (char));
3. można przypisać inny wskaźnik:
int *wsk, *ptr;
wsk = new int;
ptr = wsk;
4. ustawić wskaźnik na konkretny
wsk = FFDA
Dynamiczna alokacja pamięci
Stajemy więc przed problemem, jak tworzyć tymczasowe obiekty tak, i gdy nie będą potrzebne pozbyć się ich z pamięci.Odpowiedź jest prosta - rezerwować pamięć.
W języku C pamięć dostępna dla programu w czasie jego uruchomienia nazywa się HEAP'em, w C++ - FREE STORE- pamięć wolna. Różnica leży tylko w funkcjach używanych do dostępu do tej pamięci.
Funkcje alokacji pamięci: malloc(...), calloc(...)
W języku C, do alokacji pamięci służy grupa funkcji malloc.
void *malloc (size_t size); typedef unsigned size_t
Przydziela w obszarze stosu zmiennych dynamicznych obszar o rozmiarze size i zwraca wskaźnik do n bajtów niezainicjowanej pamięci, lub NULL jeśli żądanie nie może być spełnione.
void *calloc (size_t n, size_t size);
Przydziela w obszarze stosu zmiennych dynamicznych obszar o rozmiarze size n*size oraz zwraca NULL, gdy pamięć nie może być przydzielona. Pamięć inicjowana jest zerami.
char *wsk = (char*) malloc (100 * sizeof (char));
char *ptr = (char*) calloc (100, sizeof (char));
Funkcje zwalniania pamięci: free(...)
Po wykorzystaniu pamięci można ją zwolnić. Do tego celu służy funkcja free().
free (ptr); free (wsk);
zwalnia pamięć wskazaną przez p, przy czym p musi być wynikiem wcześniejszego wywołania funkcji malloc() lub calloc(). Nie ma ograniczeń na kolejność zwalniania pamięci, natomiast poważnym błędem jest zwalnianie czegoś, co nie było poprzednio przydzielone w/w funkcjami.
W języku C możemy korzystać z jednego z 6 standardowych modeli pamięci: tiny, small, medium,compact, large, huge które różnią się min. ilością pamięci przeznaczonej na dane. Dla modelu compact, large i huge, gdzie pamięć na dane jest ponad 64 kB, funkcja malloc zamieniana jest na funkcję farmalloc, farfree operujące na pamięci o długości ponad 1 segment.
Operatory new i delete.
Alternatywą do tych funkcji w języku C++ jest operator new i delete. Operator new tworzy obiekt, a operator delete usuwa obiekt z pamięci. Jeśli zdefiniujemy wskaźnik: char *wsk;
Alokacja pamięci.
wsk = new char;
powoduje utworzenie nowego obiektu typu char. Nie ma on nazwy, ale możemy się do niego odwoływać poprzez wskaźnik zawierający adres tego obiektu.
int *wsk_tab;
wsk_tab = new int[10];
operator new utworzył 10-elementowa tablicę typu int.
Zwalnianie pamięci.
delete wsk;
powoduje usunięcie obiektu wskazanego przez wsk z pamięci.
Kasowanie tablicy zarezerwowanej dynamicznie:
delete [] wsk_tab;
Zwróćmy uwagę na nawiasy kwadratowe.
Cechy obiektów utworzonych operatorem new
1. obiekty żyją od momentu utworzenia operatorem new aż do momentu usunięcia operatorem delete
2. obiekty nie mają nazwy. Operujemy na nich tylko przy pomocy wskaźników.
3. obiekty utworzone operatorem new nie są automatycznie
inicjowane
int dl_tab,i;
cout<<"podaj rozmiar tablicy: "
cin>>dl_tab;
int *wsk_tab = new int[dl_tab];
for(i = 0;i<dl_tab;i++)
*wsk_tab++ = i;
................................
delete [] wsk_tab;
Za pomocą operatora delete kasuje się tylko obiekty utworzone przy pomocy operatora new,
przy czym nie należy kasować wcześniej skasowanego obiektu. Można kasować natomiast wskaźnik ustawiony na NULL:
wsk = NULL;
delete wsk;
W trakcie alokowania pamięci może zdarzyć się tak, że operator new zwróci NULL. Oznacza to, że wyczerpaliśmy pamięć dostępną na dane. W związku z tym w programach tworzących dużą liczbę dużych obiektów należy kontrolować poprawność operacji alokacji. Można tego dokonać albo poprzez fragment programu:
int *wsk;
wsk = new int[10000];
if(!wsk)
cout<<"pamięć się wyczerpała";
lub przy wykorzystaniu funkcji set_new-handler:
Przykład
#include<iostram.h>
#include<stdlib.h> // exit
#include<new.h> //set_new_handler
long k;
void alarm(){
cout<<"Brak pamięci przy k = "<<k;
exit(1);
}
void main(){
set_new_handler(alarm);
for( k = 0; ; k++)
new int;
}
W funkcji main wykonuje się nieskończona pętla tworząca dynamicznie obiekty. Jeśli w którymś momencie zabraknie pamięci, sterowanie automatycznie przejmuje funkcja
set_new_handler uruchamiająca funkcję alarmową napisaną przez użytkownika. Argumentem tej funkcji jest wskaźnik do funkcji alarm
Wskaźniki
a stringi
Jeśli wskaźnik ma pokazywać na ciąg znaków, to można go zadeklarować jako:
char *text;
można go inicjalizować:
char *text = "Poniedziałek";
do tej pory:
char text_tab [] = "Poniedziałek";
char text_tab [13] = "Poniedziałek";
Przykład:
printf (″%s″, text); Ekran: poniedziałek
printf (″%s″, text_tab); Ekran: poniedziałek
printf (″%c″, text_tab[1]); Ekran: o
printf (″%c″, text[1]); Ekran: o
text++;
printf (″%s″, text); Ekran: oniedziałek
Przykład:
char text[]="BORLAND C++";
char *cel, *zrodlo;
il = strlen (text)+1; // +1 na znak NULL
cel = new char [il]; // rezerwacja miejsca
zrodlo=text;
(1) while ((*zrodlo) != NULL) {
*cel = *zrodlo;
cel++; zrodlo++; // BEZ NULL A
�
}
*cel=NULL;
(2) while (*cel = *zrodlo) // inaczej while ((*cel = *zrodlo) != \
� 0 )
�
{
cel++; zrodlo++; // Z NULL E
� M
}
(3) while (*cel++ = *zrodlo++); //Z NULL E
� M
(4) Można wykorzystać funkcję biblioteczną:
strcpy (cel, zrodlo); - kopiowanie znaków ze źródła do celu, znak NULL jest kopiowany.
Przykład: char *cel, zrodlo[]=″informatyka″;
cel=new char [strlen (zrodlo)+1];
strcpy (cel, zrodlo);