PO wyk07 v1

background image

Wskaźniki

Ogromną zaletą C++ i bardzo potężnym narzędziem jest możliwość
bezpośredniego manipulowania zawartością pamięci za pomocą wskaźników.
Należy jednak pamiętać, że wskaźniki są bardzo często przyczyną dużego
zamieszania w programach w C++.

Wskaźnik jest zmienną przechowującą adres w pamięci.

Pamięć jest miejscem przechowywania wartości. Pamięć dzieli się na
sekwencyjnie ułożone komórki. Każda komórka ma swój adres.
Każda zmienna, każdego typu umieszczona jest pod odrębnym adresem.

każda komórka = 1 bajt
zmienna nWiek typu unsigned long
= 4 bajty = 32 bity
nazwa zmiennej nWiek wskazuje
na pierwszy bajt
adres zmiennej nWiek to 102

nWiek

100 101 102 103104 105106 107108109110111


0101

1111010
1

0011000
1

1011000
0

0101

background image

Pamięć jest różnie adresowana, w zależności od typu komputera. Zazwyczaj
programista nie musi wiedzieć jaki jest szczegółowy adres danej zmiennej, od
tego jest kompilator. Jeśli jednak chcielibyście się bliżej z tym zapoznać,
trzeba wykorzystać operator adresu ( & ) .

Przykład

shortVar

longVar

sVar

0101

0000

5

ff90

ff81

ff82

ff83

ff84

ff85

ff86

ff87

ff88

ff8a ff8c

ff8d ff8f

ff8e

ff8b

ff89

1111

1111

0000

1111

1111

0000

0000

1000

- 65535

65535

shortVar

longVar

sVar

0101

0000

5

ff90

ff81

ff82

ff83

ff84

ff85

ff86

ff87

ff88

ff8a ff8c

ff8d ff8f

ff8e

ff8b

ff89

1111

1111

0000

1111

1111

0000

0000

1000

- 65535

65535

background image

Przypisywanie adresu do wskaźnika

Każda zmienna ma swój adres. Nawet nie znając konkretnego adresu
zmiennej można go przypisać do wskaźnika.
Przykład
Załóżmy, że mamy zmienną całkowitą typu int o nazwie nWiek. Aby
zadeklarować wskaźnik do przechowywania adresu tej zmiennej trzeba
napisać:

int *pWiek = 0;

Kiedy deklaruje się zmienną wskaźnikową, to można w niej umieścić adres
jakiegoś obiektu w pamięci.
W tym przypadku zmienna wskaźnikowa pWiek przechowuje adres zmiennej
całkowitej typu int.

Zauważmy, że zainicjalizowaliśmy wskaźnik pWiek wartością 0. Wskaźnik,
którego wartość wynosi zero określany jest jako

null

(pusty, nie wskazujący

na żaden obiekt). Jeśli nie wiecie jaki adres przypisać do wskaźnika, to
przypiszcie mu wartość zero.
Wskaźniki niezainicjalizowane żadną wartością określane są jako

dzikie

wskaźniki

. Stanowią one potencjalne zagrożenie dla programu, gdyż mogą

przechowywać adres dowolnej komórki pamięci (nie wiemy jakiej).
Modyfikacja pamięci pod tym adresem może doprowadzić np. do zawieszenia
się komputera.

background image

Chcielibyśmy teraz przypisać mu adres zmiennej nWiek.

int nWiek = 50; //stwórz zmienna
int *pWiek = 0; //stwórz wskaźnik
pWiek = &nWiek; //wstaw adres do wskaźnika

Przypisaliśmy adres dzięki operatorowi adresu ( & ). Gdybyśmy zapomnieli o
tym operatorze, to do wskaźnika zostałaby przypisana wartość zmiennej
nWiek (a nie jej adres). Oznacza to, że wskazywałby on na zupełnie inną
komórkę pamięci, niż oczekiwaliśmy.

Dostęp do zmiennej za pomocą jej adresu określany jest jako dostęp
pośredni.

Dostęp

pośredni

oznacza

modyfikowanie

lub

odczytywanie wartości zmiennej za pośrednictwem jej adresu
przechowywanego we wskaźniku.

Wskaźniki, tak jak wszystkie inne zmienne, mogą mieć dowolne,
poprawne w C++ nazwy. Przyjmijmy konwencję nazywania
wskaźników rozpoczynając nazwę od litery p (z ang. wskaźnik
-
pointer),
np.: pWiek, pLiczba itp.

background image

Operator dostępu pośredniego

Operator dostępu pośredniego ( * ) może służyć do odczytywania i zmieniania
wartości zmiennej, przechowywanej pod adresem zawartym we wskaźniku.
Normalna zmienna pozwala na bezpośredni dostęp do swojej wartości.

Przykład

Jeśli stworzy się zmienną typu int o nazwie nTwojWiek i chce się jej przypisać
wartość zmiennej nWiek to można to zrealizować w następujący sposób:

int nTwojWiek;
nTwojWiek = nWiek;

Wskaźnik pozwala na pośredni dostęp do wartości zmiennej, której adres
przechowuje. Żeby przypisać wartość zmiennej nWiek do zmiennej nTwojWiek
posługując się wskaźnikiem pWiek, trzeba napisać w ten sposób:

int nTwojWiek;
nTwojWiek = *pWiek;

Operator dostępu pośredniego (*) przed zmienną pWiek oznacza "wartość
przechowywana pod adresem zawartym w
". To przypisanie można przeczytać
następująco: "Weź wartość przechowywaną pod adresem zawartą we
wskaźniku pWiek i przypisz ją do zmiennej nTwojWiek
".

background image

Wskaźniki, adresy i zmienne

Bardzo ważne jest, aby odróżniać wskaźnik, adres który ten wskaźnik
przechowuje i wartość przechowywaną pod adresem zawartym we
wskaźniku. Wiele nieporozumień wynika z nieprawidłowej interpretacji i
błędnego rozumienia tych trzech różnych terminów.

Przykład

int nZmienna = 5;
int *pWskaznik = &nZmienna;

pWskaznik jest zadeklarowany jako wskaźnik na zmienną typu int i jest
inicjalizowany adresem zmiennej nZmienna. pWskaznik (jak sama nazwa
wskazuje) jest wskaźnikiem. Adres przechowywany przez pWskaznik jest
adresem zmiennej zmienna. Wartość pod adresem przechowywanym przez
pWskaznik jest równa 5.

nZmienna

100 101 102 103104 105106 107108109110111

0000010
1

5

pWskaznik

0000
0000

0000
0101

101

background image

Manipulowanie danymi za pomocą

wskaźników

Jeśli przypisze się do wskaźnika adres jakiejś zmiennej, to można
wykorzystywać ten wskaźnik do manipulowania wartością tej zmiennej.

Przykład

odczytujemy wartość spod adresu
przechowywanego

w

pWiek

i

wypisujemy tę wartość.

do

zmiennej

o

adresie

przechowywanym

we

wskaźniku

pWiek

(nMojWiek),

przypisujemy

wartość 7. Na 7 zmienia się zawartość
zmiennej nMojWiek.

do zmiennej nMojWiek przypisujemy
wartość 9. Wartość tę, bezpośrednio
i pośrednio odczytujemy w tych
fragmentach kodu

background image

Kontrolowanie adresu

Wskaźniki pozwalają na manipulowanie adresami bez wiedzy o ich faktycznej
wartości. Powiedzieliśmy, że kiedy przypisujemy adres zmiennej do
wskaźnika, to on na prawdę ma wartość równą adresowi tej zmiennej.
Dlaczego by jednak tego nie sprawdzić?

Przykład

Kwintesencja
wskaźników:

Co przechowuje
wskaźnik?

Jak odczytać tę
wartość?

background image

Zawsze do odczytania lub modyfikacji wartości zmiennej

przechowywanej pod danym adresem, wykorzystuj operator
adresowania pośredniego ( *).

Zawsze inicjalizuj wskaźniki albo konkretnym adresem zmiennej

wartością null (lub 0).

Zawsze pamiętaj o różnicy pomiędzy adresem przechowywanym

we wskaźniku, a wartością przechowywaną pod adresem.

Zazwyczaj wskaźniki są wykorzystywane w trzech sytuacjach:

Zarządzanie danymi w pamięci operacyjnej.

Dostęp do wnętrza klas - danych i funkcji.

Przekazywanie wartości do zmiennych poprzez referencje.

Programiści na ogół wyróżniają pięć obszarów pamięci:

Obszar zmiennych globalnych

Wolna pamięć

Rejestry

Kod programu

Stos

background image

Zmienne lokalne wraz z parametrami funkcji są przechowywane na stosie.
Kod znajduje się w obszarze kodu programu (co jest chyba oczywiste).
Zmienne globalne również znajdują się w przeznaczonym dla siebie
obszarze. Rejestry są wykorzystywane do wewnętrznego zarządzania
funkcjami (np. do przechowywania adresu szczytu stosu lub wskaźnika
instrukcji). Cała pozostała pamięć jest dla programu wolna (określa się ją
czasem jako stertę - ang. heap).
Problem ze zmiennymi lokalnymi polega na tym, że wraz z zakończeniem
funkcji są one przez program "zapominane". Zmienne globalne rozwiązują ten
problem, jednak kosztem nieograniczonego dostępu do nich z dowolnego
miejsca w programie, co niesie ze sobą znaczną komplikację kodu.
Umieszczenie danych w wolnej pamięci operacyjnej rozwiązuje oba problemy.

Można z powodzeniem traktować wolną pamięć jako ogromy zbiór
sekwencyjne ułożonych komórek pamięci "czekających" na dane. Jednak
dostęp do tych komórek nie jest tak swobodny jak np. dostęp do stosu. Przed
wykorzystaniem komórki trzeba „poprosić" system operacyjny o przydzielenie
adresu i zarezerwowanie odpowiedniej liczby komórek. Dopiero wtedy można
taki adres przypisać do wskaźnika i wykorzystywać.

Stos, w momencie wyjścia z funkcji, jest automatycznie czyszczony.
Wszystkie zmienne lokalne są wyrzucane z pamięci. Dane na stercie trwają
aż do zakończenia programu. Jest możliwość zwolnienia zarezerwowanej
pamięci, jeśli nie jest ona już potrzebna.

background image

Zaletą sterty:

• pamięć w niej zarezerwowana jest dostępna tak długo, aż jej się
bezpośrednio nie zwolni. Jeżeli zarezerwuje się pamięć na stercie wewnątrz
funkcji to po zakończeniu funkcji, będzie ona nadal zarezerwowana.

• możliwość dostępu tylko przez te funkcje, które mają dostęp do wskaźnika
danego obszaru. Gwarantuje to spójność dostępu do danych i eliminuje
problem niepożądanej modyfikacji danych przez niepowołane do tego
funkcje.

Aby móc wykorzystywać pamięć na stercie trzeba mieć możliwość stworzenia
wskaźnika do obszaru na stercie i przekazania tego wskaźnika do wybranych
funkcji.

background image

new

Do alokacji (rezerwacji) pamięci służy w C++ słowo kluczowe new.
Następuje po nim nazwa typu obiektu dla którego rezerwujemy pamięć.
Dzięki temu kompilator wie, ile pamięci ma zarezerwować.

Wartością zwracaną przez new jest adres w pamięci. Musi on być
przypisany do wskaźnika.

Przykład

unsigned short int * pWskaznik;
pWskaznik = new unsigned short int;

unsigned short int * pWskaznik = new unsigned short int;

W obu przypadkach, pWskaznik wskazuje na stercie na wartość typu
unsigned short int.

background image

Można ten wskaźnik wykorzystywać dokładnie tak, jak wskaźnik na zmienną i
dowolnie przypisywać wartości do pamięci:

*pWskaznik = 72;

Oznacza to: "Wstaw 72 pod adres wskazywany przez pWskaznik" albo
"Przypisz 72 do obszaru wskazywanego przez pWskaznik".

background image

delete

Kiedy zakończy się operacje na zarezerwowanym obszarze pamięci i nie
będzie się jej już więcej wykorzystywać to należy użyć instrukcji delete na
wskaźniku do danego obszaru.
Wskaźnik zadeklarowany w funkcji jest zmienną lokalną tej funkcji, w
przeciwieństwie do pamięci na stercie, na którą wskazuje. Kiedy funkcja się
skończy to wskaźnik ten, tak jak wszystkie zmienne lokalne zostanie
wyrzucony z pamięci (ze stosu). Oznacza to, że wskaźnika już nie będzie, ale
obszar na stercie będzie nadal zarezerwowany. Taki obszar jest już dla
programu niedostępny. Takie zjawisko określane jest jako ulatnianie się
pamięci.
Tak zarezerwowana pamięci pozostanie zajęta (i niedostępna) aż do
zakończenia się programu.
Żeby zwolnić pamięć na stercie, musisz użyć słowa kluczowego delete.
Przykład

delete pWskaznik;

background image

Przykład

Mimo że w tym konkretnym
przypadku jest nadmiarowa
ta instrukcja delete (koniec
programu

automatycznie

zwolni całą zarezerwowaną
pamięć)

to

dobrym

zwyczajem jest zadbanie o
to, aby samemu zwolnić,
przed

zakończeniem

programu,

całą

wykorzystywaną pamięć na
stercie.

background image

Utrata obszarów na stercie

Innym przypadkiem, w którym tracimy dostęp do zarezerwowanego na
stercie obszaru, jest przypisanie nowego adresu do wskaźnika przed
zwolnieniem pamięci wskazywanej przez ten wskaźnik.

unsigned short int * pWskaznik = new unsigned short int;
*pWskaznik = 72;
pWskaznik = new unsigned short int;
*pWskaznik = 84;

Do wskaźnika pWskaznik ponownie przypisujemy, nowy adres obszaru na
stercie i w wstawiamy do tego obszaru wartość 84. Pierwszy obszar, ten z
wartością 72 jest nadal zarezerwowany ale już niedostępny, ponieważ
wskaźnik, który na niego wskazywał otrzymał nową wartość. Nie ma
możliwości odczytania ani zmiany zawartości tego obszaru. Będzie on
niepotrzebnie zajmował pamięć aż do zakończenia się programu.

unsigned short int * pWskaznik = new unsigned short int;
*pWskaznik = 72;
delete pWskaznik;
pWskaznik = new unsigned short int;
*pWskaznik = 84;

background image

Tworzenie obiektów na stercie

Tak jak tworzyliśmy wskaźniki do zmiennych typu int, tak samo możemy
stworzyć wskaźnik do dowolnego innego obiektu. Jeżeli zadeklaruje się obiekt
typu Kot, to można zadeklarować wskaźnik do obiektów tej klasy i stworzyć
obiekt na stercie, podobnie jak na stosie. Składnia jest tu taka sama jak w
przypadku liczb całkowitych:

Kot *pKot = new Kot;

Zostanie wywołany konstruktor domyślny, ten bez parametrów. Konstruktor
jest wywoływany zawsze w momencie tworzenia obiektu danej klasy,
niezależnie od tego, czy operacja ma miejsce na stosie, czy na stercie.

Usuwanie obiektów

Kiedy wywoła się delete na wskaźniku do obiektu klasy znajdującego się na
stercie, to przed zwolnieniem pamięci zajmowanej przez obiekt zostanie
wywołany destruktor klasy, której jest dany obiekt. Dzięki temu klasa ma np.
możliwość zwolnienia dodatkowo zarezerwowanej pamięci tak, jak jest to
robione podczas usuwania obiektów ze stosu w momencie wyjścia z funkcji.

background image

Przykład

na stosie tworzony jest obiekt
klasy ZwyklyKot, wskazywany
przez pRags.

background image

Dostęp do danych wewnętrznych klasy

Dotychczas, dostęp do wewnętrznych danych i funkcji obiektów klasy
zadeklarowanych lokalnie realizowany był poprzez użycie operatora kropka ( .
). Żeby dostać się do elementów obiektu stworzonego na stercie trzeba
pośrednio odwołać się do tego obiektu za pomocą wskaźnika.

Przykład

(*pRags).PobierzWiek();

wywołuje funkcję wewnętrzną
PobierzWiek()

Nawiasy gwarantują, że odwołanie do obiektu nastąpi przed próbą dostępu
do funkcji PobierzWiek ().

Ponieważ takie stosowanie wskaźników jest raczej nieporęczne, C++
oferuje specjalny operator dla pośredniego dostępu do obiektów
wskazywanych przez wskaźniki. Operator "wskazujący na" ( -> )
składający się z myślnika ( - ) i znaku większości ( > ). C++ traktuje
to jako pojedynczy symbol.

background image

Przykład

na stercie, tworzony jest
obiekt klasy ZwyklyKot.
Konstruktor

domyślny

ustala jego wiek na 5.

wywoływana jest metoda
PobierzWiek (). Ponieważ
odwołanie

następuje

poprzez wskaźnik, to
wykorzystujemy operator
"wskazujący na" ( -> ).

background image

Dane wewnętrzne na stercie

Jedna (lub więcej) zmienna wewnętrzna może być wskaźnikiem na obiekt
na stercie. Pamięć może być zarezerwowana w konstruktorze klasy (albo w
jednej z jej metod) i może być zwolniona w destruktorze.

Przykład

Funkcja wywołująca, w tym

przypadku main (), "nie wie", że

nJegoWiek i nJegoWaga są

wskaźnikami. main () wywołuje

metody PobierzWiek () i UstawWiek

() , a szczegóły operacji na pamięci

zaszyte są wewnątrz implementacji

klasy.

Podczas

kasowania

obiektu

oFilemon,

wywoływany

jest

automatycznie

destruktor

klasy.

Destruktor kasuje wskaźniki. Jeśli
wskaźniki wskazywałyby na obiekty
innej klasy, to zostałyby wywołane
destruktory tych klas.

background image

Wskaźnik this

Każda wewnętrzna funkcja klasy ma ukryty parametr: wskaźnik this.
this zawsze wskazuje na aktualny obiekt.
Przy każdym wywołaniu metod PobierzWiek () albo UstawWiek (), wskaźnik
this jest dołączany jako ukryty parametr.

Zadaniem wskaźnika this jest wskazywanie na obiekt, którego metoda
została wywołana. Zazwyczaj nie będziemy go potrzebowali, będzie się tylko
wywoływać metody i zmieniać zmienne wewnętrzne. Jednak czasami trzeba
zagwarantować dostęp do obiektu (np. zwrócić adres aktualnego obiektu). W
takiej sytuacji this będzie bardzo pomocny.
W normalnej sytuacji, aby dostać się do elementów klasy, nie potrzebuje się
wskaźnika this. Można jednak bezpośrednio odwołać się do this.

background image

Przykład

Funkcje dostępu PobierzDlugosc () i UstawDlugosc ()
bezpośrednio wykorzystują wskaźnik this do odczytania
i modyfikacji zmiennych wewnętrznych obiektu
Prostokąt. PobierzSzerokosc() i UstawSzerokosc ()
dokonują odczytania i modyfikacji klasyczną metodą.
Efekt jest taki sam w obu sytuacjach, jednak metoda
klasyczna jest bardziej przejrzysta.

background image

Wskaźniki const

Umieszczenie go przed nazwą typu, na samym początku deklaracji wskaźnika
powoduje że wskaźnik jest stały i nie może ulec zmianie. Słowo const po
gwiazdce i przed nazwą typu obiektu powoduje, że obiekt nie może być
zmieniony z wykorzystaniem deklarowanego wskaźnika.

Przykład

const int *pJeden;
int *const pDwa;
const int *const pTrzy;

pJeden jest wskaźnikiem na stałą typu int. Wartość ta nie może zostać
zmieniona z wykorzystaniem tego wskaźnika. Oznacza to, że nie możesz
napisać np. tak:

*pJeden = 5;

pDwa jest stałym wskaźnikiem na int. Wartość, na którą wskazuje może
się zmienić, ale wskaźnik nie. Oznacza to, że nie wolno napisać tak:

pDwa = &x

pTrzy jest stałym wskaźnikiem na
stałą
int. Zarówno wartość jak i
wskaźnik nie mogą się zmienić.


Document Outline


Wyszukiwarka

Podobne podstrony:
postfix krok po kroku v1 1
PO wyk06 v1
PO wyk02 v1
Szkolenie Q Motion Controllers po polsku V1 2 3 11 2003
PO wyk04 v1
PO wyk01 v1
PO wyk05 v1
PO wyk12 v1
PO wyk08 v1
postfix krok po kroku v1 1
PO wyk06 v1
postfix krok po kroku v1 0
WR 1273252 v1 Skrypt po breaku 1
Rehabilitacja po endoprotezoplastyce stawu biodrowego
Systemy walutowe po II wojnie światowej
HTZ po 65 roku życia
Zaburzenia wodno elektrolitowe po przedawkowaniu alkoholu

więcej podobnych podstron