357
C++ język, nauka Czas nauczenia się programowania w c++ zależy od stopnia zaawansowania jaki się chce osiągnąć oraz opanowania technik towarzyszących. Zwykle 1 rok intensywnej nauki! Dwa sposoby na naukę: podglądać styl pracy kogoś doświadczonego, studiować dobre przykłady kodu w c++ Pisać samodzielnie jak najwięcej kodu! Być dociekliwym i sprawdzać nabytą wiedzę! Wszystkie poprzednie pytania egzaminacyjne dostępne na stronie przedmiotu.

Czas nauczenia się programowania w c++ zależy od stopnia …it.pwsz-ns.edu.pl/~wprzygoda/ZP2017_wyklad.pdf · C++ język, nauka • Czas nauczenia się programowania w c++ zależy

  • Upload
    lamkien

  • View
    216

  • Download
    0

Embed Size (px)

Citation preview

C++ język, nauka

• Czas nauczenia się programowania w c++ zależy od stopnia zaawansowania jaki się chce osiągnąć oraz opanowania technik towarzyszących. Zwykle 1 rok intensywnej nauki!

• Dwa sposoby na naukę: podglądać styl pracy kogoś doświadczonego, studiować dobre przykłady kodu w c++

• Pisać samodzielnie jak najwięcej kodu!• Być dociekliwym i sprawdzać nabytą wiedzę!

• Wszystkie poprzednie pytania egzaminacyjne dostępne na stronie przedmiotu.

C++ historia, współczesność, przyszłość

Język C++ jest wieloparadygmatowym językiem programowania. Stworzony w latach osiemdziesiątych XX wieku przez Bjarne Stroustrupa

•C++98 ISO/IEC 14882:1998•C++03 ISO/IEC 14882:2003•C++11 ISO/IEC 14882:2011•C++14 ISO/IEC 14882:2014

C++14 – dlaczego standard jest ważny?

Standard to brak zależności od• rodzaju kompilatora• systemu operacyjnego• CPUStandard odwołuje się / opisuje działanie abstrakcyjnej maszyny.Kompilator ma za zadanie zrealizować ten opis na konkretnym sprzęcie.

C++98/C++03 – abstrakcyjna maszyna była jednowątkowaC++11/C++14– abstrakcyjna maszyna zaprojektowana jako wielowątkowa– model pamięci (organizacja pamięci i sposoby dostępu do pamięci)– na niskim poziomie gwarantowane operacje atomowe

w określonej kolejności

C++ podstawowe cechy

• Główne cechy języka: • język kompilowalny, ogólnego przeznaczenia, określany

jako język „średniego poziomu” – dokument opisujący standard C++14 ma 1366 stron

• silna (statyczna) kontrola typów podczas kompilacji: pewna forma weryfikacji poprawności kodu, pozwalająca na wczesne wykrycie błędów lub niezamierzonego działania

• język swobodnego formatu, rozmieszczenie znaków na stronie nie ma znaczenia, ale każda instrukcja musi być zakończona średnikiem ;

• C++ nie wspiera własności specyficznych dla danej platformy lub niebędących własnościami ogólnego przeznaczenia

C++ style programowania

• C++ nie narzuca żadnego stylu, daje programiście możliwość wyboru.

• programowanie proceduralne: organizowanie kodu w postaci procedur, wykonujących ściśle określone operacje, dane nie powiązane z procedurami, jako parametry wywołania procedur

• programowanie obiektowe: zbiór obiektów komunikujących się pomiędzy sobą w celu wykonywania zadań, obiekt to element łączący stan (dane) i zachowanie (metody)… programowanie funkcjami wirtualnymi

• programowanie uogólnione: kod programu bez wcześniejszej znajomości typów danych, szukanie i systematyka abstrakcyjnych reprezentacji efektywnych algorytmów, struktur danych i innych elementów programowych… programowanie szablonami

C++ literatura (1) – kanon literatury

International Standard (można kupić – cena zaporowa)ISO/IEC 14882:2014(E)

C++14 Final Documentwww.open-std.org/jtc1/sc22/wg21/draft N3797(2013-10-13)

Bjarne Stroustrup• Język C++ (Wyd. IV)• Programowanie. Teoria i praktyka

z wykorzystaniem C++ (Wyd. II popr.)

C++ literatura (2) – „stare ale jare” (niestety, nie C++11)

Bruce EckelThinking in C++, vol. I i II (po angielsku – on-line)

Jerzy Grębosz• Symfonia C++ Standard (C++03)• Pasja C++ (niestety stare)

Uw

aga:

pow

staj

e no

wa

wer

sja…

C++ literatura (3)

Nicholas A. Solter, Scott J. KleperC++ Zaawansowane programowanie

Wydanie III po angielsku

Stephen PrataJęzyk C++. Szkoła programowania. Wydanie VI

Siddhartha RaoC++. Dla każdego. Wydanie VII

C++ literatura (4)

D. Ryan StephensC++ Receptury(O’Reilly)

Anthony WilliamsJęzyk C++ i przetwarzanie współbieżne w akcji

David Vandevoorde, Nicolai M. JosuttisC++ szablony. Vademecum profesjonalisty

Aktualizacja w roku 2017

Nicolai M. JosuttisC++. Biblioteka standardowa. Podręcznik programisty Wyd. II

C++ literatura (5)

Scott Meyers – „C++ 50 efektywnych sposobów na udoskonalenie Twoich programów”

– „Język C++ bardziej efektywny”– „STL w praktyce: 50 sposobów efektywnego wykorzystania”– „Skuteczny nowoczesny C++. 42 sposoby lepszego

posługiwania się językami C++11 I C++14”

Herb Sutter

– „Wyjątkowy język C++ 47 łamigłówek…”– „Wyjątkowy język C++ 40 nowych łamigłówek…” – „Niezwykły styl języka C++ 40 nowych łamigłówek…”– „Język C++ Standardy kodowania 101 zasad…”

(współautor: Andrei Alexandrescu)

KURSY DOSTĘPNE ON-LINEKarol „Xion” Kuczmarski – Kurs C++ (Megatutorial)Sektor van Skijlen – C++ bez cholesteroluPiotr Białas, Wojciech Palacz – Zaawansowane C++pl.wikibooks.org/wiki/C++ – niekompletny jeszcze…Frank B. Brokken – C++ Annotations Ver. 10.1.x

C++ literatura anglojęzyczna (1)

Marc Gregoire, N.A. Solter, S.J. KleperProfessional C++ 3rd Edition

Scott MeyersOverview of The NewC++ (C++11/14)Effective Modern C++

Wywiady,prezentacje… http://channel9.msdn.com

Wikipedia (EN, PL)• hasło C++11, C++14

(także C++0x)

Forum stackoverflow(tagi C++11,C++14)

http://stackoverflow.com/

C++ literatura anglojęzyczna (2)

S. B. Lippman et al.C++ Primer5th Edition

Nicolai M. JosuttisThe C++ Standard Library - A Tutorial and Reference, 2nd Edition

S. Meyers, H. SutterAndrei AlexandescuC++ and Beyond 2010-14

Stephen PrataC++ Primer Plus6th Edition

Alex KorbanC++11 Rocks(VS2013 & gcc version)

C++ literatura anglojęzyczna (3)

Herb SchildtC++ ProgrammingCookbook (2008)

Harvey M. Deitel,Paul J. DeitelC++ How to Program10th Edition

Walter Savitch• Absolute C++ (5th Edition)• Problem Solving with C++

(9th Edition)

C++ informacje, materiały wideo, konferencje

www.isocpp.org

CPPCON 2014 (Bellevue, WA)

www.youtube.com/user/CppCon

Kompilator

• Używanie gcc zamiast g++– GCC (GNU Compiler Collection) kompiluje różne języki (C, C++,

Objective-C, Objective-C++, Java, Fortran, Ada). gcc rozpoznaje kod źródłowy C++ po rozszerzeniach:

– gcc nie konsoliduje skompilowanego kodu z biblioteką standardową c++

– jeśli użyjesz gcc to będziesz musiał podać ręcznie ścieżkę do plików nagłówkowych oraz do biblioteki standardowej!

• Kompilator g++ – tłumaczy kod źródłowy na język assembler lub rozkazy komputera

• Konsolidator g++ (linker) – dopasowuje odwołania symboli do ich definicji

• Najnowsze wersje całkowicie wspierają standard C++14 (wersja 5.3)

.C, .cc, .cpp, .CPP, .c++, .cp, .cxx

Nie utrudniajmy sobie życia i używajmy g++

Kompilator – wsparcie nowego standardu

Kompilowanie kodu według nowego standarduWsparcie kompilatora dla standardu C++11 (C++14) wymaga dodatkowej opcji (flagi):

Przykładowo (linux):Program jest w katalogu /usr/binPliki nagłówkowe w katalogu /usr/include/c++/5.3Biblioteki w katalogu /usr/lib/gcc/i486-linux-gnu/5.3

Na pracowni komputerowej w chwili obecnej kompilator g++ 4.9.2 jest dostępny tylko pod windows, np. poprzez środowisko Dev-C++ ale po ustawieniu odpowiednich ścieżek oraz opcji –std=c++14 Pod linuxem na razie jest niewystarczający kompilator w wersji 4.7.2

g++ -std=c++11 …

g++ -std=c++14 …

Kompilowanie i linkowanie (konsolidacja)

Prosty program o nazwie myprog z pliku prog1.cc

Plik obiektowy (bez konsolidacji do programu)

Konsolidacja do programu wykonalnego

Uruchomienie programu (linux)

gdzie „ . ” (kropka) oznacza pełną nazwę ścieżki, chyba że ścieżka do katalogu z programem jest w zmiennej PATH

g++ -std=c++14 -o myprog prog1.cc // to samo: g++ -std=c++14 prog1.cc -o myprog

g++ -std=c++14 -c -o prog1.o prog1.cc

g++ -std=c++14 -o myprog prog1.o

./myprog

Kompilatory – kilka uwag

Można mieć zainstalowane kilka wersji g++ oraz biblioteki standardowej. Napisz: i postukaj „tab” (pokażą się wszystkie programy zaczynające się na g++)

Zwykle g++ to link symboliczny do jednej z wersji. Sprawdzenie wersji:

Inne kompilatory warte uwagi:

Kompilowanie plików nagłówkowych• niektóre kompilatory pozwalają na prekompilowanie plików nagłówkowych,

w dużych projektach znacznie może to przyspieszyć proces kompilacji• g++ kompilując plik .h tworzy plik z rozszerzeniem .h.gch• prekompilowany plik jeśli znaleziony, może być brany jako pierwszy przed plikiem .h• student robi to zwykle przez pomyłkę, niepotrzebnie umieszczając na liście plików źródłowych

do kompilowania również pliki .h (może to prowadzić do zaskakujących problemów w stylu „edytuję plik .h i nic się nie dzieje”)

g++ -v // lub: g++ --version

g++

clang ver. 3.9, Intel C++ ver. 16, Microsoft Visual Studio C++ 2015

Pierwszy program

Program wymaga napisania funkcji:

lub

W kodzie – tylko jedna funkcja main. Uwaga: dawniej (przed rokiem 1998) dopuszczano postać funkcji zwracającej void (tzn. „nic”), teraz musi zwracać int.

Paradoksalnie, jest to jedyna funkcja, w której (skoro „coś” zwraca) nie trzeba pisać instrukcji „return”. Można (ale nie trzeba) jawnie napisać:

int main() { }

int main() {return 0;

}

auto main() -> int { }

Nawiasy, komentarze

void fun() {// komentarz jednolinijkowy – do końca linii/* komentarz większego obszaru,

nie można zagnieżdżać… */}void fun2(){

// takie nawiasy // czytelniejsze?...

}

Niektóre instrukcjenie wymagają pary nawiasów:

if ( true )zawszeWykonaj();

Łatwiej jednak coś dopisaći nie pomylić się, zawszestosując nawiasy!

if ( true ) {zawszeWykonaj();latwoDopisz();

}

Pierwszy program: średnik

Uważaj na średnik – są miejsca, w których średnik jest konieczny, a są, w których jest zbędny lub nieprawidłowy.

Nie musimy stawiać średnika za nawiasem kończącym definicję funkcji – jest niepotrzebny (ale nie jest błędem)

Średnik na końcu dyrektywy preprocesora – to jest błąd! (w przykładach jak obok)

Średnik konieczny jest na końcu definicji klasy!

Przykład kompilującego siękodu, w którym przez pomyłkę mamy niezamierzone działanie…

#include "mojplik.h";#define FLAGA;

class Klasa { } ;

int fun3() {return 2;

} ;

for (int i=0; i<10; ++i);// tutaj instrukcja, którą może ktoś// zamierzał wykonać 10 razy…

; ; ; Seria średników jest legalna, bo pusty średnikoznacza pustą instrukcję.

#include <iostream>using namespace std;

int main() {cout << "I am Jan B. " << "za zycia napisalem ponad " << 100<< " ksiazek!\n";cout << "A Ty ile napisales: ";int liczba;cin >> liczba;if (liczba < 100) cout << "\n…Tak malo!";return 0; // return EXIT_SUCCESS

}

Pierwszy program – który coś robi

#include <iostream.h> // NIE UŻYWAĆczasem implementowane tak:

#include <iostream> using namespace std;

w nowych kompilatorach ostrzeżenia, a nawet może się nie skompilować

Można wskazać na konkretną deklarację użycia:using std::cout;using std::cin;Można pisać std::cout oraz std::cin- tak się robi w plikach nagłówkowych .h, w których nie piszmy dyrektywy użycia całej przestrzeni nazw std

Biblioteki z C:#include <cstdlib>#include <cstdio>#include <cassert>

Pierwszy program „wielowątkowy” ( C++11/14 )

Bez join() główny wątek nie czekałby na zakończenie wątków podrzędnych, jeśli te nie zakończą się, nastąpi terminate()

#include <iostream>#include <thread>#include <chrono>#include <atomic>

void moja_funkcja() { // static unsigned licznik;static std::atomic_uint licznik; // typy atomowestd::this_thread::sleep_for( std::chrono::seconds( 1 ) );std::cout << "Jestem watek nr " << ++licznik << "!\n";

}int main() {

std::thread t1(moja_funkcja);std::thread t2(moja_funkcja);std::thread t3(moja_funkcja);t1.join();t2.join();t3.join();std::cout << "Glowny watek!\n";

}Kolejność wykonywania wątków przypadkowa!

std::thread

”Główny wątek!\n”

narzutzwiązanyz tworzeniemwątku

”Jestem z wątku…”

wątekzablokowany

Zajrzyjmy w głąb <iostream>

#include <ostream>#include <istream>

extern istream cin;extern ostream cout;extern ostream cerr;extern ostream clog;

Deklaracje obiektów odpowiadającychza pracę na strumieniu wejście / wyjście.Obiekty konstruowane przed main()

Hier

arch

ia k

las

odpo

wie

dzia

lnyc

h za

pr

acę

na st

rum

ieni

ach

cin – obiekt odpowiedzialny za obsługę standardowego strumienia wejściowego (zwykle powiązanego z klawiaturą), wywołanie powoduje opróżnienie buforu coutcout – obiekt odpowiedzialny za strumień wyjściowy (zwykle powiązany z monitorem)cerr – obiekt standardowego strumienia komunikatów o błędach, powiązany przez system z monitorem, strumień niebuforowanyclog – obiekt wyprowadzany standardowo tak jak cerr, strumień buforowany

Operatory, manipulatory, znaki specjalne

operator<< oraz operator>> są to operatory przesunięcia bitowego, jednak dla obiektów strumienia są przeciążone i stają się „operatorami wejścia/wyjścia”

Co lepiej na końcu: std::endl czy \n ?MANIPULATORY ( tak naprawdę funkcje)endl – dodaje do buforu znak ’\n’ orazwykonuje flush – opróżnienie buforuends – wkłada znak kończący łańcuch znakowy, czyli symbol zerowy ’\0’ flush – opróżnia buforws – czyta i ignoruje białe znakiKod dla guru (przykład):

ostream& ostream::operator<<( ostream& (*op) (ostream&) ) {

return (*op) (*this); }std::ostream& std::endl (std::ostream& s) {

s.put('\n'); s.flush(); return s;

}

Można tak: std::cout << std::endl;lub tak: std::endl ( std::cout );

ZNAKI SPECJALNE (stałe znakowe)\n nowa linia\r powrót do początku linii\t pozioma tabulacja\a alarm dźwiękowy\0 symbol zerowy (koniec łańcucha)MNIEJ UŻYWANE\v pionowa tabulacja\b powrót o jedną pozycję\f nowa strona (drukarka)\? znak zapytaniaKONIECZNE W ŁAŃCUCHU ZNAKOWYM\\ lewy ukośnik\’ apostrof\” cudzysłów

Deklaracja – co to jest?

Deklaracja – to wprowadzenie w danej jednostce translacji (pliku) nazwy (lub nazw), albo redeklaracja nazw wprowadzonych poprzednimi deklaracjami.Deklaracje generalnie określają jak mają być rozumiane dane nazwy.Deklaracja może być też definicją, chyba że (i wtedy są to tylko deklaracje):• deklarujemy funkcję bez definiowania jej ciała

• deklaracja poprzedzona jest specyfikatorem extern, w znaczeniu obiektu zdefiniowanego w innym pliku

• deklaracja z użyciem extern jako sposób konsolidacji (linkowania) kodu

void fun( double d, short n, int );

gdy chcemy „zlinkować” z kodem z innego języka, musimy zadeklarować nazwy obiektów tam zdefiniowanych

extern ”C” int fun ( float );extern ”C” { /* tutaj lista deklaracj */ }

deklarujemy, że w innym pliku będzie zdefiniowana zmienna typu double o nazwie d. UWAGA: jeśli użyjemy specyfikatora extern oraz inicjalizujemy zmienną, np. extern double d = 3.14; to oznacza to już definicję a nie deklarację!

extern double d;

nie ma definicji (ciała) funkcji, czyli części ujętej w nawiasy { } to jest to tylko deklaracja

Deklaracje (2)

• deklarujemy statyczną składową w definicji klasy

• deklarujemy nazwę klasy (bez jej definiowania):• deklarujemy (silny) typ wyliczeniowy (C++11/14)

Deklaracjami nazywamy również:• deklarację z użyciem typedef

• deklarację użycia using lub dyrektywę using

class Foo;

zmienna statyczna w definicji klasy to dopiero jej deklaracja – jak się później dowiemy, taką zmienną definiuje się dopiero poza ciałem klasy

using std::cout;using namespace std;

class Foo {static int n;

};

„klasa wyliczeniowa” (albo „silny typ wyliczeniowy”) pozwala na uprzednią deklarację wraz ze specyfikacją typy danych wyliczeniowych (typ musi być całkowity)

enum class EColor;enum struct EShape : char;

deklaracja użycia czegośdyrektywa użycia którejś przestrzeni nazw

typedef int Calkowity, *PtrCalkowity;Calkowity n1; PtrCalkowity ptr1;

n1 jest typu int, zaś ptr1 jest typu „wskaźnik do int”

Definicje – reguła jednej definicji (One Definition Rule)

Jedna definicja – żadna jednostka translacji (plik) nie może zawierać więcej niż jednej definicji jakiejkolwiek zmiennej, funkcji, klasy, typu wyliczeniowego lub szablonu.

Definicja może się znajdować w programie, zewnętrznej bibliotece (standardowej, użytkownika).Definicja klasy konieczna jest w danym pliku, gdy typ klasy używany jest w sposób wymagający znajomości kompletnej definicji.

One ring to rule them all, one ring to find them,

One ring to bring them all and in the darkness bind them.

class Foo;struct Foo* ptr1;Foo *ptr2;

w tych przypadkach nie ma konieczności znajomości definicji klasy, wystarczy deklaracja jej nazwy

Czasami definicja może się „powtórzyć” w różnych plikach. Dotyczy to klasy, typu wyliczeniowego, funkcji inline (extern inline), szablonu klasy, statycznej zmiennej oraz metody składowej w szablonie klasy, niestatycznego szablonu funkcji, specjalizacji szablonu… (C++14 §3.2.6) – wszystko to pod pewnymi warunkami! (zasadniczo jest to powtórzenie tego samego kodu z ew. dopisanymi wartościami domyślnymi funkcji)

Definicje – przykłady ( C++11 §3.1.2)

int a; extern const int c = 1; int f (int x) { return x+a; }class S { int a; int b; }; struct X {

int x; static int y; X(): x(0) { }

};int X::y = 1; enum { up, down }; namespace N { int d; }namespace N1 = N; X anX;

definiuje zmienną a typu int - całkowitego

definiuje stałą c typu int (bo inicjalizacja!)

definiuje funkcję f i zmienną lokalną x

definiuje klasę S i zmienne składowe a i b

początek definicji struktury X

definicja konstruktora struktury X

deklaracja statycznej składowej y

definicja niestatycznej składowej x

definicja składowej statycznej X::y

definiuje up i down (typ wyliczeniowy)

definiuje przestrzeń nazw N i składnik d

definiuje przestrzeń nazw N1 (jako „alias”)

definiuje obiekt anX typu X

Organizacja kodu (header guard)

DEKLARACJE zmienny, funkcji nie-inline lub DEFINICJE funkcji inlineDEFINICJE klas, DEFINICJE szablonówplik nagłówkowy ( .h )Wielokrotne włączenie tego samegonagłówka (#include) – wielokrotna definicja – pogwałcenie reguły ODR – błąd!

Aby temu zapobiec, w plikach nagłówkowych zawsze korzystamyz dyrektyw preprocesora (blokada, tzw. header guard)

DEFINICJE zmiennych, funkcji, metod klasplik źródłowy ( .cc / .cpp )

Cytat z „Megatutorial-u” Karola Kuczmarskiego(Państwa niewiele starszego kolegi…)

Dyrektywa #include jest głupia jak cały preprocesor.

Dyrektywa

#pragma once• pierwotnie działała tylko w niektórych

kompilatorach, np. MS Visual C++• pragma nie jest polecana przez twórców gcc

jako dyrektywa z definicji „zależna od implementacji”, choć działa w g++ od ver. 3.4

#ifndef FIGURA_H#define FIGURA_H

// tutaj cała zawartość pliku

#endif // FIGURA_H

Pułapki myślenia o blokadach

header guard (czyli zestaw #ifndef #define … #endif) nie chroni przed problemem podczas konsolidacji plików, jeśli w pliku nagłówkowym, włączonym do tych różnych plików, zdefiniowaliśmy coś, co pogwałci ODR. Skompiluje się, ale linker zgłosi „multiple definition”header guard chroni jeden dany plik źródłowy przed wielokrotnym włączeniem (i kompilacją) tego samego pliku nagłówkowego, wielokrotne włączenie może nastąpić również nie wprost, przez inne włączane pliki

#ifndef H2_H#define H2_H#include ”h1.h”

// coś jeszcze#endif

#ifndef H1_H#define H1_H

void fun() { }#endif

#include ”h2.h” void fun2() { fun(); }

#include ”h1.h”#include ”h2.h”int main() {

fun();}

g++ main.cc test.cc –o prog/tmp/cccOPU18.o: In function `fun()':test.cc:(.text+0x0): multiple definition of `fun()'/tmp/ccGypJJH.o:main.cc:(.text+0x0): first defined herecollect2: ld returned 1 exit status

plik h2.h plik main.ccplik test.ccplik h1.h

Typy danych – typy wbudowane (§18.3.1)

• standard nie określa z ilu bitów składa się dany typ• określa minimalną i maksymalną wartość danego typu

Powyższe wyrażenia to metody lub składowe statyczne,zwracające wyrażenie stałe (constexpr – nowość C++11), np.

• nagłówki <climits>, <cfloat> takie same jak standardowe nagłówki z C o nazwie limits.h, float.h z definicjami preprocesorowymi, np.

#include <limits> // jest tu szablon numeric_limitsnumeric_limits<double>::min(); // 2.22507e-308numeric_limits<int>::max(); // 2147483647numeric_limits<float>::round_error(); // 0.5numeric_limits<short>::is_specialized; // true

static constexpr T min() noexcept; // „noexcept” – nie zgłosi wyjątku

#define INT_MAX <#if expression >= 32,767>#define LONG_MAX <#if expression >= 2,147,483,647>#define LLONG_MAX <#if expression >= 9,223,372,036,854,775,807> [added with C99]

Typy danych oraz specyfikatory

Podstawowe typy wbudowane:

wchar_t – rozszerzony typ znakowy (wielkość zależna od implementacji)char16_t i char32_t – do reprezentacji znaków standardu UnicodeSpecyfikatory (rozszerzają lub zawężają, ze znakiem lub bez znaku)

• short int (inaczej: short), int, long int (inaczej: long), long long int(inaczej: long long) oficjalnie w C++11 ze wzg. na zgodność z C99

• float, double, long doubleTyp(dwa stany logiczne: true, false – to są stałe)

char, int, float, double

short – long, signed – unsigned

bool

• operatory: && || ! < > <= >= == !=• komendy sterujące: if, for, while, do, ? :• kompilator przekształca int w bool

true – odpowiednik wartości całkowitej 1 false - odpowiednik wartości całkowitej 0

nie nadawać stanu logicznego za pomocą operacji arytmetycznej (+ lub -)→ niejawna konwersja typów

typedef, using

typedef – synonim typu istniejącego (nie żadna nowa definicja), najczęściej używany do uproszczenia zapisu (wiele razy w bibliotece standardowej) np.

using – może być użyte zamiennie jako typedef

typedef basic_fstream<char> fstream; // w nagłówku fstreamtypedef basic_string<char> string; // w nagłówku string

typedef std::vector<int>::iterator It;using It = std::vector<int>::iterator; // te dwie linie robią to samo

typedef const char* (*Fptr)( double );using Fptr = const char* (*) (double); // wskaźnik do funkcji,

też to samo co wyżej

Typy danych – zakres, bajty, precyzja

* można zobaczyć wartość typu char po wykonaniu rzutowania na int, np. -128, 127Możliwość określenia typu „ze znakiem” (signed – domyślny) oraz „bez znaku” (unsigned) dotyczy tylko typów całkowitych. Typy zmiennoprzecinkowe są signed.

typ minimum maksimum bajty precyzja

bool false true 1 -

char (*) całe ASCII całe ASCII 1 -

short intunsigned

-32768 327672

-

0 65535

int, long intunsigned

-2147483648 21474836474

-

0 4294967295

long longunsigned

-9223372036854775808 92233720368547758078

-

0 18446744073709551615

float 1.17549e-38 3.40282e+38 4 7 cyfr

double 2.22507e-308 1.79769e+308 8 15 cyfr

long double 3.3621e-4932 1.18973e+4932 10 19 cyfr

Zasięg zmiennych

• zmienne można definiować w dowolnym miejscu• często tuż przed użyciem, w obrębie wyrażeń sterujących

(pętla for i while), instrukcja if, selektor switch

zmienne globalne• na zewnątrz ciał wszystkich funkcji, dostępne dla wszystkich części programu,

czas życia == czas życia programu, w wielu plikach, można je zadeklarować (extern), można też (static) ograniczyć do jednego pliku, inicjalizowane zerem

zmienne lokalne• w obrębie jakiegoś zasięgu (od definicji do klamry zamykającej dany blok),

zmienne tymczasowe (kiedyś określane jako auto) – giną po wyjściu z zasięgu• zmienne zagnieżdżone zasłaniają zmienne wyżej położone• bez inicjalizacji wartość zmiennej lokalnej nieokreślona (śmieci)

nie można odseparować definicji / inicjalizacji za pomocą nawiasów

while ( char c = cin.get() ) // ok, ale to zawsze będzie truewhile ( ( char c = cin.get() ) ) // błąd – nie można obłożyć tego nawiasamiwhile ( char c = cin.get() != ’q’ ) // działa, ale != ważniejsze niż = więc…while ( ( char c = cin.get() ) != ’q’ ) // no właśnie, tak nie można! więc…char c; while ( ( c = cin.get() ) != ’q’ ) // teraz działa

Zasięg zmiennych, przesłanianie – przykład

int a = 1; // zmienna globalnanamespace mojeKlocki

{ int a = 7; int b = 8; }namespace { int c = 99;

// int a = 3; spowodowałoby kolizję ze zmienną globalną}

int main() {int a = 2;{

int a = 3, c = 100;for (int i=0; i<10; ++i); // nic nie robi, bo uwaga - gdzie kończy się instrukcjacout << "a lokalne = "<< a <<endl; // 3using namespace mojeKlocki;cout << "a lokalne = "<< a <<endl; // 3cout << "a z mojeKlocki = "<< mojeKlocki::a <<endl; // 7cout << "b z mojeKlocki = "<< b <<endl; // 8int b = 12;cout << "b lokalne = "<< b <<endl; // 12cout << "a nielokalne = "<< ::a <<endl; // 1cout << "c z nienazwanej przestrzeni " << ::c << endl; // 99, to też jest zmienna globalna

}cout << "a lokalne = "<< a <<endl; // 2

}

zakomentowanie globalnej zmiennej i próbaodwołania się do niej spowoduje błąd kompilacji

Rodzaje obiektów i ich cechy (static – global)

Obiekt globalny – istnieje przez cały czas wykonania programu– domyślnie łączony zewnętrznie– deklaracja extern – można użyć w innych plikach źródłowych– deklaracją static zasięg można ograniczyć do pliku

wystąpienia definicji (bez kolizji nazw)– lepszy sposób na „łączenie wewnętrzne” – użycie

nienazwanej przestrzeni nazw (namespace)– jeśli const, to zachowuje się jak static (chyba że extern const)– domyślnie inicjowany wartością zera

Statyczny obiekt lokalny – istnieje przez cały czas wykonania programu– deklaracja z modyfikatorem static– wartość takiego obiektu przetrwa między kolejnymi

wywołaniami funkcji– zasięg ograniczony jest do bieżącego kontekstu– w klasie – jeden egzemplarz dla wszystkich obiektów klasy– domyślnie inicjowany wartością zera

pam

ięć

stat

yczn

a

Obiekty globalne i statyczne (globalne) – przykłady

// w przestrzeni nazw lub przestrzeni globalnejint i; // domyślnie łączenie zewnętrzneconst int ci = 0; // domyślnie globalny const jest static (łączony wewnętrznie)extern const int eci; // jawna deklaracja łączenia zewnętrznegostatic int si; // jawnie static

// podobnie funkcje – uwaga – nie ma globalnych funkcji stałych (const)int foo(); // domyślnie łączenie zewnętrznestatic int bar(); // jawna deklaracja static

// nienazwana przestrzeń nazw jako polecany sposób na ograniczenie zakresu// widzialności nazw do danej jednostki translacjinamespace {

int i; // mimo łączenia zewnętrznego niedostępne // w innych jednostkach translacji

class niewidoczna_dla_innych { };}

Rodzaje obiektów i ich cechy (stack, heap)

Obiekt automatyczny – obiekt lokalny– przydział pamięci następuje automatycznie w chwili

wywołania funkcji– czas trwania obiektu kończy się wraz z zakończeniem bloku,

w którym został zaalokowany– zasięg ograniczony jest do bieżącego kontekstu– należy uważać na wskaźniki i referencje do obiektów

lokalnych– obiekt domyślnie nie jest inicjalizowany

Obiekt z czasem trwania określanym przez programistę– obiekt z pamięcią przydzielaną dynamicznie (operator new)– czas życia – do usunięcia operatorem delete– obiekt bez nazwy– identyfikowany pośrednio przez wskaźnik– zawieszony wskaźnik - wskazujący na nieobsługiwany obszar

pamięci (wskaźnik zwisający)– wyciek pamięci - obszar pamięci przydzielany dynamicznie na

który nie wskazuje żaden wskaźnik

stos

st

erta

Stałe dosłowne (literały całkowite i zmiennoprzecinkowe)

Stałe dosłowne są niemodyfikowalne (tzw. r-values)Literały całkowite• liczby dziesiętne (domyślnie)• ósemkowe – zaczynają się od 0• szesnastkowe – zaczynają się od 0x lub 0X• binarne – zaczynają się od 0b lub 0B (C++14)

Literały zmiennoprzecinkowe• liczba zmiennoprzecinkowa (system dziesiętny)• opcjonalnie może być z wykładnikiem:

e lub E oraz „+” (opcjonalnie) lub „–” i liczba

dopuszczalne przyrostkif, F – floatl, L – long double

• literał zmiennoprzecinkowy bez przyrostka jest rozumiany jako typ double• przyrostkiem można określić typ, np.: 3.14f jest typu float

dopuszczalne przyrostkiu, U – unsignedl, L – longll, LL – long long

• liczbę 12 można więc zapisać jako: 12, 014, 0xC, 0b1100• literał całkowity bez przyrostka jest rozumiany jako typ int• przyrostkiem można określić typ, np.: 12uL jest typu unsigned long

Manipulatory i std::bitset

W nagłówku <ios> oraz wszystkich pochodnych (czyli <iostream> też) zdefiniowane są manipulatory std::dec, std::oct i std::hex (nie ma std::bin), dzięki którym można w locie wypisać konwersję do danego systemu:

A co z systemem binarnym? Można skorzystać z szablonu klasy (konieczny nagłówek <bitset>) reprezentującego bity o zadanym rozmiarze N, std::bitset<N> a następnie wykonać rzutowanie dowolnej liczby:

Udogodnienia czytelności zapisu liczb (w dowolnym systemie) w C++14:

cout << hex << 12; // c szesnastkowocout << oct << 12; // 14 ósemkowocout << dec << 0b1100 // 12 dziesiętnie

cout << bitset<8>(12); // 00001100 binarnie na 8 bitachcout << (bitset<16>)0xc; // 00000000001100 na 16 bitachcout << static_cast<bitset<4>>(014); // 1100 na 4 bitach

1'000'000; 0xa'a'c'f; 0'123'123; // separujące apostrofy ' są ignorowane

Stałe dosłowne (literały znakowe)

Literały znakowe• jeden znak (lub więcej) ujęty w pojedynczy cudzysłów, 'x', może być

poprzedzony przedrostkiem u, U, L, na przykład: u'x', lub U'y', lub L'z'• znaki specjalne (już były): \' \" \? \\ \a \b \f \n \r \t \v• znaki specjalne ósemkowe \ooo (jedna, dwie lub trzy cyfry)• znaki specjalne szesnastkowe \xNNN (nie ma limitu na liczbę cyfr N)

– koniec znaku ósemkowego lub szesnastkowego następuje po napotkaniu pierwszej liczby spoza systemu ósemkowego / heksadecymalnego

• typ wchar_t służy do obsługi rozszerzonego typu znakowego (bez wskazania na rodzaj kodowania), typ char16_t służy reprezentowaniu kodowania w UTF-16, typ char32_t kodowaniu w UTF-32 (C++11)

• zwykły literał znakowy zawierający jeden znak jest typu char oraz wartość zgodną z ASCII• literał wieloznakowy jest typu int, a jego wartość zależy od implementacji• literał z prefiksem u jest typu char16_t, z U – typu char32_t, z L – typu wchar_t

Wsparcie standardu unicode

Dwa nowe typy znakowe: char16_t i char32_t• char16_t dla UTF-16 // potomek uint_least16_t• char32_t dla UTF-32 // potomek uint_least32_t

– Odpowiadające im literały: przedrostek u / U

• Istnieją odpowiadające im łańcuchy znakowe– u"lancuch znakowy 16-tek" // składowe typu char16_t w UTF-16– U"lancuch znakowy 32-jek" // składowe typu char32_t w UTF-32– "zwykly lanuch znakowy" // składowe typu char– u8"wsparcie dla UTF-8" // składowe typu char w UTF-8

• Pojedyncze znaki osiągalne poprzez swoje kody \unnnn i \Unnnnnnnn:– u8"Klucz wiolinowy: \U0001D11E" // (tu powinien być klucz, ale czcionka…)– U"czaszka i kosci: \u2620" // ☠

• zwykły literał znakowy zawierający jeden znak jest typu char oraz wartość zgodną z ASCII• literał wieloznakowy jest typu int, a jego wartość zależy od implementacji• literał z prefiksem u jest typu char16_t, z U – typu char32_t, z L – typu wchar_t

Stałe dosłowne (literały napisowe i inne)

Literały napisowe (łańcuchy znakowe)• sekwencja znaków zamknięta w podwójny cudzysłów, "napis",

opcjonalnie poprzedzony przedrostkiem R, u8, u8R, u, uR, U, UR, L lub LR jak np. R" (…) ", u8"…", u8R"**(…)**", u"…", uR"*~(…)*~", U"…", UR"zzz(…)zzz", L"…", LR" (…) "

Literały logiczne• dwa słowa kluczowe, true i false, będące typu boolLiterały wskaźnikowe (C++11)• słowo kluczowe nullptr (jest typu std::nullptr_t, to nie jest typ int)

autonomiczny typ na określenie wskaźnika zerowego

f(0); // wywołane f(int)f(nullptr); // wywołane f(char*)g(nullptr); // błąd: nullptr to nie intint i = nullptr; // błąd: jak wyżej

char* ptr1 = nullptr;int *ptr2 = 0; //nadal działavoid f(int); // tu argument intvoid f(char*); // tu argument char*void g(int);

Stałe (const) a preprocesor

• za pomocą preprocesora

od miejsca zdefiniowana do końca pliku

• modyfikator const

zasięg taki jak zasięg zmiennej, typ musi być określony, stała musi być zainicjalizowana (chyba że piszemy deklarację z extern)

• stałej zdefiniowanej za pomocą preprocesora nie można śledzić – bo polega na zamianie jednego symbolu na np. podaną wartość, zdecydowanie definiujmy stałe jako zmienne danego typu

• preprocesor można czasem użyć jako sprytnej makrodefinicji, np. wypisywania kontrolnego zmiennych (za Bruce Eckelem):

wtedy gdzieś w kodzie: PRINT("wartosc", a );

#define PI 3.1415

const float pi = 3.1415;

#define PRINT (STR, VAR) cout << STR " = " << VAR << endl#define PR (x) cout << #x " = " << x << " \n "

constexpr – gwarantowane wyrażenie stałe (C++11)

constexpr – uogólnione i gwarantowane wyrażenie stałe (nowe słowo kluczowe w C++11) – funkcje, konstruktory klas, także zmienne• pozwala na budowanie wyrażenia stałego przy użyciu typów

zdefiniowanych przez użytkownika• sposób gwarantujący, że inicjalizacja zachodzi podczas kompilacji• Zmienne (muszą być zainicjalizowane ale tylko literałem, inną wartością

constexpr lub wartością zwracaną przez funkcję constexpr)

• constexpr implikuje const ale nie na odwrótint a = 3; const int b = a; // a nie jest wyrażeniem stałymconstexpr int c = b; // błądconst int d = 7; // jest wyrażeniem stałymconstexpr int f = d; // ok

constexpr auto liczba = 3;constexpr auto pi = 3.14;constexpr auto nazwa = "stalowyrazeniowy ciag";

constexpr – funkcje

• funkcja constexpr musi mieć (C++11) jedną instrukcję return (często używa się operatora trójargumentowego do sprawdzenia warunków), w C++14 poluzowano warunki, może być więcej return, lokalne zmienne, if, switch, for, while, do-while (g++ od wersji 5.0)

• standard nie gwarantuje ewaluacji funkcji constexpr podczas kompilacji, chyba że występuje ona jako wyrażenie stałe (np. przypisanie do const)

• dozwolone wyrażenia ewaluowane podczas kompilacji:using, typedef, static_assert

• można wywoływać rekurencyjnie

• można użyć do wyliczenia wartości pól typu enum

• dramatyczna różnica w czasie obliczenia wartości funkcji constexprpodczas kompilacji i podczas wykonania

constexpr long long fibonacci( int n ) {return n < 1 ? -1 : ( n==1 || n==2 ? 1 : fibonacci(n-1) + fibonacci(n-2) );

}

enum Fibonacci {drugi = fibonacci(2);czternasty = fibonacci(14);

};

Można też obliczyć „w locie” (strumień):cout << fibonacci(21) << endl;ale może się okazać, że wtedy kompilator wybierzewariant obliczania podczas wykonania programu.

Operatory

• zwracają wartości na podstawie argumentów (argumentu)• 18 poziomów ważności – nie uczyć się wszystkiego! raczej używać

nawiasów ( ) do czytelnego oddzielenia; niektóre zapamiętać• operatory =, ++, -- dodatkowo zmieniają wartość argumentu

(skutek uboczny, ang. side effect)• operator przypisania = kopiuje p-wartość do l-wartości• operatory matematyczne +, -, *, /, %

• można połączyć z operatorem przypisania +=, -=, *=, /=, %=• zatem np. b %= 4; równoważne jest b = b % 4;• operator % (modulo) tylko z liczbami typu całkowitego

• operatory relacji <, >, <=, >=, ==, != zwracają wartość logiczną• operatory logiczne && (iloczyn), || (suma)• operatory bitowe & (koniunkcja), | (alternatywa), ^ (różnica

symetryczna), ~ (bitowy operator negacji)

Operatory – ciąg dalszy

• operatory przesunięć <<, >>jeśli po lewej liczba ze znakiem, to przesunięcie >> nie musi być operacją logiczną• można łączyć z operatorem przypisania <<=, >>=• bity przesunięte poza granicę są tracone

• operatory jednoargumentowe ! (negacji logicznej), -, +• operatory adresu &, wyłuskania *, -> i rzutowania

• rzutowanie: float a = 3.14; int b = (int)a; albo int b = int(a);

• operatory alokacji i usuwania: new, delete• operator trójargumentowy ? :

co się stanie:

• operator , zwraca wartość ostatniego z wyrażeń• operator sizeof

int a = --b ? b : (b = -10); // jeśli b=1, to a=-10

Operatory – tabela ważności

Level Operator Description Grouping

1 :: scope Left-to-right

2

() [] . -> ++ --dynamic_cast static_cast reinterpret_cast const_cast typeid

postfix Left-to-right

3

++ -- ~ ! sizeof new delete unary (prefix)

Right-to-left* & indirection and reference (pointers)

+ - unary sign operator

4 (type) type casting Right-to-left

5 .* ->* pointer-to-member Left-to-right

6 * / % multiplicative Left-to-right

7 + - additive Left-to-right

8 << >> shift Left-to-right

9 < > <= >= relational Left-to-right

10 == != equality Left-to-right

11 & bitwise AND Left-to-right

12 ^ bitwise XOR Left-to-right

13 | bitwise OR Left-to-right

14 && logical AND Left-to-right

15 || logical OR Left-to-right

16 ?: conditional Right-to-left

17 = *= /= %= += -= >>= <<= &= ̂ = |= assignment Right-to-left

18 , comma Left-to-right

Operatory – rzutowanie

• static_cast (konwersje niejawne, zawężające, zmieniające typ – podczas kompilowania) int b = static_cast<int>(a);void *vp; int *num = static_cast<int*>(vp);

• const_cast (od typów z modyfikatowem const lub volatiledo takich samych typów bez modyfikatora lub w drugą stronę)

• reinterpret_cast (pełna odpowiedzialność użytkownika, bez kontroli)

• dynamic_cast (rzutowanie "w dół" od abstrakcyjnego typu ogólnego do typu pochodnego – zajdzie gdy operacja taka ma sens – podczas wykonywania programu)

Referencje – lewe ( T &, const T & )

Terminologia wprowadzająca

Referencja ( T & ) „zwykła” to jakby „przezwisko” na coś.„Przezwisko” nie może istnieć samo, bez powiązania z tym, co określa. Zatem referencja w momencie definicji musi być zainicjalizowana i nie może być przestawiona na coś innego.

Niestała referencja ( T & ) może wskazywać na l-wartość. Stała referencja ( const T & lub T const & ) może wskazywać na l-warość i p-wartość. W roli p-wartości może wystąpić obiekt, który nie musi być stały, jak i obiekt, którego nie wolno modyfikować (np. tymczasowy). Do tej pory nie można było rozróżnić, na co pokazuje stała referencja.

l-value (lewa-wartość, l-wartość) coś, co można zmodyfikować, np. poprzez przypisanie (stoi po lewej stronie = )r-value (prawa-wartość, p-wartość) coś, co stoi po prawej stronie operacji przypisania, często rozumiana jako niemodyfikowalne

Nie istnieją:• referencje do referencji• tablice referencji• wskaźniki do referencji

Referencje – zakazane cv, prawe ( T && ) (C++11)

Kwalifikatory cv dla referencji, są niedopuszczalne. Wprowadzone przez typedef, albo argument szablonu, są zignorowane.

Przykład

C++11 wprowadza referencję „p-wartości” ( && ), która ma służyć wskazywaniu na p-wartości, ale w rozumieniu takim, że można je modyfikować. Służyć to ma budowaniu semantyki (składni) „przenoszenia”. Pojawiają się dzięki temu „konstruktory przenoszące” (move constructors) i „przenoszące operatory przypisania” (moveassignment operator). Więcej o tym – w dalszej części wykładu.

pamiętajmy, że w c++ funkcjonuje pojęcie kwalifikatora cv, czyli const i/lub volatile, zatem to co piszemy o const, dotyczy też volatile

Nie istnieje:T & const

int a = 3;typedef int& RINT;const RINT aref = a;aref = 4; // teraz ma wartość 4

wbrew pozorom, aref jest referencją „l-wartości” do int, a nie do const intnapisanie const RINT tu oznacza nie const int&a próbę int& const – coś takiego jest ignorowane

albo innymi słowy: referencja musi być zadeklarowana z const, potem tego const nie można dołożyć na zasadzie zmiany typu deklarowanej referencji

glvalues, prvalues, xvalues… ( C++11 )

Kategoryzacja typów wyrażeń w nowym standardzie

generalized lvalue

pure rvalue

expiring value

obiekt bliski końca czasu swojego życiajego zasoby mogą być przeniesionema swoją tożsamość• funkcja zwracająca T&&• rzutowanie na T&&• wyrażenie zwracające lvalue (np. throw)

Dwie cechy obiektów• mają (nie mają) tożsamość(i) – adres, nazwę…• mogą (nie mogą) być przenoszone – oryginał w stanie „wyzerowanym”

jego zasoby mogą być przeniesionenie ma swojej tożsamości• funkcja nie zwracająca T&&• rzutowanie nie na T&&• literały 12, 7.3e5, true• musi być ustalonego typu

lewe-wartości, prawe-wartości ( C++11 )

Jak odróżnić l-wartości od p-wartości?l-wartości: mają nazwę, mają adres, który możemy użyć

p-wartości: nie mają nazwy, zwykle obiekty tymczasowe

this – mimo, że ma nazwę, jest definiowany jako p-wartośćliterały – też są p-wartościami, ale nie posiadają operacji przenoszeniaReferencje do l-wartości ( T& ) i p-wartości ( T&& ):• l-wartości mogą być pokazane przez T&• p-wartości mogą być pokazane przez T const&

(stają się l-wartościami, bo referencja to też nazwa… ma nazwę? jest więc…)• p-wartości mogą być pokazane przez T&& (C++11)• l-wartości nie mogą być pokazane przez T&& (C++11)

size_t fun( string s ); // to co zwraca fun jest p-wartościąfun( ”word” ); // tymczasowy string inicjalizowany ”word” jest p-wartością

int i; // i jest l-wartościąint& fun(); fun() = 7; // fun() też jest l-wartością

Wskaźniki

Wskaźniki – zawierają adres i informację o typie (wyjątek: void*)T* – zwykły wskaźnik (do typu T)const T*, T const* – wskaźnik do stałego obiektu („gwarancja nietykalności”)

T* const – wskaźnik stały („gwarancja nieprzesuwalności”)

const T* const, T const* const – stały wskaźnik do stałego obiektuPonownie uwaga na typedef:

typedef int* pointer;typedef const pointer const_pointer;

const_pointer jest typu int* const,a nie typu const int*

const int ci = 10, *pc = &ci, *const cpc = pc, **ppc;int i, *p, *const cp = &i;

pc – wskaźnik na stały int, cpc – stały wskaźnik na stały int, ppc – wskaźnik do wskaźnika na stały int, p – wskaźnik na int, cp – stały wskaźnik na int

Wskaźniki – własności i arytmetyka

• wskaźnik jak tablica

można nimi operować jakby były tablicą, vInt[2] to samo co n[2]

• operacje ++ lub - -– są one inteligentne, tzn. na podstawie typu wskaźnika kompilator

wie o ile bajtów ma przeskoczyć• operacje + lub – ograniczone

– można dodawać lub odejmować liczby całkowite (operacja inteligentna tzn. z wykorzystaniem wiedzy na temat wskazywanego typu)

– nie można dodawać dwóch wskaźników– można odjąć dwa wskaźniki – wynikiem jest liczba elementów danego typu

znajdujących się pomiędzy nimi:

int *vInt = n; // wcześniej int n[10];vInt = &n[0]; // to samo

vInt – to adres początku tablicy (pierwszego jej elementu)vInt + 1 – to adres drugiego elementu tablicy*(vInt + 2) – to zawartość wskazywana pod adresem vInt + 2

* tu jakooperatorwyłuskaniazmiennejze wskaźnika

int tab[] = { 1, 2, 5, 7 }; int *p1 = tab; int *p2 = &tab[3];cout << p2 – p1 << endl; // 3

Wskaźniki – przykłady

• można dokonać zmian…

• nie wszystkie zmiany możliwe…nie można usunąć przydomka const z żadnego obiektu (można tylko rzutować)

double f1 = 0.;const double pi = 3.14;double *vZmienna = &f1;const double *vStala1 = &pi;const double *vStala2; // wskaźnik do stałego obiektu, jeszcze nie ustawionyvStala2 = vZmienna;*vZmienna = 25.;double * const vStalyZmienna = const_cast<double * const>( vStala1 );vZmienna = vStalyZmienna;

T & * - takie coś nie istnieje!

przydaje się jako argument funkcji,wtedy wskaźnik – argument, możnawewnątrz funkcji przestawić na inny adres

T * & - referencja do wskaźnika na typ T

Typy złożone w c++ (litania)

Poprzez złożone typy w języku c++ rozumie się:• tablice obiektów danego typu• funkcje, mające parametry danego typu, a zwracające void lub referencje

lub obiekty danego typu• wskaźniki do void lub obiektów, lub funkcji danego typu (włączając w to

statyczne składniki klasy)• referencje do obiektów lub funkcji (tzw. referencje lewej wartości i

referencje prawej wartości)• klasy, zawierające obiekty różnych typów oraz metody składowe, wraz z

odpowiednimi ograniczeniami dostępu• unie, które są rodzajem klasy, mogącej zawierać obiekt różnych typów, w

różnych chwilach czasu• typy wyliczeniowe, zawierające listę nazwanych stałych wartości• wskaźniki do niestatycznych składowych klasy

enum – typ wyliczeniowy „konwencjonalny”

enum – autonomiczny typ wyliczeniowy

poważne mankamenty• możliwa niejawna konwersja z enum do int (może prowadzić

do błędów, jeśli ktoś takiej konwersji nie chce)

• „wyciekanie” identyfikatorów do zewnętrznego zakresu względem miejsca zdefiniowania typu wyliczeniowego (np. enum zdefiniowany w przestrzeni globalnej eksportuje nazwy wszędzie… kolizja nazw)

• nie można określić typu, na jakim zbudowane sa identyfikatory• niemożliwa jest uprzedzająca deklaracja typu wyliczeniowegonienazwany enum – ma sens właśnie przez to, że jego identyfikatory (z listy wyliczeniowej) są widziane na zewnątrz jako stałe (całkowite):

enum EPozycja {eAsystent, // 0eAdiunkt, // 1eProfesor // 2

};

definiowanie zmiennych podobnie jak dla typu wbudowanego:

EPozycja pracownik = eAsystent;

można też zadać wartośćenum EPozycja {

eAsystent = 5,eAdiunkt = eAsystent + 2,eProfesor

}; nie można robić inkrementacji: pracownik++;

int a = eAsystent; // ok, konwersja!pracownik = 3; // bez rzutowania to jest błąd

enum { jeden = 1, dwa = 2, cztery = 4 };

sizeof( EPozycja ) = ?… pewnie 4 ale… może być mniej

enum – silny typ wyliczeniowy (C++11)

• nazwy z listy wyliczeniowej nie wyciekają na zewnątrz• nie następuje niejawna automatyczna konwersja na int

• można (opcjonalnie) zdefiniować typ (musi być całkowity), na którym zbudwany jest nowy enum (domyślnie – int) i dzięki temu kontrolować wielkość

• możliwa jest deklaracja wyprzedzająca

enum class nazwa { lista identyfikatorów };

enum Alert { green, yellow, election, red }; // standardowy, stary typ wyliczeniowyenum class Color { red, blue }; // nowy, silny, identyfikatory nieznane na zewnątrzenum struct TrafficLight { red, yellow, green }; // jak widać, nie koliduje z niczymAlert a = 7; // błąd: zwykły przypadek, nie ma konwersji z int na enumColor c = 7; // błąd: nie ma konwersji int->Colorint a2 = red; // ok: możliwa konwersja Alert::red->intint a3 = Alert::red; // błąd w C++98, ok w C++11int a4 = blue; // błąd: blue nieznane w tym zakresieint a5 = Color::blue; // błąd: brak konwersji Color->intColor a6 = Color::blue; // ok

enum class Color : char; // deklaracjavoid foo(Color* p); // teraz można już użyć

zamiast class może być struct

enum class Color : char { red, blue }; // sizeof( Color ) taki sam jak sizeof( char )

Tablice

tablica - sekwencyjny zbiór zmiennej danego typu (nie void)• dopuszczalne są typy podstawowe, wskaźniki, wskaźniki do

składowych, klasy, typy wyliczeniowe i inne tablice (z nich konstruuje się tablice wielowymiarowe)

• tablica wielkości N posiada ciągły zbiór niepustych elementów, numerowanych od 0 do N-1

• deklaracje, definicje:

typ nazwa[ stała ( lub constexpr )opcj ];…można utworzyć tablicę wskaźników do void:void * tablica[10]; // sizeof(tablica) równy 10*4

int tabl1[5], *tabl2[4]; // tablica int-ów oraz tablica wskaźników do intMojaKlasa tabl3[2];typedef float A[5];typedef const A CA; // CA – typ: tablica 5-elementowa const float

extern int tab[4]; // można też: extern int tab[]; // wtedy w definicji trzeba rozmiar podać

int tab[]; // ok, bo w deklaracji podano rozmiarclass Klasa { static int tab2[4]; /* static, więc to tylko deklaracja */ };int Klasa::tab2[]; // tu definicja – ok, bo rozmiar podano w deklaracji

Tablice – cechy, inicjalizacja

• dostęp do dowolnej pozycji bardzo szybki • wielkość (statycznej) tablicy trzeba z góry zdefiniować• z nazwy tablicy nie wiemy nic o jej wielkości• elementy tablicy są przechowywane w pamięci jeden za drugim• nazwa tablicy: adres (stały) początku tablicy• inicjalizacja:

• wyzerować tablicę można też za pomocą funkcji std::memsetznajdującą się w nagłówku <cstring>

char literki[100] = { 'a', 'b', 'c' }; // reszta zeramistd::string slowa[] = { "Windows", "Linux" };const int stale[4] = { 1, 2, 3, 4 }; // inicjalizacja konieczna

void *memset(void *s, int c, size_t n); // s – adres, c – wartość, n – rozmiarmemset ( literki, 0, sizeof(literki) ); // można jako drugi arg podać też char

std::array – tablica na miarę naszych czasów (C++11)

• łączy w sobie szybkość zwykłej C-tablicy z zaletami bycia kontenerem standardowym, czyli np. „wie jaki ma rozmiar”

• zawiera w sobie agregat; potrzebny nagłówek <array>• wielkość i przetrzymywany typ trzeba z góry określić

• można używać jak tablicę, albo odpytać daną pozycję metodą at(n), można zapytać o pierwszy – front() i ostatni – back() element

• metody empty() – true gdy pusta czyli… zrobiona tak: array<int, 0> a;• size() – rozmiar tablicy, max_size() – hipotetyczny maksymalny rozmiar• fill( const T& val ) – wypełnienie wszystkich elementów wartością val

array<int, 3> a = { 1, 3, 7 }; // znak = opcjonalny, ale…array<string, 2> b { { string("Windows"), "Linux" } }; // powyższe zagnieżdżenie to inicjalizacja wewnętrznego agregatu// ten zapis nie jest przejawem „uniwersalnej inicjalizacji” poprzez// initializer_list<T> ponieważ array nie ma napisanego konstruktora

Tablice wielowymiarowe – „tablice tablic”

• przy sekwencyjnym przechodzeniu przez tablicę, najszybciej zmienia się najbardziej skrajny prawy indeks

• na co pokazuje macierz[1] – na tablicę 3-elementową typu int• wyliczenie indeksu (offsetu) dla tablicy 2-wymiarowej ( 2 × 3 ):

offset (element i, j) = i ⋅ 3 + j // wyliczanie w porządku wierszy!czyli niepotrzebna znajomość najbardziej lewego indeksu (tutaj 2)

• dla tablicy 3-wymiarowej o rozmiarach D × C × R ( tab[D][C][R] )offset (element i, j, k) = ( i ⋅ C + j ) ⋅ R + k

• dla tablicy 4-wymiarowej, np. tab[H][D][C][R]offset (element i, j, k, l) = ( (i ⋅ D + j ) ⋅ C + k ) ⋅ R + l

int macierz[2][3] = { 1, 2, 3, 4, 5, 6 }; // ostrzeżenie kompilatora, // brace elision (opuszczenie nawiasów) nie jest tu wskazaneint macierz[2][3] = { { 1, 2, 3 }, { 4, 5, 6 } };int A[2][2][2] = { { {1,2}, {3,4} }, { {5,6}, {7,8} } };int matrix[4][5] = { 0 }; // szybka inicjalizacja zerami

std::array wielowymiarowe niespodzianki

• w zwykłej tablicy, ponieważ najszybciej rotuje prawy indeks, więc myślimy o prawym indeksie (rozmiar 3) jak o kolejnych pozycjach (kolumnach) w wierszu oznaczonym indeksem lewym (rozmiar 2)

• w tablicy zrobionej na bazie array jest odwrotnie! Najszybciej rotujący indeks to ten najbardziej zagnieżdżony, w zapisie matrix[i][j] lewy indeks; zatem poniżej jest odpowiednik int macierz[3][2];

• używając at() można sprawdzić czy wychodzi się poza zakres:matrix.at(0) = 1; // oczywiście źle, bo to jest próba przypisania// int do const std::array<int, 2ul>… poprawna składnia:matrix.at(0).at(0) = 1; // idźmy z prawym indeksem dalejmatrix.at(0).at(2) = 1; // terminate called after throwing // an instance of 'std::out_of_range' what(): array::at// czyli wyszliśmy poza zakres, bo prawy indeks dotyczy tej// zagnieżdżonej tablicy, zatem odwrotnie niż w statycznej tablicy

int macierz[2][3]; // 2 wiersze, 3 kolumny, 2 × 3

array< array< int, 2>, 3> matrix; // 3 wiersze, 2 kolumny, 3 × 2

std::array jednak nie takie samo jak C-array

• krytykowaną własnością std::array jest brak dedukcji rozmiaru z wielkości listy inicjalizującej, niemożliwy jest zapis:

• obejściem tego problemu jest napisanie, za pomocą szablonów o zmiennej liczbie parametrów funkcji tworzącej, co wykracza za bardzo poza ten wykład, ale oto przykładowe rozwiązanie:

template <typename ...Args> struct all_same { static const bool value = false; };template <typename S, typename T, typename ...Args> struct all_same<S, T, Args...> {static const bool value =

std::is_same<typename std::decay<S>::type, typename std::decay<T>::type>::value && all_same<T, Args...>::value;};template <typename S, typename T> struct all_same<S, T> {static const bool value =

std::is_same<typename std::decay<S>::type, typename std::decay<T>::type>::value;};template <typename T> struct all_same<T> { static const bool value = true; };

template <typename T, typename ...Args>typename std::enable_if<all_same<T, Args...>::value, std::array<T, sizeof...(Args) + 1>>::typemake_array(T && t, Args &&... args) {return std::array<T, sizeof...(Args) + 1> { std::forward<T>(t), std::forward<Args>(args)...};

}

array<int> n = { 1, 2, 3 }; // musimy podać rozmiar: array<int,3>

Tablice wielowymiarowe – czytanie typów

int tablica[2][3][4];

Jakiego typu jest:

tablica – int (*)[3][4]*tablica – int (*)[4]tablica[0] – int (*)[4]*tablica[0] – int*&tablica[0] – int (*)[3][4]tablica[0][0] – int**tablica[0][0] – int&tablica[0][0] – int (*)[4]tablica[0][0][0] – int&tablica[0][0][0] – int*

int *s[4];s – typu int** – tablica czteroelementowa wskaźników do int

int (*p)[3];p – typu int (*)[3] – wskaźnik do trójelementowej tablicy int

int mac[][3] = { {1,2,3},{4,5,6},{7,8,9} };p = &mac[1];cout << *(*(p+1)-4) << endl;

Skoro p jest „wskaźnikiem do trójelementowej tablicy int”, więc jednostką dla niego jest taka właśnie tablica: p+1 oznacza przejście o wielkość int[3] do przodu, natomiast *(p+1) jest typu int* więc operacja (*(p+1)-4) oznacza przejście wskaźnika o cztery int wstecz, by na koniec wyłuskać wartość *(*(p+1)-4), czyli 3

new i delete – tworzenie i niszczenie

malloc() oraz free() – funkcje biblioteczne, poza kontrolą kompilatora

tworzenie obiektu w C++• przydzielenie pamięci

• pamięć statyczna• stos (pamiętamy o czasie życia obiektów, ograniczonym

zakresem ich ważności)• sterta (dynamiczny przydział, czas życia dowolny, nie

ograniczony zasięgiem)• wywołanie konstruktora (specjalnej funkcji)operator new

operator delete

int *pN = new int(10); // wskaźnik pokazuje na 10double *pD = new double[*pN]; // tablica

delete pN;delete [] pD;

Stosowanie zwykłych wskaźników wymaga dużej dyscypliny w kontrolowaniu „kto odpowiada za dany zasób”, pilnowania, żeby nie utracić kontaktu z zasobem oraz usuwania go w odpowiednim momencie.

new i delete – tablice

dynamiczne tablice wielowymiarowe

dwuwymiarowa … to samo:

usunięcie jest operacją odwrotną

int (*pTabl)[3][4] = new int[zmienna][3][4]; // nawiasy ( ) bo priorytety operatorów!

int **pTablica;pTablica = new int*[5];for (int i = 0; i < 5; i++)

pTablica[i] = new int[7];

for (int i = 0; i < 5; i++)delete [] pTablica[i];

delete [] pTablica;

zwykły operator new działa w dwóch etapach, najpierw alokuje potrzebną ilość pamięci, następnie tworzy w niej obiekt i dopiero na koniec zwraca wskaźnik (odpowiedniego typu) do utworzonego obiektu

pamiętajmy, że istnieją dwie wersje operatora delete, jedna zwykła, a druga tzw. tablicowa delete []

int ** pTablica( new int*[5] );for ( size_t i(0); i != 5; ++i ) {

pTablica[i] = new int[7];}

operator new (sytuacja krytyczna)

• gdy zaalokowanie odpowiedniej ilości pamięci na stercie się nie powiedzie, wywołana zostaje funkcja obsługi operatora new (new handler)

• domyślna wersja funkcji zgłasza wyjątek typu bad_alloc• możemy zmienić sposób reagowania na brak pamięci, np. dodać informację

o okolicznościach wyczerpania się zasobów

#include <iostream>#include <cstdlib>#include <new>using namespace std;

static long i; // zmienne statyczne inicjalizowane przez 0void informacja_krytyczna(); // deklaracja wyprzedzająca

int main() {// podajemy adres funkcji która ma być wywołana w krytycznej sytuacjiset_new_handler(informacja_krytyczna);for (i; ; i++) new int[1000000]; // nieskończona pętla aż do wyczerpania

}void informacja_krytyczna() {cout << "Pamiec wyczerpana po: << i << " alokacjach.\n"; exit(1);// np. exit(42) zwraca kod wykonania programu równy 42

}

Funkcje – argumenty, wartości zwracane

• funkcja to podprogram• funkcję identyfikuje jej nazwa, trzeba ją zadeklarować – wyjątek to funkcja main• definicja funkcji jest deklaracją, niemniej

starajmy się deklarować wszystkie funkcje• deklarację funkcji można zagnieździć w innej funkcji, ale definicji funkcji nie

można zagnieżdżać w innej funkcji (nawet w main)• funkcja może przyjmować dowolne parametry i zwracać dany typ lub nic nie

zwracać (wtedy piszemy void)

• nigdy nie zwracamy adresu (referencji) do obiektu lokalnego(czas jego życia się skończył…)

• main zwraca zawsze int – z przyczyn historycznych nie musimy wołać komendy return, kompilator nie napotkawszy jej wstawia na koniec bloku tej funkcji return 0;

void fun(); // nic nie zwraca, ale można wewnątrz funkcji napisać // pustą instrukcję wyjścia return;

int fun(string, int); // deklaracja nie wymaga podania nazw zmiennych, // ale dla czytelności kodu warto je pisać

auto fun( double ) -> double; // nowa notacja C++11 ( -> trailing return type )auto fun( char ); // możliwe w C++14 ale wtedy przed wywołaniem funkcja // musi być zdefiniowana, sama deklaracja nie wystarczy bo nieznany jest typ zwracany

Funkcje – sposoby przekazania parametrów

• sposoby przekazywania parametrów do funkcjivoid fun(float f); // przez wartość, do wnętrza funkcji tworzona jest kopia// obiektu f, więc oryginału nie można zmienić (uszkodzić)void fun(const float f); // to nie ma sensu, tworzona jest kopia// i nawet tej kopii nie da się zmienić, czytelniej więc byłoby // jako argument używać float f, a w pierwsze linii funkcji np.// const float& argf = f;void fun(float& f); // przez referencję (adres), można // modyfikować obiekt podawany jako parametrvoid fun(const float& f); // przez referencję do stałego obiektu,// optymalny sposób! – nie jest tworzona kopia, a argument jest// chroniony przed zmianąvoid fun(float&& f); // przez referencję do prawej wartości, większy sens// ma dla typów złożonych, które umożliwiają operacje przenoszeniavoid fun(const float&& f); // zwykle bez sensu, bo blokuje przenoszenievoid fun(float* f); // przez wskaźnik, można modyfikowaćvoid fun(const float* f); // wskaźnik do stałego obiektu, nie można modyfikować

Funkcje – wywołanie a parametry

• jaka jest różnica pomiędzy parametrem "przez referencję" i "przez wskaźnik"? Sposób wywołania funkcji:

• na temat dedukcji typu zwracanego przez funkcję:

auto f(); // zwracany typ nieznanyauto f() { return 5; } // zwracany typ intauto f(); // redeklaracja – okint f(); // błąd – traktowane jako deklaracja inne funkcjiauto f() { return f(); } // błąd, dopóki typ zwracany jest nieznany,

// nie można wołać rekurencyjnieauto suma(int i) {

if (i==1) return i; // zwracany typ teraz znanyelse return suma(i-1) + i; // można więc dalej wołać rekurencyjnie

}// taka funkcja może mieć wiele instrukcji return ale każda zwracająca taki sam typ

float mojaLiczba = 0.;fun(mojaLiczba); // przez referencję, tak samo jak przez wartośćfun(&mojaLiczba); // przez wskaźnik, trzeba podać adres obiektu za pomocą &

Funkcje – tablice argumentami, inline

• tablice jako argumenty funkcji nie są przekazywane przez wartość

• funkcje inline (krótkie, w celu szybkiego wywoływania)• treść rozwijana w miejscu ich wystąpienia, o ile nie jest zbyt skomplikowana• dla zwykłej funkcji: deklaracja (bez specyfikatora) w nagłówku

definicja w plku źródłowym poprzedzona specyfikatorem inline

• podobnie dla metody składowej (tylko definicja ze słowem inline)• wszystkie funkcje zdefiniowane wewnątrz klas są automatycznie inline• jeśli pobierany jest adres funkcji – nie następuje rozwinięcie

(w szczególności w procesie „debugowania” – krokowego śledzenia działania programu)

void func1(int a[], int rozmiar); // musimy podać rozmiarvoid func2(int *a, int rozmiar); // array-to-pointer decayvoid func3(int (&a) [10]); // tylko 10-elementowa tablicavoid func4(int macierz[][3], int rozmiar);

void fun();

inline void fun() { /* definicja */ }

Funkcje – wartości domyślne

argumenty domniemane (od prawej do lewej) tylko w deklaracji

• deklaracja argumentu domyślnego tylko raz(w danym zakresie ważności)• w deklaracji funkcji – deklaracje można powtarzać, ale

nie z powtórzonymi w nich wartościami domyślnymivoid fun(int a); // w deklaracjach można zmieniać

// nazwy zmiennych, tylko po co…void fun(int a = 5); // tak jest dobrze

• w definicji funkcji jeśli ta jest jednocześnie jej deklaracją• obiekty lokalne nie mogą być wartościami domyślnymi• w nowym (lokalnym) zakresie ważności możliwa jest deklaracja

z innymi wartościami domyślnymi – nie jest to dobra praktyka!

void fun(int a, void*, float = 3.14, char znak= '\0');

Funkcje – wartości domyślne, przykłady

void g(int = 0, ...); // ok, bo … (wielokropek) to nie argument, tylko ich listavoid f(int, int);void f(int, int = 7); // powtórzenie deklaracji z dodaną wartością domyślnąvoid h() {

f(3); // OK, woła f(3, 7)void f(int = 1, int); // błąd: niezależne od wartości domyślnych deklaracji

// z innego – zewnętrznego – zasięgu }void m() {

void f(int, int); // nie ma wartości domyślnychf(4); // błąd: niepoprawna liczba argumentówvoid f(int, int = 5); // OKf(4); // OK, woła f(4, 5);void f(int, int = 5); // błąd: nie można redeklarować, nawet

// z taką samą wartością domyślną}void n() {

f(6); // OK, woła f(6, 7)}

Funkcje – dowolna liczba argumentów

int suma ( int liczba, … ) {va_list ap; // utworzenie zmiennej typu va_list (variable argument list)va_start( ap, liczba ); // ustawienie ap na pierwszy, jawnie podany, argumentint sum = 0;for (int i = 0; i < liczba; ++i ) {

sum += va_arg( ap, int ); // odczyt kolejnej zmiennej, sami określamy jej typ!}va_end( ap ); // porządkowanie stosu, ustawienie ap na 0return sum;

}int main() {

cout << sum(3, 1, 1, 1, 1, 1) << endl; // OK, możemy mniej liczyć, 3cout << sum(8, 1, 1, 1, 1, 1, 1) << endl; // śmieci, wyszliśmy poza listę

}

… - wielokropek umożliwia napisanie funkcji przyjmującej dowolną liczbę argumentów• Przynajmniej jeden (pierwszy) argument takiej funkcji musi być podany jawnie.• Obsługa (odczyt) takich argumentów za pomocą makr, pochodzących z języka C.• Konieczne włączenie nagłówka <cstdarg> ( lub stdarg.h )

Wady: argumenty poza kontrolą typów. Popularne przykłady z biblioteki: printf, sprintf

Funkcje – argumenty funkcji main

int main(int argc, char* argv[]) { // …to samo:

int main(int argc, char** argv) { // …

• argc – liczba argumentówpierwszym zawsze jest ścieżka i nazwa programuargv[0] – zapisana w pierwszej pozycji tej tablicy

• kolejne argumenty można konwertowaćpo włączeniu nagłówka #include <cstdlib>za pomocą funkcji: atoi(), atol(), atof()

• możemy wykorzystać obiekt klasy istringstream– klasa ta dziedziczy po klasie istream, ta zaś dziedziczy po klasie ios, zaś ta

po klasie ios_base– oznacza to, że obiekt ten "ma w sobie" wszystkie funkcje zdefiniowane

w powyższych klasach– ponadto ma zdefiniowaną własną funkcję:

void str(const string& tekst) const;string str() const;

Funkcje – odczyt argumentów funkcji main

#include <iostream>#include <sstream>using namespace std;

// wywołajmy program na przykład ze zmiennymi: Rok 2010

int main(int argc, char* argv[]) {istringstream ss(argv[1]); // tu pierwszy argument w konstruktorzestring slowo;ss >> slowo;// w tym momencie pozycja czytania strumienia doszła do jego końcaif (ss.eof()) {

// jeśli tak, to musimy "przywrócić" strumień do czytania od początkuss.clear( ss.rdstate() & ~ios::eofbit );

}// poddajemy nowy strumień obiektowi istringstream, czyli drugi argumentss.str(argv[2]); // tu drugi argument za pomocą funkcji strint rok;ss >> rok;cout << "Argument 1: "<< slowo << " Argument 2: "<< rok << endl;

}

Pomiędzy innymi typami a „łańcuchem znakowym”

#include <string>// konwertuje zmienną typu int na łańcuch znakowy std::stringstd::string to_string( int value );// taki sam, gdy działało sprintf o odpowiednio dużym buforzestd::sprintf(buf, "%d", value); // podobnie pozostałe:std::string to_string( long value );std::string to_string( long long value );std::string to_string( unsigned value );std::string to_string( unsigned long value );std::string to_string( unsigned long long value );std::string to_string( float value );std::string to_string( double value );std::string to_string( long double value );

Warto wiedzieć, że sytuacja, gdy „skazani byliśmy” na printf (sprintf) nie ma już miejsca!W nagłówku <string> dostępna jest seria przeciążonych funkcji to_string, działającychkomfortowo i bezpiecznie z punktu widzenia kontroli typów.Nie musimy się też martwić o wielkość wypełnianego buforu!

Pomiędzy „łańcuchem znakowym” a innymi typami

#include <string>// konwertuje łańcuch znakowy std::string na typ całkowityInt stoi( const std::string& str, size_t *pos = 0, int base = 10 );long stol( const std::string& str, size_t *pos = 0, int base = 10 );long long stoll( const std::string& str, size_t *pos = 0, int base = 10 );unsigned long stoul( const std::string& str, size_t *pos = 0, int base = 10 );unsigned long long stoull( const std::string& str, size_t *pos = 0, int base = 10 );

// konwertuje łańcuch znakowy std::string na typ zmiennoprzecinkowyfloat stof( const std::string& str, size_t *pos = 0 );double stod( const std::string& str, size_t *pos = 0 );long double stold( const std::string& str, size_t *pos = 0 );

Podobnie w drugą stronę, jeśli mamy łańcuchy znakowe (np. parametry programu), możemyteraz skorzystać z następujących funkcji konwersji. Działają one następująco: opuszczają białe znaki, czytają cyfry (tak wiele ile jest poprawne dla ustawionej bazy base, resztę ignorują), jeśli podstawi się jako drugi parametr niezerowy wskaźnik, to wpisane w niego zostaje adres pierwszego nieskonwertowanego znaku oraz jego indeks.

Wskaźniki do funkcji

nazwa funkcji jest zarazem jej adresem• wskaźnik do funkcji bez argumentów i nie zwracającej żadnej

wartościprzeczytajmy to:

bardziej skomplikowane deklaracje

spróbujmy to przeczytać:pFun1() – pFun1 jest bezarg. funkcją…*(pFun1()) – …zwracającą wskaźnik do…(*(pFun1()))[10] – …10-elementowej tablicy…(*(*(pFun1()))[10]) – …zawierającej wskaźniki do…(*(*(pFun1())[10])() – …funkcji bezargumentowych…int (*(*(pFun1())[10])() – …zwracających int

void (*pFun)();

*pFun – pFun jest wskaźnikiem do…(*pFun)() – …bezargumentowej funkcji…void (*pFun)() – …zwracającej void (czyli nic)

Co by się stało, gdyby przestawić jeden z nawiasów, np. tak:int (*(*(pFun1()) [10] ))();… byłby błąd: deklaracja funkcji zwracającej tablicę

int (*(*(pFun1()))[10])();

Wskaźniki do funkcji – przykład dla…

• typedef znacznie upraszcza zapis (czasem jest wręcz konieczny)

to też spróbujmy przeczytać:(*pFun2) – pFun2 jest wskaźnikiem do…(*pFun2)() – …bezargumentowej funkcji…(*(*pFun2)()) – …zwracającej wskaźnik do…(*(*pFun2)())[10] – …10-elementowej tablicy…(*(*(*pFun2)())[10]) – …zawierającej wskaźniki do…(*(*(*pFun2)())[10])(void (*(tab[]))()) – …funkcji, która jako argument ma…

tab[] – …tablicę… (*(tab[])) – …wskaźników do…void (*(tab[]))() – …bezargumentowej funkcji nic nie zwracającej…

double (*(*(*pFun2)())[10])(void (*(tab[]))()) - …a zwraca typ double• normalny przykład użycia – tablica wskaźników do funkcji

typedef double (*(*(*pFun2)())[10])(void (*(tab[]))());pFun2 mojaZmienna;

void glos1(); void glos2(); void glos3();// … gdzieś dalej w funkcji mainvoid (*tablica[3])() = { glos1, glos2, glos3 }; // 3-elementowa tablica wskaźników do…tablica[0](); // wywołanie funkcji glos1(*tablica[2])(); // wywołanie funkcji glos3

Funkcje – przeładowanie (przeciążenie)

• przeładowanie (przeciążenie) ma miejsce, gdy w danym zakresie ważności dostępne są więcej niż jedna funkcja o tej samej nazwie

• rozróżnienie funkcji następuje poprzez listę parametrów funkcji, która musi się różnić, ewentualnie modyfikator metody const

• typ zwracany przez funkcję nie ma znaczenia• to nie jest żadna technika obiektowa, kompilator buduje sobie nazwę funkcji

poprzez doklejanie do nazwy różnych przyrostków opisujących argumenty (ang.: name mangling – „dekorowanie”)

• „dekorowanie” nazw nie jest określone w standardzie (zależne od kompilatora)void fun(int); // _Z3funiint fun(int, double); // _Z3funidchar fun(char, short int, float*); // _Z3funcsPfdouble* fun(int&, const double&, char*); // _Z3funRiRKdPc

• jeżeli chcemy skonsolidować nasz kod z kodem skompilowanym w innym języku (C, Fortran) w którym nie ma „dekorowania”, musimy użyć specyfikatora extern "C"

extern "C" void mojaZewnFun(int, double);extern "C" { /* można też więcej w nawiasach */

#include "myHeader.h" // można włączyć nagłówek}

Funkcje – przeładowanie (szczegóły)

• użycie typedef (albo using) nie powoduje zmiany typutypedef int calkowity;void fun(int);void fun(calkowity); // nie wolno, ta sama funkcja

• enum to już zupełnie inny typ, mimo iż oparty na wielkościach całkowitych, podobnie short int to inny typ niż int

enum EPozycja { eAsystent = 1, eAdiunkt, eProfesor };void fun(int);void fun(short); // OKvoid fun(EPozycja); // OK

• o możliwości przeładowania decyduje wygląd inicjalizatora, jeżeli parametr, z którym jest wywołana funkcja wygląda tak samo, to przeładowanie niemożliwe

void fun(int tablica[]); void fun(int* ptrTab); // obie funkcje można wywołać z takim samym // argumentem, np. nazwą tablicy int tab[5];

• jeśli parametrem jest tablica, to pamiętajmy o nieznaczeniu najbardziej lewego indeksu do rozróżnienia typów

void fun(int tab1[10]);void fun(int tab2[5]); // tab1 i tab2 nierozróżnialnevoid fun(int tab3[6][3]);void fun(int tab4[6][2]); // tab3 i tab4 rozróżnialne

Funkcje – przeładowanie (c.d.)

• identyczność T oraz T& – void fun(T) i void fun(T&)są nierozróżnialne, bo wywoływane z tym samym parametrem

• identyczność T, const T, volatile T – podobnie, inicjalizator takich argumentów wygląda tak samo, nie da się rozróżnić funkcji

• rozróżnialność T*, const T*, volatile T*w przypadku takich argumentów inicjalizatory muszą być różne

• rozróżnialność T&, const T&, volatile T&podobnie, takie funkcje o takich argumentach mogą być przeładowane

• wskaźnik do funkcji pokazujący na przeciążoną nazwę funkcji – wskaźnik jest ściśle określony, więc nie ma problemu z przeciążeniem (niezależnie od tego czy wskaźnik jest argumentem lub typem zwracanym przez funkcję)

• dwie przestrzenie nazw mogą doprowadzić do przeciążenia funkcji o tej samej liście parametrów, ale tylko do chwili wywołania (niejednoznaczność sygnalizowana przez kompilator – ale tylko jeśli jest takie wywołanie, więc taka niejednoznaczność może pozostać niezauważona)namespace A { void fun(int) { cout << "Jestem z A /n"; } }namespace B { void fun(int) { cout << "Jestem z B /n"; } }using namespace A;using namespace B;fun(5); // wywołanie funkcji niejednoznaczne, błąd kompilatoraA::fun(5); // w takim wypadku trzeba jawnie podać kwalifikator

Funkcje – dopasowanie argumentów (1)

• dokładne (parametry dokładnie pasują do argumentów)• dopasowanie z tzw. trywialną konwersją

T → T&, T& → T, T[] → T*, T(arg) → (*T)(arg), T → const T, T →volatile T, T* → const T*, T* → volatile T*

• dopasowanie z konwersją bezstratną (tzw. „promocja”) float → double, char (signed char, unsigned char, short int, unsigned short int) →int lub unsigned int (wybiera ten typ, który nie obetnie danych!)bool → intwchat_t (enum) → int lub unsigned int lub long lub unsigned longpola bitowe → int lub unsigned int (ten, który nie obetnie!)

nie należy mylić tego z przeciążonymi wersjami funkcjamitu mówimy o dopasowaniu konkretnych argumentów do argumentów z deklaracji funkcji

Funkcje – dopasowanie argumentów (2)

• dopasowanie za pomocą konwersji standardowych (może być stratna)int → unsigned int, unsigned int → int, double → float, zmiennoprzecinkowy → całkowity, całkowity →zmiennoprzecinkowy, konwersje arytmetyczne, konwersje wskaźników (0 na wskaźnik do adresu zerowego), wskaźnik nie-const i nie-volatile na void*, wskaźnik klasy pochodnej na wskaźnik klasy podstawowej, referencja do klasy pochodnej na referencję do klasy podstawowej

• dopasowanie z użyciem konwersji zdefiniowanych przez użytkownika (dopuszczalna tylko jedna)

• do funkcji z wielokropkiem void fun(…) – jeśli taka jest• wskaźniki – tylko dosłownie!• jeśli funkcja z kilkoma argumentami, to wybierane najlepiej

dopasowane

std::function – uogólnienie wskaźnika na funkcję (C++11)

Wskaźnik na funkcję mógł pokazywać tylko funkcję.std::function może pokazywać na wszystko, co można wywołać: funkcje, obiekty funkcyjne, wyrażenia lambda, wyrażenia bind, wskaźniki na metody składowe.

Nowa składnia też możliwa, np.: std::function<auto(int)->void> f;Jeśli obiektu std::function nie ustawi się na coś i spróbuje wywołać, zgłoszony zostanie wyjątek: std::bad_function_callUżycie std::function jest zwykle kosztowniejsze (w sensie czasu wykonania) niż użycie auto, ale nie zawsze auto jest dozwolone:

void fun(int i) { cout << "to ja " << i << endl; }std::function<void(int)> f = fun; // f pokazuje na funkcję funf(7); // wywołanie

void useIt(auto func); // błądvoid useIt(std::function<bool(long)> func); // okclass Klasa { auto func; // błąd…class Klasa { std::function<bool(long)> func; // ok…

Nagłówek: <functional>

auto – dedukcja typu (C++11)

auto – dawniej oznaczało tylko zmienną lokalną (automatyczną)• dedukcji typu w oparciu o typ inicjalizatora

lub typu zwracanego przez funkcję

• dedukcja odbywa się tak jak w szablonach, z wyjątkiem rozpoznawania listy { a, b, c }, którą auto widzi jako std::initializer_list<T> (gdzie T to typ a, b, c)

auto i = 7; // typ intauto x = wyrażenie // x będzie typu zwracanego przez wyrażenie

template<class T> // to samo co: template<typename T>int whatever(T t) {

T x; // równoważne do auto x poza szablonem};

auto – dedukcja typu (C++11)

• korzystne gdy typ bardzo długi (szablony…)

• czasem wręcz bardzo pomocne, gdy typy nieustalone•

• może prowadzić do nowych nawyków

template<class T> void printall(const vector<T>& v) { for (typename vector<T>::const_iterator p = v.cbegin(); p != v.cend(); ++p)

cout << *p << "\n"; for (auto p = v.cbegin(); p != v.cend(); ++p)

}

auto i = MyClass(); // zamiast poniższegoMyClass i;

template<class T, class U> void (const vector<T>& vt, const vector<U>& vu) { // ... auto tmp = vt[i]*vu[i]; // ...

}

auto – zastosowania ( C++11 )

• przykłady

• działa również z operatorem new

• szczególnie wygodne do dedukcji typów iteratorów

• uwaga na przyszłość: (C++14) auto działa również wewnątrz wyrażeń lambda, np. [ ] ( auto param ) { };

for( auto i = m.begin(); i != m.end(); ++i ) .. // niech m jest typu map<int,string>const auto& y = m; // y jest typu const std::map<int, std::string>&

auto a = 0; // a jest typu intconst auto *ptr = &a, b = 5; // ptr typu const int*, b typu const intstatic auto d = 3.14; // d typu doubleauto x = { 1, 2, 3 }; // x typu std::initializer_list<int>

new auto(1); // alokowanym typem jest intauto z = new auto('a'); // alokowanym typem jest char, z jest typu char*

auto – zastosowania ( C++11/14 )

• niektóre rzeczy są (C++14) już możliwe, a niektóre nadal nie:

• możliwe jest

• uwagaauto s = "hello world"; // jest typu const char*auto& s = "hello world"; // jest typu referencja do const char[12] czyli tablicy

void fun( auto arg ) { } // autodedukcja typu argumentu możliwa w C++14class Foo {

auto m = 1; // źle: autodedukcja typu zwykłej składowej klasy niemożliwa// bo np. auto m = f(); wprowadzałoby spory problem w szukaniu // właściwej interpretacji tego czym jest f()};auto tablica[5]; // źle: autodedukcja typu z którego zbudowana jest tablica

class Foo {static const auto n = 0; // jeśli static to tylko const inicjalizowany i również auto

};

auto – nowe metody w kontenerach, nowa pętla for ( C++11 )

W kontekście auto przydatne są nowe metody kontenerów:• zwracają jawnie stałe iteratory: cbegin(), cend(), crbegin(), crend()

Nowa składnia dla pętli for (tzw. range-based loop)

Można przebiegać po tablicach, kontenerach oraz dowolnych typach wyposażonych w iteratory, zwracane przez begin() i end()

short tablica[5];for ( auto& t : tablica ) { t = -t; } std::unordered_multiset<std::shared_ptr< T >> obj;for ( const auto& r : obj ) cout << r; // wypisuje wskaźnik// pytanie: czemu powyższe przez referencję?

auto ci = m.cbegin(); // ci typu std::map<int, std::string>::const_iterator

vector<int> v { 1,2,3,4,5 };for ( int i : v ) cout << i << endl; // i bezpośrednio każdym elementem wektorafor ( auto i : v ) cout << i << endl; // to samo co powyżejfor ( int& i : v ) cout << ++i; // może być też referencją i zmieniać zawartość!for ( auto& i : v ) cout << ++i; // to samo co powyżejfor (const int i : v ) jakasMetoda( i ); // const/volatile też możliwe

w C++11 nie ma problemu zagnieżdżonychnawiasów szablonów, nie

trzeba rozdzielać spacją

auto – referencje, modyfikatory ( C++11 )

Dla zmiennych nie zadeklarowanych wprost jako referencje, modyfikatory const/volatile na najwyższym poziomie są ignorowane:

Podobnie (tak jak działa dedukcja w szablonach) referencje ignorowane:

Referencje do prawych-wartości:

Wskaźniki i modyfikatory:

const vector<int> w;auto v1 = w; // v1 typu vector<int>, const zignorowaneauto& v2 = w; // v2 typu const vector<int>& - ale jeśli przez referencję, to ok

int a; int& r = a;auto b1 = r; // b1 typu intauto& b2 = r; // b2 typu int&

int a; int& r = a;auto&& c1 = r; // c1 typu int& ponieważ r jest „lewą-wartością” więc: && → &auto&& c2 = 7; // c2 typu int&& gdyż 7 jest „prawą-wartością”

int a; const int* const pcc = &a; const int* pc = &a; int* p = &a;auto d1 = pcc; // d1 typu const int* czyli const wskaźnika zignorowaneauto d2 = pc; // d2 typu const int*auto d3 = p; // d3 typu int*

auto – referencje, modyfikatory ( C++11 )

W przypadku „referencji do p-wartości” zwracanej przez funkcję:

Tablice i nazwy funkcji redukują się do wskaźników:

Jeżeli const/volatile nie na najwyższym poziomie, to zostają:

Za pomocą auto można deklarować więcej zmiennych w linii:auto zmienna = s, *ptr_zmienna = &s; // dedukcja typu inicjalizatora – ten sam typauto i = 3, d = 3.14; // błąd – rożne typy inicjalizatorów

int&& fun();auto f1 = fun(); // f1 jest typu int

double tablica[5];auto t1 = tablica; // t1 typu double* - to się nazywa ”array decay to pointer”auto& t2 = tablica; // t2 typu double(&)[5] – właściwy typ tylko jeśli przez referencję

auto i = 10; map<int, string> m;const auto *pi = &i; // pi jest typu const int*const auto& pm = m; // pm typu const map<int, string>&

decltype ( C++11 )

decltype – służy do określenia typu w czasie kompilacji

int a, *p;struct K { float f; } *ptr;decltype(a) m; // m jest typu intdecltype(p) mp; // mp jest typi int*size_t s = sizeof( decltype( ++m ) ); // s = sizeof(int), m nie jest ewaluowana (powiększona)

decltype(ptr->f) x; // x jest typu floatdecltype(K::f) y; // y jest typu floatvector<int> v;decltype (v[0]) z; // z jest typu int&decltype(v) v2; // v2 jest typu vector<int>decltype(v)::iterator it; // vector<int>::iterator

decltype – głownie po to, żebyokreślić typy zwracane w szablonach,zależne od typów parametrów

template <typename X, typename Y>auto mnożenie(X x, Y y) -> decltype(x*y) {

return x*y;}

template <typename X, typename Y>decltype(x*y) mnożenie(X x, Y y) {

return x*y;} // składnia z auto na początku jest // konieczna, powyższe jest błędne bo// x i y jeszcze „nie zaistniały”

decltype i decltype(auto) ( C++14 )

Podwójny nawias względem wyrażenia decltype:

W standardzie C++14 decltype w połączeniu z auto:

Niespodzianki:

int&& f();auto var1 = f(); // var1 jest typu intdecltype( f() ) var2 = f(); // var2 jest typu int&&

decltype ((a)) m2 = a; // int a; m2 jest typu int& - wymaga inicjalizacji// jeśli: const int a; wtedy m2 jest typu const int&

decltype(auto) var3 = f();

decltype(auto) f1() {int x = 0;return x; // decltype(x) jest int, więc f1 zwraca int

}decltype(auto) f2() {

int x = 0;return (x); // decltype((x)) jest int&, więc f2 zwraca int& …do obiektu lokalnego!

}

Trzeba uważać na nawiasy!

auto i decltype ( C++14 ) kilka przykładów na podsumowanie

Przykłady:int a;int&& fun();auto k1 = a; // k1 jest typu intdecltype(auto) k2 = a; // k2 jest typu intauto k3 = (a); // k3 jest typu intdecltype(auto) k4 = (a); // k4 jest typu int&auto k5 = fun(); // k5 jest typu intdecltype(auto) k6 = fun(); // k6 jest typu int&&auto k7 = { 1, 2, 3 }; // k7 jest typu std::initializer_list<int>decltype(auto) k8 = { 1, 2, 3 }; // błąd: {1, 2, 3} nie jest wyrażeniemauto *k9 = &a; // k9 jest typu int*decltype(auto) *k10 = &a; // błąd: to jest niepoprawne wyrażenieW standardzie C++17 nastąpią pewne zmiany, np.:auto k11 { 7 }; // k11 będzie int, (w C++14) jest std::initializer_list<int>

Para obiektów – std::pair

Czasem przydaje się pojedynczy obiekt, ale reprezentujący więcej niż jeden typ. Dwa obiekty dowolnego typu można zamknąć w szablonie std::pair

Utworzenie pary (wprost albo za pomocą std::make_pair):

Para zawiera dwa pola, jej składowe nazwano first i second:

int tab[] = { 1, 2, 3, 4 };pair<int, double> p1( 3, 3.14 );auto p2 = make_pair( tab[2], 3.14 ); // z dedukcją typu: int, doubleauto p3 = make_pair( ref( tab[0] ), tab); // jeśli chcemy referencję// to możemy opakować w std::ref z nagłówka <functional>, drugie pole int*pair< int, double > p4 = { 3, 2.45 }; // dostępne w C++11

#include <utility> // typ std::pair w tym nagłówku

template < typename T1, typename T2 > class pair;

cout << p1.first << ", " << p1.second; // 3, 3.14cout << p3.first << ", " << *(p3.second+3); // 1, 4

std::tuple i std::get ( C++11 )

Uogólnienie std::pair na dowolny zbiór wielkości różnego typu (heterogeniczny), ale o stałej wielkości (jak w deklaracji).

Można zainicjalizować inną tuplę:

Dostęp do elementów tupli poprzez get:

Indeks (argument szablonu get) jest wygenerowany podczas kompilacji, nie można więc podstawić zmiennej!Dla lepszej czytelności, można użyć etykiet z typu wyliczeniowego itp.Pętle for/do/while po argumentach tuple niemożliwe.

std::tuple< Nazwisko, Adres, Data > info( pracownik ( n ) );

#include <tuple> // związane również z <utility>

std::tuple< Nazwisko, Adres, Data > pracownik( unsigned id );

std::get<0>(info);std::get<1>(info);std::get<2>(info);

std::tie ( C++11 )

Zamiast wiele razy wołać std::get można użyć std::tie

Jeśli chcemy odczytać tylko niektóre pola, można użyć std::ignore

std::tie można użyć też z std::pair ponieważ std::tuple akceptuje std::pair jako argument konstruktora (jednego z wielu...):

std::tie( n1, a1, std::ignore ) = pracownik( m ); // nie chcemy datystd::tie( std::ignore, a1, std::ignore ) = pracownik( m );

// chcemy tylko adres... chyba łatwiej użyć tutaj std::get

Nazwisko n1; Adres a1; Data d1;std::tie( n1, a1, d1 ) = pracownik( m ); // w nagłówku <tuple>

std::pair<Nazwisko, Adres> czytajNazwAdr(unsigned n);std::tie(n1, a1) = czytajNazwAdr(i);

Typy abstrakcyjne – klasa i obiekt

• Z myślenia w kategoriach "jak to zrobić" przechodzimy do myślenia bezpośredniego nad zagadnieniem, czyli "co zrobić"

• Odwrócona kolejność tworzenia: opis danych, przepływ danych, algorytmy

• Najważniejsze są dane, na których operujemy

• klasa– matryca, "plan" według którego powstaje obiekt (opisana zawartość,

a także sposób utworzenia – konkretyzacji)– nowy typ danych zawiera w sobie składniki danych innego typu oraz funkcje

(metody) – enkapsulacja (kapsułkowanie)• obiekt

– obiekt to egzemplarz klasy– samodzielna, ograniczona jednostka posiadająca zespół cech i zachowań– każdy obiekt ma własną kopię atrybutów (wyjątek: dane statyczne),

metody (ich implementacja) są wspólne– obiekty współpracują ze sobą, działanie jest "na rzecz" jakiegoś obiektu

Kiedy klasa jest dobra?

• klasa – reprezentuje wspólne właściwości grupy obiektów– czy istnieje potrzeba tworzenia więcej niż jednego

egzemplarza klasy? (są specjalne wyjątki – singleton)– jeśli nie ma różnić pomiędzy egzemplarzami klasy:

prawdopodobnie taka klasa powinna być wartością– nie jest tylko pojemnikiem na dane, które mogą być

modyfikowane przez funkcje– udostępnia uproszczony obraz złożonego bytu, określa

dopuszczalne do wykonania czynności

• co nie jest (dobrą) klasą– zgrupowanie kilku funkcji– kontener na dane (typu struktura w C) tylko

z funkcjami typu set i get)

Cele klasy

• cel klasy

– powinien być dobrze zdefiniowany, a klasa łatwa do zrozumienia i prosta w użyciu

– nie należy dodawać do klasy metod zupełnie z nią nie związanych, tylko po to aby zaspokoić oczekiwania grupy klientów

– jeśli klient po zetknięciu z klasą nie jest pewien do czego ona służy, projekt może być słaby i niepoprawny

– wielkość klasy: jeśli liczba metod przekracza 15-25, to warto się zastanowić czy nie należałoby z jednej "wielkiej" klasy zrobić kilka mniejszych, czytelniejszych

Czy potrafisz określić cel klasy w jednym zdaniu?

Obiekt – własności

• obiekt – powołuje klasę do życia– stan obiektu jest sumą wszystkich statycznych i dynamicznych

wartości jego właściwości, właściwość jest niepowtarzalną cechą obiektu

– stan obiektu określają typy proste lub złożone– to, jak obiekt reaguje na nasze polecenia i co robi z innymi obiektami,

zależy od jego stanu– stan obiektu kontrolują metody, zwykle metody wywoływane są

przez klienta (wyjątek to metody np. do obsługi błędów, przerwań)• zachowanie obiektu

– sposób, w jaki obiekt działa i reaguje na komunikaty– komunikat może zmienić stan obiektu, może też spowodować

wysłanie komunikatów do innych obiektów– metody stałe: takie, które (gwarantują, że) nie zmieniają stanu

obiektu– wszystko co nie powinno być dostępne dla normalnego klienta,

powinno być ukrywane

Model obiektowy

• model obiektowy– w uproszczeniu: można myśleć o klasach jak o rzeczownikach,

a o ich metodach jak o czasownikach– kluczowe elementy modelu obiektowego

• abstrakcja danych• hermentyzacja• hierarchia

abstrakcja danychwynik definiowania klas, koncentrujemy się na zewnętrznym wyglądzie obiektu i oddzielamy ważne zachowania od wewnętrznych szczegółów implementacji

hermetyzacja (ukrywanie danych)wynik ukrywania wewnętrznych szczegółów implementacji, istotna w momencie rozpoczęcia implementacji

hierarchiasposób tworzenia wzajemnych relacji pomiędzy abstrakcjami danych

Typy hierarchii

"jest-czymś", realizowane poprzez dziedziczenie, umoż-liwia stosowanie relacji ogólne-specyficzne

"ma-coś", budowanie z elementów składowych, wprowadza stosunek część-całość

RACHUNEK BANKOWY

ROR LOKATA

jest:

SAMOCHÓD ma:

silnik siedzenie

koło kierownica

Zalety modelu obiektowego

• zachęca do tworzenia systemów, które mogą podlegać zmianom, systemy są elastyczne i stabilne

• myślenie w kategoriach (klas i) obiektów jest naturalne dla człowieka

• oddzielenie klienta i programisty (hermetyzacja danych)

• wielokrotne wykorzystanie prostych klas, unikanie replikacji kodu

• rozszerzalność projektów (np. poprzez dziedziczenie), czyli zachęta do ponownego wykorzystywania istniejącego oprogramowania

Interfejs i implementacja

• interfejs to punkt widzenia użytkownika na to, jak obiekt wygląda i co można z nim zrobić

• klient używa klasy bez wgłębiania się w jej wewnętrzne działanie, dobrze zaprojektowany interfejs spełnia wymagania użytkownika

• specyfikacja interfejsu – w plikach nagłówkowych• implementacja określa w jaki sposób coś jest

wykonywane, model obiektowy pozwala na ochronę implementacji (przed klientem)

• model obiektowy pozwala na zmienianie implementacji podczas gdy interfejs pozostaje niezmieniony

Klasa

KLASApodstawowa jednostka

abstrakcji danych w języku C++

• posiada trzy regiony dostępu: prywatny, chroniony i publiczny

• zawiera sygnatury – metod niestatycznych i statycznych – deklaracje danych składowych zwykłych i statycznych

• może zawierać deklarację (definicję) innej klasy – zagnieżdżonej

Nazwy deklarowane w klasie = zakres ważności to obszar całej klasy. Domyślna etykieta dostępu (odwrotnie niż w strukturze) private

Dostęp do składników klasy

class MojaKlasa {

int nr_pokoju;std::string etykieta;

public:

int getNr();string getName();

};

dane składowe są w części prywatnej!

Dostęp do składników klasy:

MojaKlasa mojObiekt;MojaKlasa *mojWskaznik = &mojObiekt;MojaKlasa &mojaReferencja = mojObiekt;

mojObiekt.getNr();mojWskaznik->getNr();mojaReferencja.getNr();

Skąd zwykła (niestatyczna) metoda wie, na jakim komplecie danych (na jakim obiekcie) pracuje?

Otrzymuje niejawnie specjalny wskaźnik: this

zawiera adres konkretnego obiektu danego typu

this is it (kilka słów o „tym” wskaźniku)

(stały) wskaźnik this – niejawnie zdefiniowana składowakażdej (niestatycznej) metody klasy, zawiera adres obiektu

this przekazywany jest jako parametr (niejawny) niestatycznym metodom klasy, aby znały adres obiektu, na którego zmiennych działają

typ wskaźnika this zależy od atrybutów metody (const, volatile), jeśli metoda jest const (volatile), to podobnie wskaźnik this (wtedy jest stałym wskaźnikiem do stałego obiektu)

void Prostokat::ustawParam(double x, double y) {this->bokX = x; // można jawnie zapisać, ale nie trzebathis->bokY = y;

}

przypadki użycia wskaźnika this

• jawne użycie this – w przypadku kopiowania obiektu, sprawdzenie żeby obiekt się nie chciał sam na siebie skopiować (jak zobaczymy później: standardowe w operatorze przypisania =)

• nie wolno używać this do usuwania obiektu (np. deletethis), za wyjątkiem sytuacji specjalnych – np. obiekt konstruujemy na stercie, ale klasa ma zablokowany destruktor, wtedy „ręcznie” sterujemy destrukcją obiektu

void Prostokat::kopiuj(const Prostokat& figura) {if (this != &figura) { // tu sprawdzamy czy nie to samo

bokX = figura.bokX; bokY = figura.bokY;

} }

Klasa – prawa dostępu

public: protected: private:

public• dostęp bez

ograniczeń (z wnętrza i poza zakresem klasy)

• tutaj jest interfejs• składniki to funkcje

protected• tak jak private,

plus dostęp dla klas pochodnych(dziedziczenie) private

• dostęp tylko z wnętrza klasy (z zewnątrz dla klas lub fukcji - przyjaciół)

• tutaj szczegóły implementacji

w dowolnej kolejności

etykiety mogą się powtarzać

domyślny

Jak ukryć implementację? Za pomocą „uchwytu”!

Technika prywatnej implementacji (idiom pimpl: PrivateIMPLementation), nazywana: „z uchwytem” (handle), „kompilowany firewall”, technika „kota z Cheshire” –przesunąć prywatne składowe klasy do zewnętrznej klasy, w pierwotnej klasie zostawiając tylko wskaźnik.

class Handle { private:

struct CheshireCat; // deklaracjaCheshireCat *smile; // uchwyt

public: // publiczny interfejsvoid init();int read();

};

struct Handle::CheshireCat {// coś ukrytegoint i;

};void Handle::init() {

simle = new CheshireCat;smile->i = 0;

}int Handle::read() {

return smile->i;}tyle potrzebne do użycia,

użytkownik zna tylko częśćpubliczną - interfejs

plik nagłówkowy

plik źródłowy

en.wikipedia.org/wiki/Opaque_pointer

Wady i zalety pimpl

Zalety

Wady

• zmiany w części prywatnej (wydzielonej) nie pociągają za sobą konieczności rekompilacji interfejsu → szybsza kompilacja

• w pliku z definicją klasy interfejsowej nie trzeba włączać nagłówków wymaganych w implementacji → szybsza kompilacja

• silniejsza forma enkapsulacji

• więcej pracy przy implementacji• nie nadaje się dla składników części chronionej• trudniejsze studiowanie kodu• pogorszenie wydajności działania – wołanie przez

pośredni wskaźnik (zwłaszcza funkcje wirtualne)

Klasa – zasłanianie i zagnieżdżanie

class A {public:

class B { };};

// zagnieżdżony typ B jest osiągalny tak:

A::B objB;

• funkcji nie można definiować lokalnie wewnątrz innych funkcji (w tym main) i innych bloków

• klasy można zagnieżdżać• klasa zagnieżdżona jest niewidoczna

poza obszarem klasy zewnętrznej, ale poprzez odpowiedni kwalifikator zakresu, o ile zdefiniowana została w części publicznej, jest do niej dostęp:

// zmienna globalnastring rechot = "hue hue…";

class Glos {class Krtan {

public:void getStruny();

};class Przelyk;public:

void rechot();};

void Glos::rechot() {float rechot;rechot(); // błąd, zasłonięte!Glos::rechot(); // okcout << ::rechot; // globalna

}void Glos::Krtan::getStruny() { /*…*/ }class Glos::Przelyk {

// ciało klasy …};

Klasa – zagnieżdżanie a prywatność

class A {// część prywatna Aint a;

class B; // ta deklaracja wyprzedzająca// jest potrzeba jeśli chce się użyć B// ale gdy już jest, to mamy dostęp// do tego co w definicji, ale tylko do// części publicznej !void foo( B* ptr ) { ptr->b2 = 0; }

class B {// część prywatna Bint b;

// klasa B ma dostęp do części // prywatnej klasy A i to w całym// zakresie void foo( A *ptr ) { ptr->a = ptr->a2; }public:

int b2; // ciąg dalszy klasy B};

int a2; // ciąg dalszy klasy A};

• C++03: zagnieżdżenie nie narusza prywatności, obiekt jednej klasy nie ma dostępu do danych prywatnych drugiej klasy

• C++11/14: zagnieżdżony typ jest traktowany tak jak każdy inny składnik klasy, czyli:

• klasa zagnieżdżona ma dostęp do całości klasy nadrzędnej, również jej części prywatnych (cały zakres klasy)

• jednak klasa nadrzędna nie ma dostępu do części prywatnych klasy zagnieżdżonej, co więcej, jeśli chcemy użyć klasy zagnieżdżonej przed jej definicją, to potrzebna jest deklaracjawyprzedzająca, potem można użyć

Klasa – zagnieżdżona w funkcji

void fun() {

class Wewnetrzna {int a;

public:Wewnetrzna(int n=0) : a(n) {}~Wewnetrzna() {}// static int nieMoge;

} obiekt;

// jakaś funkcjonalność// wykorzystująca typ// Wewnetrzna

}

• klasę można zdefiniować równieżlokalnie wewnątrz funkcji!

• może to być nawet zagnieżdżony blok wewnątrz funkcji (np. nawet pętla)

• jej typ będzie znany tylko w bloku zakresie ważności funkcji – od miejsca zdefiniowania

• klasa w całości musi być zdefiniowanawewnątrz – czyli wszystkie metodysą typu inline

• dlatego taka klasa nie może zawierać składników statycznych… (bo te są tylko deklarowane w klasie, a definicja na zwenątrz – co jest zakazane)

• …ale może mieć metody statyczne

• klasa taka może korzystać z nazw typów zdefiniowanych w funkcji (typedef), typów wyliczeniowych (enum) oraz typów zadeklarowanych jako zewnętrzne (extern)

Klasa – konstruktor

class Trivia {int i; float f;

public:Trivia(int n=0);Trivia(int k, float d);~Trivia();

};

konstruktor ( c-tor )• funkcja wywołana podczas tworzenia obiektu,

po przydzieleniu (lub wskazaniu miejsca w) pamięci• nazwa taka sama jak nazwa klasy• niczego nie zwraca (ale nie piszemy void)• może występować w wielu odmianach, z różną liczbą

argumentów (przeciążone wersje)• „domyślny” – taki, który można wywołać bez podania

parametrów (czyli bezparametryczny lub z wartością/warościami domyślną/domyślnymi argumentów

Czym się różni:Trivia::Trivia(int n) { i=n; f = 0; } // tu jest przypisanie

od:Trivia::Trivia(int n) : i(n), f(0) { } // tu jest inicjalizacja

Czy można pomieszać kolejność:Trivia::Trivia(int n, float d) : f(d), i(n) { /* … */ }

„Można”, ale to wcale nie zmienia kolejności tworzenia obiektów (najpierw i, potem f), a kompilator ostrzeże o odwrotnej (niż zapisana w kodzie) inicjalizacji!

lista inicjatorów konstruktora, „miejsce”, gdzie powstają i są inicjalizowane obiekty otwarcie { oznacza skonstruowanie obiektu

Argument o tej samej nazwie

Należy unikać zapisów budzących wątpliwość i zmniejszających czytelność kodu: class K {

int n = 3; // domyślna wartość lokalnej zmiennej n w klasie Kpublic:

K(int& n) : n(n) { // konwencja: n inicjalizujące to argument, n inicjalizowane z klasy Kn = 77; // „bliższe” jest n argument i to on będzie zmienionyn = n; // przypisanie n argumentu na samego siebie// jeśli chcemy użyć n z klasy K, napiszmy this->n, na przykład this->n = 77;

}void f( int& n ) {

n = 88; // tak samo jak w konstruktorze, tu zmieniany argument// jeśli chcemy zmienić n z klasy K, napiszmy: this->n = 88;

}};int main() {

int t = 7;int r = 5;K obj(r);obj.f(t);

}

Klasa – destruktor

class Trivia { // to co poprzednio~Trivia();

};

destruktor ( d-tor )• funkcja wywoływana podczas usuwania obiektu• nazwa taka jak nazwa klasy poprzedzona znaczkiem ~• jest tylko jeden destruktor, niczego nie zwraca• destruktor nie może mieć żadnych parametrów• destruktor powinien „posprzątać” wszelkie dynamicznie

zaalokowane wewnątrz klasy zasoby• operator delete najpierw woła destruktor (potem zwalnia

pamięć)• zgłoszenie wyjątku gwarantuje posprzątanie obiektów na

stosie (wywołanie ich destruktorów)• wyskok za pomocą instrukcji goto też wywołuje destruktor

Trivia::~Trivia() { cout << "Good bye" << endl; }

Konstruktor kopiujący T::T (const T&)

• służy do skonstruowana obiektu, który jest kopią innego, już istniejącego obiektu tej klasy (inicjalizator kopiujący)Foo::Foo( Foo& );

– może posiadać również argumenty domyślneFoo::Foo(Foo&, float = 3.14);

– może być w postaci Foo::Foo( const Foo& );Foo::Foo( volatile Foo& );Foo::Foo( const volatile Foo& );

• jeśli go nie ma, kompilator sam go utworzy, na zasadzie tworzenia wiernej kopii (bit po bicie)

Konstruktor kopiujący T::T (const T&)

• generowany konstruktor kopiujący bezpieczny (const) chyba że któryś składnik klasy ma swój konstruktor kopiujący bez przydomka const

• jeśli klasa zawiera obiekty abstrakcyjne, to do kopiowania wołane są ich konstruktory kopiujące

• kiedy pracuje copy constructor:– wywołanie jawne (inicjalizacja przez przypisanie)

– przekazanie jako argument funkcji przez wartość– zwrócenie wartości funkcji (obiekt tymczasowy

inicjalizowany konstruktorem kopiującym – zależy od optymalizacji kompilatora)

Foo nowy = stary; // stary też klasy FooFoo nowy = Foo(stary);// ale nie: nowy = stary; tu pracuje operator =

Konstruktor kopiujący – kiedy konieczny?

class A {// klasa bez konstruktora kopiującego

int numer;char* nazwa;

};// gdzieś w programie:// konstruktor tworzy dynamiczną tablicę, // do której kopiuje słowo "Trzy"A a1(3, "Trzy"); A a2 = a1; // a2 to wierna kopia a1a2.setNumber(4);a2.setName("Cztery");cout << "a1 nazwa: " << a1.getName(); // "Cztery" !

• Prawdziwa tragedia w chwili likwidowania obiektów, destruktory dwa razy spróbują usuwać tablicę pod tym samym adresem

• Analogiczny problem mamy gdy stosujemy operator przypisania =• Zwykle w klasie, w której występują wskaźniki,

konieczne jest napisanie konstruktora kopiującego

// funkcje z nagłówka <cstring>A::A(const A& src) :

numer(src.numer), nazwa(new char[strlen(str.nazwa)+1]) {

strcpy(nazwa, src.nazwa);} // wszystko zainicjalizowaliśmy// na liście inicjatorów konstruktora

delegowanie konstruktorów ( C++11 )

Można (wreszcie) użyć do budowy obiektu konstruktora, który użyjeinnego konstruktora pomocniczo:

Ograniczenie: konstruktor delegujący budowę obiektu do innego konstruktora nie może nic innego zrobić na swojej liście inicjatorów.Konstruktor nie może wywołać samego siebie do budowy obiektu.Nie ma ograniczeń w rodzaju – konstruktory mogą być inline, explicite, w dowolnej części klasy (public / protected / private).

class Foo { public: // klasa Foo ma różne wersje konstruktorówFoo() : Foo(0) {} // woła drugiFoo(int m) : Foo( 3.14 ) { } // woła trzeci – można więc „łańcuchowo”…Foo(double d) : Foo( 0, 3.14 ) {} // woła ostatniFoo(const Foo& s) : Foo( s.fm, s.fd ) {} // woła ostatni

private:// jakieś składniki fm, fd …Foo(int m, double d) : fm(m), fd(d) { }

};

Konwersja typów – konstruktor konwersji

• definiujemy konstruktor, który ma jeden argument – obiekt (lub referencję) innego typu, za jego pomocą kompilator dokona automatyczną konwersję typów

• klasa docelowa jest odpowiedzialna za konwersję typów

class A { /* … */ };class B { public:B(const A&) { /* … */ }

};void fun(B argb);// gdzieś w programie:A obiektA;fun(obiektA); // wymagany obiekt klasy B// kompilator wie jak przekonwertować B na AB obiektB = obiektA // zaskakujące?// działa (cc-tor klasy B) c-tor konwersji A na B

Konwersja typów – operator konwersji

• słowo operator, poprzedzające nazwę typu, do którego ma zostać dokonana konwersja (przeciążanie operatora)

• klasa źródłowa jest odpowiedzialna za konwersję typów

• tylko tak można zdefiniować konwersję z typów abstrakcyjnych do typów wbudowanych

class A { public: float r, s;char* nazwa;const char* cNazwa;A(float f1 = 1.0, float f2 = 3.14);operator B() const { return B(r); }operator char*() const { return nazwa; }operator const char*() const { returnc Nazwa;}

};class B { // …B(int n);

};void fun(B argb);// gdzieś w programie:A obiektA;fun(obiektA); // działa operator konwersji fun(22); // działa konstruktor klasy B

Konwersja typów – explicit

Konstruktor konwersji:• Jeśli nie chcemy niejawnego (automatycznego) konwertowania,

należy deklarację konstruktora poprzedzić słowem kluczowymexplicit B(const A&);

• Wtedy można tylko jawnie:fun(B(obiektA));obiektB = B(obiektA);

Operator konwersji: (C++11 – tylko w nowym standardzie)• Jeśli nie chcemy niejawnego (automatycznego) konwertowania,

należy deklarację operatora poprzedzić słowem kluczowymexplicit operator A();

• Wtedy można tylko jawnie:obiektB = B(obiektA); albo obiektB = (B)obiektA;albo obiektB = static_cast<B>( obiektA );

Konwersja typów – konflikty

class A {public:A(const B);

};class B {

public:operator A() const;

};void fun(A a);// gdzieś w programie:B b;fun(b); // niejednoznaczność

• Należy się zdecydować na jeden sposób konwersji• Konwersja jest jednostopniowa (tzn. jeśli mamy zdefiniowane B→A

i C→B, to jeśli na rzecz argumentu typu C zostanie podany argument typu A, nie nastąpi łańcuch konwersji od C do A

• Najpierw sprawdzana jest dwuznaczność, potem kontrola dostępu

"przeciążenie wyjścia"

class A { /* … */ };class B { /* … */ };class C {public:operator A() const;operator B() const;

};// tu się zaczyna problem// przeładowane wersje funvoid fun(A a);void fun(B b);// gdzieś w programie:C c;fun(c); // niejednoznaczność

T&, T const&, T&&, T const&& ( C++11 )

Teraz sprawdzimy wszystkie możliwe referencje do l-wartości i p-wartości:int main() {

string modyfikowalna_lwartosc("mut_lvalue");const string stala_lwartosc("const_lvalue");string& r1 = modyfikowalna_lwartosc; // OKstring& r2 = stala_lwartosc; // błądstring& r3 = modyfikowalna_pwartosc(); // błądstring& r4 = stala_pwartosc(); // błądconst string& cr1 = modyfikowalna_lwartosc; // OKconst string& cr2 = stala_lwartosc; // OKconst string& cr3 = modyfikowalna_pwartosc(); // OKconst string& cr4 = stala_pwartosc(); // OKstring&& pr1 = modyfikowalna_lwartosc; // błądstring&& pr2 = stala_lwartosc; // błądstring&& pr3 = modyfikowalna_pwartosc(); // OKstring&& pr4 = stala_pwartosc(); // błądconst string&& cpr1 = modyfikowalna_lwartosc; // błądconst string&& cpr2 = stala_lwartosc; // błądconst string&& cpr3 = modyfikowalna_pwartosc(); // OKconst string&& cpr4 = stala_pwartosc(); // OK

}

string modyfikowalna_pwartosc() { return "mut_pvalue"; }const string stala_pwartosc() { return "const_pvalue"; }

T const&, T&& - przeciążanie ( C++11 )

Teraz sprawdzimy jak zachowuje się przeciążanie:

Reguły:• poprawność inicjalizacji (np. wyrażenie stałe nie może inicjalizować niestałego argumentu)• l-wartości mocno preferują lewe-referencje, p-wartości preferują prawe-referencje• wyrażenia modyfikowalne raczej preferują referencje bez const

int main() {string modyfikowalna_lwartosc("mut_lvalue");const string stala_lwartosc("const_lvalue");

fun( modyfikowalna_pwartosc() ); // funkcja – string&& mut_pvaluefun( stala_pwartosc() ); // funkcja – const string& const_pvaluefun( modyfikowalna_lwartosc ); // funkcja – const string& mut_lvaluefun( stala_lwartosc ); // funkcja – const string& const_lvalue

}

void fun( const string& s ) { cout << " funkcja – const string& " << s << endl; }void fun( string&& s ) { cout << " funkcja – string&& " << s << endl; }string modyfikowalna_pwartosc() { return " mut_pvalue "; }const string stala_pwartosc() { return " const_pvalue "; }

za dużo kopiowania ( C++98 )

W niektórych sytuacjach C++98 wykonuje dużo zbędnych operacji kopiowania (na przykładzie wektora):

Przeniesienie lokalnego obiektu byłoby o wiele bardziej efektywne.Również w przypadkach dodawania kolejnych elementów do kontenera, gdy brakuje miejsca, powiększanie powoduje sporo operacji kopiowania - wystarczyłoby przenoszenie:

vec.push_back( obiektT );// właśnie zabrakło miejsca…// trzeba zaalokować nową// tablicę i skopiować do niej// poprzednią zawartość

std::vector<T> getVector(); // metoda produkująca wektor zawierający Tstd::vector<T> vec;vec = getVector(); // skopiuj wartość zwracaną, potem usuń lokalny obiekt

klasyczny przykład - swap ( C++98 – C++11 )

Operacja zamiany (swap) generuje mnóstwo niepotrzebnych kopiowań, choć tak naprawdę nie chcemy żadnego!

Korzystając z funkcji szablonowej std::move (nagłówek <utility>):template <typename T>void swap( T& a, T& b) {

T tmp( std::move(a) ); // przesuń dane obiektu a do tmpa = std::move( b ); // przesuń dane obiektu b do ab = std::move( tmp ); // przesuń tmp do b

} // usuń (pusty) obiekt tmp

template <typename T>void swap( T& a, T& b ) {

T tmp(a); // skopiuj a do tmp (mamy dwa obiekty a)a = b; // skopiuj b do a (mamy dwa obiekty b)b = tmp; // skopiuj tmp do b (mamy dwa obiekty tmp)

} // zniszcz obiekt tmp

kwestia stałości prawych-wartości, przenoszenie ( C++11 )

T const&& – poprawne, ale nie nadaje się do semantyki przenoszenia

Pojawiają się konstruktor przenoszący i operator= przenoszący:

class Foo { public:Foo( const Foo& ); // konstruktor kopiującyFoo( Foo&& ); // konstruktor przesuwającyFoo& operator=( const Foo& ); // operator przypisaniaFoo& operator=( Foo&& ); // przenoszący operator przypisania

};Foo utworzFoo(); // zwracamy obiekt przez wartośćFoo m1;Foo m2 = m1; // kopiowanie, bo z lewej-wartościm2 = utworzFoo(); // przenoszenie możliwe, bo z prawej-wartościm1 = m2; // tylko lewe-wartości, kopiowanie

void fun( const T&& ); // poprawne, ale nieużyteczneconst T fun(); // nie zwracaj stałej wartości bo nie użyjesz z T&&

operacja przenoszenia ( C++11 )

Przenoszenie wymaga trzech czynności:• pozbycie się dotychczasowej zawartości obiektu, do którego

przypisujemy (tylko dla operacji przypisania)• przeniesienia źródłowej zawartości• pozostawienie źródła w stanie do użytku (ale bez danych)

class Foo { Bar *ptr; // jakiś wskaźnik do zasobów

public:Foo( Foo&& src ) : ptr( src.ptr) { src.ptr = nullptr; }Foo& operator=( Foo&& src ) {

delete ptr; // usunięcie starej zawartościptr = src.ptr; // przeniesienie nowejsrc.ptr = nullptr; // ustawienie źródła na zeroreturn *this;

}};

• wskaźniki – łatwo• typy użytkownika –

składnik po składniku z użyciem ich konstruktorów / operatorów przesunięcia

nie wolno przenosićobiektu (kopiowanie, przypisanie) na samego siebie – ”undefined behavior”

realizacja przenoszenia ( C++11 )

Uważajmy czy rzeczywiście chcemy przenieść p-wartość:

Można przenieść również l-wartość (trzeba ukryć jej nazwę):// dla kodu podobnego jak w powyższym przykładzieBar( Bar&& src ) : Foo( std::move( src ) ), s( std::move(src.s) ) { … }Bar& operator=( Bar&& src ) {

Foo::operator=( std::move( src ) );s = std::move( src.s );return *this;

}

class Foo { string s; public:

Foo( Foo const& ); // cc-torFoo( Foo&& src ) : s( src.s ) { … } // kompiluje się ale tylko kopiuje

}; class Bar : public Foo { public:

Bar( Bar&& src ) : Foo( src ) { … } // też tylko kopiuje};

src.s oraz srcmają nazwysą więc l-wartościami

klasa std::string posiadastring::string( const string& ); // konstruktor kopiującystring::string( string&& ); // konstruktor przenoszący

klasa wyposażona lub nie w operacje przenoszenia ( C++11 )

Jeśli w klasie nie zdefiniowano konstruktora przenoszenia…

Automatyczne generowanie operacji przenoszenia uwarunkowane:• wszystkie składniki i klasy bazowe „przenaszalne”

– typy wbudowane (dla nich move oznacza copy) – większość typów biblioteki standardowej (np. kontenery)

• użytkownik nie zdeklarował operacji kopiowania / przenoszenia• użytkownik nie zdeklarował destruktora

class Foo { public: // klasa Foo nie wspiera operacji przenoszenia

Foo( Foo const& ); // cc-tor – nie wygeneruje się mvc-tor}; class Bar {

Foo w;public:

Bar( Bar&& src ) : w( move(src.w) ) { … } // nie skompiluje się// move(src.w) zwraca p-wartość do typu Foo, a ta idzie do cc-tora klasy Foo

};

trzeba zdefiniować również:Foo( Foo&& );

dlaczego destruktor blokuje autogenerowanie przenoszenia? ( C++11 )

Destruktor może wskazywać na istnienie pewnych niezmienników, których nie zachowa operacja przenoszenia (automatyczna):

Jeśli użytkownik napisał destruktor, ale nie napisał konstruktorów (czy operatorów=), to operacje przenoszenia nie zostaną wygenerowane.Można przenosić l-wartości ze świadomością konsekwencji, co się

dzieje z obiektem źródłowym (np. zostaje pozbawiony zawartości).

class Foo { vector<int> x;vector<int> y;size_t suma;

public: ~Foo() { assert( suma == x.size() + y.size() ); }

}; vector< Foo > w; Foo m; // dodajemy coś do wektorów x i yw.push_back( std::move(m) ); // x i y przeniesione, wartość suma zostawiona!// np. m dalej nie używane i asercja w destruktorze niespełniona!

zapamiętaj również:• napisanie move c-tor blokuje

automatyczne generowanie cc-tor (i odwrotnie)

• napisanie move operator= blokuje automatyczne generowanie kopiującego operator= (i odwrotnie)

default, delete ( C++11 )

Jeśli jest np. jakikolwiek konstruktor, to domyślny nie będzie generowany...Odblokowanie możliwości generowania, składnia „= default”

Można też zaznaczyć co jest wersją domyślną (w zasadzie nadmiarowe)

Blokowanie funkcji za pomocą składni „= delete”Foo(const Foo&) = delete; // blokowanie operacji kopiowaniaFoo& operator=(const Foo&) = delete; // blokowanie przypisania kopiującegovoid* operator new(std::size_t) = delete; // blokowanie kreacji obiektu na stercievoid fun( int ); // wywołanie fun z int jest ok – patrz poniżej jak inne typy blokowaćvoid fun( double ) = delete; // wywołanie fun z argumentem double – błądtemplate<typename T> void fun(T) = delete; // fun z dowolnym typem – błąd

class Foo { public:Foo( const Foo& ); // jest cc-tor, nie będzie automatycznego c-tor i mvc-torFoo() = default; // deklaracja, „proszę wygenerować w razie potrzeby”Foo(Foo&&) = default; // ditto

};

virtual ~Foo() = default; // deklarowany jako wirtualnyexplicit Foo(const Foo&) = default; // deklarowany jako explicit

friend – deklaracja przyjaźni

Klasa może zadeklarować przyjaźń względem funkcji lub innej klasy. Wtedy taka funkcja (inna klasa – jej funkcje) ma dostęp do części private(a także protected)

• przyjaźń nie jest wzajemna (w przykładzie jak wyżej: Przestepca nie jest przyjacielem Prokuratora)

• przyjaciel mojego przyjaciela nie jest moim przyjacielem (nie ma przechodniości)

class Przestepca {float pieniadze; // (domyślnie) część prywatna klasystring zycie_osobiste; // jak wyżej// miejsce deklaracji przyjaźni bez znaczenia// prokurator może zbadać całą część prywatną…friend class Prokurator; // lepiej deklarować przyjaźń tylko względem funkcji// metoda poniżej ma dostęp do privatefriend void Policjant::przeszukaj(Przestepca&); friend void ukradnij(float); // globalny przyjaciel

};

friend – szczegóły

• ponieważ kompilator musi znać deklarację obiektu w chwili deklarowania przyjaźni, więc nie da się zadeklarować przyjaźni pomiędzy dwiema funkcjami dwóch różnych klas, tylko pomiędzy całymi klasamiclass A;class B { friend class A; };class A { friend class B; };

• funkcja (klasa) może być przyjacielem więcej niż jednej klasy• funkcja zaprzyjaźniona może na argumentach jej wywołania

dokonywać konwersji zdefiniowanych przez użytkownika (w szczególności: operator<<, operator>>)

• przyjacielem może być nawet funkcja napisana w innym języku

• przyjacielem jest tylko konkretna funkcja, a nie ewentualne inne przeciążone

friend – trochę egzotyki

Można zdefiniować funkcję zaprzyjaźnioną w ciele klasy deklarującej przyjaźń

class A;

class B{public:float czytaj();float czytaj(A& a);

};

class A{float f1;static float f2;// funkcja zaprzyjaźniona nie ma wskaźnika thisfriend float B::czytaj(A& a) { return a.f1; }friend float B::czytaj() { return A::f2; }// poniżej funkcja globalna, bez uprzedniej deklaracjifriend float readGlobal() { return A::f2; }

};

float A::f2 = 3.14; // definicja zmiennej statycznej

• w deklaracji przyjaźni nie mogąsię pojawić specyfikatory:static, auto, register,extern, mutablefunkcja zdefiniowana w cieleklasy A może korzystać zobowiązujących deklaracjitypedef oraz zdefiniowanychtypów wyliczeniowych enum

• klasa A nie może być klasą lokalną (tzn. zdefiniowanąw ciele funkcji)

• jeśli konstruktor klasy A prywatnyto funkcji zaprzyjaźnionej z klasyB można użyć do utworzeniaobiektu klasy A

A propos:funkcji zaprzyjaźnionej nie jest przekazywany wskaźnik this, zatem adres danego obiektu trzeba jej przekazać jawnie jako jeden z argumentów

przeciążenie (przeładowanie) operatorów

• Możliwe jest zdefiniowanie działania operatorów dla typów abstrakcyjnych (tzn. zdefiniowanych przez użytkownika)

• Przeładowanie to definiowanie nowej (przeciążonej) wersji funkcji operatora:

operator@, gdzie @ - symbol operatora

• Operatory mogą być jednoargumentowe lub dwuargumentowe, argumenty nie mogą być domyślne

• Operatory mogą być funkcjami globalnymi lub funkcjami składowymi danego typu abstrakcyjnego

• Funkcje operatorów zwracają albo referencję do obiektu typu, na którym się wykonuje operację, albo sam obiekt –chyba że są to operatory warunkowe (zwracające wartość logiczną bool)

przeciążenie (przeładowanie) – logika, czego nie można

• Należy zachować logikę działania operatora i przeciążać go wtedy, gdy zwiększy to czytelność programu

• Można przeciążać tylko istniejące operatory (nie można definiować własnych)

• Nie można zmienić reguł dotyczących priorytetów operatorów, ani natury operatorów (tzn. operator dwuargumentowy musi być dwuargumentowy)

• Nie można przeciążyć operatora wyboru składowej (.), operatora wyłuskania wskaźnika do składowej (.*), operatora zakresu (::) i trójargumentowego operatora ?:

• Nie można też przeładować znaków dla dyrektyw preprocesora # lub ##, oraz operatora sizeof i operatorów rzutowania (w stylu C++) static_cast, dynamic_cast, reinterpret_cast, const_cast

operatory 1-argumentowe (globalne)

Operatory: +, -, ~, &, !, ++, -- w wersji funkcji globalnych

class A {int i;A* This() { return this; }public:A(int n=0) : i(n) {}// przyjaźń konieczna, tylko gdy chcemy dostać się do prywatnych składowychfriend const A& operator+(const A& a);friend const A operator-(const A& a);friend const A operator~(const A& a);friend A* operator&(A& a);friend bool operator!(const A& a);// zmienia się stan obiektu, więc już nie const ("efekt uboczny")friend const A& operator++(A& a); // przedrostkowyfriend const A operator++(A& a, int); // przyrostkowyfriend const A& operator--(A& a); // przedrostkowyfriend const A operator--(A& a, int); // przyrostkowy

};

operatory 1-argumentowe (implementacja)

const A& operator+ (const A& a) {return a; // nic nie robi więc może być zwracanie przez referencję

}const A operator–(const A& a) {

return A(–a.i); // przez wartość, bo nigdy nie zwracamy} // referencji do obiektu tymczasowegoA* operator&(A& a) {

// return &a; to by nas wpędziło w nieskończoną pętlę rekurencji !!!return a.This(); // zwraca this

}// operator przedrostkowy, zwracamy referencję do obiektu po jego inkrementacjiconst A& operator++(A& a) {

a.i++; return a;}// operator przyrostkowy, drugi nienazwany argumentconst A operator++(A& a, int) { // konieczny do odróżnienia wersji

A temp(a.i);a.i++;return temp; // zwracamy obiekt przed jego inkrementacją

} // - przez wartość, bo tymczasowy

operatory 1-argumentowe (składowe)

Operatory w wersji funkcji składowych

class A {int i;public:A(int n=0) : i(n) {}// argument funkcji staje się zbędny, operator działa na rzecz obiektuconst A& operator+() const { return *this; }const A operator-() const { return A(-i); }const A operator~() const { return A(~i); }A* operator&() { return this; }bool operator!() const { return !i; } // może czytelniej: return !this->i; const A& operator++() { ++i; return *this; }const A operator++(int) { A temp(i); ++i; return temp; }const A& operator--(); // przedrostkowy, analogicznie jak wyżejconst A operator--(int); // przyrostkowy, analogicznie

};

operatory 2-argumentowe (globalne)

Operatory : +, -, *, /, %, ^, &, |, <<, >>,+=, -=, *=, /=, %=, ^=, &=, |=, >>=, <<=,==, !=, <, >, <=, >=, &&, || w wersji funkcji globalnych

class A {int i;A* This() { return this; }public:A(int n=0) : i(n) {}// pamiętajmy, że sens ma definiowanie całych grup operatorów// czyli jeśli przeładujemy operator==, to również operator!=friend const A operator+(const A& a, const A& b); // reszta analogiczniefriend A& operator+=(A& a, const B& b);friend bool operator==(const A& a, const A& b);

};

operatory 2-argumentowe (implementacja)

const A operator+ (const A& a, const A& b) {return A(a.i + b.i); // przez wartość, bo obiekt tymczasowy

}const A operator/(const A& a, const A& b) {

if (b.i == 0) { /* coś trzeba zrobić, np. rzucić wyjątek */ }return A(a.i / b.i);

}// przypisanie modyfikujące i zwracające l-wartośćA& operator+=(A& a, const A& b) {

if (&a == &b) { /* ewentualnie zareagować na taką sytuację */ }a.i += b.i; return a;

}// operator warunkowy, zwracający true lub falsebool operator==(const A& a, const A& b) {

return a.i == b.i; }

operatory 2-argumentowe (składowe)

Operatory w wersji metod składowychclass A {

int i;public:A(int n=0) : i(n) {}const A operator+(const A& b) const { return A(i + b.i); }// przypisanie modyfikujące i zwracające l-wartość (funkcja składowa)A& operator=(const A& b) {

if (this == &b) return *this; // przypisanie do samego siebiei = b.i;return *this;

}A& operator+=(const A& b) {

if (this == &b) { /* ewentualnie zareagować na taką sytuację */ }i += b.i;return *this;

}bool operator==(const A& b) const { return i == b.i; }

};

operator przypisania: operator=

Najlepiej unikać inicjalizacji przezprzypisanie i używać do tego celuwywołanie konstruktora

A a;A b = a; // tu działa konstruktor kopiującyb = a; // tu działa operator przypisania

• Jeśli operator ten nie został zdefiniowany, zostanie automatycznie wygenerowany przez kompilator: operacja przypisania polega na wiernym (bit po bicie) przepisaniu stanu obiektu z prawej strony do obiektu po lewej stronie

• Jeśli klasa zawiera obiekty składowe, to dla każdego z nich jest wywoływany rekurencyjnie operator= (jest to przypisanie za pośrednictwem elementów składowych – memberwise assignment)

• Jeśli klasa zawiera wskaźniki, to przy braku definicji operator=przypisanie jednego obiektu do drugiego da równie katastrofalne efekty, jak w przypadku działania automatycznie wygenerowanego konstruktora kopiującego (skopiujemy adres wskaźnika, a nie wskazywaną zawartość)

• operator= nie jest dziedziczony (podobnie jak konstruktory / destruktory)• Jeśli chcemy zakazać przypisywania obiektu, to w deklaracji należy dodać

„= delete” (C++11), poprzednio realizowano to deklarując operator= jako funkcję prywatną (o ile jej nie wywołamy gdzieś w obrębie klasy – nie trzeba jej definiować)

tylko funkcja składowa

operator= (automatyczny)

Przy braku zdefiniowanej funkcji operator= nastąpi próba wygenerowania.Nie uda się jeśli:• klasa ma składnik const (bo dopuszczalna tylko inicjalizacja)• klasa ma referencję (bo też musi być zainicjalizowana)• klasa ma składnik innej klasy, w której przypisanie jest

uniemożliwione (tzn. operator= jest zablokowany)• (dot. dziedziczenia) klasa podstawowa ma operator

przypisania zablokowany

Przypomnienie:Musimy kontrolować czy przypisujemy obiekt do siebie samego, ponieważ jeśli zachodzi alokowanie pamięci w obiekcie, to zanim obiekt skopiuje "od siebie do siebie", to zniszczy kawałek siebie, wykonując inicjalizację (alokację pamięci)if (this==&source) return *this;

// standardowo pierwsza linijka

Powrót z funkcji – „copy elision”

Jak może przebiegać powrót z funkcji?return A(a.i + b.i);

• Składnia obiektu tymczasowego - "utwórz obiekt tymczasowy i go zwróć"

• Obiekt tymczasowy tworzony jest bezpośrednio w miejscu przeznaczonym na wartość zwracaną – tylko jedno wywołanie konstruktora (nie potrzeba w ogóle konstruktora kopiującego)A temp(a.i + b.i);return temp;

• Tworzony obiekt tymczasowy z wywołaniem konstruktora• Konstruktor kopiujący kopiuje wartość obiektu temp do

miejsca na zewnątrz funkcji przeznaczonego na zwracaną przez nią wartość

• Na koniec, wołany destruktor obiektu temp

Współczesne kompilatory potrafią wydajnie optymalizować nawet takie sytuacje. Niewykorzystanie cc-tor’a nie oznacza, że jego „przepis” może nie być dostępny. Musi być (lub będzie wygenerowany).

Kopiowanie płytkie i głębokie

Tablica2D obiekt_zrodlo

int szerokosc int wysokosc

TablicaKomorka**

każdy element jestwskaźnikiem bez nazwyTablicaKomorka*

każdy element jestobiektem bez nazwyTablicaKomorka

Tablica2D obiekt_kopia

int szerokosc int wysokosc

TablicaKomorka**

płytkie kopiowanie

głębokie kopiowanie

• nie tylko konstruktor kopiujący musi zadbaćo głębokie kopiowanie

• również operator przypisania =Foo& operator=(const Foo& source);(będzie o tym później)

• ponieważ konstruktor kopiujący i operator= wykonują podobne operacje, można zamknąć wspólną część w osobnej funkcji

Kopiowanie przy zapisie (Copy On Write)

obiekt1

TString

• dzielenie zasobów musi być możliwe bez dodatkowych kosztów procesora i pamięci

• powinna być możliwa identyfikacja i kontrola wszystkich ścieżek, które mogą modyfikować zasoby

• implementacja musi mieć możliwość śledzenia w każdych warunkach wielu obiektów, które dzielą zasoby

idea: współdzielić zasoby (kopiowanie płytkie) tak długo jak to możliwe, głęboką kopię tworzy się dopiero przy próbie modyfikacji – kiedy jeden z obiektów dzielących zasoby próbuje je zmieniać, tworzona jest kopia (kopiowanie głębokie)

”ALFABET”

obiekt1

TString

”ALFABET”

obiekt2

TString

kopiowanie

płytkie

obiekt1

TString

obiekt2

TString

”ALFABET””alfabet”

kopiowaniegłębokie

np. wywołana jest funkcja zmieniająca wielkość liter we współdzielonym zasobie: obiekt1.tolower();

copy on write (i zliczanie referencji)

class TString {// wiele funkcji

class StrContainer {public:char* str;unsigned refCount;unsigned length;

} *ptrStr;};

TString::TString(const TString& src) {src.ptrStr->refCount++;this->ptrStr = src.ptrStr; }

TString& TString::operator=(const TString& src) {if (this==&src) return *this;src.ptrStr->refCount++;if (--ptrStr->refCount == 0) {

delete [] ptrStr->str;delete ptrStr; }

this->ptrStr = src.ptrStr;return *this; }

TString::~TString() {if (--ptrStr->refCount == 0) {

delete [] ptrStr->str;delete ptrStr;

} }

TString& TString::tolower() {char* p;if (ptrStr->refCount > 1) {// tu kopiowanie przy zapisie

unsigned len = ptrStr->length;p = new char[len+1];strcpy(p, ptrStr->str);ptrStr->refCount--;ptrStr = new StrContainer;ptrStr->refCount = 1;ptrStr->length = len;ptrStr->str = p;

}// dalej część zmieniająca// wielkość znakówreturn *this; }

TString::TString() {ptrStr = new StrContainer;ptrStr->refCount = 1;ptrStr->length = 0;ptrStr->str = 0; }

przypisujemy nowy zasób, więc w starym musimy zmniejszyć licznik i ewentualnie skasować cały zasób, jeśli był to ostatni obiekt posiadający

Wskaźniki do składników klasy

wskaźnik – zmienna przechowująca adreswskaźnik do składowych – pokazuje na położenie elementu wewnątrz klasyadres elementu wewnątrz klasy – wymaga istnienia konkretnego obiektu, składa się z adresu tego obiektu oraz przesunięcia do danego elementu klasyelement pokazywany wskaźnikiem – dostajemy się do niego przez operację wyłuskiwania (*), teraz jednak potrzebujemy również obiektu:

(normalnie) obiekt.składowawsk_do_obiektu->składowa

(gdy mamy wsk_do_skladowej)obiekt.*wsk_do_skladowejwsk_do_obiektu->*wsk_do_skladowej

Wskaźniki do składowych

• wskaźników do składników nie można inkrementować (dekrementować), ani porównywać

• pokazują one na dane niestatyczne, bo dane statyczne istnieją pomimo obiektów i mają swój adres od chwili definicji – można na nie pokazywać zwykłym wskaźnikiem

int T::*wsk; // wskaźnik do dowolnego typu int w klasie Tclass T {

public:int a, b, c;

};// ptr tylko do pokazywania jakiegoś int w klasie Tint T::*ptr = &T::b;// tworzymy obiektT mojT;T* wskT = &mojT;mojT.*ptr = 7;ptr = &T::c;wskT->*ptr = 5;

Wskaźniki do składowych (c.d.)

• nie można ich ustawić na czymś co nie ma swojej nazwy np. 7-my element tablicy:

• na samą tablicę – można

• zwykle przydatna jest tablica wskaźników do składowych klasy

class T {public:

int kontrast;int jasnosc;float tabl[8];

};

float (T::*wsk)[8];wsk = &T::tabl; // w tym przypadku & jest obligatoryjne

int T::*tabwsk[5];tabwsk[0] = &T::kontrast;tabwsk[1] = &T::jasnosc;// … i tak dalej

Wskaźniki do składowych metod klasy

• funkcje setName w klasie T są przeciążone, ale podczas ustawiania wskaźnika o tym, na co on pokaże, rozstrzyga jego typ!

class T {public:void setName(int);void setName(string&);

};// wskaźnik do funkcji z klasy T o argumencie string&// niczego nie zwracającej (void)void (T::*fptr)(string&);// ustawienie wskaźnika na daną funkcjęfptr = &T::setName; // także tutaj & jest obligatoryjne// przykład użyciaT obiekt;T *wsk_do_obiektu = &obiekt;(obiekt.*fptr)("Joseph"); // albo:(wsk_do_obiektu->*fptr)("Jan");

Wskaźniki do składowych metod klasy (c.d.)

Do czego się to przyda?

class Gielda { // Date – jakiś typy abstrakcyjnypublic:double funduszPKO (Date, double);double funduszBPH (Date, double);

// … może jeszcze jakiś, teraz jeszcze nieznany …double zarobek(double money, double okres,

double (Gielda::*fp)(Date, double)) {return faktor = (this->*fp)("dziś", okres);

}};

// tworzymy obiektGielda warsawStock;warsawStock.zarobek(1000, 365, &Gielda::funduszBPH);

Wskaźniki do składowych metod klasy (c.d.)

Tworzenie menu: można wewnątrz klasy zdefiniować tablicę wskaźników do funkcji

class T {private:

int a(double) const;int b(double) const;int c(double) const;enum { ile = 3 };

// tablica wskaźników do pewnych funkcji wewnątrz klasy Tint (T::*fptr[ile])(double) const;

public:T() { // konstruktor

fptr[0] = &T::a; // mimo tego że w klasie, pełna specyfikacjafptr[1] = &T::b; fptr[2] = &T::c;

}int select(int i, double f) {

return (this->*fptr[i])(f);}

};

operator[] (indeksowy)

tylko funkcja składowa, o jednym parametrze• zwraca referencję do obiektu, żeby mógł być l-wartością

• nie musi służyć do pracy z tablicami, a parametr dowolny (nie musi być typu całkowitego – co było niedopuszczalne dla zmiennych wbudowanych)np. jeśli mamy jakieś duże tablice … tablica[1045][234]( tablica[1045] ) [234] – pierwszy operator przeładowany, drugi nieint *linia = new int[rozmiar];int* A::operator[](int wiersz) {

// wczytywanie do tablicy całego wiersza np. z bazy danychreturn linia;

} // przykład – patrz J. Grębosz (Symfonia C++ Standard)

class A {int i[10];public:

int& operator[] (unsigned int n) {// if (n < 10) … można sprawdzić zakresreturn i[n];

}};

operator() (funkcyjny)

tylko funkcja składowa• może mieć więcej niż 2 argumenty• może mieć argumenty domyślne

obiekt(arg1, … argN);obiekt.operator()(arg1, … argN);

Do czego się może przydać?• tablica wielowymiarowa tab(2, 4, 7);• nazwanie jakiejś czynności z którą obiekt jest związany

// Image Processing UnitIPU recognition; // rozpoznawanie obrazurecognition(); // oznacza wywołanie: recognition.operator()();

• konkretna czynność

poważne i częste zastosowanie:Zdecydowanie najczęściejużywa się go tworząc tzw. obiektyfunkcyjne, czyli klasy (struktury)wyposażone w przeciążony operator()na potrzeby algorytmów uogólnionych.

class Obiad;void Obiad::operator()(const char* food) {

cout << food << " jest mniam mniam\n";}// tworzymy obiektObiad domowy;domowy("rosół"); // wywołanie: domowy.operator()(const char*);

operator-> („strzałkowy”)

tylko funkcja składowa• cel: żeby obiekt naszego typu zachowywał się „jak wskaźnik”• jednoargumentowy, działa na argumencie po jego lewej stronie „wygląda”

jak wywołany na rzecz wskaźnika, ale jest wywołany dla obiektu

• na tym co zwraca funkcja obiekt.operator->() działa zwykły dwuargumentowy operator->– może zwracać wskaźnik lub– obiekt (lub referencję do obiektu) klasy,

która też ma przeładowany operator-> ale ostatecznieprzeciążony operator musi zwrócić wskaźnik

• po co?– zwykły operator-> wymaga po lewej stronie wskaźnika– teraz można go zastosować również do typu abstrakcyjnego, aby wyglądał

„jak wskaźnik”, np. możemy „opakować” wskaźnik klasą, aby uczynić go bezpiecznym

– można przy okazji wykonać jakieś dodatkowe zadania

zwykłe wywołanie: wskaznik->składnik // chcemy: obiekt->skladnikpo przeciążeniu: (obiekt.operator->())->składnik

operator-> i operator*

• operator -> nie jest automatycznie generowany, istnieje za to dla typów wbudowanych, a takim jest wskaźnik do… (czegokolwiek)

• klasyczne zastosowanie to inteligentny wskaźnik

class A { public: void funA(); /* robi cokolwiek */ };class SmartPrt {

A *fp { nullptr };A* fTable[5] { nullptr };int fUse { 0 };public:

SmartPrt(A *prt = nullptr) : fp(prt) { }A* operator->() { fTable[fUse] = fp; fUse = (++fUse) % 5; return fp; }A& operator*() { return *(operator->()); }

};// gdzieś w programieA a;SmartPrt wsk = &a; // konstruktor SmartPtrwsk->funA(); // tu działa przeciążony operator ->// przy okazji zapisuje sobie w wewnętrznej tablicy fakt użycia(*wsk).funA(); // tak też powinno się dać, zawsze przeciążajmy parami// a może warto też dodać możliwość inkrementacji/dekrementacji?

operator->*

• cel: żeby obiekt typu abstrakcyjnego mógł być użyty tak samo jak „wskaźnik do składowej”

• dwuargumentowy (zastosowanie: inteligentny wskaźnik)

• a jest obiektem klasy A, zachowuje się jak wskaźnik, a za pomocą wskaźnika możemy się przecież chcieć dostać do składowych klasy, wskazywanych przez wskaźnik na składowe, czyli właśnie użyć ->*

• musi zwracać obiekt dla którego można wywołać funkcję operator(), któremu są przekazane argumenty, z jakimi wywołano operator->*

• operator() pobiera te argumenty i wyłuskuje prawdziwy wskaźnik do składowej

• a co z operatorem wyłuskania składowej za pomocą obiektu, czyli .*Przypomnienie: nie wolno go przeładować.

// gdzieś w programie …int tablica_danych[10]; A a; // w klasie A niech będzie składowa funkcja o nazwie calcAA::FunPointer fPointer = &A::calcA; // wskaźnik na składową (patrz następny slajd)(a->*fPointer)(tablica_danych); // tu cała akcja

operator->* (przykład)

• na zwróconym obiekcie, typu MemA, wołamy następnie operator() czyli ostatecznie poprzez wskaźnik wołamy odpowiednią funkcję, przekazując jej parametr dany jako parametr do operator() – w przykładzie powyżej jest nim int* w postaci nazwy tablicy int-ów

• wywołanie operatora ->* powoduje wywołanie funkcji operator() dla wartości zwracanej przez operator->* wraz z przekazaniem jej wszystkich argumentów podanych operatorowi ->*

class A {public:double calcA(int*) const;double calcA() const;double calcB(int*) const;typedef double (A::*FunPointer)(int*) const;class MemA {A* fpA; // na potrzeby operator() zapamiętamy wskaźnik do obiektu, którego używamyFunPointer fpmem; // na potrzeby operator() zapamiętamy którą funkcję wołamy za pomocą ->*public:MemA(A *a, FunPointer p) : fpA(a), fpmem(p) { }// operator() wywoła funkcję składową klasy A pokazywaną wskaźnikiemdouble operator()(int* pn) const { return (fpA->*fpmem)(pn); }

}; // koniec zagnieżdżonej klasy MemAMemA operator->*(FunPointer p) { return MemA(this, p); }

};

operator, (przecinkowy)

• nie jest wywoływany w przypadku listy argumentów funkcji• wywoływany dla obiektu o typie, dla którego został zdefiniowany

class Figlarz {public:// operator wywołany w sytuacji: obiekt typu Figlarz, obiekt typu doubleconst Figlarz& operator,(double) { cout<< " double za mną! "; return *this; }

}// funkcja globalna dla wersji z przecinkiem przed obiektem// wywołany w sytuacji: obiekty typu int, obiekt typu FiglarzFiglarz& operator,(int, Figlarz& f) {

cout << " int przede mną! ";return f;

}

int main() {Figlarz filutek;filutek, 3.14; // wypisze na ekranie: double za mną!5, filutek; // wypisze na ekranie: int przede mną!

}

operator<< i operator>> (wstawianie i wyjmowanie)

• operator<<wstawia obiekt do strumienia (klasa ostream)dla typów wbudowanych zachowanie zdefiniowane, np.

ostream& operator<<(int);• operator>> pobiera dane ze strumienia do obiektu (klasa istream)• dla typów abstrakcyjnych chcielibyśmy korzystać z operatorów strumieniowych

tak jak dla typów wbudowanych• spróbujmy zdefiniować jako składową klasy (tylko kawałek kodu):

• poprawne rozwiązanie: musimy zdeklarować funkcje operator<< oraz operator>>jako funkcje globalne, zaprzyjaźnione z daną klasą

• funkcje-przyjaciele nie otrzymują wskaźnika this z danego obiektumusimy im zatem przekazać adres tego obiektu jako argument

class MojaKlasa { int a, b; public:

ostream& operator<<(ostream& stream) const {stream << ”a = ” << a << ” b = ” << b;return stream;

}};// w programieMojaKlasa mojObiekt;cout << mojObiekt << endl; // błąd, to nie zadziała !mojObiekt << cout << endl; // to jest ok, ale składnia wygląda… dziwnie

operator<< i >> (zaprzyjaźniona funkcja globalna)

Dla zachowania poprawnej składni operator<< i operator>>musi być zaimplementowany jako funkcja niebędąca składową.Czy można rozwiązać to lepiej, np. bez deklaracji przyjaźni? Tak i to z dodatkowymi korzyściami!

class MojaKlasa {int a, b;

public:friend

ostream& operator<<(ostream& stream, const MojaKlasa& ref);friend

istream& operator>>(istream& stream, MojaKlasa& ref);};ostream& operator<<(ostream& stream, const MojaKlasa& ref) {

return stream << ”a = ” << ref.a << ” b = ” << ref.b;}istream& operator>>(istream& stream, MojaKlasa& ref) {

// implementacja dowolna, tutaj: wczytanie dwóch liczbreturn stream;

}

operator<< i >> (funkcja globalna i składowe)

Zalety rozwiązania z dodatkowymi składowymi funkcjami przekazującymi:• zewnętrznym operatorom << i >> nie dajemy dostępu do części prywatnej (chronionej)

klasy, czyli hermetyzacja danych pod lepszą kontrolą• jeśli funkcje WyDrukuj i OdCzytaj będą wirtualne, możliwe będzie dynamiczne wiązanie

(czyli zachowanie polimorficzne przy dziedziczeniu) – coś zupełnie niedostępnego w przypadku funkcji zaprzyjaźnionej

class MojaKlasa {int a, b;

public:ostream& WyDrukuj(ostream& stream) const; // robią to samo co operatoryistream& OdCzytaj(istream& stream) const;

};ostream& operator<<(ostream& stream, const MojaKlasa& ref) {

return ref.WyDrukuj(stream);}istream& operator>>(istream& stream, MojaKlasa& ref) {

return ref.OdCzytaj(stream);}

Trzy formy operatora new (C++98)

void* ::operator new(std::size_t size) throw(std::bad_alloc);to jest „zwykły” operator new, użycie w postaci new T, operator nie ma żadnych parametrów, przydziela pamięć, a w przypadku niepowodzenia zgłasza wyjątek (typu bad_alloc), można zdefiniować własną wersję tego operatora

void* ::operator new(std::size_t size, const std::nothrow_t&) throw();wersja niezgłaszająca wyjątków, używa się go w postaci new (std::nothrow) T, a jeśli przydział pamięci zakończy się niepowodzeniem to zwraca 0, również można zdefiniować własną wersję tego operatora

void* ::operator new(std::size_t size, void* ptr) throw();wersja operatora nazywana „placement new” (we wskazanym miejscu), użycie w postaci new (ptr) T, nie alokuje pamięci dla obiektu, tylko tworzy go we wskazanym przez wskaźnik ptr miejscu – np. uprzednio przygotowanym przez inną alokację pamięci, w związku z tym że pamięć nie jest przydzielana więc operacja taka nie może się zakończyć niepowodzeniem ani wyjątek też nie będzie rzucony, nie można napisać własnej wersji tego operatora

• to samo w wersji tablicowej – gdy tworzymy tablice, działa operator new[] W C++11 zmiana: throw() na noexcept ale nadal sprawdzane dynamicznie,ponadto deklarowanie typów zgłaszanych wyjątków ma być „przestarzałe”

operator new i delete (globalne)

• można zmienić sposób przydzielania pamięci:– ogromna ilość alokacji (poprawić efektywność)– radzenie sobie w przypadku defragmentacji sterty, limitowanych zasobów

• nie możemy zmienić faktu, że po przydzieleniu pamięci rusza konstruktor

przeciążenie globalne

#include <cstdio>#include <cstdlib>using namespace std;

void* operator new(size_t sz) { // zwraca adres początku pamięciprintf("alokujemy %d bajtow\n",sz); // nie można użyć iostream// ponieważ dla obiektów globalnych np. cout jest wołany operator newvoid* mem = malloc(sz);if (!mem) puts("brak zasobów");return mem; // jeśli 0 to nie ruszy konstruktor!

}

void operator delete(void* mem) {puts ("usuwamy alokację");free(mem);

}

operator new i delete (lokalne)

przeciążenie dla danego typu • statyczna funkcja składowa (niezależnie od tego czy zadeklarujemy static)

Zawsze tworzymy i używamy parę: new i delete

class A { // najsensowniej zrobić jakiś magazyn – banki pamięci i nimi zarządzać// pierwszy argument obligatoryjnie – rozmiar typu size_tvoid* operator new(size_t rozmiar) {

// można użyć iostream – dla nich wołane globalne wersje new / deletecout << "Alokujemy " << rozmiar << " obiektów char\n";return (::new char[rozmiar]); // char ma 1 bajt, zamiast sizeof „sami robimy”

}void operator delete(void *wsk) { // argument – adres

::delete wsk; }

};

// gdzieś w programieA *ptr = new A; // konstruktor domyślny, operator new przeciążonydelete ptr;ptr = ::new A; // teraz działa wersja globalna operatora new::delete ptr; // wersja globalna operatora delete

operatory new[] i delete[]

• dotyczy wersji operatorów dla alokowania (usuwania) tablic obiektów

można też wersje globalne new[] i delete[], tak jak poprzednio:• jeśli przeładowujesz globalny operator new / delete, to tracisz dostęp do wersji oficjalnych• przeładowanie globalne obowiązuje w całym programie, również podczas tworzenia

obiektów z bibliotek standardowych (np. cin, cout, cerr…)

Skąd delete[] wie jaki rozmiar ma tablica do skasowania? Odpowiedź: zależy od implementacji w danym kompilatorze (to nie jest w standardzie języka). Może byćnp. tak, że jest to kilka bajtów poprzedzających adres podany jako początek tablicy.

// przykład operatorów dla klasy T, pracujących na wersjach globalnych class T {

// gdzieś w części publicznejvoid* operator new[](size_t sz) {

return ::new char[sz];}void operator delete[](void* m) {

::delete [] m;}

};

Gdy kompilator napotyka wywołanieoperatora new, faktycznie wywołanyzostaje T::operator new( sizeof( typ ) )a w przypadku niezdefiniowania wywołany jest globalny::operator new( sizeof( typ ) )

operator new i delete („ze wskazaniem”)

• wersje przeciążone operatora new mogą mieć więcej niż jeden argument• możemy umieścić obiekt w miejscu wcześniej dla niego przygotowanym

(np. sloty pamięci o odpowiedniej wielkości, których adresy znamy)

Operator new „z umieszczeniem” jest faktycznie tak wołany, np.T *ptr = new ( 0x0040 ) T;T *ptr = T::operator new( sizeof( T ), 0x0040 ); // lub:T *ptr = ::operator new( sizeof( T ), 0x0040 );

T *ptr = new(addr) T;// addr jest przekazany jako drugi parametr operatora new// pierwszym (tu niejawnym) jest rozmiar obiektuclass T { // gdzieś w środku klasy…

void* operator new(size_t, void* loc) {return loc; /* nic nie alokujemy, wskazujemy tylko miejsce! */ }

};// operator delete jest tylko jeden, więc usuwając obiekt z jakiegoś slotu, umieszczony tam // za pomocą powyższego operatora new musimy jawnie wywołać jego destruktorT *ptr = new(addr) T;ptr->T::~T(); // wolno tylko w takim przypadku, w którym niszcząc obiekt na stercie

// tak naprawdę nie chcemy zwrócić pamięci tylko usunąć samo istnienie obiektu

dziedziczenie [ inheritance ]

• technika definiowania nowej klasy z wykorzystaniem już istniejącej• klucz do tworzenia relacji dziedziczenia to określenie wspólnego zachowania klas• nie potrzebujemy kodu źródłowego, tylko plik nagłówkowy – możemy np. dziedziczyć z klas

bibliotecznych (które potem linkujemy)

class B : public A { /* ... */ };lista pochodzenia

A – klasa podstawowa (bazowa)B – klasa pochodna klasy A

klasa pochodna• dziedziczy wszystkie składniki klasy podstawowej (atrybuty i zachowanie)• można w niej zdefiniować

– dodatkowe dane składowe– dodatkowe funkcje składowe

• można w niej przedefiniować– składniki / funkcje już istniejące w klasie podstawowej– redefiniowany składnik zasłania składnik z klasy podstawowej

• relacja: jest – czymś• relacja: uogólnienie – uszczegółowienie

( klasa bazowa – klasa pochodna )• klasa pochodna może

– rozszerzać możliwości klasy bazowej (implementacja nowych metod)– uściślać (ponowna implementacja metod istniejących w klasie bazowej)

Klasa pochodna zawsze może być traktowanajako klasa bazowa (w dziedziczeniu publicznym), oznacza to, że:

– można wskaźnikiem (referencją) klasy bazowej pokazywać na obiekty klaspochodnych i nie jest to operacja powodująca utratę części wskazywanegoobiektu

– dziedziczenie prywatne nie jest prawdziwym dziedziczeniem

relacja dziedziczenia – znaczenie

klasa bazowa A klasa pochodna Bprivateprotected protectedpublic public

klasa bazowa A klasa pochodna Bprivateprotected protectedpublic

klasa bazowa A klasa pochodna Bprivate privateprotectedpublic

• dostęp do części prywatnej klasy bazowej A tylko przez jej interfejs

• mamy dostęp do części public i protectedz tym że protected na zewnątrz niedostępny(tak samo jak private)

public

protected

private • domyślny, niepodanie specyfikacjioznacza dziedziczenie privateclass B : A { /* ... */ };

• stosujemy gdy chcemy ukryć fakt dziedziczenia

dziedziczenie implementacji

dziedziczenie interfejsu

sposoby dziedziczenia (public, protected, private)

• umożliwia selektywne zachowanie sposobu dziedziczenia składowych• należy umieścić w wybranej części klasy pochodnej

using klasa_podstawowa::nazwa_skladnika;można również według starego przepisu (bez słowa using)

klasa_podstawowa::nazwa_skladnika;• za pomocą using można zachować (powtórzyć) zakres dostępu

z klasy bazowej lub zmienić z protected na public (i vice versa)class A {// niedostępne w klasie pochodnejint n; void getVal(int);

protected:int k;int calc();

public:int calc(int);void getVal();

};

class B : private A {protected:using A::k;using A::calc; // nie rozróżnia nazw przeciążonych

public:using A::getVal; // nie zadziała bo getVal jest też

}; // w części private• deklaracja dostępu nie może posłużyć do odsłonięcia

nazwy zasłoniętej w klasie pochodnej, również w przypadku redefinicji funkcji (wirtualnej)

• nie usuwa ew. wieloznaczności w dziedziczeniuwielokrotnym (najpierw zawsze jest rozstrzyganawieloznaczność)

deklaracja dostępu (using)

• klasa B jest dla klasy C klasą podstawową bezpośrednią, zaśklasa A – klasą podstawową pośrednią

• inicjalizowanie klasy podstawowej poprzez wywołanie jejkonstruktora

C::C(int i, float f) : B(i,f) { // ...B::B(int i, float f) : A(i) { // ...

lista inicjatorów konstruktora• wywołujemy tylko konstruktor bezpośredniej klasy podstawowej• jeśli tego nie zrobimy, użyty będzie konstruktor domyślny, kolejność

jest “od góry” (klasa A), “do dołu” (klasa C)• gwarantowane jest też wywołanie destruktorów, w kolejności

odwrotnej (czyli od C do A)

A

B

C

dziedziczenie kilkupokoleniowe i inicjalizacja

// wcześniej definiujemy klasy: MW, MX, MY, MZ oraz klasę Aclass B : public A {

MY my;MX mx;

public:B(int i) : mx(), my(), A() { /*...*/ }~B();

};class C : public B {

MW mw;MZ mz;

public:C() : mw(3.14), B(45) { /*...*/ }~C();

};

• kolejność wywołania konstruktorówelementów składowych jest związanaz kolejnością ich wystąpieniaw definicji klasy, a nie z kolejnościąna liście inicjatorów

• w przykładzie po lewej, kolejność konstrukcji:A, MY, MX, B, MW, MZ, C

• kolejność destrukcjijest dokładnie odwrotna:C, MZ, MW, B, MX, MY, A

kompozycja i dziedziczenie

• przedefiniowanie (redefining) w przypadkuzwykłych funkcji składowych klasy bazowej

• zasłanianie (overriding) w przypadku funkcji wirtualnych klasy bazowejclass A { public:int fun() const;int fun(float) const;

};class B : public A { public:int fun() const; // przedefiniowanie

};class C : public A { public:void fun() const; // zmiana zwracanego typu

};class D : public A { public:int fun(char*) const; // zmiana listy argumentów

};

we wszystkich przypadkachniewidoczne (zasłonięte) stają się również funkcjeprzeciążone w klasie bazowej,tzn. tutaj: int fun(float) const;

gdyby w klasie A była metoda prywatna, to dostępu do niej nie mamy w klasach pochodnych, ale możemy ją przedefiniować !!! tak, że będzie działać nasza nowa wersja, tak jakby była tą funkcją składową z części prywatnej A

ukrywanie nazw w klasach pochodnych

class A { public:A(const A& a);

};class B : public A { public:B(const B& b) : A(b) { /* ... */ }

};

• konstruktory (patrz C++11)

• operator=• destruktorKONSTRUKTOR KOPIUJĄCY• ten generowany automatycznie wykorzysta konstruktory kopiujące klas-przodków i

składników– chyba że któryś z tych konstruktorów kopiujących jest prywatny– uwaga: definicja jakiegokolwiek konstruktora (np. właśnie kopiującego) wyklucza

automatyczne generowanie zwykłego konstruktora• definiowany przez nas może je wywołać

• trzeba je zdefiniować samemu(lub zostaną wygenerowane automatycznie!)

• można jednak we własnych definicjach wywołać wersjez klas podstawowych do obsłużenia odziedziczonej części obiektu

jawne wywołanie konstruktorakopiującego klasy A, inaczej zostałbywywołany zwykły konstruktordomyślny klasy A

czego się nie dziedziczy (C++98)

dziedziczenie konstruktorów ( C++11 )

Deklaracja using może być użyta z konstruktorami klasy bazowej

Dziedziczone konstruktory zachowują swoją specyfikację (tzn. są np. explicit lub są wyrażeniem stałym constexpr).

class Foo { public:explicit Foo(int); // explicit jako przykład „dobrego stylu”void fun();

};class Bar : public Foo { public:

using Foo::fun; // tu nic nowego, w zasadzie niepotrzebneusing Foo::Foo; // powoduje niejawną deklarację Bar::Bar(int); // taki konstruktor zdefiniowany/wygenerowany tylko w przypadku użyciavoid fun(); // nadpisuje Foo::fun() Bar( int, int ); // tu już samemu napisany konstruktor, bez dziedziczenia

};Bar b1( 7 ); // ok w C++11 dzięki dziedziczeniu konstruktoraBar b2( 3, 5 ); // normalne wywołanie Bar::Bar(int, int);

dziedziczenie konstruktorów – dostępność, inicjalizacja składników ( C++11 )

Może się okazać, że odziedzczony konstruktor jest prywatny

Jeśli klasa potomna ma jeszcze jakieś składowe, to użycie dziedziczonego konstruktora jest ryzykowne. Składowe klasy Bar będą albo domyślnie inicjalizowane (s) albo niezainicjalizowane (x, y).Oczywiście można:

class Foo { private:explicit Foo(int);

};class Bar : public Foo { public:

using Foo::Foo; private:

string s;int x, y;

};Bar b1( 7 ); // błąd – woła Bar(int), który woła Foo(int), a ten jest niedostępny

błąd objawia sięw momencie próby użycia

string s = ”niezainicjalizowany”;int x = 0, y = 0;

OPERATOR PRZYPISANIA operator=• ten generowany automatycznie wywoła operatory= klasy-przodka i

składników– chyba, że któryś z tych operatorów jest prywatny– chyba, że są składniki const lub składniki będące referencją – bo te

wymagają inicjalizacji• definiując operator= możemy je użyć

class A { public:A& operator=(const A& a);

};class B : public A { public:

B& operator=(const B& b) {A::operator=(b);// ...return *this; }

};

musi być podany zakres ( A:: )ponieważ nowodefiniowanyB::operator= przesłania funkcjęoperatora klasy bazowej

alternatywnie mozna tak:(*this).A::operator=(b);lubA *wsk = this; // możemy wskaźnikiem klasy bazowej(*wsk) = b; // pokazać na obiekt pochodnylubA &ref = *this; // możemy referencji do klasy bazowejref = b; // przypisać obiekt klasy pochodnej

czego się nie dziedziczy

• składniki statyczne i oczywiście definiujemy je dla klasyw której są zdeklarowane– możemy je zasłaniać

class A { public:static int ca;static int getNew() { return ca; }

};class B : public A { public:

static int ca;static int getNew() { return ca; } // zasłania funkcję z klasy Astatic int getOld() { return A::ca; } // tak możemy się dostać do “starej” wartości

};int A::ca = 2;int B::ca = 5; // z powodu re-deklaracji w klasie B, musimy zdefiniować

• statyczne funkcje składowe– gdy przedefiniowane – zasłaniają funkcje z klasy podstawowej (wszystkie

przeciążone wersje), również wtedy gdy następuje zmiana sygnaturyfunkcji

co jest dziedziczone i warto wspomnieć

• operatory konwersji typów – bo w klasach pochodnych mamy kompletinformacji do wykonania konwersji

• konstruktory konwersji nie są dziedziczone, ale…

class C { public:C(int n) : c(n) {}int c;

};

class A { public:A(int n) : a(n) {}A (const C& c) : a(c.c) {}int a;

};

void fun(const A& a) { cout << "a.a = " << a.a << endl; }void fun2(const B& b) { cout << "b.a = " << b.a << endl; }

int main() {C c(11);D d(22);A a(33);B b(44);fun(c); // normalnie, wypisze 11 – konwersja typufun(b); // co wypisze? 44 czy 46?fun(d); // co wypisze? 22 czy 25? fun2(c); // błąd – bo konstr. konwersji się nie dziedziczy

}

obiekt klasy B pokazywany referencją do klasy bazowej Ajest widziany jako obiekt klasy A,więc wypisana jest częśćobiektu z klasy A (tu zasłoniętaw klasie B)

class D : public C { public:D(int n) : C(n+3), c(n) {}int c;

};

class B : public A { public:B(int n) : A(n+2), a(n) {}int a;

};

obiekt klasy D jest również obiektem typu klasy C, więc możliwa jest konwersja obiektutypu D na obiekt typu A, wypisanajest ta część obiektu z klasy C(tu zasłonięta w klasie D)

co jeszcze jest dziedziczone

• jest bezpieczne bo od typu bardziej wyspecjalizowanego przechodzimy do typu bardziej ogólnego

• jest naturalne: wskaźnikiem (referencją) typu bazowego pokazujemy na typ pochodny

class A { public: int a; };class B : public A { public: int b; };// gdzieś w programie…B b;A *wskA = &b;A &refA = b;

– poprzez wskA i refA oczywiście nie mamy dostępu do części zdefiniowanej w klasie B (tzn. int b), ale np. poprzez jawne rzutowanie (w dół !) można się tam dostać

• co jeśli przez wartość?A a = b;

– to też dopuszczalne, ale następuje nieodwracalna strata części obiektu klasy B (tu zadziała konstruktor kopiujący z klasy A, który nic nie wie o dodatkowej części z klasy B)

rzutowanie w górę (upcasting) i w dół

class A { public: void getMe() { cout << "Jestem A/n"; }

};class B : public A { public:

void getMe() { cout << "Jestem B/n"; } };class C : public B { public:

void getMe() { cout << "Jestem C/n"; } };// …gdzieś w programieB b;C c;A *ptrA = &b;A &refA = c;A a = b;ptrA->getMe(); // "Jestem A"refA.getMe(); // "Jestem A"a.getMe(); // "Jestem A"

to nas nie zadowala, boprzecież pokazywane sąobiekty klas pochodnych

chcielibyśmy, żeby wskaźnik(referencja) inteligentniereagowały na typ obiektuna który pokazują, wołającjego funkcję…

polimorfizm – czego oczekujemy?

class A { public: virtual void getMe() { cout << "Jestem A/n"; }

};class B : public A { public:

void getMe() { cout << "Jestem B/n"; } };class C : public B { public:

void getMe() { cout << "Jestem C/n"; } };// …gdzieś w programieB b;C c;A *ptrA = &b;A &refA = c;A a = b;ptrA->getMe(); // "Jestem B"refA.getMe(); // "Jestem C"a.getMe(); // "Jestem A" – nieodwracalne "przycięcie" do A

w klasie bazowej (tutaj klasie A)

musimy w deklaracjifunkcji dodać

virtual

funkcja getMe() jest wirtualna w każdejklasie pochodnej, można (ale nie trzebabo jest to mylące) dopisać "virtual"również w klasie B i C…

• ściśle rzecz biorąc polimorficzne jestwywołanie funkcji, a nie funkcja• klasa, w której jest zdefiniowana lub odziedziczona funkcja wirtualna,nazywa się klasą polimorficzną

polimorfizm - rozwiązanie

• wiązanie (binding) – połączenie wywołania funkcji z jej ciałem– wczesne wiązanie (early binding) – gdy wykonane podczas kompilacji– późne wiązanie (late binding) – gdy wykonane w trakcie wykonywania

programu na podstawie informacji o typie• polimorfizm wymaga późnego wiązania, wiąże się to z dodatkowymi kosztami

– gdy występuje funkcja wirtualna obiekt klasy jest większy o rozmiar wskaźnikado specjalnej tablicy – niezależnie od liczby funkcji wirtualnych (tablica ta nie wchodzi w skład klasy)

– tylko w tej klasie, w której pojawia się po raz pierwszy deklaracja danej funkcji wirtualnej, tworzona jest tablica do wszystkich funkcji wirtualnych o tej nazwie

– podejmowanie decyzji którą funkcję wykonać w trakcie wykonywania programu, również stanowi narzut

• tam gdzie wywołanie funkcji wirtualnej, podczas kompilowania, wstawiony kod uruchomieniowy funkcji pokazywanej przez wskaźnik schowany w którymś elemencie tablicy

• tablica wskaźników do funkcji zawierająca adresy wszystkich kolejnych wersji funkcji wirtualnej budowana dla klasy podstawowej w chwili linkowania

wiązanie – koszty

• polimorfizm działa jeśli funkcję typu virtual wywołujemy przez wskaźnik lub referencję do obiektu klasy podstawowej

• nowa definicja funkcji wirtualnej (w klasie pochodnej) musi być identyczna co do listy argumentów, zaś zwracany typ musi być identyczny lub…

• …zwracany typ może być kowariantny (covariant)wymagania – zwracany typ to wskaźnik lub referencja

– wtedy można zmienić obiekt zwracany przez funkcję wirtualną w klasie pochodnej na wskaźnik (referencję) do obiektu klasy pochodnej jednoznacznie wskazującej na klasę bazową, do której wskaźnik (referencję) zwraca funkcja wirtualna w klasie bazowej

class Partia { public:virtual Polityk* getPolityk()

{ cout << "Teraz ja!" << endl; }virtual Przewodniczący& getPrzewodniczacy()

{ cout << "RULEZ!" << endl; }};class Dlugopis : public Partia { public:

DzialaczDlugopis* getPolityk() { cout << "Beee…" << endl; }

PrezesDlugopis& getPrzewodniczacy() { cout << "Chcesz w papę?" << endl; }

};

// …gdzieś w programiePartia *ptrP = new Dlugopis;ptrP->getPolityk(); // "Beee…"ptrP->getPrzewodniczacy(); // Chcesz w papę?

Polityk

DzialaczDlugopis

Przewodniczacy

PrezesDlugopis

funkcje wirtualne – co można zmienic?

• funkcja globalna nie może być wirtualna (bo przecież polimorficzne orientowanie ze względu na typ obiektu…)

• funkcja wirtualna nie może być statyczna• funkcja wirtualna może być przyjacielem jakiejś innej klasy, ale tylko konkretna realizacja funkcji wirtualnej

z danej klasy jest tym przyjacielem (a nie wszystkie funkcje) bo przyjaźni się nie dziedziczy• w klasie pochodnej można zasłonić funkcję wirtualną z klasy bazowej (definicja obiektu lub innej funkcji o tej

samej nazwie), ale w kolejnej klasie pochodnej (do klasy pochodnej) można ją znów zdefiniować i korzystać z polimorfizmu

• jeśli zmienia się zakres dostępu dla funkcji wirtualnej, np. w klasie bazowej funkcja ta była public, a w klasie pochodnej jest protected lub private

sposób dostępu taki jak w typie użytego wskaźnika lub referencjiclass A { public:

virtual void f() { cout << "Jestem A" << endl; }};class B : public A { private:

void f() { cout << "Jestem B" << endl; }};int main(){

A *ptrA = new B;ptrA->f(); // "Jestem B"B &refB = dynamic_cast<B&>(*ptrA);refB.f(); // błąd - virtual void B::f() is private

}

dostęp rozstrzyganyna poziomie wiedzy wyniesionej z klasybazowej A, bo pokazujemywskaźnikiem klasy bazowej

funkcje wirtualne – kilka szczegółów

• konstruktory nie są dziedziczone (C++98), nie mogą być wirtualne– żeby zadziałał polimorfizm to musi być pokazywany obiekt

danego typu (wskaźnikiem, referencją), a tego obiektu "jeszcze nie ma", jest konstruowany

• destruktor – nie jest dziedziczony, ale tak!Jeśli klasa posiada choć jedną deklarację funkcji jako virtual, jej destruktor też deklarujmy jako virtual

– wtedy destruktory klas pochodnych też będą virtual– działać będzie polimorfizm i poprawna destrukcja obiektu

konstruktor, destruktor – wirtualny

• wskaźnikiem klasy bazowej można pokazać na obiekt klasy pochodnej, ale uwaga na tablicevoid fun(A* ptr) { cout << (ptr+1)->n << endl; }class A { public: int n; };class B : public A { public: int k; };B tabB[5];// konwersja zajdzie, ale w środku funkcji wskaźnik // będzie poruszał się tak jak po obiekcie typu A !fun(tabB); // potencjalna katastrofa wewnątrz funkcji// klasa B jest rodzajem klasy A, ale // tablica klasy B nie jest rodzajem tablicy klasy A

• w przypadku wskaźnik do wskaźnika konwersja nie zajdzie (na nasze szczęście)void fun(A** ptrA);B **ptrB;fun(ptrB); // błąd

• wskaźniki na składowe klasy bazowej może zostać niejawnie zamieniony na wskaźniki na składowe klasy pochodnejint A::*calkowity = &A::n;B b;b.n = 77;cout << b.*calkowity << endl; // wypisze "77"int B::*natural = &B::n;A a;a.n = 88;// błąd, w drugą stronę nie ma niejawnej konwersji, trzeba ją samemu napisaćcout << a.*natural << endl;

dziedziczenie – a konwersje

• tworzona po to, aby być klasą bazową do dziedziczenia• będziemy korzystać z polimorfizmu (virtual)• implementacja metod niepotrzebna, deklaracja interfejsu

virtual void funkcja() = 0; // czysto wirtualna– ta wersja funkcji nigdy nie ma być wykonana, konieczność implementacji

(uściślenia) w klasie pochodnej– klasa jest abstrakcyjna gdy ma choć jedną funkcję wirtualną– dziedziczona jako czysto wirtualna, więc jeśli nie ma jej definicji w klasie

pochodnej, klasa pochodna też jest klasą abstrakcyjną– nie można stworzyć żadnego obiektu klasy abstrakcyjnej– funkcja nie może zwracać przez wartość obiektu klasy abstrakcyjnej– nie może być typem w jawnej konwersji

FUNKCJE WIRTUALNE i ich ciałavirtual void funkcja() { } // zwykła, musi mieć definicjęvirtual void funkcja() = 0; // pure virtual, bez definicji ► może mieć definicję, umieszcza się ją poza ciałem klasy→ taką funkcję można wywołać tylko wprost (z operatorem zakresu) czyli klasa::funkcja() lub

z wnętrza konstruktora (destruktora) klasy, w której jest ona czysto wirtualna→ niezdefiniowanie ciała funkcji "pure virtual" w którejś z kolejnych klas pochodnych, czyni z tej

klasy pochodnej znowu klasę abstrakcyjną

dziedziczenie – klasa abstrakcyjna

inicjalizacja dla wszystkich ( C++11 )

Domyślne wartości inicjujące składowe (niestatyczne):

Wartość nadana przez konstruktor nadpisuje powyższą inicjalizację:

Uwaga: napisanie domyślnej wartości inicjującej powoduje, że dany typ przestaje być agregatem – nie można go (bez napisania konstruktora) zainicjalizować tak jak można było agregat:

struct Bar {int m = 7; // gdyby nie wartość domyślna, ujednolicona inicjalizacja nie byłaby błędem

};Bar b { 5 }; // błąd – próba wywołania konstruktora z int, ale takiego nie ma

class Foo { int m = 3; // dopuszczalna również składnia int m { 3 };int n { 3*m }; // może zależeć od innych zmiennychstring s = getString(); // string s = { getString() }; tu znak = opcjonalnie

}; Foo w; // w.m zainicjalizowane 3, w.n 9, w.s tym co zwróci getString()

explicit Foo(int a) : m(a) {} // teraz m ma warość a, reszta jak powyżej

różne rodzaje inicjalizacji ( C++98 )

Wiele różnych sposobów inicjalizacji:

Tablice:

Proste struktury i klasy:

int a(7); // inicjalizacja bezpośredniaconst int b = 7; // inicjalizacja „kopiująca” – to nie jest przypisanie

class Para { public:

Para(int m, int n); // konstruktor};const Para p(5, 10); // inicjalizacja– wywołanie funkcji (konstruktora)Para p2 = Para(1,2); // składnia w stylu wołanie funkcji / konwersja

int tablica[] = { 1, 2, 3, 4 }; // inicjalizacja za pomocą nawiasów

struct Dane { int a, b; };const Dane d = { 5, 10 }; // inicjalizacja za pomocą nawiasów

rodzaje inicjalizacji i problemy ( C++98 )

Kontenery wymagają innych kontenerów:

Problem – nie można zainicjalizować tablic składowych klasy ani tablic tworzonych dynamicznie (na stercie):

int tab[] = { 2, 4, 6, -3, -7 }; // najpierw zwykła tablicastd::list<int> lista( tab, tab+5 ); // inicjalizacja z innego kontenera

class Tablica { const int dane[10];

public:Tablica() : /* nie da się nic zrobić z dane */ {}

};

const double* ptr = new const double[7]; // inicjalizacja niemożliwaclass Foo { // …

Foo(int n); // brak konstruktora domyślnego};Foo *ptab = new Foo[5]; // błąd! musi istnieć konstruktor domyślny!

Kłopotliwa składnia:int a(1); // definicja zmiennejint a(); // deklaracja funkcji!int a(Foo); // może być jedno// lub drugie – zależy od Foo

uogólniona (jednolita) inicjalizacja ( C++11 )

Zunifikowany sposób inicjalizowania za pomocą składni z nawiasami { }

Teraz można zainicjalizować tablice składowe klasy na liście inicjalizatorówa także tablice tworzone dynamicznie (typu prostego i abstrakcyjnego):

int a { 7 };const double b { 3.14 }; int tab[] { 0, 4, a, a+b };const Para p { 5, 10 }; // wywołanie konstruktorastd::vector<int> wektor { a, tab[3], b };

const double* ptr = new const double[3] { b, tab[3], 3.14 };class Tablica {

const int dane[2];public:

Tablica() : dane { 1.5, tab[1] } {} // konstruktor// można dodatkowe nawiasy Tablica() : dane ( { 1.5, tab[1] } ) {}

};Foo *ptab = new Foo[3] { 1, Foo(2), tab[1] }; // musi być c-tor Foo(int)

inicjalizacja – obiekt zwracany lub argument ( C++11 )

Składnia z nawiasami { } użyta w miejscu zwracania obiektu

Użycie jako listy argumentów:

agregat – tablica lub klasa bez (napisanych przez użytkownika) konstruktorów, bez (domyślnych) inicjalizacji niestatycznych składowych, bez prywatnych (bez chronionych) niestatycznych składowych, bez dziedziczenia, bez funkcji wirtualnych

Inicjalizacja agregatu – inicjalizacja składowych „od początku do końca”Inicjalizacja nie-agregatu – wywołanie konstruktoraSzczególny przypadek – unie:

Para utworzPare() { return { 0, 0 }; } // woła konstruktor klasy Para

union Unia { int a, double* p, char* c }; // definicja uniiUnia u1 = { 5 }; // tylko pierwszy element unii może być inicjalizowanyUnia u2 = { 3, 0x0 }; Unia u3 = { "tekst" }; // błąd w obu przypadkach

void fun( std::list<int>& lista ); // deklaracja funkcjifun( { a, tab[0], 3, 5 } ); // wywołanie z argumentem

inicjalizacja agregatów – szczegóły ( C++11 )

Zbyt wiele argumentów – błąd!Mniej argumentów – pozostałe obiekty inicjalizowane wartością:• typy wbudowane inicjalizowane 0• typy użytkownika (z konstruktorami) – domyślnymi konstruktorami• typy użytkownika (bez konstruktorów) – składowe inicjalizowane

wartością

Kontener std::array (tablica określonego rozmiaru) jest także agregatem:

double fun(); // ta funkcja zwraca double i za chwilę będzie użytastd::array< double, 3 > tablica = { 1, fun(), 2, 3, 5 }; // błąd// także tu za dużo inicjalizatorów

struct Dane { int a, b; }; // jak poprzednioconst Dane d = { 5 }; // to samo co { 5, 0 }const Dane d2 = { 5, 7, 9 }; // błąd – za dużo inicjalizatorów

inicjalizacja agregatów – szczegóły 2 ( C++11 )

Agregaty mogą być zagnieżdżone:

Składniki statyczne oraz anonimowe pola bitowe nie inicjalizowane:struct A {

int i;static int s;int j;int :17; // nieużywane (bo anonimowe)int k;

} a = { 1, 2, 3}; // zainicjalizowane są a.i, a.j, a.k

struct A {int x;struct B {

int i;int j;

} b;} a = { 1, { 2, 3 } };

przykład utworzenia obiektówod razu po definicji obiektów orazinicjalizacja za pomocą listy { }

inicjalizacja agregatów – szczegóły 3 ( C++11 )

Zagnieżdżony agregat bez konieczności inicjalizacji:struct S { } s; // nie posiada danych do inicjalizacjistruct A {

S s1;int i1;S s2;int i2;S s3;int i3;

} a = { { }, // nie można pominąć inicjalizacji (pustej)0, s,0

};

przykład utworzenia obiektówod razu po definicji obiektów orazinicjalizacja za pomocą listy { }

inicjalizacja nie-agregatów – szczegóły ( C++11 )

Tyle argumentów ile wymaga konstruktor!

Inicjalizacja działa również w przypadku kontenerów:

std::vector< int > w { 1, 2, a, 3, b }; // woła konstruktor wektorastd::map< std::string, long > ksiazka { { "Jan", 234567 },

{ "Anna", 456789 } }; // powyżej woła konstruktor mapystd::unordered_set< float > s { 0, 1.7, 5 }; // woła konstruktor

// unordered_set

// klasa Para jak poprzednio, ma konstruktor Para(int, int);const Para p1 { 5 }; // błąd – za mało argumentów konstruktora// A jeśli jeden z argumentów byłby domyślny?// Para( int, int = 3 );Para p11 { 5 }; // wtedy ok – równoważne z { 5, 3 }const Para p2 { 5, 7, 9 }; // błąd – za dużo argumentów

inicjalizacja – składnia z użyciem = ( C++11 )

Prawie zawsze można użyć składnię ze znakiem przypisania =

Są sytuacje, kiedy taka składnia nie działa (wszystko poniżej – błąd):const double* ptr = new const double[3] = { b, tab[3], 3.14 };class Tablica { const int dane[2];

public:Tablica() : dane = { 1.5, tab[1] } {}

};Para utworzPare() { return = { 0, 0 }; }void fun( std::list<int>& lista ); // deklaracja funkcjifun( = { a, tab[0], 3, 5 } );

int a = { 7 };const double b = { 3.14 }; int tab[] = { 0, 4, a, a+b };const Para p = { 5, 10 }; // wywołanie konstruktorastd::vector<int> wektor = { a, tab[3], b };

inicjalizacja – składnia z użyciem = bez explicit ( C++11 )

W składni z przypisaniem = nie można wołać konstruktorów „explicit”

Inicjalizacja za pomocą { } traktowana jest jako rozszerzenie możliwości języka C++. Są jednak nieliczne sytuacje, w których dotychczasowy kod będzie wymagał poprawek –wiąże się to z innym użyciem pary nawiasów – w celu zapobieżenia zaokrąglaniu (niejawnej obcinającej – stratnej konwersji) tzw. narrowing.

class Liczba {public:

explicit Liczba( int );};Liczba k1( 7 ); // ok, jawne wywołanie konstruktoraLiczba k2{ 7 }; // ok, jak wyżejLiczba k3 = 7; // błąd – składnia kopiująca: explicit c-tor nie działaLiczba k4 = { 7 }; // błąd – jak wyżej

wniosek: przyzwyczaić się raczejdo składni bez użycia znaku =

{ } w kontekście przypisania = ( C++11 )

Również w przypadku przypisania może być użyta składnia z { }int a, b;a = b = { 1 }; // oznacza a = b = 1;a = { 1 } = b; // błąd składniowyT x; // skalarny obiekt typu Tx = { a }; // tożsame z: x = T(a);x = {}; // tożsame z: x = T();

// również w przypadku operator= zdefiniowanego przez użytkownikacomplex<double> z;z = { 1, 2 }; // z.operator=( { 1, 2} );z += { 1, 2 }; // z.operator+=( { 1,2 } );

stratna (zawężająca) konwersja a sprawa inicjalizacji ( C++11 )

W języku C++ mogą się zdarzyć sytuacje stratnej konwersji:

Składnia z użyciem { } zapobiega stratnej (zawężającej) konwersji:int i { 3.5 }; // błąddouble f { i }; // też błąd – double nie reprezentuje dokładnie intunsigned u { i }; // unsigned nie reprezentuje całego intunsigned u { 34 }; // ok, kompilator wie, że unsigned może mieć 34struct Dane { int a, b; }; // jak poprzednioDane d = { 5, 7.23 }; // ok w C++98, błąd w C++11Dane d2 { 5, static_cast<int>(7.23) }; // zawsze ok

int a = 7.3;char c = 2011;int tab[] = { 1, 2, 3.14, 4, 5 };void fun1(int); fun1( 7.3 ); // bez ostrzeżeniavoid fun2(char); fun2( 2011 ); // warningvoid fun3(int[]); fun3( { 1, 2, 3, 4, 5 } ); // błąd: // niemożliwa konwersja z <brace-enclosed initializer list> do int*

stratna konwersja i różnice w inicjalizacji ( C++11 )

Ta nowa funkcja { } (blokowanie konwersji zawężającej) implikuje drobne różnice w inicjalizacji:

class Liczba { public:

Liczba(unsigned n); // konstruktor};int m;Liczba k1(m); // ok – niejawna konwersja int → unsignedLiczba k2 {m}; // błąd – bo konwersja int → unsigned zawężającaunsigned u;Liczba k3(u); // okLiczba k4 {u}; // ok – to samo co wyżej

listy inicjalizujące ( C++11 )

Typ std::initializer_list dzięki któremu możliwy jest omawiany właśnie mechanizm inicjalizacji / wstawiania:

• każda funkcja może używać listy inicjującej – także jako parametru• lista { } może konwertować na obiekt typu std::initializer_list

• w typie std::initializer_list dostępne są metody:

vector<int> v {}; // inicjalizacjav.insert( v.end(), { 3, 4, -5, -6 } ); // wstawienie wielu elementówv = { 1, 2, 3 }; // zamiana wartości w wektorze

size_t size() const; // liczba elementów w tablicyconst T* begin() const; // pierwszy elementconst T* end() const; // adres zaraz za ostatnim elementem

auto x = { 1, 2, 3 }; // x jest typu initializer_list<int>auto y = { 1 }; // y też jest typu initializer_list<int>, zmiana w C++17 na intauto z = { 1, 3.14 }; // błąd – nie można wydedukować jednego typu

listy inicjalizujące – praktyczny przykład ( C++11 )

std::initializer_list bardzo wygodne jako argument funkcji, o możliwej zmiennej liczbie argumentów (konkretnego typu):

#include <initializer_list> // potrzebny nagłówek + inne…string getName(int id); // jakaś funkcja zamieniająca int na stringclass Nazwy { public:

Nazwy( std::initializer_list<int> n ) { // konstruktornazwy.reserve( n.size() ); // możemy zarezerwować miejsce// pętla przebiegająca w całości po kontenerze (tu tablicy n)for( auto idx : n ) nazwy.push_back( getName(idx) );

}private:

vector<string> nazwy;};

// … gdzieś w programie …Nazwy m { 3, 5, 8, x, t[0] + 2 }; // wartości włożone do tablicy

Mogą być też inne argumenty:Nazwy( double x, const string& s, initializer_list<int> n );// … w programie …Nazwy m2 { 3.14, "slowo", { 1,2,3 } };// to samo co:Nazwy m2( 3.14, "slowo", initializer_list<int>({1,2,3}) );

initializer_list – rozstrzyganie przeciążenia ( C++11 )

Spośród konstruktorów w przypadku inicjalizacji przez{ } preferowany jest zawsze konstruktor z typem initializer_list:

Jeżeli dopasowanie wymaga zawężenia, wywołanie jest błędne:

class Liczby { public:Liczby(double d1, double d2); // c-tor 1Liczby(std::initializer_list<double> w); // c-tor 2

};double x, y;Liczby w1 { x, y }; // woła c-tor 2// w przypadku składni funkcyjnej – preferowany inny możliwyLiczby w2( x, y ); // woła c-tor 1

class Liczby { public:Liczby( std::initializer_list<int> );Liczby( int, int, int ); // ten c-tor nie jest brany w ogóle pod uwagę gdy argumenty { }

};Liczby w3 { 1, 3.0, 5 }; // błąd – zawężająca konwersja double → int

initializer_list – rozstrzyganie przeciążeń ( C++11 )

Kryteria dobierania kandydata na przykładzie:

class Liczby { public:Liczby(std::initializer_list<int> w); // c-tor 1Liczby(std::initializer_list<double> w); // c-tor 2Liczby(std::initializer_list<string> w); // c-tor 3Liczby(int, int , int); // jeśli argumenty jako { } to nie brany pod uwagę!

};Liczby w1 { 1, 3.14, 5 }; // niejednoznaczność: int → double, double → intLiczby w2 { 1.0f, 3.14, 5.0 }; // float → double lepsze, wołany c-tor 2string s;Liczby w3 { s, "jeden", "dwa" }; // wołany c-tor 3Liczby w4 { 1u, 2, 3 }; // też błąd bo unsigned może być int lub doubleshort k = 0;Liczby w5 { k, 2, 3 }; // wołany c-tor 1// w przypadku tablicy można użyć dowolny konstruktor inicjalizowany…Liczby *ptr = new Liczby[3] { Liczby { 1, 2, 3 }, Liczby { 1.23 }, Liczby(1,2,3) };

programowanie obiektowe grupowanie danych oraz funkcji, które nimi manipulują oddzielenie danych od kodu, który z nich korzysta, dzięki

interfejsowi w postaci funkcji działających na tych danych

hermetyzacja jest jednym z podstawowych pojęć programowania obiektowego - prawie zawsze wiąże się z ukrywaniem danych

dane składowe powinny być

zawsze prywatne!

wyjątek: proste struktury(agregaty) danych (takiejak w C) - które nie są zasadniczo klasami

dane chronione: też źle, bo pozwalająna dostęp do nich dla kodu zewnętrznegoklas pochodnych, a przecież nie stosujemyklas bazowych będących tylko prostymagregatem (strukturą) danych - wyjątkuzatem nie ma

abstrakcja danych – projetowanie obiektowe

class A { // …public:

B& getB() { return b; }protected:

C& getC() { return c; }private:

B b;C c;

};

class A { // …public:

B b;protected:

C c;};

• dostęp za pomocą funkcji inline(nie ma dodatkowych kosztów kopiowania - kompilator zoptymalizuje)• można wprowadzać dodatkowezmiany później, nie zmieniając nazwy funkcji (czyli sposobu używania)

• sztywny kod, jakakolwiek zmiana oznacza koniecznośćzmiany u wszystkich użytkowników• jeśli nawet chwilowo zostawiszgo w tej postaci, to możesz potemnie mieć okazji na zrobienie zmian,później będą one zawsze więcej kosztować niż na samym początku

najpierw dobry interfejs - resztę można poprawić później

abstrakcja danych – hermetyzacja, prosta zmiana

class A {private:

virtual void funA() {}};

int main() {A a;a.funA(); // błądusing funPtr = void (A::*)();funPtr p = &A::funA; // błądreturn (a.*p)(); // błąd wykonania

}

class B : public A {virtual void funA() {

A::funA(); // błąd}

};

składowa prywatnajej nazwa może być używana jedynie przez składowe oraz jednostki zaprzyjaźnione klasy,w której znajduje się jej deklaracja

dostęp - nie ma dostępu • ani bezpośrednio (jawne wywołanie) • ani pośrednio (przez wskaźnik na funkcję)• ani w pochodnej klasie dziedziczącej (która może przesłonić składową prywatną klasy bazowej, ale do niej dostępu nie ma)

hermetyzacja – co to znaczy składowa prywatna?

class A { public:int ai;static int si; // deklaracjaA() : ai(77) { }void getI() { cout << ai << "," << si; }

};int A::si = 88; // definicja zmiennej statycznejclass B : private A { public:using A::getI;

};class C : public B {void f();

};

void C::f() {ai = 2; // błądsi = 3; // błąd::A a; a.ai = 2; // OKa.si = 3; // OK::A::si = 4; // OK

• Klasa B dziedziczy prywatnie z klasy A, czyli wszystkie publiczne zmienne stają się prywatne

• Klasa C dziedziczy publicznie z klasy B, dostępu do pól prywatnych klasy bazowej B nie ma

• możliwy jest za to dostęp bezpośredni (lokalny obiekt, nie część odziedziczona) lub przez rzutowanie wskaźnika (do części odziedziczonej)

::A *ptrA = this; // błąd::A *ptrA2 = (::A*)this; // OKa.getI(); // 2, 4getI(); // 77, 4ptrA2->ai = 5; // OKa.getI(); // 2, 4getI(); // 5, 4

}

hermetyzacja – prywatne dziedziczenie, a dostęp

#include <complex>class Calc {

public:double Twice( double d );

private:int Twice ( int i );std::complex<float>

Twice( std::complex<float> c );};int main() {

Calc c;return c.Twice( 21 );

}

składowa prywatnajest widziana w całym kodzie,który widzi definicję klasy - oznacza to, że jej parametry muszą być zadeklarowane nawet wtedy, gdy nie są potrzebne w danej jednostce translacji (pliku)

deklaracja wyprzedzająca klasy complexkonieczna, a gdyby ta funkcja była zdefiniowanajako inline, to konieczna by była pełna definicjaklasy complex

błąd - najlepiej dopasowana funkcja Twice jest prywatnawyszukiwanie nazw - kompilator szuka zakresu, w którym znajduje się przynajmniejjeden element o nazwie Twice i tworzy listę kandydatów do wywołania, jeśli nieznajdzie to dalej szuka w klasach bazowych i przestrzeniach nazw zawierających klasę Calc, spośród kandydatów wybrana zostaje wersja najlepiej dopasowana,na koniec sprawdzenie dostępności

hermetyzacja – składowa prywatna, a widzialność

#include <string>int Twice( int i );

class Calc {private:

std::string Twice( std::string s );public:

int Test() {return Twice( 21 );

}};int main() {

return Calc().Test();}

#include <complex>class Calc {

public:double Twice( double d );

private:unsigned Twice ( unsigned i );std::complex<float>

Twice( std::complex<float> c );};

int main() {Calc c;return c.Twice( 21 );

}

przeszukiwanie kończy się gdy zostanieznaleziony przynajmniej jeden elemento odpowiedniej nazwie (dopiero potemokazuje się, że nie można go użyć - tuz powodu braku konwersji z int na string)

wybór wersji przeciążonej nie pozwalaznaleźć jednej najlepiej dopasowanejwersji funkcji na liście kandydatów(int może być przekształcony na double lub unsigned)

hermetyzacja – składowa prywatna jest widzialna

• składowa prywatna jest widziana w każdym miejscu kodu, w którym widziana jest definicja klasy

• oznacza to, że składowa ta jest brana pod uwagę w procesie wyszukiwania nazw oraz wyboru wersji przeciążonej funkcji

• dlatego składowa taka może spowodować, że wywołanie stanie się niepoprawne lub wieloznaczne

Bjarne Stroustrup mówi:kontrola za pomocą słów kluczowych public oraz privatewidzialności, a nie dostępu, powodowałaby w wyniku zmiany słowa kluczowego public na private niejawną zmianę jednej poprawnej interpretacji na inną…

[ w poprzednim przykładzie: Calc::Twice(int) na Calc::Twice(double) ]

hermetyzacja – składowa prywatna jest widzialna

class X {public:

template<class T> void f( const T& t ) { }

private:int secret;

};

namespace {struct Y { };

}template<>

void X::f( const Y& ) { secret = 2; // dostęp

}

kod, który ma dostęp do (nazwy) składowej, może go przekazać w inne miejsce, korzystając ze wskaźnika na tę składową

class Calc;typedef int (Calc::*PMember)(int);

class Calc {public:

PMember getIt() { return &Calc::Twice; }private:

int Twice( int i );};

int main() {Calc c;PMember p = c.getIt(); // dostęp do Twice(int)return (c.*p)( 21 );

}

nazwa składowej prywatnej jestdostępna jedynie dla innych składowych(włączając w to specjalizacje składowychw postaci szablonów) oraz jednostekzaprzyjaźnionych

szablon

specjalizacjaszablonu

hermetyzacja – składowa prywatna udostępniona

hermetyzacja – włamania do składowej prywatnej

class X { public:X() : secret(1) { }template<class T>

void f( const T& t ) { } int Value() { return secret; }

private:int secret;

};

reguła jednej definicjijeśli typ (tutaj X) zdefiniowany jestwięcej niż raz, jego definicje musząbyć identyczne, ale… zachowującukład danych obiektu można spróbować takie fałszerstwo

w klasie Podmieniacz układ danych ten sam co w X, większość kompilatorów pozwoli użyć utworzonej referencji do takiego oszustwa

class Podmieniacz {public:

int notSecret;};void f( X& x ) {(reinterpret_cast<Podmieniacz&>(x)).notSecret = 2;

}class X { // zamiast .h kopia całej klasy

friend ::Hijack( X& );};// funkcja włamująca się void Hijack( X& x ) { x.secret = 2; }

#define private public// teraz włączyć plik z definicją klasy X// i mamy dostęp do składowej prywatnej

nie można definiować za pomocą#define słów kluczowychto jest niedozwolone, a jednak…"działa"

Czy destruktor klasy bazowej powinien być wirtualny?Odpowiedź standardowa: destruktor klasy bazowej zawsze powinien być wirtualny!Prawidłowa odpowiedź na zagadnienie ogólne, gdzie powinny być funkcje wirtualne• publiczne - rzadko, jak najmniej• chronione - czasami• prywatne - domyślnie

staraj się tworzyć interfejsy niewirtualne

Publiczne funkcje wirtualne muszą spełniać dwa zadania• określają interfejs (są publiczne więc są częścią interfejsu)• określają szczegóły implementacji w postaci elastycznego zachowania się

Chcemy oddzielić specyfikację interfejsu od specyfikacji implementacji, która ulega zmianom

Interfejs niewirtualny(Non-virtual Interface)funkcje publiczne niewirtualne,używają prywatnych funkcji wirtualnych• w klasie bazowej istnieje możliwość pełnej kontroli interfejsu i sposób jego

działania (mniej wrażliwa na zmiany)• można kontrolować zachowanie warunków wstępnych i końcowych, dodawać

dodatkowe opcje w jednym wygodnym miejscu• rozdzielając interfejs od implementacji można każdemu z tych elementów

nadać dowolną postać

polimorfizm – funkcje wirtualne

class A {public: // stabilny niewirtualny interfejs

int process( B& ); // używa doProcess…()bool isDone(); // używa doIsDone()

private:// specjalizacja funkcji to // szczegół implementacjivirtual int doProcess1( B& );virtual int doProcess2( B& );virtual bool doIsDone();

};

• użytkownik ma dostęp do pojedynczej funkcji processjednocześnie możliwe jestelastyczne tworzenie dwóchwyspecjalizowanych wersjifunkcji (w przypadku publicznejfunkcji wirtualnej trzeba by byłoudostępnić dwie)• również samo "opakowanie"(tak jak funkcja isDone) jestopłacalne dla stabilności

Staraj się deklarować funkcje wirtualne jako prywatne, a jeśli zachodzi koniecznośćwywołania wersji z klasy bazowej w klasie pochodnej - to jako chronione

A co z destruktorem? Destruktor klasy bazowej powinien być publiczny i wirtualny (jeśli usuwanie może być wykonywane polimorficznie) - metoda interfejsu niewirtualnego nie działa - wywołanie funkcji wirtualnej z destruktora spowoduje wywołanie funkcji z klasy bazowej - obiekty pochodne przecież już nie istnieją!

polimorfizm – prywatne funkcje wirtualne, destruktor

polimorfizm – prywatne funkcje wirtualne, destruktor

chroniony i niewirtualny - np. szablon który służy jako klasa bazowa (w celu narzucenia klasom pochodnym określonych nazw definicji typu) i nie będzie usuwany polimorficznieA jeśli musimy stworzyć obiekt klasy bazowej i z niej dziedziczyć? Nie dziedzicz po klasach konkretnych. Klasy bazowe powinny być abstrakcyjne.

szablony klas z biblioteki standardowejtemplate <class Arg, class Result>struct unary_function {

typedef Arg argument_type;typedef Result result_type;

};template <class Arg1, class Arg2, class Result>struct binary_function {

typedef Arg1 first_argument_type;typedef Arg2 second_argument_type;typedef Result result_type;

};

dziedziczenie – funkcje tworzone niejawnie

class Base {public:virtual ~Base();

private:// tylko deklaracjeBase( const Base& );Base& operator=( const Base& );

};class Derived : public Base {

int i;// konstruktor domyślny - definicja się nie powiedzie// gdyż brak konstruktora domyślnego w klasie Base// konstruktor kopiujący, przypisanie kopiujące -// definicje się nie powiodą bo te z Base niedostępne

};

napisania destruktora nie da się wymusić, ale jest to mniejszy problem -destruktory są mniej podatne na wymianę na wyspecjalizowane wersje (jeden destruktor na klasę, destruktor klasy bazowej musi być wywołany)

W jaki sposób wymusićna klasie pochodnej konieczność definiowania konstruktorów, operatora przypisania, destruktora?

Jak wymusić tworzenie obiektu tylko na stosie?• zabronienie tworzenia obiektu za pomocą operatora new lub new[ ]

class X { public:X();

private:void* operator new( size_t size);void operator delete( void* addr);

};

// gdzieś w programieX objectX; // to jest okX *ptr = new X; // to się nie skompiluje// bo operator new jest prywatny

Jak wymusić tworzenie obiektu tylko na stercie?• wymuszenie tworzenia obiektu za pomocą operatora new lub new[ ]

oraz uniemożliwienie tworzenia obiektu na stosie • obiekt na stosie wymaga dostępnego konstruktora i destruktora, zatem:

class X {public:

X();void Delete() { delete this; }

private:~X(); // prywatny destruktor

}; // nie może być „= delete”

// gdzieś w programieX objectX; // to się nie skompilujeX *ptr = new X;// usuwamy jawnie za pomocą// specjalnej funkcji Delete()ptr->Delete();// delete ptr; nie skompilowałoby się

obiekty – kontrolowane tworzenie

semafor – służy do synchronizacji procesów i wątków w celu zapewnienia bezpiecznego dzielenia zasobów, kiedy proces chce użyć dzielonego zasobu, musi zapewnić wzajemne wykluczanie

void X::f() {// obiekt sam zajmujący semaforTAutoSemaphore autosem(sem);// niezbędne działania funkcjiif ( /*jakiś warunek */ ) { // coś jeszcze robimy

return;} else { // albo coś innego}// destruktor autosem sam zwalania semafor

}

class TAutoSemaphore {public:

TAutoSemaphore(TSemaphore& sem) : semaphore(sem)

{ semaphoer.acquire(); }~TAutoSemaphore() { semaphore.release(); }

private:TSemaphore& semaphore;

};

class TSemaphore { public:TSemaphore();// wywołuje klient, który chce zająć semaforbool acquire(); // wywołany, kiedy dostęp do zasobu zwolnionyvoid release();// ile zadań czeka?unsigned getWaiters() const;// blokowanie kopiowania i przenoszeniaTSemaphore(const TSemaphore&) = delete;TSemaphore(TSemaphore&&) = delete;TSemaphore& operator=(const TSemaphore&) = delete;TSemaphore& operator=(Tsemaphore&&) = delete;

};

class X { public:void f();

private:TSemaphore sem;

};void X::f() {

sem.acquire(); // zajmujemy semafor// niezbędne działania funkcjiif ( /*jakiś warunek */ ) { // coś jeszcze robimy

sem.release(); // zwolnienie zasobureturn;

} else { // albo coś innegosem.release();

}}

wada – trzeba pamiętać o zwalnianiu semaforalepsza wersja: pomocnicza klasa kontrolująca

zasoby – kontrolowany dostęp

serwer licencji – pracuje na maszynie, która udostępnia żetony licencyjne każdemu, kto chce korzystać z oprogramowania, żeton udostępniany jest wtedy, gdy liczba wydanych poprzednio jest mniejsza od liczby wykupionych stanowiskclass TLicenceToken;class TLicenceServer { public:

TLicenceServer( unsigned maxUsers );~TLicenceServer();// udostępnia nową licencję lub zwraca 0TLicenceToken* createNewLicence();// blokada kopiowania i przenoszeniaTLicenceServer( const TLicenceServer& ) = delete;TLicenceServer& operator=( const TLicenceServer& ) = delete;TLicenceServer( TLicenceServer&& ) = delete;TLicenceServer& operator=( TLicenceServer&& ) = delete;

private:unsigned numIssued;unsigned maxTokens;

};• korzystanie z aplikacji kontrolowanej przez serwer wymaga utworzenia nowego żetonu, jeśli zostaje utworzony,

serwer zwraca do niego wskaźnik• osoba, która wystawiła żądania, posiada żeton dostępu, kiedy nie chce już używać aplikacji, musi go usunąć

(wtedy serwer zmniejsza liczbę wydanych żetonów)• wada – użytkownik musi usuwać żeton, można jednak napisać implementację, w której żeton kontroluje używanie

oprogramowania i automatycznie wywołuje destruktor, jeśli oprogramowanie nie było używane przez określony okres czasu

• można użyć np. do naliczania rachunków za korzystanie z usługi (vide telewizja kablowa: pay per view)• inne rozwiązanie – żetony można duplikować, ale ich kopia traktowana jest przez serwer jako nowy żeton

(wymagana kontrola kopiowania obiektu)

Licencja udostępniana jest konkretnemu użytkownikowi – ani serwer, ani żeton nie mogą zostać zduplikowane

zasoby – kontrolowany dostęp, przykład

class TLicenceToken {public:

TLicenceToken();~TLicenceToken();// blokada kopiowania i przenoszenia

TLicenceToken( const TLicenceToken& ) = delete;TLicenceToken& operator=( const TLicenceToken& ) = delete;TLicenceToken( TLicenceToken&& ) = delete;TLicenceToken& operator=( TLicenceToken&& ) = delete;

};

• klasa może dziedziczyć bezpośrednio od więcej niż jednej klasy, można w ten sposób powiązać niezależne typy klas

• dla każdej klasy sposób dziedziczenia definiowany osobno:

• klasa bazowa może wystąpić na liście pochodzenia tylko raz• jej definicja musi być znana kompilatorowi

(deklaracja zapowiadająca to za mało)

INICJALIZACJA I DESTRUKCJA

• konstruktor klasy pochodnej w liście inicjalizacyjnej może wołać konstruktory wszystkich swoich bezpośrednich klas bazowych

• konstruktory klas bazowych wołane są w kolejności występowania na liście pochodzenia

• destrukcja odbywa się w kolejności odwrotnej

dziedziczenie wielokrotne (wielobazowe)

class D : public A, B, protected C {}; // B – (domyślnie) private

• dalsze klasy pochodne nadal dziedziczą ryzyko wieloznaczności, więc rozwiązanie go za pomocą operatora zakresu nie jest dobre

• wieloznaczna może być też funkcja wirtualna, wskazanie jej operatorem zakresu eliminuje mechanizm polimorfizmu

• w tym przypadku nadal wieloznaczność, bo najpierw rozstrzygana jest jednoznaczność,a potem prawa dostępu

• w klasach pochodnych nie ma jużwieloznaczności (można też przesłonić funkcją o tej samej nazwie)

dziedziczenie wielokrotne - wieloznaczność

class A { public: int n; };class B { public: int n; };class C : public A, public B { };// … gdzieś w programieC c;c.n = 3; // błąd wieloznacznościc.A::n = 3; // operator zakresuc.B::n = 4;

class C : public A, private B { };

class C : public A, public B {public: int n;

};

// … gdzieś w programieC c;c.n = 3; // ok – bo przedefiniowane

BLIŻSZE POKREWIEŃSTWO?

OPERATOR ZAKRESU // nie musi być klasa źródłowa tzn. d.A::n;d.C::n; // wystarczy wskazać jednoznaczną drogę do danego identyfikatora

KONFLIKT WIELOZNACZNOŚCI – WSPÓLNA KLASA BAZOWA

A

D

CBkonflikt niezależnie odsposobu dziedziczenia,np. klasa B public,a klasa C private

dziedziczenie wielokrotne – konflikty

class A { public: int n; }; class B { public: int n; };class C : public A { }; class D : public B, public C { };// … gdzieś w programieD d;d.n = 3; // kompilator g++ nadal zgłosi tu błąd wieloznaczności

• pośrednie dziedziczenie tej samej klasy bazowej (poprzez wielokrotne dziedzicznie od jej potomków) może doprowadzić do niejednoznaczności

• niejednoznaczność objawia się w momencie odniesienia się do zwykłego składnika klasy

• nie ma niejednoznaczności w przypadku składowych statycznych, typów wyliczeniowych i typów zdefiniowanych (zagnieżdżonych) wewnątrz klasy bazowej (w przykładzie poniżej: klasy A)

• rozwiązanie: dziedziczenie wirtualneclass A { }; // bazowaclass B : virtual public A { }; // może też być virtual private A; class C : virtual public A { }; // wystarczy że choć jedno dziedziczenie public class D : public B, public C { };

• klasa dziedzicząca wirtualnie otrzymuje tylko jeden wspólny podobiekt klasy podstawowej niezależnie od liczby wystąpień tej klasy w hierarchii dziedziczenia– nie ma ryzyka wieloznaczności, pomimo różnych dróg dojścia– składniki nie duplikują się (obiekt jest mniejszy)

• class D : public A, public B, public C { };– w takim przypadku (dziedziczenie raz normalnie, a raz wirtualnie pośrednio) znów mamy

dwa komplety danych z klasy bazowej A oraz konflikt wieloznaczności w przypadku odniesienia się do zwykłej składowej klasy A

wirtualne dziedziczenie wielokrotne

• dziedziczenie wirtualne klasy bazowej sprawia, że mamy jeden komplet jej zwykłych składowych

• konstruktor takiej klasy będzie uruchomiony tylko raz• klasy wirtualne są konstruowane na samym początku, przed

wszystkimi innymi klasami podstawowymi, niezależnie od kolejności wpisania ich na liście inicjalizacyjnej konstruktora

• jeśli jest więcej klas dziedziczonych wirtualnie, to kolejność ich konstrukcji zgodnie z kolejnością na liście pochodzenia

• klasa najbardziej pochodna jest odpowiedzialna za konstrukcję klas dziedziczonych wirtualnie, to ona musi wywołać ich konstruktory– w zwykłym dziedziczeniu wołaliśmy tylko konstruktory bezpośrednich

klas podstawowych– teraz każda klasa dziedzicząca wirtualnie, musi wywoływać konstruktor

klasy tak dziedziczonej, ale kompilator bierze pod uwagę tylko wywołanie konstruktora przez klasę najbardziej pochodną (na dole hierarchii dziedziczenia)

inicjalizacja w dziedziczeniu wirtualnym

class B1{ };class V1 : public B1 { };class D1 : virtual public V1 { };

class B2 { };class B3 { };class V2 : public B1, public B2 { };class D2 : virtual public V2, public B3 { };

class M1 { };class M2 { };

class X : public D1, public D2 {M1 m1;M2 m2;

};

// … gdzieś w programieX x; // uwaga: w powyższym przykładzie

// dostęp do składowych klasy B1// jest niejednoznaczny

Kolejność powstawania obiektów(wywoływania ich konstruktorów)tu – wszystkie konstruktory są konstruktoram domyślnymi

• najpierw obiekty wirtualnych klas bazowych

• konstrukcja V1B1::B1(), V1::V1()

• konstrukcja V2B1::B1(), B2::B2(), V2::V2()

• następnie obiekty niewirtualnychklas bazowych

• konstrukcja D1D1::D1()

• konstrukcja D2B3::B3(), D2::D2()

• następnie powstają składowe• M1::M1(), M2::M2()

• na koniec sam obiekt x klasy X• X::X()

dziedziczenie wirtualne – kolejność inicjalizacji

sytuacje, w których należy użyć dziedziczenia wielobazowego• łączenie modułów lub bibliotek – wiele klas zostało zaprojektowanych jako klasy

bazowe, to znaczy, że ich użycie wymaga dziedziczenia• zmiana kodu biblioteki raczej niemożliwa (możemy w ogóle nie mieć dostępu

do kodu źródłowego• klasy protokołowe (interfejsowe) – najbezpieczniej dla dziedziczenia wielobazowego

jest zdefiniować klasy złożone wyłącznie z metod czysto wirtualnych, nieobecność składowych danych w klasie bazowej pozwala na unikanie największych kompliakcjiwynikających z wielodziedziczenia

• łatwość (polimorficznego) użycia – koncepcja użycia dziedziczenia celem umożliwienia innemu kodowi zastosowania obiektu pochodnego wszędzie tam, gdzie spodziewana jest klasa bazowa (np. projekt klas wyjątków)

powody dziedziczenia wielobazowego• nie tylko dlatego, że klasa pochodna "jest" typem obu klas bazowych, może chodzić

np. o dostęp do danych chronionych jednej klasy (i dziedziczyć z niej prywatnie), a z drugiej klasy dziedziczyć publicznie w celu dziedziczenia interfejsu

dziedziczenie wielobazowe – przesłanki

• RTTI (Run Time Type Identification) – identyfikacja obiektu podczas wykonywania programu

• dostępna dla obiektów klas posiadających co najmniej jedną metodę wirtualną (klasa posiadająca deklarację funkcji wirtualnej "wie" kto jest jej potomkiem – późne wiązanie)

• dynamiczne określanie typów jest mniej bezpieczne i mniej wydajne niż statyczne (podczas kompilacji)

• określenie typu obiektu, wyrażenia za pomocą operatora typeid, który zwraca obiekt klasy type_info– wymaga deklaracji pliku nagłówkowego <typeinfo>– jeśli dwa obiekty są tego samego typu, porównanie obiektów type_info zwróci

wartość true– operator typeid() działa

• dla typów polimorficznych i niepolimorficznych(ale dynamiczne rzutowanie – tylko dla typów polimorficznych)

• dla typów wbudowanych języka jak i typów abstrakcyjnych• dla nazw typów oraz dla nazw obiektów

Run Time Type Identification (definicja)

• do uzyskania nazwy danego typu (const char*) należy użyć funkcji typeid( obiekt ).name();

• porównywanie stałych, typów wbudowanych i obiektów

RTTI – przykłady

int ii = 0, *iw = &ii;cout << typeid(ii).name() << ' ' << typeid(iw).name() << '\n'; // i Pidouble dd = 0.0, *dw = &dd;cout << typeid(dd).name() << ' ' << typeid(dw).name() << '\n'; // d Pd

cout << typeid(123).name() << ' ' << typeid(3.14).name() << '\n'; // i dint ii = 0, *iw = &ii;cout << (typeid(int) == typeid(ii)); // 1cout << (typeid(int) == typeid(iw)); // 0cout << (typeid(int) != typeid(1/3)); // 0cout << (typeid(int) != typeid(1.0/3)); // 1

class A { public:virtual void met() const = 0;

};class B : public A { public:

void met() const{ cout << "metB\n";}

};class C : public A { public:

void met() const{ cout << "metC\n";}

};

void pokaz(const A& a){

cout << typeid(a).name() << ' '<< typeid(&a).name() << ' ';

a.met();}void pokaz(const A* a){

cout << typeid(a).name() << ' '<< typeid(*a).name() << ' ';

a->met();}

RTTI jest użyteczne gdy potrzebnejest rzutowanie w dół, jednak większośćoperacji związanych z typami możnarozwiązać za pomocą funkcji wirtualnych.

Nie należy pisać kodu, który bezpośredniozależy od RTTI – jest to nie tylko kosztowne, ale też powoduje, że trudniej go potem rozszerzyć.

Metody RTTI w języku C++static_cast<T>(e)const_cast<T>(e)reinterpret_cast<T>(e)dynamic_cast<T>(e)

RTTI – przykłady z typami abstrakcyjnymi

// … gdzieś w programieB b; C c;pokaz(b); // 1B PK1A metBpokaz(c); // 1C PK1A metCpokaz(&b); // PK1A 1B metBpokaz(&c); // PK1A 1C metC

• operator dynamic_cast – rzutowanie typu w trakcie wykonania programu• przekształcenie wskaźnika wskazującego na obiekt klasy we wskaźnik do obiektu

innej klasy w ramach tej samej hierarchii (ew. przekształcenie l-wartości obiektu w referencję)

• przy niepowodzeniu rzutowania dla wskaźnika zostanie zwrócony nullptr, dla referencji zgłoszony zostanie wyjątkek bad_cast

rzutowanie dynamiczne (RTTI w akcji)

class C : public A { public:C() : c(3.14) { }void met() const { cout << "metC\n";}void inna() const { cout << c << " innaC\n";}protected:

double c;};void drukuj(A* a) {

a->met();if (C* w = dynamic_cast<C*>(a)) w->inna();

}void drukujZle(A* a) {

a->met();if (C* w = static_cast<C*>(a)) w->inna();

}

// … gdzieś w programieB b; C c;drukuj(&c); // metC 3.14 innaCdrukuj(&b); // metBdrukujZle(&c); // metC 3.14 innaCdrukujZle(&b); // metB 3.83854e-308 innaC (losowa wartość)

• dziedzicz interfejsy a nie implementacje, nie sięgaj do prywatnych składowych klas bazowych, jeśli tylko możesz twórz stabilne niewirtualne interfejsy w klasach bazowych - i nigdy nie konkretyzuj klas bazowych, najlepiej więc twórz je jako klasy abstrakcyjne

• minimalizuj zależności pomiędzy klasami, osiągniesz to poprzez relację zawierania, ograniczając na ile to możliwe relację dziedziczenia, zatem asocjacja pomiędzy klasami (czy jej szczególny typ - agregacja) jest lepszym modelem niż uogólnienie (dziedziczenie)

programowanie obiektowe – najważniejsze wskazówki

• Projektując klasy ustal ich rodzaje– klasy wartości (np.. std::vector) – modelowane na wzór typów wbudowanych– klasy bazowe – fundamenty hierarchii klas– klasy cech (trait classes) – szablony niosące informacje o typach– klasy wytycznych (police classes) – najczęściej szablony implementujące fragmenty

wymienialnego zachowania obiektów– klasy wyjątków – do obsługi sytuacji wyjątkowych– klasy pomocnicze – do obsługi poszczególnych idiomów

• Lepsze są klasy niewielkie i proste – przydatne w większej liczbie rozmaitych sytuacji

• Kompozycja czy dziedziczenie– dziedziczenie to druga co do siły (po zaprzyjaźnieniu) relacja w języku C++– ścisłe powiązanie jest mało kiedy pożądane i gdzie to możliwe należy się go wystrzegać

• Nie dziedziczyć po klasach, które nie zostały przewidziane jako bazowe• Warto pomyśleć nad interfejsem abstrakcyjnym• Uważaj na udostępnianie konwersji niejawnych• Składowe klas, z wyjątkiem prostych agregatów, powinny być prywatne

projektowanie i dziedziczenie – uwagi

TOsoba

nazwiskoadresdata urodzenia

TStudent

statuswydziałkursy

TNauczyciel

funkcjakursy

TDoktorantnie może się zapisywaćna kursy podstawowe

TOsoba

nazwiskoadresdata urodzenia

TStudent

statuswydziałkursy

TNauczyciel

funkcjakursy prowadzone

TDoktorant

TDoktorantNaucz

doktorant z obowiązkiemprowadzenia zajęć

dydaktycznych

TOsoba

nazwiskoadresdata urodzenia

wielokrotne dziedziczenie spowodujezapewne pojawienie się konfliktu niejednoznaczności, np. funkcja print() odziedziczona podwójnie…

dziedziczenie kontra zawieranie – przykład uniwersytecki

class TDoktorantNaucz {private:

TNauczyciel nauczycielProxy;TDoktorant doktorantProxy;

// sporo kodu do napisania};

• funkcje składowe implementacji klasy TDoktorantNaucz muszą wywoływać odpowiednie funkcje obiektów pomocniczych nauczycielProxy i doktorantProxy

• mamy podwójne obiekty klasy TOsoba, więc trzeba zapewnić poprawne zarządzanie stanem gdy zmieniane są dane TOsoba, taka niespójność jest uciążliwa

• zalety to lepsza hermetyzacja, implementator może udostępnić jedynie te funkcje, których klient powinien używać

TNauczyciel

TDoktorant

TDoktorantNaucz

1

1

dziedziczenie wielokrotne – alternatywa 1

class TDoktorantNaucz : public TDoktorant {

private:TNauczyciel nauczycielProxy;

// trochę kodu do napisania};

• TDoktorantNaucz dziedziczy wszystkie cechy klasy TDoktorant, a pośrednio również TStudent i TOsoba, trzeba zaś napisać funkcje, które wiążą się z klasą TNauczyciel

• nadal istnieje problem podwójnego obiektu klasy TOsoba, ale łatwiej nim zarządzać, korzystać z odziedziczonego po klasie TDoktorant, a kontrolując dostęp do TNauczyciel nie używać danych TOsoba z nim związanych

TDoktorant

TNauczycielTDoktorantNaucz1

dziedziczenie i zawieranie – alternatywa 2

• wszystkie wirtualne klasy bazowe inicjalizuje się w konstruktorze ostatniej klasy pochodnej, czyli konstruktor klasy TOsoba trzeba wywołać przy tworzeniu obiektu klasy TDoktorantNaucz, jest to niewygodne

• jeśli konstruktor ostatniej klasy pochodnej nie wywołuje jawnie konstruktora wirtualnej klasy bazowej, kompilator próbuje wywołać domyślny konstruktor wirtualnej klasy bazowej

• łatwiej pisać kod, gdy wirtualna klasa bazowa posiada konstruktor domyślny, ale w naszym przypadku to nie ma sensu (nie ma przecież "domyślnego" nazwiska etc.)

TOsoba

nazwiskoadresdata urodzenia

TStudent

statuswydziałkursy

TNauczyciel

funkcjakursy prowadzone

TDoktorant

TDoktorantNaucz

wirtualnaklasabazowa

dziedziczenie wielokrotne – alternatywa 3

TOsoba

nazwiskoadresdata urodzenia

TStudent

statuswydziałkursy

TNauczyciel

funkcjakursy prowadzone

TDoktorant

TDoktorantNaucz

wirtualnaklasabazowa

istnienie konstruktora w klasie (np. domyślnego)zależy wyłącznie od projektu interfejsu, nie należydodawać funkcji składowych tylko po to, aby uniknąć błędów kompilacji

TDoktorantNaucz

TOsoba

TNauczyciel

TDoktorant

TStudent

dziedziczenie wielokrotne - koszty

• chcemy dodać do naszej "abstrakcji uniwersytetu" asystenta badań, nie musi on być studentem i nie musi prowadzić zajęć dydaktycznych

• co jednak zrobić jeśli TDoktorant podejmie pracę jako TAsystentBadan, nawet na innym wydziale?

• problem wynika stąd, że "prowadzenie badań" to właściwość jaką może nabyć każda osoba, nie tylko student lub wykładowca

• w wyniku złożoności relacji zachodzi tu konflikt wymagań, którego nie da się rozwiązać za pomocą dziedziczenia

• dziedziczenie jest odpowiednim mechanizmem do modelowania tych relacji między klasami, które zawsze są spełnione

TOsoba{ virtual }

TStudent TNauczyciel

TDoktorant

TDoktorantNaucz

TAsystentBadan

• dziedziczenie jest relacjąstatyczną - trudno ją zmienić• kiedy relacje między klasamizmieniają się, przydatność dziedziczenia jest ograniczona• relacje w hierarchii dziedziczeniasą określone i zakodowane na stałe

dziedziczenie – statyczna relacja

• chcemy dodać możliwość zostania studentem za pomocą klasy mieszanej MozeBycStudentem

• klasa ta dodaje metody potrzebne do zapisania się na kursy oraz do identyfikacji studenta

enum EWyksztalcenie { ePodstawowe, eSrednie, eLicencjat, eMagister, eDoktor };

clas MozeBycStudentem { public:void setWydzial( EWydzial dep );EWydzial getWydzial() const;virtual bool zapiszNaKurs( const TKurs& ) = 0;virtual bool usunZKursu( const TKurs& ) = 0;virtual void pokazKursy() const = 0;virtual EWyksztalcenie getWyksztalcenie() const;// więcej kodu

};• w klasie TStudent trzeba zaimplementować

wszystkie wirtualne metody dziedziczone po MozeBycStudentem, w której można też zdefiniować jakąś domyślną implementację

TOsoba

TStudent

MozeBycStudentem

• klasa mieszana pozwala na dodanie nowych możliwości do innych klas• nie tworzymy egzemplarza klasy mieszanej (nie ma to sensu)• użycie klas pozwala łączyć różne możliwości w nowe jednostki• klasy mieszane reprezentują statyczne relacje, nowe własności można dodać w trakcie projektowania hierarchii, nie zaś dynamicznie w trakcie wykonywania programu

klasa mieszana – mix-in-class

• klasa TOsoba nie musi już być wirtualną klasą bazową, co upraszcza zarządzanie kodem

• elastyczność i prostotę projektu uzyskuje się dzięki rozłożeniu możliwości na kilka klas

• w hierarchii z użyciem klas mieszanych można dodawać nowe możliwości bez wpływu na inne klasy w hierarchii

TOsoba

TStudent

MozeBycStudentemMozeNauczacMozeWykBadania

TNauczyciel

TAsystentBadan

TDoktorant

TDoktorantNaucz

TDoktorantBadacz

Dodajemy dalszą funkcjonalność za pomocą klas mieszanych, to znaczy klasę reprezentującą osoby z kwalifikacjami do prowadzenia kursów MozeNauczac oraz do prowadzenia badań MozeWykBadania

Kiedy klasy mieszane?1. istnieje wiele

niezależnych właściwości, które klasa może posiadać

2. trzeba wybiórczo dodać nową własność do niektórych klas w istniejącej hierarchii

klasy mieszane - dyskusja

• kiedy student kończy studia i staje się doktorantem, zmiany w obiekcie powinny dotyczyć jedynie tych części, które rzeczywiście ulegają zmianie, czyli powinna istnieć możliwość dodania do obiektu TStudent części TDoktorant

• jeśli TDoktorant staje się obiektem TNauczyciel, możliwości klasy TDoktorant powinny zostać zmienione przez możliwości klasy TNauczyciel

Wiemy już, że należy unikać niepotrzebnego powielania danych (wirtualne klasy bazowe) - bo powoduje to utratę zasobów i problemy z zarządzaniem tymi danymiWarto też do minimum ograniczyć ilość kopiowanych danych kiedy przekształcamy lub kopiujemy obiekt

Jak przekształcić TStudent w TDoktorant? • trzeba utworzyć nowy obiekt

TDoktorant i zainicjalizować go (skopiować dane) z obiektu TStudent

• ponosimy tu niepotrzebne koszty kopiowania części TOsoba, która się przecież nie zmieniaWidzimy brak elastyczności dziedziczenia

wielokrotnego w dynamicznie zmieniających sięsytuacjach - często ma miejsce w bazach danych

dynamiczna zmiana sytuacji – czyli co po studiach?

• a co jeśli osoba jest doktorantem na jednym wydziale i równocześnie asystentem badań na innym? - do zarządzania potrzeba wtedy dwóch niezależnych obiektów TDoktorant oraz TAsystentBadan, a w obu powtarzają się dane części TOsoba

• a co jeśli osoba studiuje dwa kierunki?

TOsoba

TStudent

TCzlonekUniwersytetu

TNauczyciel TBadacz

TDoktorant

0 .. n

do kogo

pełni rolęDana osoba może pełnić wiele ról, ale w konkretnym momencie pełni tylko jedną rolęKażda osoba posiada n ról jako członka uniwersytetuKażda rola należy tylko do jednej osoby (relacja "do kogo")Od każdego obiektu TCzlonekUniwersytetu można uzyskać informację o tym do kogo należy dana rolaObiekt TOsoba przechowuje listę wszystkich możliwych ról pełnionych przez daną osobę – nie powiela się danych osobowychRole są oddzielone od osoby, która je pełni, role tworzą odrębną hierarchię –do każdej osoby można przypisać dowolną liczbę ról, nawet tę samą rolę dwa razy (np. student dwóch kierunków)

Implementacja – problem określania typu• Klasy TStudent, TNauczyciel, TBadacz posiadają

różne metody ale wspólną klasę bazową TCzlonekUniwersytetu. Obiekt TOsoba zwraca za pomocą metody aktualną rolę danej osoby – ale jest to obiekt typu TCzlonekUniwersytetu

• Polimorficzne używanie obiektów tej klasy bazowej może nie być zbyt użyteczne, ponieważ nie jest możliwe uchwycenie we wspólny interfejs zachowania wszystkich klas pochodnych

• Konieczne jest poznanie rzeczywistego typu obiektu, czyli użycie mechanizmu RTTI (elastyczność kosztem złożoności kodu)

dynamiczna zmiana sytuacji – role

Niepotrzebne stają się klasy złożone, typu TDoktorantBadacz, ponieważ osobie można przypisać rolę badacza oraz rolę nauczyciela (w danej chwili pełniona jest tylko jedna z nich)

Dostęp do danych TOsoba jest teraz możliwy tylko przez metody klasy TCzlonekUniwersytetu

Obiekt TCzlonekUniwersytetu nie zależy od osoby, ale zawiera informacje potrzebne osobie do pełnienia danej roli

Można więc powiązać konkretną rolę z wieloma osobami – można np. utworzyć grupę osób prowadzących te same badania, czyli pełniących taką samą rolę…

Dwie osoby mogą prowadzić taki sam wykład (rola wykładowcy), sześć osób może prowadzić takie same ćwiczenia…

Role są przenośne

role i ich konsekwencje

Problem – gdy potrzeba wiele kombinacji różnych klas, może dojść do eksplozji kombinatorycznej

• Hierarchie dziedziczenia wielokrotnego są trudniejsze do zrozumienia od hierarchii dziedziczenia jednokrotnego, dodanie wirtualnych klas bazowych komplikuje jeszcze bardziej

TOsoba

TStudent

MozeBycStudentemMozeNauczacMozeWykBadania

TNauczyciel

TAsystentBadan

TDoktorant

TDoktorantNaucz

TDoktorantBadacz

TStudentBadacz TNauczycielDoksztalc

Klasy mieszane dodają statyczne możliwości (decyzję trzeba podjąć podczas projektowania hierarchii klas)Utworzony obiekt może odpowiadać na komunikaty będące zawarte w klasie bazowej (klasach bazowych)Klasy mieszane łatwe do zrozumienia i implementacji

Obiekty pełniące rolę to lepsze rozwiązanie w dynamicznie zmieniających się sytuacjach

Można utworzyć obiekt TOsoba bez żadnych ról, które przypisze się później

klasy mieszane vs pełnione role

Klasy mieszane dodają statyczne możliwości (decyzję trzeba podjąć podczas projektowania hierarchii klas)Utworzony obiekt może odpowiadać na komunikaty będące zawarte w klasie bazowej (klasach bazowych)Klasy mieszane łatwe do zrozumienia i implementacji

Obiekty pełniące rolę to lepsze rozwiązanie w dynamicznie zmieniających się sytuacjach

Można utworzyć obiekt TOsoba bez żadnych ról, które przypisze się później

Problem – zależność od mechanizmu RTTI lub podobnych, potrzeba napisania dodatkowego kodu do używania i konwersji obiektów TCzlonekUniwersytetu

• Klasy pochodne od klasy TCzlonekUniwersytetu trzeba określić w czasie kompilacji programu

Klasy mieszane a rolerole – lepsze gdy istnieje zbyt wiele możliwych kombinacji ról i kombinacje te mogą się zmieniać dynamicznieklasy mieszane – gdy kombinacja ról jest mała i jedna osoba może pełnić tylko jedną rolę danego rodzaju

klasy mieszane a role – przypadki zastosowań

• iteratory – uogólnione wskaźniki pozwalające na pracę z różnymi kontenerami danych w ujednolicony sposób, to gwarantuje, że funkcja pracująca na iteratorach, równie dobrze pracuje ze wskaźnikami oraz że algorytmy uogólnione potrafią operować na różnych strukturach danych

• idea: separacja rodzaju kontenera (sposobu organizacji danych – szczegół implementacji) od sposobu dostępu do niego

• wszystkie iteratory iter wspierają wyrażenie *iter, zwracające wartość klasy, typu wyliczeniowego lub typu wbudowanego (typ wartości iteratora)

• wszystkie iteratory, dla których wyrażenie (*iter).m jest zdefiniowane, wspierają również wyrażenie iter->m

• standard definiuje pięć kategorii iteratorów

input (wejściowy) output (wyjściowy)

forward (do przodu)

bidirectional (dwukierunkowy)

random access (o dostępie bezpośrednim)

odczytywanie elementu, przesuwanie do przodu o jeden, algorytmy one-pass

zapisanie elementu, przesuwanie do przodu o jeden, algorytmy one-pass

odczytywanie i zapisywanie elementu w zasobniku, przesuwanie do przodu o jeden, algorytmy wielo-przejściowe

własności iteratora do przodu z możliwością przesuwania do tyłu o jeden

własności iteratora dwukierunkowego z możliwością przesuwania o dowolną liczbę elementów do przodu i do tyłu, dostęp do dowolnego elementu

iterator – wzorzec projektowy

• kontenery standardowe obsługiwane są przez następujące iteratoryvector, deque – random accesslist, set, multiset, map, multimap – bidirectionalstack, queue, priority_queue – nie obsługują iteratorów

• działania na iteratorzeinput –• ++iter, iter++ pre- i postinkrementacja• *iter p-wartość elementu wskazywanego przez iterator• iter = iter2 przypisanie wartości innego iteratora• iter == iter2 porównanie iteratorów pod względem

równości• iter != iter2 porównanie iteratorów pod względem

nierównościoutput –• ++iter, iter++ pre- i postinkrementacja• *iter l-wartość elementu wskazywanego przez iterator• iter = iter2 przypisanie wartości innego iteratoraforward –• funkcjonalność iteratora wejściowego i wyjściowegobidirectional –• funkcjonalność iteratora do przodu (forward) • --iter, iter-- pre- i postdekrementacja

random access –• funkcjonalność iteratora dwukierunkowego• iter += i, iter -= i inkrementacja i dekrementacja o i pozycji• iter + i, iter - i wynik to iterator przesunięty o i pozycji (iter bez zmian)• iter[i] daje referencję do obiektu na pozycji iter przesuniętego o i• iter < iter2, iter <= iter2, iter > iter2, iter >= iter2 zwraca prawdę gdy relacja jest spełniona

iteratory – kontenery, rodzaje iteratorów

• kontenery mają predefiniowane iteratory do poruszania się po nich

iterator – do poruszania się do przodu (odczyt, zapis)const_iterator – do poruszania się do przodu (odczyt)reverse_iterator – do poruszania się do tyłu (odczyt, zapis)const_reverse_iterator – do poruszania się do tyłu (odczyt)

• kontenery mają też zdefiniowanevalue_type – typ elementu przechowywanego

w kontenerzereference – typ referencji do elementu przechowywanego

w kontenerzeconst_reference – typ referencji do stałego elementu

przechowywanego w kontenerzepointer – typ wskaźnika do elementu przechowywanego

w kontenerze

iteratory – kierunki iteracji

• iteratory strumieniamożna traktować strumień jako kontener, więc do poruszania się po nim dostarczone są odpowiednie iteratoryistream_iterator – dla strumienia wejściowegoostream_iterator – dla strumienia wyjściowego

Przykład:vector<int> myInt; // załóżmy, że wpisaliśmy do tego wektora jakiś zestaw liczb// użyjemy teraz copy – jednej z funkcji algorytmów uogólnionych// służącej do kopiowania w zakresie od miejsca1 do miejsca2 wskazanych // przez iterator do miejsca pokazywanego przez inny iteratorcopy( myInt.begin(), myInt.end(), ostream_iterator<int>(cout, ”\n”) );// miejscem do którego kopiujemy jest strumień wyjściowy wskazywany przez// iterator strumienia wyjściowego, ze znakiem separatora ”\n”

• A co gdybyśmy chcieli skopiować z jednego wektora do drugiego, czyli wstawić po kolei do tego drugiego elementy z pierwszego? Potrzebny do tego jest specjalny adapter.

iteratory – strumienia

• iteratory wstawiające – adaptery iteratorów specjalizowane iteratorem danego kontenera, używa się ich za pomocą pomocniczych funkcjiback_inserter() – w miejscu operatora przypisania następuje wywołanie push_back()int tab[] = { 2, 4, 6, 8}; // zwykła tablicalist<int> lista(3, 7); // lista, którą na początku wypełnimy trzema 7-kami// iterator strumienia cout z separatorem ” ”ostream_iterator<int> outIter(cout, ” ”); // wstawiamy na koniec listycopy( tab, tab+3, back_inserter(lista) ); // kopiujemy do strumienia cout czyli na ekrancopy( lista.begin(), lista.end(), outIter ); // 7 7 7 2 4 6 8

• front_inserter() – w miejscu operatora przypisania następuje wywołanie push_front()copy( tab, tab+3, front_inserter(lista) ); copy( lista.begin(), lista.end(), outIter ); // 8 6 4 2 7 7 7

• inserter() – w miejscu operatora przypisania następuje wywołanie insert()wartość iteratora zwiększa się po każdym wywołaniulist<int>::iterator itList = lista.begin();copy( tab, tab+3, inserter(lista, ++itList) );copy( lista.begin(), lista.end(), outIter ); // 7 2 4 6 8 7 7

iteratory – wstawianie do kontenerów

• metody wspólne dla wszystkich kontenerów:empty() – zwraca prawdę gdy kontener jest pustymax_size() – zwraca maksymalną liczbę elementów dla zasobnikasize() – zwraca bieżącą liczbę elementówoperator= – przypisanie kontenerówoperator<, operator<=, operator>, operator>=, operator==, operator!= - zwraca prawdę, jeśli relacja między kontenerami jest spełniona (nie obsługiwane przez priority_queue)swap – wymienia elementy dwóch zasobnikówerase – usuwa jeden lub więcej elementów zasobnikaclear – usuwa wszystkie elementy zasobnika

• na przykład dla obiektu typu string a = ””;string::npos - 4294967295, a.max_size() – 1073741820 (4 razy mniej niż npos)

• begin – zwraca iterator lub const_iterator , odwołujący się do pierwszego elementu konteneraend – zwraca iterator lub const_iterator , odwołujący się do następnej pozycji po ostatnim elemencie kontenera (element „za-ostatni”)rbegin – zwraca reverse_iterator lub const_reverse_iterator , odwołujący się do ostatniego elementu kontenerarend – zwraca reverse_iterator lub const_reverse_iterator , odwołujący się do pozycji przed pierwszym elementem kontenera (element „przed-pierwszy”)

kontenery – metody wspólne

• metody wspólne dla kontenerów sekwencyjnych:front() – zwraca referencję do pierwszego elementu w kontenerzeback() – zwraca referencję do ostatniego elementu w kontenerzepush_back() – wstawia element do kontenera na końcupop_back() – usuwa element z ostatniego miejsca w kontenerze

• vector < T >szybki dostęp do danych przez operator indeksowaniadodawanie i usuwanie elementów na końcu sekwencji wydajneautomatyczne rozszerzenie dostępnego obszaru pamięcimetody: insert(), capacity(), resize(), reserve(), assign(), at(), operator[]

• list< T >wydajne dodawanie i usuwanie elementów w dowolnym miejscu sekwencjiimplementowany jako lista z dwukierunkowymi odnośnikami (do poprzedniego i do następnego)dostępny iterator dwukierunkowymetody: insert(), splice(), push_front(), pop_front(), remove(), unique(), merge(), reverse(), sort(), resize(), remove_if()

• deque< T >szybki dostęp do danych przez operator indeksowaniawydajne dodawanie i usuwanie elementów na początku i końcu sekwencjiautomatyczne rozszerzanie dostępnego obszaru pamięcimetody: insert(), resize(), assign(), at(), operator[], push_front(), pop_front()

kontenery sekwencyjne

typedef vector<int> Vi;Vi v;cout << v.size() << ’ ’ << v.capacity() << ’\n’; // 0 0v.push_back(12); // wkładamy na koniec wektora kilka liczbv.push_back(23);v.push_back(34);cout << v.size() << ’ ’ << v.capacity() << ’\n’; // 3 4Vi::iterator p = v.begin(); // iterator na początekcout << *p << ’\n’; // 12cout << *(p+2) << ’\n’; // 34cout << *++p << ’\n’; // 23cout << (p == v.begin()) << ’\n’; // 0cout << (p > v.begin()) << '\n'; // 1for (p = v.begin(); p != v.end(); ++p) cout << *p << ’ ’; // 12 23 34Vi::reverse_iterator q; // iterator do poruszania się do tyłufor (q = v.rbegin(); q != v.rend(); ++q) cout << *q << ’ ’; // 34 23 12v.push_back(112);v.push_back(123);v.push_back(134);cout << v.front() << ’\n’; // 12cout << v.back() << ’\n’; // 134v.pop_back(); // usuwamy ostatni element z wektoracout << v.back() << ’\n’; // 123

kontenery sekwencyjne (vector) – przykład

cout << v.size() << ’ ’ << v.capacity() << ’\n’; // 5 8v.erase(v.begin(), v.begin()+3); // usuwamy trzy pierwsze elementyostream_iterator<int> io(cout, " "); // iterator strumienia wyjściowegocopy(v.begin(), v.end(), io); // 112 123v.clear(); // usunięcie wszystkich elementówcout << v.size() << ’ ’ << v.capacity() << ’\n’; // 0 8Vi w(2, 10); // nowy wektor o rozmiarze 2 wypełniony liczbą 10cout << w.size() << ’ ’ << w.capacity() << ’\n’; // 2 2copy(w.begin(), w.end(), io); // 10 10w.resize(5); // zmieniamy rozmiar wektora, na nowe pozycje 0cout << w.size() << ’ ’ << w.capacity() << ’\n’; // 5 5copy(w.begin(), w.end(), io); // 10 10 0 0 0w.reserve(9); // zmieniamy pojemność wektora (wielkość wewn. buforu)cout << w.capacity() << ’\n’; // 9w.reserve(3); // zmieniamy, ale jako że nie powiększamy, więc bez zmiancout << w.capacity() << ’\n’; // 9w.resize(7, 6); // zmiana rozmiaru, na nowe pozycje wstawiana liczba 6cout << w.size() << ’\n’; // 7copy(w.begin(), w.end(), io); // 10 10 0 0 0 6 6

kontenery sekwencyjne (vector) – przykład

w.at(1) = 2; // kilka sposobów podstawienia wartości w danej pozycjiw[1] = 2;w.insert(w.begin()+3, 4);copy(w.begin(), w.end(), io); // 10 2 0 4 0 0 6 6w.clear();cout << ”w ” << (w.empty() ? ”pusty” : ”pełny”) << ”\n”; // w pusty

int t[] = {1, 3, 5, 7, 9, 12};Vi v1(t, t + 6); // inicjalizacja wektora tablicąVi v2(t, t + 4); // inicjalizacja wybranym zakresem z tablicyVi v3(v2); // konstruktor kopiujący tworzy v3 na wzór v2v3[2] = 4;copy(v1.begin(), v1.end(), io); // 1 3 5 7 9 12copy(v2.begin(), v2.end(), io); // 1 3 5 7copy(v3.begin(), v3.end(), io); // 1 3 4 7cout << boolalpha; // ustawienie manipulatora strumieniacout << (v1 < v2); // falsecout << (v1 > v3); // truecout << (v2 == v3); // falsecout << (v3 <= v2); // true

kontenery sekwencyjne (vector) – przykład

typedef list<int> Li;int t[] = {2, 6, 4, 8};Li w(t, t + 4); // inicjalizacja listy tablicąw.push_back(4);w.push_back(3);w.push_front(1); // można wstawić z przoduw.push_front(2);copy(w.begin(), w.end(), io); // 2 1 2 6 4 8 4 3w.sort(); // sortowaniecopy(w.begin(), w.end(), io); // 1 2 2 3 4 4 6 8w.unique(); // usuwa duplikaty pod warunkiem wcześniejszego posortowaniacopy(w.begin(), w.end(), io); // 1 2 3 4 6 8Li wi(t, t + 4);w.swap(wi); // wymiana zawartości między listą w oraz wicopy(w.begin(), w.end(), io); // 2 6 4 8copy(wi.begin(), wi.end(), io); // 1 2 3 4 6 8w.splice(w.end(), wi); // usuwanie i umieszczanie we wskazanym miejscucopy(w.begin(), w.end(), io); // 2 6 4 8 1 2 3 4 6 8copy(wi.begin(), wi.end(), io); // <pusto>wi.insert(wi.begin(), t, t+4); // wstawienie we wskazanym miejscucopy(wi.begin(), wi.end(), io); // 2 6 4 8

kontenery sekwencyjne (list) – przykład

wi.sort(); w.sort();w.merge(wi); // scalanie, w jest posortowane, wi czyszczonecopy(w.begin(), w.end(), io); // 1 2 2 2 3 4 4 4 6 6 6 8 8 8copy(wi.begin(), wi.end(), io); // <pusto>w.unique(); // usuwanie duplikatówcopy(w.begin(), w.end(), io); // 1 2 3 4 6 8w.assign(t, t+4); // wczytywanie sekwencji do istniejącego konteneracopy(w.begin(), w.end(), io); // 2 6 4 8wi.assign(5, 7); // wpisanie pięciu liczb 7copy(wi.begin(), wi.end(), io); // 7 7 7 7 7

// w.splice(w.begin()+2, wi); // błąd// error: no match for ’operator+’ in ’w. std::list<_Tp, _Alloc>::begin’// [with _Tp = int, _Alloc = std::allocator<int>]() + 2’

Li::iterator p = w.begin();p++; p++;w.splice(p, wi);copy(w.begin(), w.end(), io); // 2 6 7 7 7 7 7 4 8

// splice też w wersji // splice(cel, źródło, źródło_od)// splice(cel, źródło, źródło_od, źródło_do)

kontenery sekwencyjne (list) – przykład

w.reverse();copy(w.begin(), w.end(), io); // 8 4 7 7 7 7 7 6 2w.remove(4); // usunięcie liczby 4w.remove(7);copy(w.begin(), w.end(), io); // 8 6 2w.pop_front(); // usunięcie elementu z przoducopy(w.begin(), w.end(), io); // 6 2w.insert(w.end(), t, t+4);copy(w.begin(), w.end(), io); // 6 2 2 6 4 8// bool mniej5(int v) { return v < 5; }w.remove_if(mniej5);copy(w.begin(), w.end(), io); // 6 6 8

int t[] = {9, 3, 5};deque<int> dq(t, t + 3);for (int i = 0; i < dq.size(); ++i)cout << dq[i] << ' '; // 9 3 5dq.push_back(4);dq.push_back(3);dq.push_front(1);dq.push_front(2);copy(dq.begin(), dq.end(), io); // 2 1 9 3 5 4 3

kontenery sekwencyjne (list, deque) – przykład

• metody wspólne dla kontenerów skojarzeniowychcount() – z wartością klucza, zwraca liczbę wystąpień klucza w kontenerzefind() – zwraca iterator do elementu o podanym jako argument kluczuequal_range() – zwraca parę iteratorów ograniczających zbiór obiektów o wspólnej wartości klucza podanego jako argument (ma sens tylko dla kontenerów typu multi)lower_bound() – pierwszy element sekwencji o wartości identycznej z zadanąupper_bound() – ostatni element sekwencji o wartości identycznej z zadaną (formalnie adres za tym elementem)

• set < Key, Compare >, multiset < Key, Compare >szybkie zapamiętywanie i odzyskiwanie kluczy (dla multiset możliwe duplikaty)elementy porządkowane zgodnie z obiektem funkcji (drugi parametr szablonu to domyślnie less<T>elementy muszą obsługiwać odpowiednie operatory porównaniaobsługa iteratorów dwukierunkowych

• map < Key, Data, Compare >, multimap < Key, Data, Compare >szybkie zapamiętywanie i odzyskiwanie par wartości (klucz, wartość) (dla multimap możliwe duplikaty)elementy porządkowane zgodnie z obiektem funkcji (drugi parametr szablonu, domyślnie jest to less<T>) uwzględniającym wartość kluczaelementy muszą obsługiwać odpowiednie operatory porównaniaobsługa iteratorów dwukierunkowychdla zasobnika map dostępny jest operator[]

kontenery skojarzeniowe

ostream_iterator<int> out(cout, " ");int t[] = {6, 8, 2, 4, 8, 2};set<int> s(t, t+6); // kluczem jest int, bez duplikatów: 2 4 6 8s.insert(2); // to już jest w kontenerzecopy(s.begin(), s.end(), out); // 2 4 6 8cout << s.count(10); // 0cout << s.count(4); // 1set<int>::const_iterator q;q = s.find(6);if (q != s.end())copy(q, s.end(), out); // 6 8

typedef multiset<int> Muls;Muls ms(t, t+6); // teraz duplikaty są dopuszczalnems.insert(2);ms.insert(1);copy(ms.begin(), ms.end(), out); // 1 2 2 2 4 6 8 8cout << ms.count(2); // 3copy(ms.lower_bound(2), ms.upper_bound(4), out); // 2 2 2 4

pair<Muls::const_iterator, Muls::const_iterator> range;range = ms.equal_range(2);ms.erase(range.first, range.second);copy(ms.begin(), ms.end(), out); // 1 4 6 8 8

kontenery skojarzeniowe (set, multiset) – przykład

typedef map<int, string> Mis; // klucz int, wartość string

Mis m;m.insert(Mis::value_type(15, "Ala")); // dla map<Key, T> value_type oznacza pair<const Key, T>m.insert(pair<int, string>(11, "kota"));//m.insert(make_pair(12, "ma"));m.insert(make_pair<int, string>(12, "ma"));

// jeśli nie istnieje dany klucz jest automatycznie wstawiany do mapym[14] = "a"; m[13] = ”Jacek”;m[16] = "psa";

Mis::const_iterator p = m.begin();for(; p != m.end(); ++p)cout << p->first << ' ' << p->second << ' ';// 11 kota 12 ma 13 Jacek 14 a 15 Ala 16 psacout << m.count(90); // 0string tmp = m[90];cout << m.count(90); // 1

kontenery skojarzeniowe (map) – przykład

typedef multimap<int, string> Mulmis;

Mulmis mm(m.lower_bound(11), m.upper_bound(13));// wstawiamy raz jeszcze to samo, teraz możliwe są duplikatymm.insert(m.lower_bound(11), m.upper_bound(13));

Mulmis::iterator mp = mm.begin();for(; mp != mm.end(); ++mp)cout << mp->first << ' ' << mp->second << ' ';// 11 kota 11 kota 12 ma 12 ma 13 Jacek 13 Jacek

mp = mm.find(12); // iterator na pierwsze wystąpienie klucza 12// poniżej wstawienie w miejsce ”kota” (para przed pokazywaną przez mp)if (mp != mm.end()) (--mp)->second = "psa";

pair<Mulmis::iterator, Mulmis::iterator> r; // para iteratorówr = mm.equal_range(13);mm.erase(r.first, r.second); // wymazuje pary o kluczu 13mm.erase(12); // wymazuje wszystkie pary o kluczu 12

for(mp = mm.begin(); mp != mm.end(); ++mp)cout << mp->first << ' ' << mp->second << ' '; //11 kota 11 psa

kontenery skojarzeniowe (multimap) – przykład

• Nie dostarczają implementacji struktury danych, wykorzystują do tego zasobniki sekwencyjne.

• Nie obsługują iteratorów. • Pliki nagłówkowe <stack>, <queue>.• Wspólne metody: push(), pop()

stack<double> sdq; // domyślnie na dequestack<double, vector<double> > sdv; // może też być list

for (int i = 5; i < 10; ++i) {sdq.push(i/3.0); // ostatni wstawiony to 3.0sdv.push(i/2.0); // ostatni wstawiony to 4.5

}

template <typename T> void view_and_pop(T& v) {while (!v.empty()) {

cout << v.top() << ' '; // top() - zwraca referencję do szczytowego elementu stosu, bez zmiany // stanu stosu – można wielokrotnie odwołać się do tego elementu

v.pop(); // pop() - zdjęcie elementu ze szczytu stosu

}}

kontenery łączniki (stack, queue)

cout.setf(ios::showpoint); // flaga: pokaż przecinek w liczbiecout.precision(2); // ilość cyfr użytych po przecinku

view_and_pop(sdq); //3.00 2.67 2.33 2.00 1.67view_and_pop(sdv); //4.50 4.00 3.50 3.00 2.50

queue<double> qd; // domyślnie na dequeqd.push(1.6); qd.push(9.8); qd.push(0.8); qd.push(2.7);

cout << qd.back(); // 2.7while (!qd.empty()) {cout << qd.front() << ' '; // zwrócenie referencji do początku kolejkiqd.pop(); // usunięcie z kolejki

} // 1.60 9.80 0.800 2.70

priority_queue<double> pqd; // domyślnie na vector i z lesspqd.push(1.6); pqd.push(9.8); pqd.push(0.8); pqd.push(2.7);

view_and_pop(pqd); // 9.80 2.70 1.60 0.800priority_queue<double, deque<double>, greater<double> > pqdd;

pqdd.push(1.6); pqdd.push(9.8); pqdd.push(0.8); pqdd.push(2.7);view_and_pop(pqdd); // 0.800 1.60 2.70 9.80

kontenery łączniki (stack, queue, priority_queue) – przykład

Przegląd kontenerów – std::forward_list ( C++11 )

Jednokierunkowa lista obsługiwana przez iterator "do przodu" (forward iterator), czyli np. niemożliwa jest operacja --iter;

Cel: zerowy narzut w implementacji kodu, tak jak w zwykłej jednokierunkowej liście z C.Konsekwencja: kontener ten nie ma takich samych metod jak inne kontenery tylko:

insert_after, emplace_after, erase_after, before_begin() / cbefore_begin()

#include <forward_list>std::forward_list<int> f_lista { -5, -2, 3, 0, 4, 1 };auto iter = std::find( f_lista.cbegin(), f_lista.cend(), 4 );

std::forward_list ( C++11 )

insert_after (włóż element za wskazanym elementem) w miejsce insert - 5 przeciążonych wersjiemplace_after (skonstruuj element w miejscu za wskazanym elementem, konstruktor może otrzymać ewentualnie argumenty)

erase_after (usuń element na wskazanej pozycji lub ze wskazanego zakresu - 2 przeciążone wersje)W przypadku iteratorów oprócz begin/cbegin oraz end/cend jeszcze:before_begin() / cbefore_begin() - zwraca adres przed pierwszym elementem, ale tylko w celu użycia przez inne metody: insert_after(), emplace_after(), erase_after(), splice_after()Brak metod: size() czy push_back() bo ich obsługa powodowałaby koszt O(n)

template< class... Args >iterator emplace_after( const_iterator pos, Args&&... args );

Tablice mieszające – hash tables ( C++11 )

Potrzeba nieuporządkowanych kontenerów skojarzeniowych: operacje szukania, wstawiania i usuwania, mają średni stały koszt (niezależny od wielkości kontenera!)• W celu uniknięcia kolizji z rozwiązaniami spoza standardu, wybrano

nazwy zaczynające się przedrostkiem unordered_

oraz dodatkowa funkcjonalność zdefiniowana też w nagłówku <functional>

#include <unordered_set> // tu również unordered_multiset#include <unordered_map> // tu również unordered_multimap

Każdy kubełek (bucket) ma swój łańcuch elementów

Tablice haszujące ( C++11 )

Pełnoprawne kontenery STL, wraz z typowymi iteratorami i metodami składowymi:

iterator / const_iterator (tylko do przodu, brak reverse_iterator)begin / end, cbegin / cend (brak rbegin / rend oraz crbegin/crend)insert / erase, size, swap itd.

Są również: find, count, equal_range(jeśli nie znajdzie, zwraca parę kontener.end(), kontener.end() )

Nie ma: lower_bound, upper_boundKontener unordered_map udostępnia operator[] oraz metodę at()

Operatory relacji (zbyt kosztowne) nie są dostępne: <, <=, >=, >Dostępne są: == i != ale w oparciu o zawartość (a nie uporządkowanie)

Szablon std::hash ( C++11 )

Mieszający (haszujący) obiekt funkcyjny – dostarczony za pomocą szablonu std::hashjako drugi parametr (domyślny) kontenerów: unordered_set, unordered_multiset, unordered_map, unordered_multimapDostępny dla wszystkich typów wbudowanych oraz specjalizowany dla: string (i odmian u16string, u32string, wstring), inteligentnych wskaźników (unique_ptr, shared_ptr) oraz kilku innych (error_code, bitset, type_index, thread::id, vector<bool>)

template<class Value,class Hash = std::hash<Value>,class Pred = std::equal_to<Value>,class Alloc = std::allocator<Value>>class unordered_set { … };

template<class Key,class T,class Hash = std::hash<Key>,class Pred = std::equal_to<Key>,class Alloc = std::allocator<std::pair<const Key, T>>>class unordered_map { … };

Metody kontenerów haszujących ( C++11 )

zwraca numer kubełka (bucket) w jakim jest obiekt o kluczu keyval

zwraca liczbę kubełków

zwraca rozmiar kubełka o numerze nbucket

zwraca (float)size() / (float)bucket_count() czyli średnie obłożenie kubełków elementami

czyta lub ustawia (sugeruje) maksymalne obciążenie liczbą elementów na kubełek

wymuszenie przebudowy tablicy haszującej z liczbą kubełków (co najmniej) nbuckets

size_type bucket_count() const;

size_type bucket(const Key& keyval) const;

void rehash(size_type nbuckets);

size_type bucket_size(size_type nbucket) const;

float load_factor() const;

float max_load_factor() const;void max_load_factor(float factor);

C++11 – rdzeń i ułatwienia

override i final (nie są to słowa kluczowe) • zasłaniamy funkcję z klasy bazowej i jasno to zaznaczamy

• można też zabezpieczyć przed dziedziczeniem lub zasłonięciem

class Base final;class Derived : Base { }; // błądclass Base2 { public:

virtual void fun( float ) final;};class Derived2 : public Base2 { public:

void fun( float ); // błąd };

class Base { public:virtual void fun1( float ); // musi być wirtualna (żeby poniższe było możliwe)

};class Derived : public Base { public:

virtual void fun1( float ) override; // do wykrywania zmian w met. klasy bazowej};

wyrażenia lambda ( C++11/14 )

wyrażenie lambda = funktor, który można zdefiniować w miejscu użycia

• captures – „domknięcia”, zewnętrzne dostępne zmienne, przekazywane do wyrażenia poprzez wartość lub referencję

• params – argumenty wywołania, jeśli jest puste, to można pominąć• ret – typ zwracany, jeśli brak return lub jest jedno, można pominąć• statements – ciało wyrażenia lambda

Klasa k;// przez wartość, bez argumentuauto funktor1 = [ k ] { for ( int i=0; i<10; ++i ) fun( k ); }; funktor1(); // dzięki auto możemy łatwo przechować typ i potem wywołać// przez referencję, jest też argumentauto funktor2 = [ &k ] (const int& i) { return fun( k, i ); };int i = 12;funktor2( i );

[captures] (params)opt ->opt ret { statements; }

wyrażenie lambda – przechwycenie dopełnień ( C++11 )

Możliwe warianty „dopełnień” przejmowanych przez lambda:

Typ zwracany przez wyrażenie ->ret konieczność podania tylko, gdy występuje w wyrażeniu lambda więcej niż jedno return

vector<double> v;std::transform( v.begin(), v.end(), v.begin(),

[](double d)->double{

return std::sqrt( std::abs( d ) );}

);

[] nic nie przejmuje[=] przejmuje wszystko przez wartość (kopia)[=,&x] wszystko przez wartośc za wyjątkiem x – przez referencję[&] przejmuje wszystko przez referencję[&, x] wszystko przez referencję, za wyjątkiem x – przez wartość

wyrażenie lambda – przekazywanie składowych

Jeśli chcemy przekazać do wyrażenia którąś składową klasy, to musimy przekazać „this” (albo wprost, albo przez wartość =, albo referencję &):

class Foo {vector<int> v = { 1, 2, 3, 4 };int val = 1;public:

auto doIt() {return find_if( v.begin(), v.end(), [this](int i){ return i > val; } );

}};int main() {

Foo f;cout << *( f.doIt() ) << endl; // 2

}

Można też:[=] ( int i ) {…[&] ( int i ) {…

// można tez od razu wykonaćint result = [](int input) { return input * input; }(10);cout << result << endl;

wyrażenie lambda – przykłady ( C++11 )

[ t1, &t2 ] () { fun( t1, t2); }class Funktor {

T1 t1, T2& t2;public:

Funktor( T1 a1, T2& a2 ) : t1(a1), t2(a2) { }void operator()() { fun( t1, t2); }

};

dedukcja typu (auto) w wyrażeniu lambda działa w C++14[] ( auto p1, const auto& p2 ) { fun( p1, p2); } // w C++11 błądoczywiście typ zwracany jest też „dedukowany” ( skoro nie jest jawnie napisany w postaci ->ret )

[] ( T1 p1, const T2 & p2 ) { fun( p1, p2); }class Funktor {

public:void operator() ( T1 p1, const T2& p2 ) { fun( p1, p2); }

};

wyrażenie lambda – przykład z auto

#include<iostream>#include<complex>

int main() {// przechowaj uogólnione wyrażenie Lambda w zmiennej funcauto func = [](auto input) { return input * input; };

// Przykłady użycia:// kwadrat typu intstd::cout << func(10) << std::endl;

// kwadrat typu doublestd::cout << func(2.345) << std::endl;

// kwadrat na typie complexstd::cout << func(std::complex<double>(3, -2)) << std::endl;

}

wyrażenie lambda – przypadki ( C++11 )

int f( int i ) {int j = i*i;auto g = [&]( int k ) { return j+k; };j += 3;g( 3 ); // wywołanie lokalnej funkcji

}

Chcemy poprzez wyrażenie Lambda podać algorytm sortowania mapy:

Dzięki Lambda można zapisać „funkcję” zagnieżdżoną w innej funkcji:

Rekurencja (możliwa dzięki szablonowi function):std::function<int(int)> factorial = [&](int x) {

return (x==1) ? 1 : ( x* factorial(x-1)); }; }

auto f = [&](int x, int y) { return x > y; }map< int x, int y, decltype( f ) > m ( f ); // w konstruktorze map też

// można podać kryterium sortowania!

wyrażenie lambda – miejsca użycia ( C++11 )

Wiele algorytmów mających charakter „pętli” po zasobniku (na przykład for_each, copy, find, remove, transform), jest dobrym miejscem do efektywnego użycia wyrażeń lambda.

Kilka alternatyw do wyboru:std::string s;// pętla for ręcznie przebiegająca iteratoremfor ( auto it = s.begin(); it != s.end(); ++it ) { cout << *it << endl; }// pętla for według składni przebiegającej po całym zakresiefor ( auto w : s ) { cout << w << endl; }// za pomocą copy oraz iteratora strumienia wyjściowegocopy( s.begin(), s.end(), ostream_iterator<char>(cout, "\n") );// za pomocą for_each i wyrażenia lambdafor_each( s.begin(), s.end(), [](char& w) { cout << w << endl; } );

vector<int> v;auto it = find_if( v.cbegin(), v.cend(), [](int i){ return i>0 && i<10; } );

Można zdefiniować „własny rodzaj pętli”:

Można napisać np. tak:

wyrażenie lambda – własne pętle ( C++11 )

do_while ( [&] {// wykonuj aż…}, [] { return !wykonane(); }

);repeat_until( [&] {

// wykonuj aż…}, []{ return wykonane(); }

);

// zamiast klasycznej pętli z c++do {

// wykonuj aż…} while ( !wykonane() );

Mamy zmienną, która powinna być const, ale po drodze trzebacoś do niej przypisać:

Można oczywiście napisać funkcję, która obliczy potrzebną wartość,a potem zainicjalizuje zmienną:const int i = funkcja();Lepsze rozwiązanie:

wyrażenie lambda – const zachowane ( C++11 )

const int i = [=] {// coś liczymy i ostatecznie zwracamy jakąś wartośćreturn val;

} ();

int i = 0; // wartość początkowa// tu coś liczymy, w wyniku czego x = … nabywa jakąś wartość// teraz i już mogłoby być const…

Rozwiązanie z zachowaniem stałości:

wyrażenie lambda – const, inny przykład ( C++11 )

const int i = [=] -> int { try { // tu coś liczymy, a wartość zwracamy

return val;// … chyba że przed return nastąpi wyjątek

} catch ( TypWyjatku ) {return 0;

} ();

int i = 0; // wartość początkowatry { // tu coś liczymy, w wyniku czego x = … nabywa jakąś wartość

// chyba, że zgłoszony zostanie wyjątek} catch ( TypWyjatku ) {

i = 0;}

wyrażenie lambda – przykład z algorytmem sortowania

#include<vector>#include<numeric>#include<algorithm>

int main() {std::vector<int> V(10);// std::iota do wygenerowania sekwencji integer-ów od 1 (z krokiem „o jeden”)std::iota(V.begin(), V.end(), 1); // wypełni 1, 2, 3, …, 10

std::cout << "Oryginalne dane" << std::endl;std::for_each(V.begin(), V.end(), [](auto i) { std::cout << i << " "; });std::cout << std::endl;

// sortowanie danych z użyciem std::sort oraz lambdastd::sort(V.begin(), V.end(), [](auto i, auto j) { return (i > j); });

// drukowanie danych z użyciem std::for_each oraz lambdastd::cout << "Posortowane dane" << std::endl;std::for_each(V.begin(), V.end(), [](auto i) { std::cout << i << " "; });std::cout << std::endl;

}

Obiekty klas, w których przeciążono operator funkcyjny (wywołania funkcji)• Najczęstsze wykorzystaniejako predykaty (orzeczniki logiczne) używane w ogólnych algorytmach• Mogą zawierać dodatkowe dane potrzebne do wykonania operacji

Obiekty funkcyjne biblioteki standardowej – wzorce klasZdefiniowane w pliku nagłówkowym <functional>

arytmetyczne, relacyjne, logiczne

plus<T>minus<T>multiplies<T>divides<T>modulus<T>negate<T>

equal_to<T>not_equal_to<T>greater<T>greater_equal<T>less<T>less_equal<T>

logical_and<T>logical_or<T>logical_not<T>

obiekty funkcyjne (funktory)

multiplies<double> m;equal_to<char> e;logical_or<int> l;cout<< m(2.5, 3.1) <<” ”<< e(’a’, ’a’) <<” ”<< l(3, 1); // 7.75 1 1

int t[] = { 5, -7, 3, 1, -1, 2 };list<int> l(t, t+6); // w C++11 list<int> l {5, -7, 3, 1, -1, 2};l.sort( less<int>() );

// jest też algorytm sort( RandomIt, RandomIt, Pred );// ale działa na iteratorach „dostępu swobodnego”, a takie// nie obsługują listy – dlatego lista ma własną metodę sort (i inne)

ostream_iterator<int> out(cout, " ");copy(l.begin(), l.end(), out); // -7 -1 1 2 3 5

vector<int> v(t, t+6);sort( v.begin(), v.end(), greater<int>() );copy( v.begin(), v.end(), out ); // 5 3 2 1 -1 -7

obiekty funkcyjne – przykłady

struct intsort {bool operator() (const int& l, const int& r) { return l < r; }

};// można użyć własny funktorl.sort( intsort() ); // dzięki szablonowi można pokazać dowolny typ sekwencyjnytemplate<typename T>struct prezentuj {

prezentuj(ostream& o, string s = "") : strumien(o), separator(s) { }void operator () (const T& p) { strumien << p << separator; }

private:ostream& strumien;string separator;

};template <typename T>void view(T con) {

for_each(con.begin(), con.end(), prezentuj<typename T::value_type>(cout, " -=- "));}// w programie...view(v); view(l);

obiekty funkcyjne – przykłady

Aby umożliwić adaptorom i innym komponentom operacje na obiektach funkcyjnych, przyjmujących jeden lub dwa argumenty, wymagane jest aby funktory te dostarczały przypisanie (typedef) odpowiednich typów nazwom argument_type i result_type(dla funktorów jednoargumentowych) oraz first_argument_type, second_argument_type i result_type (dla funktorów dwuargumentowych).

Standardową procedurą ułatwiającą te definicje, jest dziedziczenie z następujących klas bazowych:

funktory – typy argumentów i zwracanych wartości

template <class Arg, class Result>struct unary_function {

typedef Arg argument_type;typedef Result result_type;

};

template <class Arg1, class Arg2, class Result>struct binary_function {

typedef Arg1 first_argument_type;typedef Arg2 second_argument_type;typedef Result result_type;

};

wzorce klas pozwalające na zmianę funkcjonalności obiektów funkcyjnychnie używa się ich bezpośrednio lecz za pomocą funkcji zwracających obiekty tych klaswiązadło (poniższe to „historyczne” wersje dla C++98)przekształca dwuargumentowy funktor w jednoargumentowy, związując jeden z argumentów z konkretną wartością

bind1st – wiąże pierwszy argument – szablon funkcji tworzący funktor typu binder1stbind2nd – wiąże drugi argument – szablon funkcji tworzący funktor typu binder2nd

– funktory te przechowują dwa argumenty, przekazane w wywołaniu bind1st() / bind2nd()

– pierwszy argument musi być dwuargumentową funkcją (lub funktorem) – operator() obiektu binder1st / binder2nd wywołuje przekazaną funkcję dwuargumentową, podając jej otrzymany argument wywołania i zapamiętaną wartość zadaną, odpowiednio jako pierwszy / drugi argument// zawartość listy: -7 -1 1 2 3 5l.remove_if(bind1st( less<int>(), 1 )); // 1 < prawy argument kolejne wyrazy z listy// z oryginalnej zawartości zostaną: -7 -1 1l.remove_if(bind2nd( less<int>(), 1 )); // lewy argument kolejne wyrazy z listy < 1// z oryginalnej zawartości zostaną: 1 2 3 5A gdyby użyć np. equal_to<int> ? Wtedy wszystko jedno czy bind1st czy bind2nd –bo argumenty operatora == są symetryczne.

adaptory obiektów funkcyjnych – wiązadła

std::bind ( C++11 ), bind1st, bind2nd – „przestarzałe”

bind1st, bind2nd – są ograniczone, wiążą tylko pierwszy lub drugi argument– nie mogą wiązać funkcji z argumentem typu „referencja do”– nierzadko wymagają adaptacji obiektów funkcyjnych

(za pomoca ptr_fun, mem_fun, mem_fun_ref)

Zastępcze obiekty pozwalają na mapowanie argumentów bind z argumentami wielkości wywoływalnej. Notacja: _n oznacza n-ty argument przekazany do obiektu funkcyjnego zwracanego przez bind.

Uwaga: zastępczy obiekt ma nazwę _1 (dla kolejnych parametrów _2 itd.)Te obiekty są formalnie umieszczone w przestrzeni nazwstd::placeholders. Jeśli widzimy błąd kompilacji, trzeba dodać jedno z:using namespace std::placeholders; // cała przestrzeń nazwusing std::placeholders::_1; // dyrektywa użycia konkretnego obiektu

obiekt_funkcyjny std::bind( wielkość_wywoływalna, 1arg, 2arg, …, Narg );

l.remove_if( bind ( less<int>(), 1, _1 )); // to samo co bind1st wcześniejl.remove_if( bind ( less<int>(), _1, 1 )); // to samo co bind2nd wcześniej

std::bind - przykłady

#include <iostream>#include <functional>using namespace std;using namespace std::placeholders;

int suma1( int n1, int n2, int& n3, int& n4 ) {n3 = 0; n4 = 0;return n1+n2+n3+n4;

}int main() {

cout << bind( suma1, 1, 2, 3, 4 )() << endl; // 3, argumenty przez wartość

int var1 = 1, var2 = 2, var3 = 3, var4 = 4;cout << bind( suma1, var1, var2, var3, var4 )() << endl; // 3, jak wyżejcout << var1 << ", " << var2 << ", " << var3 << ", " << var4 << endl; // 1, 2, 3, 4

var1 = 1, var2 = 2, var3 = 3, var4 = 4;cout << bind( suma1, var1, var2, ref(var3), var4 )() << endl; // 3cout << var1 << ", " << var2 << ", " << var3 << ", " << var4 << endl; // 1, 2, 0, 4

var1 = 1, var2 = 2, var3 = 3, var4 = 4;cout << bind( suma1, var1, _2, _1, var4 )(var2,var3) << endl; // 4 placeholders przez referencjęcout << var1 << ", " << var2 << ", " << var3 << ", " << var4 << endl; // 1, 0, 3, 4

cout << bind( suma1, _1, _2, _5, var4 )(77, var2, var3, var4, var1) << endl; }

std::bind – dalsze przykłady

void wypisz( ostream& strumien, int n ) {strumien << n << endl;

}

bind( wypisz, ref(cout), 777)();

list<int> ll { 1,2,3,4,5 };for_each( ll.begin(), ll.end(), bind(wypisz, ref(cout), _1) );

Wysłanie do strumienia:

Zabawy z initializer_list:int suma(initializer_list<int> il) {

int sum {0};for ( auto n : il ) { cout<<n<<" "; sum+=n; }return sum;

}

// w programie:// to nie działa: bind( suma, { 1, 2, 3, 4, 5 } )();int sumka = bind( suma, initializer_list<int>{ 1, 2, 3, 4, 5 } )(); // okcout << bind( suma, _1 )( initializer_list<int>{ 1, 2, 3, 4, 5 } ); // ok

negatorto funkcja, która zmienia wartość logiczną funktora na wartość przeciwną

not1 – odwraca wartość logiczną funktora jednoargumentowegonot2 – odwraca wartość logiczną funktora dwuargumentowego

adaptory obiektów funkcyjnych – negatory (C++98)

// negacja unary_functiontemplate <class Predicate>

unary_negate<Predicate> not1(const Predicate& pred);

// negacja binary_functiontemplate <class Predicate>

binary_negate<Predicate> not2(const Predicate& pred);

// zawartość listy: -7 -1 1 2 3 5l.remove_if( not1( bind1st( equal_to<int>(), 3 ) ) ); // bind nie działa… patrz dalej// z oryginalnej zawartości zostanie: 3

l.sort( not2( less<int>() ) );// to samo jak użycie do sortowania greater<int>// czyli oryginalna zawartość: 5 3 2 1 -1 -7

Zamiast samodzielnie dziedziczyć z klas unary_function czy binary_function, można użyć specjalnego adaptora ptr_fun, przyjmującego wskaźnik do funkcji i konwertującego ją do postaci obiektu funkcyjnego. Funkcje te muszą być jedno- lub dwuargumentowe (nie mogą być bezargumentowe).

adaptory wskaźników do funkcji – ptr_fun (C++98)

template <class Arg, class Result>pointer_to_unary_function<Arg, Result>

ptr_fun(Result (*f)(Arg)); // jednoargumentowa

template <class Arg1, class Arg2, class Result>pointer_to_binary_function<Arg1,Arg2,Result>

ptr_fun(Result (*f)(Arg1, Arg2)); // dwuargumentowa

Typy zwracane przez funkcję ptr_fun rzeczywiście dziedziczą z odpowiednich klas:template <class Arg, class Result>class pointer_to_unary_function :

public unary_function<Arg, Result>;

template <class Arg1, class Arg2, class Result>class pointer_to_binary_function :

public binary_function<Arg1,Arg2,Result>;

ptr_fun – przykład (i alternatywy w C++11)

#include <string>#include <iostream>#include <algorithm>#include <functional>

bool isvowel(char c) {return std::string("aeoiuAEIOU").find(c) != std::string::npos;

}

int main(){

std::string s = "Hello, world!";std::copy_if(s.begin(), s.end(), std::ostreambuf_iterator<char>(std::cout),

std::not1(std::ptr_fun(isvowel)));// C++11 alternatywy: // std::not1(std::cref(isvowel)));// std::not1(std::function<bool(char)>(isvowel)));// UWAGA: bind nie działa z not1 gdyż bind nie definiuje argument_type// jak tego oczekuje not1}

Gdy chcemy zaadaptować metodę z klasy, przekazując ją przez wskaźnik do składowej klasy, używamy adaptor mem_fun, który generuje obiekt funkcyjny. Jego metoda, która ma być docelowo użyta, jest wywołana za pomocą wskaźnika do tego obiektu. Są wersje dla metod z przydomkiem const i dla metod modyfikujących (jak poniżej):

adaptory do metod składowych – mem_fun (C++98), mem_fn (C++11)

template<class S, class T> mem_fun_t<S,T> mem_fun(S (T::*f)()); // bezargumentowa

template<class S, class T, class A> mem_fun1_t<S,T,A> mem_fun(S (T::*f)(A)); // jednoargumentowa

Tym razem zwracany typ to:template <class S, class T> class mem_fun_t : public unary_function<T*, S> { public:

explicit mem_fun_t(S (T::*p)());S operator()(T* p) const;

};template <class S, class T, class A> class mem_fun1_t : public binary_function<T*, A, S>;

Adaptor mem_fun_ref generuje obiekt funkcyjny, którego metoda jest wywoływany za pomocą referencji do obiektu (czyli bezpośrednio na rzecz danego obiektu). Są wersje dla metod z przydomkiem const i dla metod modyfikujących (poniżej bez const):

adaptory wskaźników do metod – mem_fun_ref (C++98) , mem_fn (C++11)

template<class S, class T> mem_fun_ref_t<S,T> mem_fun_ref(S (T::*f)()); // bezargumentowa

template<class S, class T, class A> mem_fun1_ref_t<S,T,A> mem_fun_ref(S (T::*f)(A)); // jednoargumentowa

Zwracany typ to:template <class S, class T> class mem_fun_ref_t : public unary_function<T, S> { public:

explicit mem_fun_ref_t(S (T::*p)());S operator()(T& p) const;

};template <class S, class T, class A> class mem_fun1_ref_t : public binary_function<T, A, S>;

const char* tab[] = {"alice", "has", "a", "cat"};replace_if( tab, tab+4,

not1(bind2nd(ptr_fun(strcmp), ”alice”)),”jarek” );// przykład ryzykowny, jeśli łańcuch ”jarek” byłby dłuższy niż łańcuch ”alice”...ostream_iterator<const char*> out (cout, " ");copy(tab, tab+4, out); // jarek has a cat

struct A { void fun(int i) {cout<<"A::fun = "<< i <<endl;} };struct B { int fun2(int i) const { return 2*i; } };int x[] = {-4, -2, 0, 1, 3};// za pomocą wskaźnika, czyli mem_funvector<A*> va;va.push_back(new A);va.push_back(new A); // hej, a kto to potem usunie? ;-)for_each(va.begin(),va.end(),bind2nd(mem_fun(&A::fun), 5));// za pomocą referencji do obiektu, czyli mem_fun_refvector<B> v;v.push_back(B());v.push_back(B()); // tu posprząta się samo...transform(v.begin(), v.end(), x, ostream_iterator<int>(cout, " "), mem_fun_ref(&B::fun2) );

// -8 -4

ptr_fun, mem_fun, mem_fun_ref – przykłady (C++98)

• bind może równieżzastępować adaptory!• jeśli nie ma argumentówto mem_fn wygodniejsze

class Klasa { public:Klasa(int n) : val( n ) { }void foo() { cout << val; }void bar() const { cout << 10+val; }

private:int val;

};

template<typename T>struct pointer {

T* operator() (T& t) { return &t; }};

Klasa tab[] = {9, 1, 8, 2, 7, 3, 6, 4, 5, 0};// for_each(tab, tab+10, mem_fun_ref(&Klasa::foo)); // 9182736450for_each( tab, tab+10, bind(&Klasa::foo,_1) ); // 9182736450list<const Klasa*> lista;transform(tab, tab+10, back_inserter(lista), pointer<Klasa>());// for_each(lista.begin(), lista.end(), mem_fun(&Klasa::bar)); // 19111812171316141510for_each(lista.begin(), lista.end(), bind(&Klasa::bar,_1)); // 19111812171316141510

bind zastępuje też adaptory (C++11)

• piszemy kod, wykonujący oczekiwane zadania• kod obsługujący sytuacje niepożądane otacza kod pożądany

• większa czytelność kodu• łatwiejsza kontrola nad błędami dowolnego wywołania danej funkcji• nie można zignorować zgłoszonego wyjątku

• zgłaszany (throw) wyjątek może być dowolnego typu (wbudowanego, abstrakcyjnego), zwykle używa się specjalnie napisanych klas

class UserError {const char* const errInfo;

public:UserError(const char* const msg = 0) : errInfo(msg) {}

};void fun() {

// funkcja wyrzuci wyjątek typu UserErrorthrow UserError("jestem beznadziejną funkcją");

}int main() {

// na razie bez bloku tryfun();

}

Obsługa wyjątków [ exception handling ]

Zgłaszanie wyjątku będącego obiektem klasy - ograniczenia na rodzaj klas, których można użyć do tworzenia obiektów wyjątków

• posiada odpowiedni dostępny konstruktor do utworzenia obiektu wyjątku• posiada dostępny konstruktor kopiujący• posiada dostępny destruktor• klasa nie jest klasą abstrakcyjną

Etapy zgłoszenia wyjątku będącego obiektem klasy1. wyrażenie throw tworzy obiekt tymczasowy

- wywołanie odpowiedniego konstruktora klasy 2. utworzenie kopii obiektu tymczasowego - (wywołanie konstruktora kopiującego klasy)

utworzenie obiektu reprezentującego obiekt wyjątku w celu przekazania go do procedury obsługi

3. usunięcie obiektu tymczasowego - (wywołanie destruktora) wykonywane przed przystąpieniem do szukania procedury obsługi

• zwracany jest obiekt (wyjątek) przez wartość i następuje wyjście z danego bloku zasięgu lub z funkcji

• można definiować wyrzucenie dowolnie wielu typów obiektów• obiekty lokalne utworzone do czasu wystąpienia wyjątku są usuwane

Obsługa wyjątków [ co się dzieje - wymagania ]

• obsłużenie wyjątków wymaga wstawienia kodu rzucającego wyjątki do bloku try, zgłoszone wyjątki obsługiwane są zaraz za tym blokiem

• dla każdego typu wyjątku potrzebna procedura obsługi wyjątku

try {// tu może zostać rzucony wyjątek

} catch (typA id1) {// obsługa wyjątku typu typA

} catch (typB id2) {// obsługa wyjątku typu typB

} catch (typC) {// jak widać tutaj nie ma identyfikatora// widocznie do obsługi wyjątku typu typC// wystarczy sama informacja o złapanym typie wyjątku

}

• wejście do bloku catch oznacza obsłużenie wyjątku (co wcale nie musi oznaczać rozwiązanie problemu…)

• wykonana jest tylko jedna pasująca fraza catch• program jest kontynuowany za blokiem try• możliwe warianty: zakończenie lub kontynuacja (trzeba, po obsłużeniu wyjątku, jawnie

wywołać funkcję która wygenerowała wyjątek - czyli np. blok try z funkcją w jakiejś pętli)

Obsługa wyjątków [ przechwytywanie wyjątku ]

class A { public:A() : a_(++c) { cerr << "ctorA:" << a_ << ' '; }A(const A& aa) : a_(++c) { cerr << "cctorA:" << a_ << ' '; }~A() { cerr << "dtorA:" << a_ << ' '; }int a() const { return a_; }

protected:int a_;static int c; // licznik powstałych obiektów

};int A::c = 0;

void fun1a() { throw A(); }int main() {

// fun1a(); - ctorA:1 terminate called after throwing an instance of 'A' Abort

try { fun1a(); } // poniżej: ctorA:1 wyjatekA:1 dtorA:1catch (const A& a) { cerr << "wyjatekA:" << a.a() << ' '; }// poniżej: ctorA:1 cctorA:2 wyjatekA:2 dtorA:2 dtorA:1

// catch (const A a) { cerr << "wyjatekA:" << a.a() << ' '; }}

Obsługa wyjątków [ przykład sytuacyjny ]

• szukanie najbliższej procedury obsługi wyjątków według kolejności bloków catch w kodzie• nie musi zajść dokładna zgodność typów• aby nie tworzyć kolejnej kopii obiektu wyjątku zamiast przechwycenia wartości lepiej

korzystać ze (stałej) referencji• podczas dopasowywania nie są przeprowadzane automatyczne konwersje typów

class A {}; class B { public: B(const A&) {} /* konstruktor konwertujący */ };void fun() { throw A(); }int main() {

try {fun();

} catch (B&) {// nie, bo nie zajdzie konwersja

} catch (A&) {// pasujący typ

}}

• obiekt lub referencja do obiektu klasy pochodnej pasują do procedury catch dotyczącej klasy bazowej (pamiętajmy o "przycinaniu" obiektu jeśli przekazywany jest przez wartość)

• kolejność bloków catch: od szczegółowego (klasa pochodna) do ogólnego (klasa bazowa)

Obsługa wyjątków [ dopasowanie wyjątków ]

• Wyjątek należący do klasy pochodnej może być obsłużony przez klauzulę catchprzeznaczoną dla wyjątków klasy podstawowej

// class A - tak jak poprzednio dwa slajdy wcześniejclass B : public A { public:

B() { cerr << "ctorB:" << a_ << ' '; }B(const B& bb) : A(bb) { cerr << "cctorB:" << a_ << ' '; }~B() { cerr << "dtorB:" << a_ << ' '; }

};void fun1b() { throw B(); }int main() {

try { fun1b(); } // kompilator ostrzeże, że pierwsza klauzula przechwyci wyjątek// przez referencję ctorA:1 ctorB:1 wyjatekA:1 dtorB:1 dtorA:1// przez wartość ctorA:1 ctorB:1 cctorA:2 wyjatekA:2 dtorA:2 dtorB:1 dtorA:1catch (const A& a) { cerr << "wyjatekA:" << a.a() << ' '; }catch (const B& b) { cerr << "wyjatekB:" << b.a() << ' '; }

}

Obsługa wyjątków [ dopasowanie wyjątków ]

• Wybór klauzuli obsługi wyjątku według zasady pierwszego dopasowania -obsługi dokona pierwsza napotkana klauzula mogąca obsłużyć wyjątek, a nie najlepiej dopasowana

int main() {try { fun1b(); } // przez referencję ctorA:1 ctorB:1 wyjatekB:1 dtorB:1 dtorA:1 // przez wartość ctorA:1 ctorB:1 cctorA:2 cctorB:2 wyjatekB:2 dtorB:2 dtorA:2// dtorB:1 dtorA:1catch (const B& b) { cerr << "wyjatekB:" << b.a() << ' '; }catch (const A& a) { cerr << "wyjatekA:" << a.a() << ' '; }

}• Podczas tworzenia obiektu wyjątku nie bada się aktualnego typu obiektuvoid fun2() { B b; A* a = &b; throw *a; }try { fun2(); }catch (const B& b) { cerr << "wyjatekB:" << b.a() << ' '; }// klauzula poniżej: ctorA:1 ctorB:1 cctorA:2 dtorB:1 dtorA:1 wyjatekA:2 dtorA:2 catch (const A& a) { cerr << "wyjatekA:" << a.a() << ' '; }

Obsługa wyjątków [ dopasowanie wyjątków ]

• procedura przechwytująca wyjątek dowolnego typucatch (…) {

// dowolny wyjątek złapany, czyli taka fraza powinna być ostatnia// nic nie wiemy o typie, można zwolnić zasoby i ponownie rzucić wyjątekthrow; // przechodzi do procedur obsługi wyjątków wyższego poziomu

}• ponowne zgłoszenie wyjątku powoduje zgłoszenie pierwotnego obiektu

reprezentującego wyjątek (czyli "rzucenie" go dalej)void fun3() {

try { fun1b(); } // przypominam: void fun1b() { throw B(); }catch (const A& a) { cerr << "wyjatekA:" << a.a() << ' '; throw; }

}try { fun3(); }// przez referencję: ctorA:1 ctorB:1 wyjatekA:1 wyjatekB:1 dtorB:1 dtorA:1// przez wartość: ctorA:1 ctorB:1 wyjatekA:1 cctorA:2 cctorB:2 wyjatekB:2// dtorB:2 dtorA:2 dtorB:1 dtorA:1catch (const B& b) { cerr << "wyjatekB:" << b.a() << ' '; }catch (const A& a) { cerr << "wyjatekA:" << a.a() << ' '; }

Obsługa wyjątków [ łap cokolwiek, ponownie rzuć ]

• wyjątek nie przechwycony na żadnym poziomie woła funkcję biblioteczną terminate() (plik nagłówkowy <exception>)

• funkcja terminate() jest wołana również gdy– destruktor obiektu lokalnego rzuci wyjątek podczas obsługi wyjątku– wyjątek zgłosi konstruktor lub destruktor obiektu statycznego lub globalnego

• domyślnie funkcja terminate() wywołuje funkcję abort() kończącą natychmiast działanie programu (core dump – można "debugować") – nie są wywołane destruktory obiektów globalnych i statycznych

• własna funkcja obsługi takiej sytuacji set_terminate() jako argument przyjmuje wskaźnik do bezargumentowej i nic nie zwracającej nowej funkcji obsługi, zwraca wskaźnik do poprzedniej funkcji obsługi (można ją odtworzyć)void newTerminate() { exit(0); /* powinna zakończyć działanie programu */ }void (*oldTerminate)() = set_terminate(newTerminate); // żeby ewentualnie odtworzyćclass A { public:

class B();void fun() { throw B(); }~A() { throw 'c'; }

};int main() {

try { A a;a.fun();

} catch (...) { /* nic */ }}

Obsługa wyjątków [ sytuacja bez wyjścia ]

wyjątek rzucony w destruktorze podczaslikwidacji obiektu przy obsłudze wyjątkupowoduje wywołanie funkcji terminate(),w tym przypadku newTermiate()

• wykorzystanie metod wirtualnych obiektów reprezentujących wyjątki

class A {// ... jak poprzedniopublic:

virtual void obslugaWyj() const { cerr << "Obsluga WyjatekA:" << a_ << ' '; }};class B : public A {

// ... jak poprzedniopublic:

virtual void obslugaWyj() const { cerr << "Obsluga WyjatekB:" << a_ << ' '; }};

try { fun1b(); } // jak poprzednio, rzuca wyjątek typu Bcatch (const A& a) { a.obslugaWyj(); }

// przez referencję – polimorfizm!// ctorA:1 ctorB:1 Obsluga WyjatekB:1 dtorB:1 dtorA:1// przez wartość: // ctorA:1 ctorB:1 cctorA:2 Obsluga WyjatekA:2 dtorA:2 dtorB:1 dtorA:1// obiekt "przycięty" do klasy A, nie ma polimorfizmu

Obsługa wyjątków [ polimorficzne wołanie funkcji ]

• gwarantowane jest, że przy wychodzeniu z zasięgu, dla obiektów, których konstruktory zostały wykonane do końca, wywołane zostaną destruktoryclass C { public:

C() : c(++cs) { cerr << "ctorC:" << c << ' '; }~C() { cerr << "dtorC:" << c << ' '; }

protected:int c;static int cs;

};int C::cs = 0;void fun4sub() {

C c2;fun1a(); // rzuca wyjątek typu AC c3;

}void fun4() {

C c1;fun4sub();C c4;

}try { fun4(); }// ctorC:1 ctorC:2 ctorA:1 dtorC:2 dtorC:1 Obsluga WyjatekA:1 dtorA:1 catch (const A& a) { a.obslugaWyj(); }

Obsługa wyjątków [ sprzątanie ]

problem: • jeśli wyjątek zostanie rzucony w konstruktorze

i nie wykona się on do końca, to destruktor nie jest wołany, zasoby zaalokowane w konstruktorze na stercie, przed rzuceniem wyjątku, nie zostaną zwolnione

rozwiązania:• przechwycić wyjątek w konstruktorze i zwolnić

w nim zasoby• stosować technikę pozyskiwania zasobów

w ramach inicjalizacji (RAII – Resource AcquisitionIs Initialization), alokacja w konstruktorze, zwalnianie w destruktorze – przykład: unique_ptr,czyli zdobycie zasobu wiąże się z budową jakiegoś obiektu

Blok try funkcji służydo ochrony listy inicjowaniaskładowych w konstruktorach klas (tzw. try funkcyjny)

int fun5(int i) {if (!i) throw A();return 2*i;

}class D { public:

D(int d0) try : d(fun5(d0)) { fun1b(); }catch (const A& a){ cerr << "ctorD-"; a.obslugaWyj(); }D(int d0, int) : d(fun5(d0)){

try { fun1b(); }catch (const A& a){ cerr << "ctorD-"; a.obslugaWyj(); }

}protected:

int d;};

Obsługa wyjątków [ na poziomie funkcji ]

try { D d(1,0); }catch (const A& a) { cerr << "main()-"; a.obslugaWyj(); }// ctorA:1 ctorB:1// ctorD-WyjatekB:1 dtorB:1 dtorA:1try { D d(1); }catch (const A& a) { cerr << "main()-"; a.obslugaWyj(); }// ctorA:1 ctorB:1 // ctorD-WyjatekB:1 main()-WyjatekB:1 dtorB:1 dtorA:1try { D d(0,0); }catch (const A& a) { cerr << "main()-"; a.obslugaWyj(); }// ctorA:1 main()-WyjatekA:1 dtorA:1try { D d(0); }catch (const A& a) { cerr << "main()-"; a.obslugaWyj(); }// ctorA:1 // ctorD-WyjatekA:1 main()-WyjatekA:1 dtorA:1

int main() try { throw ”mój wyjatek main”; }catch (const char* msg) {

cout << msg << endl;return 1; // tu może zakończyć jak funkcja

}

każda funkcjamoże mieć… alelepiej wewnątrz…

patrz wyjaśnieniedalej

Obiekt – zaczyna istnieć gdy konstruktor zakończy się pomyślnie (tzn. dojdziemy do końca ciała konstruktora lub do instrukcji return; )

Obiekt – kończy istnienie gdy rozpoczyna się jego destruktor.Sposobem zgłoszenia błędu konstrukcji jest zgłoszenie wyjątku. Zgłoszenie wyjątku przez konstruktor

oznacza, że obiekt nie został skonstruowany i nie istnieje. Procedura obsługi funkcyjnego bloku try w konstruktorze lub destruktorze musi zakończyć się

zgłoszeniem wyjątku – jeśli nie zakończy się jawnym zgłoszeniem wyjątku (oryginalnego lub jakiegoś nowego) i sterowanie osiągnie koniec bloku catch konstruktora lub destruktora, to oryginalny wyjątek jest automatycznie zgłoszony ponownie, tak jakby ostatnią instrukcją procedury było throw;

Jedynym zastosowaniem funkcyjnego bloku try konstruktora jest translacja wyjątku zgłoszonego przez podobiekt bazowy lub składowy. Jednak niewielka z tego korzyść, ponieważ trzeba pamiętać, że w procedurze obsługi funkcyjnego bloku try konstruktora wszystkie zmienne lokalne ciała konstruktora są już poza zasięgiem i żaden podobiekt bazowy ani składowy już nie istnieje! (… więc nie można „zwolnić” żadnych alokowanych zasobów).

Obsługa wyjątków [ blok try – wyjątek w konstruktorze ]

Nie można sprawić, aby wyjątek zgłoszony przez konstruktory podobiektu bazowego lub składowego nie wyciekł poza zawierający je konstruktor. W języku C++ jeśli konstrukcja dowolnego podobiektu bazowego lub składowego się nie uda, to nie może się też udać konstrukcja całego obiektu.

• opcjonalnie można poinformować jakie wyjątki rzuca funkcja• specyfikacja uzupełnia deklarację funkcji i pojawia się za listą argumentów

void fun(); // ta funkcja może zgłosić dowolny wyjątekvoid fun() throw(A, B, UserErr); // tu mogą być zgłoszone wyjątki typu A, B, UserErrvoid fun() throw(); // funkcja deklaruje, że nie rzuci żadnego wyjątku C++98/03void fun() noexcept; // funkcja deklaruje, że nie rzuci żadnego wyjątku C++11/14

• rzucenie wyjątku innego niż zadeklarowany powoduje wywołanie funkcji unexpected(), która domyślnie woła funkcję terminate()

• można ustawić własną obsługę takiej sytuacji poprzez podanie funkcji set_unexpected() adresu bezparametrowej funkcji typu void, można zachować wskaźnik do poprzedniej funkcji obsługi sytuacji nieoczekiwanej

• w klasach pochodnych, redefiniując funkcje składowe, nie można dodawać do listy wyjątków żadnych nowych typów – ale można podać mniej lub żaden

• jeśli nie wiemy (nie jesteśmy pewni) jakie wyjątki mogą się pojawić, nie używajmy ich specyfikacji (vide: biblioteka standardowa C++ i szablony klas – wyjątki znane są opisane w dokumentacji, a pozostałe zależą od użytkownika)

Obsługa wyjątków [ blok try – wyjątek w konstruktorze ]

– możliwe jest ponowne rzucenie wyjątku, jeśli jest to wyjątek z zadeklarowanej listy, przeszukiwanie podejmowane jest od nowa (od wywołania funkcji z taką specyfikacją wyjątków)

– jeśli jest to znowu wyjątek nie z deklarowanej listy, wywołana jest funkcja terminate()– chyba, że na liście wyjątków jest również wyjątek std::bad_exception

void fun() throw(A, B, C, bad_exception);to wyjątek nie przewidziany do rzucenia z danej funkcji zastępowany jest obiektem bad_exception i przeszukiwanie podejmowane jest od funkcji j.w.

class A {}; class B {};void myUnexpected() { throw invalid_argument(”O jej!”); }// void myUnexpected() { throw bad_cast(); } – ponownie zgłosi wyjątek nie z listyvoid myTerminate() { cout << ”Bez wyjscia: terminate” << endl; exit(1); }

void fun1() throw ( A, invalid_argument ) { throw B(); }// void fun1() throw ( A, invalid_argument, bad_exception ) { throw B(); } – tu jest bad_exception, więc…

int main() {set_unexpected( myUnexpected );set_terminate( myTerminate );try { fun1(); }catch( const exception& e) { cout << e.what() << endl; }

}

Obsługa wyjątków [ specyfikacje wyjątków ]

Hierarchia klasy wyjątków w bibliotece standardowej C++pliki nagłówkowe <exception>, <stdexcept>• klasa bazowa exception, klasy pochodne logic_error, runtime_error,

bad_cast, bad_alloc, bad_exception, bad_typeid, ios_base::failure

• błędy logiczne: klasa bazowa logic_error, klasy pochodne invalid_argument, out_of_range, length_error, domain_error

• błędy wykonania: klasa bazowa runtime_error, klasy pochodne range_error, overflow_error, underflow_errorclass exception { public:

exception() throw() { }virtual ~exception() throw();virtual const char* what() const throw();

};class tab { public:

explicit tab(int val = 0) { for(int i = 0; i < 3; ++i) tab_[i] = val; }int& operator[] (int i) {

if (i < 0 || i > 2)throw out_of_range("zły indeks tab");return tab_[i];

}protected:

int tab_[3];};

Obsługa wyjątków [ wyjątki standardowe ]

try { tab t(3); t[1] = t[4]; }catch (const exception& e) { cerr << e.what(); }// zły indeks tab

Obsługa wyjątków [ wyjątki standardowe ]

gwarancja podstawowa – po zgłoszeniu wyjątku nie nastąpi żaden wyciek pamięci, a obiekty pozostaną w stanie niekoniecznie przewidywalnym, ale umożliwiającym zniszczenie obiektów, gwarancja ta jest odpowiednia dla kodu radzącego sobie z nieudanymi operacjami, które już zmieniły stan obiektów

gwarancja silna – po zgłoszeniu wyjątku stan obiektu pozostaje niezmieniony, oznacza to semantykę „zatwierdź lub cofnij” oraz to, że żadne referencje lub iteratory do kontenera nie zostaną unieważnione, gdy operacja się nie powiedzie

gwarancja niezgłaszania wyjątków – funkcja nie zgłosi wyjątku bez względu na okoliczności, czasem jest niemożliwa implementacja silnej lub nawet podstawowej gwarancji, jeśli nie mamy gwarancji, że pewne funkcje (np. destruktory i funkcje zwalniające pamięć) nie zgłoszą wyjątku

Kod odporny na wyjątki [ gwarancje ]

#include <iostream>#include <stdexcept>using namespace std;class Device { public:

Device(int devno) { if (devno == 2) throw runtime_error(”Problem!”); }~Device() {}

};class Broker { public:

Broker(int devno1, int devno2) : dev1_(0), dev2_(0) {try {

dev1_ = new Device(devno1);dev2_ = new Device(devno2);

} catch ( … ) {delete dev1_; // bo po dev1_ mógł się nie udać tylko dev2_throw;

}}~Broker() { delete dev1_; delete dev2_; }

private: Broker(); Device* dev1_; Device* dev2_;};int main() {

try { Broker b( 1, 2 ); }catch (exception & e) { cerr << ”Wyjatek: ” << e.what() << endl; }

}

Kod odporny na wyjątki [ c-tor z gwarancją podstawową ]

Kontrolowanie obiektu (C++98) auto_ptr – deprecated (przestarzałe)

kontrola nad pojedyńczymi obiektami tworzonymi dynamicznie, zostaną automatycznie zniszczone gdy obiekt typu auto_ptr (stworzony na stosie) zakończy swoje życie (wyjdzie poza zasięg)

• mojObiekt dalej zachowuje się tak jak wskaźnik do typu T• auto_ptr nie można używać tego do opakowania tablic – powód to fakt, że destruktor

auto_ptr dla zawieranego wskaźnika wywołuje delete (a nie delete[])• kilka obiektów auto_ptr nie może pokazywać na ten sam fragment sterty• a co z tablicami? znacznie lepiej nadaje się do tego kontener vector !

#include <memory>auto_ptr<T> mojObiekt(new T);

auto_ptr<int> pInt1(new int(0));int* myPtr = new int(8); // inicjalizacja wartością 8auto_ptr<int> pInt2(myPtr); // teraz nie wolno usunąć myPtr ręcznieauto_ptr<int> pInt3 = pInt1; // przejęcie kontroli, pInt1 już nie ma!auto_ptr<int> pInt4; // zerowy, ale żeby to sprawdzić: pInt4.get();int a = 100; auto_ptr<int> pInt5(&a); // katastrofa, niszczenie stosuint *myPtr2 = pInt2.release(); // uwolnienie wskaźnika, obiekt pInt2 na 0pInt2.reset(); // usunięcie zawartościpInt2.reset(new int(10)); // skasowanie z ustawieniem nowej zawartości

auto_ptr kontra unique_ptr (C++11)

auto_ptr nie współpracował z kontenerami STL, właśnie ze wzgl. na filozofię „przekazywania praw własności” – kopia prowadziła do unieważnienia obiektu z prawej strony przypisania!unique_prt zastępuje auto_ptr (nagłówek memory), wspiera „semantykę przenoszenia”własności unique_ptr: • może być składowany w większości kontenerów i współdziała z algorytmami• nie można tworzyć przez kopiowanie ani przez przypisanie• można tworzyć przez kopiowanie lub przypisanie przenoszące• przykład zachowania auto_ptr i unique_ptr

• istnieje tablicowa wersja unique_ptr – jest to częściowa specjalizacja unique_ptr<T[]>

void f( unique_ptr<Foo> foo ); unique_ptr<Foo> pFoo ( new Foo() );f( move(pFoo) ); // jawne przesunięcie // za pomocą std::move() pFoo->method(); // błąd dostępu

void f( auto_ptr<Foo> foo ); // przez wartośćauto_ptr<Foo> pFoo ( new Foo() );f( pFoo ); // przekazanie przez wartość

// zeruje wewnętrzny wskaźnikpFoo->method(); // błąd dostępu

unique_ptr

auto_ptr

unique_ptr <int[]> intarr (new int[4]); // tablicowa wersja unique_ptr intarr[0]=10; intarr[1]=20; // i tak dalej// domyślny sposób usunięcia – wywołanie delete[]

unique_ptr (C++11)

unique_prt jest „referencją” do obiektu, który w sobie zawiera (przez wskaźnik), nie ma gwarancji, że jest to unikatowa referencja – unique_ptr można utworzyć z dowolnego wskaźnika

Bezpieczeństwo w sytuacjach nadzwyczajnych:

void f(unique_ptr<Foo> foo) { // na końcu zasięgu wołany destruktor obiektu foo

}Foo * foo = new Foo(); unique_ptr<Foo> upFoo(foo); f( move(upFoo) ); // jawne przesunięciefoo->method(); // niezdefiniowana sytuacja

unique_ptr<X> f() { unique_ptr<X> p(new X); // zgłoszenie wyjątkureturn p;

}

również metoda unique_ptr::get()prowadzi (przyczynia się) do potencjalnego powielania odnośników do tego samego obiektu (którego „właścicielem” jest unique_ptr)

X* f() { X* p = new X; // zgłoszenie wyjątkureturn p;

}

X* f() { unique_ptr<X> p(new X); // zgłoszenie wyjątkureturn p.release();

}

void g() { unique_ptr<X> q = f(); // konstruktor przenoszącyq->jakas_metoda(); // używamy q jak wskaźnikaX x = *q; // kopiowanie obiektu/* na koniec q i jego zawartość niszczone */ }

po zgłoszeniu wyjątku obiekt pzniszczy obiekt, którego wskaźnikprzechowuje (opakowuje)

unique_ptr (C++11) – standardowe tworzenie

• obiekt unique_prt zainicjalizowany

standardowe tworzenie obiektu, tak jak auto_ptr

• deleter – funkcja (lub obiekt funkcyjny) odpowiedzialny za poprawne usunięcie zasobu przechowywanego w unique_ptr

np. gdy trzeba usunąć 2-wym. tablicę dynamicznie zbudowaną

unique_ptr< string > a( new string(”witaj kolego”) );

struct Usuwacz {size_t rozmiar;Usuwacz( size_t roz=0 ) : rozmiar(roz) {}void operator()( string** ptr ) const {

for( size_t i=0; i < rozmiar; ++i ) delete ptr[i];delete [] ptr;

}};

unique_ptr< typ > nazwa ( new typ );

unique_ptr< typ, [ typ_deleter ] > nazwa( new typ, [ deleter ] );

// w programieunique_ptr< string*, Usuwacz >

a2( new string*[5], Usuwacz(5) );// można sprawdzić deleter:Usuwacz &ref = a2.get_deleter();

unique_ptr (C++11) – tworzenie c.d., użycie

• pusty obiekt unique_prt

– wewnętrzny wskaźnik obiektu ustawiony na 0– można obiekt unique_ptr sprawdzić „tak jak wskaźnik”

• inicjalizacja innym obiektem unique_ptr

działa wtedy konstruktor przenoszący, przejęcie zasobu– również w przypadku przenoszącego operatora przypisania

• obiekt unique_ptr zachowuje się jak wskaźni przechowywanego typu

unique_ptr< int > a;if ( !a ) cout << ” zero ”; // można to samo metodą a.get()

a1 = move( a2 ); // teraz a2 ma wskaźnik 0

unique_ptr< typ > nazwa;

unique_ptr< typ > a1( new string(”witaj kolego”) ); a1->insert( strlen(”witaj”), ” mily” ); // teraz: ”witaj mily kolego”

unique_ptr< typ > a1( new int ); // pierwszy obiekt unique_ptrunique_ptr< typ > a2( move(a1) ); // wskaźnik obiektu a1 ustawiony na 0

unique_ptr (C++11) – operatory i metody składowe

• operator przypisania przenoszącego =

• operatory: * i -> (wyłuskanie wskaźnika i odniesienie na składową)• metoda T* get() zwraca wskaźnik do typu T przechowywanego

przez unique_ptr, może być 0 • metoda Deleter& unique_ptr<T>::get_deleter() zwraca referencję

do obiektu „deletera” używanego przez unique_ptr• T* release() uwalania zasób przechowywany przez unique_ptr (on

sam przechodzi w stan 0), można przypisać do zwykłego wskaźnika, odpowiedzialnośc za usunięcie zasobu przekazana na zewnątrz

• void reset( T* ) podobnie jak w auto_ptr• void swap( unique_ptr<T>& ) zamiana dwóch obiektów unique_ptr

zawierających ten sam typ

unique_ptr< int > a1( new int ); unique_ptr< int > a2;a2 = std::move( a1 ); // nie można: a2 = a1; bo byłoby to przypisanie kopiujące

unique_ptr (C++11) – argument konstruktora (przykłady) (1)

• przez wartość – oznacza przekazanie zasobu („wskaźnika do…”)

– nextFoo jest „wyczyszczone”– std::move( nextFoo ) zwraca Foo&&, ponieważ argument jest przez wartość

więc tworzy obiekt tymczasowy, do którego przeniesiona jest zawartość argumentu danego konstruktorowi, a ten – przekazany (przeniesiony) do argumentu n konstruktora

Foo( std::unique_ptr<Foo> n ) : self( std::move(n) ) {}// użytkownik musi wywołać jawnie z użyciem std::moveFoo newFoo( std::move( nextFoo ) );

class Foo { public:

typedef unique_ptr<Foo> UPtrFoo;Foo( ) { } // jakie możliwości argumentów konstruktorów?virtual ~Foo() { }

protected:Foo::UPtrFoo next; // zagnieżdżone odniesienie się do Foo

};

unique_ptr (C++11) – argument konstruktora (przykłady) (2)

• przez niestałą referencję do l-wartości

– w zależności od tego co robi konstruktor, nextFoo może być wyczyszczone lub nie

• przez stałą referencję do l-wartości

– dajemy konstruktorowi dostęp do zasobu, ale tego zasobu nie można przejąć, możemy dostać się przez „wskaźnik do…” ale nie można go przejąć, bez łamania stałości (rzutowanie znoszące const)

Foo( std::unique_ptr<Foo>& n ) : self( std::move(n) ) {}// argument musi być prawdziwą, nazwaną, l-wartościąFoo newFoo( std::unique_ptr<Foo>( new Foo ) ); // błądFoo newFoo( nextFoo );

Foo( std::unique_ptr<Foo> const & n );// nie można naruszyć zawartości

unique_ptr (C++11) – argument konstruktora (przykłady) (3)

• przez referencję do p-wartości

– nie ma gwarancji, że przeniesienie zawartości ma miejsce, zależy od implementacji, sam powyższy zapis tego nie gwarantuje

• obiekt unique_ptr nie można skopiować, można tylko przesunąć– jeśli chcemy przejęcia zasobu, argument przez wartość– jeśli chcemy tylko użyć (tego co opakowuje unique_ptr) w czasie życia

(wykonania) danej metody, argument const&– jeśli metoda ma przejąć zawartość lub nie, przez && (ale jest to najmniej

czytelne bez przestudiowania co robi kod)

Foo( std::unique_ptr<Foo>&& n ) : next( std::move(n) ) { }// podobne do niestałej l-referencji, ale można wołać z tymczasowymiFoo newFoo( std::unique_ptr<Foo>( new Foo ) ); // prawidłowe// dla obiektów nietymczasowych konieczne użycie std::moveFoo newFoo( nextFoo ); // powinno być przeniesienie, ale…

shared_ptr (C++11) – inteligentny wskaźnik ze zliczaniem referencji

• zasób kontrolowany przez shared_ptr może być współdzielony, ilość udziałów jest zliczana i gdy zejdzie do 0 zasób jest usuwany

• shared_ptr obsługuje konstrutory i operatory przypisania, zarówno kopiujące jak i przenoszące

• tworzenie, składnia, metody składowe takie jak w unique_ptr• możliwe jest również kopiowanie (zwiększa się licznik referencji)

• możliwe jest też przypisanie kopiujące (lewy argument pomniejsza licznik i ew. usuwa zasób, prawy argument powiększa licznik)

• metoda stała uniqe() zwraca true, jeśli obiekt nie jest współdzielony• metoda size_t use_count() zwraca ile obiektów odnosi się do

zasobu kontrolowanego przez shared_ptr

shared_ptr< int > a1( new int(0) ); shared_ptr< int > a2( a1 );*a2 = 3; // teraz również *a1 == 3

shared_ptr (C++11) – problem z danymi cyklicznymi

• shared_ptr reprezentuje relację „posiadania”, może się zdarzyć, że obiekty zawierające shared_ptr odnoszą się wzajemnie do siebie

• gdy dwa obiekty shared_ptr odnoszą się wzajemnie do siebie, ich liczniki nigdy nie mogą zejść do 0, mimo zwolnienia zasobu!

class Rodzic;class Dziecko;class Rodzic { public: shared_ptr< Dziecko > pD; };class Dziecko { public: shared_ptr< Rodzic > pR; };int main() {

shared_ptr<Rodzic> r( new Rodzic ); // licznik 1shared_ptr<Dziecko> d( new Dziecko ); // licznik 1r->pD = d; // licznik rośnie +1d->pR = r; // licznik rośnie +1d.reset(); // zwalniam zasób, a co z licznikiem? licznik maleje -1, ale… był == 2cout << ”wsk: ” << d.get() << ” licznik: ” << d.use_count(); // wsk: 0 licznik: 1

}

gdy nie chodzi o obiekt,który jest zliczany, bo dbao „życie zasobu”, stosujemyweak_ptr (może rozwiązać problem cyklicznej relacji między obiektami)

shared_ptr (C++11) – koszty, czyli wady

• nie zamieniać swoich wskaźników bezrefleksyjnie na shared_ptr• cykliczne struktury z użyciem shared_ptr mogą prowadzić do

wycieków pamięci• obiekty shared_ptr mają tendencję do średnio dłuższego życia niż

zwykłe obiekty o ściśle określonym czasie życia (tym samym zużywają średnio więcej zasobów)

• w środowisku wielowątkowym mogą być kosztowne w obsłudze (z powodu zabezpieczenia licznika przed wyścigiem danych współużytkujących dany zasób)

• destruktor obiektu shared_ptr nie jest wywoływany w przewidywalnym czasie

• współdzielenie zasobu może powodować problemy z jego odświeżaniemBjarne Stroustrup:A shared_ptr represents shared ownership but shared ownership isn't my ideal: It is better if an object has a definite owner and a definite, predictable lifespan.

weak_ptr (C++11) – obserwator zasobów shared_ptr

• weak_ptr reprezentuje relację „obserwatora”, który patrzy na zasób, ale zasób może być usunięty mimo obiektu weak_ptr

• weak_ptr może pokazywać na shared_ptr, ale nie powiększa to licznika referencji do zasobu

• weak_ptr nie zachowuje się jak wskaźnik (nie można np. go wyłuskać operatorem *), żeby użyć go jako wskaźnika, trzeba:– utworzyć obiekt shared_ptr zbudowany z obiektu weak_ptr

(shared_ptr ma stosowny konstruktor) lub:– użyć metody składowej lock(), która zwraca shared_ptr<T>

• metoda expired() bada, czy obserwowany zasób (obiekt shared_ptr) jeszcze istnieje – odpowiada to warunkowi use_count() == 0

• weak_ptr zwykle użyte w kontekście rozwiązania problemówcyklicznej zależności (weak_ptr nie ma – i nie kontroluje zasobu)

// rozwiązanie problemu cyklicznej zależności obiektów shared_ptrclass Dziecko { public: weak_ptr< Rodzic > pR; };

Szablony funkcji [ function templates ]

• Rodzina funkcji z niektórymi elementami sparametryzowanymi

template < typename T >inline T const& max ( T const& a, T const& b ){

return a < b ? b : a;}

• typ nie jest jawnie określony, występuje jako parametr szablonu T ("T" jest zwyczajowe, zamiast "T" może być dowolny identyfikator…)

• parametry szablonu (składnia)

template < lista-parametrów-oddzielona-przecinkami >

• T reprezentuje typ umowny precyzowany dopiero w miejscu wywołania funkcji

• typ T może być dowolny o ile wszystkie operacje wykorzystane w szablonie są dla niego zdefiniowane (inaczej błąd kompilacji)

dawniej: classnadal można używać,ale nie należy mieszaćw jednym class i typename

Szablony funkcji [ wywołania ]

double x = 3.4;// konieczny operator zakresu :: bo inaczej wywołane zostanie std::maxcout << ::max( 3.2, x ) << endl; // 3.4 string s1 = "alfa";string s2 = "beta";cout << ::max( s1, s2 ) << endl; // "alfa"

• zwykle dla każdego nowego typu wywołania szablonu generowana jest osobna jednostka programowa• konkretyzacja (instantiation) szablonu - utworzenie egzemplarza

(instance) szablonu… w przykładach powyżej:const double& max( double const&, double const& );const std::string& max( std::string const&, std::string const& );

• dwukrotna kompilacja:• sam szablon, bez konkretyzacji - poprawność składniowa• konkretyzacja - pod kątem poprawności wywołań (kompilator musi się

odwołać do definicji szablonu - co narusza regułę, że do kompilacji powinna wystarczyć tylko deklaracja funkcji)

Szablony funkcji [ parametry i dedukcja typów ]

• typ T musi być określony jawnie (nie zachodzi automatyczna konwersja)max( 2, 3.4 ); // błąd, pierwszy jest int, a drugi double

• sposoby wołania z różnymi typami• jawne rzutowanie

max( static_cast<double>(2), 3.4 );• jawne określenie typu T

max<double>(2, 3.4 );• zdefiniowanie szablonu o dwóch różnych parametrach

• nie ma ograniczeń co do liczby parametrów (od C++11 mogą być domyślne): template < typename T1, typename T2 >

inline T1 max ( T1 const& a, T2 const& b ) {return a < b ? b : a;

}• zwracany typ determinowany przez jeden z parametrów, więc musi zajść

niejawna konwersja drugiego z parametrów na typ zwracany!• podczas konwersji tworzony obiekt tymczasowy - nie można zwracać

referencji do obiektu tymczasowego (stąd w przykładzie funkcja zwraca wartość - jeśli b > a, to zwracane b typu T2 konwertowane na typ T1)

Szablony funkcji [ dedukcja typów ]

• dedukcja argumentów szablonu funkcji - powiązanie parametrów szablonu z parametrami wywołania funkcji

• jawna konkretyzacja typu (czasem konieczna)

template < typename T1, typename T2, typename RT >inline RT max ( T1 const& a, T2 const& b );// powyżej nie można wydedukować typu wartości zwracanej,// typ ten nie figuruje jako jeden z parametrów wywołania funkcji// konkretyzacja:max< int, double, char > ( 3, 3.4 );

// chyba że: auto max ( T1 const& a, T2 const& b ) -> decltype( „jakaś operacja” )• częściowa dedukcja - jawnie podaje się aż do tego parametru,

który nie może być wydedukowany

// powyższy przykład po drobnej modyfikacji kolejności parametrówtemplate < typename RT, typename T1, typename T2 >inline RT max ( T1 const& a, T2 const& b );// konkretyzacja:max< char > (3, 3.4 );// parametry T1 i T2 wydedukowane na podstawie argumentów wywołania

Szablony funkcji [ przeciążanie ]Zwykłe funkcje mogą współistnieć z szablonami funkcji o tych samych nazwach, nawet konkretyzowanych dla tych samych typów co w funkcjach zwykłych

inline int const& max (int const& a, int const& b) {return a < b ? b : a;

}template <typename T>inline T const& max (T const& a, T const& b) {return a < b ? b : a;

}template <typename T>inline T const&

max (T const& a, T const& b, T const& c) {return max(max(a, b), c);

}

jeżeli do wyboru jest albo funkcja zwykła, albo konkretyzacja szablonu, proces rozstrzygający przeciążenie preferuje funkcję zwykłą

max (1, 24); // funkcja zwykła

jeżeli na podstawie szablonu można wygenerować lepsze dopasowanie, to wybierany jest szablon

max (3.0, 3.1); // max<double>max ('a', 'b'); // max<char>

można zażądać wykorzystania szablonu (wraz z dedukcją typu argumentów) wywołując z pustą listą argumentów szablonu

max<> (1, 24); // max<int>

w przypadku szablonów nie jest wykonywana automatyczna konwersja, która może zajść dla zwykłych funkcji, wtedy wołana jest funkcja zwykła// oba parametry zostaną// niejawnie zmienione na intmax (1.5, 'a'); // funkcja zwykła

trzeba pamiętać o widoczności wszystkich wersji przeciążonych w miejscu wywołania funkcji, np. przeniesienie definicji zwykłej funkcji max poza definicje szablonowe spowodowałoby wywołanie wewnątrz drugiego szablonu wersji szablonowej (wersja zwykła w tym miejscu wtedy jeszcze nie znana)

Szablony klas [ class templates ]

template < typename T >class A {

std::vector< T > tablica;public:

// konstruktor kopiującyA ( A< T > const& m); // operator przypisaniaA< T >& operator=( A< T > const& ); void put( T const& );T get() const;

};

// zewnętrzna implementacja

template < typename T >void A< T >::put( T const& m) {

tablica.push_back( m );}

nazwa klasy jest A tam gdzie stosuje się typ, to jest to: A< T > gdzie T jest parametrem szablonu

korzystanie z obiektu szablonuklasy oznacza konieczność jawnego określenia argumentu:A< float* > ptrFloatA;A< std::string > stringA;A< A< int > > intAA;

kod jest konkretyzowany tylkodla tych metod klasy, które zostanąwywołane w programie

można więc konkretyzować również dla typów, w których nie wszystkie operacje w metodach klasy da się wykonać - byle by ich nie wywoływać

metody statyczne konkretyzowaneraz dla każdego typu

Szablony klas [ specjalizacje ]

Dla pewnych typów konieczne może okazać się napisanie specjalnej wersji klasy… wszędzie wtedy typ T zastępujemy naszym typem specjalizowanym

template <>class A< std::string > {

std::deque< std::string > tablica; public:

// konstruktor kopiującyA ( A< std::string > const& m); // operator przypisaniaA< std::string >& operator=( A< std::string > const& ); void put( std::string const& );std::string get() const;

};

Specjalizacje częściowetemplate < typename T1, typename T2 >class A {

// …};

• oba parametry - ten sam typtemplate< typename T >class A< T, T > {

// …};

• jeden z parametrów określonytemplate< typename T >class A< int, T > {

// …};

• oba parametry wskaźnikamitemplate< typename T1, typename T2 >class A< T1*, T2* > {

// …};

Szablony klas [ domyślne argumenty, pozatypowe argumenty ]

Można określić wartości domyślne dla parametrów szablonów, mogą być oneokreślone przez odwołania do poprzednich parametrów szablonutemplate < typename T1, typename T2 = std::vector< T > >class A {

T2 tablica; public:

// konstruktor kopiującyA ( A< T1, T2 > const& m); // operator przypisaniaA< T1, T2 >&

operator=( A< T1, T2 > const& ); };

Pozatypowe parametry szablonów• klas:

template< typename T, int MAX >class A { };

• funkcji:template< typename T, int VAL >

Można teraz zmienić np. typ używanego kontenera:

A< int, std::deque< double > > nowyDA;A< double, std::list< double > > nowyLA;

Mogą być wartości domyślnetemplate< typename T, int MAX = 10 >class A { };Uwaga: A< int, 10 > i np. A< int, 20 > to dwa różne typy!

Pozatypowe parametry szablonówmuszą być stałymi wartościami całkowitymi lub wskaźnikami doobiektów łączonych zewnętrznie(zmiennoprzecinkowe i obiekty klas - nie)

Szablony klas [ użycie typename i this-> ]

typename trzeba stosować tam, gdzie nazwa uzależniona od parametru szablonu ma być interpretowana jako typ

template < typename T >class A {

typename T::SubType *ptr;};

typename służy tu do oznaczenia typu definiowanego wewnątrz klasy A, jeśli by opuścić typename, zostałoby to zrozumiane jako iloczyn składowej statycznej SubType klasy T oraz zmiennej ptr

przykład z iteratoremtemplate < typename T >void printContainer( T const& a ) // a jest kontenerem{

typename T::iterator pos; // iterator do odczytu byłby: T::const_iteratortypename T::iterator end( a.end() ); // koniec kontenera

for ( pos = a.begin(); pos != end; ++pos ) {std::cout << *pos << ' ';

}std::cout << std::endl;

}

Szablony klas [ szablony zagnieżdżone ]

Składowe klasy też mogą być szablonami - przydaje się to np. do napisania operatora przypisania jednego typu do drugiego:template < typename T >class A {

std::deque< T > tablica;public:

// tylko to co istotne dla przykładutemplate< typename T2 >A< T >& operator=( A< T2 > const& );

};

zewnętrzna definicjatemplate < typename T >template < typename T2 >A< T >& A< T >::operator=( A< T2 > const& op ) {

A< T2 > tmp( op ); // lokalna kopia do operacji na tym kontenerzetablica.clear();while ( !tmp.empty() ) { // tu napisać parę linii kopiowania…}return *this;

}

Inicjalizacja zerowatypy podstawowe nie zainicjalizowanemają wartość nieokreśloną, ale np. dla typu int wywołanie konstruktoradomyślnego int() da nam wartość zerotemplate < typename T >void foo() {

T zmienna = T(); // inicjalizacja}