język c++ ( programowanie ii ) rok akademicki...
TRANSCRIPT
Język 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++11/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++11 ma 1338 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:2011(E)
C++11 Final Documentwww.open-std.org/jtc1/sc22/wg21/N3290 (2011-04-11)
Bjarne Stroustrup• Język C++• 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)
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 2015
Nicolai M. JosuttisC++. Biblioteka standardowa. Podręcznik programisty
Wydanie II po angielsku
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”
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. 9.8.x
Twój edytor i 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!
.C, .cc, .cpp, .CPP, .c++, .cp, .cxx
Nie utrudniajmy sobie życia i używajmy g++W przypadku Dev-C++ chodzi o zapisaniepliku źródłowego z rozszerzeniem .cpp
Kompilator i konsolidator: gcc.gnu.org (g++)
• 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 wspierają standard C++11 (wersja 4.9.1), a także C++14
• Program zarządzający bibliotekami ar, ranlib(archiver, librarian)– biblioteki statyczne .a (oraz pliki obiektowe .o) wymagane do
skonsolidowania pliku wykonywalnego podczas linkowania– biblioteki dynamiczne .so zawierają kod maszynowy ładowany
do pamięci po uruchomieniu wykorzystującego je programu, muszą więc być dostępne podczas uruchamiania programu
• Warto zapoznać się z narzędziem make
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++/4.8Biblioteki w katalogu /usr/lib/gcc/i486-linux-gnu/4.8
Na pracowni komputerowej kompilator g++ 4.92 i nowszy jestdostępny tylko w nowych instalacjach (Dev-C++ 5.11)
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++11 -o myprog prog1.cc // to samo: g++ -std=c++11 prog1.cc -o myprog
g++ -std=c++11 -c -o prog1.o prog1.cc
g++ -std=c++11 -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.7, Intel C++ ver. 15, 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 { }
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 stawiaj średnika za nawiasem kończącym definicję funkcji – jest niepotrzebny!
Nie stawiaj średnika na końcu dyrektywy preprocesora – to jest błąd!
Ś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…
#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 też wybrać konkretneusing std::cout;using std::cin;Albo podczas wywołania pisaćstd::cout oraz std::cin itd.
Biblioteki z C:#include <cstdlib>#include <cstdio>#include <cassert>
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()
Hie
rarc
hia
klas
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)
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++11 §3.2.5) – wszystko to pod pewnymi warunkami! (zasadniczo jest to powtórzenie tego samego kodu z ew. dopisanymi wartościami domyślnymi funkcji)
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 )
Cytat z „Megatutorial-u” Karola Kuczmarskiego(Państwa niewiele starszego kolegi…)
Dyrektywa #include jest głupia jak cały preprocesor.
Nie korzystamy z dyrektywy#pragma once• pierwotnie działała tylko w niektórych
kompilatorach, np. 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
Namespace – przestrzeń nazw
Namespace – „przestrzeń nazw” – obszar posiadający swoją nazwę lub bez nazwy, służący do deklaracji. Nazwy zdeklarowane wewnątrz namespace są „zamknięte” –odwołanie do nich możliwe jest poprzez nazwę przestrzeni nazw lub odpowiednią deklarację / dyrektywę użycia. Namespaces służą unikaniu kolizji nazewniczych.
Definicja namespace może być rozbita na wiele części i może się znajdować nawet w różnych plikach – zatem sami możemy nawet coś dodać do danej „przestrzeni nazw” – nawet tej zdefiniowanej w jakiejś zewnętrznej bibliotece.
inlineopcjonalnie namespace nazwaopcjonalnie { /* zawartość */ }
inline – nowość w C++11 – w celu wspierania różnych wersji danej biblioteki, czyli ewolucji kodu, ze wskazaniem na bieżącą (np. najnowszą) wersję
Cała biblioteka standardowa jest zamknięta w przestrzeni o nazwie stdKonieczna zatem jest dyrektywa użycia:
using namespace std;ale nigdy w pliku nagłówkowym! W plikach nagłówkowych raczej piszemy std::obiekt, ewentualnie stosujemy deklarację użycia, np. using std::cout
Namespace – reguły istnienia, tworzenia, użycia
Namespace – tylko w przestrzeni globalnejalbo (zagnieżdżone) wewnątrz innej przestrzeni
tym samym
using – deklaracja/dyrektywa użycia czegoś z przestrzeni nazw
void f();namespace A {
void g();}
namespace nie można zagnieździć (definiować) wewnątrz definicji funkcji (również main) ani nie można zdefiniować wewnątrz klasy
namespace X {using ::f; // globalne fusing A::g; // g z A
}void h() {
X::f(); // woła ::fX::g(); // woła A::g
}
namespace A {int i;
}namespace A1 {
using A::i;using A::i; // tu ok, można powtórzyć
}void f() {
using A::i;using A::i; // a tu błąd!
}
Namespace – użycie, zagnieżdżenia
Bezpośrednią nadrzędną przestrzenią nazw dla danej deklaracji jest ta przestrzeń, w której deklaracja po raz pierwszy się pojawia. Później definicja (danej deklaracji) może być w innym zakresie, ale z precyzyjną specyfikacją co do pierwotnego wystąpienia deklaracji.
namespace A {namespace B {
void f(); class C { void m(); };
}void B::f() {
extern void h(); // to jest deklaracja A::B::h}void B::C::m() { // definicja metody m()}
}Można dodawać kolejną zawartość przestrzeni nazw (nawet w kolejnych plikach) ale musi być to zrobione w tym samym zasięgu znaczeniowym.
namespace A {void f(int);
}using A::f; // f jest synonimem A::f;
// czyli A::f(int)namespace A {
void f(char);}void foo() {
f(’a’); // woła f(int)} // pomimo że f(char) istniejevoid bar() {
using A::f; // f jest synonimem A::f;// zarówno A::f(int) i A::f(char)
f(’a’); // woła f(char)}
Jeśli w dwóch różnych przestrzeniach te same nazwy – konflikt w momencie użycia
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
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 inicjowany
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 (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
• 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”
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>int whatever(T t) {
T x; // równoważne do auto x poza szablonem};
auto – zastosowania ( C++11 )
• przykłady
• działa również z operatorem new
• szczególnie wygodne do dedukcji typów iteratorów
• niestety, wewnątrz wyrażeń lambda auto nie działa
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 = 7; // 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 )
• zmienne zadeklarowane za pomocą auto są nadal wielkościami statycznymi, stąd niemożliwe jest:
• 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 ) { } // źle: autodedukcja typu argumentu niemożliwaclass 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; // static tak
};
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:
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
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
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>&
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ć operacja 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)
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 )
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
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
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 = π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
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;
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.
Klasa std::string
Utworzenie obiektu typu std::string odbywa się podobnie jak dowolnej zmiennejtypu wbudowanego. Jednak w tym przypadku można też stworzyć obiektzainicjalizowany danymi – obiekt budowany jest przez specjalną metodę składową,konstruktor. Konstruktorów może być dowolnie wiele, muszą różnić sięargumentami.
#include <iostream>#include <string>
using namespace std;
auto main() -> int {string s1; // pusty string
}
Zbadajmy jaki jest rozmiar i bufor obiektu s1:
for (auto i(0); i<1025; ++i) {s1 += ”a”;cout << s1.size() << ” – ”
<< s1.capacity() << endl;}
Dodatkowo co będzie gdy:s1.clear();s1.empty(); // zwraca true lub falses1.shrink_to_fit();s1.reserve(57); // jakie capacity() ?
Tworzymy kolejne obiekty std::string
Oto kilka sposobów na utworzenie / przypisanie obiektu typu std::string
Działania na stringach bez problemu:
const char *t = ”tekst do inicjalizacji”;s1 = t;string s2( s1); // obiekt „na wzór” istniejącego wcześniejstring s3( t, 8 ); // pierwsze 8 znakówstring s4( s2, 6, 8 ); // od 6-tego do 6+8 -mego, czyli…string s5( 100, ’*’ ); // chcę mieć sto gwiazdekstring s6 = ”konstrukcja”;string s7 = { ”uniwersalna inicjalizacja” }; // = opcjonalnie
s1 = s1 + ” drugi ” + s2;s1 += s6;
Rozmiary, usuwanie…
Maksymalny rozmiar i pewna stała:
Sprawdźcie jaka jest wartość tej stałej:
Wielkie usuwanie (erase – metoda składowa):
Specjalne funkcje adresowe (zwracające tzw. iteratory czyli obiekty „udające”wskaźniki – przechowalniki adresu i wiedzy o typie):
max_size() // zwykła metoda składowastring().max_size(); // string „w locie”
erase( nr_od, nr_ile ); // zwraca „referencję do”erase( adres_od, adres_do ); // zwraca „adres” nast. znaku
std::string::npos
begin(); // adres początku „zerowej pozycji”end(); // adres za ostatnim elementem, „za-ostatni”
Usuwanie…
Przykład, dodatkowo z algorytmem find:#include <iostream>#include <algorithm>#include <string>using namespace std;int main () {
std::string s = "To jest dobry przyklad";std::cout << s << '\n';
s.erase(0, 3); // usuń "To "std::cout << s << '\n';
s.erase(std::find(s.begin(), s.end(), ' ')); // usuń pierwszą spację ' 'std::cout << s << '\n';
s.erase(s.find(' ')); // Znajdź kolejną i usuń wszystko od niej do końcastd::cout << s << '\n';
}
Małe ćwiczenie
Narysujmy za pomocą „erase” taką sekwencję…******************************************************************I tak dalej…
#include <iostream>#include <string>using namespace std;
int main () {string str (20, ’*’);while ( ! str.empty() ) {
cout << str << endl;str.erase(str.end()-1);
}}
Wczytywanie z pliku
Utwórzmy obiekt do obsługi strumienia plikowego i wczytajmy… a potem wypiszmy!#include <fstream>
string s10;string str;cout << "Wprowadz tekst: ";cin >> str; cout << "Wczytano to: " << str << endl;getline (cin, str, '@'); // koniec = znaczek @cout << "Wczytano tamto: " << str << endl;
ifstream plik("tekst.txt"); // np. wziąć z: pl.lipsum.comwhile ( ! plik.eof() ) {
getline (plik, str);s10 += str; // czego tu brakuje? Znak końca linii… + ’\n’
}// wypiszcie na ekran… cout << s10;
Bufor cin nadal trzyma starą zawartość, tu poczytajcie jak to wyczyścićhttp://cpp0x.pl/kursy/Kurs-C++/Poziom-1/Obsluga-strumienia-wejsciowego/12
Przebiegamy po stringu…
String to forma kontenera sekwencyjnego… jakby tablicy znaków…
ITERATOR – inteligentny „pośrednik” pomiędzy kontenerami (zasobnikami),„wskaźnik” z adresem do operacji na konkretnych typach, strumieniach…
ITERATOR STRUMIENIA
copy (s1.begin(), s1.end(), ostream_iterator<char>(cout,"\n"));// używamy algorytmu copy (ten z nagłówka <algorithm>)// tworzymy w locie iterator strumienia wyjściowego, ostream_iterator// konieczny nagłówek #include <iterator>
s1 = "wlazl kotek na plotek i mruga";for ( auto c : s1 ) cout << c << " "; // range-based loopfor ( auto& c : s1 ) c = ( c==’w’ ) ? ’W’ : c; // zamieniamy na wielkie W, co z nawiasami?
for ( int i=0; i < s1.length(); ++i ) cout << s1[i] << " ";
string::iterator it; // na razie pusty auto it = s1.begin();it = s1.begin(); // początek … end() koniecwhile ( it != s1.end() ) { cout << *it << endl; ++it; }
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 {
public:
int nr_pokoju;std::string etykieta;
int getNr();string getName();
};
dane składowe powinny być zdecydowanie w części prywatnej!
Dostęp do składników klasy:
MojaKlasa mojObiekt;MojaKlasa *mojWskaznik = &mojObiekt;MojaKlasa &mojaReferencja = mojObiekt;
mojObiekt.nr_pokoju;mojWskaznik->etykieta;mojaReferencja.getName();
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. delete this), za wyjątkiem sytuacji specjalnych – obiekt umieszczony jest w pamięci dynamicznej za pomocą operatora new „z umieszczeniem”, wtedy „ręcznie” sterujemy kreacją i 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
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
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ącegoint 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
A::A(const A& src) {numer = src.numer;nazwa = new char[src.strlen()+1];strcpy(nazwa, src.nazwa);
}
#ifndef TSTRING_H#define TSTRING_H#include <cstring>// w pliku nagłówkowym NIE// otwieramy przestrzeni std
class TString {public:// interfejs
private:// implementacja// składowe klasy
protected:// póki nie będziemy dziedziczyć, // to pole nas nie interesuje
}; // pamiętaj o średniku#endif
Zbudujemy klasę
Definicję klasy zapiszmy w pliku tstring.h Zapiszmy też prosty plik main.cc
class TString {private:
char* ptr; std::size_t size;
};
pola dostępu do klasy
z zewnątrz
size_t jest nazwą (typedef) na bezznakowytyp całkowity wystarczająco pojemny aby opisać wielkość dowolnego obiektu[ m.in. zwracany przez sizeof ]
#include ”tstring.h”#include <iostream>using namespace std;int main () {
TString s1;}
class TString {public:TString( const char* s = nullptr );
};
Zdefiniujmy konstruktor
Zdeklarujmy konstruktor (c-tor) : Metody definiujemy w tstring.cc
if (s > 0) {size = strlen(s);ptr = new char[ size + 1 ];strcpy( ptr, s );
}#ifdef DEBUG
cout << "TString c-tor " << size << " - " << ( ptr ? ptr : "pusty" ) << endl;#endif}
#include ”tstring.h”#include <iostream>
using namespace std;
TString::TString( const char* s ) : ptr(nullptr), size(0) {
Możemy teraz dopisać w main kolejny obiekt, np.TString s2("inicjalizacja slowem");
Kompilujemy dodatkowo dodając w liniiopcję –D definiowananazwaczyli –D DEBUG (może być bez spacji), przykładowo:g++4.8 –std=c++11 –DDEBUG
main.cc tstring.cc –o prog
class TString {public:TString( const char* s = nullptr );~TString();
};
Zdefiniujmy destruktor
Zdeklarujmy destruktor (d-tor):
TString::~TString() {
#ifdef DEBUGcout << "TString d-tor " << size << " - " << ( ptr ? ptr : "pusty" ) << endl;
#endif
delete [] ptr;}
Śledzenie pokrokowe programu (debuger) gdbKod trzeba skompilować z flagą –g (oraz nie używać flag optymalizujących takich jak –O –O2 itd.)http://www.yolinux.com/TUTORIALS/GDB-Commands.html
Definicję destruktora, jak i wszystkich kolejnych metod składowych klasy, dopisujemy jako ciąg dalszy (czyli poniżej definicji konstruktora) w pliku tstring.cc
w pliku tstring.cc jako dalsza część
class TString {public:
TString( const char* s = 0 );TString( const TString& s );
~TString();};
Zdefiniujmy konstruktor kopiujący
Zdeklarujmy konstruktor kopiujący (cc-tor): Możemy dopisać w main.cc
if (size > 0) {ptr = new char[ size + 1 ];strcpy( ptr, s.ptr );
}
#ifdef DEBUGcout << "TString cc-tor " << size << " - " << ( ptr ? ptr : "pusty" ) << endl;
#endif}
Operacja podobna do tej z konstruktora.
// poniżej to nie jest przypisanieTString s3 = s2; // albo tak:TString s3 ( s2 ); // albo tak:TString s3 { s2 };
TString::TString( const TString& s ) : ptr(nullptr), size( s.size ) {
w pliku tstring.cc jako dalsza część
class TString { public:TString& operator=
( const TString& s );};
Zdefiniujmy operator przypisania kopiujący
Zdeklarujmy operator= kopiujący: Możemy dopisać w main.cc
if ( this != &s ) { // if ( *this != s ) {delete [] ptr; ptr = nullptr; size = s.size;if ( size > 0 ) {
ptr = new char[ size + 1 ];strcpy( ptr, s.ptr );
}}
#ifdef DEBUGcout << "TString copy operator= " << size << " - " << ( ptr ? ptr : "pusty" ) << endl;
#endifreturn *this; // nie zapomnij zwrócić obiektu!}
this – specjalny wskaźnik, który otrzymuje każda niestatyczna składowa klasy, a w którym zapisany jest adres bieżącego obiektu, na którego argumentach działać ma metoda
// poniżej jest przypisanie, bo obiekt// po lewej już istniejes3 = ”alfa beta”;s3 = s2;
TString& TString::operator=(const TString& s ) {
class TString {public:
TString( TString&& s );};
Zdefiniujmy konstruktor przenoszący
Konstruktor przenoszący (mvc-tor): Możemy dopisać w main.cc
// obiekt źródłowy zostaje pozbawiony zasobów// ale pozostawiony w stanie do dalszego użytku czyli można coś np. do niego przypisać
s.ptr = nullptr;s.size = 0;
#ifdef DEBUGcout << "TString mvc-tor " << size << " - " << ( ptr ? ptr : "pusty" ) << endl;
#endif}
Operacja przenoszenia dzieje się automatycznie wtedy, gdy obiekt źródłowy „nie ma nazwy” i „nie ma adresu”
// move „maskuje” tożsamość obiektuTString s4 = std::move( s2 );// std::move będzie niepotrzebne// jeśli inicjalizować będzie obiekt// tymczasowy np. zwracany przez// funkcję jako wartość
TString::TString( TString&& s ) : ptr(s.ptr), size(s.size) {
class TString { public:TString& operator=
( TString&& s );};
Zdefiniujmy operator przypisania przenoszący
Zdeklarujmy operator= przenoszący: Możemy dopisać w main.cc
if ( this != &s ) {delete [] ptr; // usuń dotychczasowy zasóbsize = s.size; // typy proste się tylko (po prostu) kopiujeptr = s.ptr; // tu zabieramy adres wskaźnika (przeniesienie praw własności)s.size = 0; // obiekt, któremu zabraliśmy, zerujemys.ptr = nullptr; // wskaźnik również zerujemy
}#ifdef DEBUG
cout << "TString move operator= " << size << " - " << ( ptr ? ptr : "pusty" ) << endl;#endifreturn *this; // nie zapomnij zwrócić obiektu!
}
// ponownie „ukrywamy obiekt”// za pomocą std::moves3 = std::move( s1 );
TString& TString::operator=( TString&& s ) {
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ść
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 pochodnej
int 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. intb), 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
• 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
• 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
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ń