Professional Documents
Culture Documents
Dane
Inicjalizacje w C++
𝑖𝑛𝑡 𝑥 = 5;
,lub
𝑖𝑛𝑡 𝑥(5);
𝑖𝑛𝑡 𝑥{5};
𝑖𝑛𝑡 𝑥 = {5};
𝑖𝑛𝑡 𝑥{ }; 𝑙𝑢𝑏 𝑖𝑛𝑡 𝑥 = { }; −𝑜𝑧𝑛𝑎𝑐𝑧𝑎𝑗ą 𝑖𝑛𝑖𝑐𝑗𝑎𝑙𝑖𝑧𝑎𝑐𝑗ę 𝑧𝑒𝑟𝑒𝑚.
Składnia inicjalizacji klamrowej lepiej zabezpiecza przed błędami konwersji typów.
1
𝑐𝑜𝑢𝑡 ≪ 𝑑𝑒𝑐;
𝑐𝑜𝑢𝑡 ≪ 42; (𝑤𝑦ś𝑤𝑖𝑒𝑡𝑙𝑖 42)
manipulator działa dla cout tak długo aż nie zostanie ustawiony inny manipulator
(domyślnie jest to dec)
Typ char
cin i cout przeprowadzają na bieżąco konwersje – tj. gdy zainicjalizujemy zmienna
znakową o wartości np ′𝑀′ to wyświetli się nam 𝑀, a nie odpowiadający jej znak ASCII - 77.
Kompilator jednak czyta sobie ten znak już jako 77, a nie 𝑀. Tylko dla nas funkcja cout i cin
przeprowadza konwersję. Jednak gdy przypiszemy tą zmienną do zmiennej int to kompilator
pokaże już liczbę 77.
Przykład:
𝑐ℎ𝑎𝑟 𝑐ℎ =′ 𝑀′ ;
𝑖𝑛𝑡 𝑖 = 𝑐ℎ;
𝑐𝑜𝑢𝑡 ≪ "𝐾𝑜𝑑 𝐴𝑆𝐶𝐼𝐼 𝑧𝑛𝑎𝑘𝑢 " ≪ 𝑐ℎ ≪ " 𝑡𝑜 " ≪ 𝑖;
Co nam zwróci:
𝐾𝑜𝑑 𝐴𝑆𝐶𝐼𝐼 𝑧𝑛𝑎𝑘𝑢 ′𝑀′ 𝑡𝑜 77
A teraz operacja matematyczna na typie char:
𝑐ℎ+= 1;
𝑖 = 𝑐ℎ;
I ponownie po zastosowaniu cout mamy już:
𝐾𝑜𝑑 𝐴𝑆𝐶𝐼𝐼 𝑧𝑛𝑎𝑘𝑢 ′𝑁 ′ 𝑡𝑜 78
Znaki specjalne:
2
Bool
true lub 1 – prawda
false lub 0 – fałsza
przykład:
𝑏𝑜𝑜𝑙 𝑔𝑜𝑡𝑜𝑤𝑒 = 𝑡𝑟𝑢𝑒;
Można też rzutować na int (wtedy będzie true zamieniane na 1, a false na 0)
𝑖𝑛𝑡 𝑔𝑜𝑡𝑜𝑤𝑒 = 𝑡𝑟𝑢𝑒; (𝑔𝑜𝑡𝑜𝑤𝑒 𝑢𝑧𝑦𝑠𝑘𝑢𝑗𝑒 𝑤𝑎𝑟𝑡𝑜ść 1)
Można też niejawnie konwertować na int przypisując do zmiennej liczbę np:
𝑏𝑜𝑜𝑙 𝑠𝑡𝑎𝑟𝑡 = −100 (𝑠𝑡𝑎𝑟𝑡 𝑜𝑡𝑟𝑧𝑦𝑚𝑢𝑗𝑒 𝑤𝑎𝑟𝑡𝑜ść 𝑡𝑟𝑢𝑒)
𝑏𝑜𝑜𝑙 𝑠𝑡𝑜𝑝 = 0 (𝑠𝑡𝑜𝑝 𝑜𝑡𝑟𝑧𝑦𝑚𝑢𝑗𝑒 𝑤𝑎𝑟𝑡𝑜ść 𝑓𝑎𝑙𝑠𝑒)
Kwalifikator CONST
Np :
𝑐𝑜𝑛𝑠𝑡 𝑖𝑛𝑡 𝑀𝑖𝑒𝑠𝑖ą𝑐𝑒 = 12;
Przedrostek const zapewnia:
stałość takiego obiektu – kompilator nie pozwala zmieniac wartości przypisanej do
zmiennej
Const lepszej jest od #define z C bo:
określa typ stałej od razu
z const można pracować z typami zaawansowanymi np tablicami
podlega zasadom określania zakresu (widoczność tylko w określonych przedziałach)
Liczby zmiennoprzecinkowe
To liczby zawierające część ułamkową. Podlegają skalowaniu np.
32.456
, oraz
0.32456
To praktycznie takie same liczby ale inaczej skalowane. Pierwszą liczbę można zapisać jak
drugą tyle, że z czynnikiem skalującym 100 (razy sto). Czynnik skalujący powoduje
przesunięcie przecinska dziesiętnego, stąd nazwa zmiennoprzecinkowy. W przypadku c++
idea jest taka sama tylko, że w systemie dwójkowym. Czynnik skalujący to nie wielokrotność
10, a 2.
Istnieją dwa sposoby zapisu liczb zmiennoprzecinkowych:
3
z kropką np. 12.34, lub 7.02
w notacji naukowej np. 2.23e-4, gdzie e-4 to podniesienie do potęgi -4
Zapis naukowy zawsze deklaruje liczbę jako zmiennoprzecinkową, nawet jeśli zapiszemy
liczbę bez kropki np.
12𝑒4 − 𝑡𝑜 𝑡𝑜 𝑠𝑎𝑚𝑜 𝑐𝑜 − 12.0𝑒4
Dzielenie
Dzielenie można wykonać na różnych typach:
, a np dzielenie 9/4 = 0 (bo nie zaokrągla tylko ucina całą część po przecinku (bo liczby są typu int)).
4
Konwersja typów – automatyczna (bez
naszej ingerencji)
a) c++ konwertuje wartości w chwili przypisywania wartości jednego typu arytmetycznego
zmiennej innego typu arytmetycznego
5
Idea jest taka, że 𝑐4 nie działało bo kompilator wie, że ta zmienna nie jest stała i może ulec
zmianie. Teraz ją pomieści, ale co jak by została przypisana zmiennej 𝑥 nowa, większa
wartość. Wtedy by wprowadził błędne konwersje, od których kompilator przy inicjalizacji
klamrowej woli stronić. Tam gdzie w przykładach były niedozwolone akcje, kompilator wywali
błąd.
b) c++ konwertuje wartości, jeśli w wyrażeniu użyto różnych typów
Kiedy w jednym wyrażeniu łączymy dwa różne typy arytmetyczne to dochodzi do dwóch
rodzajów konwersji automatycznej.
Konwersja już w chwili pojawienia się niektórych wartości w wyrażeniu. Gdy c++
interpretuje wyrażenie, zamienia wartości typu bool, char, unsigned char, signed char
i short, na typ int. Są to promocje typów całkowitych. Kompilatorowi łatwiej jest
przeprowadzić obliczenia na int bo jest do domyślny typ.
𝑠ℎ𝑜𝑟𝑡 𝑘 = 20;
𝑠ℎ𝑜𝑟𝑡 𝑝 = 34;
𝑠ℎ𝑜𝑟𝑡 𝑑 = 𝑝 + 𝑘;
Wykonanie operacji dodawania rozpoczyna się od konwersji obu zmiennych typu short na int
(jak było pisane wcześniej), a następnie przypisanie do zmiennej d już jako typ short. Algorytm
jest taki bo po prostu jak było wcześniej pisane kompilator szybciej sobie z tym radzi.
Jest też inna opcja promocji. Typ unsigned short jest konwertowany na unsigned int jeśli oba
typy mają ten sam rozmiar (zapewnia to, że podczas promocji typu unsigned short nie dojdzie
do utraty danych). Analogicznie typ wchar_t promowany jest do pierwszego typu dostatecznie
dużego by go pomieścić, z typów int, unsigned int, long, unsigned long.
Konwersja zachodząca podczas arytmetycznego łączenia różnych typów np.
dodajemy do siebie wartości int i float. Gdy działanie jest wykonywane na dwóch
typach, typ mniejszy jest promowany do większego. Np tutaj:
Wartość 9.0 jest typu double więc program konwertuje 5 na typ double i dopiero wtedy
przeprowadza dzielenie.
Ogólnie rzecz biorąc kompilator ustala jakie konwersje należy wykonać w wyrażeniu.
W c++ 11 algorytm konwersji jest troche inny:
Jeśli którykolwiek operant jest typu long double, drugi też jest konwertowalny na ten
typ
W przeciwnym razie, jeśli którykolwiek operand jest typu double, drugi jest
konwertowany na double
W przeciwnym razie, jeśli którykolwiek operand jest typu float, drugi jest konwertowany
na float
W przeciwnym razie operandy są wartościami całkowitymi i przeprowadzane są
promocje typów całkowitych
6
W takich przypadku jeśli oba operandy są typu ze znakiem (signed), albo typu bez
znaku (unsigned), a jedno z nich jest niższego porządku, przeprowadzona jest jego
konwersja na typ wyższego porządku
W przeciwnym razie jeden operand jest typu ze znakiem, a drugi bez znaku. Jeśli
operand bez znaku jest typu wyższego porządku niż operand ze znakiem, ten ostatni
jest konwertowany na typ operandu bez znaku
W przeciwnym razie jeśli typ ze znakiem może reprezentować wszystkie wartości typu
bez znaku, operand typu bez znaku jest konwertowany na typ ze znakiem
W przeciwnym razie oba operandy są konwertowane na typ w wersji bez znaku
Porządek typu jest wprost proporcjonalny do pojemności (zakresu liczbowego). Typy całkowite
ze znakiem można uporządkować malejąca jako : long long, long, int, short, signed char. Typy
bez znaków mają ten sam porządek co odpowiadające im typy ze znakiem. Dla trójki typów
znakowych char, signed char i unsigned char przyjmuje się, że wszystkie są tego samego
porządku. Typ bool jest najniższego porządku. Typy wchar_t, char16_t, char32_t są tego
samego porządku co typy na których bazują.
c) c++ konwertuje wartości przekazywane funkcjom jako parametry
Normalnie prototypy funkcji c++ sterują konwersjami typów przekazywanych parametrów
(rozdz. 7). Są od tego wyjątki ale nie zalecane (str 114).
7
Rozdział 4. Typy złożone
Łańcuch znakowy jest tylko wtedy gdy kończy się on znakiem zero ‘\0’
Wada cin
Cin radzi sobie poprawnie jedynie z pojedynczymi wyrazami. Nie radzi sobie z łańcuchami
znakowymi, które rozdziela znak biały spacji. Po napotkaniu białego znaku cin
automatycznie, sam dodaje znak ‘\0’ i rozpoznaje tym samym koniec. Nie robi nic ze
znakiem końca wiersza, który wygenerował klawisz enter.
8
getline() vs get()
getline() oraz get() pobierają cały ciąg znaków do momentu aż napotkają znak końca linii
(niwelują wadę cin). Wiedzą, że skończyła się linia przez znak enter.
Getline() różni się od get() tym, że w pierwszym, znak zera jest włączony w pobierany
łańcuch, a get nie uwzględnia go.
Getline()
𝑐𝑖𝑛. 𝑔𝑒𝑡𝑙𝑖𝑛𝑒(𝑛𝑎𝑚𝑒1, 𝐴𝑟𝑆𝑖𝑧𝑒); (𝑤𝑐𝑧𝑦𝑡𝑢𝑗𝑒 𝑑𝑎𝑛𝑒 𝑑𝑜 𝑧𝑛𝑎𝑘𝑢 𝑛𝑜𝑤𝑒𝑔𝑜 𝑤𝑖𝑒𝑟𝑠𝑧𝑎)
𝑐𝑖𝑛. 𝑔𝑒𝑡𝑙𝑖𝑛𝑒(𝑛𝑎𝑚𝑒2, 𝐴𝑟𝑆𝑖𝑧𝑒); (𝑤𝑐𝑧𝑦𝑡𝑢𝑗𝑒 𝑑𝑎𝑛𝑒 𝑑𝑜 𝑧𝑛𝑎𝑘𝑢 𝑛𝑜𝑤𝑒𝑔𝑜 𝑤𝑖𝑒𝑟𝑠𝑧𝑎)
Albo można to połączyć, możliwa jest postać:
𝑐𝑖𝑛. 𝑔𝑒𝑡𝑙𝑖𝑛𝑒(𝑛𝑎𝑚𝑒1, 𝐴𝑟𝑆𝑖𝑧𝑒). 𝑔𝑒𝑡𝑙𝑖𝑛𝑒(𝑛𝑎𝑚𝑒2, 𝐴𝑟𝑆𝑖𝑧𝑒);
Get()
Tutaj by wszystko działało poprawnie po wczytaniu jednego ciągu znaków należy powtórzyć
komendę, ale bez argumentów:
𝑐𝑖𝑛. 𝑔𝑒𝑡(𝑛𝑎𝑚𝑒1, 𝐴𝑟𝑆𝑖𝑧𝑒); (𝑤𝑐𝑧𝑦𝑡𝑢𝑗𝑒 𝑑𝑎𝑛𝑒 𝑏𝑒𝑧 𝑧𝑛𝑎𝑘𝑢 𝑛𝑜𝑤𝑒𝑔𝑜 𝑤𝑖𝑒𝑟𝑠𝑧𝑎)
𝑐𝑖𝑛. 𝑔𝑒𝑡( ); (𝑤𝑐𝑧𝑦𝑡𝑦𝑤𝑎𝑛𝑖𝑒 𝑧𝑛𝑎𝑘𝑢 𝑛𝑜𝑤𝑒𝑔𝑜 𝑤𝑖𝑒𝑟𝑠𝑧𝑎)
𝑐𝑖𝑛. 𝑔𝑒𝑡(𝑛𝑎𝑚𝑒2, 𝐴𝑟𝑆𝑖𝑧𝑒); (𝑤𝑐𝑧𝑦𝑡𝑢𝑗𝑒 𝑑𝑎𝑛𝑒 𝑏𝑒𝑧 𝑧𝑛𝑎𝑘𝑢 𝑛𝑜𝑤𝑒𝑔𝑜 𝑤𝑖𝑒𝑟𝑠𝑧𝑎)
Getline() jest prostsze w użyciu, ale get() ułatwia sprawdzenie wyników (str. 133)
Nie warto mieszać funkcje getline() lub get() z cin. Jest to niebezpieczne bo cin sam
wstawia znak ‘\0’, a tym samym pozostaje po nim znak nowego wiersza
wygenerowany przez wciśnięcie enter. Po czyms takim wykorzystanie getline() lub
get() spowoduje że wczyta on jedynie znak nowego wiersza wygenerowany tamtym
enterem. By tego uniknąć można albo użyć bezargumentowego get() (str 134-135).
𝑐𝑖𝑛 ≫ 𝑦𝑒𝑎𝑟;
𝑐𝑖𝑛. 𝑔𝑒𝑡( );
Lub
(𝑐𝑖𝑛 ≫ 𝑦𝑒𝑎𝑟). 𝑔𝑒𝑡( );
9
Klasa string
String jest analogiczny do łańcucha znakowego char ale:
Obiekt string deklaruje się jak zwykłą zmienną, a nie jak tablicę:
𝑠𝑡𝑟𝑖𝑛𝑔 𝑠𝑡𝑟1 = "𝑝𝑎𝑛𝑡𝑒𝑟𝑎";
Sam dobiera odpowiednią wielkość (bo działa jak tablica, ale nie musimy jej aż tak
kontrolować):
𝑠𝑡𝑟𝑖𝑛𝑔 𝑠𝑡𝑟1;
𝑐𝑖𝑛 ≫ 𝑠𝑡𝑟1;
10
𝑐ℎ𝑎𝑟 𝑐1[20]; (ś𝑚𝑖𝑒𝑐𝑖)
𝑠𝑡𝑟𝑖𝑛𝑔 𝑐2; (𝑖𝑛𝑖𝑐𝑗𝑎𝑙𝑖𝑧𝑜𝑤𝑎𝑛𝑒 𝑧𝑒𝑟𝑒𝑚)
Do pobierania całych linii znaków nie korzysta się dla stringów z cin.getline(c1,20), a
z:
𝑔𝑒𝑡𝑙𝑖𝑛𝑒(𝑐𝑖𝑛, 𝑐2); (𝑛𝑖𝑒 𝑑𝑎𝑗𝑒𝑚𝑦 𝑡𝑢 𝑤𝑖𝑒𝑙𝑘𝑜ś𝑐𝑖 𝑠𝑡𝑟𝑖𝑛𝑔𝑎, 𝑘𝑙𝑎𝑠𝑎 𝑠𝑡𝑟𝑖𝑛𝑔 𝑤𝑦𝑙𝑖𝑐𝑧𝑎 𝑠𝑜𝑏𝑖𝑒 𝑤𝑖𝑒𝑙𝑘𝑜ść 𝑠𝑡𝑟𝑖𝑛𝑔𝑎 𝑠𝑎𝑚𝑎)
(jest jeszcze coś o przedrostku R w temacie : inne odmiany literałów napisowych (char16_t,
char32_t) – str. 141.
Struktury
Tablice pozwalają na tworzenie wiele zmiennych w jednym obszarze, ale wszystkie
muszą być jednego typu.
Struktury pozwalają na stosowanie w jej obrębie wielu różnych typów danych.
Przydadzą się, np. kiedy chcemy stworzyć bazę na temat koszykarzy. Koszykarz
dany będzie miał dane zapisane w strukturze, takie jak nazwisko, pobory, wzrost,
waga itp. Z kolei chcąc utworzyć całą drużynę koszykarzy możemy utworzyć tablicę
struktur.
Definiowanie struktur:
Struktura to pojemnik do którego wrzucamy różne typy danych. Nazwa struktury to jak by
nowy typ danych. Od momentu jej utworzenia można tworzyć zmienne typu np. piłkarze
Struktury można tworzyć lokalnie i globalnie
Inicjalizacja struktur:
11
𝑖𝑛𝑓𝑙𝑎𝑡𝑎𝑏𝑙𝑒. 𝑝𝑟𝑖𝑐𝑒 = 2.5;
(str 144 – 145 – przykładowe użycie struktur oraz zasięgi globalne i lokalne)
Tablica struktur
𝑖𝑛𝑓𝑙𝑎𝑡𝑎𝑏𝑙𝑒 𝑔𝑖𝑓𝑡𝑠[100];
𝑐𝑖𝑛 ≫ 𝑔𝑖𝑓𝑡𝑠[0]. 𝑣𝑜𝑙𝑢𝑚𝑒;
𝑐𝑜𝑢𝑡 ≪ 𝑔𝑖𝑓𝑡𝑠[99]. 𝑝𝑟𝑖𝑐𝑒 ≪ 𝑒𝑛𝑑𝑙;
(Przykład str. 148)
Pola bitowe (do wymuszenia by poszczególne pola zajmowały określoną liczbę bitów)
Unie (postać danych, która pozwala zapisywać różne typy danych, ale zawsze tylko jeden
na raz). Stosuje sie tam gdzie chcemy zaoszczędzić na pamięci a używamy np danych
zmiennych typu int i double, ale tylko jednej na raz (str 149-150). Zajmując się jedną, tracimy
drugą:
12
Typ wyliczeniowy enum
𝑒𝑛𝑢𝑚 𝑠𝑝𝑒𝑐𝑡𝑟𝑢𝑚{𝑐𝑧𝑒𝑟𝑤𝑜𝑛𝑦, 𝑝𝑜𝑚𝑎𝑟𝑎ń𝑐𝑧𝑜𝑤𝑦, 𝑛𝑖𝑒𝑏𝑖𝑒𝑠𝑘𝑖};
Spectrum staje się typem wyliczeniowym
Kolejne nazwy są stałymi symbolicznymi, którym odpowiadają liczby całkowite od 0
do 2. Stałe te to enumeratory. Domyślnie pierwszy z nim ma numer 0, drugi 1 itp,
można to zmienić w sposób jawny przypisując im żądany numer.
𝑒𝑛𝑢𝑚 𝑏𝑖𝑡𝑦{𝑗𝑒𝑑𝑒𝑛 = 1, 𝑑𝑤𝑎 = 2, 𝑐𝑧𝑡𝑒𝑟𝑦 = 4};
Albo tylko niektóre:
𝑒𝑛𝑢𝑚 𝑤𝑖𝑒𝑙𝑘𝑖𝑠𝑘𝑜𝑘{𝑝𝑖𝑒𝑟𝑤𝑠𝑧𝑦, 𝑑𝑟𝑢𝑔𝑖 = 100, 𝑡𝑟𝑧𝑒𝑐𝑖};
Pierwszy ma wtedy wartość 0, drugi – 100, a trzeci kolejną czyli – 101.
Można też tworzyć enumeratory z tą samą wartością (przypisując im te same wartości)
Zmienna spectrum może przyjmować tylko 3 różne wartości, które zadeklarowaliśmy
tworząc ten typ wyliczeniowy (spectrum)
𝑠𝑝𝑒𝑐𝑡𝑟𝑢𝑚 𝑝𝑎𝑠𝑚𝑜;
𝑝𝑎𝑠𝑚𝑜 = 𝑛𝑖𝑒𝑏𝑖𝑒𝑠𝑘𝑖;
𝑝𝑎𝑠𝑚𝑜 = 2000; (ź𝑙𝑒!)
Typy wyliczeniowe obsługują jedynie operator przypisania, nie można np korzystać w
ich obrębie z działań arytmetycznych:
𝑝𝑎𝑠𝑚𝑜 = 𝑝𝑜𝑚𝑎𝑟𝑎ń𝑐𝑧𝑜𝑤𝑦; (𝑑𝑜𝑏𝑟𝑧𝑒)
+ + 𝑝𝑎𝑠𝑚𝑜; (ź𝑙𝑒!)
𝑝𝑎𝑠𝑚𝑜 = 𝑝𝑜𝑚𝑎𝑟𝑎ń𝑐𝑧𝑜𝑤𝑦 + 𝑐𝑧𝑒𝑟𝑤𝑜𝑛𝑦; (ź𝑙𝑒!)
Typy wyliczeniowe to typy całkowite, więc można je rzutować na int, ale nie w drugą
stronę (str 152):
𝑖𝑛𝑡 𝑘𝑜𝑙𝑜𝑟 = 𝑛𝑖𝑒𝑏𝑖𝑒𝑠𝑘𝑖; (𝑑𝑜𝑏𝑟𝑧𝑒 𝑏𝑜 𝑛𝑖𝑒𝑏𝑖𝑒𝑠𝑘𝑖 𝑚𝑎 𝑝𝑟𝑧𝑦𝑝𝑖𝑠𝑎𝑛ą 𝑤𝑎𝑟𝑡𝑜ść 2,
𝑘𝑡ó𝑟ą 𝑚𝑜ż𝑛𝑎 𝑝𝑟𝑧𝑦𝑝𝑖𝑠𝑎ć 𝑑𝑜 𝑧𝑚𝑖𝑒𝑛𝑛𝑒𝑗 𝑡𝑦𝑝𝑢 𝑖𝑛𝑡
− 𝑛𝑖𝑒 𝑗𝑒𝑠𝑡 𝑡𝑜 𝑜𝑝𝑒𝑟𝑎𝑐𝑗𝑎 𝑛𝑎 𝑡𝑦𝑝𝑖𝑒 𝑤𝑦𝑙𝑖𝑐𝑧𝑒𝑛𝑖𝑜𝑤𝑦𝑚,
𝑛𝑖𝑒𝑏𝑖𝑒𝑠𝑘𝑖 𝑗𝑒𝑠𝑡 𝑡𝑢𝑡𝑎𝑗 𝑘𝑜𝑛𝑤𝑒𝑟𝑡𝑜𝑤𝑎𝑛𝑦 𝑛𝑎 𝑙𝑖𝑐𝑧𝑏ę 2 𝑡𝑦𝑝𝑢 𝑖𝑛𝑡.
𝑀𝑜ż𝑛𝑎 𝑝𝑟𝑧𝑦𝑝𝑖𝑠𝑦𝑤𝑎ć 𝑗𝑒 𝑗𝑎𝑘𝑜 𝑙𝑖𝑐𝑧𝑏𝑦 𝑑𝑜 𝑧𝑚𝑖𝑒𝑛𝑛𝑦𝑐ℎ 𝑡𝑦𝑝𝑢 𝑖𝑛𝑡)
𝑝𝑎𝑠𝑚𝑜 = 3; (ź𝑙𝑒 𝑏𝑜 𝑝𝑎𝑠𝑚𝑜 𝑡𝑜 𝑡𝑦𝑝 𝑠𝑝𝑒𝑐𝑡𝑟𝑢𝑚, 𝑤𝑖ę𝑐 𝑚𝑜ż𝑛𝑎 𝑤𝑝𝑖𝑠𝑎ć
𝑡𝑦𝑙𝑘𝑜 𝑠ł𝑜𝑤𝑛𝑒 𝑤𝑎𝑟𝑡𝑜ś𝑐𝑖 𝑧 𝑘𝑙𝑎𝑚𝑒𝑟 𝑛𝑝. 𝑐𝑧𝑒𝑟𝑤𝑜𝑛𝑦)
Można też pominąć nazwę typu wyliczeniowego:
𝑒𝑛𝑢𝑚 {𝑐𝑧𝑒𝑟𝑤𝑜𝑛𝑦, 𝑝𝑜𝑚𝑎𝑟𝑎ń𝑐𝑧𝑜𝑤𝑦, 𝑛𝑖𝑒𝑏𝑖𝑒𝑠𝑘𝑖};
Nie można jednak wtedy się już do nich odnosić, a jedynie używać ich jako zdefiniowanych
stałych.
Zakresy wartości w typach wyliczeniowych – str 153 – można dodawać do listy wyliczeniowej
nowe zmienne pod warunkiem ze mieszczą się w zakresie wartości, który można obliczyć
13
Wskaźniki
Deklaracja i inicjalizacja:
𝑖𝑛𝑡 𝑥 = 5;
𝑖𝑛𝑡 ∗ 𝑤𝑠𝑘;
𝑤𝑠𝑘 = &𝑥;
Lub
𝑖𝑛𝑡 ∗ 𝑝𝑡𝑟 − 𝑝𝑡𝑟 𝑡𝑜 𝑤𝑠𝑘𝑎ź𝑛𝑖𝑘 𝑛𝑎 𝑖𝑛𝑡 (𝑐𝑧𝑦𝑙𝑖 𝑝𝑟𝑧𝑒𝑐ℎ𝑜𝑤𝑢𝑗𝑒 𝑎𝑑𝑟𝑒𝑠 𝑜𝑏𝑖𝑒𝑘𝑡𝑢 𝑡𝑦𝑝𝑢 𝑖𝑛𝑡)
𝑖𝑛𝑡 ∗ 𝑝𝑡𝑟 − ∗ 𝑝𝑡𝑟 𝑗𝑒𝑠𝑡 𝑜𝑏𝑖𝑒𝑘𝑡𝑒𝑚 𝑡𝑦𝑝𝑢 𝑖𝑛𝑡 (𝑏𝑜 𝑝𝑡𝑟 𝑤𝑠𝑘𝑎𝑧𝑢𝑗𝑒 𝑛𝑎 𝑖𝑛𝑡)
𝑖𝑛𝑡 ∗ 𝑝𝑡;
14
𝑝𝑡 = (𝑖𝑛𝑡 ∗)0𝑥𝐵800000;
Operator new
Tworzymy nienazwane miejsce na dane jakiegoś typu i sięgamy do tego miejsca wskaźnikiem.
Operator new jest informowany przez nas na jakie dane potrzebna jest nam pamięć, new znajduje
odpowiedni blok pamięci i zwraca jego adres, który jest przypisywany wskaźnikowi.
Operator delete
Służy do zwalniania pamięci wcześniej zarezerwowanej operatorem new. Nie jest usuwany sam
wskaźnik, tylko pamięć wskaźnika (staje się wskaźnikiem pustym, przestaje pokazywać adres jakiejś
zmiennej). Nie wolno zwalniać pamięci zwolnionego już wskaźnika. Może to spowodować tragedię.
Dodatkowo zwalniać można pamięć zarezerwowaną jedynie operatorem new.
Delete należy używać do pamięci alokowanej za pomocą new, nie oznacza to, że trzeba używać tego
samego wskaźnika, który został zwrócony przez new, ale trzeba użyć tego samego adresu:
15
Nie używamy delete do ponownego zwolnienia bloku pamięci, który już raz został zwolniony
Jeśli pamięć alokowaliśmy za pomocą new[ ] zwalniamy ją przez delete [ ]
Jeśli pamięć alokowaliśmy za pomocą new (bez nawiasów kwadratowych), zwalniamy ją za
pomocą delete (też bez nawiasów)
Można bezpiecznie stosować operator delete do wskaźnika pustego (nic nie zostanie
zrobione)
Notacji tablicowej
𝑝𝑠𝑜𝑚𝑒[3] = 10;
Notacji wskaźnikowej
𝑝𝑠𝑜𝑚𝑒+= 3;
∗ 𝑝𝑠𝑜𝑚𝑒 = 10;
Przesuwanie wskaźnika i odczytywanie go w notacji tablicowej i wskaźnikowej (listing 4.18 str
164 + listing 4.19 str 165)
C++ interpretuje nazwę tablicy jako adres jej zerowego elementu:
𝑑𝑜𝑢𝑏𝑙𝑒 𝑤𝑎𝑔𝑒𝑠[3] { };
𝑑𝑜𝑢𝑏𝑙𝑒 ∗ 𝑝𝑤 = 𝑤𝑎𝑔𝑒𝑠;
Lub inaczej:
𝑑𝑜𝑢𝑏𝑙𝑒 ∗ 𝑝𝑤 = &𝑤𝑎𝑔𝑒𝑠[0];
Oba zapisy są takie same. Czyli nazwa tablicy to jednoczesnie adres jej zerowego elementu.
16
Podsumowanie wskaźników (str 168 – 170 + warto doczytać stronę od str 165 – lub calość xD,
szczególnie różnice między zapisem wskaźnikowym a tablicowym)
Wskaźniki i łańcuchy
W przypadku obiektu cout jak i większości innych wyrażeń c++, łańcuchy char zapisywane jako
tablice, napisy w cudzysłowach, oraz wskaźniki na znaki char, są obsługiwane tak samo, czyli są
traktowane jako adresy pierwszego znaku łańcucha, po czym wyświetlane są kolejne znaki
do momentu napotkania znaku NUL (‘\0’).
𝑎 = 20: 𝑏 = 20
𝑎 + + = 20: + +𝑏 = 21
𝑎 = 21: 𝑏 = 21
Operatory inkrementacji i dekrementacji we wskaźnikach
+ +∗ 𝑝𝑡
(∗ 𝑝𝑡) + +;
Porównywanie to odbywa się normalnie porównując dwa wyrazy. Klasa string pozwala na użycie
normalnych operatorów relacyjnych ( <, <=, !=, ==, itp.) jeśli przynajmniej jeden z operandów jest
obiektem string, drugi może być stringiem, lub łańcuchem zgodnym z C. (więcej – str. 213) (str 212 –
porównywanie staromodne – z C za pomocą strcmp() )
17
W przeciwieństwie do łańcuchów w formacie C, obiekty klasy string nie używają znaku NUL
do oznaczenia końca napisu, w rozdziale 16 omówione zostaną techniki jakie stosuje się do
wskazania ostatniego znaku obiektu string (oraz jaki to znak).
Opóźnienie programowe
Można to robić na debila iterując zmienną ileś tysięcy razy, ale po pierwsze jest to opóźnienie „na
oko”, a po drugie, jest zależne od prędkości systemu. Dlatego wprowadzono mądrzejszy sposób.
Funkcja clock(), która zwraca czas systemowy jaki upłynął od chwili uruchomienia programu. Jednak
w różnych systemach może on nie koniecznie zwracac sekundy, oraz wartość może być różnych
typów (unsigned long, long itp). Jest jeszcze lepszy sposób:
Stworzono stałą symboliczną CLOCKS_PER_SEC, która wskazuje ile jednostek czasu systemowego
składa się na sekundę. Możemy też pomnożyć tą stałą przez żądaną ilość sekund by uzyskać czas w
jednostkach systemowych.
W tej samej biblitece (ctime, lub time.h) znajduje się też specjalny typ danych – clock_t, który jest
aliasem typu zwracanego z funkcji clock(), tzn. Że kompilator sam przyjmie odpowiedni typ danych w
zależności od tego jaki typ zwróciła by na danym systemie funkcji clock().
Często mnoży się CLOCKS_PER _SEC z sekundami żądanymi by mieć jednostki czasu systemowego bo
je też zwraca funkcja clock(). Tak mamy:
Aliasy typów
Język c++ ma dwa sposoby ustalania nowej nazwy jako alternatywnej nazwy typu:
#define BAJT char -> zastąpienie wystąpienia BAJT typem char, czyli zamiast char możemy
używać BAJT (kompilator potraktuje to tak samo)
Typedef char bajt; -> zastąpienie wystąpienia bajt typem char, czyli zamiast char możemy
używać bajt (kompilator potraktuje to tak samo).
18
𝑐𝑜𝑢𝑡 ≪ 𝑥 ≪ 𝑒𝑛𝑑𝑙;
Spowoduje, że będą wyświetlane po kolei (od pierwszego) wszystkie elementy tablicy prices
𝑥 = 𝑥 ∗ 0.8;
Symbol & identyfikuje x jako zmienną referencyjną (o tym w rozdziale 8).
𝑓𝑜𝑟(𝑖𝑛𝑡 𝑥 ∶ {3,5,7,2})
𝑐𝑜𝑢𝑡 ≪′\ 𝑛′ ;
Generalnie pętle zakresowe przeznaczone są raczej do przeglądania i przetwarzania zawartości
rozmaitych klas kontenerowych, które będą omawiane w rozdziale 16.
Przeciążenie funkcji pozwala tworzyć różne funkcje mające taką samą nazwę, byle miały inne listy
parametrów. Stąd możliwe są dwa warianty cin.get:
𝑐𝑖𝑛. 𝑔𝑒𝑡(𝑐ℎ𝑎𝑟)
, oraz
𝑐𝑖𝑛. 𝑔𝑒𝑡( )
(więcej o tym w rozdziale 8)
W EOF istotna jest kombinacja klawiszy różna dla różnych systemów by zakończyć np pobieranie
znaków (listing. 5.18). Po EOF by korzystać z cin istotne jest zerowanie flagi (cin.clear() ).
Nawiązując do powyższego tematu „Wczytywanie znak po znaku...” jest jeszcze opcja dostępna z C,
opisana na stronach 227-230)
19
Pętle zagnieżdżone i tablice dwuwymiarowe
Maxtemps jest tablicą czteroelementową, a każdy z tych elementów jest pięcioelementową tablicą
liczb int
20
Rozdział 6. Instrukcje warunkowe i operatory
logiczne
𝑖𝑓(3 == 𝑛𝑢𝑚𝑏𝑒𝑟)
Zamiast:
𝑖𝑓(𝑛𝑢𝑚𝑏𝑒𝑟 == 3)
Ponieważ:
𝑖𝑓(3 = 𝑛𝑢𝑚𝑏𝑒𝑟)
𝑖𝑓(𝑛𝑢𝑚𝑏𝑒𝑟 = 3)
A tu po prostu nastąpi przypisanie (kompilator nie wykryje pomyłki.
Biblioteka cctype
Zawiera szereg gotowych funkcji, które mogą być przydatne do pracy ze zmiennymi znakowymi:
Operator ?:
Alternatywa dla if else:
Przykład:
5 > 3? 10: 12
Jest to prawda, więc wartością całego wyrażenia jest 10.
21
While(cin<<fish[i])
Jest tu wywołanie metody cin, który jest też częścią warunku, jest więc cin konwertowany na wartość
typu bool. Jest true jeśli wprowadzanie danych się powiedzie, false, jeśli się nie powiedzie. Czyli
błędne wprowadzenie danych powoduje przerwanie pętli while.
Continue i break
𝑜𝑢𝑡𝐹𝑖𝑙𝑒. 𝑜𝑝𝑒𝑛(fish.txt);
Lub
𝑐ℎ𝑎𝑟 𝑓𝑖𝑙𝑒𝑛𝑎𝑚𝑒[40];
𝑐𝑖𝑛 ≫ 𝑓𝑖𝑙𝑒𝑛𝑎𝑚𝑒;
𝑜𝑢𝑡𝐹𝑖𝑙𝑒. 𝑜𝑝𝑒𝑛(𝑓𝑖𝑙𝑒𝑛𝑎𝑚𝑒);
Dalej już zapisujemy do pliku:
𝑑𝑜𝑢𝑏𝑙𝑒 𝑤𝑡 = 2.15;
𝑜𝑢𝑡𝐹𝑖𝑙𝑒 ≪ 𝑤𝑡; (𝑧𝑎𝑝𝑖𝑠 𝑙𝑖𝑐𝑧𝑏𝑦 𝑑𝑜 𝑝𝑙𝑖𝑘𝑢 𝑓𝑖𝑠ℎ. 𝑡𝑥𝑡)
Na tym etapie ze zmiennej outFile korzystamy jak by to było nasze cout. Możemy korzystać wzgledem
niego z takich samych metod jakie były dostępne dla cout np. setf(), endl, itp. Czyli po utworzeniu
obiektu klasy ofstream używa się go jak obiektu cout.
Metoda open() tworzy nowy plik w odpowiednim miejscu (wskazanym lub tam gdzie projekt jeśli nic
konkretnego nie wskazuje ścieżka pliku) jeśli go już tam nie ma. Jeśli jednak jest już to zostaje on
nadpisany (jest usuwane wszystko co w nim było a plik jest tworzony jak by na nowo). Można tego
22
uniknąc, dzięki czemu o ile dany plik już jest i korzystamy z open() dane w tym pliku są uaktualniane o
nowe, wprowadzane dane (więcej w rozdziale 17).
𝑜𝑢𝑡𝐹𝑖𝑙𝑒. 𝑐𝑙𝑜𝑠𝑒( )
lub
𝑐ℎ𝑎𝑟 𝑓𝑖𝑙𝑒𝑛𝑎𝑚𝑒[40];
𝑐𝑖𝑛 ≫ 𝑓𝑖𝑙𝑒𝑛𝑎𝑚𝑒;
𝑖𝑛𝐹𝑖𝑙𝑒. 𝑜𝑝𝑒𝑛(𝑓𝑖𝑙𝑒𝑛𝑎𝑚𝑒);
Następnie odczytujemy dane:
𝑑𝑜𝑢𝑏𝑙𝑒 𝑤𝑡;
𝑖𝑛𝐹𝑖𝑙𝑒 ≫ 𝑤𝑡;
Lub:
23
Rozdział 7. Funkcje - podstawy
Zmienne zadeklarowane w środku funkcji są dla niej prywatne. Funkcja alokuje dla nich pamięć, a po
skończeniu działania funkcji komputer zwalnia pamięć przypisaną tym zmiennym. Są to zmienne
lokalne bo są dostępne tylko w zadeklarowanym obszarze (tutaj – funkcji), oraz są też zmiennymi
automatycznymi ponieważ są automatycznie alokowane i dealokowane w trakcie wykonywania
programu.
24
Nie zawsze cin.get() jest lepsze od cin, bo np ENTER tez generuje znak końca linii, który cin
potrafi pominąć, a cin.get() nie. (więcej strona 288-289)
𝑠𝑢𝑚_𝑎𝑟𝑟(𝑐𝑜𝑜𝑘𝑖𝑒𝑠, 𝐴𝑟𝑆𝑖𝑧𝑒);
Powoduje przekazanie tej funkcji adresu pierwszego elementu tablicy cookies oraz liczby elementów
tablicy. Funkcja przepisuje adres tablicy cookies do wskaźnika arr, a licznik ArSize do zmiennej n.
Funkcja używa więc oryginalnej tablicy. Nie narusza to obowiązującej zasady przekazywania przez
wartość, bo przekazana wartość jest kopiowana do zmiennej lokalnej, tyle że tą wartością nie jest
zawartość tablicy, a jeden adres.
25
Gdy chcemy by funkcja była odpowiedzialna jedynie za wyświetlanie elementów tablicy to
należy użyć w argumencie tej funkcji przedrostka const przy nazwie tablicy:
Deklaracja tablicy:
∗ 𝑝𝑡+= 1; (ź𝑙𝑒)
Ciekawostka : z tej deklaracji nie wynika wcale, że wartość wskazywana przez pt jest naprawde stała.
Jest ona stała tylko patrząc na nią przez pryzmat zmiennej wskaźnikowej pt. W tym przykładzie age
wskazywane przez pt nie ma przedrostka const dlatego można zmienić jej wartość. W ten sposób
wskaźnik będzie wskazywał na inną liczbę. Czyli : można zmieniać bezpośrednio wartość zmiennej age,
ale nie można zmienić jej pośrednio z użyciem wskaźnika pt.
(można przypisać adres zmiennej typu const wskaźnikowi const, ale nie można przypisać adresu
zmiennej const zwykłemu wskaźnikowi.) Jest jeszcze zagmatwanie ze wskaźnikami na wskaźniki itp
(więcej o tym na str 303-304)
𝑖𝑛𝑡 𝑠𝑙𝑜𝑡ℎ = 3;
𝑖𝑛𝑡 ∗ 𝑐𝑜𝑛𝑠𝑡 𝑓𝑖𝑛𝑔𝑒𝑟 = &𝑠𝑙𝑜𝑡ℎ
Taki zapis powoduje, że finger może wskazywać jedynie sloth, ale za pośrednictwem finger można
zmieniać wartość zmiennej sloth.
26
Różnice pokazuje rysunek :
𝑖𝑛𝑡 𝑎[100][4];
𝑖𝑛𝑡 𝑏[6][4];
𝑖𝑛𝑡 𝑡𝑜𝑡𝑎𝑙1 = 𝑠𝑢𝑚(𝑎, 100);
𝑖𝑛𝑡 𝑡𝑜𝑡𝑎𝑙2 = 𝑠𝑢𝑚(𝑏, 25);
(więcej str 306-307)
27
Struktury i funkcje
Struktury można przekazywać do funkcji przez wartość (jak normalne zmienne) (więcej str 311-
315)
Struktury można przekazywać do funkcji przez adres (wskaźnik) (więcej str 317-318)
Struktura to nie tablica, więc przekazując jej adres należy pamiętać o znaku ampersand - &
Np 𝑠ℎ𝑜𝑤_𝑝𝑜𝑙𝑎𝑟(&𝑝𝑝𝑙𝑎𝑐𝑒);
Struktury można przekazywać do funkcji przez referencje (o tym dalej w rozdziale kolejnym)
Rekurencja
Rekurencja to wywoływanie w funkcji samej siebie (czyli funkcja wywołuje w swoim ciele tą samą
funkcję) więcej o tym na str 322-325 oraz u M. Zelenta)
Nie mylić z:
28
Deklaracja wskaźnika na funkcje
𝑑𝑜𝑢𝑏𝑙𝑒(∗ 𝑝𝑓)(𝑖𝑛𝑡) (𝑝𝑓 𝑤𝑠𝑘𝑎𝑧𝑢𝑗𝑒 𝑓𝑢𝑛𝑘𝑐𝑗ę 𝑧𝑤𝑟𝑎𝑐𝑎𝑗ą𝑐ą 𝑤𝑎𝑟𝑡𝑜ść 𝑑𝑜𝑢𝑏𝑙𝑒)
Nie mylić z:
𝑑𝑜𝑢𝑏𝑙𝑒 𝑝𝑎𝑚(𝑖𝑛𝑡);
𝑑𝑜𝑢𝑏𝑙𝑒(∗ 𝑝𝑓)(𝑖𝑛𝑡)
𝑝𝑓 = 𝑝𝑎𝑚;
𝑝𝑓(5);
Tak jak by to była nazwa funkcji. Nie jest to zbyt logiczne, ale tak przyjęto jak by przez to, że nazwa
funkcji to też jej wskaźnik. Przez to wskaźnik powinien zachowywać się jak ta funkcja. I tyle, czyli z
dupy.
Auto
To wszystko można mocno zagnieżdżać i mieć skomplikowane zapisy takie jak:
𝑎𝑢𝑡𝑜 𝑝𝑐 = &𝑝𝑎;
Więcej o tym na str 329-333
𝑎𝑢𝑡𝑜 𝑝𝑏 = 𝑝𝑎;
Typedef
(str 333) Pomaga upraszczać skomplikowane deklaracje (coś poza auto). Używa się tego tak, że
piszemy np.:
29
𝑝𝑓𝑢𝑛 𝑝1 = 𝑓1;
Czyli zmienna p1 jest typu pfun. Możemy w ten sposób szybciej tworzyć wiele zmiennych tego
samego typu co pfun.
Inline
Działa tak, że w miejsce wywołania funkcji typu inline, wrzuca całe ciało tej funkcji.
Inline jest lepsze od makra znanego z C, bo nie działa ona przez przekazywanie parametrów a przez
podstawianie X, co skutkuje np.:
#𝑑𝑒𝑓𝑖𝑛𝑒 𝑆𝑄𝑈𝐴𝑅𝐸(𝑋) 𝑋 ∗ 𝑋
𝑑 = 𝑆𝑄𝑈𝐴𝑅𝐸(𝑐 + +);
Co zostanie zastąpione : 𝑑 = 𝑐 + + ∗ 𝑐 + +;
Czyli będzie podwójna inkrementacja zmiennej c, w inline taka sytuacja nie miała by miejsca.
30
Rozdział 8. Funkcje – zagadnienia
zaawansowane
Zmienne referencyjne
Referencja działa jak alias, czyli inna nazwa zmiennej zdefiniowanej już wcześniej. Najważniejszym
zastosowaniem zmiennych referencyjnych jest używanie ich jako parametrów funkcji. Jeśli parametr
jest referencją, funkcja działa na danych oryginalnych, a nie na kopii zmiennej. Jest to wygodna
alternatywa dla wskaźników przy przetwarzaniu dużych struktur za pomocą funkcji, są ważne przy
klasach.
Znak & odpowiada za pozyskanie adresu zmiennej. W c++ ten symbol ma jednak jeszcze jedno,
dodatkowe zastosowanie. Zaprzęga się go do deklarowania referencji. Aby np. rodents było
alternatywną nazwą zmiennej rats można zapisać:
𝑖𝑛𝑡 𝑟𝑎𝑡𝑠;
𝑖𝑛𝑡 & 𝑟𝑜𝑑𝑒𝑛𝑡𝑠 = 𝑟𝑎𝑡𝑠; (𝑟𝑜𝑑𝑒𝑛𝑡𝑠 𝑠𝑡𝑎𝑗𝑒 𝑠𝑖ę 𝑎𝑙𝑖𝑎𝑠𝑒𝑚 𝑑𝑙𝑎 𝑟𝑎𝑡𝑠)
W tym przypadku znak ten jest częścią identyfikatora typu zmiennej, tak jak char* był wskaźnikiem
typu char, tak int & sugeruje referencję do wartości int. Zmienne rats i rodents to dwie nazwy dla tej
samej wartości, która jest zajmowana przez ten sam adres w pamięci.
Przykład:
𝑝𝑟 = &𝑏𝑢𝑛𝑛𝑖𝑒𝑠;
Ale rodents nie można przestawić:
𝑟𝑜𝑑𝑒𝑛𝑡𝑠 = 𝑏𝑢𝑛𝑛𝑖𝑒𝑠;
Spowoduje tyle, że do rodents zostanie przypisana wartość zmiennej bunnies, czyli wartość zmiennej
rats będzie miało tą samą wartość co zmienna bunnies. Czyli zostanie przypisanie nowej wartości do
31
zmiennej o dwóch nazwach (rats i rodents), a nie zmiana referencji dla rodents (rodents dalej będzie
alternatywną nazwą tylko i wyłącznie dla rats). Analogicznie:
Zestawimy sobie przekazywanie parametrów funkcji przez wartość, wskaźnik i referencję. Przykładowe
wywołania funkcji:
32
Tutaj różnica już jest widoczna. Dodatkowo parametry funkcji będące referencjami należy traktować
jako zainicjalizowane parametrami przekazanymi w wywołaniu funkcji.
𝑠𝑤𝑎𝑝𝑟(𝑤𝑎𝑙𝑙𝑒𝑡1, 𝑤𝑎𝑙𝑙𝑒𝑡2);
Powoduje zainicjalizowanie parametrów argumentami wywołania: a jest inicjalizowane wallet1, b –
wallet2.
Właściwości referencji
Więcej o tym wszystkim (o właściwościach referencji) na stronach (348-352)
𝑐𝑜𝑢𝑡 ≪ 𝑐𝑢𝑏𝑒(𝑥);
𝑐𝑜𝑢𝑡 ≪ 𝑟𝑒𝑓𝑐𝑢𝑏𝑒(𝑥);
Da wynik:
27 = 𝑠𝑧𝑒ś𝑐𝑖𝑎𝑛 3
27 = 𝑠𝑧𝑒ś𝑐𝑖𝑎𝑛 27
Funkcja cube() przekazuje argument przez wartość, więc argument x nie zmienia wartości. Jednak w
przypadku przekazywania przez referencje argument ra jest alternatywną nazwą dla argumentu x, a co
za tym idzie – zmienna x zmieni wartość tak samo jak wartość została zmieniona dla argumentu ra.
Funkcje przkazujące parametry przez wartość jak cube() mogą używać wielu rodzajów
parametrów wywołania.
Co nie działa w taki sposób dla przekazywania argumentów przez referencje. Argument w tym
przypadku powinien być zmienną, a nie wyrażeniem. Większość kompilatorów zgłosi błąd podczas
takiej próby – jednak nie wszystkie. Niektóre z nich spowodują powstanie zmiennej tymczasowej
(wyrażenie użyte w funkcji jako argument będzie przekonwertowane i przypisane do zmiennej
tymczasowo utworzonej przez kompilator).
33
Zmienne tymczasowe, parametry referencyjne i const
Więcej o tym wszystkim (o właściwościach referencji) na stronach (348-352)
Język c++ może wygenerować zmienną tymczasową, jeśli parametr wywołania nie pasuje do parametru
referencyjnego. Obecnie dopuszcza się to tylko w przypadku parametrów referencyjnych z
modyfikatorem const, ale i to nie zawsze. C++ generuje zmienne tymczasowej w poniższych sytuacjach:
l-wartość – obiekty danych, do których można się odwołać. Są to np. zmienna, element tablicy, pole
struktury, referencja, czy wskaźnik, do którego zastosowano dereferencję. Nie są l-wartościami np.
literały (poza ciągami w cudzysłowiach), ani złożone wyrażenia. Są to jest to zarówno zwyczajne
zmienne modyfikowalne jak i zmienne deklarowane ze słowem const, ponieważ do obu można
odwołać się przez adres.
Przykładowa funkcja
Parametry side, lens[2], rd i *pd to obiekty typu double mające nazwy więc można utworzyć do nich
referencję i zbędne są jakiekolwiek zmienne tymczasowe (element tablicy zachowuje się jak zmienna
odpowiedniego typu – dla przypomnienia). Jednak edge jest wprawdzie zmienną, ale niewłaściwego
typu. Referencja double nie może dotyczyć zmiennej long. Parametry 7.0 i side+10.0 są z kolei
właściwego typu ale nie są nazwanymi obiektami danych (referencja może być utworzona jedynie dla
przypisanych zmiennych, a nie dla wyrażeń lub zwykłych wartości z nieprzypisaną nazwą zmiennej). W
każdym z tych przypadków kompilator wygeneruje tymczasową zmienną anonimową i uczyni ra
referencją do niej. Te zmienne tymczasowe istnieją tak długo jak długo działa funkcja ale później
kompilator może je usunąć z pamięci.
Jeśli zadaniem funkcji mającej parametry referencyjne jest zmiana przeykazywanych wartości,
tworzenie zmiennych tymczasowych to uniemożliwia. Rozwiązaniem jest uniemożliwienie tworzenia
34
zmiennych tymczasowych w takich sytuacjach i to właśnie teraz jest zapisane w standardzie c++. Jednak
niektóre kompilatory dalej nie reagują tak inteligentnie. Funkcja refcube() ma za zadanie użycie
przekazanych wartości, a nie ich modyfikowanie. Wobec tego wykorzystanie zmiennych tymczasowych
w niczym nie przeszkadza, a funkcja może obsłużyć szerszy wachlarz typów parametrów. Jeśli zatem z
deklaracji wynika, że referencja jest stała (const) c++ generuje w razie potrzeby zmienne tymczasowe.
Funkcje z parametrami const, którym przekazano parametry wywołania innych typów zachowują się
podobnie jak w przypadku przekazywania przez wartość, co oznacza niemożność zmiany oryginalnych
danych i użycie tymczasowej zmiennej na wartość.
UWAGA
Jeśli parametr wywołania funkcji nie jest l-wartością lub nie pasuje co do typu do odpowiedniego
stałego parametru referencyjnego c++ tworzy zmienną anonimową właściwego typu i przypisuje jej
wartość przekazanego parametru. Od tej chwili parametr odnosi się do takiej zmiennej anonimowej.
Kiedy to możliwe należy używać const w definicjach funkcji z referencją. Dlatego, że:
Parametry referencyjne należy deklarować jako stałe zawsze gdy jest to możliwe
W c++ 11 wprowadzono drugi rodzaj referencji do r-wartości, która może odnosić się do r-
wartości. Deklaruje się ją za pomocą symbolu &&
Referencja w strukturach
Referencje świetnie współpracują ze strukturami i klasami (str 352 – 358). Referencji do struktur
używa się w kontekście parametrów funkcji tak samo jak referencji do zmiennych typów
podstawowych – wystarczy w deklaracji parametru odnoszącego się do struktury dodać operator
referencji &.
𝑓𝑟𝑒𝑒 𝑡ℎ𝑟𝑜𝑤𝑠 & 𝑎𝑐𝑐𝑢𝑚𝑢𝑙𝑎𝑡𝑒 (𝑓𝑟𝑒𝑒_𝑡ℎ𝑟𝑜𝑤𝑠 & 𝑡𝑎𝑟𝑔𝑒𝑡, 𝑐𝑜𝑛𝑠𝑡 𝑓𝑟𝑒𝑒_𝑡ℎ𝑟𝑜𝑤𝑠 & 𝑠𝑜𝑢𝑟𝑐𝑒);
Czyta się to tak samo jak we wskaźnikach – mamy funkcję accumulate, wywoływaną z argumentami,
które są referencjami do struktur typu free_throws o nazwie target, oraz source, a zwracającą
strukturę typu free_throws.
35
Gdyby typ wartości zwracanej był deklarowany jako free_throws, a nie free_throws &, ta
sama instrukcja return zwróciła by kopię obiektu target (przesyłanie przez wartość). Ale skoro
wartość zwracana jest referencją, to znaczy, że wartość zwrócona z powyższego wywołania
to obiekt, który jest tożsamy z obiektem przekazanym do wywołania accumulate().
Gdyby accumulate() zwracała strukturę zamiast referencji do struktury, oznaczałoby to
skopiowanie całej struktury do tymczasowej lokalizacji, a stamtąd do odpowiedniej zmiennej.
Ale przy zwracaniu referencji mamy bezpośrednie kopiowanie struktury do odpowiedniej
zmiennej, co jest operacją dużo efektywniejszą. Pomijana jest jakaś tymczasowa lokalizacja.
Funkcja zwracająca referencję jest tak naprawdę aliasem zmiennej, której referencja dotyczy
Zwracając referencje należy unikać zwracania referencji do obszaru pamięci, który zostanie zwolniony
w chwili zakończenia wykonywania funkcji. (str 356-357 oraz trzecia wersja funkcji – version3 - na str
361). Pojawi się przez to niefortunny efekt zwracania referencji do zmiennej tymczasowej, która
przestanie istnieć w chwili zakończenia wykonywania funkcji. W momencie przypisywania lokalna
zmienna już będzie usunięta.
Można tego uniknąć przez zwracanie referencji przekazanej wcześniej funkcji jako parametr.
Parametr referencyjny odnosi się do danych używanych przez funkcję wywołującą wobec
czego zwracana referencja odniesie się do tych samych danych. Tak działa funkcja
accumulate() z listingu 8.6.
Druga metoda to alokacja pamięci operatorem new. Pokazano jak to zrobić w przykładzie na
str 357.
Załóżmy, że zamierzamy użyć referencji jako wartości zwracanej, ale chcielibyśmy zapobiec
niepożądanym przypadkom użycia, takim jak przypisanie do wartości zwracanej z funkcji
accumulate(). Aby to zrobić, wystarczy zadeklarować typ wartości zwracanej jako referencję
niemodyfikowalną (ze słowem const).
𝑐𝑜𝑛𝑠𝑡 𝑓𝑟𝑒𝑒_𝑡ℎ𝑟𝑜𝑤𝑠 & 𝑎𝑐𝑐𝑢𝑚𝑢𝑙𝑎𝑡𝑒 (𝑓𝑟𝑒𝑒𝑡ℎ𝑟𝑜𝑤𝑠 & 𝑡𝑎𝑟𝑔𝑒𝑡, 𝑐𝑜𝑛𝑠𝑡 𝑓𝑟𝑒𝑒𝑡ℎ𝑟𝑜𝑤𝑠 & 𝑠𝑜𝑢𝑟𝑐𝑒);
Teraz przypisanie :
𝑎𝑐𝑢𝑚𝑢𝑙𝑎𝑡𝑒(𝑑𝑢𝑝, 𝑓𝑖𝑣𝑒) = 𝑓𝑜𝑢𝑟;
Okaże się niemożliwe do zrealizowania (niedozwolone przez kompilator) ponieważ to co zwraca
funkcja accumulate jest teraz stałe – const – a takiego obiektu modyfikować nie można.
Funkcja z parametrem typu ostream &, może odebrać obiekt typu ostream (np cout) lub
obiekt typu ofstream (zadeklarowany przez nas) (więcej str 361-364)
36
Jeśli obiekt jest egzemplarzem klasy, nalezy użyć referencji const. Semantyka klas często
wymaga użycia referencji – to właśnie było głównym powodem dodania referencji do c++,
wobec tego standardowym sposobem przekazywania egzemplarzy klas jest użycie referencji
Parametry domyślne
To takie, które można pominąć w wywołaniu funkcji, ponieważ w jej prototypie mają już ustawioną
jakąś wartość domyślną. Gdy w wywołaniu wystąpi ten parametr, to nadpisze on jego wartość
domyślną. Jeśli pominiemy go w wywołaniu, ustawiony zostanie na wartość domyślną.
𝑙𝑒𝑓𝑡(„𝑡𝑒𝑜𝑟𝑖𝑎”, 3);
Spowoduje, że wyświetlone zostanie słowo:
𝑡𝑒𝑜
Jednak gdy wywołanie będzie miało taką postać:
𝑙𝑒𝑓𝑡("𝑡𝑒𝑜𝑟𝑖𝑎");
To wynikiem działania funkcji będzie słowo:
𝑡
Bo liczba n domyślnie ma wartość 1, więc gdy ją pominiemy w wywołaniu nie zostanie ona nadpisana,
a przyjmie właśnie tą domyślną wartość.
Gdy używamy funkcji z listą parametrów musimy wartości domyślne dodawać od strony
prawej do lewej, nie można użyć niektórych parametrów domyślnych, jeśli za nimi są
parametry bez wartości domyślnych
37
𝑏𝑒𝑒𝑝𝑠 = ℎ𝑎𝑟𝑝𝑜(8,7,6); (𝑛𝑖𝑒 𝑏ę𝑑ą 𝑢ż𝑦𝑡𝑒 ż𝑎𝑑𝑛𝑒 𝑝𝑎𝑟𝑎𝑚𝑒𝑡𝑟𝑦 𝑑𝑜𝑚𝑦ś𝑙𝑛𝑒)
Przeciążenia funkcji
(od str 367 do 391 – średnio jasne – warto wrócić)
Przeciążenie funkcji polimorfizm funkcji
Polimorfizm – posiadanie wielu postaci, zatem polimorfizm funkcji umożliwia funkcji przybieranie
wielu form. Z kolei termin przeciążenie funkcji oznacza możliwość wiązania z jedna nazwą wielu
funkcji – czyli nazwa jest przeciążana. Oba pojęcia oznaczają to samo. Korzystając z przeciążenia
można zdefiniować całą rodzinę funkcji realizujących w zasadzie to samo zadanie ale na różnych
zestawach parametrów.
Kluczem do przeciążenia funkcji jest lista parametrów zwana również sygnaturą funkcji. Jeśli dwie
funkcje mają tyle samo takich samych (co do typu) parametrów, mają takie same sygnatury, to nazwy
tych parametrów są bez znaczenia. W c++ można definiować funkcje o takich samych nazwach, ale o
różnych sygnaturach. Sygnatury mogą różnić się liczbą parametrów, ich typami lub jednym i drugim.
(sygnatury to opis argumentów i zwracanych wartośći – ich typy). Np.:
Niektóre sygnatury pozornie się różnią ale mimo to nie mogą istnieć jednocześnie. Np. weźmy
pod uwagę dwa prototypy:
Należy pamiętać, że o możliwości przeciążania decyduje sygnatura, a nie typ funkcji. Poniższe dwie
deklaracje są ze sobą niezgodne:
Można użyć różnych typów wartości zwracanej ale poza tym inne muszą być sygnatury:
38
𝑑𝑜𝑢𝑏𝑙𝑒 𝑔𝑟𝑜𝑛𝑘(𝑓𝑙𝑜𝑎𝑡 𝑛, 𝑓𝑙𝑜𝑎𝑡 𝑚); (𝑡𝑒𝑟𝑎𝑧 𝑗𝑢ż 𝑗𝑒𝑠𝑡 𝑤 𝑝𝑜𝑟𝑧ą𝑑𝑘𝑢)
Szablony funkcji
To ogólny zapis funkcji gdzie funkcja opisana jest przez typy ogólne. Pod nie można podstawić typy
konkretne, jak int, czy double. Przekazuje się szablonowi typ jako parametr, przez co kompilator
wygeneruje funkcję określonego typu. Jest to tzw. Programowanie ogólne.
Tworzenie szablonu:
𝐴𝑛𝑦𝑇𝑦𝑝𝑒 = 𝑡𝑒𝑚𝑝;
𝑡𝑒𝑚𝑝 = 𝑎;
𝑎 = 𝑏;
𝑏 = 𝑡𝑒𝑚𝑝; }
Szablonów należy używać kiedy potrzebne nam są funkcje stosujące ten sam algorytm do różnych
typów danych. Szablonów używamy jak normalnych funkcji, trzeba je zadeklarowac, napisać ich
definicję i użyć. Kompilator określi typ parametrów szablonu w momencie jego wywołania z
odpowiednimi zmiennymi użytymi jako parametry tego szablonu. Po określeniu typu parametrów
kompilator wygeneruje kod nowej funkcji (szablonu z już odpowiednimi typami parametrów) (str 374-
375)
Przeciążanie szablonów
Szablony też można przeciążać. (str 375-376)
Nie wszystkie parametry w szablonie muszą być parametrami ogólnymi
Szablony mają ograniczenia (pewne typy nie działają z pewnymi operacjami, z którymi inne
typy już działają), można je czasami przewidzieć i przeciążać szablon jakimś odpowiednikiem,
ale tak czy inaczej jest to ograniczenie, z resztą przeciążać też nie zawsze można (str 377).
39
Specjalizacje jawne
!!! Specjalizacja jawna jest do tego używana, że np. struktury mają trochę inną składnię niż inne typy,
tak samo tablice np. Dlatego w razie napotkania np. struktur trzeba zastosować specjalizacje jawną, bo
prototyp pierwotny szablonu może nie nadawać się do pracy na strukturach
Jeśli kompilator znajdzie specjalizowaną definicję pasującą dokładnie do wywołania funkcji, użyje tej
właśnie definicji, nie szukając nawet szablonów.
Deklaracja jawnej specjalizacji (przed nim nie stosujemy linijki template <typename AnyType>:
𝑡𝑒𝑚𝑝𝑙𝑎𝑡𝑒 <> 𝑣𝑜𝑖𝑑 𝑆𝑤𝑎𝑝 < 𝑗𝑜𝑏 > (𝑗𝑜𝑏 &, 𝑗𝑜𝑏 &);
Powyższy zapis jest opcjonalny gdyż z typów parametrów funkcji wynika, że jest to specjalizacja job.
Wobec tego ten sam prototyp można zapisać jako:
W skrócie:
Dla danej nazwy funkcji można mięc funkcję nieszablonową (zwykłą), szablon funkcji, oraz
jawną specjalizację szablonu funkcji, a także przeciążone wersje ich wszystkich
Prototyp i definicja jawnej specjalizacji muszą być poprzedzone zapisem template<>, powinny
wymieniać nazwę specjalizowanego typu (określać jakiego typu będzie jawna specjalizacja
szablonu – dlatego jest jawna, bo deklarujemy ten typ w sposób jawny, np strukturę)
Specjalizacja przykrywa zwykły szblon, a funkcja zwykła przykrywa je wszystkie! Kompilator
wybiera nieszablonową wersję przed jawną specjalizacją i przed szablonem, a jawna
specjalizacja jest wybierana przed wersją wygenerowaną przed szablonem.
JEŚLI KOMPILATOR znajdzie specjalizowaną definicję pasującą dokładnie do wywołania funkcji,
użyje tej właśnie definicji nie szukając nawet szablonów!
Konkretyzacje i specjalizacje
Należy pamiętać, że włączenie szablonu funkcji do kodu (jej definicja itp) nie powoduje samo z siebie
wygenerowania definicji odpowiedniej funkcji. Jest to tylko plan generowania takiej definicji. Kiedy
kompilator generuje definicję funkcji dla danego typu na podstawie szablonu to wynikiem tego jest
konkretyzacja szablonu (czyli utworzenie egzemplarza). Szablon nie jest definicją funkcji, natomiast
konkretyzacja dla danego typu już jest definicją funkcji. Konkretyzacja niejawna jest wtedy gdy
kompilator wnioskuje o konieczności przygotowania definicji po ustaleniu, że program używa funkcji z
parametrami jakiegoś typu.
Jest też konkretyzacja jawna. Można jawnie nakazać kompilatorowi utworzenie konkretnego
egzemplarza. Składnia jest taka, że deklaruje się wszystkie potrzebne funkcje, podając typy w <> i
poprzedzając deklarację słowem kluczowym template.
40
𝑡𝑒𝑚𝑝𝑙𝑎𝑡𝑒 𝑣𝑜𝑖𝑑 𝑆𝑤𝑎𝑝 < 𝑖𝑛𝑡 > (𝑖𝑛𝑡, 𝑖𝑛𝑡); (𝑗𝑎𝑤𝑛𝑎 𝑘𝑜𝑛𝑘𝑟𝑒𝑡𝑦𝑧𝑎𝑐𝑗𝑎)
Jawna konkretyzacja może być tworzona również poprzez użycia funkcji w programie:
Szablon nie dopasowałby wywołania Add(x,m) ponieważ oczekuje że oba przekazywane argumenty
będą tego samego typu. Ale wywołanie w postaci Add<double>(x,m) wymusza konkretyzację szablonu
dla typu double, a także rzutowanie typów argumentów wywołania funkcji na double w przypadku
drugiego argumentu skonkretyzowanej funkcji Add<double>(double,double)
Szablon to jak by deklaracja funkcji (z ogólnymi parametrami, które mogą być podane w sposób
niejawny lub jawny. Jawny jest wtedy gdy wprost mówimy, że może być potrzebna w
przyszłości definicja danej funkcji dla parametrów jakiegoś określonego typu np. struktury)
Konkretyzacja to jak by definicja zadeklarowanej funkcji. Gdy na podstawie danego szablonu
kompilator wygeneruje definicje funkcji, to ta definicja JEST konkretyzacją. Istnieje
konkretyzacja niejawna. Występuje ona wtedy gdy kompilator po wywołaniu funkcji z danym
parametrem na podstawie szablonu tworzy definicje funkcji, która jest zgodna z wywołaniem.
Istnieje też konkretyzacja jawna. Jest ona wtedy gdy wprost mówimy, że kompilator
napotykając ów jawną deklaracje ma utworzyć konkretną definicję funkcji. Utworzony zostanie
egzemplarz funkcji zgodny z parametrami danego typu na podstawie określonego szablonu.
Kompilator nie czeka tutaj na wywołanie funkcji z określonymi zmiennymi danego typu.
Odgórnie informujemy za pomocą jawnej konkretyzacji kompilator, że chcemy by
wygenerowana została definicja funkcji na podstawie danego szablonu, która będzie zgodna z
parametrami określonego typu.
Błędem jest użycie w jednostce translacji (w jednym pliku) jawnej konkretyzacji i jawnej
specjalizacji dla tego samego typu danych.
Wspólną cechą niejawnej konkretyzacji, jawnej konkretyzacji oraz jawnej specjalizacji jest to
że reprezentują definicję funkcji używającej konkretnych typów, a nie ogólny opis funkcji.
41
Kiedy kompilator dochodzi do jawnej konkretyzacji dla typu char na podstawie definicji szablonu
generuje wersje char funkcji Swap() (W MOMENCIE napotkania jawnej konkretyzacji). W innych
przypadkach użycia Swap() kompilator dopasowuje szablon dla argumentów wykorzystanych w
wywołaniu funkcji. Kiedy na przykład kompilator dochodzi do wywołania Swap(a,b), generuje wersje
short funkcji Swap(), gdyż oba parametry są typu short (generowanie NASTĘPUJE DOPIERO w trakcie
wywoływania) (korzysta tu z prototypu szablonu). Kiedy kompilator dochodzi do wywołania
Swap(n,m), używa odrębnej definicji (jawnej specjalizacji) przygotowanej dla typu job. Gdy kompilator
dochodzi do wywołania Swap(g,h), używa specjalizacji szablonu WYGENEROWANEJ WCZEŚNIEJ
podczas przetwarzania konkretyzacji jawnej.
Proces rozwiązywania przeciążeń szuka najlepiej pasującej funkcji. Jeśli jest tylko jedna taka funkcja,
jest ona wybierana. Jeżeli jest więcej niż jednak funkcja pasująca, ale tylko jedna z nich nie jest
szablonem, ta jest wybierana. Gdy jest więcej kandydatów na dopasowanie i są to same szablony, a
tylko jedna funkcja jest bardziej specjalizowana od reszty, to ta jest wybierana. Jeśli istnieją dwie lub
więcej dobrych funkcji niebędących szablonami, wszystkie są tak samo wyspiecjalizowane, to
wywołanie funkcji jest niejednoznaczne i powoduje błąd. Kiedy nie ma żadnych pasujących wywołań,
to oczywiście jest to bład.
Szablony tak jak normalne funkcje mogą mieć definicje na początku programu, wtedy nie
potrzebują deklaracji na górze (jak to miało miejsce gdy definicja szablonu była na dole
programu)
42
𝑖𝑛𝑡 𝑚 = 20; 𝑖𝑛𝑡 𝑛 = −30; 𝑑𝑜𝑢𝑏𝑙𝑒 𝑥 = 15.5; 𝑑𝑜𝑢𝑏𝑙𝑒 𝑦 = 25.9;
Zapis:
Zapis:
Może być funkcja jak powyżej, gdzie x i y mają różne typy T1 i T2. Nie wiadomo jakiego typu powinna
być zmienna xpy. Np jeśli T1 to short, a T2 to int, to właściwym typem dla xpy był by typ int. Ale mogą
być różne sytuację. Dlatego wymyślono w c++ 11 słowo kluczowe decltype.
𝑖𝑛𝑡 𝑥;
𝑑𝑒𝑐𝑙𝑡𝑦𝑝𝑒(𝑥) 𝑦; (𝑦 𝑏ę𝑑𝑧𝑖𝑒 𝑡𝑒𝑔𝑜 𝑠𝑎𝑚𝑒𝑔𝑜 𝑡𝑦𝑝𝑢 𝑐𝑜 𝑥)
Argumentem wyrażenia decltype może być wyrażenie, więc można go w nawiązaniu do powyższego
przykładu na zdjęciu użyć tak:
𝑑𝑒𝑐𝑙𝑡𝑦𝑝𝑒(𝑥 + 𝑦)𝑥𝑝𝑦;
𝑥𝑝𝑦 = 𝑥 + 𝑦;
Można też połączyć te instrukcje w deklarację równoczesną z inicjalizacją:
𝑑𝑒𝑐𝑙𝑡𝑦𝑝𝑒(𝑥 + 𝑦) 𝑥𝑝𝑦 = 𝑥 + 𝑦;
Logika jaka za tym stoi (skomplikowana) jest na str 389-390
𝑥𝑦𝑡𝑦𝑝𝑒 𝑥𝑝𝑦 = 𝑥 + 𝑦;
(𝑠𝑡𝑤𝑜𝑟𝑧𝑦𝑙𝑖ś𝑚𝑦 𝑡𝑦𝑝 𝑥𝑦𝑡𝑦𝑝𝑒 𝑟ó𝑤𝑛𝑜𝑤𝑎ż𝑛𝑦 𝑡𝑦𝑝𝑜𝑤𝑖 𝑤𝑦𝑛𝑖𝑘𝑎𝑗ą𝑐𝑒𝑚𝑢 𝑧 𝑑𝑒𝑐𝑙𝑡𝑦𝑝𝑒(𝑥 + 𝑦))
43
UPROSZCZONA WERSJA KONIECZNEJ PROCEDURY WNIOSKOWANIA: (str 390)
𝑑𝑒𝑐𝑘𝑡𝑦𝑝𝑒(𝑤𝑦𝑟𝑎ż𝑒𝑛𝑖𝑒)𝑧𝑚𝑖𝑒𝑛𝑛𝑎;
Jeśli wyrażenie jest prostym identyfikatorem nieujętym w nawias to zmienna jest tego samego
typu co identyfikator, z wszystkimi kwalifikatorami, takimi jak const
Jeśli wyrażenie jest wywołaniem funkcji, zmienna otrzyma typ zgodny z typem wartości
zwracanej tej funkcji
Jeśli wyrażenie jest l-wartością to zmienna jest referencją do tego typu wyrażenia. Etap ten
dotyczy wyłącznie wyrażeń niebędących prostymi identyfikatorami. Czyli musi to być
identyfikator ujęty w nawias (dokładniej coś jest na str 390)
Jeśli nie udało się rozstrzygnąć typu na poprzednich etapach, zmienna będzie tego samego
typu co wyrażenie
Zamiast
W ten sposób można przesunąc deklarację typu wartości zwracanej za deklaracje typów parametrów
funkcji. Kombinacja -> double z powyższego przykładu to tak zwana opóźniona deklaracja typu
zwracanego. Połączenie tego z decltype rozwiązuje powyższy problem kompletnie:
Teraz decltype znajduje się za deklaracjami parametrów, więc x i y są nazwami w zasięgu kompilatora
i moga być użyte do określenia typu wartości zwracanej.
44
Rozdział 9. Czas życia, zasięg i łączenie
Pamięć statyczna – zmienne definiowane poza definicjami funkcji, ewentualnie wewnątrz tych funkcji,
ale ze słowem kluczowym static. Ich czas życia jest równy czasowi wykonywania programu, w którym
są zadeklarowane.
Pamięć wątku (C++11) – wielordzeniowe procesory są dziś normą, takie procesory potrafią
współbieżnie wykonywać wiele zadań. Pozwala to na podział obciążenia obliczeniowego programu na
oddzielne wątki, zdatne do współbieżnego przetwarzania. Nie zajmujemy się nimi tu.
Pamięć dynamiczna – zmienne przydzielane są wywołaniem operatora new cechują się czasem życia
rozciągającym się od momentu przydzielenia do momentu zwolnienia albo momentu zakończenia
wykonywania programu.
STOS
45
Zmienne z łączeniem zewnętrznym – z dostępem między plikami. Należy ją zadeklarować poza blokiem
kodu. Są one często nazywane GLOBALNYMI.
Zmienne bez łączenia – z dostępem wyłączenie dla pojedynczej funkcji albo konkretnego bloku
wewnątrz funkcji. Tworzy się ją deklaracją wewnątrz bloku, uzupełnioną słowem static.
Zmienne zewnętrzne powinny być deklarowane w każdym pliku, w którym zamierzamy używać tej
zmiennej, z drugiej strony narzucają one regułę jednoktrotne definicji. Zmienna może być definiowana
tylko raz. Aby spełnić te wymogi, wyróżniono dwa rodzaje deklaracji zmiennych. Jedna z nich to
deklaracja definiująca albo po prostu definicja. To ona wymusza przydział pamięci dla zmiennej. Druga
to deklaracja. Nie wymusza ona ponownego przydziału pamięci dla zmiennej, bo pamięć ta jest już
przydzielona. Deklaracja odbywa się z użyciem słowa extern i nie obejmuje inicjalizacji. (więcej str 412-
414)
Operator zasięgu (::) – poprzedzenie takim operatorem nazwy zmiennej spowoduje, że całe
odwołanie będzie oznaczać odwołanie do globalnej wersji tej zmiennej.
Stosowanie modyfikatora static wobec zmiennej zasięgu pliku nadajej jej łączenie wewnętrzne.
Różnica między łączeniem zew i wew wychodzi na jaw dopiero w programach składających się z wielu
46
plików kodu źródłowego. W takim kontekście zmienna podlegająca łączeniu wewętrznemu jest
zmienną lokalną względem pliku, w którym została zdefiniowana. Ale zmienne podlegające łączeniu
zewnętrznemu mogą być wykorzystywane we wszystkich plikach programu.
Lokalne zmienne statyczne pozbawione łączenia tworzy się stosując do zmiennej definiowanej we
wnętrzu bloku kodu modyfikator static. Modyfikator ten powoduje przydział zmiennej w obszarze
pamięci statycznej, co oznacza, że zmienna choć niewidoczna poza blokiem definicji, jest
podrzymywana w pamięci również wtedy kiedy program nie wykonuje instrukcji bloku. Kolejne
wywołania funkcji nie powodują ponownej inicjalizacji, jak to ma miejsce w przypadku zmiennych
automatycznych. (więcej str 415-419)
Specyfikatory i kwalifikatory
Pewne słowa kluczowe tzn specyfikatory klasy pamięci (inaczej specyfikatory klasy przydziału) i
kwalifikatory cv dodatkowo informującą kompilator o kategorii przydziału zmiennej. Lista
specyfikatorów klasy przydziału
Auto
Register
Static
Extern
Thread_locale
mutable
Kwalifikatory CV:
- const – ten przedrostek tworzy stałe, które w przciwieństwie do zmiennych globalnych które mają
łączenie zewnętrzne, mają łączenie wewnętrzne. (str 420-421)
- volatile – do urządzeń które często się zmieniają bez ingerencji kodu. Do czujników np. (str 420)
Mutable : przeciwieństwo const, gdy np całą strukturę oznaczymy z przedrostkiem const, a w jej
wnętrzu jedną zmienne z przedrostkiem mutable to oznacza, że tą jedną zmienną możemy zmienić (str
420)
Łączenie a funkcje
Domyślnie funkcje cechują się łączeniem zewnętrznym, dzięki czemu mogą być współużytkowane
pomiędzy plikami programu. Można też dodać słowo static co nadaje funkcji łączenie wewnętrzne,
47
ograniczając jej zastosowanie do pliku zawierającego funkcję. Oznacza to że funkcja będzie znana
jedynie w bieżącym pliku, a w pozostałych plikach programu można stosować niezależne od niej
funkcje o identycznych nazwach. Jak w zmiennych przesłania w pliku w którym jej zdefiniowana,
definicję wersji zewnętrznej. (więcej str 421-422)
Inicjalizacja new:
48
Dla uproszczenia w przykładzie przydzielono dwie tablice statyczne, które mają być wykorzystywane
jako źródło przydziału w miejscowej wersji operatora new. Przy miejscowej wersji operatora new,
późniejsze zastosowanie delete nie zadziała (chyba, że buffer będzie należał do pamięci - sterty).
(więcej str 425-429).
Zasięg potencjalny – zasięg działania zmiennej (od początku jej deklaracji do końca obszaru
deklaracyjnego). Jest mniejszy niż obszar deklaracyjny
Można tworzyć własne przestrzenie nazw w obrębie których tworzy się zmienne. Zmienne o tych
samych nazwach ale w różnych przestrzeniach nie kolidują ze sobą
49
Przestrzenie nazw można w trakcie programu uzupełniać:
Można też skorzystać z dyrektywy using jak w przypadku przestrzeni nazw std:
W funkcji main nazwa Jill::fetch jest wciągana do lokalnej przestrzeni nazw. Nie ma jednak lokalnego
zasięgu więc nie zakrywa globalnej wersji fetch. Ale juz deklarowana lokalnie fetch zakrywa zarówno
Jill::fetch jak i globalną wersję fetch. Obie te zmienne są jednak dostępne przy użyciu operatora
zasięgu. (więcej str 433-435)
50
Ogólne zasady. Przestrzenie nazw są bardzo przydatne przy projektach w wieloma plikami (w małym
jednoplikowych kodach można pomijać to spokojnie, jednak w innych przypadkach warto korzystać z
przestrzeni nazw wg zaleceń):
klasa – to jak by przepis na coś. To zbiór informacji na temat czym jest obiekt – jak jest
stworzony oraz jakie posiada cechy. Sposób odwzorowania jakiegoś fragmentu rzeczywistości.
Klasa to wszystkie metody oraz atrybuty (cechy
obiekt – to przedstawiciel czyli reprezentant klasy.
51
Zadaniem etykiet public i private jest ograniczenie dostępu do składowych klasy. Do składowych public
może odwoływać się wprost każdy program korzystający z obiektu danej klasy. W przypadku
składowych private dostęp taki jest możliwy jedynie za posrednictwem publicznych metod klasy (albo
jak przekonamy się w rozdziale 11, za pośrednictwem funkcji zaprzyjaźnionych z klasą). Zgodnie z tym
jedyną możliwością zmiany składowej shares obiektu klasy Stock jest wywołanie na rzecz tego obiektu
jednej z metod klasy Stock. Widać więc, że metody publiczne grają rolę pośredników pomiędzy
programem a prywatnymi składowymi obiektu klasy. Istnieje jeszcze etykieta protected (w rozdziale
13).
52
Hermetyzcja – Polega na ukrywaniu metod i atrybutów dla klas zewnętrznych. Dostęp do nich
możliwy jest tylko z wewnątrz klasy, do której należą, z klas zaprzyjaźnionych lub z klas
dziedziczących. Celem tego jest ukrycie pewnych metod i atrybutów klasy, które nie chcemy
by były używane przez innych programistów korzystających z naszej klasy. Mogą oni przecież
nieźle namieszać w naszej bazie albo API (interfejsie programowania aplikacji)
Ukrywanie danych – umieszczanie danych w obszarze dostępu prywatnego – jeden z
przejawów hermetyzacji
Metody jako składniki publicznego interfejsu klasy, powinny zaś być z reguły publiczne – inaczej nie
będzie można ich wywołać z programu. Mogą być również metody prywatne. Nie da się ich wprawdzie
wywołać bezpośrednio, natomiast mogą być wywołane inną metodą (tym razem publiczną), która
korzysta z tej metody prywatnej. Metody prywatne implementują zazwyczaj operacje manipulujące
szczegółami implementacji spoza interfejsu publicznego.
W deklaracji klasy nie trzeba jawnie stosować etykiety private, ponieważ domyślnie wszystkie
składowe klas są składowymi prywatnymi.
53
Implementowanie metod klas
Klasy implementujemy najlepiej rozdzielając je na dwa pliki (.c i .h). Plik nagłówkowy (.h) to tak jak by
spis treści klasy. A plik źródłowy (.c) to faktyczny opis danych rozdziałów poruszonych w spisie treści.
PRZYKŁAD:
Jeśli definicja znajduje sie w innym pliku niż instrukcja (istnieje rozdzielenie na pliki
nagłówkowe i źródłowe) to w pliku .cpp do poszczególnych metod należy odnosić się z
operatorem zasięgu (podwójny dwukropek). On mówi, do której klasy należy dana metoda (bo
może się zdażyć, że w danym pliku źródłowym będziemy mieli funkcje która np nie należy do
klasy, albo pochodzi z innej klasy). Czyli w pliku .cpp powinno być np:
𝑣𝑜𝑖𝑑 𝐻𝑖𝑔ℎ𝑇𝑒𝑚𝑝𝑙𝑎𝑟 ∷ 𝑃𝑠𝑖𝑜𝑛𝑖𝑐_𝑆𝑡𝑜𝑟𝑚(){
{
54
Wtedy wiadomo, że metoda Psionic_Storm należy do klasy HighTemplar. Nie należy tego robić
gdy piszemy wszystko w jednym pliku. Jednak lepiej rozdzielać klasy na dwa pliki i stosować
wspomniany operator zasięgu. Dla większego porządku.
Stosowanie klas
Obiekt klasy można też utworzyć np stosując operator new. Sposób korzystania z klas ma być
jak najbardziej podobny do użycia typów wbudowanych typu int czy char.
Zmiany wprowadzane w metodach klas powinny być wprowadzane tak by nie wpływać na
zewnętrzny program. Np chcemy zmienić formatowanie metody. Po dodaniu odpowiedniego
formatowania do danej metody (które od tego miejsca działa do końca programu),
powinniśmy na jej końcu przywrócić poprzednie ustawienia (sprzed zmian) w taki sposób by
nie namieszać w zewnętrznym programie. (więcej o formatowaniu str. 363)
Podsumowanie deklaracji klas
55
W deklaracji klasy znajduje się obszar składowych prywatnych, do którego można odwoływać
się wyłącznie za pośrednictwem metod klasy. Klasa może też mieć obszar składowych
publicznych, z których można korzystać wprost z poziomu programu używającego obiektów
klasy (z maina np). Zazwyczaj obszar prywatny zawiera dane obiektu, a metody tworzące
interfejs lądują w obszarze publicznym, więc typowa deklaracja klasy ma postac:
Definicje klasy powinny być w osobnych plikach, niż ich deklaracja (korzystamy wtedy w
definicjach z operatora zasięgu), chyba, że definicja jest krótka – wtedy pozwala się pisać
definicję w deklaracji.
Konstruktory i destruktory
Konstruktory służą do inicjalizacji wstępnej obiektu klasy (pierwotnie nie można było tego
robić ze względu na prywatny dostęp do klasy. Można wprawdzie dostęp ustawić jako
publiczny, ale zatraca to idee klasy jaką jest jej hermetyzacja (ukrywatnie jej atrybutów)).
Konstruktor jest wywoływany w momencie tworzenia obiektu i przypisuje wartości ich
składowym. Nazwa konstruktora jest jak nazwa klasy (są identyczne). Konstruktor klasy Stock
miał by więc nazwę Stock() – nawiasy ponieważ konstruktor to metoda.
56
Po zastosowaniu konstruktora (konstruktor wchodzi w miejsce wcześniejszej metody):
Konstruktor ma taką samą nazwę jak klasa, oraz nie ma typu zwracanego. Pozwala również na
inicjalizację wstępną atrybutów tej metody. Służy jedynie do inicjalizacji, nie interesuje nas co
zwraca dlatego nie ma typu zwracanego. On nie tworzy obiektu, a jest wywoływany w
momencie tworzenia tego obiektu. Nie trzeba wcale zastępować konstruktorem wcześniejszej
metody. Można też w konstruktorze wywołać wcześniejszą metodę. Wyglądalo by to w ten
sposób:
Nie można wtedy tworzyć obiektu bez inicjalizacji. Dzięki konstruktorowi więc NIGDY nie może
się zdażyć sytuacja, że jakiś np przycisk („ok”) nie będzie miał przypisanych jakiś wartości
startowych. Gdyby nie konstruktor to mogła by być możliwa taka sytuacja:
57
Można jeszcze utworzyć konstruktora z wartości domyślnymi:
Jeśli wtedy nie podam wartości w funkcji main jak na obrazku, to konstruktor i tak przypisze
obiektowi wartości jakieś domyślne. Jeśli więc nie podam nic, wtedy domyślnie – defaultowo
– zostaną tam wpisane wartości domyślne podane wewnątrz konsturktora.
Więc:
Konstruktor – specjalna metoda składowa klasy, wywoływana podczas tworzenia obiektu tej
klasy. Zadaniem konstruktora jest zainicjalizowanie obiektu, czyli przypisanie atrybutom
wartości startowych.
Konstruktor ma taką samą nazwę jak klasa.
Dla konstruktora nie określa się zwracanego typu.
Konstruktor jest zawsze wywoływany atomatycznie ilekroć powołujemy do życia nowy
obiekt danej klasy.
Klasa może mieć więcej niż jeden konstruktor (występuje tzw. przeciążenie nazwy
metody)
Konstruktor może wywołać inne metody składowej swojej klasy – np. w celu kontroli
błędów
58
Jeśli w klasie nie ma zdefiniowanego żadnego konstruktora, to kompilator
samodzielnie wygeneruje tzw. konstruktor domniemany. Jeśli ten konstruktor
domniemany zostanie utworzony to i tak nic się nie stanie. Nie zostaną przypisane
atrybutom żadne wartości domyślne. Zostanie jedynie spełniony wymóg, że każda
klasa musi mieć konstruktor. Inaczej jest to niejawny konstruktor domyślny. Gdy
tworzymy własny konstruktor to on przejmuje rolę tego domniemanego, dlatego jeśli
utworzyli byśmy konstruktor, który nie jest domyślny, a utworzyli byśmy obiekt np.
Stock stock1 (bez inicjalizacji), to kompilator wskaże błąd. Bo my przejeliśmy rolę
konstruktora, chcemy go używać jak konstruktora domyślnego, ale on nim nie jest.
Komplator zareaguje. (więcej str 466 – konstruktory domyślne)
UWAGA: nazwy składowe klasy powinny być inne od nazw parametrów konstruktora.
Parametry konstruktora nie reprezentuja składowych klasy. Należy stosować inne
nazwy. Inaczej by była linijka kodu typu – shares = shares. (więcej str 465)
Klasa może mieć tylko jeden konstruktor domyślny (ale wiele normalnych
konstruktorów – pracujących na przeciążeniach)
Stosowanie konstruktorów
Konstruktora można wywoływać w sposób jawny i niejawny. Sposób jawny:
Może być tylko jeden konstruktor domyślny (czyli albo ten z argumentami albo ten bez)
59
Dla porównania jeszcze raz standardowa wersja konstruktora (z domyślnie ustawionymi
parametrami – drugim i trzecim (pierwszy wymaga inicjalizacji jawnej))
Dodatkowo:
Uwaga: w aplikacjach okienkowych warto w funkcji main zapisać jeszcze jeden dodatkowy
zestaw klamer obejmujący cały blok main (poza return) – patrz UWAGA na str 472.
Mamy dwa warianty inicjalizacji konstruktorów (główne warianty) :
𝑆𝑡𝑜𝑐𝑘 𝑠𝑡𝑜𝑐𝑘1("𝑁𝑎𝑛𝑜", 20,12.0);
Powoduje to tworzenie obiektu klasy Stock o nazwie stock1 i inicjalizacji jego składowych
zgodnie z wartościami które podano w wywołaniu konstruktora
Drugi wariant:
𝑆𝑡𝑜𝑐𝑘 𝑠𝑡𝑜𝑐𝑘2 = 𝑆𝑡𝑜𝑐𝑘("𝑏𝑢𝑟𝑎𝑘", 2,5.4);
Inicjalizacja tego wariantu ma dwie opcje w zależności od kompilatora:
Przyjęcie implementacji identycznej z przyjętą dla pierwszego wariantu
61
Utworzenie nienazwanego obiektu tymczasowego, który jest następnie kopiowany do
obiektu stock2. Obiekt tymczasowy jest oczywiście zwalniany po wykonaniu
przypisania. Jeśli kompilator implementuje ten wariant, zobaczymy komunikat, który
w tym przykładzie ustawiliśmy w destruktorze. (patrz str 470 i 472 i 473)
Możliwe jest przypisanie między obiektami klasy:
𝑠𝑡𝑜𝑐𝑘1 = 𝑠𝑡𝑜𝑐𝑘2;
Dojdzie wtedy do kopiowania zawartości pszczególnych składowych z obiektu źródłowego do
obiektu docelowego przypisania (jak w strukturach)
UWAGA: przypisując obiekt do innego obiektu tej samej klasy, inicjujemy niejawnie
kopiowanie wartości poszczególnych składowych obiektu źródłowego do odpowiednich
składowych obiektu docelowego przypisania)
Konstruktor można wykorzystać nie tylko do inicjalizacji obiektu. W naszym programie
przykkładowym mamy w funkcji main następującą instrukcję:
𝑠𝑡𝑜𝑐𝑘1 = 𝑆𝑡𝑜𝑐𝑘("𝐹𝑢𝑡𝑟𝑜", 23,15.5);
Obiekt stock1 istniał już wcześniej. Zamiast inicjalizacji obiektu stock1 mamy więc de facto
przypisanie do tego obiektu nowych wartości składowych. Otóż wywołanie konstruktora
tworzy nowy nienazwany obiekt tymczasowy, po czym następuje wykonanie przypisania tego
obiektu do obiektu stock1. Po wykonaniu przypisania program zwalnia obiekt tymczasowy,
wywołując destruktor.
Zmienne automatyczne przydzielane są na stosie, więc podczas zwalniania pamięci
destruktorem, pierwszy zwalniany jest obiekt utworzony ostatnio, a jako ostatni ten, który
został przydzielony jako pierwszy (wszystko jest git)
Różnica między poniższymi instrukcjami podsumowując:
𝑆𝑡𝑜𝑐𝑘 𝑠𝑡𝑜𝑐𝑘2 = 𝑆𝑡𝑜𝑐𝑘("𝐵𝑢𝑟𝑎𝑘", 2, 2.9);
𝑠𝑡𝑜𝑐𝑘1 = 𝑆𝑡𝑜𝑐𝑘("𝐹𝑢𝑡𝑟𝑜", 49, 2.1);
Pierwsza z tych instrukcji prowoduje inicjalizację – tworzy obiekt z zadaną wartością
początkową i może, ale nie musi, doprowadzić do utworzenia obiektu tymczasowego. Druga z
tych instrukcji prowokuje przypisanie. Takie stosowanie konstruktora w przypisaniach zawsze
powoduje utworzenie obiektu tymczasowego, który ma być obiektem źródłowym przypisania.
Ciekawostka: Destruktor i konstruktor mogą nam pozwolić na zliczanie ilości obiektów danej
klasy:
62
Poniżej jest niby dlaczego stringa przekazujac do funkcji obarczamy referencja
I tend to agree with what Peter87 has said. Simple variables are only a couple of bytes long and
are easy to copy.
A "std::string" is not a simple variable it is a class with many member functions. Making a copy of
all this is much more involved. So accessing the variable by reference is easier than trying to copy
everything.
Many times I find that when a variable needs to be changed in a function it is easier to pass by
reference and be able to make changes than it is to figure how to return more than one item.
http://www.cplusplus.com/forum/beginner/233274/
𝑆𝑡𝑜𝑐𝑘 𝑡𝑒𝑚𝑝 { }
𝑆𝑡𝑜𝑐𝑘 𝑗𝑜𝑐 = {"𝑆𝑡𝑒𝑑", 25}
Pierwszy przykład zainicjalizuje wszystkie dane jak pokazano, drugi przykład zainicjalizuje dane
konstruktorem domyślnym. Trzeci obiekt zostanie zainicjalizowany normalnie, przy czym trzeci, nie
podany argument zostanie ustawiony na 0.0
𝑙𝑎𝑛𝑑. 𝑠ℎ𝑜𝑤( );
Kompilator powinien zgłosić błąd bo nie deklarujemy nigdzie w metodzie show, że przyjmuje ona
argumenty typu const. Wcześniej robiliśmy to jawnie pokazując, że argument będzie typu const, z
63
użyciem referencji const lub wskaźnika const. Tu jest inaczej bo obiekt do którego odnosi się ta metoda
przekazywany jest niejawnie. Powinno stosować się więc taki zapis:
Należy tak używać metod (ustawiać je jako niemodyfikowalne) zawsze wtedy, kiedy ich kod daje
gwarancję niezmienności obiektu, na rzecz którego zostaną wywoływane.
Lub:
64
Podczas pisania definicji napotykamy trudność:
Chcemy zwrócić referencję do całego obiektu (referencje bo w klasach lepiej zwracać referencje
właśnie). Jeśli total_val obiektu s, jest większa od total_val obiektu przekazywanego niejawnie to
zwracany jest obiekt s. A co jeśli jest na odwrót? Drugi obiekt przekazywany jest niejawnie, nie mamy
więc do niego bezpośredniego dostępu. Nie wiemy jak do niego się zwracać. Wiemy tylko, że wartość
: total_val jest wartością tego obiektu, mamy też dostęp do wszystkich metod i wartości zapisanych w
tym obiekcie. Ale nie wiemy jak odnieść się do tego obiektu! W wywołaniu:
𝑠𝑡𝑜𝑐𝑘1. 𝑡𝑜𝑝𝑣𝑎𝑙(𝑠𝑡𝑜𝑐𝑘2);
Znamy referencję (alias) obiektu stock2 (jest nim s), ale nie znamy aliasu (referencji) obiektu stock1.
Rozwiązaniem tego jest właśnie wskaźnik this. Wskazuje on obiekt, na rzecz którego wywołano metodę
(zasadniczo wskaźnik ten jest de facto przekazywany w wywołaniu metody, choc nie jest jednym z
argumentów jawnych). This jest ustawiany w powyższym wywołaniu na adres obiektu stock1 i za jego
pośrednictwem obiekt ten można wykorzystać we wnętrzu metody. W rzeczy samej zapis total_val we
wnętrzu metody topval( ) jest po prostu skrótem od: this -> total_val. (jak w przypadku struktór, znak
„->” jest do odwoływania się do składowych klas za pośrednictwem wskaźników.
65
W naszej metodzie nie będziemy zwracać samego this ponieważ to jedynie adres obiektu. Musimy
skorzystać z operatora wyłuskania * by uzyskać wartość obiektu, mamy więc: *this.
Tablice obiektów
Tablice obiektów klasy działają tak samo jak normalne tablice, np:
𝑚𝑦𝑠𝑡𝑢𝑓𝑓[0]. 𝑢𝑝𝑑𝑎𝑡𝑒( );
𝑚𝑦𝑠𝑡𝑢𝑓𝑓[3]. 𝑠ℎ𝑜𝑤( );
𝑐𝑜𝑛𝑠𝑡 𝑆𝑡𝑜𝑐𝑘 ∗ 𝑡𝑜𝑝𝑠 = 𝑚𝑦𝑠𝑡𝑢𝑓𝑓[2]. 𝑡𝑜𝑝𝑣𝑎𝑙(𝑚𝑦𝑠𝑡𝑢𝑓𝑓[1]);
Do inicjalizacji elementów tablicy można zastosować konstruktor. W takich przypadku trzeba go
wywołać oddzielnie dla każdego elementu tablicy:
Gdy klasa ma więcej niż jeden konstruktor można by poszczególne elementy inicjalizować wywołaniami
różnych konstruktorów:
66
Powyższy kod inicjalizuje elementy stocks[0] i stocks[2] za pośrednictwem konstruktora Stock(const
string *co, long n, double pr), a także element stocks[1] za pośrednictwem konstruktora Stock().
Ponieważ deklaracja tablicy zawiera inicjalizację jedynie części elementów, pozostałe siedem będzie
inicjalizowane konstruktorem domyślnym.
STESTOWAĆ LISTING 10.9 – sprawdzić czy można pobrać np wartość obiektu a nie adres. Czy można
nie korzystać ze wskaźnika itp.
Zasięg klasy
Obejmuje nazwy zdefiniowane w klasie, na przykład nazwy składowych i metod. Elementy o zasięgu
klasy są znane i widoczne w obrębie klasy, ale już nie poza nią. Nie ma dzięki temu ryzyka
sprowokowania kolizji nazw w obrębie różnych klas. Oznacza też to, że nie można wprost odwoływać
się do składowych klasy spoza tej klasy. Dotyczy to też publicznych metod składowych klasy. Aby
bowiem wywołać metodę publiczną klasy, trzeba posłużyć się obiektem.
𝑐𝑙𝑎𝑠𝑠 𝐵𝑎𝑘𝑒𝑟𝑦{
𝑝𝑟𝑖𝑣𝑎𝑡𝑒:
𝑐𝑜𝑛𝑠𝑡 𝑖𝑛𝑡 𝑀𝑜𝑛𝑡ℎ𝑠 = 12; (𝑏łą𝑑)
𝑑𝑜𝑢𝑏𝑙𝑒 𝑐𝑜𝑠𝑡𝑠[𝑀𝑜𝑛𝑡ℎ𝑠]; }
67
Niestety nie zadziała ten kod ponieważ deklaracja klasy opisuje wygląd obiektu, ale go nie tworzy. Do
momentu utworzenia obiektu nie ma gdzie przechowywać wartości (w c++ 11 już można to robić, o
tym później – rozdz. 12). Można ten efekt osiągnąć jednak innymi sposobami.
Przede wszystkim wewnątrz klasy można deklarować typ wyliczeniowy. Deklaracja taka wewnątrz
deklaracji klasy cechuje się zasięgiem klasy. Można więc za pośrednictwem tego typu udostępnić klasie
stałe symboliczne dla wartości całkowitych. Warto wiedzieć, że tak zadeklarowane wyliczenie nie
tworzy nowego elementu danych (składowej danych) klasy. Znaczy to, że tworzone obiekty nie będą
nosić w sobie zbioru wyliczeniowego. Po prostu kompilator będzie zastępował odpowiednie nazwy
stałą symboliczną. Ponieważ zastosowany w kolasie Bakery typ wyliczeniowy tworzy jedynie stałą
symboliczną, bez zamiaru tworzenia zmiennych tego typu, nie trzeba podawać etykiety typu
wyliczeniowego.
𝑐𝑙𝑎𝑠𝑠 𝐵𝑎𝑘𝑒𝑟𝑦{
𝑝𝑟𝑖𝑣𝑎𝑡𝑒:
𝑒𝑛𝑢𝑚 {𝑀𝑜𝑛𝑡ℎ𝑠 = 12};
𝑑𝑜𝑢𝑏𝑙𝑒 𝑐𝑜𝑠𝑡𝑠 [𝑀𝑜𝑛𝑡ℎ𝑠]; }
𝑐𝑙𝑎𝑠𝑠 𝐵𝑎𝑘𝑒𝑟𝑦{
𝑝𝑟𝑖𝑣𝑎𝑡𝑒:
𝑠𝑡𝑎𝑡𝑖𝑐 𝑐𝑜𝑛𝑠𝑡 𝑖𝑛𝑡 𝑀𝑜𝑛𝑡ℎ𝑠 = 12;
𝑑𝑜𝑢𝑏𝑒 𝑐𝑜𝑠𝑡𝑠 [𝑀𝑜𝑛𝑡ℎ𝑠]; }
W ten sposób tworzona jest pojedyncza stała o nazwie Months przechowywana nie w obiektach, ale
w obszarze zmiennych statycznych programu. Dzięki temu wszystkie obiekty klasy Bakery
współużytkują jeden egzemplarz stałej Months. Więcej o statycznych składowych klas powiemy w
rozdziale 12. (Można definiować chyba nie tylko wartości całkowito liczbowe jak chyba w enum)
Nie jest to możliwe, bo Small i Medium z wyliczenia Egg i Tshirt będą nazwami z tego samego zasięgu,
a nazwy elementów wyliczenia będą ze sobą kolidowały. W c++11 wprowadzono możliwość nadania
im zasięgu klasy:
68
𝑡𝑠ℎ𝑖𝑟𝑡 𝐹𝑙𝑜𝑦𝑑 = 𝑡𝑠ℎ𝑖𝑟𝑡 ∷ 𝑀𝑒𝑑𝑖𝑢𝑚;
W c++ 11 zapewniono również większe bezpieczeństwo typowe dla wyliczeń o zasięgu klasy.
Zwyczajnie wyliczenia w c++ są w pewnych sytuacjach automatycznie konwertowane na wartości typu
liczbowego (dochodzi do tego np. przy przypisaniu elementu wyliczenia do zmiennej typu int, kiedy to
enum jest konwertowane na int, albo w wyrażeniu porównania z elementem wyliczeni. W noowych
ograniczonych wyliczeniach niejawna konwersja na typ liczbowy została zablokowana.
Przeciążenia operatorów
Język c++ pozwala rozciągnąć przeciążenie operatorów na typy własne, definiowane samodzielnie,
umożliwiając na przykład dodawanie dwóch obiektów z wykorzystaniem znaku +. Dość częstym
zadaniem programistycznym jest właśnie sumowanie wartości odpowiednich elementów dwóch tablic
(realizowane pętlą for). Możemy to zastąpić definiując klasę reprezentującą tablicę i przeciążając dla
niej operator +:
𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟𝑜𝑝(𝑙𝑖𝑠𝑡𝑎 − 𝑝𝑎𝑟𝑎𝑚𝑒𝑡𝑟ó𝑤)
Gdzie op należy zastąpić symbolem operatora do przeciążenia. Np:
𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 + ()
Przeciąża opearator +. Muszą to być istniejące operatory, nie może być np - @, bo nie ma tekigo
operatora, ale może już być – [ ] – operator indeksowania tablicy:
𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟[ ]( )
PRZYKŁAD
Przykład gdzie pokazano dodawanie do siebie czasów (godzin i minut) bez użycia przeciążenia
operatora oraz z na str – 499-505
69
- w pierwszej opcji skorzystano z wywołania funkcji na rzecz jednego obiektu, która z kolei potrzebuje
jako argument drugiego obiektu. Wtedy ma ona dostęp do atrybutów obu obiektów i można dodać te
dane. Nie można jednak zwracać referencji (bo – patrz - UWAGA)
UWAGA:
Nie należy zwracać referencji do zmiennych lokalnych i im podobnych obiektów tymczasowych. Kiedy
działanie funkcji, w której te zmienne utworzono, dobiegnie końca, referencje te staną się referencjami
nieistniejących obiektów i danych.
- w drugiej opcji skorzystano już z przeciążenia operatora (str 502-505). Konwersja jednego sposobu na
drugi nie jest trudny. Wystarczy bowiem zmienić nazwę metody Sum () na nieco dziwacznie
wyglądającą – „oparator+( )”. Właśnie tak, trzeba symbol operatora (tu +) dodać na koniec ciągu
operator i tak utworzona nazwę zastosować jako nazwę metody. Dodajemy ten znak zarówno w
deklaracji jak i definicji. To jedyny przypadek kiedy w nazwie identyfikator może występować znak inny
niż litera, cyfra albo znak podkreślenia.
Podobnie jak metoda Sum ( ), metoda operator+ ( ) wywoływana jest na rzecz jednego obiektu (tutaj
klasy Time), z drugim przekazywanym jako argument. Można więc wywołać metodę operatora tak
samo jak wcześniej wywoływaliśmy metodę Sum ( ):
Ale dzięki specjalnemu nazewnictwu metod operatora możemy też zastosować wygodniejszy zapis
operatorowy:
Krótko mówiąc, nazwa metody „operator+ ( )” pozwala tak na jej wywoływanie jako zwykłej metody
klasy, jak i wywoływanie niejawne w wyrażeniu angażującym operator +. Kompilator zaś w takich
wyrażeniach decyduje o sposobie ich obliczenia, rozpoznając typy operandów:
𝑖𝑛𝑡 𝑎, 𝑏, 𝑐;
𝑇𝑖𝑚𝑒 𝐴, 𝐵, 𝐶;
𝑐 = 𝑎 + 𝑏; (𝑑𝑜𝑑𝑎𝑤𝑎𝑛𝑖𝑒 𝑤𝑎𝑟𝑡𝑜ś𝑐𝑖 𝑖𝑛𝑡)
𝐶 = 𝐴 + 𝐵; (𝑑𝑜𝑑𝑎𝑤𝑎𝑛𝑖𝑒 𝑧𝑑𝑒𝑓𝑖𝑛𝑖𝑜𝑤𝑎𝑛𝑒 𝑑𝑙𝑎 𝑜𝑏𝑖𝑒𝑘𝑡ó𝑤 𝑘𝑙𝑎𝑠𝑦 𝑇𝑖𝑚𝑒)
𝑇4 = 𝑇1 + 𝑇2 + 𝑇3;
𝑇4 = 𝑇1. 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 + (𝑇2. 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 + (𝑇3)));
70
Ograniczenia przeciążania operatorów
Można przeciążyć zdecydowaną większość operatorów języka c++. Operatory przeciążone (z pewnymi
wyjątkami) nie muszą koniecznie być metodami klas. Ważne jest by przynajmniej jeden z operandów
był typu definiowanego przez użytkownika.
Ograniczenia:
𝑇𝑖𝑚𝑒 𝑠ℎ𝑖𝑣𝑎;
%𝑥; (niedozwolone zastosowanie operatora modulo)
%𝑠ℎ𝑖𝑣𝑎 (również niedozwolone zastosowanie operatora przeciążonego)
Nie można tworzyć własnych symboli operatorów. Nie można na przykład zdefiniować w klasie
metody operator**(), która miała by implementować np.potęgowanie
Nie można przeciążać operatorów:
a) sizeof()
d) :: (operator zasięgu)
Większość operatorów można przeciążać zarówno metodą klasy, jak i funkcją niebędącą
składową klasy. Jednak w przypadku poniższych operatorów dozwolone jest przeciążenie
wyłącznie metodą klasy:
a) = (operator przypisania)
c) [] (operator indeksowania)
71
Dalszy przykład przeciążeń str - 507-509
Klasy zaprzyjaźnione
Funkcja zaprzyjaźniona z klasą to funkcja, która mimo, iż nie jest składnikiem klasy ma dostęp do
wszystkich nawet prywatnych składników klasy. Jako że funkcja zaprzyjaźniona nie jest składnikiem
klasy, to nie posiada tzw. Wskaźnika this (stąd trzeba będzie do takiej funkcji wysyłać obiekty). Funkcja
zaprzyjaźniona może być przyjacielem więcej niż jednej klasy, czyli może mieć dostęp do prywatnych
składników kilku klas.
Mechanizm przyjaźni uzupełnia hermetyzację, dając nam swobodę dostępu do prywatnych atrybutów
dla wybranych funkcji. Oczywiście im więcej przyjaciół tym potem trudniej znaleźć ewentualne błędy
w spójności danych.
Jeśli funkcja ma mieć dostęp do składników prywatnych dwóch klas, to mamy do wyboru dwie opcje:
To klasa określa kto jest jej przyjacielem – bo każda funkcja mogłaby twierdzić, że jest zaprzyjaźniona
a tak dysponujemy oficjalną listą przyjaciół. Czyli deklarować przyjaźń może tylko klasa a nie funkcja.
72
Jako argument własnej funkcji nie trzeba wysyłać po kolei wszystkich atrybutów obiektu danej klasy.
Można spokojnie wysłać do niej cały obiekt (lepiej to robić przez referencje, by nie kopiować wszystkich
atrybutów obiektu)
Przyjaciele najważniejsi
Przeciążenie z mnożeniem jest w tym przypadku troche inne (bo mamy jeden obiekt klasy Time, i jedną
wartość typu double. Otrzymujemy przez to ograniczenie sposobu stosowania operatora. Jak
pamiętamy w wywołaniach operatorów przeciążonych obiekt wywołujący znajduje się po lewej stronie
operatora. Czyli wyrażenie:
𝐴 = 𝐵 ∗ 2.75;
Tłumaczone jest na następujące wywołanie:
𝐴 = 𝐵. 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 ∗ (2.75);
Inaczej jest w przypadku poniższego wyrażenia:
73
metodą klasy (większość operatorów da się przeciążyć tak metodą klasy jak i zwykłą funkcją). Funkcja
niebędąca składową klasy nie jest wywoływana na rzecz żadnego z obiektów, a wszystkie wartości do
których funkcja ta się odwołuje są jawnie przekazywane arguntami wywołania. Kompilator mógłby
więc dopasować wyrażenie:
Deklarowanie przyjaźni
Pierwszym krokiem do utworzenia funkcji zaprzyjaźnionej jest umieszczenie jej prototypu w deklaracji
klasy; prototyp ten powinien być poprzedzony słowem kluczowym friend.
𝑓𝑟𝑖𝑒𝑛𝑑 𝑇𝑖𝑚𝑒 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 ∗ (𝑑𝑜𝑢𝑏𝑙𝑒 𝑚, 𝑐𝑜𝑛𝑠𝑡 𝑇𝑖𝑚𝑒 & 𝑡); (𝑤 𝑑𝑒𝑘𝑙𝑎𝑟𝑎𝑐𝑗𝑖 𝑘𝑙𝑎𝑠𝑦)
Z powyższego prototypu wynika, że:
Choć funkcja operator*() jest deklarowana wewnątrz klasy, nie jest jej składową (metodą). Nie
można więc wywoływać jej za pośrednictwem obiektu klasy i operatora dostępu do składowej
(dlatego, że lewy operand to nie zmienna typu Time)
Choć funkcja operator*() nie jest metodą, ma dostęp do składowych klasy właściwy właśnie
metodom klasy. Dlatego, że jest funkcją zaprzyjaźnioną
Drugi krok polega na napisaniu definicji funkcji. Ponieważ nie jest metodą, w nazwie nie stosuje się
kwalifikatora Time::. W definicji nie występuje też słowo friend. Definicja powinna więc wyglądać
następująco:
𝑇𝑖𝑚𝑒 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 ∗ (𝑑𝑜𝑢𝑏𝑙𝑒 𝑚, 𝑐𝑜𝑛𝑠𝑡 𝑇𝑖𝑚𝑒 & 𝑡){ (𝑏𝑒𝑧 𝑠ł𝑜𝑤𝑎 𝑓𝑟𝑖𝑒𝑛𝑑 𝑤 𝑑𝑒𝑓𝑖𝑛𝑖𝑐𝑗𝑖)
}
𝐴 = 2.75 ∗ 𝐵;
Na takie:
74
Można funkcje zaprzyjaźnione rozpatrywać jako elementy rozszerzające publiczny interfejs klasy,. Na
przykład z pozycji koncepcyjnej mnożenie double przez Item jest operacją bliźniacza semantycznie do
mnożenia Item przez double. To pierwsze z nich wymaga zdefiniowania funkcji zaprzyjaźnionej, a druga
może być zrealizowana metodą klasy.
Uwaga:
Aby przeciążyć operator dla klasy, tak by operator ten przyjmował za pośrednictwem pierwszego
operandu wartość typu innego niż typ klasy, można przeciążyć operator funkcją zaprzyjaźnioną z klasą.
𝑐𝑜𝑢𝑡 ≪ 𝑡𝑟𝑖𝑝;
Jest to dozwolone bo operator << można przeciążać. W zasadzie to już korzystamy z jego przeciążeń
bo pierwotnie jest to operator przesunięcia bitowego w lewo ale klasa ostream przeciąża ten operator,
stosując go w roli narzędzia wyprowadzającego dane. Deklaracja klasy ostream zawiera metody
przeciążające operator – operator<<( ) – dla wszystkich podstawowych typów. Istnieje więc definicja
dla argumentu typu int, double itp. Jednym ze sposobów nauczania klasy ostream rozpoznawania
obiektów Time byłoby więc uzupełnienie deklaracji klasy ostream o prototyp nowej metody
przeciążającej operator <<, ale ingerencja w deklarację klasy ostream i grzebanie w interfejsach
standardowych to pomysł ryzykowny i nieelegancki. Musimy więc zadowolić się rozwiązaniem
odwrotnym, polegającym na takim wyposażeniu klasy Time, aby rozpoznawała obiekty klasy ostream
i dzięki temu radziła sobie z cout.
𝑡𝑟𝑖𝑝 ≪ 𝑐𝑜𝑢𝑡;
Porządany efekt:
𝑐𝑜𝑢𝑡 ≪ 𝑡𝑟𝑖𝑝;
Uzyskamy dzięki funkcji zaprzyjaźnionej – przeciążając operator następująco:
75
𝑣𝑜𝑖𝑑 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 ≪ (𝑜𝑠𝑡𝑟𝑒𝑎𝑚 & 𝑜𝑠, 𝑐𝑜𝑛𝑠𝑡 𝑇𝑖𝑚𝑒 & 𝑡){
𝑜𝑠 ≪ 𝑡. ℎ𝑜𝑢𝑟𝑠 ≪ "𝑔𝑜𝑑𝑧𝑖𝑛. ";
}
𝑐𝑜𝑢𝑡 ≪ 𝑡𝑟𝑖𝑝;
Ponieważ umożliwia to zapis operatorowy
Przyglądając się ciału funkcji operatora, można zauważyć, że co prawda odwołuje się ona do
poszczególnych składowych klasy Time – np. t.hours (stąd wymóg zaprzyjeźnienia z klasą) -, ale obiekt
klasy ostream wykorzystuje zawsze jako całość (tutaj „os”). Skoro funkcja przeciążająca operator<<()
nie odwołuje się do prywatnych składowych klasy ostream, nie musi być z nią zaprzyjaźniona.
Wywołanie cout << trip powinno korzystać z obiektu cout, a nie z jego kopii, więc wymuszamy
przekazanie go do funkcji operatora przez referencję, a nie przez wartość. Wynika z tego, że wyrażenie
cout << trip czyni os (we wnętrzu funkcji operatora) aliasem obiektu cout, a wyrażenie cerr << trip czyni
os aliasem obiektu cerr. Obiekt klasy Time może być przekazywany tak przez wartość, jak i przez
referencję, bo tak czy inaczej uzyskamy w funkcji operatora dostęp do jego wartości. I tym razem
decyzja o zastosowaniu referencji jest uzasadniona wydajnością realizacji wywołania. Lepiej generalnie
przekazywać obiekty klas przez referencje, właśnie ze względu na wydajność (nie trzeba kopiować
wszystkich atrybutów).
𝑐𝑜𝑢𝑡 ≪ 𝑡𝑟𝑖𝑝;
Zadziała świetnie, ale nie będziemy mogli konstruować kaskadowych wywołań operatora <<, tak
wygodnych w przypadku korzystania z obiektów cout. Aby zrozumieć dlaczego mamy takie
ograniczenie i jak je pokonać należy lepiej zrozumieć działanie obiektu cout:
𝑖𝑛𝑡 𝑥 = 5;
𝑖𝑛𝑡 𝑦 = 7;
𝑐𝑜𝑢𝑡 ≪ 𝑥 ≪ 𝑦;
Kompilator czyta wyrażenie z operatorem << od lewej do prawej, co oznacza, że ostatnia z powyższych
instrukcji odpowiada zapisowi:
(𝑐𝑜𝑢𝑡 ≪ 𝑥) ≪ 𝑦);
Operator wedle definicji w ramach biblioterki iostream przyjmuje po lewej stronie obiekt klasy
ostream. W oczywisty sposób podwyrażenie cout << x spełnia ten wymóg, bo cout jest obiektem klasy
ostream. Jednak z powyższej instrukcji wynika, że również całe podwyrażenie (cout << x), skoro stoi po
76
lewej stronie operatora <<, musi być obiektem typu ostream. To zaś oznacza, że klasa ostream
implementuje funkcję operatora operator<<() tak, że zwraca referencję do obiektu klasy ostream. W
szczególności zwraca ona referencję do obiektu, na rzecz, którego nastąpiło wywołanie (czyli tutaj
cout). Dzięki temu podwyrażenie (cout << x) daje wartość typu ostream w postaci obiektu cout, która
może wystąpić po lewej stronie kolejnego operatora <<.
𝑟𝑒𝑡𝑢𝑟𝑛 𝑜𝑠;
}
𝑐𝑜𝑢𝑡 ≪ 𝑡𝑟𝑖𝑝;
Będzie reallizowana wywołaniem:
𝑓𝑟𝑖𝑒𝑛𝑑 𝑇𝑖𝑚𝑒 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 + (𝑐𝑜𝑛𝑠𝑡 𝑇𝑖𝑚𝑒 & 𝑡1, 𝑐𝑜𝑛𝑠𝑡 𝑇𝑖𝑚𝑒 & 𝑡2);
Operator dodawania wymaga obecności dwóch operandów. W przypadku funkcji przeciążacjącej
będącej metodą pierwszy z nich jes przekazywany w wywołaniu operatora niejawnie i dostępny
wewnątrz za pośrednictwem wskaźnika this; drugi operand jest przekazywany jawnie argumentem
wywołania. W przypadku przeciążenia funkcją nieskładową oba operandy są przekazywane w
wywołaniu jawnie.
UWAGA
Funkcja przeciążająca operator dla klasy niebędąca jej składową wymaga zdefiniowania tylu
parametrów, ile operandów przyjmuje operator. Funkcja przeciążająca operator będąca
składową (metodą) wymaga zdefiniowania jednego parametru mniej, ponieważ jeden z
operandów jest przekazywany do operatora niejawnie w roli obiektu wywołującego.
𝑇1 = 𝑇2 + 𝑇3;
77
Na następujące wywołanie:
Definiując własną wersję operatora, musimy zdecydować się na jedną z dwóch wersji – nie wolno
definiować obu. Podwójna definicja wprowadziłaby niejednoznaczność – oba wywołania pasowałyby
do wyrażenia, więc kompilator nie potrafiłby zdecydować o wyborze jednej z nich.
Która z nich jest lepsza? W przypadku niektórych wspomnianych wcześniej operatorów nie mamy
wyboru – trzeba je implementować jako metody klas. Z kolei w przypadkach wątpliwych różnica jest
zazwyczaj pomijalna. Niekiedy z cech definicji klasy, zwłaszcza jeśli zdefiniowano dla niej konwersje
typów, wynika, że lepszym wyborem jest funkcja niebędąca składową. Więcej o tym pod koniec tego
rozdziału – „Konwersje a klasy zaprzyjaźnione”.
Wektor fizycznie możemy przedstawić za pomoca współrzędnych lub za pomocą długości i kierunkiem.
Niekiedy wygodniejsza jest jedna, innym razem druga reprezentacja – najlepiej więc było by
uwzględnić w opisie klasy obie („Wielorakie reprezentacje w klasach” – poniżej). Klasę trzeba
zaprojektować tak by po zastąpieniu jednej reprezentacji drugą obiekt automatycznie aktualizował
reprezentację wektora. Taka inteligencja obiektów to zaleta klas języka c++.
Jeśli metoda musi skonstruować i zainicjalizować nowy obiekt klasy, warto sprawdzić czy nie da się
zadania przekazać konstruktorowi. Nie tylko zaoszczedzimy sobie wysiłku ale i zagwarantujemy
poprawną konstrukcję nowego obiektu.
Str 529 (czy muszą być oba przeciążenia metody odpowiadającej za mnożenie? Nie można zasotosować
jedynie metody zaprzyjaźnionek?)
UWAGA:
Ponieważ przeciążenie operatorów jest implementowane funkcjami, można przeciążyć ten sam
operator wielokrotnie, o ile tylko każda z funkcji przeciążających ma niepowtarzalną sygnaturę i taką
samą liczbę operandów jak odpowiednia wersja operatora wbudowanego w c++. Znak odejmowania
występuje w dwóch wariantach (działa na dwa argumenty (odejmowanie) oraz jeden argument
(zmiana znaku), dlatego tak samo możemy napisać dwa przeciążenia dla znaku „-„.
78
Automatyczna konwersja i rzutowanie typów klas
Str 534 - 544
Ponieważ klasa Stonewt reprezentuje pojedynczą wartość, uzasadnione wydaje się
umożliwienie konwersji na obiekt klasy Stonewt wartości typu int bąrdz wartości
zmiennoprzecinkowej. W języku c++ każdy konstruktor przyjmujący w wywołaniu tylko jeden
argument występuje jako przepis na konwersję wartości tego typu argumentu na obiekt klasy,
więc:
𝑆𝑡𝑜𝑛𝑒𝑤𝑡(𝑑𝑜𝑢𝑏𝑙𝑒 𝑙𝑏𝑠); (𝑝𝑟𝑧𝑒𝑝𝑖𝑠 𝑛𝑎 𝑘𝑜𝑛𝑤𝑒𝑟𝑠𝑗ę 𝑑𝑜𝑢𝑏𝑙𝑒 𝑛𝑎 𝑆𝑡𝑜𝑛𝑒𝑤𝑡)
Służy jako instruktaż konwersji wartości typu double na wartość typu Stonewt. Możemy więc
pisać jak poniżej:
𝑆𝑡𝑜𝑛𝑒𝑤𝑡 𝑚𝑦𝐶𝑎𝑡; (𝑢𝑡𝑤𝑜𝑟𝑧𝑒𝑛𝑖𝑒 𝑜𝑏𝑖𝑒𝑘𝑡𝑢 𝑆𝑡𝑜𝑛𝑒𝑤𝑡)
𝑚𝑦𝐶𝑎𝑡 = 19.8; (𝑤𝑦𝑤𝑜ł𝑎𝑛𝑖𝑒 𝑚𝑒𝑡𝑜𝑑𝑦 𝑆𝑡𝑜𝑛𝑒𝑤𝑡(𝑑𝑜𝑢𝑏𝑙𝑒)𝑑𝑜 𝑘𝑜𝑛𝑤𝑒𝑟𝑠𝑗𝑖 𝑙𝑖𝑐𝑧𝑏𝑦 19.8
Powyższy kod wykorzystuje wywołanie konstruktora Stonewt(double) do utworzenia
tymczasowego obiektu klasy Stonewt inicjalizowanego wartością 19.8. Następnie przy użyciu
domyślnego sposobu przypisania (kopiowania odpowiednich składowych) obiekt ten jest
przypisyywany do myCat. Proces ten nosi nazwę konwersji niejawnej, ponieważ odbywa się
automatycznie i bez wymogu stosowania operatora rzutowania.
W roli funkcji konwesrji może występować tylko taki konstruktor klasy, który da się wywołać
z jednym argumentem. Poniższy konstruktor przyjmuje dwa argumenty wywołania, nie nadaje
się więc do stosowania jako konstruktor konwertujący:
𝑆𝑡𝑜𝑛𝑒𝑤𝑡(𝑖𝑛𝑡 𝑠𝑡𝑛, 𝑑𝑜𝑢𝑏𝑙𝑒 𝑙𝑏𝑠); (𝑛𝑖𝑒 𝑗𝑒𝑠𝑡 𝑡𝑜 𝑘𝑜𝑛𝑠𝑡𝑟𝑢𝑘𝑡𝑜𝑟 𝑘𝑜𝑛𝑤𝑒𝑟𝑡𝑢𝑗ą𝑐𝑦)
Mógłby natomiast pełnić taką rolę gdybyśmy zdefiniowali wartość domyślną dla drugiego
argumentu wywołania:
𝑆𝑡𝑜𝑛𝑒𝑤𝑡(𝑖𝑛𝑡 𝑠𝑡𝑛, 𝑑𝑜𝑢𝑏𝑙𝑒 𝑙𝑏𝑠 = 0); (𝑢𝑚𝑜ż𝑙𝑖𝑤𝑖𝑎 𝑘𝑜𝑛𝑤𝑒𝑟𝑠𝑗ę 𝑧 𝑖𝑛𝑡 𝑛𝑎 𝑆𝑡𝑜𝑛𝑒𝑤𝑡)
W miarę zdobywania doświadczenia w programowaniu okazuje się, że właśnie
automatyczność konwersji nie zawsze jest tak pożądana, jak mogłoby się zdawać, bo
prowokuje niekiedy konwersje niechciane. W c++ załatwiono to nowym słowem kluczowym –
explicit. Oznaczo ono, że dany konstruktor nie może być wywoływany niejawnie, co blokuje
automatykę konwersji. Można więc zadeklarować konstruktor jak poniżej:
𝑒𝑥𝑝𝑙𝑖𝑐𝑖𝑡 𝑆𝑡𝑜𝑛𝑒𝑤𝑡(𝑑𝑜𝑢𝑏𝑙𝑒 𝑙𝑏𝑠); (𝑏𝑒𝑧 𝑚𝑜ż𝑙𝑤𝑖𝑜ś𝑐𝑖 𝑤𝑦𝑤𝑜ł𝑎𝑛𝑖𝑎 𝑤 𝑛𝑖𝑒𝑗𝑎𝑤𝑛𝑒𝑗 𝑘𝑜𝑛𝑤𝑒𝑟𝑠𝑗𝑖)
Zablokuje to niejawne konwersje, wciąż pozostanie jednak możliwość wykonywania konwersji
jawnych, wymuszonych operatorem rzutowania typu obiektu:
79
𝑚𝑦𝐶𝑎𝑡 = (𝑆𝑡𝑜𝑛𝑒𝑤𝑡)19,6; (𝑤 𝑝𝑜𝑟𝑧ą𝑑𝑘𝑢 𝑘𝑜𝑛𝑤𝑒𝑟𝑠𝑗𝑎 𝑗𝑎𝑤𝑛𝑎 𝑤𝑚𝑢𝑟𝑧𝑜𝑛𝑎 𝑟𝑧𝑢𝑡𝑜𝑤𝑎𝑛𝑖𝑒𝑚)
UWAGA
W języku c++ konstruktor klasy, który przyjmuje jeden argument, definiuje funkcję konwersji
typu argumentu na typ klasy. Jeśli konstruktor zostanie oznaczony słowem explicit, będzie
mógł uczestniczyć wyłącznie w konwersjach jawnych; konstruktory jednoargumentowe bez
słowa explicit mogą uczestniczyć również w konwersjach niejawnych (automatycznych).
Kiedy kompilator będzie stosował metodę Stonewt(double)? Jeśli w deklaracji konstruktora
znajdzie się słowo explicit, Stonewt(double) będzie wywoływane jedynie w wyniku jawnego
rzutowania na typ klasy; w obliczu braku tego słowa konstruktor może być wywoływany
również w konwersjach niejawnych:
Przy inicjalizacji obiektu klasy Stonewt wartością typu double
Przy przypisywaniu wartości typu double do obiektu klasy Stonewt
Przy przekazywaniu wartości typu double w roli argumentu funkcji oczekującej
przekazania obiektu typu Stonewt
Przy próbach zwracania wartości typu double z funkcji, która deklaruje typ wartości
zwracanej jako Stonewt
W powyższych przypadkach kiedy występuje w nich typ inny niż double, dający się bez
niejednoznaczności skonwertować na double
W odniesieniu do ostatniego punktu proces dopasowania wywołania funkcji na podstawie
typów argumentów pozwala kosntruktorowi Stonewt(double) występować w roli funkcji
konwersji nie tylko typu double, ale i innych typów liczbowych. Czyli poniższe instrukcje
działają, najpierw konswertując wartość typu int na wartośc typu double, a dopiero potem
wywołując konstruktor Stonewt(double)
𝑆𝑡𝑜𝑛𝑒𝑤𝑡 𝐽𝑢𝑚𝑏𝑜(6000); (𝑤𝑦𝑤𝑜ł𝑎𝑛𝑖𝑒 𝑆𝑡𝑜𝑛𝑒𝑤𝑡, 𝑘𝑜𝑛𝑤𝑒𝑟𝑠𝑗𝑎 𝑖𝑛𝑡 𝑛𝑎 𝑑𝑜𝑢𝑏𝑙𝑒)
𝐽𝑢𝑚𝑏𝑜 = 4600; (𝑤𝑦𝑤𝑜ł𝑎𝑛𝑖𝑒 𝑆𝑡𝑜𝑛𝑒𝑤𝑡(𝑑𝑜𝑢𝑏𝑙𝑒), 𝑘𝑜𝑛𝑤𝑒𝑟𝑠𝑗𝑎 𝑖𝑛𝑡 𝑛𝑎 𝑑𝑜𝑢𝑏𝑙𝑒)
Pamiętać trzeba jednak, że prezentowany powyżej dwuetapowy proces konwersji jest
podejmowany wyłącznie wtedy, kiedy nie istnieje niejednoznaczność wyboru konstruktorów i
poeratorów. Jeśliby klasa definiowała również kosntruktor Stonewt(long), kompilator
odrzuciłby obie powyższe instrukcje sygnalizując przy tym (najprawdopodobniej), że int da się
równie dobrze konwertowac na typ double, jak na typ long, więc występuje
niejednoznacznośc kodu.
Zauważmy, że kiedy konstruktor przyjmuje pojedynczy argument, inicjalizację obiektu można
przeprowadzić następująco (składnia inicjalizacji obiektu klasy przy użyciu konstruktora
jednoargumentowego):
𝑆𝑡𝑜𝑛𝑒𝑤𝑡 𝑖𝑛𝑐𝑜𝑔𝑛𝑖𝑡𝑜 = 259;
To postać równoważna dwóm, do których jestemy przyzwyczajeni (standardowe składnie
inicjalizacji obiektów klas):
𝑆𝑡𝑜𝑛𝑒𝑤𝑡 𝑖𝑛𝑐𝑜𝑔𝑛𝑖𝑡𝑜(260);
80
𝑆𝑡𝑜𝑛𝑒𝑤𝑡 𝑖𝑛𝑐𝑜𝑔𝑛𝑖𝑡𝑜 = 𝑆𝑡𝑜𝑛𝑒𝑤𝑡(265);
Jednak te dwie ostatnie moa być zastosowane również z konstruktorami
wieloargumentowymi.
Wywołanie funkcji:
𝑑𝑖𝑠𝑝𝑙𝑎𝑦(422); (𝑘𝑜𝑛𝑤𝑒𝑟𝑠𝑗𝑎 422 𝑛𝑎 𝑡𝑦𝑝 𝑑𝑜𝑢𝑏𝑙𝑒, 𝑎 𝑝𝑜𝑡𝑒𝑚 𝑛𝑎 𝑆𝑡𝑜𝑛𝑒𝑤𝑡)
Prototyp funkcji display() wskazuje, że pierwszym argumentem wywołania powinien być
obiekt klasy Stonewt. Po skonfrontowaniu tego z występującym w wywołaniu typem int
kompilator szuka najpierw konstruktora Stonewt(int), a kiedy go nie znajdzie, szuka
konstruktora dla innego typu wbudowanego, na który dałoby się skonwertować wartość
występującą w wywołaniu. Wymóg ten spełnia Stonewt(double). Kompilator konwertuje więc
int na double, a potem wywołaniem konstruktora Stonewt(double) konwertuej double na
Stonewt.
Funkcje konwersji
Konwersja odwrotna tj. obiekt Stonewt na postać wartości typu double – można to zrobić, ale
nie za pośrednictwem konstruktorów klasy. Konstruktory potrafią jedynie tworzyć na
podstawie rozmaitych wartości obiekty klasy. W celu wykonania operacji odwrotnej trzeba
skorzystać ze specjalnej katergorii operatorów zwanych funkcjami konwersji. Definiują one
sposób rzutowania typów definiowanych przez użytkownika i stosuje sie je tak jak klasyczne
operatory rzutowania. Jeśli na przykład zdefiniujemy metodę konwersji klasy Stonewt na typ
double, będziemy mogli zapisac:
𝑆𝑡𝑜𝑛𝑒 𝑤𝑜𝑙𝑓𝑒(254.5);
𝑑𝑜𝑢𝑏𝑙𝑒 ℎ𝑜𝑠𝑡 = 𝑑𝑜𝑢𝑏𝑙𝑒 (𝑤𝑜𝑙𝑓𝑒); (𝑠𝑘ł𝑎𝑑𝑛𝑖𝑎 1)
𝑑𝑜𝑢𝑏𝑙𝑒 𝑡ℎ𝑖𝑛𝑘𝑒𝑟 = (𝑑𝑜𝑢𝑏𝑙𝑒)𝑤𝑜𝑙𝑓𝑒; (𝑠𝑘ł𝑎𝑑𝑛𝑖𝑎 2)
Można też zdać się na kompilator:
𝑆𝑡𝑜𝑛𝑒𝑤𝑡 𝑤𝑒𝑙𝑙𝑠(20,4);
𝑑𝑜𝑢𝑏𝑙𝑒 𝑠𝑡𝑎𝑟 = 𝑤𝑒𝑙𝑙𝑠; (𝑛𝑖𝑒𝑗𝑎𝑤𝑛𝑒 𝑤𝑦𝑤𝑜ł𝑎𝑛𝑖𝑒 𝑓𝑢𝑛𝑘𝑐𝑗𝑖 𝑘𝑜𝑛𝑤𝑒𝑟𝑠𝑗𝑖)
Jak tworzy się funkcję konwersji? By umożliwić konwersję obiektu klasy na typ nazwa-typu,
powinno sie zdefiniowac funkcję konwersji w postaci:
Operator nazwa-typu();
Zauważmy, że:
Funkcja konwersji musi być składową klasą (metodą)
Funkcja konwersji nie może definiować typu wartości zwracanej
Funkcja konwersji nie może przyjmować argumentów
Np. funkcja konwersji na typ double powinna mieć prototyp:
𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 𝑑𝑜𝑢𝑏𝑙𝑒( );
81
O typie docelowym konwersji informuje element nazwa-typu (tutaj jest nim double), nei ma
więc potrzeby określenia typu wartości zwracanej. Z tego zaś, że funkcja konwersji jest metoda
klasy, wynika, iż będzie zawsze wywoływana na rzecz konkretnego obiektu (tego
konwertowanego), nie trzeba więc również przekazywać do niej żadnych argumentów.
Jeśli chcemy wyposażyć klase Stonewt w możlwiość konwersji na typy int i double, powinny
uzupełnić deklarację klasy o następujące prototypy:
𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 𝑖𝑛𝑡 ( );
𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 𝑑𝑜𝑢𝑏𝑙𝑒 ( );
(przykład str 540-542)
82
jest do zmiennej innego niz ta klasa typu, albo gdzie wystepuje operator rzutowania
na ów typ.
Konwersja a zaprzyjaźnienie
Jak juz wiemy - do przeciazania operatorów dla klasy nadaja sie tak metody tej klasy, jak i
funkcje z nia zaprzyjaznione (dla uproszczenia zakladamy, ze klasa nie definiuje funkcji
konwersji poastaci operator double() ). Dodawanie mozna zaimplementowac nastepujaca
metoda:
PRZEANALIZOWAĆ PRZYKŁAD: str 545-546
Wniosek:
Przeciążenie dodawania funkcją zaprzyjaźnioną ułatwia automatyczne konwertowanie typów.
Skoro bowiem oba operandy są przekazywane argumentami wywołania, oba mogą podlegać
konwersji w wywolaniu. Kompilator nie próbuje skonwertować wartości double na obiekt
klasy Stonewt gdy dotyczy to obiektu wywołującego. Konwersja dotyczyć moze WYŁĄCZNIE
argumentów przekazywanych w wywołaniu metody, ale nigdy obiektu wywołującego.
(Potrzebujemy do tego konstruktora pracującego z danych typem - np.Stonewt(double) dla
konwersji typu double na Stonewt)
83
pasuje do wywołania funkcji zaprzyjaźnionej operator+(double x, Stonewt & s);
Takie same kombinacje były przy przeciążaniu klasy Vector. Każda decyzja przynosi dwojakie
efekty. Wybór możlwiosci pierwszej (poleganie na niejawnej konwersji argumentów) daje
korzyść w postaci skrócenia programów, bo nie trzeba definiować wielu funkcji. Oznacza to
mniejszy wysiłek programisty definiującego klasę, ale i programistów-użytkowników klasy.
Wadą jest zaś dodatkowy narzut czasowy i pamięciowy wynikający z wywołania konstruktora
celem konwersji arugmentu wywołania przeciążonego operatora. Druga możliwość
(dodatkowe funkcje przeciążające operator dla kombinacji typów argumentów) zwiększa
nakłady pracy, ale skraca odrobinę czas wykonania programu.
Jeśli w programie planujemy intensywne korzystanie z operacji dodawania wartości double do
obiektów Stonewt, bezpośrednie obsługiwanie takiej operacji osobnym przeciążonym
operatorem dodawania może się opłacić. Jeśli zas program ma realizować takie operacje
jedynie od czasu do czasu, prościej i wygodniej jest polegać na niejawnej konwersji typów
argumentów w wywołaniu uniwersalnego operatora dodawania (ostrożni mogą każdorazowo
stosować konwersje jawne)
PODSUMOWANIE:
Str 547-548
Statyczna składowa klasy ma specjalną własność program utworzy tylko jedną kopię statycznej
składowej klasy niezależnie od liczby utworzonych obiektów tej klasy. Składowa ta będzie
współużytkowana przez wszystkie obiekty klasy mniej więcej tak jak jeden numer telefonu
stacjonarnego jest wykorzystywany przez wszystkich członków zamieszkującej dom rodziny. Jest to
przydatne dla danych, które powinny pozostać prywatne względem klasy, ale muszą mieć identyczną
wartość dla wszystkich obiektów. Obrazuje to rysunek powyżej:
84
Kod destruktora czyli np. usuwanie pamięci po wskaźnikach musiny napisac sami (patrz str 556 – sam
dół – ostatnia metoda)
Statyczną zmienną musimy definiować poza deklaracją tej klasy, ponieważ deklaracja jest opisem
sposobu przydzielenia pamięci, ale nie jest samym przydziałem tej pamięci. Pamięc dla obiektu klasy
przydziela się tworząc obiekt. W przypadku składowej statycznej można jednak dokonać inicjalizacji
niezależnie od któregokolwiek z obiektów osobną instrukcją znajdującą się poza deklaracją klasy.
Zauważmy, że słowo kluczowe static jest tylko w deklaracji:
Deklaracja
𝑠𝑡𝑎𝑡𝑖𝑐 𝑖𝑛𝑡 𝑛𝑢𝑚_𝑠𝑡𝑟𝑖𝑛𝑔;
𝑑𝑒𝑓𝑖𝑛𝑖𝑐𝑗𝑎
𝑖𝑛𝑡 𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑 ∷ 𝑛𝑢𝑚_𝑠𝑡𝑟𝑖𝑛𝑔 = 0;
Wyjątkiem od reguły unikania inicjalizacji statycznej składowej w obrębie deklaracji klasy jest
przypadek kiedy składowa ta jest opatrzona modyfikatorem const i jest typu całkowitoliczbowego lub
wyliczeniowego.
UWAGA
Statyczna składowa danych jest deklarowana we wnętrzu deklaracji klasy i inicjalizowana w pliku
implementacji metod klasy. W wyrażeniu inicjalizacyjnym uczestniczy operator zasięgu kojarzący
składową z klasą, do której ta należy. Wyjątkiem są statyczne składowe typu const typu
całkowitoliczbowego bądź wyliczeniowego, które mogą być inicjalizowane wprost w deklaracji klasy.
Ważne jest też by w omawianym przykładzie w metodzie konstruktora mieć świadomość, że zapis:
𝑠𝑡𝑟 = 𝑠;
Był by błędny. Powyższe przypisanie jednie zapisuje w składowej str adres ciągu przekazanego do
konstruktora, nie wykonuje jednak kopii ciągu na użytek obiektu. Należy sobie uzmysłowić, że właściwy
85
ciąg nie jest przechowywany we wnętrzu obiektu, a w obszarze pamięci sterty. Obiekt zaś przechowuje
jedynie informację o położeniu tego obszaru.
Bardzo ważny w tym przykładzie jest destruktor. Rozpoczyna on działanie od ogłoszenia tego, że został
wywołany – przydatne tylko do celów diagnostycznych. Istotne jest jednak, że wywołanie operatora
delete w nim następuje. Wiemy, że składwa str wskazuje pamięć przydzieloną w konstruktorze
wywołaniem new. Kiedy obiekt klasy StringBad jest usuwany, znika również wskaźnik str, ale pamięć
wskazywana uprzednio przez str pozostaje przydzielona do momentu wywołania operatora delete w
celu jej zwolnienia. Usunięcie obiektu zwalnia pamięć zajmowaną przez obiekt jako taki, nie zwalnia
jednak automatycznie pamięci przydzielonej dynamicznie i wskazywanej przez składowe obiektu
(przydzielonej właśnie wspomnianym powyżej operatorem new). Do jej zwolnienia służy destruktor.
Umieszczając w nim wywołanie operatora delete, gwarantujemy uzupełnienie procesu zwalniania
pamięci obiektu o zwolnienie pamięci przydzielonej w jego konstruktorze wywołaniem new.
WAŻNE
Zawsze kiedy w konstruktorze dowolnej klasy stosuje się wywołanie operatora new, nie wolno
zapomnieć o uzupełnieniu destruktora tej klasy o wywołanie delete. Jeśli zaś konstruktor przydziela
pamięć wywołaniem new[], w destruktorze powinno znaleźć się odpowiadające mu wywołanie
delete[].
𝑐𝑎𝑙𝑙𝑚𝑒2(ℎ𝑒𝑎𝑑𝑙𝑖𝑛𝑒2);
Tutaj w wywołaniu funkcji callme2() występuje obiekt headline2 przekazywany nie przez
referencje ale przez wartość co prowadzi do poważnych problemów. Kluczem do zagadki jest
zliczanie realizowane za pomocą static int. Każdy obiekt jest na pewno konstruowany i
usuwany tylko raz, więc liczba wywołań konstruktorów powinna być identyczna z liczbą
wywołań destruktorów. Skoro składowa num_strings została zmniejszona więcej razy niż
zwiększona (o dwa), musiało dojść do wywołania konstruktora, który nie zwiększyl wartości
num_strings. W deklaracji klasy mamy deklaracje i definicje dwóch konstruktorów, ale one
oba zwiększają odpowiednio wartość num_strings. Wychodzi więc na to, że program wywołuje
jeszcze jakiś konstruktor. Weźmy np wiersz:
𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑 𝑠𝑎𝑖𝑙𝑜𝑟 = 𝑠𝑝𝑜𝑟𝑡𝑠;
Który konstruktor jest tu wywoływany? Taka inicjalizacja jest równoważna poniższej:
86
Specjalne metody klasy
Problemy z zachowaniem klasy StringBad wynikają z obecności dodatkowych, specjalnych
metod klasy. Są to metody definiowane automatycznie. W przypadku klasy StringBad
zachowanie tych automatycznych wersji metod specjalnych okazuje sie słabo dopasowane do
specyfiki klasy. W szczególności kompilator c++ automatycznie generauje poniższe metody
klasy:
Konsturktor domyślny (o ile nie zdefiniowano żadnych konstruktorów),
Operator przypisania (jeśli nie został zdefiniowany jawnie),
Konstruktor kopiujący (o ile nie został zdefiniowany jawnie),
Domyślny destruktor (o ile nie został zdefiniowany),
Operator pobierania adresu (o ile nie został zdefiniowany jawnie).
Konstruktory domyślne
Załóżmy np., że zdefiniujemy klasę Klunk bez żadnych konstruktorów. Kompilator wygeneruje
więc domyślną wersję. Taki konstruktor nie przyjmie żadnych argumentów i nie wykona
żadnych operacji. Jest on jednak konieczny, bo tworzenie obiektu zawsze implikuje wywołanie
konstruktora:
𝐾𝑙𝑢𝑛𝑘 𝑘𝑙𝑢𝑛𝑘; (𝑤𝑦𝑤𝑜ł𝑎𝑛𝑖𝑒 𝑘𝑜𝑛𝑠𝑡𝑟𝑢𝑘𝑡𝑜𝑟𝑎 𝑑𝑜𝑚𝑦ś𝑙𝑛𝑒𝑔𝑜)
Po zdefiniowaniu dowolnego konstruktora klasy kompilator wstrzymuje sie od generowania
konstruktora domyślnego. Możemy stworzyć dwa rodzaje konstruktorów domyślnych:
Nie przyjmujący żadnych argumentów, ale nie znaczy to, że nie można w nim ustawiać
wartości składowych:
𝐾𝑙𝑢𝑛𝑘 ∷ 𝑘𝑙𝑢𝑛𝑘(){
𝑘𝑙𝑢𝑛𝑘_𝑐𝑡 = 0; }
Konstruktor przyjmujący argumenty, o ile dla kązdego z tych argumentów zostanie
zdefiniowana wartość domyślna.
𝐾𝑙𝑢𝑛𝑘(𝑖𝑛𝑡 𝑛 = 0){𝑘𝑙𝑢𝑛𝑘𝑐𝑡 = 𝑛; }
Klasa może jednak mieć najwyżej jeden konsturktor domyślny. Dlaczego? (str 563-564 lub w
rozdziale o tym – wcześniej!)
Konstruktory kopiujące
Konstruktor kopiujący wykorzystywany jest do kopiowania obiektu do nowo utworzonego
obiektu tej samej klasy. Jest więc stosowany wyłącznie podczas inicjalizacji (z przypadkiem
przekazania obiektu do funkcji przez wartość), a nie podczas zwykłego przypisania.
Konstruktor kopiujący ma zwykle następujący prototyp:
𝑛𝑎𝑧𝑤𝑎 − 𝑘𝑙𝑎𝑠𝑦(𝑐𝑜𝑛𝑠𝑡 𝑛𝑎𝑧𝑤𝑎 − 𝑘𝑙𝑎𝑠𝑦 &);
87
Kiedy działa konstruktor kopiujący?
Wywoływany jest zawsze wtedy kiedy tworzony jest nowy obiekt, a jego utworzeniu
towarzyszy inicjalizacja innym obiektem tej samej klasy. Wywołania te następują w kilku
przypadkach. Najczęściej mamy do czynienia z jawną inicjalizacja nowego obiektu innym
istniejącym obiektem. Na przykład jeśli obiekt motto jest obiektem klasy StringBad poniższe
cztery deklaracje sprowokują wywołanie konstruktora kopiującego:
𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑 𝑑𝑖𝑡𝑡𝑜(𝑚𝑜𝑡𝑡𝑜);
𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑 𝑚𝑒𝑡𝑜𝑜 = 𝑚𝑜𝑡𝑡𝑜;
𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑 𝑎𝑙𝑠𝑜 = 𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑(𝑚𝑜𝑡𝑡𝑜);
𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑 ∗ 𝑝𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑 = 𝑛𝑒𝑤 𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑(𝑚𝑜𝑡𝑡𝑜);
Mniej oczywiste jest zastosowanie konstruktora kopiującego zawsze wtedy kiedy program
generuje kopie obiektu. W szczególności następuje to przy przekazywaniu obiektu do funkcji,
jeśli przekazanie ma miejsce przez wartość (jak w wywołaniu funkcji callme2()) albo przy
zwracaniu obiektu z funkcji. Trzeba pamiętać, że przekazywanie przez wartość oznacza
utworzenie kopii pierwotnego obiektu. Kompilator ponadto korzysta z konstruktora
kopiującego również wtedy, kiedy generuje obiekty tymczasowe. Kompilator może na
przykład wygenerować obiekt tymczasowy klasy Vector celem przechowania w nim wartości
pośredniej przy dodawaniu dwóch obiektów tej klasy. Kompilatory mają co do tworzenia
obiektów tymczasowych pewną swobodę, wszystkie jednak jak jeden mąż wywołują
konstruktor kopiujący przy przekazywaniu obiektów do funkcji (przez wartość) i zwracaniu
obiektów z funkcji. Nawiasem mówiąc, to, że przekazanie obiektu przez wartość angażuje
wywołanie kostruktora kopiującego jest dodatkowym argumentem za stosowaniem
przekazywania przez referencję. Pozwala to zaoszczędzić czas potrzebny na wykonanie
konstruktora kopiującego i miejsce w pamięci, które zostaje przydzielone tymczasowemu
obiektowi.
Jak działa domyślny konstruktor kopiujący?
Domyślny konstruktor kopiujący (ten generowany automatycznie przez kompilator) realizuje
proste kopiowanie wartości odpowiednich składowych (kopia taka określana jest niekiedy
mianem płytkiej kopii). Każda składowa jest kopiowana przez wartość. Instrukcja:
88
num_strings, pozostają nietknięte, ponieważ należą nie tyle do poszczególnych obiektów, ile
do klasy jako takiej. Działanie niejawnego konstruktora kopiującego ilustruje rysunek:
89
Drugi dziwny efekt wykonania programu jest bardziej subtelny i bardziej niebezpieczny.
Przyczyna choroby programu tkwi w tym, że niejawny konsturktor kopiujący kopiuje obiekty
przez wartość. Weźmy choćby instrukcję inicjalizacji obiektu sailor z listingu 12.3. Wiemy już,
że w ramach tej inicjalizacji wykonywana jest instrukcja:
𝑠𝑎𝑖𝑙𝑜𝑟. 𝑠𝑡𝑟 = 𝑠𝑝𝑜𝑟𝑡𝑠. 𝑠𝑡𝑟;
Wbrew pozorom nie mamy tu do czynienia z kopiowaniem ciągu znaków, a jedynie z
wykonaniem kopii wskaźnika tego ciągu (widać to na rysunku powyżej – 12.2). Oznacza to, że
kiedy obiekt sailor zostanie zainicjalizowany obiektem sports, w pgroamie będziemy mieli
jeden ciąg wskazywany przez dwa obiekty. Nie jest to problem dla wywołania operatora
operator<<() wyświetlającego ciągi przechowywane przez obiekty. Staje się jednak
problemem, kiedy wywołany zostaje destruktor jednego ze współużytkujących ciąg obiektów.
Jak pamiętamy, destruktor klasy StringBad zwalnia pamięć wskazywaną przez składową str
usuwanego obiektu. Daje to efekt równoważny instrukcji:
90
Konieczność zdefiniowania własnej wersji konstruktora kopiującego wynika stąd, że niektóre
ze składowych klasy są inicjalizowane wywołaniem new wskaźnikami właściwych danych, a nie
samymi danymi. Przebieg wykonywania głębokiej kopii obiektów StringBad ilustruje rysunek:
Ostrzeżenie!
Jeśli klasa zawiera skłądowe będące wskaźnikami i icjalizuje te składowe wywołaniem
operatora new, to powinno się dla takiej klasy zdefiniować konstruktor kopiujący, który
nie będzie ograniczać się do kopiowania wartości wskaźników, ale będzie wykonywał
kopię danych wskazywanych, czyli tzw. głęboką kopię obiektu. Alternatywna
implementacja kopiowania (kopiowanie płytkie) kopiuje jedynie wartości wskaźników,
nie próbując nawet wykonać rzeczywistej kopii wszystkich danych obiektu.
Kolejne słabości StringBad: operatorzy przypisania
Nie wszystkie problemy programu z listingu 12.3 można zrzucić na domyślną implementację
konstruktora kopiującego – winny jest też operator przypisania. Tak jak ANSI C pozwala na
przypisanie pomiędzy sobą egzemplarzy struktur tego samego typu, tak standard c++ pozwala
na przypisywanie do siebie obiektów klas. Operacja ta jest realizowana przez automatyczną
implementację przeciążonego dla klas operatora przypisania. Operator ten ma następujący
prototyp:
𝑛𝑎𝑧𝑤𝑎 − 𝑘𝑙𝑎𝑠𝑦 & 𝑛𝑎𝑧𝑤𝑎 − 𝑘𝑙𝑎𝑠𝑦 ∷ 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 = (𝑐𝑜𝑛𝑠𝑡 𝑛𝑎𝑧𝑤𝑎 − 𝑘𝑙𝑎𝑠𝑦 &);
Jak widać, metoda przeciążonego dla klasy operatora przypisania przyjmuje w wywołaniu i
zwraca referencję obiektu tejże klasy. Przykładowy prototyp wygląda tak:
𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑 & 𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑 ∷ 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 = (𝑐𝑜𝑛𝑠𝑡 𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑 &);
91
Kiedy i do czego stosowany jest operator przypisania?
Przeciążony dla klasy operator przypisania wywoływany jest zawsze wtedy kiedy w programie
występuje przypisanie jednego obiektu do innego istniejącego już (nie dopiero
inicjalizowanego!) obiektu tej klasy:
𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑 ℎ𝑒𝑎𝑑𝑙𝑖𝑛𝑒1 ("𝑆𝑝𝑖𝑒𝑤 𝑠𝑘𝑜𝑤𝑟𝑜𝑛𝑘𝑎");
𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑 𝑘𝑛𝑜𝑡;
𝑘𝑛𝑜𝑡 = ℎ𝑒𝑎𝑑𝑙𝑖𝑛𝑒1; (𝑤𝑦𝑤𝑜ł𝑎𝑛𝑖𝑒 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟𝑎 𝑝𝑟𝑧𝑦𝑝𝑖𝑠𝑎𝑛𝑖𝑎)
Operator przypisania nie jest obowiązkowo wywoływany przy inicjalizacji obiektu innym
obiektem:
𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑 𝑚𝑒𝑡𝑜𝑜 = 𝑘𝑛𝑜𝑡;
Obiekt metoo jest tu nowo tworzonym obiektem klasy StringBad inicjalizowanym wartością
obiektu knot, stąd zastosowanie konstruktora kopiującego. Jednak kompilatory mogą działać
jak chcą, więc w niektórych z nich inicjalizacja obiektu innym obiektem może być realizowana
tak, że najpierw konstruktor kopiujący tworzy obiekt tymczasowy, a potem operator
przypisania kopiuje wartości obiektu tymczasowego do obiektu nowo tworzonego. W
inicjalizacji obiektu innym obiektem zawsze dochodzi do wywołania konstruktora kopiującego,
niekiedy uzupełnionego wywołaniem operatora przypisania. Podobnie jak konsturktor
kopiujący, operator przypisania w wersji definiowanej niejawnie przez kompilator realizuje
proste, powierzchowne kopiowanie wartości składowych pomiędzy obiektami. Jeśli któraś, ze
składowych jest obiektem klasy, do wykonania jej kopii wywoływany jest operator przypisania
zdefiniowany dla tej klasy. Statyczne składowe klasy pozostają w wyniku przypisania
niezmienione.
Niepoprawnośc przypisania w StringBad
Program z listringu 12.3 przypisuje obiekt headline1 do obiektu knot:
𝑘𝑛𝑜𝑡 = ℎ𝑒𝑎𝑑𝑙𝑖𝑛𝑒1;
Kiedy potem następuje wywołanie destruktora dla obiektu knot program działa dobrze, jednak
później wywoływany jest destruktor dla obiektu headline1 i wtedy kompilator działa już
błędnie. Mamy tu do czynienia z tym samym problemem co w przypadku niejawnej, domyślnej
wersji konstruktora kopiującego. Przyczyną błędów znów jest powierzchowne kopiowanie
składowych w wyniku czego składowe knot.str i headline1.str wskazują nie taki sam, ale ten
sam ciąg znaków. Kiedy następuje wywołanie destruktora na rzecz obiektu knot, zawarte w
nim wywołanie operatora delete zwalnia pamięć ciągu, a potem w destruktorze obiektu
headline1 następuje próba ponownego zwolnienia tej pamięci. Wiemy już, że efekt operacji
zwolnienia zwolnionej już wcześniej pamięci jest nie do przewidzenia.
Jak poprawić operator przypisania?
Rozwiązanie problemów wynikających z niewłaściwej implementacji domyślnego operatora
przypisania polega na jawnym zdefiniowaniu własnej wersji, poprawnie realizującej
92
kopiowanie (głębokie) składowych obiektów. Implementacja jest zupełnie podobna do
konstruktora kopiującego, z pewnymi tylko różnicami:
Ponieważ obiekt docelowy może być już w pełni zainicjalizowanym obiektem klasy
StringBad i wskazywać jakiś inny ciąg, trzeba ów ciąg zwolnić wywołaniem operatora
delete[]
Funkcja operatora przypisania powinna zabezpieczać przez wykonywaniem
przypisania obiektu do samego siebie – w przeciwnym przypadku zwolnienie pamięci
zalecane w poprzednim punkcje spowoduje utratę danych obiektu
Funkcja operatora przypisania powinna zwracac referencję obiektu wywołującego
(tzn. Tego, na rzecz którego wywołano operator przypisania)
Zwracając obiekt wywołujący, funkcja operatora może emulować kaskadowe wywołania
operatora przypisania, które niekiedy wykorzystuje się dla typów wbudowanych. W takim
układzie można (dla S0, S1, S2 będących obiektami klasy StringBad) konstruować instrukcje:
𝑆0 = 𝑆1 + 𝑆2;
W notacji z jawnym wywołaniem funkcji operatora odpowiada to wyrażeniu:
Jak widać, wartość zwracana z wywołania S1.operator=(S2) staje się argumentem wywołania
funkcji S0.operator=(). Ponieważ wartośc zwracana z operatora przypisania to obiekt klasy
StringBad, nie ma mowy o niedopasowaniu typów argumentów.
Oto jak można zdefiniować operator przypisania dla klasy StringBad:
W pierwszej koleności kod operatora wykrywa próby przypisania obiektu do samego siebie.
Kontrola polega na porównaniu adresu obiektu występującego w roli prawego operandu (&st)
z adresem obiektu docelowego przypisania (this). Jeśli adresy są identyczne, operator zwraca
obiekt wywołujący. Z rozdziału 10. Pamiętamy, że operator przypisania to jeden z tych
operatorów, które daje się przeciążać jedynie za pośrednictwem metody klasy. Po
wyeliminowaniu ryzyka autoprzypisania funkcja operatora zwalnia pamięć wskazywaną
dotychczas przez składową str. Trzeba to zrobić, bo zaraz potem str zostanie ustawiona na
adres nowego ciągu. Jeśli nie zwolnimy wcześniej pamięci poprzedniego ciągu wywołaniem
operatora delete, ciąg ten pozostanie w pamięci, a skoro nie będziemy już znać jego adresu,
nie będzie go można zwolnić – dojdzie do wycieku pamięci. Następnie program podejmuje
operacje typowe dla konsturktora kopiującego, a więc przydziela pamięć dla nowego ciągu, a
93
potem kopiuje ciąg obiektu źródłowego (tego z prawej storny operatora przypisania) do nowo
przydzielonego obszaru pamięci. Po zakończeniu kopiowania operator zwraca *this i kończy
działanie. Operacja przypisania nie tworzy nowych obiektów ani nie usuwa istniejących, nie
trzeba więc aktualizować wartości statycznej składowej num_strings. Uzupełnienie klasy
StringBad o zaprezentowany tu operator przypisania i konstruktor kopiujący eliminuje
podstawowe wady programu z listringu 12.3.
Porównywanie ciągów
Przeciążenie operatorów relacji za pośrednictwem funkcji zaprzyjaźnionych z klasą pozwala na
realizowanie porównań nie tylko pomiędzy dwoma obiektami klasy String, ale również na
stosowanie operatorów relacji w kombinacjach obiektów String ze zwykłymi ciągami języka C.
(przykład str 573)
Indeksowanie ciągu
Załóżmy, że opera to ciąg klasy String:
𝑆𝑡𝑟𝑖𝑛𝑔 𝑜𝑝𝑒𝑟𝑎(Czarodziejski flet);
Jeśli zastosujemy w kodzie programu wyrażenie opera[4], kompilator przeszuka deklarację
klasy pod kątem metody o poniższej nazwie i sygnaturze:
𝑆𝑡𝑟𝑖𝑛𝑔 ∷ 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟[ ](𝑖𝑛𝑡 𝑖)
Jeśli znajdzie w klasie odpowiedni prototyp, zastąpi wyrażenie opera[4] poniższym
wywołaniem:
95
inicjalizowały wskaźnik wywołaniem operatora new, a inne przypisywały do wskaźnika
wskazanie puste (0 albo nullptr w c++11) – dla wskaźnika pustego dozwolone jest
bowiem wywołanie operatora delete (w wersji bez nawiasów prostokątnych)
Należy zdefiniować konstruktor kopiujący inicjalizujący obiekt innym obiektem danej
klasy, a konkretnie głęboką kopią tego ostatniego. (str 582 – przyklad) W szczególności
konstruktor kopiujący powinien przydzielać pamięć potrzebną do kopii danych i
wykonać tę kopie, a nie ograniczać się do skopiowania wartości wskaźnika danych.
Konstruktor kopiujący powinien też odpowiednio aktualizować ewentualne składowe
statyczne, których wartość ma związek z wykonywaną operacją kopiowania
Należy zdefiniować operator przypisania, który realizuje przypisanie za pośrednictwem
operacji wykonania głębokiej kopii obiektu źródłowego. (str 582 – przykład) W
szczególności metoda przeciążająca operator przypisania powinna wykrywać próbę
przypisania obiektu do samego siebie. Powinna też zwalniać pamięć przydzieloną do
składowych wskazujących poprzednie dane obiektu – operator taki powinien
następnie skopiować dane, a nie tylko ich adresy, powinien też zwracać referencję
obiektu wywołującego.
Zalecenia i przestrogi
Str 582-583
Nasuwają sie tu trzy uwagi: po pierwsze, jak pamiętamy, zwracanie obiektu z funkcji powoduje
wywołanie kosntruktora kopiującego obiektu – przy zwracaniu referencji nie ma takiej
konieczności, z tego powodu wersja 2., jako zwolniona z konieczności tworzenia obiektów
96
tymczasowych, jest wersja efektywniejszą. Po drugie zwracana referencja powinna odnosić się
do obiektu istniejącego również (?) poza zasięgiem funkcji (czy chodzi oto by argumentem był
obiekt który jest z poza funkcji???). W tym przykładzie zwracana referencja odnosi się albo do
force1 albo do force2, a oba są przecież właśnie obiektami przekazanymi w wywołaniu spoza
funkcji. Po trzecie v1 i v2 są deklarowane jako referencje const, więc i typ wartości zwracanej
musi mieć modyfikator const.
𝑠3 = 𝑠2 = 𝑠1;
W powyższym przykładzie do obiektu s3 przypisywana jest wartość zwracana z wywołania
s2.operator(s1). Działać będzie tu zarówno zwrócenie obeiktu klasy String (przez wartość), jak
i referencji takiego obiektu, jednak tak jak w przykładzie z klasą Vector, zastosowanie
referencji zwiększy wydajność realizacji wywołań przez wyeliminowanie konieczności
konstruowania obiektów tymczasowych. W prezentowanym przypadku typ wartości
zwracanej nie obejmuje modyfikatora const, ponieważ metoda operator=() zwraca referencję
obiektu s2, który to obiekt modyfikuje (bo następuje nadpisanie).
Wartośc zwracana operatora operator<<() jest z kolei wykorzystywana w kaskadowych
operacjach wyjścia:
𝑐𝑜𝑢𝑡 ≪ 𝑠1 ≪ ", 𝑠𝑚𝑎𝑐𝑧𝑛𝑒𝑔𝑜! ";
Tutaj wartość zwracana z wywołania operator<<(cout,s1) jest obiektem klasy ostream
wykorzystywanym w następnym wywołaniu, wyświetlającym za pośrednictwem tego obiektu
napis „, smacznego!”. Stąd typ ten musi być określony jako ostream&, a nie po prostu ostream.
Zastosowanie ostream (bez referencji) wymagałoby przecież wywołania konstruktora
kopiującego obiektu cout, a okazuje się, że klasa ostream nie definiuje publicznego
konstruktora kopiującego. Na szczęście zwracanie przez referencje nie powoduje żadnych
problemów, bo obiekt cout istnieje również poza funkcją operatora operator<<() (jest w końcu
przekazywany doń w jego wywołaniu).
Wskaźniki obiektów
Przykład str 587 – 589
Inicjalizacja obiektu z użyciem operatora new
Mając klasę nazwa-klasy i wartość typu nazwa-typu, to instrukcja:
𝑛𝑎𝑧𝑤𝑎 − 𝑘𝑙𝑎𝑠𝑦 ∗ 𝑝𝑐𝑙𝑎𝑠𝑠 = 𝑛𝑒𝑤 𝑛𝑎𝑧𝑤𝑎 − 𝑡𝑦𝑝𝑢(𝑤𝑎𝑟𝑡𝑜ść);
Prowoduje wywołanie konstruktora:
99
znaków (wskazywanego przez str) – to zadanie wykonuje jego wywołany automatycznie (tu
przed zwolnieniem obiektu) destruktor – pokazuje to rys poniżej:
𝑆𝑡𝑟𝑖𝑛𝑔 ∗ 𝑔𝑙𝑎𝑚𝑜𝑢𝑟;
Wskaźnik można inicjalizować adresem już istniejącego obiektu:
𝑆𝑡𝑟𝑖𝑛𝑔 ∗ 𝑓𝑖𝑟𝑠𝑡 = &𝑠𝑎𝑦𝑖𝑛𝑔𝑠[0];
Wskaźnik można inicjalizować wywołaniem new, przydzielającym pamięć dla nowego
obiektu i wywołującego jego konstruktor:
100
Wywołanie operatora new z nazwą klasy wywołuje stosowany konstruktor klasy
inicjalizujący nowo tworzony obiekt:
𝑆𝑡𝑟𝑖𝑛𝑔 ∗ 𝑔𝑙𝑒𝑒𝑝 = 𝑛𝑒𝑤 𝑆𝑡𝑟𝑖𝑛𝑔; (𝑘𝑜𝑛𝑠𝑡𝑢𝑟𝑘𝑡𝑜𝑟 𝑑𝑜𝑚𝑦ś𝑙𝑛𝑦)
𝑆𝑡𝑟𝑖𝑛𝑔 ∗ 𝑔𝑙𝑜𝑝 = 𝑛𝑒𝑤 𝑆𝑡𝑟𝑖𝑛𝑔("𝑜𝑗 𝑜𝑗"); (𝑘𝑜𝑛𝑠𝑡𝑟𝑢𝑘𝑡𝑜𝑟 𝑆𝑡𝑟𝑖𝑛𝑔(𝑐𝑜𝑛𝑠𝑡 𝑐ℎ𝑎𝑟 ∗)
𝑆𝑡𝑟𝑖𝑛𝑔 𝑓𝑎𝑣𝑜𝑟𝑖𝑡𝑒 = 𝑛𝑒𝑤 𝑆𝑡𝑟𝑖𝑛𝑔(𝑠𝑎𝑦𝑖𝑛𝑔[𝑐ℎ𝑜𝑖𝑐𝑒]); (𝑘𝑜𝑛𝑠𝑡𝑟𝑢𝑘𝑡𝑜𝑟 𝑆𝑡𝑟𝑖𝑛𝑔(𝑐𝑜𝑛𝑠𝑡 𝑆𝑡𝑟𝑖𝑛𝑔&)
Metody obiektu zadanego wskaźnikiem można wywoływać za pośrednictwem
operatora ->:
Dla wartości obiektu wskazywanego (czyli do obiektu jako takiego) można się odwołać
za pośrednictwem operatora wyłuskania (*):
𝑖𝑓(𝑠𝑎𝑦𝑖𝑛𝑔𝑠[𝑖] <∗ 𝑓𝑖𝑟𝑠𝑡) (𝑝𝑜𝑟ó𝑤𝑛𝑎𝑛𝑖𝑒 𝑤𝑎𝑟𝑡𝑜ś𝑐𝑖 𝑜𝑏𝑖𝑒𝑘𝑡ó𝑤)
𝑓𝑖𝑟𝑠𝑡 = &𝑠𝑎𝑦𝑖𝑛𝑔𝑠[𝑖]; (𝑘𝑜𝑝𝑖𝑜𝑤𝑎𝑛𝑖𝑒 𝑎𝑑𝑟𝑒𝑠𝑢 𝑑𝑜 𝑤𝑠𝑘𝑎ź𝑛𝑖𝑘𝑎)
101
Jeszcze o miejscowej wersji new
Operator new pozwala na samodzielne wskazanie obszaru pamięci, w którym ma nastąpić
przydział realizowany wywołaniem operatora (rozdział 9). Przykład jak wygląda to w obiektach
jest na str 592-593. Zarządzanie miejscem wykorzystanej pamięci wiąrze się z problemami:
Tworzenie drugiego z obiektów przydzielanego miejscowo wersją new zamazuje obiekt
istniejący w tej pamięci wcześniej. Oznacza to, że dla owego zamazanego obiektu nigdy
nie doczekamy się wywołania destruktora. Może dojść do wycieku pamięci gdyby
obiekt korzystał z pamięci przydzielanej dynamicznie. Wywołanie delete [ ] dla bufora
(wskazywanego miejsca w pamięci) nie powoduje automatyznego wywołania
destruktorów obiektów przydzielanych w tym bufforze miejscową wersją wywołania
new
Nauka płynie taka, że korzystając z miejscowej wersji new trzeba być odpowiedzialnym za
zarządzanie pamięcią bufora wykorzystywanego do realizacji przydziałów. Aby w takim
buforze przydzielić dwa rozróżnialne obiekty, trzeba zadbać o takie ich rozmieszczenie, aby
jeden nie zamazywał drugiego. Można to np zrobić tak:
𝑝𝑐1 = 𝑛𝑒𝑤(𝑏𝑢𝑓𝑓𝑒𝑟)𝑗𝑢𝑠𝑡𝑡𝑒𝑠𝑡𝑖𝑛𝑔;
𝑝𝑐3 = 𝑛𝑒𝑤(𝑏𝑢𝑓𝑓𝑒𝑟 + 𝑠𝑖𝑧𝑒𝑜𝑓(𝑗𝑢𝑠𝑡𝑡𝑒𝑠𝑡𝑖𝑛𝑔)𝑗𝑢𝑠𝑡𝑡𝑒𝑠𝑡𝑖𝑛𝑔("𝐿𝑒𝑝𝑠𝑧𝑦 𝑝𝑜𝑚𝑦𝑠𝑙", 6);
Uzyskujemy tutaj przesunięcie o odpowiedni rozmiar wskazywanego obiektu klasy justtesting
Musimy sami zadbać o wywołanie destruktorów tych obiektów. Nie da się usunąć
obiektu jak w przypadku sterty (tj. np. delete pc2)
102
Sęk w tym, że delete współpracuje co prawda z wywołaniem new, ale nie z jego wersją
miejscową. Na przykład wskaźnik pc1 nie otrzymał warrtości zwracanej przez new, więc
instrukcja pc1 sprowokuje błąd czasu wykonania. Co do wskaźnika pc1, to ten przechowuje
adres identyczny z adresem bufora-poligonu (buffer). Bufor ten był przydzielany wywołaniem
new [ ], więc powinien być zwalniany nie przez delete, a przez delete [ ]. Tak więc linijka delete
[ ] buffer; zwalnia cały blok pamięci przydzielony wywołaniem operatora new. Zwolnienie to
nie obejmuje jednak wywołań destruktorów dla żadnych obiektów przydzielonych w tym
bloku pamięci wywołaniami miejscowej wersji operatora new. Mamy tu jedną z sytuacji
wyjątkowych, kiedy to destruktor wymaga jawnego wywołania. Jawne wywołanie destruktora
wymaga określenia obiektu, dla którego ma ono nastąpić. Ponieważ obiekty te mamy dane w
postaci wskaźników obiektów, musimy zastosować te wskaźniki:
𝑝𝑐1− > ~𝑗𝑢𝑠𝑡𝑡𝑒𝑠𝑡𝑖𝑛𝑔( );
Ważnym elementem programu przykładowego jest kolejność zwalniania – obiekty
konstruowane miejscową wersją new powinny być usuwane w kolejności odwrotnej do
kolejności ich tworzenia. Chodzi o to, że zasadniczo obiekt utworzony później może w jakimś
zakresie być zależny od obiektów istniejących wcześniej. No i trzeba pamiętać, że bufor, w
którym przydzielane były obiekty, można zwolnić dopiero po zwolnieniu wszystkich tych
obiektów.
𝑝𝑐1− > ~𝑗𝑢𝑠𝑡𝑡𝑒𝑠𝑡𝑖𝑛𝑔( );
𝑝𝑐3− > ~𝑗𝑢𝑠𝑡𝑡𝑒𝑠𝑡𝑖𝑛𝑔( );
𝑑𝑒𝑙𝑒𝑡𝑒 [ ]𝑏𝑢𝑓𝑓𝑒𝑟;
(przykład str 595 – 596)
Funkcje konwersji
Aby skonwertować wartość pewnego typu na obiekt pewnej klasy, należy zdefiniować dla tej
klasy konstruktor o następującym prototypie:
103
𝑛𝑎𝑧𝑤𝑎 − 𝑘𝑙𝑎𝑠𝑦(𝑛𝑎𝑧𝑤𝑎 − 𝑡𝑦𝑝𝑢);
Ciąg nazwa-klasy reprezentuje tu nazwę klasy, której obiekty chcemy otrzymywać w wyniku
konwersji, a nazwa-typu reprezentuje nazwę typu, który ma być poddawany konwersji.
Aby z kolei skonwertować obeikt klasy na wartość pewnego innego typu, trzeba w klasie
zdefiniować metodę o następującym prototypie:
104
Symulacja kolejki
Od strony 598! WAŻNE I PRZYDATNE – OBSZERNY PRZYKŁAD
Zagnieżdżone struktury i klasy
Struktura, klasa, czy typ wyliczeniowy deklarowany we wnętrzu deklaracji klasy noszą miano
zagnieżdżonych w klasie. Cechują się przy tym zasięgiem klasy. Deklaracje takie nie tworzą
obiektów danych, a jedynie określają typ, który potem może być wykorzystywany
wewnętrznie przez klasę. Jeśli deklaracja zagnieżdżonego typu wystąpi w obszarze składowych
prywatnych klasy, to zadeklarowany tak typ może być wykorzystywany jedynie w obrębie
klasy; jeśli deklaracja trafi do obszaru publicznego, typ będzie mógł być używany również poza
klasą, ale jedynie za pośrednictwem operatora zasięgu. Gdyby na przykład typ Node został w
klasie Queue zadeklarowany za etykietą public, to można by w programie również poza klasą
definiować zmienne typu Queue::Node.
Lista inicjalizacyjna
Konstruktor kiedy zaczyna się wykonywanie kodu jego ciała, dysponuje już utworzonym
obiektem. Wywołanie konstruktora realizowane jest najpierw przydziałem pamięci dla
wszystkich składowych klasy, a dopiero potem sterowanie jest przekazywane do wnętrza
konstruktora, gdzie mamy już do czynienia nie z inicjalizacją, ale ze zwykłymi przypisaniami
wartości do przydzielonych wcześniej składowych. Jeśli więc chcemy zainicjalizować składową
oznaczoną jako const musimy to zrobić w momencie tworzenia obiektu jeszcze zanim
sterowanie zostanie przekazane do ciała konstruktora. Służy do tego lista inicjalizacyjna.
Składa się ona z oddzielanych przecinkami wyrażeń inicjalizujących i jest poprzedzona
dwukropkiem. Umieszcza się ja za nawiasem ograniczającym listę parametrów konstruktora,
ale jeszcze przed nawiasem klamrowym otwierającym ciało konstruktora. Przykład:
𝑄𝑢𝑒𝑢𝑒 ∷ 𝑄𝑢𝑒𝑢𝑒(𝑖𝑛𝑡 𝑞𝑠) ∶ 𝑞𝑠𝑖𝑧𝑒(𝑞𝑠) { (𝑖𝑛𝑖𝑐𝑗𝑎𝑙𝑖𝑧𝑎𝑐𝑗𝑎 𝑒𝑠𝑖𝑧𝑒 𝑤𝑎𝑟𝑡𝑜ś𝑐𝑖ą 𝑞𝑠)
}
105
Można tak inicjalizować każdą wartość, nie musi to być zmienna stała. Listę inicjalizacyjną
można stosować jedynie w konstruktorach. Stosuje się go też do składowych deklarowanych
jako referencje. Te, podobnie jak dane deklarowane z const, mogą być inicjalizowane jedynie
w momencie tworzenia. W przypadku zwykłych składowych klas mamy dowolność, możemy
je inicjalizować zarówno w liście inicjalizacyjnej jak i w ciele konstruktora.
Jeżeli jest taka możliwość, powinno się jednak wszystkie możliwe składowe inicjalizować za
pomocą listy inicjalizacyjnej, ponieważ jest to rozwiązanie szybsze i wydajniejsze od użycia
konstruktorów.
Składnia listy inicjalizacyjnej:
𝐶𝑙𝑎𝑠𝑠𝑦 ∷ 𝐶𝑙𝑎𝑠𝑠𝑦(𝑖𝑛𝑡 𝑛, 𝑖𝑛𝑡 𝑚) ∶ 𝑚𝑒𝑚1(𝑛), 𝑚𝑒𝑚2(0), 𝑚𝑒𝑚3(𝑛 ∗ 𝑚 + 2){
}
Składowe listy inicjalizacyjnej, są inicjalizowane w kolejności w jakiej występują w deklaracji
klasy, nie zaś w takiej, w jakiej występują wyrażenia inicjalizujące na liście inicjalizacyjnej
konstruktora.
Inicjalizacja składowych klas w c++ 11
Tu wreszcie możliwy jest oczywisty i intuicyjny zapis (patrz str 604) – wewnątrz ciała
konstruktora.
W c++ istenieje lepszy sposób na rozszerzenie i zmianę możliwości klasy niż modyfikacje jej
kodu - jest nim dziedziczenie, dzięki któremu można dziedziczyć nową klasę po istniejącej,
zwanej klasą bazową. Podobnie jak odziedziczenie fortuny jest łatwiejsze niż jej zarobienie,
tak i utworzenie nowej klasy za pomocą dziedziczenia jest zwykle prostsze niż
zaprojektowanie jej od nowa. Korzyści płynące z dziedziczenia:
106
dziedziczenie pozwala dodawać nowe możliwości do istniejącej klasy. Możemy na
przykład dodać operacje arytmetyczne do podstawowej klasy do obsługi tablic
dziedziczenie pozwala dodawać nowe dane do klasy. Możemy na przykład skorzystać
z podstawowej klasy do obsługi ciągów znaków i utworzyć klasę pochodną ze
składową reprezentującą kolor używany do wyświetlania ciągu
dziedziczenie pozwala zmieniać działanie metod klasy. Możemy na przykład użyć
Pasażer, która reprezentuje usługi świadczone pasażerom linii lotniczych, i utworzyć
klasę pochodną PasażerPierwszejKlasy do przedstawienia usług na wyższym
poziomie.
Oczywiście ten sam cel możemy osiągnąć, kopiując kod klasy bazowej, a następnie
modyfikując go, jednak dziedziczenie pozwala skoncentrować się jedynie na nowych cechach
tworzonej klasy. Aby utworzyć klasę pochodną, nie potrzebujemy nawet dostępu do kodu
źródłowego. Dzięki temu kupno biblioteki klas, w której udostępnione są jedynie pliki
nagłówkowe oraz skomplikowany kod metod klasy także pozwala utworzyć nową klasę za
pomocą dziedziczenia. Możemy też rozpowszechniać własne klasy, ukrywając fragmenty
implementacji, a jednocześnie umożliwiając klientom dodawania nowych cech do
dostarczanych klas.
Dziedziczenie
𝑐𝑙𝑎𝑠𝑠 𝑅𝑎𝑡𝑒𝑑𝑃𝑙𝑎𝑦𝑒𝑟 ∶ 𝑝𝑢𝑏𝑙𝑖𝑐 𝑇𝑎𝑏𝑙𝑒𝑇𝑒𝑛𝑛𝑖𝑠𝑃𝑙𝑎𝑦𝑒𝑟{
}
Dwukropek oznacza, że klasa RatedPlayer dziedziczy po klasie TableTennisPlayer.
W tym konkretnym przypadku deklaracja określa, że klasa TableTennisPlayer jest
publiczną klasą bazową nazywamy to dziedziczeniem publicznym. Obiekt klasy
pochodnej wchłania dzięki temu obiekt klast bazowej. W przypadku dziedziczenia
publicznego składowe i metody publczne klasy bazowej stają się składowymi
publicznymi klasy pochodnej. Z kolei elementy prywatne klasy bazowej stają się
częścią klasy pochodnej, ale dostęp do nich istnieje tylko za pomocą publicznych i
chronionych metod klasy bazowej. Składowe chronione opisane są w dalszej częsci
rozdziału.
Deklarując obiekt RatedPlayer, ma on specjalne właściwości :
obiekt typu pochodnego zawiera w sobie dane składowe typu bazowego.
Klasa pochodna dziedziczy implementację klasy bazowej
obiekt typu pochodnego może używać metod typu bazowego. Klasa pochodna
dziedziczy interfejs klasy bazowe
107
Co trzeba do odziedziczenia cech?:
klasa pochodna potrzebuje własnego konstruktora
w klasie pochodnej można dodać nowe składowe oraz metody
Konstruktory muszą dostarczać danych dla nowych składowych, jeśli takie istnieją, oraz dla
składowych odziedziczonych po klasie bazowej. (str 628). Można to zrobić na dwa sposoby:
używać oddzielnych parametrów dla każdej składowej
korzystać z parametru wp ostaci referencji do klasy bazowej
Fragment:
: 𝑇𝑎𝑏𝑙𝑒𝑇𝑒𝑛𝑛𝑖𝑠𝑃𝑙𝑎𝑦𝑒𝑟(𝑓𝑛, 𝑙𝑛, ℎ𝑡);
108
To właśnie lsita inicjalizacyjna. Jest to kod wykonywalny, który powoduje wywołanie
konsturktora klasy TableTennisPlayer.
Rysunek przedstawia jak to funkcjonuje (opis – str 628-629). Gdy opuścimy liste inicjalizacyjną
Jeśli nie chcemy używać konstruktora domyślnego, musimy jawnie wywołać odpowiedni
konstruktor klasy bazowej.
Jak było pisane wcześniej poza wysyłaniem odpowiednich parametrów dla każdej składowej
możemy też wysłać od razu referencje do klasy bazowej:
Powyższy kod wywołuje konstruktor kopiujący klasy bazowej. Jeśli wystarczy konstruktor
kopiujący płytko to może być to robione niejawnie, jednak jeśli kopia musi być głęboka (w tym
przypadku nie) to trzeba zdefiniować własny konsturktor kopiujący. Liste inicjalizacyjną
możemy też użyć do inicjalizacji składowych klasy pochodnej (w tym przypadku należy używać
nazw składowych zamiast nazwy klasy, bo nie zahaczamy o konstrktor klasy bazowej?):
109
konstruktor klasy pochodnej powinien inicjalizować wszystkie dane składowej, które
zostały dodane do tej klasy
W przpadku destruktorów najpierw wywoływany jest destruktor klasy pochodnej, a później
klasy bazowej (odwrotnie do kolejności tworzenia obiektów).
Możliwość wskazywania przez wskaźniki klasy bazowej na obiekty klasy pochodnej bez
konieczności jawnego rzutowania typu
Możliwość odnoszenia się za pomocą referencji klasy bazowej do obiektu klasy
pochodnej, bez konieczności jawnego rzutowania typu.
Jednak:
wskaźnik lub referencja klasy bazowej mogą wywoływać jedynie metody klasy
bazowej, nie można więc użyć rt lub pt do wywołania metody klasy pochodnej.
Nie można przypisać obiektu ani adresu klasy bazowej do referencji ani wskaźnika klasy
pochodnej
Używanie referencji / wskaźnika klasy bazowej do wywołania metod klasy bazowej dla obiektu
klasy pochodnej jest możliwe dzięki temu, że klasa pochodna dziedziczy metody i dane
składowe klasy bazowej. Przypisanie obiektu klasy bazowej do referencji / wskaźnika typu
klasy pochodnej jest nie możliwe ponieważ nie ma to sensu – wywołanie metody klasy
pochodnej na rzecz obiektu klasy bazowej nie może się odbywać bo klasa bazowa nie ma
atrybutów klasy pochodnej (w drugą stronę to działa – dokładny opis str 633).
Po co to? Bo np.:
Pozwala nam na wywoływanie funkcji show() zarówno z argumentem w postaci obiektu klasy
bazowej jak i pochodnej.
110
To samo ze wskaźnikiem.
Możemy dzięki takiemu mechanizmowi również:
Inicjalizować obiekt klasy bazowej za pomocą obiektu klasy pochodnej (choć w nieco
pośredni sposób) – patrz str 634
Przypisać obiekt klasy pochodnej do obiektu klasy bazowej – patrz str 634-635
111
Ponowne definiowanie metody klasy bazowej w klasie pochodnej
Używanie metod wirtualnych
W klasie Brass w deklaracjach metod ViewAcct() oraz Withdraw() użyte jest nowe
słowo kluczowe – virtual. Metody te to metody wirtualne (używane są one za pomocą
wskaźników lub referencji, ponieważ charakteryzują się wiązaniem dynamicznym, a nie
statycznym, o tym dalej troszkę. W każdym razie metody wirtualne muszą być używane
ze wskaźnikami lub referencjami) – czemu referencje niby jeśli nie można zmienić
obiektu na którą działa referencja?
112
Jeśli powtórnie definiujemy metodę klasy bazowej w klasie pochodnej, powinniśmy
zadeklarować metodę klasy bazowej jako wirtualną. Dzięki temu program może wybrać wersję
metody na podstawie typu obiektu zamiast na podstawie typu referencji lub wskaźnika. Jeśli
nie zastosujemy słowa kluczowego virtual, program wybierze metodę na podstawie typu
referencji lub wskaźnika.
W tym przypadku typ obu referencji to Brass, ale b2_ref odnosi się do obiektu BrassPlus, więc
program używa metody BrassPlus::ViewAcct(). Tak samo wygląda to przy korzystaniu ze
wskaźników.
Jeśli metoda jest zadeklarowana jako wirtualna w klasie bazowej, staje się ona automatycznie
wirtualna w klasie pochodnej, niemniej dobrym zwyczajem jest stosowanie tego słowa
kluczowego również w klasach pochodnych. Słowo virtual stosuje się jedynie w prototypach
metod, a nie w ich definicjach.
W klasie Brass zadeklarowany jest także destruktor wirtualny, chociaż nie wykonuje
on żadnych czynności
---------------------------------------------------------------------------------------------------------------------------
Klasa pochodna nie ma bezpośredniego dostępu do danych praywatnych klasy bazowej. Aby
uzyskać w klasie pochodnej dostęp do takich danych, trzeba używać metod publicznych klasy
bazowej. Sposób uzyskiwania tego dostępu zależy od rodzaju metody. Inną technikę
wykorzystuje się w przypadku konstruktorów a inną w przypadku pozostałych metod.
a) Dla konstruktorów inicjalizacja danych prywatnych klasy bazowej odbywa się za
pomocą listy inicjalizacyjnej.
113
Każdy z tych konstruktorów używa listy inicjalizacyjnej do przekazania informacji o
klasie bazowej do jej konstruktora, a następnie w ciele kosntruktora inicjalizuje nowe
dane klasy BrassPlus.
b) Nie można używać listy inicjalizacyjnej w metodach, które nie są konsturktorami. Jeśli
na moment zignorujemy zagadnienia związane z formatowaniem, główna część
metody ViewAcct() klasy BrassPlus będzie wyglądać następująco
Jeśli element tablicy wskazuje na obiekt typu Brass, wywoływana jest metoda
Brass::ViewAcct(). Jeśli element wskazuje na obiekt typu BrassPlus, wywoływana jest metoda
114
BrassPlus::ViewAcct(). Jeśli metoda Brass::ViewAcct() nie została by zadeklarowana jako
wirtualna była by wywoływana we wszystkich przypadkach.
Destruktor wirtualny
Jeśli destruktor nie jest wirtualny to wywoływany jest destruktor typu wskaźnika. W przypadku
destruktora wirtualnego wywoływany jest destruktor typu obiektu, na który wskazuje
wskaźnik. (więcej str. 648)
Jednak jak już wiemy, referencję lub wskaźnik typu klasy bazowej można powiązać z obiektem
klasy pochodnej bez używania jawnego rzutowania typu:
Przekształcenie referencji lub wskaźnika klasy pochodnej na referencję lub wskaźnik klasy
bazowej nazywa się rzutowaniem w górę i można stosować ten mechanizm w dziedziczeniu
publicznym bez konieczności jawnego rzutowania typu (obiekt BrassPlus jest też obiektem
typu Brass). Rzutowanie w górę jest przechodnie (patrz str 649). Rzutowanie w dół jest
niedozwolone i wymaga jawnego rzutowania.
115
Rzutowanie w górę odbywa się również w funkcjach, które jako parametry przyjmują
referencje albo wskaźniki do typu bazowego. Weźmy poniższy fragment kodu (załóżmy, że
każda z definiowanych tam funkcji woła na rzecz przekazanego obiektu jego wirtualną metodę
ViewAcct()).
Przekazywanie obiektu przez wartość powoduje, że z obiektu klasy BrassPlus do funkcji fv()
trafia tylko podobiekt klasy bazowej Brass. Ale w przypadku wskaźników i referencji w
momencie wywołania metody dochodzi do niejawnego rzutowania w górę, a więc w
metodach fr() i fp() dochodzi do wywołania metody Brass::ViewAcct() dla referencji bądź
wskaźnika do obiektu Brass i wowołania metody BrassPlus::ViewAcct() dla referencji bądź
wskaźnika do obiektu BrassPlus.
Niejawne rzutowanie w górę umożliwia wskaźnikom i referencjom typu klasy bazowej
odnoszenie się do obiektów zarówno klasy bazowej, jak i klasy pochodnej. Powstaje przez to
potrzeba stosowania wiązania dynamicznego. Odpowiedzią języka c++ na tę potrzebę są
metody wirtualne.
116
Wiązanie statyczne jest bardziej wydajne (nie trzeba marnować pamięci na zapamiętywanie
procesów, które muszą być realizowane dynamicznie). Dodatkowo metody powinniśmy
deklarować jako wirtualne tylko wtedy jeśli spodziewamy się, że mogą być one redefiniowane.
Czyli wirtoalność tylko gdy jest polimorfizm obecny.
Jak działa wiązanie dynamiczne
Mechanizm jest opisany na str 651-652 oraz na rysunku na stronie 652
Używanie funkcji wirtualnych generuje pewne koszty związane z pamięcią i czasem
wykonania. Wynikają one głównie z trzech przyczyn:
Rozmiar każdego obiektu zwiększa się o ilość pamięci potrzebną do zapisania adresu
Dla każdej klasy kompilator musi utworzyć tablicę adresów funkcji wirtualnych
Dla każdego wywołania funkcji potrzebne jest dodatkowe sprawdzenie adresu w
tablicy
117
pochodnej. Istnieje w niej bezpośredni dostęp do składowych chronionych klasy bazowej,
który nie jest możliwy w przypadku składowych prywatnych. Tak więc składowe z poziomu
chronionego zachowują się jak prywatne dla świata zewnętrznego, ale jak publiczne w
przypadku klas pochodnych.
Załóżmy, że składowa balance w klasie Brass jest chroniona:
W tym przypadku w klasie BrassPlus istnieje bezpośredni dostęp do składowej balance bez
konieczności używania publicznych metod klasy Brass. Używanie danych składowych
chronionych może uprościć pisanie kodu, ale wywiera ujemny wpływ na projektowanie. Jeśli
składowa balance jest chroniona, możemy napisać kod:
Klasa Brass zostałą zaprojektowana w taki sposób aby zmiana wartości składowej balance była
możliwa jedynie za pomocą publicznych metod interfejsu – Deposit(), oraz Withdraw(). Z kolei
metoda Reset() w rzeczywistości zmienia składową balance w publiczną, przynajmniej dla
obiektów BrassPlus, ignorując wszelkie zabezpieczenia znajdujące się w metodzie Withdraw().
(patrz str 656-657)
UWAGA:
Zaleca się deklarowanie składowych klas jako prywatnych a nie chronionych. Dostęp do
danych klasy bazowej w klasie pochodnej powinien istnieć poprzez metody publiczne klasy
bazowej. Jednak chroniony poziom dostępu może być całkiem użyteczny w przypadku metod.
Pozwala to udostępnić w klasie pochodnej funkcje wewnętrzne, które nie są dostępne
publiczne.
118
Załóżm, że dziedziczymy klasę Circle po klasie Ellipse.
Chociaż okrąg jest elipsą to dziedziczenie jest dziwaczne. Okrąg potrzebuje tylko jednej
wartości promienia do opisu rozmiaru i kształtu, podczas gdy elipsa potrzebuje dwóch.
Konstruktory klasy Circle mogą sobie z tym poradzić przypisując taką samą wartość do obu
składowych, ale powoduje to nadmiarową reprezentację tej samej informacji. Parametr angle,
oraz metoda Rotate(), nie mają sensu w odniesieniu do okręgu. Metoda Scale() w tej postaci
może zmienić okrąg w inną figurę. Można skorzystać z różnych sztuczek jak np. umieścić
przedefinowaną metodę Rotate() w sekcji prywatnej klasy Circle, aby zapobiec publicznego
używaniu tej metody dla okręgu. Jednak łatwiejsze jest zdefiniowanie klasy Circle bez
używania dziedziczenia.
Teraz klasa ma tylko te składowe, które są rzeczywiście potrzebne jednak takie rozwiązanie
nie jest szczególnie atrakcyjne. Klasy Circle i Ellipse mają wiele wspólnych ech, które są
ignorowane jeśli definiujemy obie klasy osobno.
Istnieje rozwiązanie! Możemy wyabstrachować wspólne właściwości klasy Ellipse oraz Circle i
umieścić je w abstrakcyjnej klasie bazowej. Następnie możemy utworzyć klasy ellipse i circle,
dziedzicząc je po abstrakcyjnej klasie bazowej. Dzięki polimorfizmowi możemy wtedy używać
tablicy wskaźników na klasę bazową do jednoczesnego przechowywania obiektów klas Ellipse
i Circle. W tym przypadku współną cechą obu klas jest współrzędna środka figury, metoda
Move(), taka sama dla obu figur, a także metoda Area(), która jednak działa inaczej w każdej
klasie. W rzeczywistości metody Area() nie można zaimplementować w abstrakcyjnej klasie
bazowej, ponieważ nie ma ona odpowiednich składowych. W języku c++ istnieje sposób na
umieszczenie funkcji bez implementacji w postaci funkcji czysto wirtualnej. Funkcja czysto
119
wirtualna ma wyrażenie = 0 na końcu deklaracji, jak w metodzie Area() w poniższym
przykładzie:
Kiedy deklaracja klasy zawiera funkcję czysto wirtualną, nie możemy tworzyć obiektów tej
klasy. Wynika to stąd, że klasy z funkcjami czysto wirtualnymi są wykorzystywane jedynie jako
klasy bazowe. Aby klasa była prawdziwą klasą czysto abstrakcyjną musi mieć przynajmniej
jedną funkcję czysto wirtualną. Funkcja staje się czysto wirtualna dzięki umieszczeniu w jej
prototypie wyrażenia =0. W przypadku metody Area() funkcja nie ma definicji, ale język c++
zezwala na umieszczenie definicji nawet funkcji czysto wirtualnej. Prawdopodobnie większość
metod klasy bazowej jest podobna do metody Move() w tym aspekcie, że mogą one być
zdefiniowane dla klasy bazowej, ale klasa wciąż musi być abstrakcyjna. Możemy wtedy
utworzyć wirtualny prototyp takiej metody:
𝑣𝑜𝑖𝑑 𝑀𝑜𝑣𝑒(𝑖𝑛𝑡 𝑛𝑥, 𝑛𝑦) = 0;
Dzięki temu klasa bazowa staje się abstrakcyjna. Następnie w pliku implementacji można
udostępnić definicję tej funkcji.:
𝑣𝑜𝑖𝑑 𝐵𝑎𝑠𝑒𝐸𝑙𝑙𝑖𝑝𝑠𝑒 ∷ 𝑀𝑜𝑣𝑒(𝑖𝑛𝑡 𝑛𝑥, 𝑛𝑦){
𝑥 = 𝑛𝑥;
𝑦 = 𝑛𝑦; }
Wyrażenie =0 w prototypie określa, że klasa jest abstrakcyjną klasą bazową i jej funkcje nie
muszę mieć definicji. Teraz możemy utworzyć klasy Ellipse oraz Circle dziedziczące po klasie
BaseEllipse i dodać do każdej z nich potrzebne składowe. Zauważmy, że klasa Circle zawsze
reprezentuje okręgi, podczas gdy klasa Ellipse reprezentuje elipsy, które mogą być okręgami.
Jednak w klasie Ellipse okrąg może zostać przekształcony w figurę, która nie jest okręgiem,
podczas gdy w klasie Circle okreąg musi zachować swój kształt. W tym programie można
tworzyć obiekty klasy Ellipse, oraz klasy Circle, ale już nie obiekty klasy BaseEllipse. Ponieważ
obiekty Ellipse i Circle mają wspólną klasę bazową, kolejką takich obiektów można zarządzać
za pomocą tablicy wskaźników typu BaseEllipse. Klasy takie jak Circle lub Ellipse są czasem
nazywane klasami konkretnymi, aby podkreślić, że można tworzyć obiekty takiego typu.
Abstrakcyjne klasy bazowe opisują interfejs, który używa przynajmniej jednej funkcji czysto
wirtualnej, a klasy od nich pochodne używają funkcji wirtuanych do implementajci tego
interfejsu w specyficzny dla danej klasy sposób. (dokładny opis – 657-659)
Przykład str 659-664
120
Filozofia abstrakcyjnych klas bazowych
Przed zaprojektowaniem abstrakcyjnej klasy bazowej trzeba utworzyć model zawierający klasy
potrzebne do reprezentacji problemu oraz relacji między nimi. Wg założeń jednej ze szkół w
projekcie hierarchii klas jedynymi klasami konkretnymi powinny być te, które nie służa jako
klasy bazowe. Takie podejście wiąże się z tworzeniem bardziej przejrzystych i mniej
skomplikowanych projektów. Abstrakcyjna klasa bazowa wymaga przesłonięcia swoich funkcji
czysto wirtualnych w każdej konkretnej klasie pochodnej, wymuszając przez to przestrzeganie
reguł interfejsu ustalonych w abstrakcyjnej klasie bazowej.
121
To samo tyczy się przypisania. Domyślny operator przypisania automatycznie używa operatora
przypisania klasy bazowej. Tak więc i w tym przypadku wszystko działa poprawnie.
122
domyślny. Kolejną czynnością jaką wykonuje konstruktor domyślny, jest wywoływanie
konstruktorów domyślnych wsyzstkich klas bazowych i wszystkich składowych będących
obiektami innej klasy. Ponadto jeśli napiszemy konstruktor klasy pochodnej, który nie
wywołuje jawnie konstruktora klasy bazowej na liście inicjalizacyjnej, kompilator użyje
konsturktora domyślnego klasy bazowej do utworzenia danych z klasy bazowej w nowym
obiekcie. Jeśli w klasie bazowej nie ma konsturktora domyślnego, kompilator zgłosi błąd
kompilacji. Jeśli zdefiniujemy jakikolwiek konsturktor, kompilator nie utworzy konsturktora
domyślnego. W tym przypadku sami musimy udostępnić konstruktor domyślny, jeśli jest
potrzebny. Zwróćmy uwagę, że jednym z powodów tworzenia konstruktorów jest zapewnienie
poprawnej inicjalizacji obiektów. Ponadto jeśli klasa ma składowe w postaci wskaźników,
muszą one zostać zainicjalizowane. Dlatego dobrym pomyślem jest udostępnienie jawneog
konstruktora domyślnego, który inicjalizuje wszystkie składowe klasy za pomocą poprawnych
wartości
Konstruktor kopiujący
To konstruktor przyjmujący jako argument obiekt klasy do której należy. Zwykle argumentem
tym jest stała referencja typu klasy. Konstruktor kopiujący klasy Star może mieć prototyp:
𝑆𝑡𝑎𝑟 (𝑐𝑜𝑛𝑠𝑡 𝑆𝑡𝑎𝑟 &);
Konstruktor kopiujący jest używany w następujących sytuacjach:
Kiedy nowy obiekt inicjalizowany jest za pomocą obiektu tej samej klasy
Kiedy obiekt przekazywany jest do funkcji przez wartość
Kiedy funkcja zwraca obiekt przez wartość
Kiedy kopmilator tworzy obiekt tymczasowy
Jeśli program nie używa konstruktora kopiującego (jawnie lub niejawnie) wtedy kompilator
tworzy jego prototyp ale już nie definicję. W przeciwnym razie kompilator generuje
konstruktor kopiujący, który wykonuje płytkie kopiowanie. Oznacza to, że każda składowa
nowego obiektu inicjalizowana jest za pomocą wartości odpowiedniej skłądowej obiektu
oryginalnego. Jeśli składowa jest sama obiektem jakiejś klasy, wtedy jeg inicjalizacja wymusza
zastosowanie konstruktora kopiującego zdefiniowanego dla tejże klasy. W niektórych
przypadkach kopiowanie składowych działa niepoprawnie. Np. składowe w postaci
wskaźników inicjalizowane za pomocą słowa kluczowego new najczęściej wymagają
kopiowania głębokiego, jak w klasie bazowej – baseDMA (patrz str 665). Klasa może także mieć
zmienne statyczne, które wymagają modyfikacji. W takich przypadkach należy zdefiniować
własny konstruktor kopiujący.
Operator przypisania
Domyślny operator przypisania obsługije przypisywanie jednego obiektu danej klasy do
drugiego obiektu tej samej klasy. Nie należy mylić przypisania z inicjalizacją. Jeśli wyrażenie
tworzy nowy obiekt wtedy używa inicjalizacji. Z kolei przypisanie to zmiana wartości
istniejącego obiektu.
123
Domyślny operator przypisania operuje składowa po skłądowej. Jeśli dana składowa jet
obiektem jakiejś klasy, przypisanie wartości odbywa się za pomocą operatora przypisania
zdefiniowanego dla typu klasy. Jeśli potrzebujemy jawnego konstruktora kopiującego,
potrzebujemy też z tych samych powodów jawnego operatora przypisania. Prototyp
operatora przypisania klasy Star wygląda następująco:
𝑆𝑡𝑎𝑟 & 𝑆𝑡𝑎𝑟 ∷ 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 = (𝑐𝑜𝑛𝑠𝑡 𝑆𝑡𝑎𝑟 &);
Zauważmy, że operator przypisania zwraca referencję do obiektu typu Star. Typowym
przykładem stosowania jawnego operatora przypisania jest klasa baseDMA (patrz str 665).
Kompilator nie generuje operatorów przypisania pozwalających przypisywać obiekty jednego
typu do obiektów innego typu. Załóżmy, że chcemy przypisać ciąg znaków do obiektu typu
Star. Jedna możliwość to jawne zdefiniowanie takiego operatora:
𝑆𝑡𝑎𝑟 & 𝑆𝑡𝑎𝑟 ∷ 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 = (𝑐𝑜𝑛𝑠𝑡 𝑐ℎ𝑎𝑟 ∗){ }
Inne podejście to skorzystanie z funkcji konwersji (zobacz w dalszej części – „konwersja”) do
przekształcenia ciągu znaków na obiekt klasy Star, a następnie użycie funkcji przypisującej
obiekt klasy star do obiektu tej samej klasy. Pierwsze rozwiązanie jest szybsze ale wymaga
większej ilości kodu. W niektórych przypadkach kompilator może mieć problemy z funkcjami
konwersji.
Inne metody
Konstruktory
Konstruktory różnią się od innych metod klasy tym, że tworzą nowe obiekty, podczas gdy inne
metody działają na obiektach już istniejących. Jest to jeden z powodów, dla których nie
dziedziczą konstruktorów. Dziedziczenie oznacza, że obiekt klasy pochodnej może używać
metod klasy bazowej jednak w przypadku konstruktorów obiekt nie istnieje do czasu aż
konstruktor zakończy działanie
Destruktory
Musimy pamiętać o zdefiniowaniu jawnego destruktora, który zwalnia pamięc zaalokowaną w
konstruktorze klasy za pomocą słowa kluczowego new a także wykonuje inne istotne
czynności potrzebne w chwili usuwania obiektu. Jeśli klasa ma służyć jako klasa bazowa,
powinniśmy udostępnić destruktor wirtualny nawet jeśli klasa ta nie wymaga destruktora
Konwersja
Każdy konstruktor, który można wywołać z dokładnie jednym argumentem, definiuje
konwersje z typu argumentu na typ klasy. Zastanówmy się nad prototypami konsturktora w
klasie Star:
𝑆𝑡𝑎𝑟(𝑐𝑜𝑛𝑠𝑡 𝑐ℎ𝑎𝑟 ∗); (𝑝𝑟𝑧𝑒𝑘𝑠𝑧𝑡𝑎ł𝑐𝑎 𝑐ℎ𝑎𝑟 ∗ 𝑛𝑎 𝑡𝑦𝑝 𝑆𝑡𝑎𝑟
124
𝑆𝑡𝑎𝑟 (𝑐𝑜𝑛𝑠𝑡 𝑆𝑝𝑒𝑐𝑡𝑟𝑎𝑙 &, 𝑖𝑛𝑡 𝑚𝑒𝑚𝑏𝑒𝑟𝑠 = 1); (𝑝𝑟𝑧𝑒𝑘𝑠𝑧𝑡𝑎ł𝑐𝑎 𝑆𝑝𝑒𝑐𝑡𝑟𝑎𝑙 𝑛𝑎 𝑡𝑦𝑝 𝑆𝑡𝑎𝑟)
Konstruktory konwersji używane są między innymi wtedy, kiedy do funkcji przyjmującej
argument typu klasy przekazywany jest typ, który można przekształcić na tę klasę, jak w
poniższym przykładzie:
𝑆𝑡𝑎𝑟 𝑛𝑜𝑟𝑡ℎ;
𝑛𝑜𝑟𝑡ℎ = "𝑝𝑜𝑙𝑎𝑟𝑖𝑠";
Drugie wyrażenie może wywołać funkcję Star::operator=(const Star &) przy użyciu
konstruktora Star::Star(const char *) do utworzenia obiektu klasy Star, który posłuży jako
argument dla operatora przypisania. Zakładamy w tym momencie, że w klasie nie ma
operatora przypisującego typ (char*) do typu Star. Używanie słowa kluczowego explicit w
prototypie konsturktora jednoargumentowego uniemożliwia niejawną konwersjeę, ale nie
zapobiega konwersji jawnej.
Aby przekstałcić obiekt klasy na inny typ, musimy zdefiniować funkcję konwersji. Funkcja
konwersji to metoda bez argumentów i zwracanego typu, a jej nazwa jest taka sama jak typu
, na który przekształca obiekt klasy. Choć w funkcji konwersji zwracany typ nie jest określony,
to powinna ona zwracać wartość odpowiedniego typu. Poniżej znajdują się proste przykłady:
Powinno się ostrożnie podchodzić do takich funkcji i stosować je tylko wtedy, kiedy ma to
dobre uzasadnienie. Ponadto w niektórych projektach klas funcje konwersji zwiększają
prawdopodobieństwo napisania wieloznacznego kodu. Załóżmy, że zdefiniujemy konwersję
na typ double dla klasy Vector (rozdz. 11). Co się stanie dla kodu:
Kompilator może przekształcić ius na typ double i użyć dodawania dla typu double, lecz może
też przekształcić liczbę 20.2 na Vector za pomocą jednego z konstruktorów i użyć dodawania
dla typu vector. W rzeczywistości kompilator nie zrobi żadnej z tych rzeczy, lecz poinformuje
użytkownika o wieloznacznym kodzie. W c++11 jest możliwe stosowanie przy metodach
konwersji słowa explicit. Tak jak w przypadku konstrukotór oznacza to możliwość użycia tych
metod w jawnych wyrażeniach rzutowania, ale nie w niejawnych konwersjach typów.
Przekazywanie obiektów przez wartość i przez referencję
Kiedy piszemy funkcję z argumentem w postaci obiektu, przeważnie powinniśmy przekazywać
go przez referencję, a nie przez wartość. Jednym z powodów jest większa wydajność takiego
rozwiązania. Przekazywanie obiektu przez wartość wiąże się z tworzeniem jego tymczasowej
125
kopii, co oznacza wywołanie konstruktora kopiującego i następnie destruktora. Wywoływanie
tych funkcji wymaga czasu, a kopiowanie dużych obiektów może być znacznie wolniejsze niż
przekazywanie referencji. Jeśli funkcja nie zmienia stanu obiektu, powinno się zadeklarować
referencję jako stałą. Kolejna zaleta przekazywania obiektów przez referencję ma związek z
użytkowaniem funkcji wirtualnych. Funkcję, która przyjmuje argument w postaci referencji do
klasy bazowej, możemy także stosować dla obiektów klas pochodnych, co opisane jest we
wcześniejszej części tego rozdziału. W dalszej części rozdziału znajduje się punkt „Metody
wirtualne”, który także opisuje ten mechanizm.
Zwracanie obiektu a zwracanie referencji
Pewne metody klasy zwracają obiekty. Jak wiemy, niektóre z nich zwracają obiekt
bezpośrednio, podczas gdy inne przez referencję. Czasami metoda musi zwrócić obiekt, ale
jeśli nie jest to konieczne powinno się używać referencji. Po pierwsze, jedyna różnica w kodzie
między tymi sposobami zwracania obiektu to inny prototyp i nagłówek funkcji:
𝑆𝑡𝑎𝑟 𝑛𝑜𝑣𝑎𝑙 (𝑐𝑜𝑛𝑠𝑡 𝑠𝑡𝑎𝑟 &) (𝑧𝑤𝑟𝑎𝑐𝑎 𝑜𝑏𝑖𝑒𝑘𝑡 𝑠𝑡𝑎𝑟)
𝑆𝑡𝑎𝑟 & 𝑛𝑜𝑣𝑎𝑙 2(𝑐𝑜𝑛𝑠𝑡 𝑠𝑡𝑎𝑟 &) (𝑧𝑤𝑟𝑎𝑐𝑎 𝑟𝑒𝑓𝑒𝑟𝑒𝑛𝑐𝑗ę 𝑡𝑦𝑝𝑢 𝑠𝑡𝑎𝑟)
Po drugie bezpośrednie zwracanie obiektu jest o tyle niekorzystne, że wymaga utworzenia
jego tymczasowej kopii. To właśnie kopia jest dostępna dla funkcji wywołującej. Dlatego
zwracanie obiektu powoduje koszt w postaci wywołania konstruktora kopiującego do
utworzenia kopii obiektu, a następnie wywołania desutrktora do usunięcia tej kopii. Zwracanie
referencji oszczędza czas i pamięć. Bezpośrednie zwracanie obiektu jest podobne do
przekazywania obiektu przez wartość – oba miechanizmy wymagają utworzenia tymczasowej
kopii. Z kolei zwracanie referencji jest podobne do przekazywania obiektu przez referencję –
zarówno funkcja wywołująca, jak i wywoływania działają na tym samym obiekcie. Jednak nie
zawsze można zwrócić referencję. Funkcja nie powinna zwracać referencji do obiektu
tymczasowego utworzonego w funkcji, ponieważ referencja ta staje się niepoprawna w
momencie kiedy funkcja kończy działanie i obiekt zostaje usunięty. W takim przypadku należy
zwrócić obiekt, aby utworzyć kopię dostępną w funkcji wywołującej. Możemy postępować
według prostej reguły i nie stosować referencji, jeśli funkcja zwraca utworzony przez siebie
obiekt tymczasowy. Na przykład poniższa metoda używa konstruktora do utworzenia nowego
obiektu, a następnie zwraca jego kopię:
Jeśli funkcja zwraca obiekt przekazany do niej przez referencję lub przez wskaźnik, powinno
się zwrócić taki obiekt za pomocą referencji. Poniższy kod zwraca przez referencję albo
obiekt, dla którego funkcja ta została wywołana albo obiekt przekazany jako argument:
126
Używanie słowa kluczowego const
Powinno się zwracać uwagę na możliwości zastosowania modyfikatora const. Dzięki jego
użyciu można być pewnym, że metoda nie zmieni wartości argumentu:
𝑆𝑡𝑎𝑟 ∷ 𝑆𝑡𝑎𝑟(𝑐𝑜𝑛𝑠𝑡 𝑐ℎ𝑎𝑟 ∗ 𝑠){ }
(𝑓𝑢𝑛𝑘𝑐𝑗𝑎 𝑛𝑖𝑒 𝑚𝑜ż𝑒 𝑧𝑚𝑖𝑒𝑛𝑖ć 𝑐𝑖ą𝑔𝑢 𝑧𝑛𝑎𝑘ó𝑤, 𝑛𝑎 𝑘𝑡ó𝑟𝑦 𝑤𝑠𝑘𝑎𝑧𝑢𝑗𝑒 𝑠)
W podobny sposób możemy zapobiec modyfikowaniu przez metodę obiektu, na rzecz którego
jest ona wywołana:
𝑣𝑜𝑖𝑑 𝑆𝑡𝑎𝑟 ∷ 𝑠ℎ𝑜𝑤( ) 𝑐𝑜𝑛𝑠𝑡 { } (𝑓𝑢𝑛𝑘𝑐𝑗𝑎 𝑛𝑖𝑒 𝑚𝑜ż𝑒 𝑧𝑚𝑖𝑒𝑛𝑖ć 𝑜𝑏𝑖𝑒𝑘𝑡𝑢)
W tym przypadku const oznacza const Star * this, gdzie this wskazuje na obiekt, dla którego
została wywołana metoda. Funkcja która zwraca referencję, zwykle może się znajdować po
lewej stronie przypisania, co oznacza, że możemy przypisać wartość do obiektu, na który
wskazuje referencja. Możemy jednak użyć modyfikatora const, aby uniemożliwić takie
przypisanie:
W przykładzie metoda zwraca referencję do this lub s. Ponieważ zarówno this, jak i s są
zadeklarowane za pomocą modyfikatora const, funkcja nie może zmienić ich wartości,
dlatego zwracana referencja także musi być zadeklarowana jako stała. Zwróćmy uwagę, że
jeśli w funkcji zadeklarujemy argument jako referencję lub wskaźnik na obiekt stały, nie
możemy w niej przekazać tego argmentu do następnej funkcji, jeśli nie zapewnia ona stałości
tego obiektu.
Dziedziczenie publiczne
Co nie podlega dziedziczeniu
Operator przypisania
Składowe prywatne a chronione
Metody wirtualne
Destruktory
Funkcje zaprzyjaźnione
Zagadnienia związane z używaniem metod klasy bazowej
Metody klasy - podsumowanie
127
128