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 [] !