background image

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

background image

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

background image

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

background image

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

background image

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

background image

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

background image

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

background image

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

background image

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

background image

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