Wykład 6

Transkrypt

Wykład 6
Dr Mirek Łątka
Programowanie w C
Wiosna 2011
Wykład 6
Wskaźniki (na podstawie Prata, rozdział 4)
Program komputerowy musi pamiętać o trzech rzeczach aby móc korzystać z danych:
•
gdzie w pamięci operacyjnej są przechowywane informacje
•
jaka wartość jest przechowywana
•
jakiego typu jest przechowywana wartość.
Omawialiśmy to zagadnienie przy okazji definicji prostych zmiennych. Instrukcja deklaracji opisuje
typ i symboliczną nazwę wartości; wystarczy to programowi do przydzielenia (alokacji) niezbędnej
pamięci i do zapamiętania położenia tej danej.
Teraz omówimy inną strategię przechowywania danych, która ma fundamentalne znaczenie dla
programowania obiektowego. Strategia ta opiera się na wskaźnikach, zmiennych, które nie
przechowują wartości, ale jedynie ich adresy. Zanim jednak omówimy wskaźniki, pokażmy jak
określić adres zwykłej zmiennej. Wykorzystamy do tego celu jednoargumentowy operator adresu &
Zazwyczaj adres jest wyświetlany w notacji szesnastkowej
Wskaźniki zawierają adresy wartości, a więc nazwa wskaźnika opisuje położenie wartości.
Zastosowanie operatora *, zwanego operatorem dereferencji, pozwala pobrać wartość umieszczoną
pod danym adresem (tak, to ten sam symbol, który używa się przy mnożeniu; kompilator na podstawie
kontekstu określa czy chodzi o mnożenie czy dereferencję). Niech p będzie wskaźnikiem. Wtedy p
zawiera adres, a *p wartość umieszczoną pod tym adresem.
Po dopisaniu powyższego fragmentu kodu otrzymamy:
Jak widzimy zmienna x typu int oraz wskaźnik p to dwa spojrzenia na ten sam byt. x to przede
wszystkim wartość, ale za pomocą operatora adresu & można pobrać jej adres. p to przede wszystkim
adres, ale za pomocą operatora dereferencji *można pobrać wskazywaną przez niego wartość.
Wyrażenie *p można użyć wszędzie tam gdzie normalnie używa się zmiennej typu int. Można nawet
przypisać *p nową wartość, spowoduje to jednocześnie zmianę wskazywanej zmiennej czyli x.
Zwróć uwagę, że kompilator musi kontrolować typ wartości wskazywanej przez wskaźnik. Na
przykład adres wartości typu char wygląda tak samo jak adres wartości typu double, ale oba te typy
zajmują inną liczbę bajtów, więc trzeba je odróżniać od siebie i dlatego w deklaracji wskaźnika trzeba
określić typ wskazywanej wartości. W naszym przypadku deklaracja ma postać: int *p;
może ona
jednak wyglądać nieco inaczej int* p; Są one równoważne, drugi sposób podkreśla fakt, że mamy do
czynienia ze wskaźnikiem na liczby całkowite typu int. Uważaj na deklaracje typu: int* p1, p2; to w
rzeczywistości deklaracja jednego wskaźnika (p1) i jednej zmiennej typu int (p2). Przed każdą nazwą
zmiennej, która ma być wskaźnikiem musisz umieścić gwiazdkę.
Program obiektowy różni się od klasycznego programu proceduralnego (tylko takie pisaliśmy do tej
pory) o tyle, że w programie obiektowym decyzje podejmowane są głównie w czasie wykonywania
programu, a nie w czasie kompilacji. Czas wykonywania to czas, kiedy program jest uruchomiony, a
czas kompilacji ma miejsce wtedy, kiedy kompilator zestawia program w całość. Decyzje podejmowane w trakcie działania programu można porównać do wakacyjnych decyzji o tym, gdzie się udać,
podejmowanych już w trakcie wypoczynku. Można wtedy uwzględnić aktualną pogodę czy swój
nastrój. Natomiast decyzje podejmowane podczas kompilacji oznaczają trzymanie się ustalonego
schematu postępowania niezależnie od warunków.
Decyzje podejmowane w trakcie działania programu zapewniają elastyczność pozwalającą dostosować
się do aktualnych okoliczności. Weźmy na przykład pod uwagę alokację pamięci na tablicę. Tradycyjne
podejście zakłada zadeklarowanie tablicy. W tym celu trzeba ustalić od razu rozmiar tej tablicy, już na
etapie kompilacji wielkość ta staje się stałą. A jeśli uważamy, że w 80% przypadków wystarczy nam 20
elementów, ale czasami program będzie potrzebował 200 elementów? Na wszelki wypadek trzeba użyć
tablicy 200-elementowej. Powoduje to, że program przez większość czasu marnuje pamięć.
Programowanie obiektowe próbuje uelastycznić program, zostawiając takie decyzje na czas
wykonywania tego programu. W ten sposób można na bieżąco decydować, kiedy potrzebujemy 20
elementów, a kiedy 205.
Krótko mówiąc, w programowaniu obiektowym decyzję o wielkości tablicy należy podejmować
podczas wykonywania programu. Aby było to w ogóle możliwe, program musi pozwalać tworzyć
tablice lub ich odpowiedniki dynamicznie czyli już w trakcie działania programu.
Teraz przeanalizujmy alokowanie pamięci podczas działania programu za pomocą operatora new. Jak
dotąd, wskaźniki inicjalizowaliśmy adresem zmiennych. Zmienna to nazwany obszar pamięci
alokowany podczas kompilacji. Używany przez na wskaźnik p stanowił jedynie drugą nazwę zmiennej
służącą do sięgnięcia do tego samego miejsca pamięci. Wypróbujmy tworzenie nienazwanego miejsca
na dane typu int i sięganie do niego za pomocą wskaźnika.
Fragment int *pnowy= new int; mówi, że potrzebne jest nam miejsce na daną typu int. Operator new
na podstawie przekazanego typu oblicza liczbę potrzebnych bajtów, rezerwuje pamięć i zwraca jej
adres. Ten adres jest przypisywany wskaźnikowi pnowy. Zatem pnowy to adres a *pnowy to wartość.
Operator delete umożliwia zwalnianie niepotrzebnej pamięci: delete pnowy;
new i delete powinny się balansować. W przeciwnym razie dochodzi do wycieku pamięci (memory
leak), pamięć będzie alokowana, ale kiedy przestanie być używana, nie będzie zwalniana. Nie należy
jednak zwalniać raz już zwolnionego obszaru pamięci!
Wynik takiego działania jest bowiem
nieokreślony. Co więcej delete można używać tylko do zwalniania pamięci zaalokowanej za pomocą
new.
delete zwalnia pamięć, ale nie usuwa wskaźnika. Wskaźnik może być ponownie użyty
Poniższy fragment kodu pokazuje dość nieoczekiwany związek między wskaźnikami a tablicami.
Nazwa tablicy to adres jej pierwszego elementu, który możemy użyć do zainicjalizowania wskaźnika.
Zaskakujące jest to, że operator indeksowania [] można użyć dla wskaźników. I tak ptab[0] i ptab[4]
oznaczają odpowiednio pierwszy i ostatni element zadeklarowanej tablicy.
A oto przykład tworzenia tablicy dynamicznej.
Instrukcja int *p2= new int[5]; tworzy wskaźnik na pierwszy element 5-elementowego bloku wartości
typu int. Od tego momentu możemy traktować p2 jako zwykłą tablicę. Proszę zwrócić uwagę na
charakterystyczny dla dynamicznych tablic sposób zwalniania pamięci: delete [] p2; Nie można
zapomnieć o [] !