thinking in C++
Różnice pomiędzy C a C++:
1. W definicji funkcji w C nazwy są wymagane, w C++ - nie. (Jak nie ma nazwy
nieużywanego argumentu to nie ma irytującego warninga kompilatora).
2. Funkcja może przyjmować dowolną ilość argumentów:
1. W C: zapis f(); W C++: zapis f(…).
2. W C++, zapis f() oznacza, że funkcja nie przyjmuje argumentów.
3. W C i C++, zapis f(void) oznacza to co zapis f() w C++.
3. Zwracana wartość: jak nie ma podanej, to w C domyślnie jest to int. W C++
musi być podana zwracana wartość.
4. W C++ zmienne mogą być deklarowane w locie (w C nie).
5. Stałe w C, to po prostu zwykłe zmienne, których wartości nie mogą być
zmieniane. W C++ stałe czasem nie stają się zmiennymi, i są łączone wewnętrznie.
6. W C++ stała zawsze musi być od razu inicjalizowana, w przeciwieństwie do
C.
7. W C++, w odróżnieniu od C przypisanie wartości typu void* nie jest możliwe
bez rzutowania.
Różne:
* W deklaracji funkcji nie są potrzebne nazwy argumentów.
* Wyrażenie w while(<tu>) może być dowolnie skomplikowane, dopóki zwraca
wartość logiczną.
* W switch(selektor) selektor jest wyrażeniem dającym wartość całkowitą.
* Limits.h, float.h – maksymalne i minimalne wartości możliwe do wyrażenia w
rozmaitych typach danych.
* Zmienne można też definiować wewnątrz if(<tu>), while i for i switch, ale
są problemy wtedy z nawiasami – nie można „onawiasować” takiej definicji.
* Łączenie wewnętrzne – zmienne i funkcje łączone wewnętrznie są widoczne
tylko w obrębie pliku, w przeciwieństwie do łączonych zewnętrznie.
* l-wartość - musi być pojedynczą, nazwaną zmienną (fizycznym miejscem
przeznaczonym do przechowywania danych)
* p-wartość – dowolna stała, zmienna lub wyrażenie zwracające wartość.
* ‘\’ w makroinstrukcji pozwala przejść do następnej linii
* ~ - bitowy operator negacji.
* Jeżeli wartość argumentu znajdującego się po prawej stronie operatora
przesunięcia bitowego jest większa od jego ilości bitów, to wynik jest
nieokreślony. Jeżeli po prawej stronie jest liczba ze znakiem, to zachowanie
operatora jest niezdefiniowane.
* Operator trójargumentowy: i1 ? i2 : i3. jeżeli i1 == true, to wykonywane
jest i2. Zwracaną wartością jest wartość wykonanego wyrażenia
* Słowo asm – można wstawić do programu C++ kod asemblerowy.
* typedef opis-istniejącego-typu nowa-nazwa. W C wykorzystywane przy
deklaracji struktur.
* Identyfikator tablicy nie jest l-wartością. Identyfikator tablicy można
traktować jako wskaźnik do jej początku przeznaczony tylko do odczytu.
* int main(int argc, char* argv[]) – lista argumentów main(), pierwszy to
nazwa programu. Te identyfikatory nie muszą mieć takich nazw.
* Konwersje z biblioteki <cstdlib>
o atoi() – konwersja tablicy znaków na int, wpisanie do funkcji
stringa liczby zmiennoprzecinkowej spowoduje, że atoi() uwzględni wyłącznie
cyfry znajdujące się przed kropką. Jeżeli zostaną znaki nie będące cyframi, to
atoi() zwróci zero.
o atol() – na long
o atof() – na double
* W <cassert> jest makroinstrukcja assert(). Jako argument podaje się
wyrażenie, które deklaruje się jako prawdziwe. Jak nie jest prawdziwe, to assert
wywala program i informuje o błędzie.
* Wskaźniki do funkcji: void (*funcPtr)(); - wskaźnik do funkcji
bezargumentowej nic nie zwracającej. Nazwa funkcji jest jej adresem, tak
następuje inicjalizacja wskaźnika.
Jawne rzutowanie w C++:
Składnia:
zmienna1 = rodzaj_cast<typ>(zmienna2);
Strona 1
thinking in C++
- zmiennej 1 zostaje przypisana zmienna 2 rzutowana ta typ ‘typ’.
static_cast – typowe konwersje niewymagające rzutowania, przekształcenia
zmniejszające rozmiar danych, wymuszenie rzutowania typu void*, niejawne
konwersje typów, statyczne poruszanie się w obrębie hierarchii klas.
const_cast - jeżeli potrzebujemy konwersji z typu oznaczonego modyfikatorem
const lub volatile do typu nie posiadającego takiego modyfikatora. Uwaga: można
przez const_cast dokonać rzutowania tylko takiego rodzaju (mogą być błędy, gdy w
<typ> będzie potrzebne jakieś inne rzutowanie). Rzutuje się poprzez zrzutowanie
wskaźnika do obiektu, a nie samego obiektu.
// long* l – const_cast<long*>(&i); - błąd, gdy i jest const int.
reinterpret_cast – najmniej bezpieczne, można traktować obiekt jakby był
zupełnie innego typu. Przykład:
Struct X { int a[sz]; };
...
X x; int* xp = reinterpret_cast<int*>(&x);
For(int* i = xp; i < xp+sz; ++i)*i = 0;
Rozdział 4 – Abstrakcja danych
• W C++ nie można bez rzutowania przypisać jakiemuś wskaźnikowi wskaźnika typu
void*.
• Można utworzyć strukturę bez składowych, ma ona jednak niezerowy rozmiar.
• Kompilator traktuje podwójne deklaracje klas i struktur jako błąd.
• Nie używać nigdy dyrektyw typu using w plikach nagłówkowych. W praktyce dla
każdego typu lub grupy typów jest osobny plik nagłówkowy i plik z definicjami
funkcji.
• Można zagnieżdżać w sobie struktury.
Rozdział 5 – ukrywanie implementacji
Przyjaciele:
1. Przyjacielem może być:
• funkcja globalna: friend deklaracja_funkcji;
• składowa funkcja innej klasy: friend zwr_typ klasa::funkcja();
• cała inna klasa: friend klasa; (jest to również niepełna specyfikacja przy
okazji…)
2. Utworzenie struktury zagnieżdżonej nie zapewnia jej automatycznie prawa
dostępu do składowych prywatnych. Aby to osiągnąć, należy: najpierw zadeklarować
strukturę zagnieżdżoną, następnie zadeklarować ją używając słowa kluczowego
friend i wreszcie wtedy ją zdefiniować. Definicja musi być oddzielona od friend,
bo kompilator nie uznałby jej za składową struktury.
Różne:
• Niepełna specyfikacja typu – class nazwa_klasy;
• Funkcja memset(adres, wartość, rozmiar(bajty)) – wypełnia wskazany obszar
pamięci zadaną wartością, w bibliotece <cstring>
• Kompilator nie musi umieszczać danych w strukturze z różnymi specyfikatorami
dostępu w jednym bloku kolejno.
Klasy uchwyty:
Jeżeli zmieniam coś w pliku nagłówkowym klasy, to muszę przekompilować (a nie
łączyć jeszcze raz) wszystkie pliki, do których go dołączam. Rozwiązanie – klasy
uchwyty: wszystko, co dotyczy wewnętrznej implementacji znika i pozostaje
pojedynczy wskaźnik – uśmiech - do struktury zawierającej wewnętrzną
implementację, zawartej w pliku zawierającym implementacje funkcji:
Class Handle
{
Struct Cheshire;
Cheshire* smile; // wewnętrzna implementacja
Public:
...
};
Strona 2
thinking in C++
W pliku implementacyjnym:
Struct Handle::Cheshire { int i; };
Trzeba w funkcji inicjalizującej zainicjalizować:
Smile = new Cheshire; np. smile->i = 0; // no i o zwolnieniu pamięci trzeba
pamiętać
Rozdział 6 – inicjalizacja i końcowe porządki
• Instrukcja dalekiego goto, zaimplementowana w postaci funkcji setjmp() i
longjmp(), zawartych w standardowej bibliotece C, nie powoduje wywołania
destruktorów.
• C99 pozwala na definicje zmiennych w dowolnym miejscu zasięgu, podobnie jak w
C++.
• Kompilator sprawdza, czy definicja obiektu (a zatem i wywołanie konstruktora)
nie została umieszczona w miejscu, w którym sterowanie tylko warunkowo
przechodzi przez punkt sekwencyjny, jak na przykład w switch, lub w takim które
może przeskoczyć goto. Przykład:
Switch(i)
{
Case 1: X x2; break; // tu jeszcze działa
Case 2:... //tu już nie działa, bo możemy odwołać się do nie zainicjalizowanego
x2
}
Inicjalizacja agregatowa:
• tablicy elementów typu wbudowanego: int a[5] = { 1,2,3,4,5 };
• Jeżeli zostanie podanych za mało argumentów, to kompilator użyje pierwszych
inicjatorów do zainicjowania pierwszych elementów, a resztę wyzeruje
• Automatyczne zliczanie – nie trzeba podawać rozmiaru, kompilator sam ustali.
• struktury (bez konstruktora, wszystko publiczne): Analogicznie: np. X x2[3] =
{ {2,3,4}, {4,5,6} }; // trzeci jest zerowany.
• jeżeli jest konstruktor, trzeba przy inicjalizacji jawnie go wywołać: Y y1[] =
{ Y(1), Y(2), Y(3) };
Rozdział 7: Przeciążenie nazw funkcji i argumenty domyślne
• Unie mogą również posiadać: konstruktory, destruktory i kontrolę dostępu
• Unia anonimowa – gdy nie podano nazwy typu ani nazwy zmiennej unii – rezerwuje
ona pamięć dla unii, nie wymaga jednak podawania nazwy zmiennej i kropki przy
dostępie do jej elementów.
• Domyślne argumenty są umieszczane wyłącznie w deklaracji funkcji.
• Funkcja memcpy(&gdzie,&skąd,ile) – kopiuje pamięć z jednego do drugiego
obszaru (efektywnie)
Rozdział 8 – Stałe
• Zrobione za pomocą #define – nie ma żadnej informacji o typie, co może
prowadzić do błędów.
• Składanie stałych – kompilator upraszcza złożone wyrażenia złożone ze stałych
podczas kompilacji.
• Stałe są domyślnie łączone wewnętrznie, więc aby móc używać takiej stałej we
wszystkich plikach trzeba w nagłówkowym umieścić deklarację ze słowem extern.
Jednak wymusza się wtedy przydzielenie stałej pamięci. Nie dokonuje się wtedy
składanie stałych.
• Można robić stałe agregaty, wtedy jednak prawie na pewno zostanie im
przydzielona pamięć, nie można używać też wartości zmiennych z takiego agregatu
w trakcie kompilacji (np. do wyznaczania rozmiaru tablicy)
• Różnice w stosunku do C: w C stałe to po prostu zmienne, które nie mogą być
zmieniane, przez co nie można ich wykorzystać np do size’owania tablic.
• Jeżeli pobierany jest adres stałej (nawet niejawnie np podczas pobierania
referencji w funkcji) albo stała zostanie zdefiniowana z użyciem słowa extern,
to jest jej przydzielana pamięć. Stałe można umieścić w pliku nagłówkowym nie
martwiąc się o kolizje w trakcie łączenia.
Wskaźniki do stałych:
• Const int* u; - zapobiega zmianom elementu, na który wskazuje, można zmienić
Strona 3
thinking in C++
adres takiego wskaźnika. Taki zapis jest równoważny: int const* u;
Stały wskaźnik:
Int d = 1;
Int* const w = &d; // można zmieniać wskazywany element, ale nie można zmieniać
adresu – stały wskaźnik zawsze wskazuje na to samo.
• Można utworzyć stały wskaźnik do stałej
• Można przypisać wskaźnikowi do stałej zmienną nie będącą stałą, ale nie
odwrotnie.
• Niezmienniczość jest bezwzględnie egzekwowana w literałach napisowych
(literały będące tablicami znakowymi).
• Można napisać: char* cp = „czesc”; Jednak technicznie jest to błąd, ponieważ
literały napisowe są tworzone przez kompilator jako stałe tablice znakowe, a
wartością zwracaną przez tablicę znaków ujętych w cudzysłów jest adres początku
jej w pamięci. Modyfikacja jakiegokolwiek znaku, znajdującego się w takiej
tablicy jest błędem wykonania programu, chociaż nie wszystkie kompilatory
egzekwują to w poprawny sposób. Zmiana znaku w takim stałym literale jest
niezdefiniowana i nie jest błędem tylko przez mus zgodności z C. Jeżeli chce się
modyfikować łańcuch powinno się go umieścić w tablicy: char cp[] = „czesc”;
• Przekazywanie stałej przez wartość – można dowolną zmienną wrzucić, ale nie
można zmieniać w funkcji wartości jej kopii.;
• Zwracanie stałej przez wartość – zwracana kopia nie może być zmieniana, przez
co funkcji zwracającej stałą nie można użyć jako l-wartości
• Do funkcji pobierającej referencje lub wskaźnik nie można przekazywać obiektu
tymczasowego(np. wartości zwracanej przez inną funkcję), chyba że funkcja
pobiera referencję do stałej.
• Obiekty tymczasowe – są automatycznie tworzone jako stałe.
• Zapis f() = wartość nie jest błędny, gdyż następuje inicjalizacja wartością
stałego obiektu tymczasowego, który i tak jest za chwilę niszczony.
• Ilekroć przekazuje do funkcji obiekt przez referencję lub wskaźnik, powinienem
dać modyfikator const, żeby móc wrzucać do funkcji stałe.
• Return „napis”; - zwraca adres do stałej (litarału znakowego), zapisanego w
danych statycznych (nie jest to obiekt tymczasowy)
• Jak funkcja f() zwraca np const int* const, to jest możliwe przypisanie const
int* x= f(); - Nie trzeba pisać drugiego const, gdyż kopiowana jest wartość
(którą jest adres przechowywany we wskaźniku), zapewnienie że wskazywana przez
nią wartość pozostanie nienaruszona jest automatycznie podtrzymywane, tak więc
ten specyfikator ma znaczenie tylko jeżeli chodzi o używanie jako l-wartości.
• Lista inicjatorów konstruktora – stałe tylko tu można inicjalizować za pomocą
ich konstruktorów (dlatego też wbudowanym typom danych „wbudowano”
konstruktory).
• Na stałych obiektach można wykonywać tylko stałe funkcje składowe. Aby zrobic
stałą funkcję składową, trzeba const napisać tuż przed średnikiem (otwierającym
nawiasem klamrowym). Trzeba powtórzyć słowo const w definicji funkcji, następnie
kompilator wymusza dla takiej funkcji nienaruszalność składowych.
W jaki sposób można zmienić składowe obiektu będącego stałą:
1. Pobiera się wskaźnik this i rzutuje się go na wskaźnik do obiektu bieżącego
typu (this jest domyślnie wskaźnikiem do stałej, więc trzeba rzutować). Nie jest
to jednak zalecane.
2. Słowo kluczowe mutable przed odpowiednimi składowymi.
Modyfikator volatile – składnia identyczna jak w przypadku const (też np funkcje
volatile), oznacza że zmienna może być modyfikowana bez wiedzy
programu/kompilatora.
Rozdział 9 – funkcje inline
• Preprocesor nie ma prawa dostępu do prywatnych składowych klas.
• W makroinstrukcjach trzeba uważać na nawiasowanie.
• Podczas każdego użycia argumentu w makroinstrukcji jest obliczana jego
wartość. Jest to problem, gdy obliczanie wartości wiąże się ze skutkami
ubocznymi.
• Definicje funkcji inline prawie zawsze muszą być umieszczane w plikach
nagłówkowych – kompilator umieszcza w tablicy symboli typ funkcji oraz ciało
funkcji. Funkcja taka ma szczególny status, gdyż nie powoduje to błędu
Strona 4
thinking in C++
wielokrotnej definicji funkcji (definicja musi być jednak identyczna we
wszystkich miejscach, gdzie jest dołączana funkcja inline).
• Kompilator nie jest w stanie dokonać rozwinięcia funkcji gdy: są w niej pętle
lub rekurencja, Gdy bezpośrednio lub pośrednio jest pobierany adres takiej
funkcji (tylko tam funkcja nie będzie rozwijana).
Specjalne instrukcje preprocesora w makroinstrukcjach:
- łańcuchowanie
- łączenie łańcuchów: łączy w jedną dwie sąsiednie tablice znakowe
- sklejanie symboli: dyrektywa ## - skleja dwa symbole, tworząc nowy
identyfikator:
Np: #define FIELD(a) char* a##_string;
Rozdział 10 – zarządzanie nazwami
• Ilekroć są projektowane są funkcje zawierające zmienne statyczne, należy
pamiętać o kwestiach dotyczących wielowątkowości
• Statyczne zmienne w funkcjach, jeżeli nie zostały zainicjalizowane, to są
automatycznie inicjalizowane zerami.
• Funkcja exit() – zakończenie pracy programu, są wszystkie destruktory
wywoływane( najczęściej funkcja main() ją domyślnie wywołuje) – wywołanie w
destruktorze może doprowadzić do nieskończonej rekurencji.
• Funkcja abort() – zakończenie pracy, nie są wywoływane destruktory obiektów
statycznych
• Funkcja atexit() – można ustalić działania które nastąpią po opuszczeniu maina
(wywołana exit()) – funkcja zarejestrowana przez atexit() moze zostać wywołana
przed destrukotarmi wszelkich obiektów, utworzonych przed opuszczeniem funkcji
main().
• Podobnie jak w przypadku zwykłego niszczenia obiektów, niszczenie obiektów
statycznych następuje w kolejności odwrotnej do kolejności ich inicjalizacji.
• Obiekty globalne – tworzone zawsze przed wejściem do funkcji main(), a usuwane
po jej zakończeniu.
• Natomiast konstruktor globalnego obiektu statycznego jest uruchamiany jeszcze
przed wywołaniem funkcji main() – można wykonywać kod przed wejściem do main() i
po wyjściu (destruktor jest uruchamiany po wyjściu z main())
• Słowo kluczowe extern określa w jawny sposób, że widoczność nazwy obejmuje
wszystkie jednostki translacji. Jeżeli zmienną, która wygląda na zmienną
lokalną, zadeklaruje się przy użyciu słowa kluczowego extern, oznacza , że jest
ona przechowywana w jakimś innym miejscu pamięci (jest więc ona w rzeczywistości
globalna w stosunku do funkcji).
• Słowa static można też używać w stosnku do funkcji
Przestrzenie nazw:
• Namespace myLib { deklaracje } – składnia, przestrzeń musi być w zasięgu
globalnym, ew. zagnieżdżone w innym namespace
• Definicja przestrzeni nazw może być „kontynuowana” w wielu plikach
nagłówkowych, za pomocą „powtórnej” definicji.*
• Można utworzyć synonim nazwy przestrzeni nazw: namespace nowa_nazwa =
stara_nazwa;
• Bezimienne przestrzenie nazw – ograniczają tak jak static widoczność do
zasięgu pliku.
• Do przestrzeni nazw można „wstrzyknąć” nazwę funkcji poprzez umieszczenie
wewnątrz przestrzeni klasy z deklaracją friend do tej funkcji:
Namespace me
{
Class us { ... friend void f(); };
} // teraz f() należy też do przestrzeni nazw me
• Sposoby odwoływania się do nazw z przestrzeni:
1. pomocą operatora zasięgu, nazwa_przestrzeni::nazwaZTejPrzestrzeni
2. using namespace nazwa – zaimportowanie od razu całej przestrzeni nazw – można
za pomocą tego przenieść wszystkie nazwy z jednej przestrzeni do drugiej. Można
stosować wewnątrz funkcji. Uwaga – przez takie używanie using, można zrobić
kolizje nazw, które są wykryte dopiero przy korzystaniu z funkcji.
3. deklaracja using: jest deklaracją w obrębie bierzącego pliku, może zasłonić
dyrektywę using, składnia intuicyjna: using nazwa. (nawet przy funkcji – nie
podajemy np. nawiasów). Oznacza to, że jeżeli są przeciążane funkcje, to
Strona 5
thinking in C++
wszystkie uaktywniamy.
• Nie ma czegoś takiego jak problem wielokrotnej deklaracji za pomocą using.
• Składowe statyczne – definicja najczęściej w pliku implementacyjnym
• Przykład definicji dla static int A::i: int A::i = 0; (lub wywołanie
konstruktora)
• Tablice statyczne – definicja taka jak przy inicjalizacji agregatowej
• Stałe statyczne całkowitych typów – definicji można dokonać wewnątrz klasy.
• Statyczne dane nie mogą być definiowane w klasach definiowanych lokalnie
• Można tworzyć statyczne funkcje składowe – wywołuje się je często nie na
konkretnym elemencie, tylko z wykorzystaniem operatora zasięgu: X::f(); mogą
operować tylko na statycznych składowych, nie jest im przekazywany this.
• Można umieścić wewnątrz klasy statyczną składową o typie tym samym co klasa.
• Obiekty statyczne (w sensie zajmowanej pamięci a nie widoczności) umieszczone
w jednostce translacji są inicjalizowane przed pierwszym wywołaniem funkcji
znajdującej się w tej jednostce.
• Nie istnieją żadne ustalenia dotyczące kolejności inicjalizacji obiektów
statycznych znajdujących się w różnych jednostkach translacji. Może to
doprowadzić do poważnych problemów, gdy nasze obiekty statyczne są wzajemnie od
siebie uzależnione.
• Zanim następuje dynamiczna inicjalizacja obiektow statycznych (wywołanie np.
konstruktora), wywoływana jest statyczna inicjalizacja polegająca na wyzerowaniu
zawartości pamięci, nie zawsze jednak to wystarcza, nie zawsze ma znaczenie.
Jak to rozwiązać:
1. tworzy się statyczny obiekt specjalnej klasy służącej do inicjalizowania
zmiennych statycznych – ma ona domyślny konstruktor i destruktor, które
przesuwają licznik, tak że inicjalizacja i kasowanie zmiennych globalnych odbywa
się tylko na początku lub na końcu działania programu. Definicja tej klasy
znajduje się w pewnym pliku nagłówkowym, w pliku implementacyjnym znajduje się
inicjalizacja licznika i definicje zmiennych globalnych. Działa nie tylko w
stosunku do typów wbudowanych, jednak aby obiekty w ten sposób zainicjalizować,
muszą one posiadać jakieś funkcje inicjalizujące i sprzątające zamiast
konstruktorów i destruktorów (o innych nazwach), lub robi się wskaźniki do
obiektów i wykonuje się new w klasie initializer.
2. W przypadku każdej zależności dotyczącej inicjalizacji, obiekt statyczny jest
umieszczany wewnątrz funkcji, zwracającej referencję do tego obiektu, Dzięki
temu jedynym sposobem uzyskania dostępu do statycznego obiektu jest wywołanie
funkcji, a w przypadku gdy obiekt ten musi odwoływać się do innego obiektu, od
którego zależy, musi wywołać funkcję tego obiektu, dzięki czemu kolejność
inicjalizacji jest na pewno właściwa. Uwaga – nie można robić tych funkcji
inline
• Nie można wywołać globalnie funkcji, chyba że jest ona używana do
inicjalizacji zmiennych.
Rozdział 11 – Referencje i konstruktor kopiujący
Referencje przypominają stałe wskaźniki, które są automatycznie wyłuskiwane,
referencja musi być od razu zaninicjalizowana
Można zrobić referencję do wskaźnika: int*& i – dobre przy przekazywaniu
wskaźnika do funkcji, która ma na celu zmodyfikowanie jego adresu.
Jak kompilator wywołuje funkcję:
1) argumenty są najpierw umieszczane na stosie – od prawej strony do lewej, a
następnie jest wywoływana funkcja. Kod wywołujący funkcję jest odpowiedzialny za
usunięcie argumentów ze stosu. W trakcie wykonywania instrukcji call asemblera,
procesor umieszcza na stosie adres kodu programu, spod którego nastąpiło
wywołanie funkcji, dzięki czemu instrukcja return asemblera może wykorzystać ten
adres w celu powrotu do miejsca wywołania.
2) Wartość zwracana przez funkcję jest umieszczana w rejestrze (nie na stosie!)
W przypadku dużych obiektów, które mogą się nie zmieścić w rejestrze, jest
przesyłany adres miejsca, gdzie taką wartość należy zapisać(ukryty argument
funkcji)
Konstruktor kopiujący, składnia: X(const X& x)
Jeżeli wartość zwracana jest ignorowana, to i tak jest tworzony obiekt
tymczasowy, który ją przechowuje.
W przypadku kompozycji, konstruktor kopiujący wywołuje konstruktory kopiujące
składowych.
Strona 6
thinking in C++
Wskaźniki do składowych:
Zdefiniowanie: typSkładowej Klasa::*wskaźnikSkładowej = &KlasaObiektu::składowa
Używanie: wskaźnikObiektu->*wskaźnikSkładowej = wartość;
Obiekt.*wskaźnikSkładowej = wartość;
W rzeczywistości nie istnieje coś takiego jak adres składowej, tak więc
wyrażenie &KlasaObiektu::składowa może być użyte tylko w kontekście wskaźnika do
składowej.
Tych wskaźników nie można inkrementować ani porównywać, można robić wskaźniki do
funkcji składowych. Podobne do normalnego wskaźnika do funkcji, tylko trzeba
dodać operator zasięgu, oraz w trakcie przypisywania trzeba dodać operator
zasięgu.,
Rozdział 12 – przeciążenie operatorów
Operatory można przeciążać operatory w postaci funkcji globalnych
(zaprzyjaźnionych, niebędących składowymi), jak i funkcji składowych
Operatory jednoargumentowe (+,-,~,&,!,++,—)
W klasie Klasa trzeba zadeklarować:
Friend const Klasa& operator@(const Klasa& a);// dla przyrostkowego —,++:
jeszcze ,int
W przypadku operatorów typu ++ - bez const
Zwraca się *this;
Następnie jest definicja odpowiednich funkcji.
Funkcje składowe – analogicznie, tylko nie dajemy dodatkowego argumentu Klasa&
a.
Operatory dwuargumentowe – analogicznie, tylko dwa argumenty. Dla funkcji
modyfikujących, lewy argument nie może być stałą (dla arg. Typu +=)
Operatory warunkowe zwracają int będący wartością logiczną.
W operatorach modyfikujących należy dopisać: if(&left == &right) { ewentulana
obsługa przypisania do samego siebie }. W funkcjach składowych jest tylko right.
Operator = może być tylko funkcją składową.
Możliwe jest przeciążanie operatorów działających na różnych typach danych
(dodawanie gruszek do jabłek)
Ogólne uwagi:
1) jeżeli nie zamierza się modyfikować argumentów operatora, to należy je
przekazać jako referencje do stałej
2) Typ wartości zwracanej powinien zależeć od spodziewanego znaczenia operatora
3) Nie należy zwracać referencji do stałej, żeby wyrażenie mogło wystąpic jako
l-wartość
4) Dla przyrostkowego operatora in(de)krementacji, konieczne jest zwrócenie
wartości przez wartość, trzeba się dobrze zastanowić czy to ma być stała, czy
nie
Return X(x); - nie jest to wywołanie konstruktora, tylko utworzenie obiektu
tymczasowego, dlatego efektywniejsze to jest od: X x1(x); return x1;
Dlatego efektywniejsze, gdyż gdy kompilator widzi taką instrukcję, to wie, że
obiekt jest tworzony wyłącznie w celu jego zwrócenia, więc tworzy obiekt
bezpośrednio w miejscu przeznaczonym na zwróconą wartość, nie ma też
konieczności wywołania destruktora, gdyż nie jest tworzony obiekt lokalny.
Nietypowe operatory:
- operator[] – musi być funkcją składową, i wymaga jednego argumentu, używamy
gdy chcemy, żeby obiekt zachowywał się jak tablica
- operator przecinkowy, nie jest raczej używany.
- operator -> - używany, gdy chcemy, żeby obiekt zachowywał się jak wskaźnik,
musi być funkcją składową klasy, ma dodatkowe ograniczenie: musi zwracać obiekt
(albo referencję), również posiadający operator wyłuskania wskaźnika, albo
zwracać wskaźnik, który może zostać użyty do wybrania tego, co wskazuje strzałka
operatora.
Operator *-> - operator dwuargumentowy, naśladowanie wskaźnika do składowej
Funkcja ta musi zwracać obiekt, dla którego można wywołać funkcję operator() z
argumentami przeznaczonymi dla wywoływanej funkcji składowej.
Operator() – też musi być funkcją składową, dopuszcza dowolną liczbę argumentów,
obiekt wygląda tak, jak by był funkcją
Strona 7
thinking in C++
Operatory, których nie można przeciążać: wyboru składowej (.), wyłuskania
wskaźnika do składowej (.*), nie ma operatorów definiowanych przez użytkownika,
nie można zmieniać reguł dotyczących priorytetów operatorów.
Funkcja składowa, czy nie? Zalecenia:
Wszystkie operatory jednoargumentowe funkcje składowe
=,(),[],->,->* muszą być składowe
+=,-=,/=,*=,^=,&=,|=,%=,»=,«= składowe
Wszystkie pozostałe operatory dwuargumentowe nie składowe
Technika zwiększająca szybkość: przy kopiowaniu obiektu zwiększamy tylko licznik
obiektów, tworzymy nowy dopiero przy zapisie.
Automatyczna konwersja typów
Konwersja za pomocą konstruktora: (odpowiedzialna klasa docelowa)
Definiujemy konstruktor, pobierający jako jedyny argument obiekt (lub
referencję) innego typu, niejawnie jest on wywoływany. Jeżeli nie chcemy do tego
doprowadzać, należy przed konstruktorem dodać słowo kluczowe explict – wtedy
będzie trzeba jawnie wywoływać
Przeciążenie operatora: (odpowiedzialna klasa źródłowa)
Składania: operator Typ() const { defincja }, też można explict użyć
Jednym z najważniejszych powodów stosowania globalnych przeciążonych operatorów
jest fakt, że w ich przypadku konwersja może być zastosowana w odniesieniu do
każdego argumentu (np wyrażenie 1 + a przy składowym nie będzie się kompilować)
Może być tylko jedna konwersja z danego typu do drugiego, w przeciwnym wypadku
będzie dwuznaczność.
Rozdział 13 – dynamiczne tworzenie obiektów
1.Pamięć może zostać przydzielona, zanim rozpocznie się praca programu – w
obrębie obszaru danych statycznych, obszar ten istnieje przez cały czas
działania programu.
2. Pamięć moża zostać przydzielona na stosie
3. Pamięć może zostać przydzielona na stercie. W celu przydzielenia tej pamięci
w trakcie pracy programu, wywoływana jest funkcja.
Obsługa sterty w C:
Funkcje: malloc(), calloc(), realloc() – przydzielają pamięć
Free() – zwalnia pamięć.
Przykład użycia malloc(): Obj* obj – (Obj*)malloc(sizeof(Obj)); free(obj);
Malloc() zwraca void*. Malloc() zwraca wartość zerową, gdy nie uda się jej
przydzielić pamięci.
Jeżeli zrobimy pamięć przez malloc() i zwolnimy przez free(), to wynik jest
niezdefiniowany (prawdopodobnie będzie działać, tylko destruktor nie zostanie
wywołany)
Jeżeli wskaźnik usuwany za pomocą delete jest zerowy, to nic się nie stanie.
Jak to działa:
1) wywoływana jest funkcja malloc(), zgłaszająca żądanie przydziału pamięci
2) przeszukiwana jest pula pamięci w celu znalezienia wystarczająco dużego bloku
– działa to w różnym czasie, jest wolniejsze od stosu.
Usuwanie wskaźnika void* jest prawdopodobnie błędem – delete musi wiedzieć, jaki
jest typ usuwanego obiektu. Należy uważać, żeby nie usunąć przez delete czegoś,
co nie jest na stercie
Gdy operator new nie potrafi znaleźć pamięci, to wywoływana jest funkcja obsługi
operatora new (sprawdzany jest wskaźnik do tej funkcji i jeżeli nie jest zerowy,
to funkcja jest wywoływana).
Domyślnym zachowaniem funkcji obsługi operatora new jest zgłoszenie wyjątku.
Aby wymienić funkcję obsługi operatora new, należy dołączyć do programu plik
nagłówkowy new.h i wywołać funkcję set_new_handler(), podając jej adres funkcji,
która ma zostać zainstalowana. Funkcja ta nie może pobierać argumentów ani
zwracać wartości.
Przeciążanie argumentów new i delete:
Strona 8
thinking in C++
Przeciążony operator musi pobierać argument typu size_t. Argument ten jest
generowany i przekazywany funkcji przez kompilator. Funkcja musi zwrócić albo
wskaźnik do obiektu żądanej wielkości (lub większej) – void*, albo wartość
zerową, w przypadku gdy nie można znaleźć wolnej pamięci (wtedy nie zostanie
wywołany konstruktor)
Uwaga na przeciążanie podczas dziedziczenia!
Gdy robimy tablicę operatorem new, to rozmiar jej jest 4 bajty większy – w nich
jest zapisana m.in. informacja o rozmiarze tablicy.
Rozdział 14 – dziedziczenie i kompozycja
Składnia dziedziczenia: class Son : public/private/protected Parent { … };
Wszystkie składowe klasy Parent stają się składowymi klasy potomnej. W
rzeczywistości następuje coś podobnego do kompozycji: klasa Son zawiera obiekt
podrzędny klasy Parent.
Klasa potomna nie ma dostępu bezpośredniego dostępu do składowych prywatnych, ma
za to do protected i publicznych. Można w klasie potomnej tworzyć nowe funkcje,
składowe o nazwach takich samych jak w podstawowej – zasłaniają one poprzednie.
Aby wygrzebać poprzednią, trzeba to zrobić operatorem zasięgu.
Trzeba wywołać konstruktor klasy podstawowej w liście inicjatorów konstruktora.
Zawsze wywoływane są wszystkie konstruktory i destruktory klas podrzędnych –
konstruktory od dołu do góry, destruktory odwrotnie. Kolejność wywołań nie jest
związana z kolejnością na liście inicjatorów, kolejność ta jest wyznaczona przez
kolejność obiektów w klasie. Przedefiniowanie jednej funkcji, zasłania wszystkie
funkcje przeciążone z klasy podrzędnej.
Funkcje, które nie są automatycznie dziedziczone:
- operator przypisania
- konstruktory i destruktory
Statyczne funkcje składowe tak samo są dziedziczone.
Domyślnie, dziedziczenie jest typu private ( gdy nie użyje się specyfikatora)
Rzutowanie w górę (upcasting) – konwersja wskaźnika lub referencji do klasy
potomnej na wskaźnik/referencję klasy podstawowej, nie trzeba używać jawnych
rzutowań.
Każdy komunikat, który może być wysłany do klasy podstawowej, może być też
wysłany do klasy potomnej.
Ilekroć tworzymy konstruktor kopiujący, powinniśmy pamiętać o poprawnym
wywołaniu konstruktora klasy podstawowej.
Rozdział 15 – polimorfizm
Wiązanie wywołania funkcji – połączenie wywołania funkcji z jej ciałem.
Kiedy wiązanie jest dokonywane przez kompilator lub program łączący – wtedy jest
to wczesne wiązanie.
Późne wiązanie – wiązanie jest wykonywane w trakcie wykonywania programu na
podstawie informacji o typie obiekt – kompilator co prawda nie wie, jaki jest
rzeczywisty typ obiektu, ale wstawia kod umożliwiający odnalezienie i wywołanie
odpowiedniego ciała funkcji.
Składnia do użycia późnego wiązania – słowo kluczowe virtual w deklaracji
funkcji, znajdującej się w klasie podstawowej (wyłącznie w deklaracji). Jeżeli
funkcja została zadeklarowana jako wirtualna w klasie podstawowej, to
automatycznie wszystkie funkcje w klasach pochodnych są wirtualne. Zasłanianie –
przedefiniowanie funkcji wirtualnej w klasie pochodnej.
W jaki sposób C++ realizuje późne wiązanie:
Typowy sposób:
Kompilator dla każdej klasy zawierającej funkcje wirtualne tworzy pojedynczą
tablicę, nazywaną VTABLE, gdzie umieszcza adresy funkcji wirtualnych zawartych w
klasie. W każdej klasie posiadającej funkcje wirtualne niejawnie jest
umieszczany wskaźnik wirtualny (VPTR), wskazujący na VTABLE tej klasy. Gdy za
pomocą wskaźnika obiektu klasy podstawowej wywołuje się funkcję wirtualną,
kompilator niejawnie wstawia kod, pobierający wskaźnik VPTR i odnajdujący adres
funkcji w tablicy VTABLE.
W każdej klasie z f. wirtualnymi jest ukryta informacja o typie. W przypadku
umieszczenia w klasie bez składowych informacji o typie, zasłania ona „ślepą”
Strona 9
thinking in C++
składową.
Wszystkie obiekty klasy podstawowej i klas pochodnych tej klasy, posiadają
wskaźniki VPTR w tym samym miejscu. Konstruktor inicjalizuje VPTR.
Klasa abstrakcyjna - jest taka wtedy, gdy tworzy się w niej chociaż jedną
funkcję czysto wirtualną.
Funkcja czysto wirtualna – też słowo kluczowe virtual, ale na jej końcu
występuje = 0;
Nie można tworzyć obiektów takiej klasy, aby klasa pochodna nie była
abstrakcyjna, musi mieć zasłonięte wszystkie czysto wirtualne funkcje z
podstawowej. Nie trzeba pisać definicji funkcji wirtualnych, wystarczy napisać
=0;
Przez to nie da się przez wartość przekazać obiektu klasy abstrakcyjnej np.
poprzez rzutowanie. Funkcja czysto wirtualna może jednak zawierać definicję,
która np. może mieć wspólny kawałek kodu dla reszty funkcji.
Jeżeli dokona się rzutowania w górę obiektu, a nie wskaźnika lub referencji, to
zostanie on okrojony.
Strona 10