You are on page 1of 128

Rozdział 3.

Dane

Inicjalizacje w C++
𝑖𝑛𝑡 𝑥 = 5;
,lub
𝑖𝑛𝑡 𝑥(5);

Wg. standardu C++ 11 dozwolone są formy:

𝑖𝑛𝑡 𝑥{5};
𝑖𝑛𝑡 𝑥 = {5};
𝑖𝑛𝑡 𝑥{ }; 𝑙𝑢𝑏 𝑖𝑛𝑡 𝑥 = { }; −𝑜𝑧𝑛𝑎𝑐𝑧𝑎𝑗ą 𝑖𝑛𝑖𝑐𝑗𝑎𝑙𝑖𝑧𝑎𝑐𝑗ę 𝑧𝑒𝑟𝑒𝑚.
Składnia inicjalizacji klamrowej lepiej zabezpiecza przed błędami konwersji typów.

Typy short, int, long i long long


 short – przynajmniej 16 bitów
 int – nie mniejsze od liczby typu short
 long – przynajmniej 32 bity i nie mniejsze od liczby typu int
 long long – przynajmniej 64 bity i nie mniejsze od liczby typu long
Domyślnie wszystkie typy są signed.
Poszczególne zakresy typów są opisane w pliku nagłówkowym climits danego kompilatora

Zapis dziesiętny, ósemkowy i szesnastkowy


 zapis dziesiętny – np. int x = 42; (dziesiętnie x == 42)
 zapis ósemkowy – np. int x = 042; (dziesiętnie x == 34)
 zapis szesnastkowy – np. int x = 0x42; (dziesiętnie x == 66)

 cout domyślnie wyświetla liczby dziesiętne!


By wyświetlano liczby w postaci ósemkowej lub szesnastkowej należy dodać manipulator do
cout:
𝑐𝑜𝑢𝑡 ≪ ℎ𝑒𝑥;
𝑐𝑜𝑢𝑡 ≪ 66; (𝑤𝑦ś𝑤𝑖𝑒𝑡𝑙𝑖 𝑙𝑖𝑐𝑧𝑏ę 0𝑥42)
𝑐𝑜𝑢𝑡 ≪ 𝑜𝑐𝑡;
𝑐𝑜𝑢𝑡 ≪ 34; (𝑤𝑦ś𝑤𝑖𝑒𝑡𝑙𝑖 042)

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:

 Przypisanie do zmiennej char wartości, cout wypisze jako znak ASCII. Np


𝑐ℎ𝑎𝑟 𝑥 = 55;
Cout wypisze x’a jako znak ASCII, któremu odpowiada wartość 55.
 Dodatkowo w książce m.in. UNICODE, wchar_t (rozszerzone znaki), char16_t,
char32_t

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)

Const należy od razu inicjalizować, taki zapis będzie błędny:

𝑐𝑜𝑛𝑠𝑡 𝑖𝑛𝑡 𝑝𝑎𝑙𝑐𝑒;


𝑝𝑎𝑙𝑐𝑒 = 10;

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

 Typy liczb zmiennoprzecinkowych: Float, double, long double. Używa się je w


zależności od tego ile jest liczb cyfr znaczących. (ile cyfr wyświetli poprawnie). Jeśli
chcemy większą dokładność to lepiej użyć double lub long double zamiast float.
Domyślnie są double.
Do liczb można dodać końcówki, które określają typ liczby np:
1.234𝑓 − 𝑓𝑙𝑜𝑎𝑡
2𝑢𝐿 − 𝑢𝑛𝑠𝑖𝑔𝑛𝑒𝑑 𝑙𝑜𝑛𝑔
2.2𝐿 − 𝑙𝑜𝑛𝑔 𝑑𝑜𝑢𝑏𝑙𝑒

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

Konwersja przy inicjalizacji klamrowej (C++ 11)


Konwersja z wartości całkowitej na wartość innego typu całkowitego albo typu
zmiennoprzecinkowego jest dozwolona pod warunkiem, że kompilator jest w stanie ustalić, że
zmienna inicjalizowana może dokładnie reprezentować wartość inicjalizującą. Można na
przykład zainicjalizować zmienna typu long wartością typu int, ponieważ zmienna typu long
pomieści każdą wartość typu int, ale konwersja odwrotna nie jest już taka oczywista i jej
powodzenia zależy nie tylko od typu wartości inicjalizującej ale od jej konkretnegj wartości.
𝑐𝑜𝑛𝑠𝑡 𝑖𝑛𝑡 𝑐𝑜𝑑𝑒 = 55;
𝑖𝑛𝑡 𝑥 = 55;
𝑐ℎ𝑎𝑟 𝑐1 {312325}; (𝑧𝑎𝑤ęż𝑒𝑛𝑖𝑒 𝑡𝑦𝑝𝑢, 𝑛𝑖𝑒𝑑𝑜𝑧𝑤𝑜𝑙𝑜𝑛𝑒)
𝑐ℎ𝑎𝑟 𝑐2 = {55}; (𝑑𝑜𝑧𝑤𝑜𝑙𝑜𝑛𝑒, 𝑏𝑜 𝑡𝑦𝑝 𝑐ℎ𝑎𝑟 𝑚𝑖𝑒ś𝑐𝑖 𝑤𝑎𝑟𝑡𝑜ść 55)
𝑐ℎ𝑎𝑟 𝑐3 {𝑐𝑜𝑑𝑒}; (𝑗𝑎𝑘 𝑤𝑦ż𝑒𝑗)
𝑐ℎ𝑎𝑟 𝑐4 = {𝑥}; (𝑛𝑖𝑒𝑑𝑜𝑧𝑤𝑜𝑙𝑜𝑛𝑒, 𝑏𝑜 𝑥 𝑛𝑖𝑒 𝑗𝑒𝑠𝑡 𝑠𝑡𝑎łą)
𝑥 = 31256;
𝑐ℎ𝑎𝑟 𝑐5 = 𝑥; (𝑤 𝑠𝑘ł𝑎𝑑𝑛𝑖 𝑛𝑖𝑒 𝑘𝑙𝑎𝑚𝑟𝑜𝑤𝑒𝑗, 𝑑𝑜𝑧𝑤𝑜𝑙𝑜𝑛𝑒)

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).

Rzutowanie typów - konwersja jawna (w wyniku


naszej ingerencji)
 (nazwaTypu) wartość – zwraca wartość po konwersji na typ nazwaTypu (stara
składnia)
 nazwaTypu (wartość) – zwraca wartość po konwersji na typ nazwaTypu (nowa
składnia)
 static_cast<typ> (wartość) - zwraca wartość skonwertowaną na typ (static_cast jest
mądrzejszy od poprzedniczek – o tym w rozdziale 15)

Automatyczne deklaracje typów w C++ 11


W nowym standardzie kompilator może dedukować typ zmiennej na podstawie typu
wartości inicjalizującej.
𝑎𝑢𝑡𝑜 𝑛 = 100; (𝑛 𝑗𝑒𝑠𝑡 𝑡𝑦𝑝𝑢 𝑖𝑛𝑡)
𝑎𝑢𝑡𝑜 𝑥 = 1.5; (𝑥 𝑗𝑒𝑠𝑡 𝑡𝑦𝑝𝑢 𝑑𝑜𝑢𝑏𝑙𝑒)
𝑎𝑢𝑡𝑜 𝑦 = 1.3𝑒12𝐿 (𝑦 𝑗𝑒𝑠𝑡 𝑡𝑦𝑝𝑢 𝑙𝑜𝑛𝑔 𝑑𝑜𝑢𝑏𝑙𝑒)
Lepiej je stosować w bardziej skomplikowanych a nie tak banalnych przypadkach (ponoć).
Łatwo się pomylić przez nieuwagę.
− 𝑎𝑢𝑡𝑜 𝑥 = 0.0; (𝑤𝑠𝑧𝑦𝑠𝑡𝑘𝑜 𝑗𝑒𝑠𝑡 𝑤 𝑝𝑜𝑟𝑧ą𝑑𝑘𝑢, 𝑐ℎ𝑐𝑖𝑎ł𝑒𝑚 𝑑𝑜𝑢𝑏𝑙𝑒 𝑖 𝑢𝑧𝑦𝑠𝑘𝑎𝑚 𝑑𝑜𝑢𝑏𝑙𝑒)
− 𝑑𝑜𝑢𝑏𝑙𝑒 𝑦 = 0; (𝑧𝑎𝑝𝑜𝑚𝑛𝑖𝑎ł𝑒𝑚 𝑜 𝑝𝑟𝑧𝑒𝑐𝑖𝑛𝑘𝑢, 𝑎𝑙𝑒 𝑡𝑦𝑝 𝑑𝑜𝑢𝑏𝑙𝑒 𝑗𝑒𝑠𝑡
𝑤𝑖ę𝑐 𝑛𝑎𝑠𝑡ą𝑝𝑖 𝑎𝑢𝑡𝑜𝑚𝑎𝑡𝑦𝑐𝑧𝑛𝑎 𝑘𝑜𝑛𝑤𝑒𝑟𝑠𝑗𝑎 𝑛𝑎 0.0)
− 𝑎𝑢𝑡𝑜 𝑧 = 0; (𝑚𝑖𝑎ł 𝑏𝑦ć 𝑑𝑜𝑢𝑏𝑙𝑒, 𝑎 𝑧𝑎𝑝𝑜𝑚𝑛𝑖𝑎ł𝑒𝑚 𝑜 𝑝𝑟𝑧𝑒𝑐𝑖𝑛𝑘𝑢 𝑖 𝑗𝑒𝑠𝑡 𝑖𝑛𝑡)
Automatyczna dedukcja typów jest bardzo użyteczna gdy operujemy na typach
skomplikowanych takich jak typy z bibliotek szablonów STL (o tym w dalszym rozdziale).

7
Rozdział 4. Typy złożone

Inicjalizacja tablic w c++ 11


1) można pominąć znak przypisania:
𝑑𝑜𝑢𝑏𝑙𝑒 𝑒𝑎𝑟𝑛𝑖𝑛𝑔𝑠 [4]{1.4𝑒, 3.5𝑒, 1.8𝑒3, 6.2𝑒5}
2) można zainicjalizować wszystkie elementy tablicy zerem podająć pustą listę:
𝑢𝑛𝑠𝑖𝑔𝑛𝑒𝑑 𝑖𝑛𝑡 𝑐𝑜𝑢𝑛𝑡𝑠[10] = { };
(wcześniej można to był robić podając jedno zero:
𝑢𝑛𝑠𝑖𝑔𝑛𝑒𝑑 𝑖𝑛𝑡 𝑐𝑜𝑢𝑛𝑡𝑠[10] = {0};
3) zabezpieczenie przed zawężeniem typów:
𝑙𝑜𝑛𝑔 𝑝𝑙𝑖𝑓𝑠[ ] = {25,94,2.4};
To nie przejdzie bo liczba 2.4 jest innego typu niż poprzednie.

 Łańcuch znakowy jest tylko wtedy gdy kończy się on znakiem zero ‘\0’

 Funkcja strlen(nazwa_tablicy) – zwraca ilość znaków w tablicy (bez zera)

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;

Inicjalizacja łańcuchów znakowych w c++ 11


𝑐ℎ𝑎𝑟 𝑓𝑖𝑟𝑠𝑡[ ] = {"Tomek"};
𝑐ℎ𝑎𝑟 𝑠𝑒𝑐𝑜𝑛𝑑[ ] {"Ania"};
𝑐ℎ𝑎𝑟 𝑡ℎ𝑖𝑟𝑑 = {"Tomek"};
𝑐ℎ𝑎𝑟 𝑓𝑜𝑢𝑟𝑡ℎ {"𝐴𝑛𝑖𝑎"};
Przewaga string nad char w łańcuchach tekstowych (str. 137 – 139)
 Char lepiej używać do tablic pojedynczych znaków, a nie skomplikowanych
łańcuchów tekstowych. Powodem jest prostota obsługi stringów.
 String można przypisać (w tablicy potrzebna była funkcja kopiująca elementy tablicy –
strcpy()). Przykład przypisania stringa:
𝑠𝑡𝑟𝑖𝑛𝑔 𝑠𝑡𝑟1 = pantera
𝑠𝑡𝑟𝑖𝑛𝑔 𝑠𝑡𝑟2 = 𝑠𝑡𝑟1;
 Klasy string można do siebie dodawać i łączyć (wcześniej konieczne było
wykorzystanie funkcji z C, strcat() ):
𝑠𝑡𝑟𝑖𝑛𝑔 𝑠𝑡𝑟1 = "jeden"
𝑠𝑡𝑟𝑖𝑛𝑔 𝑠𝑡𝑟2 = "𝑑𝑤𝑎"
𝑠𝑡𝑟𝑖𝑛𝑔 𝑠𝑡𝑟3 = 𝑠𝑡𝑟1 + 𝑠𝑡𝑟2
Lub
𝑠𝑡𝑟1+= 𝑠𝑡𝑟2
W obu przypadkach da to wynik:
𝑗𝑒𝑑𝑒𝑛𝑑𝑤𝑎
 Można łatwo określać długość stringa:
𝑖𝑛𝑡 𝑙𝑒𝑛 = 𝑠𝑡𝑟1. 𝑠𝑖𝑧𝑒()
Korzystamy tu z metody klasy string ( o metodach klasy przy temacie klasy ), wcześniej w
tablicach należało korzystać z funkcji strlen().
 Tablica bez przypisania do niej wartości jest zainicjalizowana śmieciami, a string bez
przypisania wartości początkowej inicjalizowany jest zerem.

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:

Lub w jednej linijce


𝑖𝑛𝑓𝑙𝑎𝑡𝑎𝑏𝑙𝑒 𝑔𝑢𝑒𝑠𝑡 = {"𝐺𝑙𝑜𝑟𝑖𝑜𝑢𝑠 𝐺𝑙𝑜𝑟𝑖𝑎", 1.88.29.99};
 Do elementów struktury odnosimy się kropką:
𝑐𝑜𝑢𝑡 ≪ 𝑖𝑛𝑓𝑙𝑎𝑡𝑎𝑏𝑙𝑒. 𝑝𝑟𝑖𝑐𝑒

11
𝑖𝑛𝑓𝑙𝑎𝑡𝑎𝑏𝑙𝑒. 𝑝𝑟𝑖𝑐𝑒 = 2.5;
(str 144 – 145 – przykładowe użycie struktur oraz zasięgi globalne i lokalne)

Inicjalizacja struktur w c++ 11


 Bez znaku ‘=’
𝑖𝑛𝑓𝑙𝑎𝑡𝑎𝑏𝑙𝑒 𝑔𝑢𝑒𝑠𝑡 {"𝐺𝑙𝑜𝑟𝑖𝑜𝑢𝑠 𝐺𝑙𝑜𝑟𝑖𝑎", 1.88.29.99};
 Inicjalizacja zerami
𝑖𝑛𝑓𝑙𝑎𝑡𝑎𝑏𝑙𝑒 𝑔𝑢𝑒𝑠𝑡 = { };
---------------------------------------------------------------------------------------------------------------------------
------
Struktury można przekazywać do funkcji, można przypisywać jedną strukturę drugiej itp.
𝑖𝑛𝑓𝑙𝑎𝑡𝑎𝑏𝑙𝑒 𝑧𝑚𝑖𝑒𝑛𝑛𝑎1;
𝑖𝑛𝑓𝑙𝑎𝑡𝑎𝑏𝑙𝑒 𝑧𝑚𝑖𝑒𝑛𝑛𝑎2;
𝑧𝑚𝑖𝑒𝑛𝑛𝑎2 = 𝑧𝑚𝑖𝑒𝑛𝑛𝑎1;
 Można też połączyć definicję struktury z tworzeniem zmiennych, jednak jest to mniej
czytelne zazwyczaj (str 147)

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

𝑖𝑛𝑡 ∗ 𝑤𝑠𝑘 = &𝑥;


Taki sam zapis, ale inaczej można do tego podejść: (mała różnica w podejściu do czytania jak by – nic
nie znaczy):

 𝑖𝑛𝑡 ∗ 𝑝𝑡𝑟 − 𝑝𝑡𝑟 𝑡𝑜 𝑤𝑠𝑘𝑎ź𝑛𝑖𝑘 𝑛𝑎 𝑖𝑛𝑡 (𝑐𝑧𝑦𝑙𝑖 𝑝𝑟𝑧𝑒𝑐ℎ𝑜𝑤𝑢𝑗𝑒 𝑎𝑑𝑟𝑒𝑠 𝑜𝑏𝑖𝑒𝑘𝑡𝑢 𝑡𝑦𝑝𝑢 𝑖𝑛𝑡)
 𝑖𝑛𝑡 ∗ 𝑝𝑡𝑟 − ∗ 𝑝𝑡𝑟 𝑗𝑒𝑠𝑡 𝑜𝑏𝑖𝑒𝑘𝑡𝑒𝑚 𝑡𝑦𝑝𝑢 𝑖𝑛𝑡 (𝑏𝑜 𝑝𝑡𝑟 𝑤𝑠𝑘𝑎𝑧𝑢𝑗𝑒 𝑛𝑎 𝑖𝑛𝑡)

Przypisywanie wskaźnikowi konkretnego adresu musi odbyć się za pomocą rzutowania


𝑖𝑛𝑡 ∗ 𝑝𝑡;
𝑝𝑡 = 0𝑥𝐵800000;
My możemy wiedzieć, że powyższa liczba B800000 jest adresem, jakiejś danej w np karcie graficznej,
którą używamy, ale kompilator tego nie wie. Musimy wieć przeprowadzić rzutowanie.

𝑖𝑛𝑡 ∗ 𝑝𝑡;

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.

𝑖𝑛𝑡 ∗ 𝑝𝑠 = 𝑛𝑒𝑤 𝑖𝑛𝑡 (𝑂𝐾)


𝑑𝑒𝑙𝑒𝑡𝑒 𝑝𝑠; (𝑂𝐾)

𝑑𝑒𝑙𝑒𝑡𝑒 𝑝𝑠; (Ź𝐿𝐸, 𝑑𝑟𝑢𝑔𝑖 𝑟𝑎𝑧 𝑘𝑎𝑠𝑜𝑤𝑎𝑛𝑖𝑒)

𝑖𝑛𝑡 𝑗𝑢𝑔𝑠 = 5; (𝑂𝐾)


𝑖𝑛𝑡 ∗ 𝑝𝑖 = &𝑗𝑢𝑔𝑠; (𝑂𝐾)

𝑑𝑒𝑙𝑒𝑡𝑒 𝑝𝑖; (Ź𝐿𝐸 − 𝑝𝑎𝑚𝑖ęć 𝑛𝑖𝑒 𝑏𝑦ł𝑎 𝑎𝑙𝑜𝑘𝑜𝑤𝑎𝑛𝑎 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟𝑒𝑚 𝑛𝑒𝑤)

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:

𝑖𝑛𝑡 ∗ 𝑝𝑠 = 𝑛𝑒𝑤 𝑖𝑛𝑡; (𝑎𝑙𝑜𝑘𝑎𝑐𝑗𝑎 𝑝𝑎𝑚𝑖ę𝑐𝑖 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟𝑒𝑚 𝑛𝑒𝑤)


𝑖𝑛𝑡 ∗ 𝑝𝑞 = 𝑝𝑠; (𝑢𝑠𝑡𝑎𝑤𝑖𝑒𝑛𝑖𝑒 𝑤𝑠𝑘𝑎ź𝑛𝑖𝑘𝑎 𝑛𝑎 𝑡𝑒𝑛 𝑠𝑎𝑚 𝑏𝑙𝑜𝑘 − 𝑝𝑞 = 𝑝𝑠);
𝑑𝑒𝑙𝑒𝑡𝑒 𝑝𝑞; (𝑂𝐾 − 𝑢𝑠𝑢𝑛𝑖ę𝑐𝑖𝑒 𝑝𝑟𝑧𝑒𝑧 𝑑𝑟𝑢𝑔𝑖 𝑤𝑠𝑘𝑎ź𝑛𝑖𝑘 𝑝𝑎𝑚𝑖ę𝑐𝑖 𝑎𝑙𝑜𝑘𝑜𝑤𝑎𝑛𝑒𝑗 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟𝑒𝑚 𝑛𝑒𝑤)
INACZEJ – ZA POMOCĄ DELETE WSKAZUJE WSKAŹNIKIEM DANYM ADRES PAMIĘCI, KTÓRY BĘDZIE
USUWANY

 Operatora new używa się raczej do większych fragmentów pamięci od pojedynczych


obiektów, takich jak tablice, struktury, łańcuchy

New do tworzenia tablic dynamicznych


𝑖𝑛𝑡 ∗ 𝑝𝑠𝑜𝑚𝑒 = 𝑛𝑒𝑤 𝑖𝑛𝑡[10];
Operator new zwraca adres pierwszego elementu z bloku (wskaźnik pokazuje adres pierwszego
elementu)

𝑑𝑒𝑙𝑒𝑡𝑒 [ ] 𝑝𝑠𝑜𝑚𝑒; (𝑝𝑜𝑠𝑡𝑎ć 𝑑𝑒𝑙𝑒𝑡𝑒 𝑑𝑙𝑎 𝑤𝑠𝑘𝑎ź𝑛𝑖𝑘ó𝑤 𝑛𝑎 𝑡𝑎𝑏𝑙𝑖𝑐𝑒)


Obecność nawiasów kwadratowych wskazuje, że należy zwolnić całą tablicę, a nie tylko jeden
wskazywany element.
Zasady stosowania new i delete:

 Nie używamy delete do zwalniania pamięci nieprzydzielonej za pomocą new

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)

Można korzystać we wskaźnikach na tablice z:

 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.

𝑠𝑡𝑎𝑐𝑘𝑠[1] == ∗ (𝑠𝑡𝑎𝑐𝑘𝑠 + 1) == ∗ (𝑝𝑠 + 1)


Nazwy wskaźników i nazwy tablic można używać zamiennie:

𝑛𝑎𝑧𝑤𝑎𝑡𝑎𝑏𝑙𝑖𝑐𝑦 [𝑖] == ∗ (𝑛𝑎𝑧𝑤𝑎𝑡𝑎𝑏𝑙𝑖𝑐𝑦 + 𝑖)

𝑛𝑎𝑧𝑤𝑎𝑤𝑠𝑘𝑎ź𝑛𝑖𝑘𝑎 [𝑖] ==∗ (𝑛𝑎𝑧𝑤𝑎𝑤𝑠𝑘𝑎ź𝑛𝑖𝑘𝑎 + 𝑖)


Ale lepiej korzystać ze wskaźników

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’).

Pętle i wyrażenia relacyjne

 Inkrementacja przedrostkowa i przyrostkowa

𝑎 = 20: 𝑏 = 20
𝑎 + + = 20: + +𝑏 = 21
𝑎 = 21: 𝑏 = 21
 Operatory inkrementacji i dekrementacji we wskaźnikach

Opisano jak działają zapisy typu:

+ +∗ 𝑝𝑡
(∗ 𝑝𝑡) + +;

Itp. (str. 202)

 Porównywanie obiektów klasy string

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).

Typedef jest lepsze bo można deklarować zmienne po przecinku:

#𝑑𝑒𝑓𝑖𝑛𝑒 𝐹𝐿𝑂𝐴𝑇_𝑃𝑂𝐼𝑁𝑇𝐸𝑅 𝑓𝑙𝑜𝑎𝑡 ∗;


𝑡𝑦𝑝𝑒𝑑𝑒𝑓 𝐹𝐿𝐴𝑂𝑇_𝑃𝑂𝐼𝑁𝑇𝐸𝑅 𝑝𝑎, 𝑝𝑏;
To zadziała tylko na pa, a nie na pb, w przypadku typedef zadziałało by na oba (str 218-219)

Zakresowe pętle for (C++ 11) (str. 221)


Pojawia się nowa postać pętli for, która ma na celu uproszczenie typowego zadania tej pętli, tzn.
Przetworzenia w jakiś sposób każdego z elementów tablicy. Np odczytywanie:

𝑑𝑜𝑢𝑏𝑙𝑒 𝑝𝑟𝑖𝑐𝑒𝑠[3] = {3.45, 6.12, 7.03};


𝑓𝑜𝑟(𝑑𝑜𝑢𝑏𝑙𝑒 𝑥 ∶ 𝑝𝑟𝑖𝑐𝑒𝑠)

18
𝑐𝑜𝑢𝑡 ≪ 𝑥 ≪ 𝑒𝑛𝑑𝑙;
Spowoduje, że będą wyświetlane po kolei (od pierwszego) wszystkie elementy tablicy prices

Można też modyfikować nią elementy tablicy:

𝑓𝑜𝑟 (𝑑𝑜𝑢𝑏𝑙𝑒 &𝑥 ∶ 𝑝𝑟𝑖𝑐𝑒𝑠)

𝑥 = 𝑥 ∗ 0.8;
Symbol & identyfikuje x jako zmienną referencyjną (o tym w rozdziale 8).

Można też inicjalizować:

𝑓𝑜𝑟(𝑖𝑛𝑡 𝑥 ∶ {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.

Wczytywanie znak po znaku tekstu z klawiatury lub z


pliku
 Cin – najprostsze (str. 222) – nie reaguje na spacje
 Cin.get(char) – (str 223) pozwala na wczytanie znaków nawet jeśli jest to spacja

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

𝑐𝑖𝑛. 𝑔𝑒𝑡(𝑐ℎ𝑎𝑟)
, oraz

𝑐𝑖𝑛. 𝑔𝑒𝑡( )
(więcej o tym w rozdziale 8)

Wykrywanie końca pliku EOF


Od str 224 – 227

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() ).

Przykład – str 226

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

Stosowanie tablic dwuwymiarowych (przykład ze wskaźnikiem str 232


– warto wrócić)

20
Rozdział 6. Instrukcje warunkowe i operatory
logiczne

Ochrona przed błędami:


Programiści próbują chronić się przed błędnymi zapisami instrukcji warunkowych:

𝑖𝑓(3 == 𝑛𝑢𝑚𝑏𝑒𝑟)

Zamiast:

𝑖𝑓(𝑛𝑢𝑚𝑏𝑒𝑟 == 3)

Ponieważ:

𝑖𝑓(3 = 𝑛𝑢𝑚𝑏𝑒𝑟)

Spowoduje, że wyskoczy error (do stałej jest próba przypisania wartości)

𝑖𝑓(𝑛𝑢𝑚𝑏𝑒𝑟 = 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:

𝑤𝑦𝑟𝑎ż𝑒𝑛𝑖𝑒1 ? 𝑤𝑦𝑟𝑎ż𝑒𝑛𝑖𝑒2 ∶ 𝑤𝑦𝑟𝑎ż𝑒𝑛𝑖𝑒3


Jeśli wyrażenie1 ma wartość true, wartością całego wyrażenie z operatorem wyboru jest wyrażenie2,
w przeciwnym przypadku wartością całego wyrażenia jest wyrażenie3.

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

Zapis do pliku tekstowego


Jest duże podobieństwo do cout. Trzeba dodać plik nagłówkowy fstream. Dalej deklaruje się obiekt
typu ofstream, oraz następnie otwiera się strumień wejściowy.

𝑜𝑓𝑠𝑡𝑟𝑒𝑎𝑚 𝑜𝑢𝑡𝐹𝑖𝑙𝑒; (𝑧𝑚𝑖𝑒𝑛𝑛𝑎 𝑜𝑢𝑡𝐹𝑖𝑙𝑒 𝑡𝑦𝑝𝑢 𝑜𝑓𝑠𝑡𝑟𝑒𝑎𝑚)

𝑜𝑢𝑡𝐹𝑖𝑙𝑒. 𝑜𝑝𝑒𝑛(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).

Na końcu zamykamy strumień:

𝑜𝑢𝑡𝐹𝑖𝑙𝑒. 𝑐𝑙𝑜𝑠𝑒( )

Odczyt danych z pliku tekstowego:


Analogia do cin. Dodaje się najpierw plik nagłówkowy fstream (jak w przypadku zapisu danych do
pliku .txt). A następnie tworzy się obiekt typu ifstream (w przypadku zapisu był obiekt typu ofstream).

𝑖𝑓𝑠𝑡𝑟𝑒𝑎𝑚 𝑖𝑛𝐹𝑖𝑙𝑒; (𝑜𝑏𝑖𝑒𝑘𝑡 𝑘𝑙𝑎𝑠𝑦 𝑖𝑓𝑠𝑡𝑟𝑒𝑎𝑚 − 𝑡𝑦𝑝𝑢)

Następnie otwieramy strumień przesyłu danych z plikiem tekstowym:

𝑖𝑛𝐹𝑖𝑙𝑒. 𝑜𝑝𝑒𝑛("𝑏𝑜𝑤𝑙𝑖𝑛𝑔. 𝑡𝑥𝑡");

lub

𝑐ℎ𝑎𝑟 𝑓𝑖𝑙𝑒𝑛𝑎𝑚𝑒[40];
𝑐𝑖𝑛 ≫ 𝑓𝑖𝑙𝑒𝑛𝑎𝑚𝑒;
𝑖𝑛𝐹𝑖𝑙𝑒. 𝑜𝑝𝑒𝑛(𝑓𝑖𝑙𝑒𝑛𝑎𝑚𝑒);
Następnie odczytujemy dane:

𝑑𝑜𝑢𝑏𝑙𝑒 𝑤𝑡;
𝑖𝑛𝐹𝑖𝑙𝑒 ≫ 𝑤𝑡;
Lub:

𝑐ℎ𝑎𝑟 𝑙𝑖𝑛𝑒 [80];


𝑖𝑛𝐹𝑖𝑙𝑒. 𝑔𝑒𝑡𝑙𝑖𝑛𝑒(𝑙𝑖𝑛𝑒, 80);
 Szczególnie starannie trzeba budować pętle odczytu danych z pliku. Są pewne kwestie które
warto sprawdzać. Przede wszystkim kontrolować, czy podczas odczytów program nie doszedł
do EOF (end of file). Po drugie program może natknąć się na dane niewłaściwego typu np.
program oczekuje pliku z samymi liczbami a dostaje stringa.
 Metoda fail() – zwraca true gdy ostatnia próba odczytu danych zakończyła się niezgodnością
typów (metoda zwraca też true po natknięciu się na EOF). W końcu może póść źle coś
całkiem nieprzewidzianego – np. plik może być uszkodzony lub może wystąpić awara
sprzętowa.
 Metoda bad() – zwraca true jeśli w ostatniej próbie odczytu wystąpił jeden z opisanych
błędów.
 Metoda good() - zamiast sprawdzać każdy z wymienionych warunków z osobna łatwiej
wykorzystać tą metodę, która zwraca true tylko wtedy gdy nie wystąpił żaden błąd.

Więcej o tych zabezpieczeniach na stronach 271-274!!!


WAŻNE -> ścieżka do pliku musi być zedytowana. Znak ‘ \ ’, ma być zastąpiony ‘ \\ ‘. Np
inFile.open("C:\\Users\\PC\\Desktop\\kurs C++\\Jezyk c++ Stephan Prata\\zad 6_str
278_rozdz 6\\sponsorzy.txt"); -> dwa znaki slasha a nie jeden jak domyślnie podczas
kopiowania scieżki!

23
Rozdział 7. Funkcje - podstawy

Funkcja może posiadać więcej niż jedną komendę return.

Przekazywanie przez wartość

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)

Wskaźniki w funkcjach przetwarzających tablice


𝑖𝑛𝑡 𝑠𝑢𝑚_𝑎𝑟𝑟(𝑖𝑛𝑡 𝑎𝑟𝑟[ ], 𝑖𝑛𝑡 𝑛);
𝑖𝑛𝑡 𝑠𝑢𝑚 = 𝑠𝑢𝑚_𝑎𝑟𝑟(𝑐𝑜𝑜𝑘𝑖𝑒𝑠, 𝐴𝑟𝑆𝑖𝑧𝑒);
Cookies jest tutaj tablicą, ale przekazywanie tablicy do funkcji odbywa sie ZAWSZE za pomocą
wskaźnika (nazwa tablicy jest jednocześnie wskaźnikiem do jej zerowego elementu).

Skutki użycia tablic jako parametrów


Wywołanie funkcji :

𝑠𝑢𝑚_𝑎𝑟𝑟(𝑐𝑜𝑜𝑘𝑖𝑒𝑠, 𝐴𝑟𝑆𝑖𝑧𝑒);
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.

Przykład str 294

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:

𝑣𝑜𝑖𝑑 𝑠ℎ𝑜𝑤𝑎𝑟𝑟𝑎𝑦(𝑐𝑜𝑛𝑠𝑡 𝑑𝑜𝑢𝑏𝑙𝑒 𝑎𝑟[ ], 𝑖𝑛𝑡 𝑛);


Wynika z tego ze wskaźnik ar (każda tablica to wskaźnik na jej zerowy element) pokazuje na stałą (czyli
zerowy element tablicy (oraz później kolejne jej elementy) są stałe.

Funkcje korzystające z zakresu tablic


Tradycyjne podejście C++ zakłada przekazywanie wskaźnika na początek tablicy jako jednego
parametru i wielkości tablicy jako drugiego parametru. Można też wykorzystać inny sposób – zakres
elementów. Przekazuje się wtedy dwa wskaźniki – jeden na początek tablicy, drugi na jej koniec.

Deklaracja tablicy:

𝑖𝑛𝑡 𝑠𝑢𝑚𝑎𝑟𝑟(𝑐𝑜𝑛𝑠𝑡 𝑖𝑛𝑡 ∗ 𝑏𝑒𝑔𝑖𝑛, 𝑐𝑜𝑛𝑠𝑡 𝑖𝑛𝑡 ∗ 𝑒𝑛𝑑);


No i wykorzystuje się to np. tak:

𝑖𝑛𝑡 𝑠𝑢𝑚 = 𝑠𝑢𝑚𝑎𝑟𝑟(𝑐𝑜𝑜𝑘𝑖𝑒𝑠, 𝑐𝑜𝑜𝑘𝑖𝑒𝑠 + 𝐴𝑟𝑆𝑖𝑧𝑒);


(więcej str 301-302)

Wskaźniki i modyfikator const


 Deklaracja zmiennej wskazującej na stałą:

𝑖𝑛𝑡 𝑎𝑔𝑒 = 39;


𝑐𝑜𝑛𝑠𝑡 𝑖𝑛𝑡 ∗ 𝑝𝑡 = &𝑎𝑔𝑒;
Deklaracja ta mówi, że pt wskazuje wartość const int. Wobec tego nie można użyć pt do zmiany tej
wartości. Wartość *pt jest stała i nie można jej zmieniać.

∗ 𝑝𝑡+= 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)

 Deklaracja stałego wskaźnika

𝑖𝑛𝑡 𝑠𝑙𝑜𝑡ℎ = 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 :

Można też zadeklarować stały wskaźnik stałego obiektu:

𝑐𝑜𝑛𝑠𝑡 𝑡𝑟𝑜𝑢𝑏𝑙𝑒 = 2.9𝐸30;


𝑐𝑜𝑛𝑠𝑡 𝑑𝑜𝑢𝑏𝑙𝑒 ∗ 𝑐𝑜𝑛𝑠𝑡 𝑠𝑡𝑖𝑐𝑘 = &𝑡𝑟𝑜𝑢𝑏𝑙𝑒;
Wtedy zarówno strick jak i *stick są stałe.

Funkcje i tablice dwuwymiarowe


Deklaracja takiej funkcji może wyglądać np tak:

𝑖𝑛𝑡 𝑠𝑢𝑚(𝑖𝑛𝑡 𝑎𝑟2[ ][4], 𝑖𝑛𝑡 𝑠𝑖𝑧𝑒);


Wystarczy określić drugi wymiar tablicy, dzięki temu pierwszy wymiar można zmieniac, a co za tym
idzie funkcja jest bardziej uniwersalna:

𝑖𝑛𝑡 𝑎[100][4];
𝑖𝑛𝑡 𝑏[6][4];
𝑖𝑛𝑡 𝑡𝑜𝑡𝑎𝑙1 = 𝑠𝑢𝑚(𝑎, 100);
𝑖𝑛𝑡 𝑡𝑜𝑡𝑎𝑙2 = 𝑠𝑢𝑚(𝑏, 25);
(więcej str 306-307)

Funkcje zwracające adresy łańcucha


Funkcja nie może zwrócić łańcucha char, ale może zwrócić adres tego łańcucha.

 [n+1] jest powyżej bo jeszcze znak null na końcu łańcucha char.

(więcej str 309-310)

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)

Wskaźniki na funkcje – podstawy


Algorytm:

 Uzyskać adres wywołania funkcji


 Zadeklarować wskaźnik na funkcję
 Użyć wskaźnika na funkcję do wywołania samej funkcji

Pozyskiwanie adresu funkcji


𝑝𝑟𝑜𝑐𝑒𝑠𝑠(𝑡ℎ𝑖𝑛𝑘) (𝑝𝑟𝑧𝑒𝑘𝑎𝑧𝑎𝑛𝑖𝑒 𝑑𝑜 𝑓𝑢𝑛𝑘𝑐𝑗𝑖 𝑝𝑟𝑜𝑐𝑒𝑠𝑠( ) 𝑎𝑑𝑟𝑒𝑠𝑢 𝑓𝑢𝑛𝑘𝑐𝑗𝑖 𝑡ℎ𝑖𝑛𝑘( )

Nie mylić z:

𝑝𝑟𝑜𝑐𝑒𝑠𝑠(𝑡ℎ𝑖𝑛𝑘( )) (𝑝𝑟𝑧𝑒𝑘𝑎𝑧𝑎𝑛𝑖𝑒 𝑓𝑢𝑛𝑘𝑐𝑗𝑖 𝑝𝑟𝑜𝑐𝑒𝑠𝑠( ) 𝑤𝑎𝑟𝑡𝑜ś𝑐𝑖 𝑧𝑤𝑟ó𝑐𝑜𝑛𝑒𝑗 𝑝𝑟𝑧𝑒𝑧 𝑓𝑢𝑛𝑘𝑐𝑗ę 𝑡ℎ𝑖𝑛𝑘( ))

28
Deklaracja wskaźnika na funkcje
𝑑𝑜𝑢𝑏𝑙𝑒(∗ 𝑝𝑓)(𝑖𝑛𝑡) (𝑝𝑓 𝑤𝑠𝑘𝑎𝑧𝑢𝑗𝑒 𝑓𝑢𝑛𝑘𝑐𝑗ę 𝑧𝑤𝑟𝑎𝑐𝑎𝑗ą𝑐ą 𝑤𝑎𝑟𝑡𝑜ść 𝑑𝑜𝑢𝑏𝑙𝑒)
Nie mylić z:

𝑑𝑜𝑢𝑏𝑙𝑒 ∗ 𝑝𝑓(𝑖𝑛𝑡) (𝑝𝑓( ) 𝑡𝑜 𝑓𝑢𝑛𝑘𝑐𝑗𝑎 𝑧𝑤𝑟𝑎𝑐𝑎𝑗ą𝑐𝑎 𝑤𝑠𝑘𝑎ź𝑛𝑖𝑘 𝑡𝑦𝑝𝑢 𝑑𝑜𝑢𝑏𝑙𝑒)

Po poprawnym zadeklarowaniu wskaźnika można przypisać mu adres interesującej nas funkcji:

𝑑𝑜𝑢𝑏𝑙𝑒 𝑝𝑎𝑚(𝑖𝑛𝑡);
𝑑𝑜𝑢𝑏𝑙𝑒(∗ 𝑝𝑓)(𝑖𝑛𝑡)
𝑝𝑓 = 𝑝𝑎𝑚;

Wywołanie funkcji przez wskaźnik


Robimy to jak zazwyczaj (od 5, bo funkcja pam ma argument typu int, a wskaźnik pf wskazuje na tą
funkcję + deklaracja wskaźnika też informuje nas, że wskazywana funkcja będzie miała argument typu
int):
(∗ 𝑝𝑓)(5);

Lub można też prościej:

𝑝𝑓(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:

𝑐𝑜𝑛𝑠𝑡 𝑑𝑜𝑢𝑏𝑙𝑒 ∗ (∗ (𝑝𝑑)[3])(𝑐𝑜𝑛𝑠𝑡 𝑑𝑜𝑢𝑏𝑙𝑒 ∗, 𝑖𝑛𝑡) = &𝑝𝑎;


 Dzięki c++ 11 można to uprośić dzięki auto

𝑎𝑢𝑡𝑜 𝑝𝑐 = &𝑝𝑎;
Więcej o tym na str 329-333

 Auto nie działa do inicjalizacji listą:

𝑐𝑜𝑛𝑠𝑡 𝑑𝑜𝑢𝑏𝑙𝑒 ∗ (∗ 𝑝𝑎[3])(𝑐𝑜𝑛𝑠𝑡 𝑑𝑜𝑢𝑏𝑙𝑒 ∗, 𝑖𝑛𝑡) = {𝑓1, 𝑓2, 𝑓3};

 Ale działa na pojedynczą wartość (tutaj pb to wskaźnik do pierwszego elementu pa)

𝑎𝑢𝑡𝑜 𝑝𝑏 = 𝑝𝑎;

Typedef
(str 333) Pomaga upraszczać skomplikowane deklaracje (coś poza auto). Używa się tego tak, że
piszemy np.:

𝑡𝑦𝑝𝑒𝑑𝑒𝑓 𝑐𝑜𝑛𝑠𝑡 𝑑𝑜𝑢𝑏𝑙𝑒 ∗ (∗ 𝑝𝑓𝑢𝑛)(𝑐𝑜𝑛𝑠𝑡 𝑑𝑜𝑢𝑏𝑙𝑒 ∗, 𝑖𝑛𝑡);


, a później możemy napisać:

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:

𝑖𝑛𝑡 𝑟𝑎𝑡𝑠 = 101;


𝑖𝑛𝑡 & 𝑟𝑜𝑑𝑒𝑛𝑡𝑠 = 𝑟𝑎𝑡𝑠; (𝑟𝑜𝑑𝑒𝑛𝑡𝑠 𝑡𝑜 𝑟𝑒𝑓𝑒𝑟𝑒𝑛𝑐𝑗𝑎)
𝑖𝑛𝑡 ∗ 𝑝𝑟𝑎𝑡𝑠 = &𝑟𝑎𝑡𝑠; (𝑝𝑟𝑎𝑡𝑠 𝑡𝑜 𝑤𝑠𝑘𝑎ź𝑛𝑖𝑘)
Dalej można używać wymiennie wyrażeń rodents, *prats, rats, oraz &rodents, prats, &rats. Z tego
punktu widzenia referencja wygląda jak inaczej zapisane wskaźniki, w których operator dereferencji
jest dany niejawnie. Jednak poza samym zapisem są jeszcze inne różnice. Referencję trzeba
zainicjalizować w chwili jej deklarowania. Później już nie można tego zrobić. Tak jak w zmiennych z
przedrostkiem const. Inicjalizacja jest możliwa jedynie podczas deklaracji. Czyli te zapisy są pokrewne:

𝑖𝑛𝑡 & 𝑟𝑜𝑑𝑒𝑛𝑡𝑠 = 𝑟𝑎𝑡𝑠;


𝑖𝑛𝑡 ∗ 𝑐𝑜𝑛𝑠𝑡 𝑝𝑟 = &𝑟𝑎𝑡𝑠;
Rodents jest tutaj nazwą alternatywną dla rats. Wskaźnik można przestawić teraz na inną zmienną np.:

𝑝𝑟 = &𝑏𝑢𝑛𝑛𝑖𝑒𝑠;
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:

𝑖𝑛𝑡 𝑟𝑎𝑡𝑠 = 101;


𝑖𝑛𝑡 ∗ 𝑝𝑡 = &𝑟𝑎𝑡𝑠;
𝑖𝑛𝑡 & 𝑟𝑜𝑑𝑒𝑛𝑡𝑠 = ∗ 𝑝𝑡;
Spowoduje, że rodents będzie nazwą alternatywną dla tego co wskazuje wskaźnik pt, czyli – rats.

Referencje jako parametry funkcji


Najczęściej referencje używa się jako parametry funkcji, powodując, że nazwa zmiennej widoczna w
funkcji jest aliasem zmiennej programu wywołującego. Taka metoda przekazywania parametrów
nazywana jest przekazywaniem przez referencję. Taka metoda pozwala funkcji pracować na zmiennych
oryginalnych (a nie na kopiach jak w przekazywaniu argumentów przez wartość).

Zestawimy sobie przekazywanie parametrów funkcji przez wartość, wskaźnik i referencję. Przykładowe
wywołania funkcji:

𝑠𝑤𝑎𝑝𝑟(𝑤𝑎𝑙𝑙𝑒𝑡1, 𝑤𝑎𝑙𝑙𝑒𝑡2); (𝑝𝑟𝑧𝑒𝑘𝑎ż 𝑧𝑚𝑖𝑒𝑛𝑛𝑒 − 𝑝𝑟𝑧𝑒𝑧 𝑟𝑒𝑓𝑒𝑟𝑒𝑛𝑐𝑗𝑒)

𝑠𝑤𝑎𝑝𝑝(&𝑤𝑎𝑙𝑙𝑒𝑡1, &𝑤𝑎𝑙𝑙𝑒𝑡2); (𝑝𝑟𝑧𝑒𝑘𝑎ż 𝑎𝑑𝑟𝑒𝑠𝑦 𝑧𝑚𝑖𝑒𝑛𝑛𝑦𝑐ℎ − 𝑤𝑠𝑘𝑎ź𝑛𝑖𝑘)

𝑠𝑤𝑎𝑝𝑣(𝑤𝑎𝑙𝑙𝑒𝑡1, 𝑤𝑎𝑙𝑙𝑒𝑡2); (𝑝𝑟𝑧𝑒𝑘𝑎ż 𝑤𝑎𝑟𝑡𝑜ś𝑐𝑖 𝑧𝑚𝑖𝑒𝑛𝑛𝑦𝑐ℎ − 𝑝𝑟𝑧𝑒𝑧 𝑤𝑎𝑟𝑡𝑜ść)


W wywołaniu nie ma różnicy między przekazywaniem przez wartość, a przekazywaniem przez
referencje. Różnica widoczna jest tutaj dopiero w definicji lub prototypie 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)

𝑑𝑜𝑢𝑏𝑙𝑒 𝑐𝑢𝑏𝑒(𝑑𝑜𝑢𝑏𝑙𝑒 𝑎);


𝑑𝑜𝑢𝑏𝑙𝑒 𝑟𝑒𝑓𝑐𝑢𝑏𝑒(𝑑𝑜𝑢𝑏𝑙𝑒 &𝑟𝑎);

Gdzie ciało funkcji wygląda w ten sposób:

i analogicznie dla funkcji cube()

Różnica między tymi funkcjami jest taka, że wywołanie obu funkcji:

𝑐𝑜𝑢𝑡 ≪ 𝑐𝑢𝑏𝑒(𝑥);
𝑐𝑜𝑢𝑡 ≪ 𝑟𝑒𝑓𝑐𝑢𝑏𝑒(𝑥);

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:

 Kiedy parametr wywołania ma prawidłowy typ, ale nie jest l-wartością


 Kiedy parametr wywołania ma niewłaściwy typ, ale typ ten może być przekonwertowany na
właściwy

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

Przykładowe skutki odwołania się do tej funkcji w różny sposób:

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:

 Użycie const chroni nas przed błędami programistycznymi powodującymi niezamierzoną


zmianę wartości
 Użycie const pozwala funkcji przetwarzać wartości stałe i niestałe, a pominięcie w propotypie
const oznacza, że funkcja może przetwarzać tylko dane niebędące stałymi
 Użycie referencji stałej pozwala funkcji generować tymczasowe zmienne i ich używać.

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 &&

Wprowadzono ją głównie dla tworzenie efektywniejszych implementacji pewnych operacji w


bibliotekach – o tym więcej w rozdziale 18. Pierwotna idea referencji będzie teraz nazywana referencją
do l-wartości.

Więcej o tym wszystkim (o właściwościach referencji) na stronach (348-352)

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 &.

Deklaracja (prototyp) funkcji z referencją struktury (patrz str 352):

𝑓𝑟𝑒𝑒 𝑡ℎ𝑟𝑜𝑤𝑠 & 𝑎𝑐𝑐𝑢𝑚𝑢𝑙𝑎𝑡𝑒 (𝑓𝑟𝑒𝑒_𝑡ℎ𝑟𝑜𝑤𝑠 & 𝑡𝑎𝑟𝑔𝑒𝑡, 𝑐𝑜𝑛𝑠𝑡 𝑓𝑟𝑒𝑒_𝑡ℎ𝑟𝑜𝑤𝑠 & 𝑠𝑜𝑢𝑟𝑐𝑒);

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.

Dlaczego przy zwracaniu używać const? – str 357-358

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)

Kiedy używać referencji, kiedy wskaźników, a kiedy przekazywać przez


wartość (nie zawsze, ale często można tak):
1) funkcja korzysta z przekazanych danych ale ich nie modyfikuje
 Jeśli obiekt danych jest mały na przykład jest to wbudowany typ danych, lub mała struktura,
należy go przekazać przez wartość
 Jeśli obiekt danych to tablica, musimy użyć wskaźnika – powinien on być typu const
 Jeśli obiekt jest duży, należy użyć wskaźnika const lub referencji const, by w ten sposób
zwiększyć szybkość działania programu. Zaoszczędzimy czas i pamięć, gdyż nie będzie trzeba
kopiować struktury, czy klasy

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

2) Funkcja modyfikuje przekazane dane:


 Jeśli obiekt danych to typ wbudowany należy użyć wskaźnika. Jeśli natkniemy się gdzieś w
kodzie na zapis poprawto(&x), gdzie x jest typu int, będzie dość oczywiste, że zadaniem
funkcji jest modyfikacja x.
 Jeśli obiekt danych to tablica użyć musimy wskaźnika
 Jeśli obiekt danych to struktura używamy wskaźnika lub referencji
 Jeśli obiekt danych to egzemplarz klasy używamy 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ą.

Przykładowy prototyp funkcji:

𝑐ℎ𝑎𝑟 ∗ 𝑙𝑒𝑓𝑡 (𝑐𝑜𝑛𝑠𝑡 𝑐ℎ𝑎𝑟 ∗ 𝑠𝑡𝑟, 𝑖𝑛𝑡 𝑛 = 1);


Wywołanie

𝑙𝑒𝑓𝑡(„𝑡𝑒𝑜𝑟𝑖𝑎”, 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

𝑖𝑛𝑡 ℎ𝑎𝑟𝑝𝑜(𝑖𝑛𝑡 𝑛, 𝑖𝑛𝑡 𝑚 = 4, 𝑖𝑛𝑡 𝑗 = 5); (𝐷𝑂𝐵𝑅𝑍𝐸)

𝑖𝑛𝑡 𝑐ℎ𝑖𝑐𝑜(𝑖𝑛𝑡 𝑛, 𝑖𝑛𝑡 𝑚 = 6, 𝑖𝑛𝑡 𝑗); (Ź𝐿𝐸)

𝑖𝑛𝑡 𝑔𝑟𝑜𝑢𝑐ℎ𝑜(𝑖𝑛𝑡 𝑘 = 1, 𝑖𝑛𝑡 𝑚 = 2, 𝑖𝑛𝑡 𝑛 = 3); (𝐷𝑂𝐵𝑅𝑍𝐸)


Przykładowe wywołanie funkcji harpo():

𝑏𝑒𝑒𝑝𝑠 = ℎ𝑎𝑟𝑝𝑜(2); (𝑟ó𝑤𝑛𝑜𝑤𝑎ż𝑛𝑒 ℎ𝑎𝑟𝑝𝑜(2,4,5); )


𝑏𝑒𝑒𝑝𝑠 = ℎ𝑎𝑟𝑝𝑜(1,8); (𝑟ó𝑤𝑛𝑜𝑤𝑎ż𝑛𝑒 ℎ𝑎𝑟𝑝𝑜(1,8,5); )

37
𝑏𝑒𝑒𝑝𝑠 = ℎ𝑎𝑟𝑝𝑜(8,7,6); (𝑛𝑖𝑒 𝑏ę𝑑ą 𝑢ż𝑦𝑡𝑒 ż𝑎𝑑𝑛𝑒 𝑝𝑎𝑟𝑎𝑚𝑒𝑡𝑟𝑦 𝑑𝑜𝑚𝑦ś𝑙𝑛𝑒)

Argumenty są przypisywane odpowiednim parametrom od lewej strony do prawej; nie można


żadnych z nich pomijać. Wobec tego poniższy zapis jest niedozwolony:

𝑏𝑒𝑒𝑝𝑠 = ℎ𝑎𝑟𝑝𝑜(3, ,8); (Ź𝐿𝐸, 𝑚 𝑛𝑖𝑒 𝑧𝑜𝑠𝑡𝑎𝑛𝑖𝑒 𝑢𝑠𝑡𝑎𝑤𝑖𝑜𝑛𝑒 𝑛𝑎 4)


Parametry domyślne ustawiane są jedynie w prototypie (deklaracji), definicja funkcji ma już normalną
postać, bez parametrów domyślnych, jak by ich nie było – patrz str 366, listing 8.9.

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.:

𝑣𝑜𝑖𝑑 𝑝𝑟𝑖𝑛𝑡(𝑐𝑜𝑛𝑠𝑡 𝑐ℎ𝑎𝑟 ∗ 𝑠𝑡𝑟, 𝑖𝑛𝑡 𝑤𝑖𝑑𝑡ℎ);


𝑣𝑜𝑖𝑑 𝑝𝑟𝑖𝑛𝑡(𝑑𝑜𝑢𝑏𝑙𝑒 𝑑, 𝑖𝑛𝑡 𝑤𝑖𝑑𝑡ℎ);
𝑣𝑜𝑖𝑑 𝑝𝑟𝑖𝑛𝑡(𝑖𝑛𝑡 𝑖, 𝑖𝑛𝑡 𝑤𝑖𝑑𝑡ℎ);
Kompilator widzi, że funkcje są tak samo nazwane, ale odróżnia je po ilości i typie parametrów.

 Niektóre sygnatury pozornie się różnią ale mimo to nie mogą istnieć jednocześnie. Np. weźmy
pod uwagę dwa prototypy:

𝑑𝑜𝑢𝑏𝑙𝑒 𝑐𝑢𝑏𝑒(𝑑𝑜𝑢𝑏𝑙𝑒 𝑥);


𝑑𝑜𝑢𝑏𝑙𝑒 𝑐𝑢𝑏𝑒(𝑑𝑜𝑢𝑏𝑙𝑒 &𝑥);
Można by pomyśleć, że mamy przeciążenie funkcji, gdyż sygnatury są różne. Jest jednak inaczej,
parametr x pasuje do obu prototypów. Kompilator nie jest w stanie stwierdzić, której funkcji użyć.

 Kompilator rozróżnia zmienne z modyfikatorem const i bez niego.

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
𝑑𝑜𝑢𝑏𝑙𝑒 𝑔𝑟𝑜𝑛𝑘(𝑓𝑙𝑜𝑎𝑡 𝑛, 𝑓𝑙𝑜𝑎𝑡 𝑚); (𝑡𝑒𝑟𝑎𝑧 𝑗𝑢ż 𝑗𝑒𝑠𝑡 𝑤 𝑝𝑜𝑟𝑧ą𝑑𝑘𝑢)

Przeciążenia do parametrów referencyjnych (str 369-372)


Kiedy korzystać z przeciążenia funkcji
W przypadku funkcji realizujących te same zadania, ale na różnego rodzaju danych. Poza tym warto się
zastanowić czy nie da się tego samego osiągnąć wykorzystując parametrów domyślnych. (str 372)

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:

𝑡𝑒𝑚𝑝𝑙𝑎𝑡𝑒 < 𝑡𝑦𝑝𝑒𝑛𝑎𝑚𝑒 𝐴𝑛𝑦𝑇𝑦𝑝𝑒 > (𝑛𝑜𝑤𝑠𝑧𝑒)


, lub

𝑡𝑒𝑚𝑝𝑙𝑎𝑡𝑒 < 𝑐𝑙𝑎𝑠𝑠 𝐴𝑛𝑦𝑇𝑦𝑝𝑒 > (𝑠𝑡𝑎𝑟𝑠𝑧𝑒)


Linia ta deklaruje tworzenie szablonu, ustala też, że typ parametryzujący będzie się nazywał AnyType.
Linia ta musi być nad każdym prototypem (deklaracją) funkcji, oraz jej definicją (czyli do jednej funkcji
może pojawić się w dwóch miejscach). Słowa kluczowe template i typename są obowiązkowe, tyle że
zamiast słowa typename można użyć class. Obowiązkowe są też nawiasy kątowe. Nazwa typu (u nas
AnyType) jest dowolna.

Po tej linijce dalej można napisać funkcję na przykład:

𝑣𝑜𝑖𝑑 𝑆𝑤𝑎𝑝(𝐴𝑛𝑦𝑇𝑦𝑝𝑒 &𝑎, 𝐴𝑛𝑦𝑇𝑦𝑝𝑒 &𝑏){

𝐴𝑛𝑦𝑇𝑦𝑝𝑒 = 𝑡𝑒𝑚𝑝;
𝑡𝑒𝑚𝑝 = 𝑎;
𝑎 = 𝑏;
𝑏 = 𝑡𝑒𝑚𝑝; }
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
𝑡𝑒𝑚𝑝𝑙𝑎𝑡𝑒 𝑣𝑜𝑖𝑑 𝑆𝑤𝑎𝑝 < 𝑖𝑛𝑡 > (𝑖𝑛𝑡, 𝑖𝑛𝑡); (𝑗𝑎𝑤𝑛𝑎 𝑘𝑜𝑛𝑘𝑟𝑒𝑡𝑦𝑧𝑎𝑐𝑗𝑎)

Kompilator po natknięciu się na powyższą deklarację na podstawie szablonu Swap() utworzy


egzemplarz z typem int. Deklaracja ta znaczy więc : „użyj szablonu Swap() do wygenerowania definicji
funkcji dla typu int”.

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.

Strategia rozwiązywania przeciążeń:

Kompilator ma strategie postępowania z przeciążeniami funkcji, szablonami funkcji, przeciążeniami


szablonów. Pozwala ona decydować której definicji funkcji użyć w poszczególnych wywołaniach.

1) Zbieranie listy funkcji potencjalnie przydatnych w danej sytuacji. Wybierane są funkcje i


szablony funkcji mające taką samą nazwę jak funkcje wywołane
2) Spośród funkcji potencjalnie przydatnych wybierane są funkcje pasujące. Są to funkcje mające
odpowiednią liczbę parametrów, dla których istnieją niejawne metody konkwersji lub
dokładnie dopasowanie typu parametru. Na przykład jeśli funkcja jest wywwoływana z
parametrem typu float, przekazana wartość zostanie skonwertowana na typ double, aby
pasowała do parametru double, ale w przypadku konkretyzacji będzie ona już dotyczyć typu
float
3) Sprawdzenie czy istnieje funkcja pasująca najlepiej. Jeśli tak, zostanie ona użyta. Jeśli nie –
wystąpi błąd.

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)

Mamy szablon lesser() z typem T, oraz wartości: (str 387-388)

42
𝑖𝑛𝑡 𝑚 = 20; 𝑖𝑛𝑡 𝑛 = −30; 𝑑𝑜𝑢𝑏𝑙𝑒 𝑥 = 15.5; 𝑑𝑜𝑢𝑏𝑙𝑒 𝑦 = 25.9;
 Zapis:

𝑙𝑒𝑠𝑠𝑒𝑟 <> (𝑚, 𝑛);


Powoduje, że kompilator powinien wybrac wersję szablonową a nie zwykłą

 Zapis:

𝑙𝑒𝑠𝑠𝑒𝑟 < 𝑖𝑛𝑡 > (𝑥, 𝑦);


To jawne żądanie konkretyzacji szablonu z typem int jako T i ta właśnie funkcja jest tu
wywołana. Wartości x,y są w ramach wywołania funkcji rzutowane na typ int, a sama funkcja
też zwraca typ int (jest pełne rzutowanie typów)

Słowo decltype (C++ 11)

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

Można też zastosować typedef jeśli mamy więcej deklaracji:

𝑡𝑦𝑝𝑒𝑑𝑒𝑓 𝑑𝑒𝑐𝑙𝑡𝑦𝑝𝑒(𝑥 + 𝑦) 𝑥𝑦𝑡𝑦𝑝𝑒;

𝑥𝑦𝑡𝑦𝑝𝑒 𝑥𝑝𝑦 = 𝑥 + 𝑦;
(𝑠𝑡𝑤𝑜𝑟𝑧𝑦𝑙𝑖ś𝑚𝑦 𝑡𝑦𝑝 𝑥𝑦𝑡𝑦𝑝𝑒 𝑟ó𝑤𝑛𝑜𝑤𝑎ż𝑛𝑦 𝑡𝑦𝑝𝑜𝑤𝑖 𝑤𝑦𝑛𝑖𝑘𝑎𝑗ą𝑐𝑒𝑚𝑢 𝑧 𝑑𝑒𝑐𝑙𝑡𝑦𝑝𝑒(𝑥 + 𝑦))

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)

𝑑𝑒𝑐𝑙𝑡𝑦𝑝𝑒((𝑥𝑥))𝑟2 (𝑟2 𝑗𝑒𝑠𝑡 𝑟𝑒𝑓𝑒𝑟𝑒𝑛𝑐𝑗ą 𝑥𝑥)

 Jeśli nie udało się rozstrzygnąć typu na poprzednich etapach, zmienna będzie tego samego
typu co wyrażenie

Alternatywna składnia funkcji (opóźniona deklaracja typu zwracanego)


Słowo decltype z wyrażeniem angażującym parametry funkcji może występować jedynie po
zadeklarowaniu tych parametrów. Dlatego taki zapis jest problematyczny

I stworzono składnie alternatywną

𝑎𝑢𝑡𝑜 ℎ(𝑖𝑛𝑡 𝑥, 𝑓𝑙𝑜𝑎𝑡 𝑦)−> 𝑑𝑜𝑢𝑏𝑙𝑒;

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ęć automatyczna – zmienne deklarowane wewnątrz definicji funkcji . Są powołane do życia w


momencie przekazywania sterowania do wnętrza funkcji lub bloku, w której są definiowane i zwalniane
po zakończeniu wykonywania funkcji (bloku). Są to generalnie zmienne LOKALNE. Są przechowywane
na stosie – last in first out (LIFO)

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 z łączeniem wewnętrznym – z dostępem z funkcji z pojedynczego pliku. Należy ją


zadeklarować poza blokiem kodu ze słowem static.

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)

Łączenie językowe – str 422-423


Mowa tu o specjalnych identyfikatorach, które mają każde funkcje, a dzięki którym kompilator jest w
stanie określić daną funkcję.

Kategorie przydziału a przydział dynamiczny


Typowo kompilator korzysta z trzech oddzielnych obszarów pamięci – jednego dla zmiennych
statycznych (ten może być podzielony na mniejsze bloki), jednego dla zmiennych automatycznych
(stos), jednego dla zmiennych dynamicznych (sterta)

Inicjalizacja new:

𝑖𝑛𝑡 ∗ 𝑝𝑖 = 𝑛𝑒𝑤 𝑖𝑛𝑡 (6)//𝑠𝑡𝑎𝑟𝑦 𝑠𝑡𝑎𝑛𝑑𝑎𝑟𝑑


𝑖𝑛𝑡 ∗ 𝑎𝑟 = 𝑛𝑒𝑤 𝑖𝑛𝑡[4]{2,4,5,6} (𝑐 + +11)
𝑑𝑜𝑢𝑏𝑙𝑒 ∗ 𝑝𝑑𝑜 = 𝑛𝑒𝑤 𝑑𝑜𝑢𝑏𝑙𝑒 {99.99} (𝑐 + +11)

Kiedy new zawiedzie


Moze się zdarzyć, że operator new nie zdoła przydzielić potrzebnej ilości pamięci. W pierwszej dekadzie
istnienia języka c++ taka ewentualność była obsługiwana w ten sposób, że operator new zwracał
wskaźnik pusty. Obecnie jednak nieudane wywołanie new prowokuje zrzucenie wyjątku bad_alloc. O
działaniu każdej z tych metod będzie w rozdziale 15.

Miejscowa wersja operatora new


Normalnie operator new wyszukuje w pamięci sterty odpowiednio duży blok wolnej pamięci nadający
się do realizacji żądania przydziału pamięci. Istnieje jednak też miejscowa wersja operatora new –
miejscowy operator new. Pozwala on na wskazywanie lokacji przydziału przez wywołującego. By z
niego skorzystać należy włączyć do programu plik nagłówkowy new. Od tego momentu można
wywołać operator new z argumentem wskazującym źródłowy adres przydziału. Poza dodatkowym
argumentem składnia wywołania nie różni się od składni klasycznej wersji 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).

Tradycyjne przestrzenie nazw języka c++


Obszar deklaracyjny – cały blok w którym można umieszczać deklaracje

Zasięg potencjalny – zasięg działania zmiennej (od początku jej deklaracji do końca obszaru
deklaracyjnego). Jest mniejszy niż obszar deklaracyjny

Zasięg zmiennej – zasięg potencjalny ale pomniejszony o ewentualne przesłanianie zmiennych


globalnych przez zmienne lokalne itp.

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ć:

Odnosić się do nich można w ten sposób:

𝐽𝑎𝑐𝑘 ∷ 𝑝𝑎𝑖𝑙 = 12.42;


𝐽𝑖𝑙𝑙 ∷ 𝑓𝑒𝑡𝑐ℎ();

Można też skorzystać z dyrektywy using jak w przypadku przestrzeni nazw std:

𝑢𝑠𝑖𝑛𝑔 𝐽𝑖𝑙𝑙 ∷ 𝑓𝑒𝑡𝑐ℎ;


Wprowadza to nazwę fetch do obszaru deklaracyjnego wyznaczonego funkcją main(). Po tym można
stosować normalnie nazwę fetch zamiast Jill::fetch.

Lub cały komplet nazw z przestrzeni nazw:

𝑢𝑠𝑖𝑛𝑔 𝑛𝑎𝑚𝑒𝑠𝑝𝑎𝑐𝑒 𝐽𝑖𝑙𝑙;


Jeśli dyrektywa using zaimportuje nazwę która jest już zdefiniowana w funkcji, nazwa lokalna będzie
zakrywać zarówno nazwę importowaną z przestzrni nazw jak i ewentualną nazwę deklarowaną
globalnie. Wciąż można odwołąć się jednak do nazwy z przestrzeni nazw stosując operator zasięgu:

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)

Przestrzenie nazw można też zagnieżdzać.

Jak dokładnie korzystać z przestrzeni nazw opis na str 436-441

Nienazwane przestrzenie nazw


Zastępują przydział statyczny, łączenie wewnętrzne (str 437):

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ń):

Rozdział 10. Obiekty i klasy

 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

Elementy danych powinny z reguły trafiać do obszaru składowych prywatnych.

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.

RÓŻNICA MIĘDZY STRUKTURAMI A KLASAMI : STRUKTURY SĄ PRZEZ DOMNIEMANIE PUBLICZNE, A


KLASY PRYWATNE!

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.

Obiekt takiej klasy tworzy się w taki sposób:


𝐻𝑖𝑔ℎ𝑇𝑒𝑚𝑝𝑙𝑎𝑟 𝑝𝑒𝑟𝑠𝑜𝑛1;
Person1 jest obiektem klasy HighTemplar. Odwołujemy się do poszczególnych metod
obiektów w ten sposób:
𝑝𝑒𝑟𝑠𝑜𝑛1. 𝑃𝑠𝑖𝑜𝑛𝑖𝑐_𝑆𝑡𝑜𝑟𝑚;
W wywołaniu metody nazwy składowych odnoszą się do nazw obiektu na rzecz którego
nastąpiło wywołanie. Czyli jeśli wewnątrz metody jest jakieś odwołanie do atrybutu, to ten
atrybut jest częścią tej klasy, której obiekt jest w użytku.
W pamięci każdy nowo utworzony obiekt ma swoje własne miejsce, jednak metody w danej
klasie już nie są rezerwowane osobno dla każdego obiektu. Jest jedna metoda dla wszystkich
obiektów korzystających z danej klasy.

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.

Deklarowanie i definiowanie konstruktorów


Przed zastosowaniem konstruktora

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:

𝑆𝑡𝑜𝑐𝑘 𝑓𝑜𝑜𝑑 = 𝑆𝑡𝑜𝑐𝑘("KapustPOL", 250, 1.25);


Sposób niejawny:
𝑆𝑡𝑜𝑐𝑘 𝑓𝑜𝑜𝑑("KapusPOl", 250, 1.25);
Język C++ zakłada korzystanie z konstruktora klasy wszędzie tam gdzie tworzony jest obiekt tej
klasy, nawet jeśli programista tworzy obiekt wywołaniem operatora new. :
𝑆𝑡𝑜𝑐𝑘 ∗ 𝑝𝑠𝑡𝑜𝑐𝑘 = 𝑛𝑒𝑤 𝑆𝑡𝑜𝑐𝑘("𝐸𝑙𝑒𝑘𝑡𝑟𝑜𝑃𝑂𝐿", 25,1.2);
Taka instrukcja tworzy obiekt Stock, inicjalizuje go wartościami argumentów konstruktora i
przypisuje do wskaźnika stojącego po lewej stronie przypisania adres gotowego obiektu. O
wskaźnikach obiektów bardziej szczegółowo w rozdz 11.
Konstruktor możemy też tworzyć domyślny – bez argumentów. Można to zrobić albo
definiując wartości początkowe w definicji konstuktora:
𝑆𝑡𝑜𝑐𝑘(𝑐𝑜𝑛𝑠𝑡 𝑠𝑡𝑟𝑖𝑛𝑔 &𝑐𝑜 = "𝐵łą𝑑", 𝑖𝑛𝑡 𝑛 = 0, 𝑑𝑜𝑢𝑏𝑙𝑒 𝑝𝑟 = 0.0);
Albo korzystając z przeciążenia i zdefiniować drugi konstruktor, tym razem bez żadnych
argumentów:

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:

Jeśli programista zaniedba zdefiniowanie kosntruktrów klasy, kompilator wygeneruje


konstruktor domyślny na własną rękę. Jeśli jednak zdefiniujemy sami jakikolwiek konstruktor
klasy, powinniśmy zdefiniować również konstruktor domyślny. Może to być osobny
kosntruktor bez parametrów albo konstruktor z definicjami wartośici domyślnych dla
wszystkich parametrów
Destruktor
PO CO NAM DESTRUKTOR?
Destruktor to jak by sprzątaczka, która sprząta podczas usuwania obiektu (ten usuwa się
automatycznie w momencie, w którym wychodzi ze swojego obszaru zasięgu. Przykład z
radarem na lotnisku. Kropka na ekranie reprezentująca samolot to obiekt. Usuwany jest on
gdy samolot wyląduje na lotnisku. Wtedy właśnie warto skorzystać z destruktora. Wprawdzie
pamięć zarezerwowana na rzecz obiektu będzie zwolniona automatycznie, jednak np. może
być wskaźnik pokazujący poza obszarem klasy na jakiś atrybut obiektu tej klasy. Wtedy po
usunięciu pamięci wskaźnik ten dalej będzie wskazywał na ten fragment pamięci, jednak w tej
komórce pamięci będą już jakieś smieci. To może spowodować błąd krytyczny. W celu
uniknięcia tego używa się destruktora, by posprzątał śmieci, czyli w tym przypadku przestawił
wskaźnik na null. Może on też np. wykorzystać operator delete w celu usunięcia
zarezerwowanej operatorem new pamięci. Itp.
Destruktor – specjalna metoda, wywoływana tuż przed usunięciem obiektu, mająca za zadanie
wykonać czynności składające się na jego „znieszczenie”, inne niż zwolnienie pamięci
zajmowanej przez sam obiekt (to się dzieje zawsze automatycznie)
 Destruktor ma nazwę identyczną jak nazwa klasy lecz poprzedzoną znakiem ~
 Destruktor nie może mieć określonego typu zwracanego, podobnie jak konstruktor
 Destruktor wywoływany jest automatycznie kiedy obiekt klasy jest likwidowany
60
 Destruktor nie może mieć żadnych argumentów, stąd może istnieć tylko jeden
destruktor wewnątrz klasy (nie ma przeciążeń nazwy)
 Destruktor może wywoływać inną metodę składową swojej klasy
 Jeśli w klasie nie ma zdefiniowanego przez programistę destruktora, to kompilator sam
wygeneruje destruktor dla klasy
Gdy destruktor nie ma nic do roboty, zadanie wygenerowania destruktora można przekazać
kompilatorowi, który niejawnie zdefiniuje taki nic nie robiący destruktor.
Prototyp destruktora klasy Stock powinien wyglądać tak:
~𝑆𝑡𝑜𝑐𝑘( );
Jako, że destruktor klasy Stock nie ma żadnych obowiązków jego definicja może być pusta:
𝑆𝑡𝑜𝑐𝑘 ∷ ~𝑆𝑡𝑜𝑐𝑘( ){
}
Kiedy należy wywołać destruktor? Decyzję tę podejmuje kompilator, normalnie kod nie
powinien zawierać wywołać destruktorów (z wyjątkiem opisanym w punkcie „jeszcze o
miejscowej wersji new” w rozdziale 12). Jeśli tworzymy obiekt przydziału statycznego, jego
destruktor zostanie wywołany automatycznie po zakończeniu programu. W przypadku
obiektu przydziału automatycznego jego destruktor zostanie wywołany automatycznie przy
opuszczeniu bloku kodu, w którym obiekt został zdefiniowany. Kiedy zaś obiekt zostanie
przydzielony dynamicznie wywołaniem operatora new, jego destruktor zostanie wywołany z
poziomu operatora delete przy zwalnianiu obiektu. Wreszcie program może tworzyć obiekty
tymczasowe na użytek niektórych operacji, obiekty te program zwalnia automatycznie i wtedy
też automatycznie następuje wywołanie desuktorów. Z racji automatyzacji wywołań
destruktorów w momencie zwalniania obiektu destruktor jest obowiązkowym elementem
klasy. Jeśli programista nie dostarczy jego deklaracji, zrobi to niejawnie kompilator, a jeśli
wykryje kod prowadzący do zwolnienia obiektu, wygeneruje też definicję jego destruktora.

%%%%%%%%%%%%%%%%%% Przykład – str 469-474 %%%%%%%%%%%%%%%%%%%

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.

UWAGA: Mając wybór między ustaleniem wartości obiektu przez inicjalizacje i


przez przypisanie, należy wybierać inicjalizację. Jest to zazwyczaj operacja
efektywniejsza!
%%%%%%%%%%%%%%%% KONIEC OPISU PRZYKŁADU %%%%%%%%%%%%%%%%%%

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/

Inicjalizacja listą w c++ 11


W standardzie 11 można inicjalizować klasy listą o ile typy danych są zgodne z konstruktorem klasy:

𝑆𝑡𝑜𝑐𝑘 ℎ𝑜𝑡𝑡𝑖𝑝 {"𝑆𝑝𝑜𝑟𝑡", 43,2.5}

𝑆𝑡𝑜𝑐𝑘 𝑡𝑒𝑚𝑝 { }
𝑆𝑡𝑜𝑐𝑘 𝑗𝑜𝑐 = {"𝑆𝑡𝑒𝑑", 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

Metody niemodyfikowalne (const)


𝑐𝑜𝑛𝑠𝑡 𝑆𝑡𝑜𝑐𝑘 𝑙𝑎𝑛𝑑 = 𝑆𝑡𝑜𝑐𝑘("𝐴𝑅𝑅");

𝑙𝑎𝑛𝑑. 𝑠ℎ𝑜𝑤( );
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.

KONSTUKTORY I DESTRUKTORY PODSUMOWANIE – str 475-476

Tożsamość obiektu – wskaźnik this


Mamy metodę, która ma za zadanie porównywać dwa obiekty i zwracać ten który jest większy:

𝑐𝑜𝑛𝑠𝑡 𝑆𝑡𝑜𝑐𝑘 & 𝑡𝑜𝑝𝑣𝑎𝑙 (𝑐𝑜𝑛𝑠𝑡 𝑆𝑡𝑜𝑐𝑘 & 𝑠)𝑐𝑜𝑛𝑠𝑡;


Funkcja ta odwołuje się do jednego obiektu niejawnie, a do drugiego jawnie, w wyniku tego zwraca
referencję jednego z nich. Modyfikator const wewnątrz nawiasów obiecuje, że funkcja nie będzie
modyfikować obiektu, z którego korzysta jawnie, a const znajdujący się za listą parametrów to
obietnica, że metoda nie zmieni również wartości obiektu przekazywanego doń niejawnie. Skoro oba
obiekty porównywane w metodzie są deklarowane z const to i wartość zwracana będzie
niemodyfikowalna – stąd const w deklaracji typu zwracanego. Załózmy więc, że chcemy porównać dwa
obiekty klasy Stock: stock1 i stock2, przypisując większy z nich do obiektu top. Operację taką możemy
wykonać na dwa sposoby:

𝑡𝑜𝑝 = 𝑠𝑡𝑜𝑐𝑘1. 𝑡𝑜𝑝𝑣𝑎𝑙(𝑠𝑡𝑜𝑐𝑘2);


Bo, wywołujemy metodę topval, na rzecz obiektu stock1, co powoduje, że niejawnie w obrębie tej
metody możemy korzystać z wartości przypisanych do obiektu stock1. Dodatkowo argumentem
metody jest obiekt stock2, więc w obrębie tej metody mamy również do dyspozycji wartości przypisane
do stock2. Metoda zwraca stałą referencję typu Stock. Referencja jest do obiektu top. Jej obiekt więc
zmieniamy.

Lub:

𝑡𝑜𝑝 = 𝑠𝑡𝑜𝑐𝑘2. 𝑡𝑜𝑝𝑣𝑎𝑙(𝑠𝑡𝑜𝑐𝑘1);


Co jest taką samą sytuacją tylko na odwrót. Następnie w metodzie wywołanej (topval), możemy
porównać oba obiekty:

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.

Przykład pełny (str 480 – 482)

Tablice obiektów
Tablice obiektów klasy działają tak samo jak normalne tablice, np:

𝑆𝑡𝑜𝑐𝑘 𝑚𝑦𝑠𝑡𝑢𝑓𝑓[4]; (𝑡𝑤𝑜𝑟𝑧𝑦 𝑡𝑎𝑏𝑙𝑖𝑐ę 4 𝑜𝑏𝑖𝑒𝑘𝑡ó𝑤 𝑘𝑙𝑎𝑠𝑦 𝑆𝑡𝑜𝑐𝑘)

𝑚𝑦𝑠𝑡𝑢𝑓𝑓[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.

𝑆𝑡𝑜𝑐𝑘 𝑠𝑙𝑒𝑒𝑝𝑒𝑟("𝐾𝑟𝑧𝑎", 10, 0.24); (𝑢𝑡𝑤𝑜𝑟𝑧𝑒𝑛𝑖𝑒 𝑜𝑏𝑖𝑒𝑘𝑡𝑢)


𝑠𝑙𝑒𝑒𝑝𝑒𝑟. 𝑠ℎ𝑜𝑤( ) (𝑧𝑎𝑠𝑡𝑜𝑠𝑜𝑤𝑎𝑛𝑖𝑒 𝑜𝑏𝑖𝑒𝑘𝑡𝑢 𝑑𝑜 𝑤𝑦𝑤𝑜ł𝑎𝑛𝑖𝑎 𝑚𝑒𝑡𝑜𝑑𝑦)
𝑠ℎ𝑜𝑤( ) (𝑛𝑖𝑒𝑝𝑜𝑝𝑟𝑎𝑤𝑛𝑒, 𝑏𝑜 𝑛𝑖𝑒 𝑚𝑜ż𝑛𝑎 𝑤𝑦𝑤𝑜ł𝑎ć 𝑚𝑒𝑡𝑜𝑑𝑦 𝑏𝑒𝑧𝑝𝑜ś𝑟𝑒𝑑𝑛𝑖𝑜)

Podczas definiowania klas też trzeba posłużyć się operatorem zasięgu:

𝑣𝑜𝑖𝑑 𝑆𝑡𝑜𝑐𝑘 ∷ 𝑢𝑝𝑑𝑎𝑡𝑒(𝑑𝑜𝑢𝑏𝑙𝑒 𝑝𝑟𝑖𝑐𝑒){ }


Krótko mówiąc wewnątrz deklaracji klasy i w ciele definicji metody można korzystać ze skróconych
(niekwalifikowanych) nazw składowych klasy, jak w przypadku metody sell( ), w której mamy
wywołanie set_tot( ) – bez rzadnych dodatkowych odwołań by była ona widoczna przez metodę sell(
). Samodzielnie występująca nazwa konstruktora jest rozpoznawalna tylko dlatego, że jest identyczna
z nazwą klasy. W pozostałych przypadkach w odwołaniach do nazw składowych trzeba stosować
operator dostępu do składowej (.), operator odwołania pośredniego do składowej (->), albo operator
zasięgu (::), zależnie od kontekstu.

Przykład – str 485

Stałe zasięgu klasy


Przydatne może się okazać stworzenie stałej wartości, którą będą mogły się posługiwać wszystkie klasy,
np. przy określaniu wielkości tablicy. Z pozoru postulat ten spełnia deklaracja:

𝑐𝑙𝑎𝑠𝑠 𝐵𝑎𝑘𝑒𝑟𝑦{
𝑝𝑟𝑖𝑣𝑎𝑡𝑒:
𝑐𝑜𝑛𝑠𝑡 𝑖𝑛𝑡 𝑀𝑜𝑛𝑡ℎ𝑠 = 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};
𝑑𝑜𝑢𝑏𝑙𝑒 𝑐𝑜𝑠𝑡𝑠 [𝑀𝑜𝑛𝑡ℎ𝑠]; }

Drugą możlwiością jest użycie słowa kluczowego static:

𝑐𝑙𝑎𝑠𝑠 𝐵𝑎𝑘𝑒𝑟𝑦{
𝑝𝑟𝑖𝑣𝑎𝑡𝑒:
𝑠𝑡𝑎𝑡𝑖𝑐 𝑐𝑜𝑛𝑠𝑡 𝑖𝑛𝑡 𝑀𝑜𝑛𝑡ℎ𝑠 = 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)

Wyliczenia z własnym zasięgiem (c++ 11)


Wada typu wyliczeniowego jest taka , że elementy dwóch różnych wyliczeń mogą ze sobą kolidować.
Załóżmy, że pracujemy nad projektem, którego elementami są obiekty opisaujące jajka i koszulki:

𝑒𝑛𝑢𝑚 𝑒𝑔𝑔{𝑆𝑚𝑎𝑙𝑙, 𝑀𝑒𝑑𝑖𝑢𝑚, 𝐽𝑢𝑚𝑏𝑜};


𝑒𝑛𝑢𝑚 𝑡𝑠ℎ𝑖𝑟𝑡{𝑆𝑚𝑎𝑙𝑙, 𝑀𝑒𝑑𝑖𝑢𝑚, 𝑋𝑙𝑎𝑟𝑔𝑒};

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:

𝑒𝑛𝑢𝑚 𝑐𝑙𝑎𝑠𝑠 𝑒𝑔𝑔{𝑆𝑚𝑎𝑙𝑙, 𝑀𝑒𝑑𝑖𝑢𝑚, 𝐽𝑢𝑚𝑏𝑜};


𝑒𝑛𝑢𝑚 𝑐𝑙𝑎𝑠𝑠 𝑡𝑠ℎ𝑖𝑟𝑡{𝑆𝑚𝑎𝑙𝑙, 𝑀𝑒𝑑𝑖𝑢𝑚, 𝑋𝑙𝑎𝑟𝑔𝑒};
Zamiast słowa class można ewentualnie użyć słowa sturct. Tak czy inaczej do elementów wyliczenia
trzeba wtedy stosować kwalifikację nazwą typu wyliczeniowego jak tutaj:

𝑒𝑔𝑔 𝑐ℎ𝑜𝑖𝑐𝑒 = 𝑒𝑔𝑔 ∷ 𝑀𝑒𝑑𝑖𝑢𝑚;

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.

Wyliczenia są wewnętrznie reprezentowane którymś z typów. Muszą być całkowitoliczbowe i w


klasycznym c++ wybór typu był zależny od implementacji. W c++ 11 ustawiono domyślny typ – int.
Można oczywiście ten typ zmienić:

𝑒𝑛𝑢𝑚 𝑐𝑙𝑎𝑠𝑠 ∶ 𝑠ℎ𝑜𝑟𝑡 𝑝𝑖𝑧𝑧𝑎 {𝑠𝑚𝑎𝑙𝑙, 𝑚𝑒𝑑𝑖𝑢𝑚, 𝑥𝑙𝑎𝑟𝑔𝑒};


Komponent short wymusza na kompilatorze zastosowanie dla tego wyliczenia typu short.

Abstrakcyjne typy danych


Str 488-491

Rozdział 11. Stosowanie klas

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 +:

𝑒𝑣𝑒𝑛𝑖𝑛𝑔 = 𝑠𝑎𝑚 + 𝑗𝑎𝑛𝑒𝑡;


By przeciążyć operator należy zdefiniować dla klasy specjalną funkcję zwaną funkcją operatora. Funkcja
operatora ma postać:

𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟𝑜𝑝(𝑙𝑖𝑠𝑡𝑎 − 𝑝𝑎𝑟𝑎𝑚𝑒𝑡𝑟ó𝑤)
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 ( ):

𝑡𝑜𝑡𝑎𝑙 = 𝑐𝑜𝑑𝑖𝑛𝑔. 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 + (𝑓𝑖𝑥𝑖𝑛𝑔) (𝑧𝑎𝑝𝑖𝑠 𝑓𝑢𝑛𝑘𝑐𝑦𝑗𝑛𝑦)


Z powyższego wynika, że metoda operatora odwołuje się do obiektu coding niejawnie (jest to obiekt
wywołujący) i jawnie przyjmuje w wywołaniu obiekt fixing (przekazany argumentem wywołania).
Zauważ, że tam nie ma dodawania. To jest nazwa metody – „operator+( )”.

Ale dzięki specjalnemu nazewnictwu metod operatora możemy też zastosować wygodniejszy zapis
operatorowy:

𝑡𝑜𝑡𝑎𝑙 = 𝑐𝑜𝑑𝑖𝑛𝑔 + 𝑓𝑖𝑥𝑖𝑛𝑔 (𝑧𝑎𝑝𝑖𝑠 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟𝑜𝑤𝑦)


Obie notacje prowokują wywołanie metody operator+ ( ). Istotne jest by zwrócić uwagę, że pierwszy
operand po lewej (ten bliżej znaku równa się), jest tym, na rzecz którego wywoływana jest funkcja w
zapisie funkcyjnym (wywołanie metody – pierwsza opcja opisywana powyżej).

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:

𝑖𝑛𝑡 𝑎, 𝑏, 𝑐;
𝑇𝑖𝑚𝑒 𝐴, 𝐵, 𝐶;
𝑐 = 𝑎 + 𝑏; (𝑑𝑜𝑑𝑎𝑤𝑎𝑛𝑖𝑒 𝑤𝑎𝑟𝑡𝑜ś𝑐𝑖 𝑖𝑛𝑡)
𝐶 = 𝐴 + 𝐵; (𝑑𝑜𝑑𝑎𝑤𝑎𝑛𝑖𝑒 𝑧𝑑𝑒𝑓𝑖𝑛𝑖𝑜𝑤𝑎𝑛𝑒 𝑑𝑙𝑎 𝑜𝑏𝑖𝑒𝑘𝑡ó𝑤 𝑘𝑙𝑎𝑠𝑦 𝑇𝑖𝑚𝑒)

Możliwe jest sumowanie większej ilości obiektó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:

 przeciążony operator musi przyjmować przynajmniej jeden operand typu własnego,


definiowanego przez użytkownika. Zapobiega to przeciążeniu operatorów przewidzianych dla
typów standardowych i wbudowanych. Nie można więc na własną rękę przedefiniować
operatora odejmowania (-) tak aby na przykład wywołany dla dwóch wartości typu double
zwracał ich sumę, a nie różnicę.
 Nie można korzystać z operatora w sposób naruszający reguły składni pierwotnej wersji
operatora. Nie można na przykład przeciążyć operatora modulo tak by dało się go wywołać dla
jednego operandu int x;

𝑇𝑖𝑚𝑒 𝑠ℎ𝑖𝑣𝑎;
%𝑥; (niedozwolone zastosowanie operatora modulo)
%𝑠ℎ𝑖𝑣𝑎 (również niedozwolone zastosowanie operatora przeciążonego)

Podobnie nie da się w c++ zmieniać pierwszeństwa operatorów

 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()

b) . (operator dostępu do składowej)

c) .* (operator wskazania składowej)

d) :: (operator zasięgu)

e) ?: (operator warunkowy - alternatywny if() )

f) typeid (operator RTTI)

g) const_cast, dynamic_cast, reinterpret_cast, static_cast (operatory rzutowania)

 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)

b) () (operator wywołania funkcji)

c) [] (operator indeksowania)

d) -> (operator dostępu do składowej przez wskaźnik)

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:

 Funkcja jest przyjacielem dwu klas,


 Funkcja jest składnikiem jednej, a przyjacielem drugiej

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:

𝐴 = 2.75 ∗ 𝐵; (𝑛𝑖𝑒 𝑚𝑜ż𝑒 𝑏𝑦ć 𝑤𝑦𝑤𝑜ł𝑎𝑛𝑖𝑒𝑚 𝑚𝑒𝑡𝑜𝑑𝑦 𝐵)


Logicznie rzecz biorąc 2.75*B powinno oznaczać to samo co B*2.75, ale pierwszego z tych wyrażeń nie
można zrealizować wywołaniem metody, ponieważ wartość 2.75 nie jest obiektem typu Time. Jak
pamiętamy, obiektem wywołującym metodę jest lewy operand, a tu mamy w tej roli 2.75. Możemy
poradzić sobię z tym problemem w taki sposób, że poinformujemy wszystkich potencjalnych
użytkowników klasy Item o konieczności stosowania zapisu B*2.75, nigdy zaś 2.75*B. Nie jest to dobre
rozwiązanie dla klientów. Mamy drugą – lepszą – możliwość. Przeciążyć operator funkcją niebędącą

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:

𝐴 = 2.75 ∗ 𝐵; (𝑛𝑖𝑒 𝑚𝑜ż𝑒 𝑏𝑦ć 𝑤𝑦𝑤𝑜ł𝑎𝑛𝑖𝑒𝑚 𝑚𝑒𝑡𝑜𝑑𝑦 𝐵)

Do poniższego wywołania funkcji nieskładowej:

𝐴 = 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 ∗ (2.75, 𝐵);


Prototyp takiej funkcji wyglądał by następująco:

𝑇𝑖𝑚𝑒 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 ∗ (𝑑𝑜𝑢𝑏𝑙𝑒 𝑚, 𝑐𝑜𝑛𝑠𝑡 𝑇𝑖𝑚𝑒 & 𝑡);


Dla porównania prototyp metody wywoływanej na rzecz obiektu i biorący jako argument obiekt
wyglądał tak:

𝑇𝑖𝑚𝑒 𝑇𝑖𝑚𝑒 ∷ 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 ∗ (𝑑𝑜𝑢𝑏𝑙𝑒 𝑚𝑢𝑙𝑡) 𝑐𝑜𝑛𝑠𝑡;


Przy przeciążeniu operatora funkcją niebędącą składową klasy lewy operand reprezentowany jest
pierwszym argumentem, prawy zaś drugim argumentem wywołania. Zastosowanie funkcji niebędącej
składową klasy rozwiązuje problem kolejności operandów, wprowadza jednak nową trudność – funkcje
nieskładowe nie mogą odwoływać się wprost do prywatnych danych klasy. Moga to jednak robić
funkcje zaprzyjaźnione.

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:

𝑇𝑖𝑚𝑒 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 ∗ (𝑑𝑜𝑢𝑏𝑙𝑒 𝑚, 𝑐𝑜𝑛𝑠𝑡 𝑇𝑖𝑚𝑒 & 𝑡){ (𝑏𝑒𝑧 𝑠ł𝑜𝑤𝑎 𝑓𝑟𝑖𝑒𝑛𝑑 𝑤 𝑑𝑒𝑓𝑖𝑛𝑖𝑐𝑗𝑖)
}

Z powyższą deklaracją, tudzież definicją, kompilator może już przetłumaczyć wyrażenie:

𝐴 = 2.75 ∗ 𝐵;
Na takie:

𝐴 = 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 ∗ (2.75, 𝐵);

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.

Tę konkretną funkcję zaprzyjaźnioną, którą rozpatrujemy moglibyśmy w zasadzie zdefiniować jako


niezaprzyjaźnioną, zmieniając w ciele funkcji kolejność operandów i wywołując metodą przeciążającą
operator mnożenia:

𝑇𝑖𝑚𝑒 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 ∗ (𝑑𝑜𝑢𝑏𝑙𝑒 𝑚, ){


𝑟𝑒𝑡𝑢𝑟𝑛 𝑡 ∗ 𝑚; (𝑧𝑎𝑠𝑡𝑜𝑠𝑜𝑤𝑎𝑛𝑖𝑒 𝑡. 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 ∗ (𝑚))
}

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ą.

Typowa przyjaźń – przeciążenie operatora <<


Przydatnym elementem interfejsu klasy jest przeciążenie operatora << pozwalające na stosowanie
obiektów w wyrażeniach z obiektami klasy ostream i wyświetlanie ich zawartości na wyjściu programu.
Pod pewnymi względami przeciążenie tego operatora jest trudniejsze od przeciążenia operatorów z
dotychczasowych przykładów. Załóżmy, że trip jest obiektem klasy Time. Dotychczas do wyświetlania
wartości obiektów tej klasy służyła nam jej metoda Show(). Ale fajnie by było móc zastosować taki
zapis:

𝑐𝑜𝑢𝑡 ≪ 𝑡𝑟𝑖𝑝;
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.

Przeciązenie operatora << - podejście pierwsze


Aby nauczyć klasę Time korzystania z obiektu cout można zastosować funkcję zaprzyjaźnioną.
Gdybyśmy zastosowali do przeciążenia operatora << metodę klasy Time, musielibyśmy po lewej stronie
operatora stawiać zawsze obiekt klasy Time, co dało by mylący efekt:

𝑡𝑟𝑖𝑝 ≪ 𝑐𝑜𝑢𝑡;
Porządany efekt:

𝑐𝑜𝑢𝑡 ≪ 𝑡𝑟𝑖𝑝;
Uzyskamy dzięki funkcji zaprzyjaźnionej – przeciążając operator następująco:

75
𝑣𝑜𝑖𝑑 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 ≪ (𝑜𝑠𝑡𝑟𝑒𝑎𝑚 & 𝑜𝑠, 𝑐𝑜𝑛𝑠𝑡 𝑇𝑖𝑚𝑒 & 𝑡){
𝑜𝑠 ≪ 𝑡. ℎ𝑜𝑢𝑟𝑠 ≪ "𝑔𝑜𝑑𝑧𝑖𝑛. ";
}

Dzięki temu możemy już pisać:

𝑐𝑜𝑢𝑡 ≪ 𝑡𝑟𝑖𝑝;
Ponieważ umożliwia to zapis operatorowy

Zaprzyjaźniać czy nie zaprzyjaźniać?

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.

Definicja operator<<() korzysta w powyższym przykładzie z referencji obiektu klasy ostream


przekazywanej pierwszym argumentem wywołania. Zwykle referencja ta odnosić się będzie do obiektu
cout, jak w wyrażeniu cout << trip. Ale tak zdefiniowany operator można oczywiście wykorzystać z
dowolnymi obiektami klasy ostream jak np. cerr (wyjście diagnostyczne).

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).

Przeciązenie operatora << - podejście drugie


Wypracowana dopiero co implementacja ma poważną wadę. Wedle niej instrukcja:

𝑐𝑜𝑢𝑡 ≪ 𝑡𝑟𝑖𝑝;
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 <<.

Podobny sposób moglibyśmy zastosować w funkcji:

𝑜𝑠𝑡𝑟𝑒𝑎𝑚 & 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 ≪ (𝑜𝑠𝑡𝑟𝑒𝑎𝑚 & 𝑜𝑠, 𝑐𝑜𝑛𝑠𝑡 𝑇𝑖𝑚𝑒 𝑡){


𝑜𝑠 ≪ 𝑡. ℎ𝑜𝑢𝑟𝑠 ≪ "𝑔𝑜𝑑𝑧𝑖𝑛";

𝑟𝑒𝑡𝑢𝑟𝑛 𝑜𝑠;
}

Teraz typ wartości zwracanej to ostream &. Czyli:

𝑐𝑜𝑢𝑡 ≪ 𝑡𝑟𝑖𝑝;
Będzie reallizowana wywołaniem:

𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 ≪ (𝑐𝑜𝑢𝑡, 𝑡𝑟𝑖𝑝);


A wywołanie to zwróci obiekt cout. Ta wersja operatora nadaje się też do stosowania z wyjściem
plikowym. (str 515).

PRZYKŁAD WAŻNY WSZYSTKIEGO ZEBRANEGO DO KUPY : str 516-518

Przeciążenie operatorów – metody kontra funkcje nieskładowe


Tak więc, reasumując, można przeciążyć operator w postaci funkcji jako składowej klasy (metody), oraz
funkcji niebędącej składową. Jako przykład posłuży operator dodawania przeciążony dla klasy Time:

𝑇𝑖𝑚𝑒 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 + (𝑐𝑜𝑛𝑠𝑡 𝑇𝑖𝑚𝑒 & 𝑡) 𝑐𝑜𝑛𝑠𝑡; (𝑠𝑘ł𝑎𝑑𝑜𝑤𝑎 − 𝑚𝑒𝑡𝑜𝑑𝑎)


Albo

𝑓𝑟𝑖𝑒𝑛𝑑 𝑇𝑖𝑚𝑒 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 + (𝑐𝑜𝑛𝑠𝑡 𝑇𝑖𝑚𝑒 & 𝑡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.

Oba te prototypy mogą uczestniczyć w obliczeniu wyrażenia T2 + T3, w którym T2 i T3 są obiektami


klasy Time. Kompilator może więc przetłumaczyć poniższą instrukcję:

𝑇1 = 𝑇2 + 𝑇3;

77
Na następujące wywołanie:

𝑇1 = 𝑇2. 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 + (𝑇3); (𝑚𝑒𝑡𝑜𝑑𝑎 𝑝𝑟𝑧𝑒𝑐𝑖ąż𝑎𝑗ą𝑐𝑎 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟)


𝑇1 = 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 + (𝑇2, 𝑇3) (𝑓𝑢𝑛𝑘𝑐𝑗𝑎 𝑛𝑖𝑒𝑠𝑘ł𝑎𝑑𝑜𝑤𝑎 𝑝𝑟𝑧𝑒𝑐𝑖ąż𝑎𝑗ą𝑐𝑎 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟)

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”.

Przeciążenia ciąg dalszy – klasa Vector


Wektory świetnie nadają się do ilustracji zastosowań przeciążenia operatorów. Po pierwsze, wektora
nie da się reprezentować pojedynczą liczbą, trzeba więc zdefiniować dla potrzeb tej reprezentacjji
własną klasę. Po drugie, wektory podlegają operacjom odpowiadającym zwykłym operacjom
arytmetycznym, jak choćby doadwaniu i odejmowaniu. Ta paralela sugeruje przeciążenie operatorów
owych operacji arytmetycznych dla nowego typu danych.

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++.

Przeciążenie operatorów arytmetycznych dla klasy Vector + kolejne ulepszenia


Operacje dodawania i mnożenia wektorów – str 528-529

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 „-„.

Wektorowe błądzenie losowe


Drugi przykład wektorów w zadaniu – str. 530 – 534
+ o liczbach pseudolosowych na str 533

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:

𝑆𝑡𝑜𝑛𝑒𝑤𝑡 𝑚𝑦𝐶𝑎𝑡; (𝑢𝑡𝑤𝑜𝑟𝑧𝑒𝑛𝑖𝑒 𝑜𝑏𝑖𝑒𝑘𝑡𝑢 𝑆𝑡𝑜𝑛𝑒𝑤𝑡)


𝑚𝑦𝐶𝑎𝑡 = 19,8; (𝑛𝑖𝑒𝑑𝑜𝑧𝑤𝑜𝑙𝑜𝑛𝑒, 𝑗𝑒ś𝑙𝑖 𝑆𝑡𝑜𝑛𝑒𝑤𝑡(𝑑𝑜𝑢𝑏𝑙𝑒)𝑧𝑎𝑑𝑒𝑘𝑙𝑎𝑟𝑜𝑤𝑎𝑛𝑒 𝑧 𝑒𝑥𝑝𝑙𝑖𝑐𝑖𝑡)
𝑚𝑦𝐶𝑎𝑡 = 𝑆𝑡𝑜𝑛𝑒𝑤𝑡(19,6); (𝑤 𝑝𝑜𝑟𝑧ą𝑑𝑘𝑢 𝑘𝑜𝑛𝑤𝑒𝑟𝑠𝑗𝑎 𝑗𝑎𝑤𝑛𝑎)

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)

Automatyczne stosowanie konwersji typu


Lepiej nie korzystac z niejawnej konwersji typów klasy na typ standardowy. Moze to
spowodowac glupi blad. W c++11 mozemy zadeklarowac w sposób jawny równiez operator
konwersji z uzyciem explicit.Obecnosc tych deklaracji pozwala na wywolywanie tych funkcji
jako przeciazonych operatorów konwersji typu w wyrazeniach rzutowania typów. Mozna tez
zastapic funkcje konwersji funkcja o niezgodnym prototypie, ale wykonujaca to samo zadanie;
takie funkcje nie beda przeciazeniami operatora konwersji, wiec trzeba bedzie je wywolywac
jawnie:
zamiast:
𝑆𝑡𝑜𝑛𝑒𝑤𝑡 ∷ 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 𝑖𝑛𝑡(){𝑟𝑒𝑡𝑢𝑟𝑛 𝑖𝑛𝑡(𝑝𝑜𝑢𝑛𝑑𝑠 + 0.5); }
napisac:
𝑖𝑛𝑡 𝑆𝑡𝑜𝑛𝑒𝑤𝑡 ∷ 𝑆𝑡𝑜𝑛𝑒_𝑡𝑜_𝐼𝑛𝑡(){𝑟𝑒𝑡𝑢𝑟𝑛 𝑖𝑛𝑡(𝑝𝑜𝑢𝑛𝑑𝑠 + 0.5); }
UWAGA:
Stosowanie konwersji niejawnych wymaga duzej ostroznosci, czesto lepiej zrezygnowac z nich
na rzecz funkcji dajacych sie wywolywac jedynie jawnie
Podsumowujac jezyk c++ daje w zakresie konwersji typu klas nastepujace mozliwosci:
 konstruktor klas przyjmujacy pojedynczy argument moze sluzyc jako instruktaz co do
konwersji wartosci typu argumentu na obiekt klasy. Na przyklad konstruktor klasy
Stonewt przyjmujacy argument typu int moze zostac wywolany automatycznie
wszedzie tam, gdzie w przypisaniu do obiektu Stonewt wystepuje wartosc typu int.
Oznaczajac taki konstruktor slowem explicit blokujemy mozliwosc jego niejawnego
wywolania, zostawiamy jednak opcje jego wywolania w konwersjach jawnych
 specjalna metoda klasy zwana funkcja konwersji sluzy jako instruktaz co do konwersji
wartosci obiektu na wartosc innego typu. Funkcja konwersji jest metoda klasy, nie
deklaruje typu wartosci zwracanej, nie przyjmuje argumentów i ma postac operator
nazwa-typu(), gdzie nazwa-typu to okreslenie typu docelowego konwersji. Funkcja
taka jest wywolywana automatycznie wszedzie tam, gdzie obiekt klasy przypisywany

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)

Możliwość implementacji dodawania


Przymierzając się do dodawania wartości typu double do obiektu Stonewt mamy kilka
możlwiości.
 Pierwszą z nich jest zdefiniowanie funkcji zaprzyjaźnionej:
𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 + (𝑐𝑜𝑛𝑠𝑡 𝑆𝑡𝑜𝑛𝑒𝑤𝑡 &, 𝑐𝑜𝑛𝑠𝑡 𝑆𝑡𝑜𝑛𝑒𝑤𝑡 &);
przy dostępności konstruktora Stonewt(double) w wywołaniu tak przeciążonego
operatora będzie mogła zostać wykonana konwersja argumentów z typu double na typ
Stonewt.
 Druga możliwość polega na kolejnym przeciążeniu operatora dodawania funkcjami
przyjmującymi jawnie argument typu double:
𝑆𝑡𝑜𝑛𝑒𝑤𝑡 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 + (𝑑𝑜𝑢𝑏𝑙𝑒 𝑥); (𝑚𝑒𝑡𝑜𝑑𝑎)
𝑓𝑟𝑖𝑒𝑛𝑑 𝑆𝑡𝑜𝑛𝑒𝑤𝑡 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 + (𝑑𝑜𝑢𝑏𝑙𝑒 𝑥, 𝑆𝑡𝑜𝑛𝑒𝑤𝑡 & 𝑠); (𝑓𝑢𝑛𝑘𝑐𝑗𝑎 𝑧𝑎𝑝𝑟𝑧𝑦𝑗𝑎ź𝑛𝑖𝑜𝑛𝑎)

Dzięki temu instrukcja:


𝑡𝑜𝑡𝑎𝑙 = 𝑗𝑒𝑛𝑛𝑦𝑆𝑡 + 𝑘𝑒𝑛𝑛𝑦𝐷; (𝑆𝑡𝑜𝑛𝑒𝑤𝑡 + 𝑑𝑜𝑢𝑏𝑙𝑒)
pasuje do wywołania operatora operator+(double x) przeciążanego w klasie Stonewt. A
instrukcja:
𝑡𝑜𝑡𝑎𝑙 = 𝑝𝑒𝑛𝑛𝑦𝐷 + 𝑗𝑒𝑛𝑛𝑦𝑆𝑡; (𝑑𝑜𝑢𝑏𝑙𝑒 + 𝑆𝑡𝑜𝑛𝑒𝑤𝑡)

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

Rozdział 12. Klasy a dynamiczny przydział


pamięci.

Powtórka z pamięci dynamicznej i statyczne składowe klas (Str 554 – 562)

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[].

O programie (str 561)


Mamy fragment:

𝑐𝑎𝑙𝑙𝑚𝑒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:

𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑 𝑠𝑎𝑖𝑙𝑜𝑟 = 𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑(𝑠𝑝𝑜𝑟𝑡𝑠);


Ponieważ sports jest typu StringBad to odpowiadający temu wywołaniu konstruktor musiałby
mieć sygnaturę:
𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑(𝑐𝑜𝑛𝑠𝑡 𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑 &);
Okazuje się, że kompilator automatycznie wygenerował taki konstruktor (tzw. konstruktor
kopiujący) w realizacji operacji inicjalizacji jednego obiektu drugim. Ta wygenerowana
automatycznie wersja nie potrafiła zaktualizować składowej statycznej num_strings i popsuła
zliczanie obiektów.

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:

𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑 𝑠𝑎𝑖𝑙𝑜𝑟 = 𝑠𝑝𝑜𝑟𝑡𝑠;


Odpowiada mniej więcej blokowi kodu:
𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑 𝑠𝑎𝑖𝑙𝑜𝑟;
𝑠𝑎𝑖𝑙𝑜𝑟. 𝑠𝑡𝑟 = 𝑠𝑝𝑜𝑟𝑡𝑠. 𝑠𝑡𝑟;
𝑠𝑎𝑖𝑙𝑜𝑟. 𝑙𝑒𝑛 = 𝑠𝑝𝑜𝑟𝑡𝑠. 𝑙𝑒𝑛;
Jeśli składowa jest sama w sobie obiektem, to do wykonania jej kopii wywoływany jest
konstruktor kopiujący właściwy dla klasy tego obiektu. Z kolei składowe statyczne, jak

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:

W czym tkwi problem z konstruktorem kopiującym w Stringbad?


Pierwszy wniosek jest taki, że program usunał więcej (o dwa) obiektów niż utworzył. Otóż
program utworzył dwa dodatkowe obiekty za pośrednictwem domyślnego, niejawnego
konstruktora kopiującego. Raz konstruktor kopiujący został wywołany, by zainicjalizować
parametr funkcji callme2() w momencie wywołania tej funkcji, drugi raz zainicjalizował
obiektem sports nowo tworzony obiekt sailor. Domyślny konstruktor kopiujący nie ogłasza, że
został wywołany i nie zwiększa wartości licznika num_strings, ale już destruktor każdorazowo
zmniejsza ów licznik, a jest wywoływany dla wszystkich utworzonych obiektów, niezależnie od
sposobu ich utworzenia. Pierwszy problem polega więc na nieskuteczności zliczania obiektów
programu. Rozwiązaniem będzie zdefiniowanie jawnego konstruktora kopiującego, który
będzie odpowiednio aktualizował licznik:
𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑 ∷ 𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑(𝑐𝑜𝑛𝑠𝑡 𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑 & 𝑠){
𝑛𝑢𝑚_𝑠𝑡𝑟𝑖𝑛𝑔𝑠 + +; }
Wskazówka:
Jeśli dana klasa ma statyczną składową, której wartośc zmienia się przy tworzeniu
nowych obiektów powino się koniecznie zdefiniować dla tej klasy jawny kosntruktor
kopiujący, w którym również ta składowa będzie odpowiednio obsługiwana.

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:

𝑑𝑒𝑙𝑒𝑡𝑒 [ ]𝑠𝑎𝑖𝑙𝑜𝑟. 𝑠𝑡𝑟;


Wskaźnik sailor.str wskazuje ciąg „ ... „ , ponieważ został zainicjalizowany wartością składowej
sports.str, która wskazywała właśnie taki ciąg. Wywołanie operatora delete zwalnia pamięć,
w której ten ciąg się znajduje. Przez to przy późniejszym usuwaniu obiektu sports mamy do
czynienia z operacją:

𝑑𝑒𝑙𝑒𝑡𝑒 [ ]𝑠𝑝𝑜𝑟𝑡𝑠. 𝑠𝑡𝑟


Wskaźnik sports.str wskazuje obszar pamięci, który zostął już zwolniony w wywołaniu
destruktora na rzecz obiektu sailor, a efekt zwalniania zwolnionej już pamięci jest
niezdefiniowany i najczęściej katastrofalny dla programu.
Definiowanie jawnego konstruktora kopiującego
Lekiem na całe zło jest wykonywanie w konstruktorze kopiującym głębokiej kopii obiektu.
Zamiast kopiować pomiędzy składowymi adres ciągu, trzeba by raczej powielić ciąg i przypisac
do składowej str obiektu docelowego adres nowego egzemplarza ciągu. W ten sposób każdy z
obiektów zyska własna kopię ciągu i nie będzie już przypadków zgubnego w skutkach
współużytkowania ciągu przez wiele obiektów – w każdym wywołaniu destruktora na rzecz
kolejnych obiektów zwalniany będzie osobno obszar pamięci. Oto jak można by
zaimplementować poprawiony jawny konstruktor kopiujący klasy StringBad:
𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑 ∷ 𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑(𝑐𝑜𝑛𝑠𝑡 𝑆𝑡𝑟𝑖𝑛𝑔𝐵𝑎𝑑 & 𝑠𝑡){
𝑛𝑢𝑚𝑠𝑡𝑟𝑖𝑛𝑔𝑠 + +;

𝑙𝑒𝑛 = 𝑠𝑡. 𝑙𝑒𝑛


𝑠𝑡𝑟 = 𝑛𝑒𝑤 𝑐ℎ𝑎𝑟 [𝑙𝑒𝑛 + 1];
𝑠𝑡𝑑 ∷ 𝑠𝑡𝑟𝑐𝑝𝑦(𝑠𝑡𝑟, 𝑠𝑡. 𝑠𝑡𝑟);
}

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:

𝑆0. 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 = (𝑆1. 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 = (𝑆2));

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.

Nowa, ulepszona klasa – String


W c++ 11 wskaźnik pusty można zaznaczać komendą nullptr (np str = nullptr).

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:

𝑜𝑝𝑒𝑟𝑎. 𝑜𝑝𝑒𝑟𝑎[ ](4);


Oznacza ono wywołanie na rzecz obiektu opera metody operator[ ] () z argumentem o
wartości 4. Oto prosta implementacja operatora indeksowania:
𝑐ℎ𝑎𝑟 & 𝑆𝑡𝑟𝑖𝑛𝑔 ∷ 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 [ ](𝑖𝑛𝑡 𝑖 ){
𝑟𝑒𝑡𝑢𝑟𝑛 𝑠𝑡𝑟[𝑖]; }
W obliczu takiej definicji instrukcja:
𝑐𝑜𝑢𝑡 ≪ 𝑜𝑝𝑒𝑟𝑎[4];
Zostanie zastąpione przez kompilator na :

𝑐𝑜𝑢𝑡 ≪ 𝑜𝑝𝑒𝑟𝑎. 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟[4];


Wartością zwracaną z metody przeciążającej operator jest opera.str[4] czyli znak ‘o’. (więcej
str. 574-575)
Język c++ rozróżnia przy przeciążeniu operatorów sygnatury metod ze słowem const i bez tego
słowa jako osobne wersje operatora!!! Można więc zdefiniować dla klasy String drugą wersję
94
operatora indeksowania, która będzie nadawać się do wywoływania na rzecz
niemodyfikowalnych (const) obiektów klasy String:
𝑐𝑜𝑛𝑠𝑡 𝑐ℎ𝑎𝑟 & 𝑆𝑡𝑟𝑖𝑛𝑔 ∷ 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟[ ](𝑖𝑛𝑡 𝑖)𝑐𝑜𝑛𝑠𝑡{
𝑟𝑒𝑡𝑢𝑟𝑛 𝑠𝑡𝑟[𝑖]; }

Statyczne metody klasy


Metody klas można deklarować jako składowe statyczne (słowo static występuje wtedy tylko
w ich deklaracjach, nie w definicjach – chyba, że definicja znajduje się w definicji). Takie
oznaczenie metody ma dwojakie konsekwencje.
Po pierwsze statyczna metoda klasy nie może zostać wywołana na rzecz któregokolwiek z
obiektów klasy – w ciele metody nie jest nawet dostępny wskaźnik this. Jeśli statyczna metoda
klasy zostanie zadeklarowana jako publiczna, można ją wywołać, kwalifikując jej nazwę nazwą
klasy i operatorem zasięgu. Gdy statyczna metoda klasy jest zadeklarowana jako publiczna,
można ją wywołać kwalifikując jej nazwę nazwą klasy i operatorem zasięgu. Mamy np
statyczną metodę klasy:
𝑠𝑡𝑎𝑡𝑖𝑐 𝑖𝑛𝑡 𝐻𝑜𝑤𝑀𝑎𝑛𝑦(){𝑟𝑒𝑡𝑢𝑟𝑛 𝑛𝑢𝑚_𝑠𝑡𝑟𝑖𝑛𝑔𝑠; }
Metodę tę można wywołać tak:
𝑖𝑛𝑡 𝑐𝑜𝑢𝑛𝑡 = 𝑆𝑡𝑟𝑖𝑛𝑔 ∷ 𝐻𝑜𝑤𝑀𝑎𝑛𝑦( );
Po drugie, metoda statyczna jako nieskojarzona z żadnym konkretnym obiektem klasy, może
odwoływać się jedynie do statycznych danych składowych tej klasy. Na przykład metoda
HowMany( ) może odwoływać się do składowej num_strings, ale nie wolno jej korzystać ze
składowych str, czy len. Metoda statyczna może służyć do ustawiania wspólnego dla
wszystkich obiektów znacznika kontrolującego zachowanie interfejsu klasy. Może na przykład
przełączyć tryb wyświetlania danych obiektów klasy.

Dalsze przeciążanie operatora przypisania


Te 3 punkty na str 576 ogarna. Czy dobrze rozumiem dlaczego wywolywany jest konstruktor
itp

O czym należy pamiętać stosując new w konstruktorach?


Należy pamiętać, że :
 Jeśli w konstruktorze składowa (wskaźnik) jest inicjalizowana wywołaniem operatora
new, to w destruktorze należy ją zwolnić wywołaniem operatora delete
 Wywołania new (w konstruktorze) i delete (w destruktorze) powinny być zgodne – new
należy parować z delete, a new [] z delete [].
 Jeśli klasa definiuje wiele konstruktorów, wszystkie powinny stosować new tak samo
(czy to z nawiasami prostokątnymi, czy też bez takich nawiasów). Klasa może
definiować tylko jeden destruktor, więc wszystkie konstruktory trzeba uzgnodnić z
tymże destruktorem. Dopuszczalne jest jednak, aby niektóre z konstruktorów

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

Kopiowania obiektów składowa po składowej


Str 583-584 (nasza klasa string, a standardowa klasa String)

Zwracanie niemodyfikowalnej (const) referencji obiektu


Uzasadnieniem dla zwracania niemodyfikowalnej referencji obiektu jest zazwyczaj wydajność,
choć nie zawsze mamy pełną swobodę wyboru. Jesli funkcja zwraca obiekt przykazywany do
niej czy to w wywołaniu czy niejawnie, wydajność metody można zwiększyć, deklarując ją jako
zwracającą referencję. Mamy kod:

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.

Zwracanie modyfikowalnej (bez const) referencji do obiektu


Ze zwracaniem obiektów przez referencje mamy do czynienia często przy przeciążeniu
operatora przypisania i przeciążeniu operatora << do stosowania obiektów klas z obiektem
cout. W pierwszym przypadku chodzi o efektywność, w drugim nie mamy większego wyboru.
Wartość zwracana z operatora przypisania (operator=()) wykorzystywana jest również w
przypisaniach kaskadowych:

𝑠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).

Zwracanie obiektu przez wartość


Jeśli zwracany obiekt jest obiektem lokalnym względem funkcji, to nie powinien być zwracany
przez referencję, ponieważ ta w momencie wyjścia z funkcji straci ważność. Kiedy więc
sterowanie zostanie przekazane do wywołującego, nie będzie już obiektu, do którego
referencja mogłaby się odnosić. W takich okolicznościach należy zwrócić obiekt przez wartość.
Typowym przykładem funkcji wymagającej takiego przekazywania jest funkcja przeciążająca
operator arytmetyczny – jak w poniższym przykładzie, ponownie odwołującym się do definicji
klasy Vector:
97
𝑉𝑒𝑐𝑡𝑜𝑟 𝑓𝑜𝑟𝑐𝑒1(50,70);
𝑉𝑒𝑐𝑡𝑜𝑟 𝑓𝑜𝑟𝑐𝑒2(10,60);
𝑉𝑒𝑐𝑡𝑜𝑟 𝑛𝑒𝑡;
𝑛𝑒𝑡 = 𝑓𝑜𝑟𝑐𝑒1 + 𝑓𝑜𝑟𝑐𝑒2;
Wartością zwracaną nie jest ani force1, ani force2 – oba te obiekty powinny po
przeprowadzeniu operacji dodawania zachować swoje pierwotne wartości. Wartość zwracana
nie może więc być referencją żadnego z obiektów przekanych w wywołaniu metody
przeciążającej operator. Metoda Vector::operator+() musi samodzielnie utworzyć nowy
tymczasowy obiekt klasy Vector, reprezentujący sumę dwóch przekazanych obiektów; obiektu
o zasięgu lokalnym nie wolno zwracać przez referencję. (Chodzi o to, że referencja musi
odnosić się do czegoś poza funkcją, przekazywanie przez wartość pozwala na przesłaniu do
nowego obiektu wartości, nie zmieniając wartości pierwotnych obiektów, co chcemy tu
osiągnąć). Metoda musi zwracać obiekt jako taki:
𝑉𝑒𝑐𝑡𝑜𝑟 𝑉𝑒𝑐𝑡𝑜𝑟 ∷ 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 + (𝑐𝑜𝑛𝑠𝑡 𝑉𝑒𝑐𝑡𝑜𝑟 & 𝑏)𝑐𝑜𝑛𝑠𝑡{
𝑟𝑒𝑡𝑢𝑟𝑛 𝑉𝑒𝑐𝑡𝑜𝑟(𝑥 + 𝑏. 𝑥, 𝑦 + 𝑏. 𝑦); }
Narazimy się przy tym na dodatkowy koszt wykonania wynikający z wywołania konstruktora
kopiującego tworzącego obiekt zwracany, ale tego narzutu nie sposób uniknąć. Jeszcze jedno
w przykładzie z metodą Vector::operator+() wywołanie konstruktora Vector(x+b.x+y+b.y
tworzy nowy obiekt, dostępny dla metody operator+(); jednak dopiero niejawne wywołanie
konstruktora kopiującego z instrukcji return tworzy obiekt dostępny poza funkcją.

Zwracanie przez wartość obiektu niemodyfikowalnego (const)


Prezentowana poprzednio definicja metody Vector::operator+() ma pewną dziwaczną
właściwość. Otóż przeznaczyliśmy ją do takiego stosowania:
𝑛𝑒𝑡 = 𝑓𝑜𝑟𝑐𝑒1 + 𝑓𝑜𝑟𝑐𝑒2; (𝑡𝑟𝑧𝑦 𝑜𝑏𝑖𝑒𝑘𝑡𝑦 𝑘𝑙𝑎𝑠𝑦 𝑉𝑒𝑐𝑡𝑜𝑟)
Jednak sama definicja pozwala również na takie zastosowania:

𝑓𝑜𝑟𝑐𝑒1 + 𝑓𝑜𝑟𝑐𝑒2 = 𝑛𝑒𝑡; (𝑑𝑦𝑠𝑙𝑒𝑘𝑠𝑗𝑎 𝑝𝑟𝑜𝑔𝑟𝑎𝑚𝑖𝑠𝑡𝑦𝑐𝑧𝑛𝑎)


𝑐𝑜𝑢𝑡 ≪ (𝑓𝑜𝑟𝑐𝑒1 + 𝑓𝑜𝑟𝑐𝑒2 = 𝑛𝑒𝑡). 𝑚𝑎𝑔𝑣𝑎𝑙( )
≪ 𝑒𝑛𝑑𝑙; (𝑑𝑒𝑚𝑒𝑛𝑐𝑗𝑎 𝑝𝑟𝑜𝑔𝑟𝑎𝑚𝑖𝑠𝑡𝑦𝑐𝑧𝑛𝑎)
Po co ktoś miał by konstruować takie instrukcje? Skąd możliwość ich zastosowania? Jaki efekt
był by ich wykonania? (o tym na str 586 – 587)
Podsumowując rozważania: Jeśli metoda zwraca obiekt o zasięgu lokalnym, powinna zwracać
go przez wartość, a nie przez referencję. W powyższym przykładzie program generuje kopię
zwracanego obiektu widoczną dla wywołującego za pośrednictwem konstruktora
kopiującego. Gdyby metoda albo funkcja zwracała obiekt klasy, dla której nie istnieje publiczny
konstruktor kopiujący, jak w przypadku klasy ostream, byłaby zmuszona do zwracania nie
obiektu, ale referencji do niego. Wreszcie niektóre metody i funkcje, na przykład przeciążające
operator przypisania, mogą zwracać albo referencję, albo obiekt. W prezentowanym
98
przykładzie decyzję o zwracaniu obiektu przez referencję uzasadniliśmy wydajnością realizacji
wywołania funkcji (metody)

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:

𝑛𝑎𝑧𝑤𝑎 − 𝑘𝑙𝑎𝑠𝑦(𝑛𝑎𝑧𝑤𝑎 − 𝑡𝑦𝑝𝑢);


Dokładny typ wartości nie musi idealnie pasować do typu deklarowanego w sygnaturze
konstruktora – możliwe są podstawowe konwersje:
𝑛𝑎𝑧𝑤𝑎 − 𝑘𝑙𝑎𝑠𝑦 (𝑐𝑜𝑛𝑠𝑡 𝑛𝑎𝑧𝑤𝑎 − 𝑡𝑦𝑝𝑢 &);
Ponadto dozwolone są zwyczajne konwersje stosowane w procesie roztrzygania wywołania
na bazie typów argumentów, na przykład konwersja int na double (o ile wielość możliwych
konwersji nie doprowadzi do niejednoznaczności). Z kolei inicjalizacja w postaci:
𝑛𝑎𝑧𝑤𝑎 − 𝑘𝑙𝑎𝑠𝑦 ∗ 𝑝𝑡𝑟 = 𝑛𝑒𝑤 𝑛𝑎𝑧𝑤𝑎 − 𝑘𝑙𝑎𝑠𝑦;
Prowokuje wywołanie konstruktora domyślnego klasy nazwa-klasy.

Jeszcze o new i delete


Zauważmy, że program złożony z kodu zaczęrpniętego z listingów 12.4,12.5, 12.7 stosuje
wywołania operatorów new i delete niejako na dwóch poziomach. Po pierwsze new służy tu
do przydzielania pamięci dla ciągów kojarzonych z poszczególnymi obiektami podczas ich
tworzenia. Dzieje się to w obrębie metod konstruktorów, co zmusza destruktor do stosowania
delete do zwolnienia przydzielonej przy konstrukcji pamięci. Ponieważ każdy ciąg jest de facto
tablicą znaków, destruktor wywołuje delete w wersji z nawiasami prostokątnymi. Razem daje
to efekt zwolnienia pamięci ciągu przy usuwaniu obiektu. Po drugie zaś, program z listingu
12.7 stosuje new do przydzielania całego obiektu:

𝑠𝑡𝑟𝑖𝑛𝑔 ∗ 𝑓𝑎𝑣𝑜𝑟𝑖𝑡𝑒 = 𝑛𝑒𝑤 𝑆𝑡𝑟𝑖𝑛𝑔(𝑠𝑎𝑦𝑖𝑛𝑔𝑠[𝑐ℎ𝑜𝑖𝑐𝑒]);


Instrukcja ta przydziela pamięć nie dla ciągu kojarzonego z obiektem, ale dla samego obiektu
– tzn. Dla wskaźnika str przechowującego adres ciągu i dla składowej len (przydział nie
obejmuje składowej num_strings, bo ta jest statyczna i jest przydzielana jednokrotnie dla całej
klasy, niezależnie od tworzonych obiektów). Utworzenie obiektu prowokuje z kolei wywołanie
jego konstruktora, w którym następuje dopiero przydział pamięci dla ciągu znaków
kojarzonego z obiektem i przypisanie adresu przydzielonego obszaru do składowej str. Dalej
program do zwolnienia tak przydzielonego obiektu wywołuje operator delete – bez nawiasów,
bo obiekt usuwany jest pojedynczym obiektem (nie tablicą). Wywołanie to zwalnia jedynie
pamięć przydzieloną wcześniej dla składowych str i len, nie zaś pamięć przydzieloną do ciągu

99
znaków (wskazywanego przez str) – to zadanie wykonuje jego wywołany automatycznie (tu
przed zwolnieniem obiektu) destruktor – pokazuje to rys poniżej:

Wskaźniki obiektów – podsumowanie


O stosowaniu wskaźników obiektów należy wiedzieć co następuje:
 Wskaźnik obiektu deklaruje się za pomocą zwykłej notacji deklaracji wskaźnika:

𝑆𝑡𝑟𝑖𝑛𝑔 ∗ 𝑔𝑙𝑎𝑚𝑜𝑢𝑟;
 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:

𝑆𝑡𝑟𝑖𝑛𝑔 ∗ 𝑓𝑎𝑣𝑜𝑟𝑖𝑡𝑒 = 𝑛𝑒𝑤 𝑆𝑡𝑟𝑖𝑛𝑔(𝑠𝑎𝑦𝑖𝑛𝑔𝑠[𝑐ℎ𝑜𝑖𝑐𝑒]);


Przykład inicjalizacji wskaźnika obiektu wywołaniem operatora new jest też
prezentowany i komentowany na rysunku poniżej:

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)

𝑑𝑒𝑙𝑒𝑡𝑒 𝑝𝑐1; (𝑢𝑠𝑢𝑛𝑖ę𝑐𝑖𝑒 𝑜𝑏𝑖𝑒𝑘𝑡𝑢 𝑤𝑠𝑘𝑎𝑧𝑦𝑤𝑎𝑛𝑒𝑔𝑜 𝑝𝑟𝑧𝑒𝑧 𝑝𝑐1? 𝑁𝐼𝐸! Ź𝐿𝐸)

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)

Powtórka poznanych technik


 Przeciążenia operatora <<
Aby zdefiniować wersję operatora << dla własnej klasy, tak by dało się stosować obiekty tej
klasy w wyrażeniach z obiektem cout, trzeba zdefiniować funkcję przeciążającą operator w
postaci funkcji zaprzyjaźnionej, jak tu:
𝑜𝑠𝑡𝑟𝑒𝑎𝑚 & 𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 ≪ (𝑜𝑠𝑡𝑟𝑒𝑎𝑚 & 𝑜𝑠, 𝑐𝑜𝑛𝑠𝑡 𝑛𝑎𝑧𝑤𝑎 − 𝑘𝑙𝑎𝑠𝑦 & 𝑜𝑏𝑗){
𝑜𝑠 ≪ ⋯ ;
𝑟𝑒𝑡𝑢𝑟𝑛 𝑜𝑠; }
Ciąg nazwa-klasy reprezentuje tu nazwę klasy, dla której przeciążamy operator. Jeśli klasa
udostępnia publiczne metody dające dostęp do potrzebnych w operacji wyświetlania danych
obiektu, funkcja przeciążająca operator nie musi być nawet zaprzyjaźniona z klasą.

 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:

𝑜𝑝𝑒𝑟𝑎𝑡𝑜𝑟 𝑛𝑎𝑧𝑤𝑎 − 𝑡𝑦𝑝𝑢( );


Choć prototyp ten nie deklaruje wartości zwracanej, metoda powinna zwracać obiekt typu
określonego w nazwie metody.
Trzeba pamiętać, że stosowanie funkcji konwersji wymaga sporeż ostrożności. Aby zapobiec
ewentualnym przypadkowym, niezamierzonym wywołaniom funkcji konwersji, można
oznaczyć konstruktory jednoargumentowe słowem kluczowym explicit – wtedy będą mogły
być wywoływane jedynie jawnie

 Klasy wykorzystujące new w konstruktorach


Projektując klasę wykorzystującą w konstruktorze wywołanie operatora new do
zainicjalizowania adresem przydzielonego obszaru pamięci składowej klasy, trzeba mieć na
uwadze, że:
 Wszystkie składowe wskazujące obszary pamięci przydzielone w konstruktorze
wywołaniem new powinny zostać poddane działaniu operatora delete w destruktorze
klasy. Zapewnia to zwolnienie pamięci pozostającej w dyspozycji obiektu
 Jeśli destruktor zwalnia pamięć wywołując delete ze składową będącą wskaźnikiem tej
pamięci, to należy zadbać o inicjalizację tej składowej w każdym możliwym
konstruktorze klasy – inicjalizacja może przy tym odbywać się bądź to wywołaniem
new, bądź przypisaniem do składowej wskaźnika pustego
 Konstruktory powinny zgodnie korzystać z jednolitej wersji wywołania operatora new
– niech wszystkie wywołują new, albo new [ ], ale bez dowolności. Jeśli w
konstruktorach znajduje się wywołanie new [ ], destruktor powinien zawierać
odpowiednie wywołanie delete [ ], analogicznie wywołaniem new w konstruktorach
powinno odpowiadać wywołanie delete w destruktorze.
 Powinno sie zdefiniować konstruktor kopiujący wykonujący głęboką, a nie
powierzchowną kopię obiektu kopiowanego. Dzięki temu program będzie mógł
inicjalizować nowo tworzone obiekty klasy już istniejącymi obiektami tej klasy.
Konstruktor kopiujący powinien mieć prototyp:
𝑛𝑎𝑧𝑤𝑎 − 𝑘𝑙𝑎𝑠𝑦 (𝑐𝑜𝑛𝑠𝑡 𝑛𝑎𝑧𝑤𝑎 − 𝑘𝑙𝑎𝑠𝑦 &);
 Należy zdefiniować metodę przeciążającą dla klasy operator przypisania, która
odpowiada prototypowi prezentowanemu poniżej (ptr jest tam składową klasy
nazwa0klasy; składowa ta jest wskaźnikiem typu nazwa-typu). Poniższy przykład
zakłada, że konstruktory inicjalizują zmienną ptr wywołaniem operatora new [ ]:

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.

Rozdział 13. Klasy i dziedziczenie.

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

Konstruktory – zagadnienia związane z poziomem dostępu


Klasa pochodna nie ma bezpośrednio dostępu do składowych prywatnych klasy bazowej.
Dostęp do nich istnieje tylko za pośrednictwem metod klasy bazowej. Np. w konstruktorze
klasy RatedPlayer nie można bezpośrednio ustawić wartości odzedziczonych składowych.
Zamiast tego należy użyć publicznych metod klasy bazowej, by uzyskać dostęp do jej
składowych prywatnych. W szczególności konstruktory klasy pochodnej muszą korzystać z
konsturktorów klasy bazowej.
Kiedy program tworzy obiekt klasy pochodnej, najpierw musi utworzyć obiekt klasy bazowej.
Oznacza to,że obiekt klasy bazowej powinien zostać utworzony zanim program wejdzie w
zakres ciała konstruktora klasy pochodnej. Aby to umożliwić, w języku c++ używa się listy
inicjalizacyjnej składowych. Konstruktor klasy RatedPlayer:

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ą

to program użyje konstruktora domyślnego, więc kod będzie równoważny temu:

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?):

Kluczowe mechanizmy związane z konsturktorami klasy pochodnej:


 w pierwszej kolejności tworzony jest obiekt klasy bazowej
 konstruktor klasy pochodnej powinien przekazywać dane klasy bazowej do jej
konstruktora za pomocą listy inicjalizacyjnej

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).

Relacja między klasą pochodną a bazową


 Obiekt klasy pochodnej może korzystać z metod klasy bazowej pod warunkiem, że nie
są to metody prywatne.

 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

Dziedziczenie – relacja jest-czymś


W języku c++ istnieją trzy rodzaje dziedziczenia – publiczne, chronione i prywatne.
Dziedziczenie publiczne, jest najczęściej spotykane i reprezentuje relację jest-czymś. Jest to
skrótowe określenie tego, że obiekt klasy pochodnej jest także obiektem klasy bazowej.
Cokolwiek można zrobić z obiektem klasy bazowej, można także zrobić z obiektem klasy
pochodnej.
Aby wyjaśnić relację jest-czymś zastanówmy się nad przykładami, które nie pasują do tego
modelu. Dziedziczenie publiczne nie oddaje relacji:
 ma-coś. Drugie śniadanie może zawierać owoce, ale nie jest owocem. Dlatego klasa
DrugieŚniadanie nie powinna dziedziczyć klasy Owoc. Poprawny sposób zawarcia
owocu w drugim śniadaniu to wykorzystanie relacji ma-coś – drugie śniadanie zawiera
owoc. ( o tym więcej w rozdziale 14).
 Jest-jak-coś – czyli nie przedstawia podobieństw. Mówi się np. często, że prawnicy są
jak rekiny. Jednak dosłownie nie jest to prawdą. Rekiny mogą np. żyć pod wodą. Z tego
powodu klasa Prawnik nie powinna dziedziczyć po klasie Rekin. Za pomocą
dziedziczenia można dodawać właściwości do klasy bazowej, a nie można ich usuwać.
 Jest-zaimplementowane – można np. zaimplementować stos za pomocą tablicy.
Jednak nie należy dziedziczyć klasy Stos po klasie Tablica. Np. indeksowanie tablicy nie
jest cechą stosu bo stos to pamięć.
 Używa-czegoś – komputer może używać drukarki laserowej, ale nie ma sensu
dziedziczyć klasy Drukarka po klasie Komputer lub na odwrót.
W c++ nic nie zabroni zrobić dziedziczenia w tych niekorzystych relacjach, jednak prowadzić to
może do problemów, więc warto trzymać się relacji jest-czymś.

Polimorfizm dziedziczenie publiczne


Można zetknąć się z sytuacja, w której metody powinny zachowywać się inaczej dla obiektów
klasy pochodnej a inaczej dla obiektów klasy bazowej. Oznacza to, że zachowanie konkretnej
metody może zależeć od obiektu, dla którego jest wywoływana. Takie bardziej złożone
zachowanie określane jest mianem polimorficznego (mającego wiele postaci). , ponieważ
może istnieć wiele sposobów działania metody zależnych od kontekstu. Istnieją dwa główne
mechanizmy związane z implementacją polimorficznego dziedziczenia publicznego:

111
 Ponowne definiowanie metody klasy bazowej w klasie pochodnej
 Używanie metod wirtualnych

Rzeczy warte uwagi na podstawie tego co powyżej:


 W klasie BrassPlus dodane są trzy nowe prywatne dane składowe oraz trzy nowe
publiczne metody
 W obu klasach zadeklarowane są metody ViewAcct() oraz Withdraw(). Metody te
działają inaczej dla obiektów każdej klasy
Dwa prototypy metody ViewACct() oznaczają, że potrzebne są dwie definicje tej metody.
Jedna wersja metody przeznaczona jest dla klasy bazowej, na co wskazuje kwalifikowana
nazwa Brass::ViewAcct(), druga wersja, o nazwie BrassPlus::ViewAcct(), przezonaczona jest dla
klasy pochodnej. Program korzysta z typu obiektu aby wybrać odpowiednią metodę.

 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.

Zmienne referencyjne są typu Brass, więc program używa metody Brass::ViewAcct()


Jeśli użyjemy słowa virtual, program wybierze metodę na podstawie typu obiektu, z jakim
powiązana jest referencja lub wskaźnik.

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

Metoda BrassPlus::ViewAcct() wywołuje metodę klasy bazowej Brass::ViewAcct() do


wyświetlania danych składowych klasy bazowej, a następnie wyświetla dane dodane w klasie
BrassPlus. Używanie operatora zakresu jest standardową techniką wywoływania metod klasy
bazowej w klasie pochodnej. Użycie operatora zakresu w takiej sytuacji jest niezbędne. Jeśli
go zabraknie to kompilator przyjmie, że wywołanie ViewAcct() odnosi się do metody
BrassPlus::ViewAcct() i utworzy funkcję rekurencyjną, która nigdy się nie zakończy.
Przykład działania metody wirtualnej

Polimorfizm znajduje swe zastosowanie w poniższym fragmencie kodu:

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)

Wiązanie statyczne i dynamiczne


Wiązanie to intepretacja wywołania funkcji w kodzie źródłowym jako wykonania kodu
odpowiedniej funkcji.
Wiązanie, które zostaje utworzone w czasie kompilacji, nazywamy wiązaniem statycznym
Funkcje wirtualne muszą jednak być wiązane w czasie trwania programu. Bo wskaźniki mogą
zmianiać obiekty na które wskazują. Takie postępowanie nazywamy wiązaniem dynamicznym.
Czyli takie, które jest ustalane na bieżąco w trakcie trwania programu.

Zgodność typów wskaźnikowych i referencyjnych


Wiązanie dynamiczne w języku c++ związane jest w wywołaniem metod za pomocą
wskaźników i referencji, co po części obsługiwane jest przez mechanizm dziedziczenia.
Normalnie język c++ nie pozwala na przypisywanie adresu jednego typu do wskaźnika na inny
typ. Nie pozwala również na wiązanie referencji jednego typu z obiektami innego typu.

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.

Metody wirtualne i wiązania dynamiczne


Jak już wiemy, jeśli metoda klasy bazowej nie jest wirtualna, to kompilator korzysta z wiązania
statycznego, jeśli metoda ta jest wirtualna to korzysta z wiązania dynamicznego. W większosci
przypadków wiązanie dynamiczne jest rozwiązaniem korzystnym, ponieważ pozwala wybrać
w programie metodę przeznaczoną dla konkretnego typu. Wiedząc o tym, możemy zadawać
sobie następujące pytania:
 Po co istnieją dwa typy wiązania
 Jeśli wiązanie dynamiczne jest takie wygodne to czemu nie jest domyślne

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

Co trzeba wiedzieć o metodach wirtualnych


 Deklaracja metody w klasie bazowej za pomocą słowa kluczowego virtual powoduje,
że funkcja ta jest wirtualna w klasie bazowej oraz wszystkich klasach od niej
pochodnych, włączając w to klasy pochodne klas pochodnych
 Jeśli metoda wirtualna wywoływana jest za pomocą referencji lub wskaźnika, program
używa metody zdefiniowanej dla obiektu, a nie metody zdefiniowanej dla typu
referencji lub wskaźnika. Mechanizm ten nazywa się wiązaniem dynamicznym lub
późnym. Takie zachowanie jest ważne, ponieważ wskaźnik lub referencja klasy bazowe
mogą być zawsze powiązane z obiektem klasy pochodnej.
 Jeśli definiujemy klasę, która ma stanowić klasę bazową, powinniśmy deklarować jako
wirtualne te funkcje, które mogą wymagać powtórnej definicji w klasach pochodnych
Dodatkowo:
Konstruktory – nie mogą być wirtualne
Destruktory – jeśli klasa może być wykorzystywana jako bazowa, to destruktory powinny być
wirtualne. (patrz str 653-654)
Funkcje zaprzyjaźnione – nie mogą być wirtualne, ponieważ nie są one składowymi klasy, a
tylko one mogą być wirtualne.
Brak nowej definicji – jeśli w klasie pochodnej nie ma nowej definicji to klasa używa wersji tej
funkcji z klasy bazowej. Jeśli klasa pochodna jest ogniwem długiego łańcucha dziedziczenia,
wtedy używa wersji tej funkcji zdefiniowanej najbliżej. Wyjątkiem jest sytuacja, w której
wersja klasy bazowej jest ukryta (patrz str 654-655)

Kontrola dostępu – poziom chroniony


Poziom chroniony – protected – jest podobne do private pod tym względem, że klasy
składowe w sekcji chronionej są dostępne jedynie poprzez metody publiczne. Różnica
między tymi poziomami polega na innym dostępie do składowych odziedziczonych w klasie

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.

Abstrakcyjne klasy bazowe


Czasami stosowanie relacji jest-czymś nie jest takie proste na jakie wygląda. Załóżmy, że
tworzymy program graficzny, w którym znajduje się reprezentacja okręgu i elipsy. Okrąg jest
specyficznym przypadkiem elipsy – jej oś wielka jest taka sama jak oś mała. Dlatego wszystkie
okręgi to elipsy i kuszące jest utworzenie klasy Circle pochodnej od klasy Elipse. Kiedy jednak
zajmiemy się szczegółami możemy natrafić na problemy. Najpierw zastanówmy się, jakie
składowe powinny znaleźć się w klasie Ellipse. Dane powinny opisywać środek elipsy, długość
półosi wielkiej i małej, oraz kąt nachylenia. Klasa może także zawierać metody do przesuwania
elipsy, obliczania pola, obracania i skalowania obu osi figury.

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.

Dziedziczenie i dynamiczny przydział pamięci


Jak dziedziczenie współpracuje z dynamicznym przydziałem pamięci odbywającym się za
pmocą słów kluczowych new i delete? Jeśli klasa bazowa korzysta z dynamicznego przydziału
pamięci oraz ma definicję operatora przypisania i konstruktora kopiującego, jak wpływa to na
implementację klasy pochodnej? Odpowiedź zależy od charaktery klasy pochodnej. Jeśli nie
korzysta ona z dynamicznego przydziału pamięci, wtedy nie musimy podejmować żadnych
specjalnych kroków. Jeśli jednak klasa pochodna także używa dynamicznego przydziału
pamięci, należy zastosować kilka sztuczek. Są to dwa przypadki:

Przypadek pierwszy – klasa pochodna bez dynamicznego przydziału pamięci


Opis z przykładem – str 665-666
W deklaracji znajdują się specjalne metody, które są wymagane, jeśli konstruktor korzysta ze
słowa kluczowego new – destruktor, konstruktor kopiujący, przeciążony operator przypisania.
Załóżmy teraz, że tworzymy klasę pochodną lacksDMA, która nie używa słowa kluczowego
new ani innych wsłaściwości wymagających specjalnych zabiegów. Czy także w tym przypadku
potrzebne są: jawny destruktor, konstruktor kopiujący i operator przypisania? NIE!
Po pierwsze jeśli nie zdefiniujemy destruktora to kompilator utworzy destruktor domyślny,
który nic nie zrobi. W rzeczywistości destruktor domyślny w klasie pochodnej zawsze robi
chociaż jedno – wywołuje destruktor klasy bazowej. Ponieważ klasa pochodna nie wymaga
żadnych specjalnych działań, funkcjonowanie destruktora domyślnego jest zadawalające.
Po drugie konstruktor kopiujący – ten domyślny wykonuje płytkie kopiowanie, które jest
niedpowiednie w przypadku korzystania z dynamicznego przydziału pamięci. Jest więc okej dla
klasy pochodnej. Problemem jest klasa bazowa. Jednak jak wiemy, płytkie kopiowanie stosuje
sposób kopiowania zdefiniowany dla typu danych, na którym działa. Tak więc kopiowanie
zmiennej typu long do innej zmiennej tego typu odbywa się za pomocą zwykłego przypisania.
Z kolei kopiowanie składowych w postaci klasy lub odziedziczonych fragmentów klasy odbywa
się za pomocą konsturktora kopiującego tej właśnie klasy i dlatego domyślmy konstruktor
kopiujący klasy pochodnej używa jawnego konstruktora kopiującego klasy bazowej do
skopiowania danych klasy bazowej należących do klasy pochodnej. Dzięki temu domyślny
konstruktor kopiujący działa poprawnie zarówno dla nowej składowej klasy pochodnej jak i
dla odziedziczonego obiektu klasy 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.

Przypadek drugi – klasa pochodna z dynamicznym przydziałem pamięci


Opis z przykładem – str 666-668
Załóżmy teraz, że klasa pochodna używa słowa kluzowego new. W tym przypadku oczywiście
musimy zdefiniować w klasie pochodnej desutruktor, konstruktor kopiujący oraz operator
przypisania.
Destruktor klasy pochodnej automatycznie wywołuje destruktor klasy bazowej, tak więc
jedyną jego funkcją jest zwrócenie zasobów zajętych w konstruktorze. Dlatego destruktor
klasy pochodnej musi zwolnić pamięc, na którą wskazuje wskaźnik tej klasy, ale może polegać
na destruktorze klasy bazowej w kwestii zwolnienia pamięci, na którą wskazuje wskaźnik klasy
bazowej.
Konstruktor kopiujący klasy bazowej działa w sposób standardowy (na podstawie przykładu
str 667) dla tablic elementów typu char. Konstruktor kopiujący klasy pochodnej ma dostęp
jedynie do danych tej klasy, dlatego należy wywołać konstruktor kopiujący klasy bazowej do
obsługi danych związanych z klasą bazową. (więcej w przykładzie)
Operator przypisania klasy bazowej działa w sposób standardowy. Ponieważ klasa pochodna
także używa dynamicznego przydziału pamięci należy udostępnić w niej jawny operator
przypisania. Jako metoda klasy pochodnej ma ona bezpośredni dostęp do danych tylko tej
klasy, mimo to jawny operator przypisania klasy pochodnej musi także zadbać o przypisanie
odziedziczonych danych klasy bazowej. Możemy to osiągnąc za pomocą jawnego wywołania
operatora przypisania klasy bazowej. (więcej w przykładzie)
Podsumowując zarówno klasa bazowa jak i pochodna korzystają z dynamicznego przydziału
pamięci, destruktor, konstruktor kopiujący oraz operator przypisania w klasie pochodnej
muszą używać swoich odpowiedników z klasy bazowej do obsługi danych tej klasy. Ta ogólna
zasada realizowana jest na trzy sposoby. W przypadku destruktora odbywa się to
automatycznie. W przypadku konstruktora można wywołać na liście inicjalizaycjnej
konstruktor kopiujący klasy bazowej – w przeciwnym razie wywołany zostanie konstruktor
domyślny tej klasy. W operatorze przypisania należy zastosować operator zakresu i jawnie
wywołać operator przypisania klasy bazowej.
Przykład dziedziczenia z wykorzystaniem dynamicznego przydziału pamięci oraz funkcji
zaprzyjaźnionych – str 668-673

Projektowanie klas – przegląd zagadnień


Metody automatycznie generowane przez kompilator
Konstruktor domyślny
To taki, który nie ma argumentów lub wszystkie jego argumenty są domyślne. Jeśli nie
zdefiniujemy żadnego konstruktora, kompilator automatycznie utworzy konstruktor

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

You might also like