Rozdział 9.
Referencje
W poprzednim rozdziale poznałeś wskaźniki i dowiedziałeś się, jak za ich pomocą można operować obiektami na stercie oraz jak odwoływać się do obiektów pośrednio. Referencje mają prawie te same możliwości, co wskaźniki, ale posiadają przy tym dużo prostszą składnię.
Z tego rozdziału dowiesz się:
czym są referencje,
czym różnią się od wskaźników,
jak się je tworzy i wykorzystuje,
jakie są ich ograniczenia,
w jaki sposób przekazywać obiekty i wartości do i z funkcji za pomocą referencji.
Czym jest referencja?
Referencja jest aliasem (inną nazwą); gdy tworzysz referencję, inicjalizujesz ją nazwą innego obiektu, będącego [Author ID1: at Wed Oct 31 09:10:00 2001
]celem[Author ID1: at Wed Oct 31 09:10:00 2001
]u[Author ID1: at Wed Oct 31 09:10:00 2001
] referencji. Od tego momentu referencja działa jak alternatywna nazwa celu. Wszystko, co robisz z referencją, w rzeczywistości jest robione[Author ID1: at Wed Oct 31 09:11:00 2001
] dotyczy[Author ID1: at Wed Oct 31 09:11:00 2001
]z[Author ID1: at Wed Oct 31 09:11:00 2001
] jej obiektu[Author ID1: at Wed Oct 31 09:11:00 2001
]em[Author ID1: at Wed Oct 31 09:11:00 2001
] docelowego[Author ID1: at Wed Oct 31 09:11:00 2001
].
Referencję tworzy się, zapisując typ obiektu docelowego, operator referencji (&) oraz nazwę referencji.
Nazwy referencji mogą być dowolne, ale wielu programistów woli poprzedzać jej nazwę literą „r”. Jeśli masz zmienną całkowitą o nazwie someInt, możesz stworzyć referencję do niej pisząc:
int &rSomeRef = someInt;
Odczytuje się to jako: „rSomeRef jest referencją do wartości[Author ID1: at Wed Oct 31 09:12:00 2001
] zmiennej [Author ID1: at Wed Oct 31 09:12:00 2001
]typu int. Ta referencja została zainicjalizowana tak[Author ID1: at Wed Oct 31 09:12:00 2001
], aby [Author ID1: at Wed Oct 31 09:12:00 2001
]odnosił[Author ID1: at Wed Oct 31 09:12:00 2001
]a[Author ID1: at Wed Oct 31 09:12:00 2001
] się do zmiennej someInt.” Sposób tworzenia referencji i korzystania z niej przedstawia listing 9.1.
UWAGA Operator referencji (&) ma taki sam symbol, jak operator adresu. Nie są to jednak te same operatory (choć oczywiście są ze sobą powiązane).
Zastosowanie spacji przed operatorem referencji jest obowiązkowe, użycie spacji pomiędzy operatorem referencji a nazwą zmiennej referencyjnej jest opcjonalne. Tak więc:
int &rSomeRef = someInt; // ok
int & rSomeRef = someInt; // ok
Listing 9.1. Tworzenie referencji i jej użycie
0: //Listing 9.1
1: // Demonstruje użycie referencji
2:
3: #include <iostream>
4:
5: int main()
6: {
7: using namespace std;
8: int intOne;
9: int &rSomeRef = intOne;
10:
11: intOne = 5;
12: cout << "intOne: " << intOne << endl;
13: cout << "rSomeRef: " << rSomeRef << endl;
14:
15: rSomeRef = 7;
16: cout << "intOne: " << intOne << endl;
17: cout << "rSomeRef: " << rSomeRef << endl;
18:
19: return 0;
20: }
Wynik
intOne: 5
rSomeRef: 5
intOne: 7
rSomeRef: 7
Analiza
W linii 8. jest deklarowana lokalna zmienna intOne. W linii 9. referencja rSomeRef (jakaś referencja) jest deklarowana i inicjalizowana tak, by odnosiła się do zmiennej intOne. Jeśli zadeklarujesz referencję, lecz jej nie zainicjalizujesz, kompilator zgłosi błąd powstały podczas kompilacji. Referencje muszą być zainicjalizowane.
W linii 11. zmiennej intOne jest przypisywana wartość 5. W liniach 12. i 13. są wypisywane wartości zmiennej intOne i referencji rSomeRef; są one oczywiście takie same.
W linii 17. referencji rSomeRef jest przypisywana wartość 7. Ponieważ jest to referencja, czyli inna nazwa zmiennej intOne, w rzeczywistości wartość ta jest przypisywana tej zmiennej (co potwierdzają komunikaty wypisywane w liniach 16. i 17.).
Użycie operatora adresu z referencją
Gdy pobierzesz adres referencji, uzyskasz adres jej celu. Wynika to z natury referencji (są one aliasami dla obiektów docelowych). Pokazuje to listing 9.2.
Listing 9.2. Odczytywanie adresu referencji
0: //Listing 9.2
1: // Demonstruje użycie referencji
2:
3: #include <iostream>
4:
5: int main()
6: {
7: using namespace std;
8: int intOne;
9: int &rSomeRef = intOne;
10:
11: intOne = 5;
12: cout << "intOne: " << intOne << endl;
13: cout << "rSomeRef: " << rSomeRef << endl;
14:
15: cout << "&intOne: " << &intOne << endl;
16: cout << "&rSomeRef: " << &rSomeRef << endl;
17:
18: return 0;
19: }
Wynik
intOne: 5
rSomeRef: 5
&intOne: 0012FF7C
&rSomeRef: 0012FF7C
UWAGA W twoim komputerze dwie ostatnie linie mogą wyglądać inaczej.
Analiza
W tym przykładzie referencja rSomeRef ponownie odnosi się do zmiennej intOne. Tym razem jednak wypisywane są adresy obu zmiennych; są one identyczne. C++ nie umożliwia dostępu do adresu samej referencji, gdyż jego użycie, w odróżnieniu od użycia adresu zmiennej, nie miałoby sensu. Referencje są inicjalizowane podczas tworzenia i zawsze stanowią synonim dla swojego obiektu docelowego (nawet gdy zostanie zastosowany operator adresu).
Na przykład, jeśli masz klasę o nazwie President, jej egzemplarz możesz zadeklarować następująco:
President George_Washington;
Możesz wtedy zadeklarować referencję do klasy President i zainicjalizować ją tym obiektem:
President &FatherOfOurCountry = George_Washington;
Istnieje tylko jeden obiekt klasy President; oba identyfikatory odnoszą się do tego samego egzemplarza obiektu tej samej klasy. Wszelkie operacje, jakie wykonasz na zmiennej FatherOfOurCountry (ojciec naszego kraju), będą odnosić się do obiektu George_Washington.
Należy odróżnić symbol & w linii 9. listingu 9.2 (deklarujący referencję o nazwie rSomeRef) od symboli & w liniach 15. i 16., które zwracają adresy zmiennej całkowitej intOne i referencji rSomeRef.
Zwykle w trakcie używania referencji nie używa się operatora adresu. Referencji używa się tak, jak jej zmiennej docelowej. Pokazuje to linia 13.
Nie można zmieniać przypisania referencji
Nawet doświadczonym programistom C++, którzy wiedzą, że nie można zmieniać przypisania referencji, gdyż jest ona aliasem swojego obiektu docelowego, zdarza się próba zmiany jej przypisania. To, co wygląda w takiej sytuacji na ponowne przypisanie referencji, w rzeczywistości jest przypisaniem nowej wartości obiektowi docelowemu. Przedstawia to listing 9.3.
Listing 9.3. Przypisanie do referencji
0: //Listing 9.3
1: //Ponowne przypisanie referencji
2:
3: #include <iostream>
4:
5: int main()
6: {
7: using namespace std;
8: int intOne;
9: int &rSomeRef = intOne;
10:
11: intOne = 5;
12: cout << "intOne:\t" << intOne << endl;
13: cout << "rSomeRef:\t" << rSomeRef << endl;
14: cout << "&intOne:\t" << &intOne << endl;
15: cout << "&rSomeRef:\t" << &rSomeRef << endl;
16:
17: int intTwo = 8;
18: rSomeRef = intTwo; // to nie to o czym myślisz!
19: cout << "\nintOne:\t" << intOne << endl;
20: cout << "intTwo:\t" << intTwo << endl;
21: cout << "rSomeRef:\t" << rSomeRef << endl;
22: cout << "&intOne:\t" << &intOne << endl;
23: cout << "&intTwo:\t" << &intTwo << endl;
24: cout << "&rSomeRef:\t" << &rSomeRef << endl;
25: return 0;
26: }
Wynik
intOne: 5
rSomeRef: 5
&intOne: 0012FF7C
&rSomeRef: 0012FF7C
intOne: 8
intTwo: 8
rSomeRef: 8
&intOne: 0012FF7C
&intTwo: 0012FF74
&rSomeRef: 0012FF7C
Analiza
Także w tym programie zostały zadeklarowane (w liniach 8. i 9.) zmienna całkowita i referencja do niej. W linii 11. zmiennej jest przypisywana wartość 5, po czym w liniach od 12. do 15. wypisywane są wartości i ich adresy.
W linii 17. tworzona jest nowa zmienna, intTwo, inicjalizowana wartością 8. W linii 18. programista próbuje zmienić przypisanie referencji rSomeRef tak, aby odnosiła się do zmiennej intTwo, lecz mu się to nie udaje. W rzeczywistości referencja rSomeRef w dalszym ciągu jest aliasem dla zmiennej intOne, więc to przypisanie stanowi ekwiwalent dla:
intOne = intTwo;
Potwierdzają to wypisywane w liniach 19. do 21. komunikaty, pokazujące wartości zmiennej intOne i referencji rSomeRef. Ich wartości są takie same, jak wartość zmiennej intTwo. W rzeczywistości, gdy w liniach od 22. do 24. są wypisywane adresy, okazuje się, że rSomeRef w dalszym ciągu odnosi się do zmiennej intOne, a nie do zmiennej intTwo.
TAK |
NIE |
W celu stworzenia aliasu do obiektu używaj referencji. Inicjalizuj wszystkie referencje. |
Nie zmieniaj przypisania referencji. Nie myl operatora adresu z operatorem referencji. |
Do czego mogą odnosić się referencje?
Referencje mogą odnosić się do każdego z obiektów, także do obiektów zdefiniowanych przez użytkownika. Zwróć uwagę, że referencja odnosi się do obiektu, a nie do klasy, do której ten obiekt należy. Nie możesz napisać:
int & rIntRef = int; // źle
Musisz zainicjalizować referencję rIntRef tak, aby odnosiła się do konkretnej zmiennej całkowitej, na przykład:
int howBig = 200;
int & rIntRef = howBig;
W ten sam sposób[Author ID1: at Wed Oct 31 09:14:00 2001
]Nie możesz zainicjalizować też referencji do klasy CAT:
CAT & rCatRef = CAT; // źle
Musisz zainicjalizować referencję rInt[Author ID1: at Wed Oct 31 09:14:00 2001
]Cat[Author ID1: at Wed Oct 31 09:14:00 2001
]Ref tak, aby odnosiła się do konkretnego egzemplarza tej klasy:
CAT mruczek;
CAT & rCatRef = mruczek;
Referencje do obiektów są używane w taki sam sposób, jak obiekty. Dane i funkcje składowe są dostępne poprzez ten sam operator dostępu do składowych (.) i, podobnie jak w typach wbudowanych, referencja działa jak inna nazwa obiektu. Ilustruje to listing 9.4.
Listing 9.4. Referencje do obiektów
0: // Listing 9.4
1: // Referencje do obiektów klas
2:
3: #include <iostream>
4:
5: class SimpleCat
6: {
7: public:
8: SimpleCat (int age, int weight);
9: ~SimpleCat() {}
10: int GetAge() { return itsAge; }
11: int GetWeight() { return itsWeight; }
12: private:
13: int itsAge;
14: int itsWeight;
15: };
16:
17: SimpleCat::SimpleCat(int age, int weight)
18: {
19: itsAge = age;
20: itsWeight = weight;
21: }
22:
23: int main()
24: {
25: SimpleCat Mruczek(5,8);
26: SimpleCat & rCat = Mruczek;
27:
28: std::cout << "Mruczek ma: ";
29: std::cout << Mruczek.GetAge() << " lat. \n";
30: std::cout << "i wazy: ";
31: std::cout << rCat.GetWeight() << " funtow. \n";
32: return 0;
33: }
Wynik
Mruczek ma: 5 lat.
i wazy: 8 funtow.
Analiza
W linii 25. zmienna Mruczek jest deklarowana jako obiekt klasy SimpleCat (prosty[Author ID1: at Wed Oct 31 09:15:00 2001
]zwykły[Author ID1: at Wed Oct 31 09:15:00 2001
] kot). W linii 26. jest deklarowana referencja rCat do obiektu klasy SimpleCat, która odnosi się do obiektu Mruczek. W liniach 29. i 31. są wykorzystywane akcesory klasy SimpleCat, najpierw poprzez obiekt klasy, a potem poprzez referencję. Zwróć uwagę, że dostęp do nich jest identyczny. Także w tym przypadku referencja jest inną nazwą (aliasem) rzeczywistego obiektu.
Referencje
Referencję deklaruje się, zapisując typ, operator referencji (&) oraz nazwę referencji. Referencje muszą być inicjalizowane w trakcie ich tworzenia.
Przykład 1
int hisAge;
int &rAge = hisAge;
Przykład 2
CAT Filemon;
CAT &rCatRef = Filemon;
Puste[Author ID1: at Wed Oct 31 09:17:00 2001
]Zerowe[Author ID1: at Wed Oct 31 09:17:00 2001
] wskaźniki i puste[Author ID1: at Wed Oct 31 09:17:00 2001
]zerowe[Author ID1: at Wed Oct 31 09:17:00 2001
] referencje
Gdy wskaźniki nie są inicjalizowane lub zostaną zwolnione, powinno się im przypisać wartość zerową [Author ID1: at Wed Oct 31 09:18:00 2001
]null (0). W przypadku referencji sytuacja wygląda inaczej. Referencja nie może być pusta[Author ID1: at Wed Oct 31 09:19:00 2001
]zerowa[Author ID1: at Wed Oct 31 09:19:00 2001
], a program zawierający referencję do nie istniejącego ([Author ID1: at Wed Oct 31 09:19:00 2001
]czyli [Author ID1: at Wed Oct 31 09:21:00 2001
]pustego)[Author ID1: at Wed Oct 31 09:19:00 2001
] obiektu, jest uważany za niewłaściwy. Gdy program jest niewłaściwy, może zdarzyć się prawie wszystko. Może się zdarzyć, że taki program działa, ale równie dobrze może też usunąć wszystkie pliki z dysku.
Większość kompilatorów obsługuje puste obiekty, powodując załamanie programu tylko wtedy, gdy spróbujesz użyć takiego obiektu. Obsługa pustych obiektów nie jest dobrym pomysłem. Gdy przeniesiesz program do innego komputera lub kompilatora, puste obiekty mogą spowodować tajemnicze błędy w działaniu programu.
Przekazywanie argumentów funkcji przez referencję
Z rozdziału 5., „Funkcje”, dowiedziałeś się, że funkcje mają dwa ograniczenia: argumenty są przekazywane przez wartość, a funkcja może zwrócić tylko jedną wartość.
Przekazywanie argumentów funkcji poprzez referencję może zlikwidować oba te ograniczenia. W C++, przekazywanie przez referencję odbywa się na dwa sposoby: z wykorzystaniem wskaźników i z wykorzystaniem referencji. Zauważ różnicę: przekazujesz poprzez referencję, używając wskaźnika lub przekazujesz poprzez referencję, używając referencji.
Składnia użycia wskaźnika jest inna niż użycia referencji, ale ogólny efekt jest taki sam. W dużym uproszczeniu można powiedzieć, że zamiast tworzyć w funkcji kopię przekazywanego obiektu, program przekazuje jej obiekt oryginalny.
Z rozdziału 5. dowiedziałeś się, że argumenty funkcji są im przekazywane poprzez stos. Gdy funkcja otrzymuje wartość poprzez referencję (z użyciem wskaźnika lub referencji), na stosie umieszczany jest adres obiektu, a nie cały obiekt.
W niektórych komputerach adres jest przechowywany w rejestrze i nie jest umieszczany na stosie. Kompilator wie, jak odwołać się do oryginalnego obiektu, więc zmiany są dokonywane w tym obiekcie, a nie w jego kopii.
Przekazanie obiektu przez referencję umożliwia funkcji dokonywanie zmian w tym obiekcie.
Przypomnij sobie,[Author ID1: at Wed Oct 31 09:22:00 2001
] że listing 5.5 z rozdziału piątego pokazywał,[Author ID1: at Wed Oct 31 09:22:00 2001
] że wywołanie funkcji swap() nie miało wpływu na wartości w funkcji wywołującej. Listing 5.5 został tu dla wygody odtworzony[Author ID1: at Wed Oct 31 09:22:00 2001
]powtórzony[Author ID1: at Wed Oct 31 09:22:00 2001
] jako listing 9.5.
Listing 9.5. Przykład przekazywania przez wartość
0: //Listing 9.5 Demonstruje przekazywanie przez wartość
1:
2: #include <iostream>
3:
4: using namespace std;
5: void swap(int x, int y);
6:
7: int main()
8: {
9: int x = 5, y = 10;
10:
11: cout << "Funkcja main(). Przed funkcja swap(), x: " << x << " y: " << y << "\n";
12: swap(x,y);
13: cout << "Funkcja main(). Po funkcji swap(), x: " << x << " y: " << y << "\n";
14: return 0;
15: }
16:
17: void swap (int x, int y)
18: {
19: int temp;
20:
21: cout << "Funkcja swap(). Przed zamiana, x: " << x << " y: " << y << "\n";
22:
23: temp = x;
24: x = y;
25: y = temp;
26:
27: cout << "Funkcja swap(). Po zamianie, x: " << x << " y: " << y << "\n";
28:
29: }
Wynik
Funkcja main(). Przed funkcja swap(), x: 5 y: 10
Funkcja swap(). Przed zamiana, x: 5 y: 10
Funkcja swap(). Po zamianie, x: 10 y: 5
Funkcja main(). Po funkcji swap(), x: 5 y: 10
Analiza
Wewnątrz funkcji main() program inicjalizuje dwie zmienne i przekazuje je funkcji swap() (zamień), która wydaje się je zamieniać. Jednak gdy ponownie sprawdzimy ich wartości w funkcji main(), okaże się, że nie uległy one zmianie!
Problem polega na tym, że zmienne x i y są przekazywane funkcji swap() poprzez wartość. Oznacza to, że wewnątrz tej funkcji są tworzone ich lokalne kopie. Nam potrzebne jest przekazanie zmiennych x i y przez referencję.
W C++ istnieją dwie możliwości rozwiązania tego problemu: parametry funkcji swap() możesz zamienić na wskaźniki do oryginalnych wartości, lub przekazać referencje do pierwotnych wartości.
Tworzenie funkcji swap() otrzymującej wskaźniki
Przekazując wskaźnik, przekazujesz adres obiektu, dlatego funkcja może manipulować wartością znajdującą się pod tym adresem. Aby za pomocą wskaźników umożliwić funkcji swap() zamianę wartości swoich argumentów, powinieneś zadeklarować ją jako przyjmującą dwa wskaźniki do zmiennych całkowitych. Następnie, poprzez wyłuskanie wskaźników (czyli dereferencję)[Author ID1: at Wed Oct 31 09:23:00 2001
], możesz zamienić wartości zmiennych miejscami [Author ID1: at Wed Oct 31 09:23:00 2001
]miejscami[Author ID1: at Wed Oct 31 09:23:00 2001
]. Demonstruje to listing 9.6.
Listing 9.6. Przekazywanie przez referencję za pomocą wskaźników
0: //Listing 9.6 Demonstruje przekazywanie przez referencję
1:
2: #include <iostream>
3:
4: using namespace std;
5: void swap(int *x, int *y);
6:
7: int main()
8: {
9: int x = 5, y = 10;
10:
11: cout << "Funkcja main(). Przed funkcja swap(), x: " << x << " y: " << y << "\n";
12: swap(&x,&y);
13: cout << "Funkcja main(). Po funkcji swap(), x: " << x << " y: " << y << "\n";
14: return 0;
15: }
16:
17: void swap (int *px, int *py)
18: {
19: int temp;
20:
21: cout << "Funkcja swap(). Przed zamiana, *px: " << *px <<
22: " *py: " << *py << "\n";
23:
24: temp = *px;
25: *px = *py;
26: *py = temp;
27:
28: cout << "Funkcja swap(). Po zamianie, *px: " << *px <<
29: " *py: " << *py << "\n";
30:
31: }
Wynik
Funkcja main(). Przed funkcja swap(), x: 5 y: 10
Funkcja swap(). Przed zamiana, *px: 5 *py: 10
Funkcja swap(). Po zamianie, *px: 10 *py: 5
Funkcja main(). Po funkcji swap(), x: 10 y: 5
Analiza
Udało się! W linii 5. został zmieniony prototyp funkcji swap(), w którym zadeklarowano że oba parametry funkcji są wskaźnikami do zmiennych typu int, a nie zmiennymi tego typu. Gdy w linii 12. następuje wywołanie funkcji swap(), jako argumenty są jej [Author ID1: at Wed Oct 31 09:24:00 2001 ]przekazywane adresy zmiennych x i y.
W linii 19., w funkcji swap(),deklarowana jest lokalna zmienna temp. Ta zmienna nie musi być wskaźnikiem; w czasie życia funkcji swap() przechowuje ona wartość *px (tj. wartość zmiennej x zadeklarowanej w funkcji wywołującej). Gdy funkcja swap() zakończy działanie, zmienna temp nie będzie już potrzebna.
W linii 24. zmiennej temp przypisywana jest wartość wskazywana przez px. W linii 25. zmiennej wskazywanej przez px przypisywana jest wartość wskazywana przez py. W linii 26. zmiennej[Author ID1: at Wed Oct 31 09:24:00 2001
]wartość umieszczona [Author ID1: at Wed Oct 31 09:24:00 2001
]przechowywana [Author ID1: at Wed Oct 31 09:24:00 2001
]w zmiennej temp (tj. oryginalna wartość wskazywana przez px) jest umieszczana w zmiennej wskazywanej przez py.
Efektem przeprowadzonych przez nas działań jest zamiana wartości tych zmiennych[Author ID1: at Wed Oct 31 09:25:00 2001 ], których adresy zostały przekazane do [Author ID1: at Wed Oct 31 09:26:00 2001 ]funkcji swap().
Implementacja funkcji swap() za pomocą referencji
Przedstawiony wcześniej program działa, ale składnia pokazanej w nim funkcji swap() ma dwie wady. Po pierwsze, konieczność wyłuskiwania wskaźników wewnątrz funkcji swap() ułatwia popełnieni błędów i zmniejsza czytelność programu. Po drugie, konieczność przekazania adresów zmiennych przez funkcję wywołującą zdradza użytkownikom sposób działania funkcji swap().
W języku C++ użytkownik funkcji nie ma możliwości poznania sposobu jej działania. Przekazywanie wskaźników do argumentów[Author ID1: at Wed Oct 31 09:26:00 2001
]parametrów[Author ID1: at Wed Oct 31 09:26:00 2001
] oznacza konieczność odpowiednich przygotowań w funkcji wywołującej, a przecież przygotowania te powinny należeć do obowiązków funkcji wywoływanej. W listingu 9.7 funkcja swap() została ponownie przepisana, tym razem z zastosowaniem referencji, a nie wskaźników.
Listing 9.7. Funkcja swap() przepisana z zastosowaniem referencji
0: //Listing 9.7 Demonstruje przekazywanie przez referencję
1: // z zastosowaniem referencji!
2:
3: #include <iostream>
4:
5: using namespace std;
6: void swap(int &x, int &y);
7:
8: int main()
9: {
10: int x = 5, y = 10;
11:
12: cout << "Funkcja main(). Przed funkcja swap(), x: " << x << " y: "
13: << y << "\n";
14:
15: swap(x,y);
16:
17: cout << "Funkcja main(). Po funkcji swap(), x: " << x << " y: "
18: << y << "\n";
19:
20: return 0;
21: }
22:
23: void swap (int &rx, int &ry)
24: {
25: int temp;
26:
27: cout << "Funkcja swap(). Przed zamiana, rx: " << rx << " ry: "
28: << ry << "\n";
29:
30: temp = rx;
31: rx = ry;
32: ry = temp;
33:
34:
35: cout << "Funkcja swap(). Po zamianie, rx: " << rx << " ry: "
36: << ry << "\n";
37:
38: }
Wynik
Funkcja main(). Przed funkcja swap(), x: 5 y: 10
Funkcja swap(). Przed zamiana, rx: 5 ry: 10
Funkcja swap(). Po zamianie, rx: 10 ry: 5
Funkcja main(). Po funkcji swap(), x: 10 y: 5
Analiza
Podobnie, jak w przykładzie ze wskaźnikami, także i tu (w linii 10.) deklarowane są dwie zmienne, których wartości wypisywane są w linii 12. W linii 15. następuje wywołanie funkcji swap(), ale zwróć uwagę, że tym razem nie są przekazywane adresy zmiennych x i y, lecz same zmienne. Funkcja wywołująca po prostu przekazuje zmienne.
Gdy wywoływana jest funkcja swap(), działanie programu przechodzi do linii 23., w której zmienne zostają zidentyfikowane jako referencje. Ich wartości są wypisywane w linii 27., zwróć uwagę, że nie wymagają one przeprowadzania żadnych dodatkowych operacji. Są to aliasy oryginalnych wartości, które mogą zostać użyte jako te wartości.
W liniach od 30. do 32. wartości są zamieniane, a następnie ponownie wypisywane w linii 35. Wykonanie programu wraca do funkcji wywołującej, zatem funkcja main() (w linii 17.) ponownie wypisuje wartości zmiennych. Ponieważ parametry funkcji swap() zostały zadeklarowane jako referencje, wartości w funkcji main() zostały przekazane przez referencję, dlatego są zamienione również w tej funkcji.
Referencje ułatwiają korzystanie z normalnych zmiennych, zachowując przy tym możliwość przekazywania argumentów poprzez referencję.
Nagłówki i prototypy funkcji
Listing 9.6 zawierał funkcję swap(), używającą wskaźników, zaś listing 9.7 zawierał tę samą funkcję używającą referencji. Stosowanie funkcji korzystającej z referencji jest łatwiejsze; łatwiejsze jest także zrozumienie kodu, ale skąd funkcja wywołująca wie, czy wartości są przekazywane poprzez wartość, czy poprzez referencję? Jako klient (czyli użytkownik) funkcji swap(), programista musi mieć pewność, że funkcja ta faktycznie zamieni swoje parametry.
Oto kolejne zastosowanie prototypów funkcji. Sprawdzając parametry zadeklarowane w prototypie, który zwykle znajduje się w pliku nagłówkowym wraz z innymi prototypami, programista wie, że wartości przekazywane do funkcji swap() są przekazywane poprzez referencję i wie, jak powinien ich użyć.
Gdyby funkcja swap() była częścią klasy, informacji tych dostarczyłaby deklaracja klasy, także umieszczana zwykle w pliku nagłówkowym.
W języku C++ wszystkich informacji potrzebnych klientom klas i funkcji mogą dostarczyć pliki nagłówkowe; pełnią one rolę interfejsu dla klasy lub funkcji. Implementacja jest natomiast ukrywana przed klientem. Dzięki temu programista może skupić się na analizowanym aktualnie problemie i korzystać z klasy lub funkcji bez zastanawiania się, w jaki sposób ona działa.
Gdy John Roebling projektował Most Brookliński, zajmował się takimi szczegółami, jak sposób wylewania betonu czy metoda produkcji drutu do kabli nośnych. Znał każdy fizyczny i chemiczny proces związany z tworzeniem materiałów przeznaczonych do budowy mostu. Obecnie inżynierowie oszczędzają czas, używając dobrze znanych materiałów budowlanych, nie zastanawiając się, w jaki sposób są one tworzone przez producenta.
Języka C++ umożliwia programistom korzystanie z „dobrze znanych” klas i funkcji, bez konieczności zajmowania się szczegółami ich działania. Te „części składowe” zostały złożone[Author ID1: at Wed Oct 31 09:28:00 2001
]mogą zostać po[Author ID1: at Wed Oct 31 09:28:00 2001
]łączone[Author ID1: at Wed Oct 31 09:28:00 2001
] w celu stworzenia programu (podobnie jak łączone są kable, rury, klamry i inne części w celu stworzenia mostu czy budynku).
Inżynier przeglądający[Author ID1: at Wed Oct 31 09:28:00 2001
] specyfikację betonu w celu poznania jego wytrzymałości, ciężaru własnego, czasu krzepnięcia, itd., a programista przegląda interfejs funkcji lub klasy w celu poznania usług, jakich ona dostarcza, parametrów, których potrzebuje i wartości, jakie zwraca.
Zwracanie kilku wartości
Jak wspominaliśmy wcześniej, funkcja może zwracać (bezpośrednio) tylko jedną wartość. Co zrobić, gdy chcesz otrzymać od funkcji dwie wartości? Jednym ze sposobów rozwiązania tego problemu jest przekazanie funkcji dwóch obiektów poprzez referencje. Funkcja może wtedy wypełnić te obiekty właściwymi wartościami. Ponieważ przekazywanie przez referencję umożliwia funkcji zmianę pierwotnego obiektu, może ona zwrócić dwie oddzielne informacje. Dzięki temu wartość zwracana przez funkcję bezpośrednio może zostać wykorzystana w inny sposób, na przykład do zgłoszenia informacji o błędach.
Także w tym przypadku do zwracania wartości w ten[Author ID1: at Wed Oct 31 09:29:00 2001
]ten sposób można użyć wskaźników lub referencji. Listing 9.8 przedstawia funkcję zwracającą trzy wartości: dwie zwracane jako parametry mające postać [Author ID1: at Wed Oct 31 09:30:00 2001
]przekazywane przez[Author ID1: at Wed Oct 31 09:30:00 2001
] wskaźników[Author ID1: at Wed Oct 31 09:30:00 2001
] i jedną jako wartość zwracaną [Author ID1: at Wed Oct 31 09:31:00 2001
]otną[Author ID1: at Wed Oct 31 09:31:00 2001
] funkcji.
Listing 9.8. Zwracanie wartości poprzez wskaźniki
0: //Listing 9.8
1: // Zwracanie kilku wartości z funkcji
2:
3: #include <iostream>
4:
5: using namespace std;
6: short Factor(int n, int* pSquared, int* pCubed);
7:
8: int main()
9: {
10: int number, squared, cubed;
11: short error;
12:
13: cout << "Wpisz liczbe (0 - 20): ";
14: cin >> number;
15:
16: error = Factor(number, &squared, &cubed);
17:
18: if (!error)
19: {
20: cout << "liczba: " << number << "\n";
21: cout << "do kwadratu: " << squared << "\n";
22: cout << "do trzeciej potegi: " << cubed << "\n";
23: }
24: else
25: cout << "Napotkano blad!!\n";
26: return 0;
27: }
28:
29: short Factor(int n, int *pSquared, int *pCubed)
30: {
31: short Value = 0;
32: if (n > 20)
33: Value = 1;
34: else
35: {
36: *pSquared = n*n;
37: *pCubed = n*n*n;
38: Value = 0;
39: }
40: return Value;
41: }
Wynik
Wpisz liczbe (0 - 20): 3
liczba: 3
do kwadratu: 9
do trzeciej potegi: 27
Analiza
W linii 10. zostały zadeklarowane trzy krótkie zmienne całkowite: number (liczba), squared (do kwadratu) oraz cubed (do trzeciej potęgi). Wartość zmiennej number jest wpisywana przez użytkownika. Ta liczba oraz adresy zmiennych squared i cubed są przekazywane do funkcji Factor() (czynnik).
Funkcja Factor() sprawdza pierwszy parametr, który jest przekazywany przez wartość. Jeśli jest większy od 20 (maksymalnej wartości, jaką może obsłużyć funkcja), zwracanej wartości Value (wartość) przypisywany jest prosty [Author ID1: at Wed Oct 31 09:31:00 2001
]kod błędu. Zwróć uwagę,[Author ID1: at Wed Oct 31 09:32:00 2001
] że wartość zwracana [Author ID1: at Wed Oct 31 09:32:00 2001
]funkcji[Author ID1: at Wed Oct 31 09:32:00 2001
]a[Author ID1: at Wed Oct 31 09:32:00 2001
] Factor() jest zarezerwowana dla [Author ID1: at Wed Oct 31 09:33:00 2001
]może [Author ID1: at Wed Oct 31 09:33:00 2001
]zwrotu[Author ID1: at Wed Oct 31 09:33:00 2001
]ócić[Author ID1: at Wed Oct 31 09:33:00 2001
] tę[Author ID1: at Wed Oct 31 09:33:00 2001
] albo tej [Author ID1: at Wed Oct 31 09:33:00 2001
]wartości[Author ID1: at Wed Oct 31 09:33:00 2001
]ć[Author ID1: at Wed Oct 31 09:33:00 2001
] błędu lub[Author ID1: at Wed Oct 31 09:34:00 2001
]albo[Author ID1: at Wed Oct 31 09:34:00 2001
] wartości[Author ID1: at Wed Oct 31 09:34:00 2001
]ć[Author ID1: at Wed Oct 31 09:34:00 2001
] 0, oznaczającej[Author ID1: at Wed Oct 31 09:34:00 2001
]ą[Author ID1: at Wed Oct 31 09:34:00 2001
],[Author ID1: at Wed Oct 31 09:34:00 2001
] że wszystko poszło dobrze; wartość tę funkcja zwraca w linii 40.
Obliczane w funkcji [Author ID1: at Wed Oct 31 09:37:00 2001
]wartości, czyli podniesiona do potęgi [Author ID1: at Wed Oct 31 09:38:00 2001
]drugiej i do trzeciej potęgi [Author ID1: at Wed Oct 31 09:38:00 2001
]liczba, są zwracane nie poprzez instrukcję return, ale bezpośrednio [Author ID1: at Wed Oct 31 09:38:00 2001
]poprzez zmianę wartości zmiennych [Author ID1: at Wed Oct 31 09:38:00 2001
]wskazywanych przez wskaźniki przekazane do funkcji.
W liniach 36. i 37. zmiennym [Author ID1: at Wed Oct 31 09:39:00 2001
]wskazywanym poprzez [Author ID1: at Wed Oct 31 09:39:00 2001
]wartościom[Author ID1: at Wed Oct 31 09:39:00 2001
]wskaźniki [Author ID1: at Wed Oct 31 09:39:00 2001
]przypisywane są w[Author ID1: at Wed Oct 31 09:39:00 2001
]y[Author ID1: at Wed Oct 31 09:40:00 2001
]ob[Author ID1: at Wed Oct 31 09:40:00 2001
]liczone wartości wcześniej. W linii 38. zmiennej Value jest przypisywany kod sukcesu, który jest zwracany w linii 40.
Jednym z ulepszeń wprowadzonych do tej funkcji mogła[Author ID1: at Wed Oct 31 09:40:00 2001
]o[Author ID1: at Wed Oct 31 09:40:00 2001
]by być napisanie[Author ID1: at Wed Oct 31 09:40:00 2001
]deklaracja[Author ID1: at Wed Oct 31 09:40:00 2001
]:
enum ERROR_VALUE { SUCCESS, FAILURE};
Dzięki temu, zamiast zwracać wartości 0 lub 1, program mógłby zwracać odpowiednią wartość[Author ID1: at Wed Oct 31 09:41:00 2001
] [Author ID1: at Wed Oct 31 09:42:00 2001
]stałą[Author ID1: at Wed Oct 31 09:41:00 2001
] [Author ID1: at Wed Oct 31 09:42:00 2001
]typu [Author ID1: at Wed Oct 31 09:41:00 2001
]wyliczeniowego[Author ID1: at Wed Oct 31 09:41:00 2001
]a[Author ID1: at Wed Oct 31 09:41:00 2001
] ERROR_VALUE (wartość błędu), czyli albo [Author ID1: at Wed Oct 31 09:41:00 2001
]SUCCESS (sukces) lub[Author ID1: at Wed Oct 31 09:41:00 2001
]albo[Author ID1: at Wed Oct 31 09:41:00 2001
] FAILURE (porażka).
Zwracanie wartości przez referencję
Choć program z listingu 9.8 działa poprawnie, byłby łatwiejszy w użyciu i modyfikacji, gdyby zamiast wskaźników zastosowano w nim referencje. Listing 9.9 przedstawia ten sam program przepisany tak, aby wykorzystywał referencje i typ [Author ID1: at Wed Oct 31 09:43:00 2001
]wyliczeniowy[Author ID1: at Wed Oct 31 09:43:00 2001
]e[Author ID1: at Wed Oct 31 09:43:00 2001
] ERR_CODE (kod błędu).
Listing 9.9. Listing 9.8 przepisany z zastosowaniem referencji
0: //Listing 9.9
1: // Zwracanie kilku wartości z funkcji
2: // z zastosowaniem referencji
3:
4: #include <iostream>
5:
6: using namespace std;
7: typedef unsigned short USHORT;
8: enum ERR_CODE { SUCCESS, ERROR };
9:
10: ERR_CODE Factor(USHORT, USHORT&, USHORT&);
11:
12: int main()
13: {
14: USHORT number, squared, cubed;
15: ERR_CODE result;
16:
17: cout << "Wpisz liczbe (0 - 20): ";
18: cin >> number;
19:
20: result = Factor(number, squared, cubed);
21:
22: if (result == SUCCESS)
23: {
24: cout << "liczba: " << number << "\n";
25: cout << "do kwadratu: " << squared << "\n";
26: cout << "do trzeciej potegi: " << cubed << "\n";
27: }
28: else
29: cout << "Napotkano blad!!\n";
30: return 0;
31: }
32:
33: ERR_CODE Factor(USHORT n, USHORT &rSquared, USHORT &rCubed)
34: {
35: if (n > 20)
36: return ERROR; // prosty kod błędu
37: else
38: {
39: rSquared = n*n;
40: rCubed = n*n*n;
41: return SUCCESS;
42: }
43: }
Wynik
Wpisz liczbe (0 - 20): 3
liczba: 3
do kwadratu: 9
do trzeciej potegi: 27
Analiza
Listing 9.9 jest prawie identyczny z listingiem 9.8, z dwiema różnicami. Dzięki zastosowaniu wyliczenia ERR_CODE zgłaszanie błędów w liniach 36. i 41., a także ich obsługa w linii 22., są bardziej przejrzyste.
Istotną zmianą jest to, że tym razem funkcja Factor() została zadeklarowana jako przyjmująca referencje, a nie wskaźniki, do zmiennych squared i cubed. Dzięki temu operowanie tymi parametrami jest prostsze i bardziej zrozumiałe.
Przekazywanie przez referencję zwiększa efektywność działania programu
Za każdym razem, gdy przekazujesz obiekt do funkcji poprzez wartość, tworzona jest kopia tego obiektu. Za każdym razem, gdy zwracasz z funkcji obiekt poprzez wartość, tworzona jest kolejna kopia.
Z rozdziału 5. dowiedziałeś się, że obiekty te są kopiowane na stos. Wymaga to sporej ilości czasu i pamięci. W przypadku niewielkich obiektów, takich jak wbudowane typy całkowite, koszt ten jest niewielki.
Jednak w przypadku większych, zdefiniowanych przez użytkownika obiektów, ten koszt staje się dużo większy. Rozmiar zdefiniowanego przez użytkownika obiektu umieszczonego na stosie jest sumą rozmiarów wszystkich jego zmiennych składowych. Każda z tych zmiennych także może być obiektem zdefiniowanym przez użytkownika, a przekazywanie takich rozbudowanych struktur przez kopiowanie ich na stos może być mało wydajne i zużywać dużoe[Author ID1: at Wed Oct 31 09:44:00 2001
] pamięci.
Pojawiają się także dodatkowe koszty. W przypadku tworzonych przez ciebie klas, za każdym razem gdy kompilator tworzy kopię tymczasową, wywoływany jest specjalny konstruktor: konstruktor kopiujący[Author ID1: at Wed Oct 31 09:44:00 2001
]i[Author ID1: at Wed Oct 31 09:44:00 2001
]. Działanie konstruktorów kopiujących[Author ID1: at Wed Oct 31 09:44:00 2001
]i[Author ID1: at Wed Oct 31 09:44:00 2001
] i metody ich tworzenia zostaną omówione w następnym rozdziale, na razie wystarczy,[Author ID1: at Wed Oct 31 09:45:00 2001
] że będziesz wiedział,[Author ID1: at Wed Oct 31 09:45:00 2001
] że konstruktor taki jest wywoływany za każdym razem, gdy na stosie jest umieszczana tymczasowa kopia obiektu.
Gdy niszczony jest obiekt tymczasowy (na zakończenie działania funkcji), wywoływany jest destruktor obiektu. Jeśli obiekt jest zwracany z funkcji poprzez wartość, konieczne jest stworzenie i zniszczenie kopii także i tego obiektu.
W przypadku dużych obiektów, takie wywołania konstruktorów i destruktorów mogą być kosztowne ze względu na szybkość i zużycie pamięci. Aby to zilustrować, listing 9.9 tworzy okrojony, zdefiniowany przez użytkownika obiekt klasy[Author ID1: at Wed Oct 31 09:46:00 2001
]:[Author ID1: at Wed Oct 31 09:46:00 2001
] SimpleCat. Prawdziwy obiekt byłby większy i droższy, ale nasz obiekt wystarczy do pokazania, jak często wywoływany jest konstruktor kopiujący[Author ID1: at Wed Oct 31 09:46:00 2001
]i[Author ID1: at Wed Oct 31 09:46:00 2001
] oraz destruktor.
Listing 9.10 tworzy obiekt typu [Author ID1: at Wed Oct 31 09:46:00 2001 ]SimpleCat, po czym wywołuje dwie funkcje. Pierwsza z nich otrzymuje obiekt poprzez wartość i zwraca go również poprzez wartość. Druga funkcja otrzymuje wskaźnik do obiektu i zwraca także wskaźnik, bez przekazywania samego obiektu.
Listing 9.10. Przekazywanie obiektów poprzez referencję, za pomocą wskaźników
0: //Listing 9.10
1: // Przekazywanie wskaźników do obiektów
2:
3: #include <iostream>
4:
5: using namespace std;
6: class SimpleCat
7: {
8: public:
9: SimpleCat (); // konstruktor
10: SimpleCat(SimpleCat&); // konstruktor kopiujący[Author ID1: at Wed Oct 31 09:47:00 2001
]i[Author ID1: at Wed Oct 31 09:47:00 2001
]
11: ~SimpleCat(); // destruktor
12: };
13:
14: SimpleCat::SimpleCat()
15: {
16: cout << "Konstruktor klasy SimpleCat...\n";
17: }
18:
19: SimpleCat::SimpleCat(SimpleCat&)
20: {
21: cout << "Konstruktor kopiujący[Author ID1: at Wed Oct 31 09:47:00 2001
]i[Author ID1: at Wed Oct 31 09:47:00 2001
] klasy SimpleCat...\n";
22: }
23:
24: SimpleCat::~SimpleCat()
25: {
26: cout << "Destruktor klasy SimpleCat...\n";
27: }
28:
29: SimpleCat FunctionOne (SimpleCat theCat);
30: SimpleCat* FunctionTwo (SimpleCat *theCat);
31:
32: int main()
33: {
34: cout << "Tworze obiekt...\n";
35: SimpleCat Mruczek;
36: cout << "Wywoluje funkcje FunctionOne...\n";
37: FunctionOne(Mruczek);
38: cout << "Wywoluje funkcje FunctionTwo...\n";
39: FunctionTwo(&Mruczek);
40: return 0;
41: }
42:
43: // FunctionOne, parametr przekazywany poprzez wartość
44: SimpleCat FunctionOne(SimpleCat theCat)
45: {
46: cout << "FunctionOne. Wracam...\n";
47: return theCat;
48: }
49:
50: // FunctionTwo, parametr przekazywany poprzez wskaźnik
51: SimpleCat* FunctionTwo (SimpleCat *theCat)
52: {
53: cout << "FunctionTwo. Wracam...\n";
54: return theCat;
55: }
Wynik
Tworze obiekt...
Konstruktor klasy SimpleCat...
Wywoluje funkcje FunctionOne...
Konstruktor kopiujący[Author ID1: at Wed Oct 31 09:47:00 2001
]i[Author ID1: at Wed Oct 31 09:47:00 2001
] klasy SimpleCat...
FunctionOne. Wracam...
Konstruktor kopiujący[Author ID1: at Wed Oct 31 09:47:00 2001
]i[Author ID1: at Wed Oct 31 09:47:00 2001
] klasy SimpleCat...
Destruktor klasy SimpleCat...
Destruktor klasy SimpleCat...
Wywoluje funkcje FunctionTwo...
FunctionTwo. Wracam...
Destruktor klasy SimpleCat...
Analiza
W liniach od 6. do 12. została zadeklarowana bardzo uproszczona klasa SimpleCat. Zarówno konstruktor, jak i konstruktor kopiuj[Author ID1: at Wed Oct 31 09:47:00 2001
]ą[Author ID1: at Wed Oct 31 09:48:00 2001
]cy[Author ID1: at Wed Oct 31 09:47:00 2001
]i[Author ID1: at Wed Oct 31 09:48:00 2001
] oraz destruktor wypisują odpowiednie dla siebie komunikaty, dzięki którym wiadomo, w którym momencie zostały wywołane.
W linii 34. funkcja main() wypisuje komunikat widoczny w pierwszej linii wyniku. W linii 35. tworzony jest egzemplarz obiektu klasy SimpleCat. Powoduje to wywołanie konstruktora tej klasy, co potwierdza druga linia wyniku.
W linii 36. funkcja main() zgłasza (poprzez wypisanie komunika[Author ID1: at Wed Oct 31 09:49:00 2001
]tu w trzeciej linii wydruku)[Author ID1: at Wed Oct 31 09:49:00 2001
], [Author ID1: at Wed Oct 31 09:50:00 2001
]że wywołuje funkcję FunctionOne.[Author ID1: at Wed Oct 31 09:50:00 2001
], która wypisuje komunikat[Author ID1: at Wed Oct 31 09:50:00 2001
] w trzeciej linii wyniku[Author ID1: at Wed Oct 31 09:49:00 2001
]. Ponieważ ta funkcja otrzymuje obiekt typu [Author ID1: at Wed Oct 31 09:50:00 2001
]SimpleCat przekazywany poprzez wartość, na stosie tworzona jest lokalna dla tej funkcji kopia obiektu klasy SimpleCat. To powoduje wywołanie konstruktora kopiującego[Author ID1: at Wed Oct 31 09:51:00 2001
]i[Author ID1: at Wed Oct 31 09:51:00 2001
], który wypisuje czwartą linię wyniku.
Wykonanie programu przechodzi do wywoływanej funkcji, do linii 46., w której wypisywany jest komunikat informacyjny, stanowiący piątą linię wyniku. Następnie funkcja wraca i zwraca obiekt typu [Author ID1: at Wed Oct 31 09:51:00 2001
]SimpleCat poprzez wartość. To powoduje utworzenie kolejnej kopii obiektu (łącznie z[Author ID1: at Wed Oct 31 09:51:00 2001
]poprzez[Author ID1: at Wed Oct 31 09:51:00 2001
] wywołaniem[Author ID1: at Wed Oct 31 09:52:00 2001
] konstruktora kopiującego[Author ID1: at Wed Oct 31 09:52:00 2001
]i[Author ID1: at Wed Oct 31 09:52:00 2001
], wypisującego też [Author ID1: at Wed Oct 31 09:52:00 2001
]szóstą linię wyniku).
Wartość zwracana przez funkcję FunctionOne() nie jest niczemu przypisywana, więc tymczasowy obiekt utworzony na stosie jest odrzucany, co powoduje wywołanie destruktora, który wypisuje siódmą linię wyniku. Ponieważ działanie funkcji FunctionOne() się zakończyło, jej lokalna kopia obiektu wychodzi z zakresu i jest niszczona; powoduje to wywołanie destruktora i wypisanie ósmej linii wyniku.
Program wraca do funkcji main(), w której zostaje teraz wywołana funkcja FunctionTwo(), lecz tym razem jej parametr jest przekazywany przez referencję. Nie jest tworzona żadna kopia, dlatego nie jest wypisywany żaden komunikat konstruktora. Funkcja FunctionTwo() wypisuje jedynie własny komunikat w dziesiątej linii wyniku, po czym zwraca obiekt typu [Author ID1: at Wed Oct 31 09:53:00 2001 ]SimpleCat, także poprzez wskaźnik, zatem także tym razem nie jest wywoływany konstruktor ani destruktor.
Program kończy swoje działanie i obiekt Mruczek wychodzi z zakresu, powodując jeszcze jedno wywołanie destruktora, wypisującego komunikat w jedenastej linii wyniku.
Ponieważ parametr funkcji FunctionOne() jest przekazywany i zwracany przez wartość, jej wywołanie wiąże się z dwoma wywołaniami konstruktora kopiującego[Author ID1: at Wed Oct 31 09:53:00 2001
]i[Author ID1: at Wed Oct 31 09:53:00 2001
] i dwoma wywołaniami destruktora; natomiast wywołanie funkcji FunctionTwo() nie wymagało wywołania ani konstruktora, ani destruktora.
Przekazywanie wskaźnika const
Choć przekazywanie wskaźnika jest dużo bardziej efektywne w funkcji FunctionTwo(), jednak jest także bardziej niebezpieczne. Funkcja FunctionTwo() nie powinna mieć możliwości zmiany otrzymanego obiektu SimpleCat, mimo, że[Author ID1: at Wed Oct 31 09:53:00 2001
]ale[Author ID1: at Wed Oct 31 09:54:00 2001
] otrzymuje wskaźnik do tego obiektu. To[Author ID1: at Wed Oct 31 09:54:00 2001
]Ten wskaźnik[Author ID1: at Wed Oct 31 09:54:00 2001
] daje jej jednak [Author ID1: at Wed Oct 31 09:56:00 2001
]możliwość zmiany wartości tego [Author ID1: at Wed Oct 31 09:55:00 2001
]obiektu, co nie jest możliwe w przypadku przekazywania obiektu przez wartość.
Przekazywanie poprzez wartość przypomina przekazanie do muzeum reprodukcji arcydzieła, zamiast prawdziwego obrazu. Nawet, gdy do muzeum zakradnie się wandal, oryginał nie poniesie uszczerbku. Przekazywanie poprzez referencję przypomina przesłanie do muzeum swojego adresu domowego i zaproszenie gości do oglądania oryginałów.
Rozwiązaniem tego problemu jest przekazanie wskaźnika do stałego (const) obiektu typu [Author ID1: at Wed Oct 31 09:56:00 2001
]SimpleCat. W ten sposób zabezpieczamy ten obiekt przed wywoływaniem metod tej klasy [Author ID1: at Wed Oct 31 09:57:00 2001
]innych niż metody typu [Author ID1: at Wed Oct 31 09:57:00 2001
]niż[Author ID1: at Wed Oct 31 09:57:00 2001
] const [Author ID1: at Wed Oct 31 09:57:00 2001
], chroniąc go tym samym[Author ID1: at Wed Oct 31 09:57:00 2001
] [Author ID1: at Wed Oct 31 09:58:00 2001
]metod tej klasy, czyli chronimy go[Author ID1: at Wed Oct 31 09:57:00 2001
] przed zmianami.
Przekazanie referencji typu [Author ID1: at Wed Oct 31 09:59:00 2001 ]const umożliwia gościom oglądanie oryginału, ale nie umożliwia jego modyfikacji. Demonstruje to listing 9.11.
Listing 9.11. Przekazywanie wskaźnika do obiektu const
0: //Listing 9.11
1: // Przekazywanie wskaźników do obiektów
2:
3: #include <iostream>
4:
5: using namespace std;
6: class SimpleCat
7: {
8: public:
9: SimpleCat();
10: SimpleCat(SimpleCat&);
11: ~SimpleCat();
12:
13: int GetAge() const { return itsAge; }
14: void SetAge(int age) { itsAge = age; }
15:
16: private:
17: int itsAge;
18: };
19:
20: SimpleCat::SimpleCat()
21: {
22: cout << "Konstruktor klasy SimpleCat...\n";
23: itsAge = 1;
24: }
25:
26: SimpleCat::SimpleCat(SimpleCat&)
27: {
28: cout << "Konstruktor kopiujący[Author ID1: at Wed Oct 31 10:01:00 2001
]i[Author ID1: at Wed Oct 31 10:01:00 2001
] klasy SimpleCat...\n";
29: }
30:
31: SimpleCat::~SimpleCat()
32: {
33: cout << "Destruktor klasy SimpleCat...\n";
34: }
35:
36: const SimpleCat * const FunctionTwo
37: (const SimpleCat * const theCat);
38:
39: int main()
40: {
41: cout << "Tworze obiekt...\n";
42: SimpleCat Mruczek;
43: cout << "Mruczek ma " ;
44: cout << Mruczek.GetAge();
45: cout << " lat\n";
46: int age = 5;
47: Mruczek.SetAge(age);
48: cout << "Mruczek ma " ;
49: cout << Mruczek.GetAge();
50: cout << " lat\n";
51: cout << "Wywoluje funkcje FunctionTwo...\n";
52: FunctionTwo(&Mruczek);
53: cout << "Mruczek ma " ;
54: cout << Mruczek.GetAge();
55: cout << " lat\n";
56: return 0;
57: }
58:
59: // functionTwo, otrzymuje wskaźnik const
60: const SimpleCat * const FunctionTwo
61: (const SimpleCat * const theCat)
62: {
63: cout << "FunctionTwo. Wracam...\n";
64: cout << "Mruczek ma teraz " << theCat->GetAge();
65: cout << " lat \n";
66: // theCat->SetAge(8); const!
67: return theCat;
68: }
Wynik
Tworze obiekt...
Konstruktor klasy SimpleCat...
Mruczek ma 1 lat
Mruczek ma 5 lat
Wywoluje funkcje FunctionTwo...
FunctionTwo. Wracam...
Mruczek ma teraz 5 lat
Mruczek ma 5 lat
Destruktor klasy SimpleCat...
Analiza
Klasa SimpleCat zawiera dwa akcesory: GetAge() w linii 13., będący funkcją const oraz SetAge() w linii 14., nie będący funkcją const. Oprócz tego posiada zmienną składową itsAge, deklarowaną w linii 17.
Konstruktor, konstruktor kopiujący[Author ID1: at Wed Oct 31 10:02:00 2001
]i[Author ID1: at Wed Oct 31 10:02:00 2001
] oraz destruktor wypisują odpowiednie komunikaty. Jednak konstruktor kopiują[Author ID1: at Wed Oct 31 10:03:00 2001
]cy[Author ID1: at Wed Oct 31 10:03:00 2001
]i[Author ID1: at Wed Oct 31 10:03:00 2001
] nie jest wywoływany, gdyż obiekt przekazywany jest poprzez referencję i nie jest tworzona żadna kopia. Na początku programu, w linii 42., tworzony jest obiekt, a w liniach[Author ID1: at Wed Oct 31 10:03:00 2001
]i[Author ID1: at Wed Oct 31 10:03:00 2001
] od [Author ID1: at Wed Oct 31 10:31:00 2001
]43. do 45[Author ID1: at Wed Oct 31 10:03:00 2001
]. [Author ID1: at Wed Oct 31 10:03:00 2001
]jest wypisywany wiek początkowy.
W linii 47. zmienna składowa itsAge jest ustawiana za pomocą akcesora SetAge(), zaś wynik jest wypisywany w liniach[Author ID1: at Wed Oct 31 10:03:00 2001
]i[Author ID1: at Wed Oct 31 10:03:00 2001
] od [Author ID1: at Wed Oct 31 10:03:00 2001
]48. do 50[Author ID1: at Wed Oct 31 10:03:00 2001
]. W tym programie nie jest używana funkcja FunctionOne(). [Author ID1: at Wed Oct 31 10:04:00 2001
] i [Author ID1: at Wed Oct 31 10:04:00 2001
]P[Author ID1: at Wed Oct 31 10:04:00 2001
]p[Author ID1: at Wed Oct 31 10:04:00 2001
]osługujemy się tylko [Author ID1: at Wed Oct 31 10:04:00 2001
]funkcją FunctionTwo(). Uległa ona jednak niewielkiej zmianie; jej nagłówek został zmodyfikowany tak, że funkcja przyjmuje teraz stały wskaźnik do stałego obiektu i zwraca stały wskaźnik do stałego obiektu.
Ponieważ parametr i wartość zwracana[Author ID1: at Wed Oct 31 10:04:00 2001
] [Author ID1: at Wed Oct 31 10:05:00 2001
]otna[Author ID1: at Wed Oct 31 10:05:00 2001
] wciąż są przekazywane poprzez referencje, nie są tworzone żadne kopie, nie jest zatem wywoływany konstruktor kopiujący[Author ID1: at Wed Oct 31 10:05:00 2001
]i[Author ID1: at Wed Oct 31 10:05:00 2001
]. Jednak obecnie obiekt wskazywany w funkcji FunctionTwo() jest obiektem const, więc nie można wywoływać jego metod, nie będących metodami const, czyli nie można wywołać jego metody SetAge(). Gdyby wywołanie tej metody w linii 66. nie zostało umieszczone w komentarzu, program nie skompilowałby się.
Zwróć uwagę, że obiekt tworzony w funkcji main() nie jest const, więc możemy dla niego wywołać funkcję SetAge(). Do funkcji FunctionTwo()przekazywany jest adres tego zwykłego obiektu, ale ponieważ deklaracja tej funkcji określa, że ten parametr jest wskaźnikiem const do obiektu const, obiekt ten jest traktowany, jakby był stały!
Referencje jako metoda alternatywna
Listing 9.11 rozwiązuje problem tworzenia dodatkowych kopii i w ten sposób zmniejsza ilość wywołań konstruktora kopiując[Author ID1: at Wed Oct 31 10:05:00 2001
]egoi[Author ID1: at Wed Oct 31 10:05:00 2001
] i destruktora. Używa stałych wskaźników do stałych obiektów, rozwiązując w ten sposób problem zmiany obiektu przez funkcję. Jednak w dalszym ciągu jest dość nieczytelny, gdyż obiekty przekazywane do funkcji są wskaźnikami.
Ponieważ wiemy, że ten obiekt nie jest pusty, możemy ułatwić sobie pracę w funkcji, stosując przekazanie przez referencję, a nie przez wskaźnik. Pokazuje to listing 9.12.
Listing 9.12. Przekazywanie referencji do obiektów
0: //Listing 9.12
1: // Przekazywanie wskaźników do obiektów
2:
3: #include <iostream>
4:
5: using namespace std;
6: class SimpleCat
7: {
8: public:
9: SimpleCat();
10: SimpleCat(SimpleCat&);
11: ~SimpleCat();
12:
13: int GetAge() const { return itsAge; }
14: void SetAge(int age) { itsAge = age; }
15:
16: private:
17: int itsAge;
18: };
19:
20: SimpleCat::SimpleCat()
21: {
22: cout << "Konstruktor klasy SimpleCat...\n";
23: itsAge = 1;
24: }
25:
26: SimpleCat::SimpleCat(SimpleCat&)
27: {
28: cout << "Konstruktor kopiujący[Author ID1: at Wed Oct 31 10:05:00 2001
]i[Author ID1: at Wed Oct 31 10:05:00 2001
] klasy SimpleCat...\n";
29: }
30:
31: SimpleCat::~SimpleCat()
32: {
33: cout << "Destruktor klasy SimpleCat...\n";
34: }
35:
36: const SimpleCat & FunctionTwo (const SimpleCat & theCat);
37:
38: int main()
39: {
40: cout << "Tworze obiekt...\n";
41: SimpleCat Mruczek;
42: cout << "Mruczek ma " ;
43: cout << Mruczek.GetAge();
44: cout << " lat\n";
45: int age = 5;
46: Mruczek.SetAge(age);
47: cout << "Mruczek ma " ;
48: cout << Mruczek.GetAge();
49: cout << " lat\n";
50: cout << "Wywoluje funkcje FunctionTwo...\n";
51: FunctionTwo(Mruczek);
52: cout << "Mruczek ma " ;
53: cout << Mruczek.GetAge();
54: cout << " lat\n";
55: return 0;
56: }
57:
58: // functionTwo, otrzymuje referencję do obiektu const
59: const SimpleCat & FunctionTwo (const SimpleCat & theCat)
60: {
61: cout << "FunctionTwo. Wracam...\n";
62: cout << "Mruczek ma teraz " << theCat.GetAge();
63: cout << " lat \n";
64: // theCat.SetAge(8); const!
65: return theCat;
66: }
Wynik
Tworze obiekt...
Konstruktor klasy SimpleCat...
Mruczek ma 1 lat
Mruczek ma 5 lat
Wywoluje funkcje FunctionTwo...
FunctionTwo. Wracam...
Mruczek ma teraz 5 lat
Mruczek ma 5 lat
Destruktor klasy SimpleCat...
Analiza
Wynik jest identyczny z wynikiem z listingu 9.11. Jedyną istotną różnicą w programie jest to, że obecnie funkcja FunctionTwo() otrzymuje i zwraca referencję do stałego obiektu. Także tym razem praca z referencjami jest nieco prostsza od pracy ze wskaźnikami, a na dodatek zapewnia tę samą efektywność oraz bezpieczeństwo obiektu const.
Referencje const
Programiści C++ zwykle nie uznają różnicy pomiędzy „stałą referencją do obiektu typu [Author ID1: at Wed Oct 31 10:06:00 2001 ]SimpleCat” a „referencją do stałego obiektu typu [Author ID1: at Wed Oct 31 10:06:00 2001 ]SimpleCat”. Referencje nigdy nie mogą otrzymać ponownego przypisania i odnosić się do innego obiektu, więc są zawsze stałe. Jeśli słowo kluczowe const zostanie zastosowane w odniesieniu do referencji, sprawi, że to obiekt związany z referencją staje się stały.
Kiedy używać wskaźników, a kiedy referencji
Programiści C++ zdecydowanie przedkładają referencje nad wskaźniki. Referencje są bardziej przejrzyste i łatwiejsze w użyciu, ponadto lepiej ukrywają szczegóły implementacji, co mogliśmy zobaczyć w poprzednim przykładzie.
Nie można zmieniać obiektu docelowego referencji. Jeśli chcesz najpierw wskazać na jeden obiekt, a potem na inny, musisz użyć wskaźnika. Referencje nie mogą być zerowe [Author ID1: at Wed Oct 31 10:06:00 2001
]puste[Author ID1: at Wed Oct 31 10:06:00 2001
], więc jeśli istnieje jakakolwiek możliwość,[Author ID1: at Wed Oct 31 10:07:00 2001
] że dany obiekt będzie pusty [Author ID1: at Wed Oct 31 10:07:00 2001
](tzn., że może przestać istnieć)[Author ID1: at Wed Oct 31 10:07:00 2001
], nie możesz użyć referencji. Musisz użyć wskaźnika.
To ostatnie zagadnienie dotyczy operatora new. Gdy new nie może zaalokować pamięci na stercie, zwraca wskaźnik null (wskaźnik zerowy[Author ID1: at Wed Oct 31 10:08:00 2001 ], czyli [Author ID1: at Wed Oct 31 10:08:00 2001 ]pusty). Ponieważ referencje nie mogą być puste, nie wolno ci przypisać referencji do tej pamięci, dopóki nie upewnisz się, że nie jest pusta. Właściwy sposób przypisania pokazuje poniższy przykład:
int *pInt = new int;
if (pInt != NULL)
int &rInt = *pInt;
W tym przykładzie deklarowany jest wskaźnik pInt do typu int; jest on inicjalizowany adresem pamięci zwracanym przez operator new. Następnie jest sprawdzany adres w pInt i jeśli nie jest on pusty, wyłuskiwany jest wskaźnik. Rezultatem wyłuskania wskaźnika do typu int jest obiekt int, więc referencja rInt jest inicjalizowana jako odnosząca się do tego obiektu. W efekcie, referencja rInt staje się aliasem do wartości int o adresie zwróconym przez operator new.
TAK |
NIE |
Jeśli jest to możliwe, przekazuj
Jeśli jest to możliwe, przekazuj przez referencję wartość zwracaną [Author ID1: at Wed Oct 31 10:19:00 2001
] Jeśli jest to możliwe, używaj const do ochrony referencji i wskaźników. |
Nie używaj wskaźników tam, gdzie można użyć referencji. |
Łączenie referencji i wskaźników
Dozwolone jest jednoczesne deklarowanie wskaźników oraz referencji na tej samej liście parametrów funkcji, a także obiektów przekazywanych przez wartość. Na przykład:
CAT * SomeFunction (Person &theOwner, House *theHouse, int age);
Ta deklaracja informuje, że funkcja SomeFunction ma trzy parametry. Pierwszy z nich jest referencją[Author ID1: at Wed Oct 31 10:19:00 2001
]a[Author ID1: at Wed Oct 31 10:19:00 2001
] do obiektu klasy Person (osoba), drugim jest wskaźnik do obiektu klasy House (dom), zaś trzecim jest wartość typu int. Funkcja zwraca wskaźnik do obiektu klasy CAT.
Pytanie, gdzie powinien zostać umieszczony operator referencji (&) lub wskaźnika (*), jest bardzo kontrowersyjne. Możesz zastosować któryś z poniższych zapisów:
1: CAT& rMruczek;
2: CAT & rMruczek;
3: CAT &rMruczek;
UWAGA Białe spacje są całkowicie ignorowane, dlatego wszędzie tam, gdzie można umieścić spację, można także umieścić dowolną ilość innych spacji, tabulatorów czy nowych linii.
Jeśli powyższ[Author ID1: at Wed Oct 31 10:20:00 2001
]e zapis[Author ID1: at Wed Oct 31 10:20:00 2001
]y Pozostawiając zagadnienia wyrażeń[Author ID1: at Wed Oct 31 10:20:00 2001
]są równoważn[Author ID1: at Wed Oct 31 10:20:00 2001
]e, który z zapisów [Author ID1: at Wed Oct 31 10:21:00 2001
]nich [Author ID1: at Wed Oct 31 10:21:00 2001
]jest najlepszy? Oto argumenty przemawiające za wszystkimi trzema:
Argumentem przemawiającym za przypadkiem 1. jest to, że rMruczek jest zmienną, której nazwą jest rMruczek, zaś typ może być traktowany jako „referencja do obiektu klasy CAT”. Zgodnie z tą argumentacją, & powinno znaleźć się przy typie.
Argumentem przeciwko przypadkowi 1. jest to, że typem jest klasa CAT. Symbol & jest częścią „deklaratora” zawierającego nazwę klasy i znak ampersand (&). Jednak umieszczenie & przy CAT może spowodować wystąpienie poniższego błędu:
CAT& rMruczek, rFilemon;
Szybkie sprawdzenie tej linii może doprowadzić cię do odkrycia, że zarówno rMruczek, jak i rFilemon są referencjami do obiektów klasy CAT, ale w rzeczywistości tak nie jest. Ta deklaracja informuje, że rMruczek jest referencją do klasy CAT, zaś rFilemon (mimo zastosowanego przedrostka) nie jest referencją, lecz zwykłym obiektem klasy CAT. Tę deklarację należy przepisać następująco:
CAT &rMruczek, rFilemon;
Wniosek płynący z powyższych rozważań brzmi następująco: deklaracje referencji i zmiennych nigdy nie powinny występować w tej samej linii. Oto poprawny zapis:
CAT& rMruczek;
CAT Filemon;
Wielu programistów optuje za zastosowaniem operatora pośrodku, tak jak pokazuje przypadek 2.
Oczywiście, wszystko, co powiedziano dotąd na temat operatora referencji (&), odnosi się także do operatora wskaźnika (*). Należy zdawać sobie sprawę, że styl zapisu zależy od programisty. Wybierz więc styl, który ci odpowiada i konsekwentnie stosuj go w programach; przejrzystość kodu jest w końcu jednym z twoich głównych celów.
Deklarując referencje i wskaźniki, wielu programistów przestrzega następujących konwencji:
Umieszczaj znak ampersand lub gwiazdkę pośrodku, ze spacją po obu stronach.
Nigdy nie deklaruj w tej samej linii referencji, wskaźników i zmiennych.
Nie pozwól funkcji zwracać referencji do obiektu, którego nie ma w zakresie!
Gdy programiści C++ nauczą się korzystać z referencji, przejawiają tendencję do używania ich bez zastanowienia, wszędzie, gdzie tylko się da. Można z tym przesadzić. Pamiętaj, że referencja jest zawsze aliasem do innego obiektu. Gdy przekazujesz referencje do lub z funkcji, pamiętaj, by zadać sobie pytanie: „Czym jest obiekt, do którego odnosi się referencja, i czy będzie istniał przez cały czas, gdy będę z niego korzystał?”
Listing 9.13 pokazuje niebezpieczeństwo zwrócenia referencji do obiektu, który już nie istnieje.
Listing 9.13. Zwracanie referencji do nieistniejącego obiektu
0: // Listing 9.13
1: // Zwracanie referencji do obiektu
2: // który już nie istnieje
3:
4: #include <iostream>
5:
6:
7: class SimpleCat
8: {
9: public:
10: SimpleCat (int age, int weight);
11: ~SimpleCat() {}
12: int GetAge() { return itsAge; }
13: int GetWeight() { return itsWeight; }
14: private:
15: int itsAge;
16: int itsWeight;
17: };
18:
19: SimpleCat::SimpleCat(int age, int weight)
20: {
21: itsAge = age;
22: itsWeight = weight;
23: }
24:
25: SimpleCat &TheFunction();
26:
27: int main()
28: {
29: SimpleCat &rCat = TheFunction();
30: int age = rCat.GetAge();
31: std::cout << "rCat ma " << age << " lat!\n";
32: return 0;
33: }
34:
35: SimpleCat &TheFunction()
36: {
37: SimpleCat Mruczek(5,9);
38: return Mruczek;
39: }
Wynik
Błąd kompilacji: próba zwrócenia referencji do lokalnego obiektu!
OSTRZEŻENIE Ten program nie skompiluje się z kompilatorem firmy Borland. Skompiluje się jednak z kompilatorem firmy Microsoft, jednak[Author ID1: at Wed Oct 31 10:21:00 2001
]co mimo [Author ID1: at Wed Oct 31 10:21:00 2001
]wszystko powinno to być uważane za błąd.
Analiza
W liniach od 7. do 17. deklarowana jest klasa SimpleCat. W linii 29. referencja do klasy SimpleCat jest inicjalizowana rezultatem wywołania funkcji TheFunction(), zadeklarowanej w linii 25. jako zwracająca referencję do obiektów [Author ID1: at Wed Oct 31 10:22:00 2001 ]klasy SimpleCat.
W ciele funkcji TheFunction() jest deklarowany lokalny obiekt typu SimpleCat; konstruktor inicjalizuje jego wiek i wagę. Następnie ten obiekt lokalny jest zwracany poprzez referencję. Niektóre kompilatory są na tyle inteligentne, by wychwycić ten błąd i nie pozwolić na uruchomienie programu. Inne pozwolą na jego skompilowanie i uruchomienie, co może spowodować nieprzewidywalne zachowanie komputera.
Gdy funkcja TheFunction() kończy działanie, jej obiekt lokalny, Mruczek, jest niszczony (zapewniam, że bezboleśnie). Referencja zwracana przez tę funkcję staje się aliasem do nieistniejącego obiektu, a to poważny błąd.
Zwracanie referencji do obiektu na stercie
Być może kusi cię rozwiązanie problemu z listingu 9.13 - modyfikacja funkcji TheFunction() tak, by tworzyła Mruczka na stercie. Dzięki temu, gdy funkcja zakończy działanie, Mruczek będzie nadal istniał.
W tym miejscu pojawia się następujący problem: co zrobisz z pamięcią zaalokowaną dla obiektu Mruczek, gdy nie będzie już potrzebny? To zagadnienie ilustruje listing 9.14.
Listing 9.14. Wycieki pamięci
0: // Listing 9.14
1: // Unikanie wycieków pamięci
2:
3: #include <iostream>
4:
5: class SimpleCat
6: {
7: public:
8: SimpleCat (int age, int weight);
9: ~SimpleCat() {}
10: int GetAge() { return itsAge; }
11: int GetWeight() { return itsWeight; }
12:
13: private:
14: int itsAge;
15: int itsWeight;
16: };
17:
18: SimpleCat::SimpleCat(int age, int weight)
19: {
20: itsAge = age;
21: itsWeight = weight;
22: }
23:
24: SimpleCat & TheFunction();
25:
26: int main()
27: {
28: SimpleCat & rCat = TheFunction();
29: int age = rCat.GetAge();
30: std::cout << "rCat ma " << age << " lat!\n";
31: std::cout << "&rCat: " << &rCat << std::endl;
32: // jak się go pozbędziesz z pamięci?
33: SimpleCat * pCat = &rCat;
34: delete pCat;
35: // a do czego teraz odnosi się rCat??
36: return 0;
37: }
38:
39: SimpleCat &TheFunction()
40: {
41: SimpleCat * pMruczek = new SimpleCat(5,9);
42: std::cout << "pMruczek: " << pMruczek << std::endl;
43: return *pMruczek;
44: }
Wynik
pMruczek: 004800F0
rCat ma 5 lat!
&rCat: 004800F0
OSTRZEŻENIE Ten program kompiluje się, uruchamia i sprawia wrażenie, że działa poprawnie. Jest jednak swego rodzaju bombą zegarową, która może w każdej chwili wybuchnąć.
Funkcja TheFunction() została zmieniona tak, że już nie zwraca referencji do lokalnej zmiennej. W linii 41. funkcja alokuje pamięć na stercie i przypisuje jej adres do wskaźnika. Adres zawarty w tym wskaźniku jest wypisywany w następnej linii, po czym wskaźnik jest wyłuskiwany, a wskazywany przez niego obiekt typu [Author ID1: at Wed Oct 31 10:22:00 2001 ]SimpleCat jest zwracany przez referencję.
W linii 28. wynik funkcji TheFunction() jest przypisywany referencji do obiektu [Author ID1: at Wed Oct 31 10:23:00 2001 ]klasy SimpleCat, po czym ta referencja jest używana w celu uzyskania wieku kota, wypisywanego w linii 30.
Aby udowodnić, że referencja zadeklarowana w funkcji main() odnosi się do obiektu umieszczonego na stercie przez funkcję TheFunction(), do referencji rCat został zastosowany operator adresu. Oczywiście, wyświetla on adres obiektu, do którego odnosi się referencja, zgodny z adresem pamięci na stercie.
Jak dotąd wszystko jest w porządku. Ale w jaki sposób możemy zwolnić tę pamięć? Nie można wywołać operatora delete na[Author ID1: at Wed Oct 31 10:23:00 2001
]dla[Author ID1: at Wed Oct 31 10:23:00 2001
] referencji. Sprytnym rozwiązaniem jest utworzenie kolejnego wskaźnika i zainicjalizowanie go adresem uzyskanym od referencji rCat. Dzięki temu można zwolnić pamięć i „powstrzymać” jej wyciek. Powstaje jednak pewien problem: do czego odnosi się referencja rCat po wykonaniu linii 34.? Jak już wspomnieliśmy, referencja zawsze musi stanowić alias rzeczywistego obiektu; jeśli odnosi się do obiektu pustego (tak, jak w tym przypadku), program jest błędny.
UWAGA Jeszcze raz należy przypomnieć, że program z referencją do pustego obiektu może się skompilować, ale jest błędny i jego działanie jest nieprzewidywalne.
Istnieją trzy rozwiązania tego problemu. Pierwszym jest zadeklarowanie obiektu typu [Author ID1: at Wed Oct 31 10:23:00 2001 ]SimpleCat w linii 28. i zwrot tego obiektu z funkcji TheFunction() poprzez wartość. Drugim jest zadeklarowanie w funkcji TheFunction() obiektu SimpleCat na stercie, lecz ze zwróceniem wskaźnika. Wtedy funkcja wywołująca może sama usunąć ten wskaźnik gdy, nie będzie już potrzebować obiektu.
Trzecim rozwiązaniem, tym właściwym, jest zadeklarowanie obiektu w funkcji wywołującej i przekazanie go funkcji TheFunction() przez referencję.
Wskaźnik, wskaźnik, kto ma wskaźnik?
Gdy program alokuje pamięć na stercie, otrzymuje wskaźnik. Przechowywanie tego wskaźnika jest koniecznością, gdy zostanie on utracony, pamięć nie będzie mogła zostać zwolniona i powi[Author ID1: at Wed Oct 31 10:24:00 2001
]ę[Author ID1: at Wed Oct 31 10:33:00 2001
]kszy [Author ID1: at Wed Oct 31 10:24:00 2001
]stanie się[Author ID1: at Wed Oct 31 10:24:00 2001
] tzw. [Author ID1: at Wed Oct 31 10:24:00 2001
]wyciek [Author ID1: at Wed Oct 31 10:24:00 2001
]iem[Author ID1: at Wed Oct 31 10:24:00 2001
] pamięci.
W czasie przekazywania bloku pamięci pomiędzy funkcjami, ktoś przez cały czas „posiada” ten wskaźnik. Zwykle wartości w bloku są przekazywane poprzez referencje, zaś funkcja, która stworzyła pamięć, zajmuje się jej zwolnieniem. Jest to jednak reguła poparta doświadczeniem, a nie zasada wyryta w kamieniu.
Tworzenie pamięci w jednej funkcji i zwalnianie jej w innej może być niebezpieczne. Nieporozumienia co do tego, kto posiada wskaźnik, mogą spowodować dwa następujące problemy: zapomnienie o zwolnieniu wskaźnika lub dwukrotnie zwolnienie go. W obu przypadkach jest to poważny błąd programu. Bezpieczniej jest budować funkcje tak, by usuwały pamięć, którą stworzyły.
Jeśli piszesz funkcję, która musi stworzyć pamięć, po czym przekazać ją funkcji wywołującej, zastanów się nad zmianą jej interfejsu. Niech funkcja wywołująca sama alokuje pamięć i przekazuje ją innej funkcji przez referencję. Dzięki temu zarządzanie pamięcią pozostaje w tej funkcji, która jest przygotowana do jej usunięcia.
TAK |
NIE |
Gdy jesteś do tego zmuszony, przekazuj parametry przez wartość. Gdy jesteś do tego zmuszony, zwracaj wynik funkcji przez wartość. |
Nie przekazuj referencji[Author ID1: at Wed Oct 31 10:24:00 2001
] Nie używaj referencji do pustych obiektów. |
Ta nazwa jest skrótem od słowa temporary (tymczasowa) i bardzo często występuje w programach. — przyp.tłum.
2 Część I ♦ Podstawy obsługi systemu WhizBang (Nagłówek strony)
2 F:\korekta\r09-06.doc[Author ID2: at Sat Nov 17 08:57:00 2001
]C:\Moje dokumenty\jr\doc\Korekt_rzeczo\2\Kopia r09-05.doc[Author ID2: at Sat Nov 17 08:57:00 2001
]