PO wyk08 v1

background image

Referencje

Referencja jest czymś w rodzaju synonimu lub odsyłacza. Gdy tworzy się
referencje, to trzeba ją zainicjalizować nazwą innego, docelowego obiektu. Od
tego momentu referencja spełnia rolę alternatywnej nazwy dla obiektu i
wszystkie operacje wykonywane na referencji będą również wykonywane na
obiekcie docelowym.
Niektórzy programiści twierdzą, że referencja to wskaźnik.

Nie jest to prawdą!

Oczywiście, zazwyczaj referencje tworzone są z wykorzystaniem wskaźników,
ale jest to sprawa tylko i wyłącznie twórców kompilatora.

Trzeba rozróżniać te dwa pojęcia:

wskaźniki są zmiennymi przechowującymi adresy innych obiektów.

referencje są "synonimami" lub odsyłaczami do obiektów.

Tworzenie referencji

Referencję tworzy się poprzez wpisanie nazwy typu docelowego
poprzedzonego symbolem operatora referencji ( & ). Następnie należy podać
nazwę dla referencji. Nazwa referencji może być dowolną, poprawną nazwą
C++.

Przyjmijmy że będziemy referencje nazywać zaczynając nazwę od litery r.

background image

Przykład:

Jeżeli mamy zmienną całkowitą nPom, to można stworzyć do niej referencję w
następujący sposób:

int &rPom = nPom ;

Taką deklarację można przeczytać: "rPom jest referencją do zmiennej typu int
zainicjowaną tak, aby odsyłała do zmiennej nPom
".

Przykład:

Zauważ, że referencja jest od razu
inicjalizowana.

Gdybyśmy

pominęli

inicjalizację, kompilator zgłosiłby błąd.

background image

Wykorzystanie operatora adresu na

referencjach

Jeżeli pobierze się adres referencji, to otrzyma się adres zmiennej, którą ta
referencja reprezentuje. Taka jest natura referencji - są one odsyłaczami do
obiektów docelowych.

Przykład:

W C++ nie ma możliwości otrzymania fizycznego adresu referencji gdyż
byłoby to zbędne (w przeciwieństwie do wskaźników). Referencje są
inicjalizowane w momencie tworzenia i zawsze stanowią synonim obiektu
docelowego, nawet w przypadku użycia operatora adresu.

background image

Jeżeli mielibyśmy klasę o nazwie Prezydent, to wówczas można zadeklarować
obiekt tej klasy następująco:

Przykład:

Prezydent oLechWalesa;

Prezydent &rWalesa = oLechWalesa;

Teraz można zadeklarować referencję do obiektu klasy Prezydent:

W pamięci będzie istnieć tylko jeden obiekt. Obydwa stworzone identyfikatory
odnoszą się do tego obiektu. Każda operacja wykonana na obiekcie rWalesa
zostanie również wykonana na oLechWalesa.

Zapamiętaj: aby odróżniać symbol & który deklaruje referencje (o
nazwie rWalesa) do obiektu klasy lub zmiennej, a symbolem &
zwracającym adres zmiennej. Normalnie, przy wykorzystywaniu
referencji, nie jest potrzebny operator adresu. Po prostu używa się
referencji tak, jak obiektu docelowego.

Czasami nawet doświadczeni programiści nie są pewni, co się stanie, przy
próbie ponownego przypisania do referencji. Okazuje się, że taka operacja
powoduje przypisanie nowej wartości do obiektu docelowego (co po chwili
namysłu jest oczywiste i zgodne z definicją referencji).

background image

Przykład:

Rezultat tej operacji jest inny niż
zamierzony. rRef nadal jest powiązana
ze zmienną nPom1, lecz zmianie uległa
wartość, która jest teraz równa 8.

Chcieliśmy zmienić znaczenie referencji
rRef i przypisać jej nowy obiekt
docelowy – nPom2.

Dzieje się tak dlatego ponieważ przypisanie:

nPom1 = nPom2 ;

rRef = nPom2 ;

background image

Do czego można tworzyć referencje?

Referencje można stworzyć dla dowolnego obiektu, niezależnie od tego, czy
jest on standardowym obiektem C++ czy też zdefiniowanym przez
użytkownika. Ważne jest, że referencja odnosi się do konkretnego obiektu, a
nie do klasy.

Oznacza to, że nie można napisać tak: int &rIntRef = int;

Trzeba zainicjować referencje za pomocą konkretnej zmiennej typu int:

int nPom = 200;
int &rIntRef = nPom ;

Podobnie rzecz się ma z obiektami zdefiniowanymi przez użytkownika. Nie
można napisać tak:

KOT &rKotRef = KOT ;

Tutaj, podobnie jak w przypadku zmiennej całkowitej, trzeba zainicjować
referencję za pomocą konkretnego obiektu klasy KOT:

KOT oFilemon ;
KOT &rKotRef = oFilemon ;

Referencje do obiektów wykorzystuje się tak, jak obiekty. Dane i funkcje
wewnętrzne dostępne są z wykorzystaniem operatora dostępu do klasy -
kropki ( . ).

background image

Puste (null) wskaźniki i puste (null) referencje.

Kiedy wskaźnik jest niezainicjowany lub ewentualnie skasowany za pomocą
delete, to powinna być my przypisana wartość null (0).

Referencja nie może być pusta ( null ). Program, w którym znajdują się
referencje do obiektów null jest błędny i jego działanie jest
nieprzewidywalne. Może on działać pozornie prawidłowo, lecz w pewnym
momencie może np. niespodziewanie skasować wszystkie zbiory na dysku.

Ta reguła nie obowiązuje w przypadku referencji !

Większość kompilatorów honoruje puste obiekty. Problemy pojawiają się
jedynie przy próbie wykonania operacji na takich obiektach. Pamiętajcie, że w
takiej sytuacji, próba przeniesienia programu pod inny kompilator lub na inną
maszynę może skończyć się niepowodzeniem jeśli to inne środowisko nie
zezwala na istnienie pustych ( null ) obiektów.

background image

Przekazywanie argumentów funkcji przez
referencje.

Powiedzieliśmy sobie, że funkcje mają dwa ograniczenia:

argumenty są przekazywane przez wartość

funkcja może zwrócić tylko jedną wartość.

Przekazywanie argumentów przez referencje pozwala na obejście obydwu
tych ograniczeń.
W C++, przekazywanie przez referencje, może być przeprowadzone
na dwa sposoby:

wykorzystaniem wskaźników,

referencji.

Wewnątrz funkcji nie jest tworzona kopia przekazywanego obiektu. Do
funkcji przekazywany jest oryginalny obiekt.
Przekazywanie obiektów przez referencje pozwala funkcji na
wykonywanie modyfikacji w tych obiektach.

Każde rozwiązanie ma własną składnię jednak efekt końcowy jest taki
sam.

background image

Przykład:

Problem polega tutaj na
tym, że zarówno x jak i y są
przekazywane

przez

wartość. Oznacza to, że są
tworzone lokalne kopie tych
zmiennych

i

wszystkie

operacje są wykonywane na
kopiach, nie powodując
żadnych zmian zmiennych
oryginalnych.

To,

czego

tutaj

potrzeba,

to

przekazywanie x i y przez
referencje.

Można ten problem rozwiązać na dwa sposoby:
1. można zamienić parametry funkcji Zamien() na wskaźniki do oryginalnych

wartości

2. przekazywać referencje.

background image

Ad.1. Implementacja funkcji Zamien () z wykorzystaniem
wskaźników

Kiedy przekazuje się wskaźnik, to faktycznie przekazuje się adres zmiennej,
co pozwala funkcji na manipulowanie jej wartością. Żeby funkcja Zamien ()
mogła zamieniać dwie zmienne wartościami, to trzeba ją zadeklarować tak,
aby jako parametry pobierała dwa wskaźniki do zmiennych typu int. Taki
zabieg pozwoli funkcji na pośrednie odwołanie się do tych zmiennych i
zamianę ich wartości.

Funkcja Zamien () jest wywoływana z
parametrami którymi są adresy zmiennych,
które chcemy zamienić.

Zmienna lokalna nTemp nie musi być wskaźnikiem ponieważ
jej zadaniem jest przechowanie wartości *pX

background image

Ad.2. Implementacja funkcji Zamien () z wykorzystaniem
referencji

Poprzedni program działa, lecz funkcja Zamien () jest raczej nieporęczna z

następujących powodów:

1. wielokrotne pośrednie odwoływanie się do zmiennych wskazywanych

powoduje, że takie rozwiązanie jest podatne na błędy

2. konieczność przekazywania adresów zmiennych powoduje "ujawnienie"

wewnętrznych mechanizmów funkcji Zamien () .

Zamierzeniem C++ jest oddzielenie użytkownika funkcji od jej wewnętrznych
mechanizmów i implementacji. Przekazywanie wskaźników przenosi część
ciężaru wykonania funkcji na funkcję wywołującą (na użytkownika).

Tym razem, bezpośrednio przekazywane są do
funkcji zmienne, a nie ich adresy.

W funkcji Zamien () zmienne x i y są
identyfikowane przez referencje. Można tak
napisać, gdyż jak już wcześniej wielokrotnie
mówiłem, stanowią one odnośniki, synonimy
oryginalnych zmiennych.

background image

Interpretacja nagłówków i prototypów funkcji

Poprzez analizę zadeklarowanych parametrów w prototypie, który zazwyczaj
znajduje się w specjalnym pliku nagłówkowym (*.hpp , *h ) wraz z innymi
prototypami, programista dowiaduje się, że parametry przekazywane do
funkcji zamień są przekazywane przez referencje, co w przypadku funkcji
Zamien () oznacza, że zostaną one prawidłowo zamienione.

Skąd jednak funkcja wywołująca wie, czy wartości są przekazywane
przez referencje czy przez wartość?

Zwracanie wielu wartości

Jak już mówiliśmy, każda funkcja, normalnie może zwrócić tylko jedną
wartość.

Jedna z możliwości polega na przekazaniu do funkcji przez referencje dwóch
roboczych obiektów. Funkcja może nadać im wymagane wartości.
Przekazanie przez referencje gwarantuje, ze przypisanie wartości nastąpi do
obiektów oryginalnych, co umożliwi ich odczytanie. W efekcie uzyskujemy
funkcję, która zwraca dwie wartości.

Co jednak zrobić, gdy chcemy zwrócić z funkcji dwie wartości?

Takie rozwiązanie pozwala na ominięcie normalnej wartości zwracanej przez
funkcję, którą można w tym momencie wykorzystać np. do zwracania kodu
błędu.

background image

Zmienna

nLiczba,

i

adresy

dwóch

pozostałych zmiennych, są przekazywane
do funkcji Potegi () .

Dodatkowe wartości, kwadratu i sześcianu podanej
liczby,

nie

zwracane

z

wykorzystaniem

mechanizmów oferowanych przez return. Zmieniona
zostaje zawartość pamięci wskazywanej przez
przekazane do funkcji wskaźniki, co powoduje
modyfikację

wartości

zmiennych

w

funkcji

wywołującej, do których te adresy nalezą.

background image

Ten program można udoskonalić, np. poprzez wprowadzenie deklaracji dla
wartości błędu i sukcesu:

enum WARTOSC_BLEDU {SUKCES, PORAZKA};

Teraz, zamiast zwracać niewiele mówiące wartości 0 lub 1, możemy zwracać
SUKCES albo PORAZKA.

Zwracanie wartości przez referencje

background image

Efektywność przekazywania przez referencje

Za każdym razem, kiedy przekazuje się przez wartość obiekt do funkcji,
tworzona jest kopia obiektu. Również, kiedy zwraca się obiekt z funkcji (za
pomocą return) jest tworzona kopia. W przypadku dużych, stworzonych
przez użytkownika obiektów, koszt tworzenia kopii jest znaczący.
Wykorzystywane jest więcej pamięci niż faktycznie potrzeba i program działa
wolniej.
Rozmiar obiektu stworzonego przez użytkownika na stosie jest równy sumie
wielkości jego zmiennych wewnętrznych. Każda zmienna wewnętrzna również
może być kolejnym obiektem stworzonym przez użytkownika. Przekazywanie
tak złożonego i dużego obiektu i kopiowanie go na stos może bardzo obciążać
system i spowalniać działanie programu, przy jednoczesnym wzroście
zapotrzebowania na pamięć.
Występuje tutaj jeszcze jeden, dodatkowy koszt.

Każda kopia obiektu jest wykonywana za pomocą specjalnego konstruktora, a
mianowicie konstruktora kopiującego. Kiedy taki chwilowy obiekt jest
kasowany (np. w momencie wyjścia z funkcji) to zostaje wywołany destruktor
klasy, do której ten obiekt należy. Jeśli obiekt jest wartością zwracaną, to
również musi być najpierw stworzony, a następnie skasowany.
W przypadku dużych obiektów, zarówno konstruktor jak i destruktor,
pochłaniają dużo czasu procesora i pamięci.

background image

Przykład:

Rzeczywisty obiekt byłby dużo większy,
ale ten przykład dostatecznie dobrze
pokazuje jak często są wywoływane
konstruktory i destruktory w trakcie
normalnej pracy programu.

background image

Ponieważ Funkcja1() pobiera obiekt klasy ZwyklyKot przez wartość, dlatego
na stosie musi zostać stworzona kopia obiektu. Zostaje wywołany konstruktor
kopiujący. Wartość zwracana z Funkcja1() nie jest przypisywana do żadnego
obiektu, zatem obiekt tworzony w momencie wyjścia z funkcji jest
natychmiast wyrzucany z pamięci poprzez wywołanie destruktora. Ponieważ
Funkcja1() się skończyła, dlatego lokalna kopia obiektu również ulega
skasowaniu poprzez wywołanie destruktora.

W funkcji Funkcja2 () parametr jest przekazywany przez wskaźnik. Jak widać
na wydruku, nie jest tworzona żadna kopia obiektu. Funkcja2 () wypisuje
komunikat i zwraca wskaźnik do obiektu klasy ZwyklyKot. Znowu nie jest
wywoływany ani konstruktor ani destruktor klasy.

Jak widać, wywołanie funkcji Funkcja1 (), ze względu na przekazywanie
parametrów przez wartość, spowodowało dwukrotne wywołanie konstruktora
i destruktora, w przeciwieństwie do funkcji Funkcja2 (), której wywołanie
odbywało się bez uczestnictwa konstruktora i destruktora klasy.

background image

Przekazywanie stałych (const) wskaźników

Mimo że przekazywanie wskaźników jest bardzo efektywne, to jest ono
jednocześnie bardzo niebezpieczne. Funkcja2 () (z ostatniego programu) nie
ma prawa zmieniać przekazanego obiektu, który aktualnie jest przekazywany
w formie adresu. Takie rozwiązanie niesie ze sobą niebezpieczeństwo
nieświadomej (spowodowane błędem programisty) zmiany oryginalnego
obiektu, w przeciwieństwie do przekazywania przez wartość, gdzie obiekt
oryginalny jest "bezpieczny".
Przekazywanie przez wartość, to jakby dawanie do muzeum fotografii dzieła
sztuki, a nie oryginalnego dzieła. Jeśli ktoś zniszczy kopie to oryginał i tak
zostanie nienaruszony. Przekazywanie przez referencje można porównać do
wysyłania do muzeum swojego, domowego adresu i zaproszenia do oglądania
własnych zasobów.

Rozwiązaniem tego problemu jest przekazywanie stałych (const)
wskaźników. Takie posunięcie zabezpiecza przed użyciem metod, które nie są
zadeklarowane jako const, przez co mogą zmieniać obiekt.

background image

Przykład:

W tym przykładzie konstruktor kopiujący nie jest nigdy wywoływany,
ponieważ obiekt jest przekazywany do funkcji przez wskaźnik i nie jest
wykonywana jego kopia.

Parametr i wartość zwracana zostały zadeklarowane tak, aby funkcja
pobierała stały wskaźnik (const) do stałego (czyli również const) obiektu i
żeby zwracała również stały wskaźnik do stałego obiektu. Ponieważ zarówno
parametr jak i wartość zwracana są przekazywane przez wskaźnik, to nie jest
tworzona żadna kopia obiektu i, co się z tym ściśle wiąże, nie jest
wywoływany konstruktor kopiujący. Wskaźnik występujący w funkcji Funkcja2
() jest stały i nie mamy możliwości wykorzystania go do wywołania funkcji
UstawWiek () (zadeklarowanej bez użycia słowa kluczowego const).

Gdybyśmy usunęli komentarz to kompilator
zgłosiłby błąd.

background image

Kiedy wykorzystywać referencje, a kiedy
wskaźniki?

Programiści C++ preferują referencje. Są łatwiejsze, bardziej eleganckie i
pozwalają na lepsze ukrycie szczegółów implementacyjnych.

 Jednak raz stworzona referencja nie może być przypisana do innego
obiektu.

 Jeżeli chce się mieć wskazanie najpierw na jeden obiekt, a następnie na
drugi, trzeba wykorzystać wskaźniki.

 Referencje nie mogą być puste (null) i jeżeli jest możliwość, że obiekt
będzie pusty to wykorzystanie referencji w takim przypadku jest błędem,
trzeba użyć wskaźnika.

Inny problem pojawia się przy wykorzystaniu operatora new. Jeżeli new nie
jest w stanie zarezerwować pamięci na stercie to zwraca pusty (null)
wskaźnik. Referencja nie może być pusta dlatego nie wolno inicjalizować
referencji do zarezerwowanej pamięci zanim nie upewnimy się, że wskaźnik
nie jest pusty.

Przykład:

Oto przykładowa sekwencja instrukcji, która prawidłowo obsługuje tworzenie
referencji do pamięci na stercie:

int *pInt = new int;
if (pInt != NULL)
{
int &rInt = *pInt;
}

background image

Nie zwracaj referencji do obiektów lokalnych

Często, kiedy programista pozna referencje, to wykorzystuje je, gdzie się
tylko da. Można wtedy, mówiąc prostym językiem, "przedobrzyć".
Pamiętajcie, że referencja jest zawsze odnośnikiem do innego, istniejącego
obiektu. Jeżeli przekazuje się referencje do lub z funkcji zawsze zadajcie sobie
pytanie, czy obiekt, do którego się dana referencja odnosi, będzie nadal
istniał.

Przykład:

Ten program nie skompiluje
się pod kompilatorami firmy
Borland. Kompilatory firmy
Microsoft skompilują ten kod,
ale trzeba zaznaczyć, że jest
to błędne podejście.

Funkcja

deklaruje

lokalny

obiekt

klasy

ZwyklyKot

i

inicjalizuje jego wagę i wiek.
Następnie zwraca referencje to
tego,

lokalnego

obiektu.

Niektóre kompilatory wychwycą
ten oczywisty błąd i nie
pozwolą

na

uruchomienie

programu. Inne skompilują to
bez

zastrzeżeń,

ale

efekt

działania takiego programu jest
nieprzewidywalny.
Kiedy

Funkcja

ulegnie

zakończeniu, to lokalny obiekt,
oFilemon, zostanie wyrzucony z
pamięci. Referencja zwrócona z
funkcji

odnosiłaby

się

do

nieistniejącego obiektu co jest
oczywiście

sprzeczne

z

definicją referencji.

background image

Zwracanie referencji do obiektu na stercie

Można rozwiązać stworzony w poprzednim programie problem poprzez
zmianę Funkcja tak, aby tworzyła obiekt klasy ZwyklyKot na stercie. Dzięki
temu, po zakończeniu funkcji, taki obiekt będzie nadal istniał.
Pojawia się tutaj kolejny problem: Co zrobić z pamięcią zarezerwowaną dla
obiektu oFilemon po zakończeniu wszystkich operacji.

Ten program się kompiluje i pozornie działa. Jednak jest to
klasyczna, programistyczna bomba zegarowa z opóźnionym
zapłonem!

Funkcja Funkcja () nie zwraca już referencji do
lokalnego

obiektu.

Teraz

pamięć

jest

rezerwowana na stercie i przypisywana do
wskaźnika.

Adres

przechowywany

przez

wskaźnik jest wypisywany, a następnie są
zwracane referencje do obiektu przez niego
wskazywanego.

Wartość zwracana z funkcji jest przypisywana
do referencji obiektu klasy ZwyklyKot. Obiekt
ten wykorzystujemy do odczytania wieku kota.
Dowodem na to, że referencja zadeklarowana w
main () odnosi się do obiektu stworzonego na
stercie jest odczytanie adresu tej referencji. Jest
on taki sam jak adres wypisany w Funkcja().

Ale

jak

teraz

zwolnić

zarezerwowaną pamięć? Nie
można skasować referencji za
pomocą

delete.

Jedyne

nasuwające się rozwiązanie
polega

na

zadeklarowaniu

kolejnego

wskaźnika

i

zainicjowaniu

go

adresem

odczytanym z rKot. W ten
sposób

możemy

zwolnić

zarezerwowaną

pamięć.

Jednak i tu pojawia się kolejny
problem. Do jakiego obiektu
odnosi się rKot? Jak już
mówiliśmy, referencja musi
odnosić się do istniejącego
obiektu. Jeśli jest inaczej
(obiekt jest null) to program
jest nieprawidłowo napisany.

background image

Istnieją trzy rozwiązania tego problemu:

Pierwsze polega na zadeklarowaniu obiektu klasy ZwyklyKot i
zwrócenie go z funkcji Funkcja () przez wartość.

Drugie idzie dalej i polega na zadeklarowaniu w funkcji Funkcja ()
obiektu klasy ZwyklyKot na stercie i zwróceniu wskaźnika do tego
obiektu. Funkcja wywołująca będzie wtedy miała możliwość
skasowania wskaźnika.

Trzecie, najbardziej eleganckie, polega na zadeklarowaniu obiektu
w funkcji wywołującej i przekazaniu go do funkcji Funkcja () przez
referencje.


Document Outline


Wyszukiwarka

Podobne podstrony:
PO wyk07 v1
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 wyk07 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