• Nie Znaleziono Wyników

Funkcja niezależna zaprzyjaźniona z klasą

W dokumencie Język C++ – podstawy programowania (Stron 133-144)

Możemy, wykorzystując odpowiednie konstrukcje języka C++ spowodować, że „zwykła” funkcja będzie mogła wykonywać operacje na obiekcie w ta-ki sposób, jak czyni to funkcja składowa klasy. W tym przypadku, obiekt a może być traktowany jako jeden z argumentów formalnych funkcji. W takim przypadku wywołanie może mieć postać:

ustaw ( a , 5 , 5 ) ;

W tym wywołaniu, obiekt a jest traktowany tak samo jak pozostałe argu-menty funkcji. Tym specjalnym mechanizmem w języku C++ jest mecha-nizm deklaracji zaprzyjaźnienia. Deklaracja zaprzyjaźnienia (ang. friend) pozwala zadeklarować w danej klasie funkcje, które będą miały dostęp do składowych prywatnych, ale nie są funkcjami składowymi klasy. Wyróżnia-my następujące sytuacje:

— Funkcja niezależna zaprzyjaźniona z klasą X

— Funkcja składowa klasy Y zaprzyjaźniona z klasą X

— Wszystkie funkcje klasy Y są zaprzyjaźnione z klasą X (klasa zaprzyjaź-niona)

Funkcje zaprzyjaźnione mają dostęp do wszystkich składowych klasy zadeklarowanych jako private lub protected. Z formalnego punktu widzenia, funkcje zaprzyjaźnione łamią zasadę hermetyzacji (ukrywania) danych, ale czasami ich użycie może być korzystne.

6.2. Funkcja niezależna zaprzyjaźniona z klasą

Funkcja zaprzyjaźniona z klasą jest zdefiniowana na zewnątrz jej zasię-gu i ma dostęp do składowych prywatnych klasy. Aby zadeklarować funkcję jako zaprzyjaźnioną (mającą dostęp do danych prywatnych) z jakąś klasą, prototyp funkcji w jej definicji należy poprzedzić słowem kluczowym friend.

Mimo, że prototyp funkcji umieszczony jest w definicji klasy, nie jest ona jej funkcją składową. Etykiety definiujące sposób dostępu do składowych, ta-kie jak private, public i protected nie mają nic wspólnego z definicją funkcji zaprzyjaźnionych. Dobrym zwyczajem programistycznym jest umieszczanie prototypów wszystkich funkcji zaprzyjaźnionych z daną klasą na jej począt-ku, ale nie jest to konieczne. Rozważmy program, pokazany na wydruku 6.1, w którym wykorzystamy funkcje zaprzyjaźnioną. W pokazanym przykładzie mamy prostą klasę punkt:

c l a s s punkt {

i n t x , y ; public:

124 6. Funkcje zaprzyjaźnione

punkt ( i n t xx = 0 , i n t yy = 0 )

{x = xx ; y = yy ; }

friend void pokaz ( punkt ) ; } ;

Dwie dane składowe x i y są prywatne, dostęp do nich możliwy jest jedynie poprzez funkcje składowe klasy punkt. Klasa posiada konstruktor.

Jeżeli chcemy, aby funkcja zewnętrzna miała dostęp do danych składowych klasy punkt, musimy jawnie zadeklarować taką funkcję jako zaprzyjaźnioną.

W naszym przykładzie tworzymy funkcję zaprzyjaźnioną o nazwie pokaz():

friend void pokaz ( punkt ) ;

Listing 6.1. Dostęp do składowych prywatnych; funkcje zaprzyjaźnione

1#include <i o s t r e a m . h>

Dzięki specyfikacji friend funkcja pokaz() staje się funkcją zaprzyjaźnio-ną, ma dostęp do prywatnych danych klasy punkt. Funkcja pokaz() wy-świetla aktualne wartości danych prywatnych x i y. Funkcja pokaz() jest samodzielną funkcją w stylu języka C – nie jest ona funkcją składową klasy punkt:

void pokaz ( punkt p1 )

{ c o u t << " P o z y c j a ␣X␣=␣ " << p1 . x << e n d l ; c o u t << " P o z y c j a ␣Y␣=␣ " << p1 . y << e n d l ; }

6.2. Funkcja niezależna zaprzyjaźniona z klasą 125 W programie testującym:

i n t main ( )

{ punkt a1 ( 2 , 4 ) ; pokaz ( a1 ) ; return 0 ; }

tworzymy obiekt a1 klasy punkt i inicjujemy go wartościami 2 i 4. Aby wyświetlić na ekranie monitora wartości x i y musimy wywołać funkcje pokaz(). Ponieważ jest to funkcja zaprzyjaźniona, możemy wywoływać tą funkcje tak, jak zwykłą funkcję w języku C:

pokaz ( a1 ) ;

W naszym przykładzie funkcja pokaz() pobiera jako argument a1. Pamię-tamy, że gdyby funkcja pokaz() była funkcją składową klasy punkt, to wy-wołanie jej miałoby postać:

a1 . pokaz ( ) ;

Pomimo, że funkcja pokaz() nie jest składową klasy punkt, ze względu na deklarację zaprzyjaźnienia ma ona pełny dostęp do prywatnych składowych tej klasy. Funkcja zaprzyjaźniona pokaz() jest wywoływana bez użycia ope-ratora kropka. Nie jest funkcją składową, nie może być poprzedzana nazwą obiektu.

W omawianym przykładzie, zamiany funkcji składowej klasy na funkcję zaprzyjaźnioną z klasą niczego nie wnosi, a faktycznie nawet osłabia pod-stawową zasadę programowania obiektowego, jaką jest zasada ukrywania danych, to zdarzają się sytuacje gdzie stosowanie funkcji zaprzyjaźnionych jest korzystne a nawet konieczne.

Rysunek 6.2. Funkcja zaprzyjaźniona z klasą definiowana jest na zewnątrz jej za-sięgu, a mimo tego ma dostęp do jej składowych prywatnych

126 6. Funkcje zaprzyjaźnione Na pokazanym diagramie widzimy dwie metody dostępu do prywatnych danych klasy – przy pomocy funkcji składowych klasy lub przy pomocy funkcji zewnętrznych, ale zaprzyjaźnionych z klasą. W kolejnym przykła-dzie opracujemy klasę punkt, która ma jedną funkcję składową inicjującą dane klasy (ustalimy współrzędne punktu na płaszczyźnie) i jedną funkcję zaprzyjaźnioną (obliczy ona odległość punktu od początku układu współ-rzędnych). W omówionym przykładzie (program z wydruku 6.2), tworzymy obiekt p1 a następnie przy pomocy funkcji składowej ustaw() przypisujemy wartości prywatnym zmiennym składowym:

p1 . ustaw ( 1 . 0 , 1 . 0 ) ;

Funkcja zaprzyjaźniona promien() nie może odwołać się do zmiennych skła-dowych bezpośrednio, wykorzystuje do tego celu obiekt n klasy punkt. W wywołaniu:

c o u t << " O d l e g l o s c ␣=␣ " << promien ( p1 ) << e n d l ;

funkcji zaprzyjaźnionej promien() przekazywany jest argument aktualny p1.

Listing 6.2. Dostęp do składowych prywatnych; funkcje zaprzyjaźnione

1#include <i o s t r e a m >

Jeszcze bardziej uproszczona wersje poprzedniego przykładu pokazano na kolejnym przykładzie (wydruk 6.3).

6.2. Funkcja niezależna zaprzyjaźniona z klasą 127

Listing 6.3. Dostęp do składowych prywatnych; funkcje zaprzyjaźnione

1#include <i o s t r e a m >

Zmodyfikowana klasa punkt ma postać:

c l a s s punkt { f l o a t x , y ;

friend f l o a t promien ( punkt n , f l o a t a , f l o a t b ) ; } ;

Składowe x i y są danymi prywatnymi klasy (gdy nie podamy specyfikatora private, składowe domyślnie są zadeklarowane jako prywatne). Ponieważ klasa nie posiada żadnych funkcji składowych, nie ma potrzeby stosowa-nia specyfikatora public (funkcje zaprzyjaźnione nie podlegają specyfikacji dostępu, możemy je umieszczać w dowolnej sekcji zarówno prywatnej jak i publicznej). Funkcja zaprzyjaźniona:

otrzymuje potrzebne argumenty i ustawia zmienne x i y, a następnie oblicza odległość punktu od początku układu współrzędnych.

W języku C++ argumenty w wywoływanej funkcji przekazywane są przez wartość lub przez referencję. Przypomnimy krótko zasady posługi-wania się referencją. Zasadniczo referencja jest to niejawny wskaźnik. Kiedy argument przekazywany jest przez wartość, tworzona jest kopia wartości

128 6. Funkcje zaprzyjaźnione argumentu i ta kopia przekazywana jest do wywoływanej funkcji. Możemy dokonywać dowolnych zmian na kopii, oryginalna wartość jest bezpieczna – nie ulega zmianom. Przekazywanie argumentów przez wartość jest bezpiecz-ne, ale bardzo wolne. Przekazywanie parametrów funkcji przez referencje jest korzystne ze względu na wydajność – jest po prostu procesem bardzo szybkim. Za każdym razem, gdy przekazywany jest obiekt do funkcji przez wartość, tworzona jest kopia tego obiektu. Za każdym razem, gdy zwracany jest obiekt z funkcji tworzona jest kolejna kopia. Obiekty kopiowane są na stos. Wymaga to sporej ilości czasu i pamięci. W przypadku zdefiniowa-nych przez programistę dużych obiektów, ten koszt staje się bardzo duży.

Rozmiar obiektu umieszczonego na stosie jest sumą rozmiarów wszystkich jego zmiennych składowych. Stosowanie referencji jest korzystne, ponieważ eliminuje koszty związane z kopiowaniem dużych ilości danych.

Parametr referencji jest aliasem (synonimem) do odpowiadającego mu argumentu. Referencja jest specjalnym, niejawnym wskaźnikiem, który dzia-ła jak alternatywna nazwa dla zmiennej. Zmienną o podanej po nazwie typu z przyrostkiem &, np.

T& nazwa

gdzie T jest nazwą typu, nazywamy referencją do typu T. Na przykład, deklaracja

i n t &l i c z b a

umieszczona w nagłówku funkcji oznacza „liczba jest referencją do int”. W krótkim przykładzie przypomnimy przekazywanie argumentów przez refe-rencję.

Listing 6.4. Przekazywanie parametrów przez referencję

1#include <i o s t r e a m >

6.2. Funkcja niezależna zaprzyjaźniona z klasą 129

17 return 0 ; }

19 void Kwadrat ( i n t &x r e f )

{ x r e f ∗= x r e f ; } // argument zmodyfikowany

Po uruchomieniu programu otrzymujemy wydruk:

zmienna x zmodyfikowana : x = 2

x = 4

zmienna x n i e zmodyfikowana : x = 4

x = 4

W programie parametr funkcji Kwadrat() jest przekazywany przez referen-cję, co widać wyraźnie w prototypie:

void Kwadrat ( i n t & ) ;

Programista analizując wywołanie funkcji:

Kwadrat ( x ) ; l u b Kwadrat ( a ) ;

nie jest wstanie zorientować się, czy parametry przekazywane są przez war-tość czy przez referencję (taką pewność ma tylko wtedy, gdy zanalizuje pro-totyp funkcji). Może to prowadzić do nieoczekiwanych skutków ubocznych, co demonstruje nasz program. Po pierwszym wywołaniu funkcji Kwadrat() zmienna x zmieniała wartość. Aby uniknąć takich skutków wielu programi-stów przesyła niemodyfikowalne argumenty stosując referencję do stałych.

Ponieważ zagadnienia wydajności są istotne, w kolejnym programie de-monstrującym wykorzystanie funkcji zaprzyjaźnionych, zastosujemy prze-kazywanie argumentów przez referencję.

Mamy następującą klasę:

c l a s s Demo_1

{ friend void ustawA ( Demo_1 &, i n t ) ; // f u n k c j a z a p r z y j a z n i o n a

public:

Demo_1( ) { a = 0 ; }

void pokaz ( ) const { c o u t << a << e n d l ; } private:

i n t a ; } ;

W tej klasie jest zadeklarowana dana składowa a, która jest prywatna, czyli

130 6. Funkcje zaprzyjaźnione jest niedostępna dla funkcji nie będących składowymi klasy. Aby funkcja ze-wnętrzna ustawA() miała dostęp do tej danej, zadeklarowano ją jako friend:

friend void ustawA ( Demo_1 &, i n t ) ; // f u n k c j a z a p r z y j a z n i o n a

Definicja funkcji ustawA() ma postać:

void ustawA ( Demo_1 &x , i n t i l e ) { x . a = i l e ;

}

Funkcja ustawA() ma możliwość modyfikacji danej a ponieważ jest zaprzy-jaźniona z klasą Demo 1. Cały program ma postać przedstawiona na wy-druku 6.1.

Listing 6.5. Dostęp do składowych prywatnyc; funkcje zaprzyjaźnione

1#include <i o s t r e a m . h>

Po uruchomieniu otrzymujemy następujący wynik:

zm . a po u t w o r z e n i u o b i e k t u : 0

zm . a po wywolaniu f u n k c j i friend ustawA : 10

W trakcie wykonywania programu zostaje utworzony obiekt zm i danej a jest

6.2. Funkcja niezależna zaprzyjaźniona z klasą 131 przypisana wartość 0. Następnie wywołana zostaje funkcja zaprzyjaźniona ustawA() i danej a zostaje przypisana wartość 10. Funkcja składowa pokaz():

void pokaz ( ) const { c o u t << a << e n d l ; }

została zadeklarowana jako const, co oznacza, że nie może ona modyfikować danych obiektów. Funkcja deklarowana jest jako const zarówno w prototypie jak i w definicji przez wstawienie słowa kluczowego po liście jej parametrów i, w przypadku definicji przed nawiasem klamrowym rozpoczynającym ciało funkcji. Programista może określić, które obiekty wymagają modyfikacji, a które pod żadnym pozorem nie mogą być zmieniane. Gdy obiekt nie może być zmieniony, programista może posłużyć się słowem kluczowym const.

Wszystkie próby późniejszej modyfikacji takiego obiektu znajdowane są już w trakcie kompilacji programu. W kolejnym przykładzie, pokażemy jak napi-sać niezależną funkcję pokaz(), zaprzyjaźnioną z klasą punkt, wypisującą na ekranie współrzędne punktu. Deklaracja wyjściowa klasy punkt ma postać:

c l a s s punkt {

i n t x , y ; public:

punkt (i n t xx = 0 , i n t yy = 0 )

{ x = xx ; y = yy ;

} } ;

Aby mieć dostęp do prywatnych danych klasy punkt, co jest nam potrzebne, aby wyświetlić współrzędne punktu x i y, musimy dysponować zewnętrzną funkcją pokaz(), zaprzyjaźnioną z klasą punkt. Argumenty do tej funkcji muszą być przekazane jawnie. Wobec tego prototyp tej funkcji może mieć postać:

void pokaz ( punkt ) ;

gdy chcemy przekazywać argumenty przez wartość, lub:

void pokaz ( punkt & ) ;

gdy chcemy przekazać argumenty przez referencję. Możemy usprawnić funk-cję pokaz() wiedząc, że ta funkcja nie zmienia współrzędnych i zastosować modyfikator const:

void pokaz ( const punkt & ) ;

132 6. Funkcje zaprzyjaźnione Modyfikacja pierwotnej klasy punkt może mieć postać pokazaną na wydruku 6.6.

Listing 6.6. Dostęp do składowych prywatnych; funkcje zaprzyjaźnione

1 // d e k l a r a c j a k l a s y punkt , p l i k p u n k t 1 . h

#i f n d e f _PUNKT1_H

3 #d e f i n e _PUNKT1_H c l a s s punkt

5 { i n t x , y ; public:

7 friend void pokaz ( const punkt & ) ; punkt (i n t xx = 0 , i n t yy = 0 )

9 { x = xx ; y = yy ;

}

11 } ;

#e n d i f

Funkcja służąca do wyświetlenia współrzędnych ma postać:

friend void pokaz ( const punkt &) ;

Funkcja pokaz() jest zaprzyjaźniona z klasą punkt i ma dostęp do prywat-nych daprywat-nych x i y. Jeżeli powstanie obiekt klasy punkt o nazwie p to możemy uzyskać dostęp do jego składowych klasycznie:

p . x i p . y

Definicję funkcji pokaz() umieszczamy w oddzielnym pliku (wydruk 6.7).

Listing 6.7. Dostęp do składowych prywatnych; funkcje zaprzyjaźnione

1 // d e f i n i c j a k l a s y p u n k t , p l i k p u n k t 1 . cpp

#include " punkt1 . h"

3#include <i o s t r e a m . h>

5 void pokaz (const punkt &p )

{ c o u t << " ␣ w s p o l r z e d n e ␣ punktu : ␣ " << p . x << " ␣ " << p . y

7 << " \n"; }

Funkcja testująca pokazana jest na wydruku 6.8:

Listing 6.8. Dostęp do składowych prywatnych; funkcje zaprzyjaźnione

// program t e s t u j a c y f u n k c j e z a p r z y j a z n i o n e

2#include <i o s t r e a m . h>

#include <c o n i o . h>

4#include <punkt1 . h>

6.2. Funkcja niezależna zaprzyjaźniona z klasą 133

i n t main ( )

6 { c o u t << " zmienna␣ automatyczna ␣ : ␣ " << e n d l ; punkt p1 ( 1 0 , 2 0 ) ;

8 pokaz ( p1 ) ;

c o u t << " zmienna␣ dynamiczna␣ : ␣ " << e n d l ;

10 punkt ∗wsk ;

wsk = new punkt ( 2 0 , 4 0 ) ;

12 pokaz ( ∗ wsk ) ; g e t c h ( ) ;

14 return 0 ;

}

Po uruchomieniu otrzymujemy następujący wydruk;

zmienna automatyczna : w s p o l r z e d n e punktu : 10 20

zmienna dynamiczna : w s p o l r z e d n e punktu : 20 40

W pokazanym programie przypomniano także tworzenie obiektów dyna-micznych. W języku C++ dla każdego programu przydzielany jest pewien obszar pamięci dla alokacji obiektów tworzonych dynamicznie. Obszar ten jest zorganizowany w postaci kopca (ang. heap). Na kopcu alokowane są obiekty dynamiczne. Obiekty dynamiczne tworzone są przy pomocy opera-tora new. Operator new alokuje (przydziela) pamięć na kopcu. Gdy obiekt nie jest już potrzebny należy go zniszczyć przy pomocy operatora delete.

Aby można było zastosować operator new należy najpierw zadeklarować zmienną wskaźnikową, dla przykładu:

i n t ∗ wsk ;

Tworzenie zmiennej dynamicznej ma postać:

wsk = new typ ;

lub

wsk = new typ ( w a r t o s c ) ;

gdzie typ oznacza typ zmiennej (np. int, float, itp.) Możemy deklarację zmiennej wskaźnikowej połączyć z tworzeniem zmiennej dynamicznej:

typ ∗ wsk = newt typ ;

Tak złożona instrukcja może mieć przykładową postać:

134 6. Funkcje zaprzyjaźnione

i n t ∗ wsk = new i n t;

W pokazanym programie utworzono zmienną dynamiczną w następujący sposób:

c o u t << " zmienna ␣ dynamiczna␣ : ␣ " << e n d l ; punkt ∗wsk ;

wsk = new punkt ( 2 0 , 4 0 ) ;

W dokumencie Język C++ – podstawy programowania (Stron 133-144)