Programowanie Systemów Sterowania

Transkrypt

Programowanie Systemów Sterowania
Programowanie Systemów
Sterowania
Dr inż. Dariusz Bismor
Gliwice, 2007
Operator new
a W przypadku poprzedniej klasy i definicji:
Dynamiczna *tab = new Dynamiczna[ 10 ];
nie zostaną wywołane przeładowane wersje operatorów
new i delete
a Powodem jest istnienie drugiej wersji operatorów,
wykorzystywanych w przypadku przydziału pamięci na
tablice
a Przydział pamięci na tablice wygląda nieco inaczej, gdyż,
oprócz zapamiętania ilości potrzebnej pamięci, system
musi zapamiętać liczbę elementów tablicy
a Liczba elementów tablicy musi zostać zapamiętana w
celu wywołania odpowiedniej liczby konstruktorów i
destruktorów
Operator new
void * Dynamiczna::operator new[]( size_t rozmiar ){
cout << ”Tworzenie tablicy obiektów klasy Dynamiczna, rozmiar ‘’
<< rozmiar << ‘’B’’;
return ::new char[ rozmiar ] );
};
Jedyna różnica w kodzie operatora!
void Dynamiczna::operator delete[]( void *wsk ){
cout << ”Kasowanie tablicy obiektów klasy Dynamiczna‘’ << endl;
::delete [] static_cast<char*>(wsk);
};
Operator new
aPrzeładować można także globalną wersję
operatorów new i delete
aJest to bardzo zaawansowana i
potencjalnie niebezpieczna technika
aZastosowanie: detekcja wycieków pamięci
w programie, optymalizacja dostępu do
pamięci
Operator new
void * operator new( size_t rozmiar ){
printf("Przydzielam %zd B pamięci… ", rozmiar );
void *wsk = malloc( rozmiar );
if( !wsk ){
printf("błąd!\n");
throw bad_alloc();
}
printf("zrobione!\n");
return wsk;
};
void operator delete( void *wsk ){
printf("Zwalniam przydzieloną pamięć!\n");
free(wsk);
};
Operator new
a Przeładowanie operatora new można wykorzystać do
umieszczenia obiektu w wybranym segmencie pamięci,
na przykład przy obsłudze urządzeń, których obszar
wejścia-wyjścia jest mapowany w obszar pamięci
(memory mapped I/O)
a Technika ta nosi nazwę umiejscowiania alokowanej
pamięci (ang. placement new)
a Miejsce w pamięci należy przygotować przed
wywołaniem operatora new
a Programista ponosi całkowitą odpowiedzialność za
właściwe przygotowanie miejsca (np. stronicowanie)
a Utworzonego w ten sposób obiektu nie wolno usuwać
operatorem delete – w zamian należy jawnie wywołać
destruktor
Operator new
class Umieszczana{
public:
void *operator new( size_t, void *gdzie ){
return gdzie;
}
…
};
int main(){
char bufor[ 2 * sizeof(Umieszczana) ];
void *miejsce = bufor;
Umieszczana *wskU = new(miejsce) Umieszczana;
…
Umiejscowienie obiektu w zadanym miejscu
wskU->Umieszczana::~Umieszczana();
}
Zamiast delete – wywołanie destruktora
Operatory inkrementacji
aWyróżnia się operatory preinkrementacji (++i) i
postinkrementacji (i++)
aOperatory te są jednoargumentowe
aOperator preinkrementacji jest definiowany na
zasadach ogólnych
aOperator postinkrementacji jest definiowany
jako dwuargumentowy, przy czym drugi
argument jest argumentem nienazwanym
Operatory inkrementacji
Przykład przeładowania operatorów inkrementacji:
class K{
public:
K( const K &wzor );
K & operator+=( const int i );
K& operator++();
const K operator++( int );
};
Operatory inkrementacji
K& K::operator++(){
*this += 1;
return *this;
}
// inkrementacja
// zwrócenie wartości po zmianie
Argument nienazwany, nie jest
const K K::operator++( int ){ wykorzystywany w ciele funkcji
K tmp = *this;
*this += 1;
return tmp;
// utworzenie kopii
// inkrementacja
// zwrócenie wartości sprzed zmiany
}
Te same uwagi dotyczą operatora
dekrementacji (--i oraz i-- )
Wyczerpujący przykład
Jakie błędy potrafisz wskazać
w przedstawionym kodzie?
class Zespolona{
float _re, _im;
public:
Zespolona( float re, float im = 0 ): _re(re), _im(im) {};
void operator+( Zespolona z ){
_re = _re + z._re;
_im = _im + z._im; }
void operator<<( ostream os ){
os << ‘’(‘’ << _re << ‘’,’’ << _im << ‘’)’’; }
Zespolona operator++(){
++_re;
return *this; }
Zespolona operator++(int){
Zespolona tmp = *this;
++_re;
return temp; }
};
Wyczerpujący przykład
class Zespolona{
float _re, _im;
Nie należy używać identyfikatorów
rozpoczynających się od znaku podkreślenia
(Standard rezerwuje niektóre z takich
identyfikatorów na potrzeby implementacji)
Wyczerpujący przykład
class Zespolona{
float _re, _im;
public:
Zespolona( float re, float im = 0 ): _re(re), _im(im) {};
Tak zdefiniowany konstruktor jest jednocześnie
konwerterem z typu float do typu Zespolona
(może to być celowe lub nie)
Wyczerpujący przykład
class Zespolona{
float _re, _im;
public:
Zespolona( float re, float im = 0 ): _re(re), _im(im) {};
void operator+( Zespolona z ){
_re = _re + z._re;
_im = _im + z._im; }
Ze względu na lepszą wydajność, ten parametr
powinien być referencją do stałego obiektu
Wyczerpujący przykład
class Zespolona{
float _re, _im;
public:
Zespolona( float re, float im = 0 ): _re(re), _im(im) {};
void operator+( Zespolona z ){
_re = _re + z._re;
_im = _im + z._im; }
Operator dodawania nie powinien być funkcją
składową klasy, lecz funkcją (lub parą funkcji)
globalną
Przy takiej deklaracji można napisać a=b+1,
lecz nie można a=1+b
(Dla lepszej wydajności być może należy także zdefiniować
operator+(Zespolona,int) i operator+(int,Zespolona))
Wyczerpujący przykład
class Zespolona{
float _re, _im;
public:
Zespolona( float re, float im = 0 ): _re(re), _im(im) {};
void operator+( Zespolona z ){
_re = _re + z._re;
_im = _im + z._im; }
Operator dodawania nie powinien modyfikować
oryginalnego obiektu, lecz tworzyć obiekt tymczasowy
i zwracać go
Typ zwracany
const Zespolona
Najlepiej,powinien
z punktubyć
widzenia
wydajności,w celu
uniknięcia
zapisów operator+=
a+b = c; zamiast operator+
jest zdefiniować
Wyczerpujący przykład
class Zespolona{
Jeszcze lepszym rozwiązaniem jest zdefiniowanie
float _re, _im;
funkcji składowej wypisz() i wywołanie jej
public:
z globalnej funkcji operatorowej
Zespolona( float re, float im = 0 ): _re(re), _im(im) {};
void operator+( Zespolona z ){
_re = _re + z._re;
_im = _im + z._im; }
void operator<<( ostream os ){
os << ‘’(‘’ << _re << ‘’,’’ << _im << ‘’)’’; }
Operator wstawiania do strumienia nie powinien być
funkcją składową, a jego parametrami powinny być
(ostream &, const Zespolona &)
Wyczerpujący przykład
class Zespolona{
float _re, _im;
public:
Zespolona( float re, float im = 0 ): _re(re), _im(im) {};
void operator+( Zespolona z ){
_re = _re + z._re;
_im = _im + z._im; }
void operator<<( ostream os ){
os << ‘’(‘’ << _re << ‘’,’’ << _im << ‘’)’’; }
Typem zwracanym powinna być ostream&, a funkcja
operatorowa powinna kończyć się przez return os;
Dzięki temu możliwe będzie zapisanie wywołań
łańcuchowych cout << z1 << z2;
Wyczerpujący przykład
class Zespolona{
float _re, _im;
public:
Operator
preinkrementacji powinien zwracać
Zespolona( float re, float im = 0 ): _re(re), _im(im) {};
referencję
do oryginalnego
void operator+(
Zespolona z ){ obiektu (Zespolona
Zawsze
naśladować
_re =należy
_re + z._re;
_im oryginalne
= _im + z._im; }
zachowanie
operatorów!
void operator<<(
ostream os ){
os << ‘’(‘’ << _re << ‘’,’’ << _im << ‘’)’’; }
Zespolona operator++(){
++_re;
return *this; }
&)
Wyczerpujący przykład
class Zespolona{
float _re, _im;
public:
Zespolona( float re, float im = 0 ): _re(re), _im(im) {};
void operator+( Zespolona z ){
Operator
postinkrementacji powinien zwracać
_re = _re + z._re;
_im = _im + z._im; }
stały obiekt tymczasowy (const Zespolona)
void operator<<( ostream os ){
Dzięki
temu uniknie się wywołań z1++++;
os << ‘’(‘’ << _re << ‘’,’’ << _im << ‘’)’’; }
Zespolona operator++(){
++_re;
return *this; }
Zespolona operator++(int){
Zespolona tmp = *this;
++_re;
return temp; }
};
Wyczerpujący przykład
class Zespolona{
float _re, _im;
public:
Zespolona( float re, float im = 0 ): _re(re), _im(im) {};
void operator+( Zespolona z ){
_re = _re + z._re;
_im = _im + z._im; }
Najlepiej jest zdefiniować operator
void operator<<( ostream os ){
postinkrementacji
przy wykorzystaniu
os << ‘’(‘’ << _re << ‘’,’’ << _im << ‘’)’’; }
operatora
preinkrementacji
Zespolona operator++(){
++_re;
return *this; }
Zespolona operator++(int){
Zespolona tmp = *this;
++_re;
return temp; }
};
Wyczerpujący przykład
class Zespolona{
float s_re, s_im;
public:
explicit Zespolona( float re, float im = 0 ): s_re(re), s_im(im) {};
Zespolona& operator+=( const Zespolona& z ){
s_re += z.s_re;
s_im += z.s_im;
return *this; }
Zespolona& operator++(){
++s_re;
return *this; }
const Zespolona operator++(int){
Zespolona tmp = *this;
++(*this);
return temp; }
ostream& wypisz( ostream& os ) const {
return os << "(" << _re << "," << _im << ")"; }
};
Wyczerpujący przykład
const Zespolona operator+( const Zespolona s1,
const Zespolona s2){
Zespolona suma(s1);
suma += s2;
return s2;
}
ostream& operator<<( ostream &os, const Zespolona& z ){
W bibliotece
standardowej istnieje szablon complex,
return z.wypisz(os);
}
który jest prawdopodobnie bardziej wydajną
i bardziej bezbłędną implementacją, niż klasa
napisana przez nawet średnio zaawansowanego programistę
Obsługa wyjątków
a Najprostsze podejście do obsługi błędów to funkcja
assert() (wyłączana w gotowym oprogramowaniu)
a W języku C kod obsługi błędów musiał być przeplatany z
kodem programu
a Problem pojawiał się wtedy, gdy obsługa błędu nie była
możliwa w bieżącym kontekście (funkcji, zakresie, itp.)
a Standardowa biblioteka języka C udostępnia funkcje do
„długich” skoków: setjmp() longjmp(), lecz problem
odtworzenia stanu sprzed awarii pozostaje
a Funkcje te nie wywołują destruktorów
a Język C++ umożliwia łatwe rozdzielenie kodu obsługi
błędów od „właściwego” kodu programu
Obsługa wyjątków
a W języku C++, w momencie napotkania problemu,
którego nie można obsłużyć w danym kontekście, można
wysłać informację o błędzie do wyższego kontekstu
a Zazwyczaj odbywa się to przez przesłanie obiektu
specjalnie zaprojektowanej do obsługi błędów klasy
a Proces nosi nazwę wyrzucania wyjątków (throwing an
exception) i jest wykonywany instrukcją „throw”
a Wyrzucenie wyjątku wewnątrz funkcji powoduje jej
natychmiastowe opuszczenie
a Aby nie opuszczać bieżącego kontekstu, można posłużyć
się blokiem „try”
a Obsługa wyjątku następuje przez przechwycenie
przesłanego obiektu i reakcję wewnątrz bloku „catch”
Obsługa wyjątków
class ObslugaBledow{ Specjalna klasa do obsługi błędów
const char const *opis;
public:
ObslugaBledow( const char* info ): opis(info) {};
const char *co() const { return opis };
};
void fun(){
throw ObslugaBledow( ”Funkcja fun(): wystąpił błąd!” );
}
„Wyrzucenie” błędu z utworzeniem obiektu klasy
int main(){
służącej do obsługi błędów
try{
„Próba”, czy dany kod wykona się poprawnie
fun();
Przechwycenie „wyrzuconego
} catch( ObslugaBledow &blad ){
cout << ”Niedobrze! ” << blad.co() <<obiektu
endl; i reakcja
exit( 1 );
}
return 0;
// Nigdy tutaj nie dojdzie!
}
Obsługa wyjątków
a „Wyrzucać” można obiekty dowolnych typów, także
wbudowanych (np. int)
a Polecenie throw kopiuje egzemplarz wyrzucanego
obiektu, który następnie zostaje zwrócony
a Wszystkie automatyczne obiekty w pełni utworzone w
kontekście, z którego wyjątek jest „wyrzucany” są
niszczone (tzw. odwijanie stosu, stack unwinding)
a „Wyrzucony” obiekt zostaje „złapany” w kodzie obsługi
błędu
a Składnia instrukcji catch przypomina funkcję z jednym
parametrem; można użyć także parametru
nienazwanego
Obsługa wyjątków
a „Wyrzucony” obiekt danego typu musi być „złapany”
przez instrukcję catch z parametrem zgodnego typu
a Kilka instrukcji catch z różnymi przechwytywanymi
typami może następować po sobie:
try{
…
}catch( Typ1 &x1 ){
… // Obsługa wyjątków typu pierwszego
}catch( Typ2 &x2 ){
… // Obsługa wyjątków typu pierwszego
}catch( Typ3 &x3 ){
… // Obsługa wyjątków typu pierwszego
} // Ewentualnie inne typy
Obsługa wyjątków
a Jedyne konwersje wykonywane na typach w instrukcji
catch to konwersje związane z dziedziczeniem i
konwersja wskaźników dowolnego typu na wskaźnik void
a Możliwe jest złapanie obiektu dowolnego typu przez
instrukcję
catch(...){
// Kod obsługi wyjątku
}
a Wyjątki można wyłapywać przez wartość, referencję i
przez wskaźnik
a Wyłapywanie wyjątku przez wartość nie pozwala
wykonywać konwersji z typu klasy pochodnej na typ
klasy podstawowej
a Dlatego najczęściej wyjątki wyłapuje się przez referencję
Obsługa wyjątków
a Wyjątek można „przerzucić” z kodu obsługi wyjątku, np. po
dealokacji niepotrzebnych zasobów, do innego kontekstu,
za pomocą instrukcji throw:
catch(...){
cout << ”Wyjątek, hurra!” << endl;
// Tutaj ew. dealokacja zasobów
throw;
}
a Nie złapany lub przerzucony wyjątek przechodzi do
następnego, wyższego kontekstu: bardziej „zewnętrznego”
bloku try lub funkcji wywołującej daną funkcję
a Także tam nie przechwycony wyjątek przechodzi do
jeszcze wyższego kontekstu, itd.
Obsługa wyjątków
a Jeżeli wyjątek nie zostanie przechwycony na żadnym z
poziomów, zostanie wywołana funkcja biblioteczna
treminate(), zdeklarowana w nagłówku <exception>
a Funkcja ta domyślnie wywołuje funkcję abort(),
powodującą natychmiastowe zakończenie programu, a w
systemach unixowych wygenerowanie pliku core
a Funkcja abort() nie pozwala wykonać się destruktorom
obiektów globalnych i statycznych
a Możliwe jest zainstalowanie własnej funkcji obsługi nie
przechwyconych wyjątków za pomocą funkcji
set_terminate(), która zwraca wskaźnik do bieżącej funkcji
obsługi wyjątków
Obsługa wyjątków
a Funkcja ta musi być bezparametrowa i musi zwracać void:
void terminator(){
cout << ”Nareszcie koniec męczarni!” << endl;
exit(1);
}
// Zmienna globalna i jej inicjalizacja
void (*stary)() = set_terminate( terminator );
a Funkcja terminate() zostanie także wywołana w sytuacji,
gdy nastąpi wyjątek w trakcie wykonywania kodu obsługi
innego wyjątku
Wyjątki w konstruktorze
aKonstruktory nie posiadają wartości zwracanej
aDlatego najlepszym sposobem sygnalizacji
niepowodzenia pracy konstruktora są wyjątki
aWyjątek, który wystąpił w konstruktorze obiektu
danej klasy, nie powoduje uruchomienia
destruktora tej klasy
aPowodem jest brak możliwości rozróżnienia,
które zasoby zostały już zaalokowane, a które
jeszcze nie
Wyjątki w konstruktorze
class Poprawna{
public:
Poprawna(){ cout << ”Poprawna: konstruktor!\n”; }
~Poprawna(){ cout << ”Poprawna: destruktor!\n”; }
};
class Wadliwa{
public:
Wadliwa(){ cout << ”Wadliwa: konstruktor!\n”; throw 1; }
~Wadliwa(){ cout << ”Wadliwa: destruktor!\n”; }
};
class Dziedzic{
Poprawna *pop;
Wadliwa *wad;
public:
Dziedzic( int liczba=1 );
~Dziedzic();
};
Wyjątki w konstruktorze
Dziedzic::Dziedzic( int liczba ){
cout << ”Dziedzic: konstruktor!\n”;
pop = new Poprawna[ liczba ];
wad = new Wadliwa;
}
Dziedzic::~Dziedzic(){
cout << ”Dziedzic: destruktor!\n”;
}
int main(){
try{
Dziedzic dz(5);
}catch(int){
cout << ”Nastąpił wyjątek!” << endl;
}
cout << ”Koniec programu!” << endl;
return 0;
}
Wynik działania programu:
Dziedzic: konstruktor!
Poprawna: konstruktor!
Poprawna: konstruktor!
Poprawna: konstruktor!
Poprawna: konstruktor!
Poprawna: konstruktor!
Wadliwa: konstruktor!
Nastąpił wyjątek!
Koniec programu!
Wyjątki w konstruktorze
a Wyjątek, który wystąpił w konstruktorze obiektu danej
klasy, nie powoduje uruchomienia destruktora tej klasy,
ani konstruktorów klas pochodnych
a Zatem konstruktor powinien sam rozróżniać, które
zasoby zostały zaalokowane, i w sytuacji wyjątkowej
podejmować różne akcje
a Można to osiągnąć na kilka sposobów, na przykład przez
wstępną inicjalizację wskaźników zerami, lub przez
zastosowanie funkcji inicjalizujących
Wyjątki w konstruktorze
Dziedzic::Dziedzic(): pop(0), wad(0){
cout << ”Dziedzic: konstruktor!\n”;
try{
pop = new Poprawna;
wad = new Wadliwa;
}catch( bad_alloc ){
delete pop;
delete wad;
}
}
Dziedzic::~Dziedzic(){
delete pop;
delete wad;
cout << ”Dziedzic: destruktor!\n”;
}
Wyjątki w konstruktorze
a Lepszą metodą jest zastosowanie idiomu RAII (Resource
Acquisition Is Initialization)
a Zakłada on, że każdy alokowany dynamicznie w
konstruktorze obiekt powinien „za sobą posprzątać” sam
a RAII pozwala na uniknięcie pisania wielu bloków try-catch
a RAII można zrealizować używając „zręcznych” wskaźników
a W pliku nagłówkowym <memory> zdefiniowano szablon
auto_ptr, który posiada jeden parametr będący typem
obiektu, do którego wskaźnik należy „zręcznie”
zdefiniować
a Szablon auto_ptr posiada przedefiniowane operatory *
oraz ->, co ułatwia posługiwanie się takim wskaźnikiem
Wyjątki w konstruktorze
Zmiany w programie:
class Dziedzic{
auto_ptr<Poprawna> pop;
auto_ptr<Wadliwa> wad;
public:
Dziedzic(int liczba=1 );
~Dziedzic();
};
Wynik działania programu:
Dziedzic: konstruktor!
Poprawna: konstruktor!
Wadliwa: konstruktor!
Poprawna: destruktor!
Nastąpił wyjątek!
Koniec programu!
Dziedzic::Dziedzic( int liczba ){
cout << ”Dziedzic: konstruktor!\n”;
pop.reset( new Poprawna );
wad.reset( new Wadliwa );
}
Wada rozwiązania – brak możliwości zdefiniowania tablicy
Wyjątki w destruktorze
aNigdy nie należy dopuścić do wydostania się
wyjątku z destruktora!
aJeżeli podczas procedury ,,odwijania'' stosu
zostanie zgłoszony wyjątek, uruchomiona
zostanie funkcja terminate()
aDomyślnym zachowaniem funkcji terminate()
jest wywołanie abort()
aSytuacja taka zajdzie, jeżeli wyjątek zostanie
zgłoszony w destruktorze obiektu, który jest
usuwany podczas ,,odwijania'' stosu
aDlatego nigdy nie wolno dopuścić do wydostania
się wyjątku z destruktora!
Obsługa wyjątków
Funkcja może zdeklarować, jakie wyjątki będzie „wyrzucać”:
// Poniższa funkcja może „wyrzucić” każdy wyjątek
void fun();
// Poniższa funkcja „wyrzuca” jedynie obiekty klas
// MojBlad i BladPamieci
void fun1() throw( MojBlad BladPamieci );
// Ta funkcja nigdy nie „wyrzuca” wyjątków
void fun2() throw();
Obsługa wyjątków
a Jeżeli funkcja „wyrzuci” wyjątek, który nie jest
zadeklarowany na liście jej wyjątków, zostanie wywołana
funkcja unexpected()
a Możliwe jest ustawienie własnej wersji funkcji unexpected()
za pomocą funkcji set_unexpected(), na zasadach takich,
jak przy set_terminate()
a Funkcja ustawiona zamiast funkcji unexpected() może
„przerzucić” wyjątek, lub „wyrzucić” inny wyjątek
a Jeśli ten inny wyjątek jest na liście wyjątków funkcji, która
początkowo wyrzuciła wyjątek spoza listy, program
kontynuuje działanie w miejscu wywołania tej funkcji
Obsługa wyjątków
aKonstruktory zwykle sygnalizują błędy poprzez
zgłaszanie wyjątków
aWiele istotnych działań konstruktor wykonuje za
pomocą listy inicjalizacyjnej
aJak wyłapać wyjątki zgłoszone podczas
wykonywania operacji listy inicjalizacyjnej?
aRozwiązanie: blok try{} na poziomie funkcji
aCiekawostka: można także używać w stosunku do
innych funkcji (nie tylko konstruktora)
Obsługa wyjątków
ObiektDyskretny::ObiektDyskretny( int ndA=1, int ndB=0, int nk=1) try:
dA(ndA), dB(ndB), k(nk),
parametryA( new TabFloat(ndA) ),
parametryB( new TabFloat(ndB+1) ),
pamiecA( new TabFloat(ndA) ),
pamiecB( new TabFloat(ndB+k) )
{
cout << "Konstruktor klasy ObiektDyskretny!";
}catch( … ){
cout << "Wyjątek w konstruktorze klasy ObiektDyskretny!";
throw;
}
Obsługa wyjątków
a Jeśli tak nie jest, a funkcja, która naruszyła specyfikację
wyjątków, ma na liście wyjątek standardowy bad_exception,
wyjątek zgłaszany przez funkcję obsługi jest zamieniany na
wyjątek bad_exception, a sterowanie zwracane do miejsca
wywołania funkcji
a Jeśli typ wyjątku zgłaszanego przez funkcję obsługi sytuacji
naruszenia specyfikacji wyjątków nie występuje na liście
wyjątków funkcji, która specyfikację naruszyła, i
jednocześnie funkcja ta nie posiada wyjątku bad_exception
na swojej liście wyjątków, wywoływana jest funkcja
terminate()
Obsługa wyjątków
aBiblioteki języka C++ posiadają więcej definicji
standardowych wyjątków, którymi można się
posługiwać
aGdy definicje te nie wystarczają, można klasę
wyjątku odziedziczyć i pozmieniać według potrzeb
aPodstawowe klasy wyjątków to logic_error i
runtime_error, dziedziczące z klasy exception,
zdefiniowane w <stdexcept>
aKlasy te posiadają konstruktor akceptujący „string”
i funkcję what()
Wyjątki standardowe
exception
bad_alloc
bad_typeid
logic_error
domain_error
invalid_argument
length_error
out_of_range
bad_cast
bad_exception
runtime_error
range_error
overflow_error
underflow_error
Obsługa wyjątków
Przykład:
class MojBlad: public runtime_error{
public:
MojBlad( std::string & info = ”” ): runtime_error( info ){}
};
int main(){
try{
throw( MojBlad( ”Przykład błędu!” );
}catch( MojBlad &blad ){
cout << ”Wystąpił błąd: ” << blad.what() << endl;
}
}
Obsługa wyjątków
a Programiści języka C++ używają trzech podstawowych
określeń dotyczących bezpieczeństwa – w sensie wyjątków
– pisanego kodu
a Podstawowa gwarancja bezpieczeństwa oznacza, że po
wystąpieniu wyjątku nie następuje wyciek zasobów
a Oznacza także, że obiekty pozostają w stanie
umożliwiającym ich dalsze wykorzystanie
a Silna gwarancja bezpieczeństwa oznacza, że po wystąpieniu
wyjątku nie zmienia się stan programu
a Zapewnienie takiej gwarancji wymaga programowania w
stylu transakcji (całkowite wykonanie lub wycofanie)
a Gwarancja nie wyrzucania wyjątków oznacza, że funkcja
nigdy nie dopuszcza do wydostania się wyjątku na zewnątrz
Obsługa wyjątków
a Każdy dobrze napisany, niebanalny kod powinien dostarczać
jednej z powyższych gwarancji
a Dostarczana gwarancja powinna być najsilniejsza lecz taka,
która nie powoduje nadmiernego zużycia zasobów (czasu,
pamięci).
a Istnieją funkcje, które nie mogą zgłaszać wyjątków
(destruktor, operator delete)
a Aby zrealizować tak postawiony cel nieodzowne jest
staranne zaprojektowanie polityki obsługi sytuacji
wyjątkowych przed rozpoczęciem kodowania
a Najlepszym sposobem realizacji tego celu jest wykorzystanie
wzorców projektowych w celu zapewnienia automatycznej
dealokacji zasobów (RAII, techniki transakcyjne, technika
,,jedna funkcja – jedna odpowiedzialność"
Obsługa wyjątków - test
Pytanie: ile niezależnych ścieżek wykonania zawiera
poniższa, 3-liniowa funkcja?
String EvaluateSalaryAndReturnName( Employee e ) {
if( e.Title() == "CEO" || e.Salary() > 100000 ) {
cout << e.First() << " " << e.Last()
<< " is overpaid" << endl;
}
return e.First() + " " + e.Last();
}
Założenia: a) destruktory nie zgłaszają wyjątków
b) wyjątek zgłoszony przez jedną funkcję, niezależnie od
typu, liczony jest jako jedna ścieżka
Obsługa wyjątków - test
Ile znalazłeś?
1-2
3
4-14
15-23
– powtórz kurs podstaw języka C i C++
– przeciętna znajomość języka C++, słaba wyjątków
– masz świadomość, że istnieją wyjątki
– wyjątki to Twoja specjalność
Obsługa wyjątków - test
String EvaluateSalaryAndReturnName( Employee e ) {
if( e.Title() == "CEO" || e.Salary() > 100000 ) {
4 - konstruktor przekazywanego przez wartość parametru
zgłosić
12może
-–e.Title()
==
"CEO", wykonywany
jest
środek
instrukcji
if,
e.Title()
!=wyjątek
e.Salary()
>jak
100000,
wykonywany
9
–
jak
6
8
–
5
10
–
jak
7
11
–
jak
6
i
9
cout <<
e.First()
<<7 "– "w<<
e.Last()
5
–
funkcja
składowa
Title()
może
zgłosić
celu
dopasowania
do typu argumentu
6
–
operator==
może
zgłoscić
wyjątek
nie
wywoływana
e.Salary()!
jestjest
"środek"
instrukcji
if
<<wyjątek,
" is overpaid"
<<zwrócić
endl;== rezultat
lub może
przez
operatora
może być
konieczne
}
wartość,
a wyjątek
zgłosi konstruktor
wykonanie
konwersji,
konstruktor
3 – e.Title()
!= "CEO"
i e.Salary()
< 100000,a"środek"
konwertujący może zgłosić wyjątek
instrukcji if nie jest wykonywany
return e.First() + " " + e.Last();
}
Obsługa wyjątków - test
String EvaluateSalaryAndReturnName( Employee e ) {
19-20
– funkcje
if( e.Title() == "CEO" || e.Salary()
> 100000
) {First() i
Last() mogą zgłosić wyjątek,
lub zwrócić rezultat przez
cout << e.First() << " " << e.Last()
wartość, a konstruktor zgłosi
<< " is overpaid" << endl; wyjątek
}
12-16 – każde z 5-ciu21
wywołań
<<
– jak17-18
7 operatora
i 10– funkcje
First() i
może zgłosić wyjątek
Last() mogą zgłosić wyjątek,
lub zwrócić rezultat przez
return e.First() + " " + e.Last();
wartość, a konstruktor zgłosi
}
22 – jak236 wyjątek
–i 9jak 6 i 9
Obsługa wyjątków - epilog
Pytanie: czy w języku C++ istnieje konieczność sprawdzania,
czy udało się zarezerwować pamięć po każdym
wywołaniu operatora new, jak w poniższym kodzie?
MojaKlasa *wsk = new MojaKlasa();
if( p == NULL ){
throw bad_alloc();
}
Odpowiedź: nie! Biblioteki obsługujące dynamiczny przydział
pamięci same wyrzucają wyjątek std::bad_alloc()
w momencie, gdy zabraknie pamięci na obiekt
alokowany dynamicznie
Operator new nigdy nie zwraca wartości NULL!
Klasy składowe
aKlasa może zawierać w sobie inne klasy
nazywane klasami składowymi
aOdzwierciedla to sposób myślenia człowieka
typu „elementy składowe” (np. samochód
składa się z karoserii, silnika, kół, itp.), i jest
ważnym elementem enkapsulacji
aKlasy składowe są zwykle niepublicznymi
elementami składowymi
aKlasa ma dostęp jedynie do publicznych
składników swoich klas składowych
Klasy składowe
class Wielomian{
public:
Wielomian( int rozmiar = 1);
Wielomian( const std::vector<double> &wsp );
Wielomian( const Wielomian &wzor );
~Wielomian();
Wielomian & operator=( const Wielomian &wzor );
double operator()( double we );
void wypisz( ostream &wy ) const;
…
private:
std::vector<double> s_wspolczynniki;
std::dequeue<double> s_pamiec;
…
};
Klasy składowe
class ObiektDyskretny{
public:
ObiektDyskretny();
ObiektDyskretny( int rzA, int rzB, int k);
ObiektDyskretny( std::vector<double> wA, std::vector<double> wB );
float symuluj( float Wejscie );
…
protected:
int dB, dA, k;
Wielomian s_A, s_B;
private:
int sprawdzStopnie();
int ustawStopnie( int kNowe, int dBNowe, int dANowe )
};
Klasy składowe
ObiektDyskretny::ObiektDyskretny( int rzA, int rzB, int k_ )
: dA( rzA ), dB( rzB ), k( k_ ),
s_A( rzA ),
s_B( rzB ) {
if( parametryA == NULL || parametryB == NULL ){
cout << ”Utworzono nieokreślony obiekt!”;
}else{
cout << ”Utworzono obiekt i nadano parametry!”;
}
};
Obiekt klasy składowej może być inicjalizowany
wyłącznie za pomocą listy inicjalizacyjnej
Klasy składowe
aJeśli klasa obiektu będącego składnikiem nie
posiada konstruktora, obiektu nie inicjalizuje się
za pomocą listy inicjalizacyjnej
aJeśli klasa obiektu będącego składnikiem
posiada konstruktor domniemany, nie ma
obowiązku inicjalizacji przez listę inicjalizacyjną
aJeśli klasa obiektu będącego składnikiem
posiada konstruktory, ale nie posiada
konstruktora domyślnego, trzeba skorzystać z
listy inicjalizacyjnej (lub wystąpi błąd kompilacji)
Klasy składowe – deklaracja
zagnieżdżona
class ObiektDyskretny{
public:
ObiektDyskretny();
class Wielomian{
public:
Wielomian( int rozmiar, float *parametry = NULL,
float *pamiec = NULL );
float licz( float Wejscie );
...
}
...
};
Klasa zagnieżdżona - definicja
ObiektDyskretny::Wielomian::Wielomian( int rozmiar,
float *parametry = NULL, float *pamiec = NULL ){
...
}
Klasy składowe – definicja
zagnieżdżona
aKlasa wewnętrzna zadeklarowana wewnątrz
części prywatnej klasy zewnętrznej znana jest
tylko wewnątrz tej klasy
aKlasa wewnętrzna zadeklarowana wewnątrz
części publicznej klasy wewnętrznej jest znana
na zewnątrz tej klasy jako typ zagnieżdżony:
ObiektDyskretny::Wielomian w1;
aDeklaracja klasy zagnieżdżonej nie tworzy
jeszcze obiektu deklarowanej klasy
aTaki sposób deklaracji nie zmienia zwykłych
reguł dostępu (części prywatne obydwu klas są
wzajemnie niedostępne, o ile nie zdeklarowano
przyjaźni)
Dziedziczenie
class ObiektDyskretny{
public:
ObiektDyskretny();
ObiektDyskretny( std::vector<double> wA, std::vector<double> wB );
float symuluj( float Wejscie );
void wypiszA( ostream &wy ) const { return s_A.wypisz( wy ); }
void wypiszB( ostream &wy ) const { return s_B.wypisz( wy ); }
…
protected:
Wielomian s_A, s_B;
private:
…
};
Dziedziczenie
class Regulator : public ObiektDyskretny {
public:
float wartosc_zadana;
};
Lista dziedziczenia
Regulator( int dR, int dS );
Przesłonięcie funkcji.
float symuluj( float y);
Nie jest to przeładowanie, bo
int samonastrajanie();
funkcja ma taki sam zestaw
parametrów
Nie jest to błędem, ponieważ obydwie funkcje symuluj()
mają inny zasięg.
Dziedziczenie
protected:
Wielomian s_A, s_B;
int dA, dB, k;
protected:
Wielomian s_A, s_B;
int dA, dB, k;
public:
ObiektDyskretny();
ObiektDyskretny(int
nA, int nB, …);
float symuluj(float u);
public:
ObiektDyskretny();
ObiektDyskretny(int
nA, int nB, …);
float symuluj(float u);
ObiektDyskretny
ObiektDyskretny
Dziedziczenie
(publiczne)
public:
Regulator(int nR, int nS);
float symuluj(float w);
int samonastrajanie();
Regulator
Dziedziczenie
Regulator reg1( 5, 5 );
reg1.wypiszA( cout );
Wywołanie funkcji wypiszA()
odziedziczonej z klasy podstawowej
reg1.symuluj( 0.1 );
Wywołanie „nowej” wersji
funkcji symuluj()
reg1.ObiektDyskretny::symuluj( 0.5 );
Wywołanie przesłonientej funkcji symuluj()
z klasy ObiektDyskretny
Dziedziczenie – dostęp do
składników chronionych
class ObiektDyskretny{
…
protected:
};
Wielomian s_A, s_B;
class Regulator: public ObiektDyskretny{
public:
};
…
void wypisz( ostream& wy ) const {
}
wy << "Parametry mianownika: ";
s_A.wypisz( wy );
wy << endl << "Parametry licznika: ";
s_B.wypisz( wy );
Reguła przesłaniania
class Podst{
public:
};
void fun(){}
class Pochodna: public Podst{
public:
};
void fun(int i){}
Inny zestaw parametrów –przesłonięcie!
…
Pochodna obPoch;
obPoch.fun():
// Błąd!!!
Reguła przesłaniania
class Pochodna: public Podst{
public:
using Podst::fun;
};
"Odsłonięcie"
void fun(int i){}
…
Pochodna obPoch;
obPoch.fun( 4 );
// OK
obPoch.fun():
// OK
Dziedziczenie
aJest istotnym elementem programowania
orientowanego obiektowo
aPozwala na oszczędność pracy, bo umożliwia
wykorzystanie raz napisanego (i przetestowanego)
kodu – wszelkie zmiany wprowadza się w klasie
pochodnej
aPozwala na łatwe ustawienie hierarchii klas w
sposób bliski myśleniu człowieka (np. samochód
marki Opel jest rodzajem (dziedziczy z)
samochodu osobowego, a posiada części
składowe: silnik, koła, karoseria, itp.)
Dziedziczenie
aPozwala na korzystanie ze skompilowanych klas,
do których dołączono pliki nagłówkowe
aPozwala czasami na traktowanie obiektów
pochodnych tak, jak obiekty klasy podstawowej
(Opel to także samochód)
aUmożliwia tworzenie klas ogólnych, które same
niczego nie robią, a służą jedynie do
dziedziczenia, np. klasa „kolejka”, klasa
„kontrolka” (widget)
Dziedziczenie
class regulator : public ObiektDyskretny{
Sposób dziedziczenia
Klasa podstawowa
Klasa pochodna
private
private
protected
protected
public
public
public
Dziedziczenie
class regulator : protected ObiektDyskretny{
Sposób dziedziczenia
Klasa podstawowa
Klasa pochodna
private
private
protected
public
protected
protected
public
Dziedziczenie
class regulator : private ObiektDyskretny{
Sposób dziedziczenia
Klasa podstawowa
Klasa pochodna
private
private
protected
public
private
protected
public
Dziedziczenie publiczne
aNajczęściej spotykany typ dziedziczenia
aDaje dostęp do składników publicznych klasy
podstawowej wszystkim użytkownikom klasy
aDaje dostęp do składników chronionych klasy
podstawowej wszystkim potomkom klasy
aNie daje dostępu do składników prywatnych
klasy podstawowej
aZastosowanie tego typu dziedziczenia świadczy
zazwyczaj o zamiarze wykorzystania
mechanizmu polimorfizmu
Dziedziczenie chronione
aRzadko stosowane
aDaje dostęp do składników publicznych klasy
podstawowej jedynie potomkom klasy
aDaje dostęp do składników chronionych klasy
podstawowej wszystkim potomkom klasy
aPozwala potomkom klasy na poznanie relacji
dziedziczenia
aNie daje dostępu do składników prywatnych
klasy podstawowej
Dziedziczenie prywatne
aNie daje dostępu do żadnych składników klasy
podstawowej nawet potomkom klasy
aNie daje dostępu do składników prywatnych
klasy podstawowej
aUkrywa przed potomkami klasy relację
dziedziczenia
aJeżeli używane jest dziedziczenie, a polimorfizm
nie, to prawdopodobnie dziedziczenie powinno
być prywatne
Dziedziczenie prywatne
a Efekt dziedziczenia prywatnego jest podobny do
posiadania prywatnej klasy składowej
a Jednakże dziedziczenie prywatne:
`pozwala mieć tylko jeden egzemplarz klasy dziedziczonej
`może wprowadzić (niepotrzebne) wielokrotne dziedzictwo
`daje klasie pochodnej dostęp do składników protected klasy
podstawowej
`pozwala klasie pochodnej na zastąpienie funkcji wirtualnych
klasy bazowej
`pozwala jedynie składnikom klasy pochodnej i funkcjom
zaprzyjaźnionym na konwersję wskaźnika do klasy pochodnej na
wskaźnik do klasy bazowej
a Dziedziczenia prywatnego należy używać tylko wtedy,
gdy nie jest możliwe zastosowanie klasy składowej
Dziedziczenie wybiórcze
class regulator : private ObiektDyskretny {
public:
using ObiektDyskretny::symuluj;
…
};
Funkcja symuluj() musi mieć zakres „public” w klasie ObiektDyskretny!
W ten sposób udostępnione zostają wszystkie wersje przeładowanej
funkcji symuluj()!
Dziedziczenie wybiórcze może jedynie powtórzyć
zakres dostępu klasy podstawowej.
Nie może go ani rozluźnić, ani zaostrzyć!
Dziedziczenie wybiórcze
Udostępnienie tylko jednej wersji przeładowanej funkcji:
class regulator : private ObiektDyskretny {
public:
float ObiektDyskretny::symuluj( float u);
...
};
Dziedziczenie
Kolejność wykonywania konstruktorów:
anajpierw konstruktory klas podstawowych
dziedziczonych wirtualnie
apotem konstruktory innych klas podstawowych
apotem konstruktory ewentualnych klas
składowych
ana koniec konstruktor klasy pochodnej
Kolejność destruktorów jest odwrotna!
Dziedziczenie
Nie dziedziczy się:
akonstruktora (żadnego)
aoperatora przypisania (operator=)
adestruktora
Dziedziczenie – inicjalizacja
Konstr. kopiującego (ani żadnego innego) się nie dziedziczy!
Przy braku konstruktora kopiującego w klasie pochodnej
kompilator wygeneruje konstruktor kopiujący automatycznie
jako:
klasa::klasa( klasa& );
a kopiowanie będzie przebiegało następująco:
`typy wbudowane zostaną skopiowane
`dla klas podstawowych zostaną uruchomione konstruktory kopiujące
`dla klas składowych zostaną uruchomione konstruktory kopiujące
a jeśli składnik klasy lub jej „przodek” (klasa podstawowa)
posiada konstruktor kopiujący, który jest niedostępny,
konstruktor kopiujący klasy pochodnej nie zostanie
wygenerowany!
Dziedziczenie – przypisanie
Operatora przypisania się nie dziedziczy!
Przy braku operatora przypisania w klasie pochodnej
kompilator wygeneruje operator przypisania automatycznie
jako
klasa & klasa::operator=( klasa& );
a przypisanie będzie przebiegało następująco:
`typy wbudowane zostaną skopiowane
`dla klas składowych zostaną uruchomione ich operatory przypisania
`dla klas podstawowych zostaną uruchomione ich operatory
przypisania
a jeśli klasa posiada składnik typu const lub referencja,
operator = nie zostanie wygenerowany!
a jeśli klasa posiada operator przypisania, który nie jest
dostępny, operator = nie zostanie wygenerowany!
Dziedziczenie – przypisanie i
inicjalizacja
a Automatycznie generowany konstruktor kopiujący i operator
przypisania nie zawsze umożliwiają kopiowanie obiektów
typu const
a Aby wygenerowane funkcje umożliwiały kopiowanie
obiektów typu const, muszą być zdefiniowane jako:
klasa::klasa( const klasa& ); i
klasa & klasa::operator=( const klasa& );
a Kompilator wygeneruje takie funkcje tylko wtedy, gdy
wszystkie klasy podstawowe i składowe będą posiadały
konstruktory kopiujące i operatory przypisania przyjmujące
argumenty typu const
Dziedziczenie – przypisanie i
inicjalizacja
class Regulator: public ObiektDyskretny{
public:
Regulator(const Regulator &);
Regulator & operator=(const Regulator &):
…
};
Regulator::Regulator(const Regulator &wzor):
ObiektDyskretny(wzor){
Inicjalizacja klasy podstawowej
// Skopiowanie pozostałej części klasy
};
Dziedziczenie – przypisanie i
inicjalizacja
3 sposoby wywołania operatora przypisania z klasy podstawowej:
Regulator & Regulator::operator=(const Regulator &wzor){
(*this).ObiektDyskretny::operator=( wzor );
ObiektDyskretny *wskOD = this;
*wskOD = wzor;
// 2 sposób
ObiektDyskretny &refOD = *this;
refOD = wzor;
// 3 sposób
// Skopiowanie pozostałej części klasy
};
// 1 sposób
Konwersje standardowe przy
dziedziczeniu
aWskaźnik do obiektu klasy pochodnej może
być niejawnie przekształcony na wskaźnik
dostępnej jednoznacznie klasy podstawowej
aReferencja do obiektu klasy pochodnej może
być niejawnie przekształcony na referencję
dostępnej jednoznacznie klasy podstawowej
aJest to możliwe, gdy klasa podstawowa jest
dziedziczona publicznie
Konwersje standardowe przy
dziedziczeniu – kiedy?
aPrzy przesyłaniu argumentów do funkcji za pomocą
referencji lub wskaźników
aPrzy zwracaniu rezultatu funkcji za pomocą
referencji lub wskaźników
aPrzy przeładowanych operatorach – zamiast
obiektu (lub referencji) klasy podstawowej można
podać obiekt (lub referencję) klasy pochodnej
aPrzy wyrażeniach inicjalizujących, aby
zainicjalizować tę część klasy pochodnej, która jest
dziedziczona
Dziedziczenie
class ObiektDyskretny{
public:
ObiektDyskretny();
…
ostream & wypiszA( ostream &wy ) const {
return s_A.wypisz(wy);
}
ostream & wypiszB( ostream &wy ) const {
return s_B.wypisz(wy);
}
protected:
};
Wielomian s_A, s_B;
…
Dziedziczenie
void zapiszWspWielomianow( ObiektDyskretny *ob ){
string nazwa = PytajONazwePliku();
string wiel1 = PytajONazweWielomianu( 1 );
string wiel2 = PytajONazweWielomianu( 2 );
ofstream plik( nazwa.c_str() );
if( !plik ){
throw runtime_error("Błąd otwarcia pliku!");
}
plik << wiel1;
ob->wypiszA( plik );
plik << wiel2;
ob->wypiszB( plik );
}
Dziedziczenie
int main(){
class Regulator:
public ObiektDyskretny{
…
…
zapiszWspWielomianow( &ob1 );
};
…
Regulator reg1( 3, 3 );
ObiektDyskretny ob1( 2, 1, 1 );
zapiszWspWielomianow( &reg1 );
}
…
Dzięki konwersjom standardowym!
Podobna konwersja wystąpi wtedy, gdy funkcja
zapiszWspWielomianow() będzie przyjmowała parametry
przez referencję
Dziedziczenie wielokrotne
aKlasa może wywodzić się bezpośrednio od więcej
niż jednej klasy podstawowej – takie dziedziczenie
nazywa się dziedziczeniem wielokrotnym
aZaletą takiego rozwiązania jest możliwość
powiązania kilku klas o różnych właściwościach
aDana klasa podstawowa może się pojawić na liście
pochodzenia tylko raz
aDefinicja klasy (pełna) na liście pochodzenia musi
być kompilatorowi wcześniej znana
Dziedziczenie wielokrotne
aKażda klasa podstawowa ma określony na
liście pochodzenia swój sposób dziedziczenia
(private, protected, public, domyślnie
private)
aKonstruktory klas podstawowych zostaną
uruchomione w takiej kolejności, w jakiej
klasy te są umieszczone na liście
pochodzenia
Dziedziczenie wielokrotne
class Pojazd{
public:
Pojazd();
void jedź();
string silnik() const;
…
};
class Łódź{
public:
Łódź();
void płyń();
string silnik() const;
…
};
Autor przeprasza za, niewłaściwe jego
zdaniem, używanie polskich liter w
przykładach kodu dotyczących amfibii.
Jednakże odpowiedniki bez polskich
liter – "Lodz", "jedz", czy "plyn" – są
w tym przypadku wręcz śmieszne,
i utrudniają zrozumienie sensu kodu.
Dziedziczenie wielokrotne
class Amfibia: public Pojazd, public Łódź{
public:
Amfibia();
…
};
Dziedziczenie wielokrotne
aMechanizm dziedziczenia wielokrotnego można
zastąpić, niemal bez zmian w składni, pewnymi
,,sztuczkami" programistycznymi
aMechanizm dziedziczenia wielokrotnego
przydaje się najbardziej:
`w celu dziedziczenia kilku klas definiujących interfejs
(zazwyczaj klas abstrakcyjnych)
`w celu łączenia funkcjonalności kilku bibliotek, do
których nie kodu źródłowego nie ma dostępu
`w celu ułatwienia wykorzystania polimorfizmu w kilku
ścieżkach dziedziczenia
Ryzyko wieloznaczności
a Wieloznaczność występuje wtedy, gdy składnik klasy o tej
samej nazwie istnieje w więcej niż jednej klasie
podstawowej:
Amfibia a;
cout << a.silnik() << endl;
// a.Pojazd::silnik() czy a.Łódź::silnik()?
a Sama definicja tak skonstruowanej klasy nie jest błędna,
lecz błąd wystąpi w przypadku próby odwołania się w klasie
Amfibia do funkcji składowej silnik()
a Błąd wystąpi nawet wtedy, gdy funkcja składowa silnik() w
jednej z klas ma zakres „private”
Ryzyko wieloznaczności
a W obrębie klasy pochodnej (Amfibia) można posługiwać się
operatorem zakresu w celu usunięcia wieloznaczności:
Pojazd::y lub Łódź::y
a Takie rozwiązanie ma wady:
`niejednoznaczność będzie „dziedziczona” w przypadku, gdy klasa
Amfibia będzie klasą podstawową innej klasy
`jeśli składnik był funkcją wirtualną, to poprzedzenie go operatorem
zakresu anuluje mechanizm polimorfizmu
a Lepszym rozwiązaniem jest zdefiniowanie w klasie
pochodnej (Amfibia) składnika o takiej samej nazwie, który
przesłoni składniki z klas podstawowych – to rozwiązanie
usuwa także problemy przy dziedziczeniu klasy pochodnej
Dziedziczenie wielokrotne
class Amfibia: public Pojazd, public Łódź{
public:
Amfibia();
string silnik() const { return Pojazd::silnik(); }
…
};
Uwaga! Niejednoznaczność nie wystąpi, jeżeli składniki
różni długość ścieżki dziedziczenia!
class Samochod: public Pojazd { … };
class Amfibia: public Samochod, public Łódź{ … };
Dziedziczenie wirtualne
Dziedzic:
class Podstawowa{
public:
int dana;
};
class Pochodna1: public Podstawowa{
};
Pochodna1:
Pochodna2:
Podstawowa:
public:
int dana
Podstawowa:
public:
int dana
class Pochodna2: public Podstawowa{
};
class Dziedzic: public Pochodna1, public Pochodna2{
public:
void funkcja(){
dana = 5;
}
};
BŁĄD! O którą wersję
składnika „dana” chodzi?
Dziedziczenie wirtualne
int main(){
Dziedzic *dz = new Dziedzic;
Podstawowa *p;
p = dz;
}
WIELOZNACZNOŚĆ!
Na którą wersję klasy „Podstawowa” wskazać?
Dziedziczenie wirtualne
Rozwiązanie pierwszego problemu:
class Dziedzic: public Pochodna1, public Pochodna2{
public:
void funkcja(){
Oznaczenie
Pochodna1::dana = 5;
zakresu
}
};
Rozwiązanie drugiego problemu:
int main(){
Dziedzic *dz = new Dziedzic;
Podstawowa *p;
p = (Pochodna1*)dz;
}
pożądanego
Obydwa rozwiązania są doraźne, gdyż nie
usuwają problemu istnienia dwóch wersji
klasy Podstawowa wewnątrz klasy
Dziedzic
Jawna konwersja wskaźnika do klasy
Pochodna1, potem niejawna konwersja
do klasy Podstawowa
Dziedziczenie wirtualne
Lepsze rozwiązanie obydwu problemów:
class Pochodna1 : public virtual Podstawowa{
...
Dziedziczenie
};
wirtualne
class Pochodna2 : public virtual Podstawowa{
...
Dziedzic:
};
Pochodna1:
Dziedziczenie wirtualne
dotyczy klasy najbardziej
podstawowej, nic nie da
wirtualne odziedziczenie klas
Pochodna1 i Pochodna2
Pochodna2:
Podstawowa:
public:
dana
Dziedziczenie wirtualne
Pytanie:
Która z klas dziedziczących wirtualnie
uruchomi konstruktor klasy podstawowej?
Odpowiedź:
Dziedzic:
Pochodna1:
Pochodna2:
Podstawowa:
public:
dana
Żadna. Za uruchomienie konstruktora klasy
podstawowej odpowiedzialny jest
konstruktor klasy ,,najbardziej pochodnej”
– w tym wypadku klasy „Dziedzic”
To jedyna sytuacja, gdy za konstrukcję klasy
„dziadka” odpowiada „wnuk” (lub „prawnuk”, lub
„praprawnuk”, lub ...)
Dziedziczenie wirtualne
a Konstruktory klas wirtualnych są wykonywane zawsze
przed konstruktorami innych klas podstawowych
a Jeżeli klasa dziedziczy wirtualnie więcej klas, o kolejności
wywołania konstruktorów decyduje lista pochodzenia
a Jeżeli dziedziczenie wirtualne jest kilkupokoleniowe
(wnuki, prawnuki, itd.), każdy z konstruktorów klas
dziedziczących powinien zawierać wywołania konstruktora
klasy dziedziczonej wirtualnie
a Przy konstrukcji danego obiektu kompilator uwzględni
wówczas jedynie wywołanie w klasie „najbardziej
pochodnej”
Dziedziczenie wirtualne
a Jeżeli na liście inicjalizacyjnej klasy „najbardziej pochodnej”
nie ma wywołania konstruktora klasy dziedziczonej
wirtualnie, kompilator uruchomi jej konstruktor
domniemany
a Dlatego przy tworzeniu klasy, która będzie dziedziczona
wirtualnie warto zaopatrzyć ją w domniemany konstruktor
a Dziedziczenie wirtualne stosuje się zazwyczaj dziedzicząc
klasy abstrakcyjne lub czysto abstrakcyjne, czyli takie, które
posiadają niewiele składników-danych, lub nie posiadają ich
wcale
a Konstruktory klas czysto abstrakcyjnych nie posiadają
zazwyczaj parametrów
Dziedziczenie wirtualne
aDziedziczenie wirtualne może być poprzedzone
zarówno zakresem public, jak i protected i private
aJeżeli klasa „wnuka” dziedziczy kilka klas, z których
choć jedna dziedziczy wirtualnie w sposób
publiczny, pozostałe ograniczone zakresy
dziedzictwa nie mają w klasie „wnuka” znaczenia
"Pimpl" - zapora ogniowa dla
kompilatora
Dana jest przykładowa klasa:
class K{
public:
// Interfejs publiczny klasy
protected:
// Interfejs chroniony klasy
private:
int s_dana;
void fun();
…
};
Pomimo enkapsulacji, zmiana dowolnego składnika
prywatnego klasy wymusza konieczność kompilacji całego
kodu, który wykorzystuje klasę K
"Pimpl" - zapora ogniowa dla
kompilatora
Rozwiązanie: ukrycie kodu prywatnej części klasy (czyli
implementacji) w oddzielnej klasie pomocniczej, dostępnej
przez wskaźnik:
class KImpl;
class K{
public:
// Interfejs publiczny klasy
protected:
// Interfejs chroniony klasy
private:
KImpl *pimpl;
};
"Pimpl" - zapora ogniowa dla
kompilatora
a Klasa KImpl powinna zostać zapisana w oddzielnym pliku
a W klasie K funkcje i dane klasy KImpl są dostępne przez
wskaźnik (może to być zręczny wskaźnik)
a Może istnieć konieczność zapewnienia wskaźnika zwrotnego
(wskaźnika "self" w klasie KImpl wskazującego na obiekt
klasy K)
a Zmiana implementacji klasy KImpl nie powoduje
konieczności rekompilacji kodu korzystającego z klasy K
(stąd nazwa – zapora ogniowa dla kompilatora)
a Technika stosowana bardzo często (wzorzec projektowy),
zwana, od zwyczajowej nazwy wskaźnika do klasy
implementacji, techniką "pimpl"
Funkcje wirtualne
class Regulator {
public:
float wartosc_zadana;
Regulator( int dR, int dS );
virtual float symuluj( float y){
return -y;
}
...
};
int samonastrajanie();
Deklaracja funkcji
wirtualnej
Funkcje wirtualne
Klasa pochodna:
class RegulatorPID : public Regulator {
public:
virtual float symuluj( float y){
...
return P + I + D;
};
...
};
może się pojawić,
lecz nie jest konieczne
Funkcje wirtualne
Inna klasa pochodna:
class RegulatorGPC : public Regulator {
public:
float symuluj( float y){
...
u += q*(w-y);
return u;
...
};
Funkcje wirtualne
Funkcja pracująca z referencją do klasy Regulator:
void rysuj( Regulator &dowolnyRegulator, float y ){
float u;
u = dowolnyRegulator.symuluj( y )
}
cout << u << endl;
Funkcje wirtualne
int main(){
RegulatorGPC regGPC;
RegulatorPID regPID;
// Obiekt klasy RegulatorGPC
// Obiekt klasy RegulatorPID
RegulatorMV regMV;
// Obiekt klasy RegulatorMV
Regulator *wskRegulatora;
...
// Wskaźnik do dowolnego
// regulatora
wskRegulatora = &regGPC;
cout << wskRegulatora->symuluj(0.1) << endl;
}
Która wersja funkcji symuluj() zostanie uruchomiona?
Funkcje wirtualne
wskRegulatora = &regPID;
cout << wskRegulatora->symuluj(0.05);
Czy te wywołania
uruchomią tę samą
wskRegulatora = &regMV;
wersję funkcji
cout << wskRegulatora->symuluj(0.25); symuluj()?
...
rysuj( regGPC, 0.1);
rysuj( regPID, 0.05);
rysuj( regMV, 0.25);
Którą wersję funkcji symuluj()
uruchomi wywołanie wewnątrz
funkcji rysuj()? Czy tę samą?
Funkcje wirtualne
Dzięki deklaracji funkcji symuluj(float) jako wirtualnej:
wskRegulatora = &regGPC;
cout << wskRegulatora->symuluj(0.1) << endl;
// uruchamia funkcję RegulatorGPC::symuluj( float )
wskRegulatora = &regPID;
cout << wskRegulatora->symuluj(0.05);
// uruchamia funkcję RegulatorPID::symuluj( float )
wskRegulatora = &regMV;
cout << wskRegulatora->symuluj(0.25);
// uruchamia funkcję RegulatorMV::symuluj( float )
Gdyby nie słowo kluczowe virtual, zawsze uruchamiana
byłaby funkcja Regulator::symuluj( float )
Powyższy kod wykazuje polimorfizm!
Funkcje wirtualne
a Funkcje wirtualne to najważniejsze, z punktu widzenia
technik orientowanych obiektowo, narzędzie
programistyczne
a Funkcje wirtualne umożliwiają klasom pochodnym
zastąpienie metod klasy bazowej swoimi własnymi
wersjami
a Kompilator uruchomi wersję z klasy pochodnej ilekroć
obiekt, na którym pracuje, jest rzeczywiście obiektem
klasy pochodnej
a Ta „inteligencja” kompilatora będzie działała, gdy obiekt
klasy pochodnej będzie wskazywany wskaźnikiem lub
referencją do klasy podstawowej
Funkcje wirtualne
a W przypadku zwykłych (nie-wirtualnych) funkcji kompilator
już na etapie kompilacji decyduje, jaki jest adres funkcji
składowej, w zależności od wskaźnika lub referencji, przez
którą następuje odwołanie (tzw. „wczesne wiązanie”)
a W przypadku funkcji wirtualnych adres funkcji składowej,
którą należy wykonać, nie jest określany na etapie
kompilacji, lecz na etapie uruchomienia programu
a Technika ta nosi nazwę „późnego wiązania” lub
„dynamicznego wyboru”
a Realizacja „późnego wiązania” zazwyczaj wiąże się się z
obecnością dodatkowego, ukrytego wskaźnika do tablicy
funkcji wirtualnych klasy, dodatkowego wskaźnika dla
każdej wirtualnej funkcji oraz dwóch dodatkowych pobrań
adresów
Funkcje wirtualne
aW efekcie stary, skompilowany do postaci binarnej,
kod (np. z biblioteki), może wywołać kod nowy, nie
istniejący w momencie kompilacji tego pierwszego
aDzięki temu bardzo łatwo jest uzupełnić stary
program o obsługę nowego typu danych
aCech ta nosi nazwę rozszerzalności
(ang. extensibility)
aKod programu rzeczywiście może podejmować
akcje zależne od obiektów, na których pracuje
(orientować się obiektowo)
Funkcje wirtualne
Wczesne wiązanie dla funkcji wirtualnych
zachodzi:
a gdy funkcję wirtualną wywołuje się na rzecz konkretnego
obiektu, np.:
RegulatorPID regPID(1.1, 1, 0);
regPID.symuluj( -0.25 );
a gdy parametr funkcji zostanie przekazany przez wartość:
void zleRysuj( Regulator r, float u ){
r.symuluj( u );
…
}
Funkcje wirtualne
Wczesne wiązanie dla funkcji wirtualnych
zachodzi:
a gdy wywołując funkcję przez wskaźnik lub referencję
jawnie użyje się operatora zakresu, np.:
wskRegulatora->Regulator::symuluj(...);
(tego sposobu używa się jedynie, by wywołać funkcję
wirtualną z klasy podstawowej)
a gdy wywołanie funkcji wirtualnej następuje z konstruktora
lub destruktora klasy podstawowej
(gdy podczas konstrukcji obiektu klasy pochodnej pracuje
konstruktor klasy podstawowej, obiekt klasy pochodnej
jeszcze nie jest kompletny)
Funkcje wirtualne
a Funkcje wirtualne mogą także być umieszczone w sekcji
„protected”
a Deklarowanie funkcji wirtualnych w sekcji „private” rzadko
ma sens
a Funkcje wirtualne mogą być deklarowane jako „in-line” –
mechanizm wstawiania kodu będzie dla nich uruchamiany
jedynie wtedy, gdy zachodzi dla nich „wczesne wiązanie”
a Funkcje wirtualne nie mogą być funkcjami statycznymi
Destruktor wirtualny
class RegulatorGPC : public Regulator {
…
};
int main(){
Regulator *wskR = new RegulatorGPC;
…
delete wskR;
}
Który destruktor zostanie uruchomiony?
(Destruktor klasy pochodnej uruchamia destruktr klasy podstawowej)
Destruktor wirtualny
class Regulator {
public:
virtual ~Regulator();
...
}
Pomimo różnych nazw
destruktor może być wirtualny!
class RegulatorGPC : public Regulator {
public:
~RegulatorGPC();
...
}
Zasada przybliżona: jeżeli klasa
posiada jakąkolwiek funkcję
wirtualną, destruktor także powinien
być wirtualny!
Wirtualna funkcja inline
class RegulatorPID: public Regulator {
public:
…
virtual double symuluj( double y ) {
…
return P+I+D;
}
…
};
a Fakt ,,wirtualności'' przeważa nad mechanizmem inline
a W przypadku, gdy dla funkcji zachodzi wczesne wiązanie, kompilator
zastosuje rozwijanie kodu funkcji
Funkcje wirtualne - przesłanianie
class Podst {
public:
virtual void f( int ) { cout << "Podst::f(int)" << endl; }
virtual void f( double ) { cout << "Podst::f(double)" << endl; }
virtual void g( int i = 10 ) { cout << i << endl; }
};
class Poch: public Podst {
public:
void f( vector<double> ) { cout << "Poch::f(vector)" << endl; }
void g( int i = 20 ) { cout << "Poch::g() " << i << endl; }
};
Funkcje wirtualne
int main() {
Podst pd;
Poch pch;
Podst* wskPd = new Poch;
pd.f(1.0);
pch.f(1.0);
wskPd->f(1.0);
pd.g();
pch.g();
wskPch->g();
delete wskPd;
return 0;
}
Rezultat: "Podst::f(double)"
Rezultat: "Poch::f(vector)"!
Wyjaśnienie: deklaracja w klasie
pochodnej przeładowująca funkcję
wirtualną innym typem przesłoniła
funkcje z klasy podstawowej
W celu ,,odsłonięcia'' funkcji należy
posłużyć się instrukcją ,,using"
Funkcje wirtualne
int main() {
Podst pd;
Poch pch;
Podst* wskPd = new Poch;
pd.f(1.0);
pch.f(1.0);
wskPd->f(1.0);
pd.g();
pch.g();
wskPch->g();
delete wskPd;
return 0;
}
Rezultat: "Podst::f(double)"
(ponieważ dopasowanie typów
funkcji przeładowanych odbywa się
na statycznym typie klasy)
Funkcje wirtualne
int main() {
Podst pd;
Poch pch;
Podst* wskPd = new Poch;
pd.f(1.0);
pch.f(1.0);
wskPd->f(1.0);
pd.g();
pch.g();
wskPch->g();
delete wskPd;
return 0;
}
Rezultat: 10
Rezultat: "Poch::g() 20"
Rezultat: "Poch::g() 10"
(ponieważ wartości domniemane
zawsze są brane ze statycznego
typu klasy)
Funkcje wirtualne
int main() {
Podst pd;
Poch pch;
Podst* wskPd = new Poch;
pd.f(1.0);
pch.f(1.0);
wskPd->f(1.0);
pd.g();
pch.g();
wskPch->g();
delete wskPd;
return 0;
}
Katastrofa! Z braku wirtualnego
destruktora co najmniej wyciek
pamięci.
Klasy abstrakcyjne
class Regulator {
public:
virtual float symuluj( float y) = 0;
...
Funkcja czysto wirtualna (ang. pure virtual)
};
a Klasa zawierająca choć jedną funkcję czysto wirtualną
nosi nazwę klasy abstrakcyjnej
a Jeśli klasa jest abstrakcyjna, to nie można utworzyć
obiektu tej klasy – taka klasa służy tylko do
dziedziczenia
Klasy abstrakcyjne
class Regulator {
public:
virtual float symuluj( float y) = 0;
...
};
float Regulator::symuluj( float y ){
// Tutaj, ewentualnie, wspólna część kodu
}
class RegulatorGPC{
public:
float symuluj( float y ){
return Regulator::symuluj( y );
}
...
};
Klasy abstrakcyjne
a Klasa pochodna od klasy abstrakcyjnej musi
zaimplementować swoje wersje wszystkich funkcji
czysto wirtualnych, lub także będzie abstrakcyjna
a Funkcja czysto wirtualna może mieć definicję, ale nie
definicję „in-line”
a Czysto wirtualny destruktor musi mieć definicję (choćby
pustą)!
class CzWirtualna{
public:
…
~CzWirtualna() = 0;
};
CzWirtualna::~CzWirtualna(){}
Klasy abstrakcyjne
a Definicję funkcji czysto wirtualnej można wykorzystać w
celu umożliwienia twórcom klas pochodnych świadomej
akceptacji domyślnego działania funkcji
class K{
public:
};
virtual int fun() = 0;
…
int K::fun(){ … }
class P: public K{
public:
};
int fun(){ return K::fun() }
...
Klasy abstrakcyjne
a Definicję funkcji czysto wirtualnej wykorzystuje się także
do diagnozy sytuacji, gdy funkcja taka nie powinna
zostać wywołana (dobry kompilator do tego nie dopuści)
class K{
public:
};
virtual int fun() = 0;
…
int K::fun(){
cout << "Wywołanie funkcji czysto wirtualnej, kończę!"
<< endl;
throw logic_error();
}
Klasy abstrakcyjne
a Interfejs klasy (lub użytkownika) należy do
najcenniejszych zasobów firmy programistycznej
a Tworzenie dobrego interfejsu zazwyczaj trwa dłużej niż
tworzenie klas realizujących funkcje interfejsu
a Tworzeniem interfejsu zwykle zajmują się najbardziej
wykwalifikowani („najdrożsi”) pracownicy firmy
a Jest tak dlatego, że warto poświęcić czas na
odseparowanie interfejsu od implementacji
a Do tworzenia interfejsów wykorzystuje się klasy
abstrakcyjne
Klasy abstrakcyjne
class Parametry;
// Klasa przechowująca parametry regulatora
class Regulator{
public:
virtual float symuluj( float y) = 0;
virtual float wyjscie() = 0;
virtual float wartoscZadana( float w ) = 0;
virtual int samonastrajanie() = 0;
virtual int nastaw( Parametry *par ) = 0;
};
To wszystko, co tworzy interfejs – brak składników danych!
Identyfikacja typu
aPozwala na określenie, w trakcie pracy
programu, typu obiektu wskazywanego
przez wskaźnik lub powiązanego z
referencją klasy podstawowej
aJest wykorzystywana poprzez operator
dynamicznego rzutowania dynamic_cast
oraz poprzez operator typeid
aMechanizm identyfikacji typu jest rzadko
(koniecznie) potrzebny
Rzutowanie dynamiczne
a Operator static_cast umożliwia statyczne – czyli na
etapie kompilacji – przechodzenie w górę drzewa
hierarchii klas (z typu klasy pochodnej na typ klasy
podstawowej)
a Dynamiczne – czyli na etapie wykonania programu –
rzutowanie można wykonywać operatorem
dynamic_cast
a Dynamiczne rzutowanie można wykonywać zarówno w
górę, jak i w dół drzewa hierarchii klas
a Dynamiczne rzutowanie w dół drzewa hierarchii klas
można wykonywać jedynie w stosunku do klas, które
posiadają choć jedną funkcję wirtualną
Rzutowanie dynamiczne
a Operator dynamic_cast sprawdza, czy wykonywana
konwersja jest poprawną konwersją w sensie
przechodzenia po drzewie hierarchii klas
a W przypadku, gdy konwersja nie jest prawidłowa, a
konwertowany jest wskaźnik, operator zwraca wskaźnik
pusty
a W przypadku, gdy konwersja nie jest prawidłowa, a
konwertowana jest referencja, operator zgłasza wyjątek
typu bad_cast
a Testy wykonywane w trakcie pracy operatora wiążą się
z pewnym (zwykle nieistotnym) nakładem czasowym
Rzutowanie dynamiczne
void nastawy( Regulator *reg ){
…
RegulatorPID *rPID = dynamic_cast<RegulatorPID*>(reg);
if( rPID ){
rPID->ustawP( 1.15 );
}else{
cout << "To nie jest regulator PID!";
}
}
Rzutowanie dynamiczne
void nastawy( Regulator &reg ){
…
try{
RegulatorPID &rPID = dynamic_cast<RegulatorPID&>(reg);
rPID.ustawP( 1.15 );
}catch( bad_cast ){
cout << "To nie jest regulator PID!";
}
}
Operator typeid
a Operator typeid umożliwia uzyskanie informacji o typie
klasy najbardziej pochodnej wskazywanej przez pewien
wskaźnik lub referencję klasy bazowej
a Operator w rezultacie zwraca obiekt klasy type_info,
zdefiniowanej w nagłówku <typeinfo>
a Klasa type_info nie posiada publicznie dostępnych
konstruktorów ani operatora przypisania
a Klasa type_info definiuje metodę name(), która zwraca
wskaźnik do łańcucha znakowego zawierającego nazwę
klasy (format zależny od implementacji)
a Klasa przeładowuje operatory == oraz !=, co umożliwia
pisanie przenaszalnego kodu
Operator typeid
a Użycie operatora typeid na typie, który nie jest
polimorficzny, powoduje zwrot statycznej informacji o
typie
a Użycie operatora na dereferencji wskaźnika o zerowej
wartości powoduje zgłoszenie wyjątku typu bad_typeid
int main(){
RegulatorGPC rGPC;
Regulator *wskR = &rGPC; // Zał: klasa Regulator polimorf.
cout << typeid(*wskR).name();
}
if( typeid(Regulator) == typeid(*wskR) ){
cout << "Typy identyczne!";
else{
cout << "Różne typy!";
}
Przestrzenie nazw (namespaces)
aWydzielone przestrzenie nazw umożliwiają
zgrupowanie globalnych zmiennych,
funkcji i klas w jednym „pojemniku”
aDzięki przestrzeniom nazw możliwe jest
uniknięcie błędów wynikających z
redefinicji obiektów o tych samych
identyfikatorach
Przestrzenie nazw (namespaces)
namespace Pierwsza{
int lInt = 5;
float f( int x ){ … }
}
namespace Druga{
float f;
unsigned int lInt = 10;
}
int lInt = 100;
int main(){
int lInt = -1;
cout << Pierwsza::lInt << endl;
cout << Druga::lInt << endl;
cout << f;
// Błąd! Brak definicji zmiennej f w tym zakr.
cout << Pierwsza::f( Pierwsza::lInt ) << endl;
cout << lInt << " " << ::lInt << endl;
}
Przestrzenie nazw (namespaces)
a Przestrzenie nazw są podobne do klas
a Istotne różnice to:
`nie jest możliwe utworzenie obiektu przestrzeni nazw,
`deklaracja przestrzeni nazw musi pojawić się w miejscu o zasięgu
globalnym, bądź w innej przestrzeni nazw,
`deklaracji przestrzeni nazw nie trzeba kończyć średnikiem,
`definicja przestrzeni nazw może być kontynuowana w innej
jednostce kompilacji,
`przestrzeni nazw można nadać przezwisko (,,alias''):
namespace PrzestrzenODlugiejINiewygonejNazwie{
…
}
namespace TaDluga=PrzestrzenODlugiejINiewygodnejNazwie;
Przestrzenie nazw (namespaces)
Dyrektywa using:
namespace Pierwsza{
int lInt = 5;
Zakres ważności to zakres bloku,
float f( int x ){ … }
którym dyrektywę użyto
}
namespace Druga{
unsigned int lInt = 10;
}
int main(){
using namespace Pierwsza;
cout << f(lInt) << endl;
// OK!
cout << Druga::lInt << endl;
cout << Pierwsza::lInt;
// Też OK!
}
w
Przestrzenie nazw (namespaces)
Instrukcja using:
namespace Pierwsza{
Zakres ważności to zakres bloku,
int lInt = 5;
w którym instrukcję użyto
float f( int x ){ … }
}
namespace Druga{
unsigned int lInt = 10;
}
int main(){
using Pierwsza::f;
cout << f(lInt) << endl;
// Błąd! lInt nieznane
cout << f(Druga::lInt) << endl;
// OK!
cout << f(Pierwsza::lInt) << endl;
// Też OK!
}
Anonimowe przestrzenie nazw
namespace{
int lInt = 5;
float f( int x ){ … }
}
int main(){
cout << f(lInt) << endl;
}
// OK!
a Zmienne z anonimowej przestrzeni nazw są dostępne, bez
kwalifikacji, wewnątrz jednostki, w której przestrzeń
zdefiniowano
a Zmienne nie są dostępne w innych jednostkach kompilacji
a Zaleca się używanie anonimowych przestrzeni nazw zamiast
kwalifikatora static w stosunku do zmiennych globalnych
Standardowe nagłówki
języka C++
a Obecnie istniejący standard ISO dla języka C++ nie
stosuje zaimka .h dla włączanych nagłówków
standardowych
a Standardowa biblioteka C++ gwarantuje 18
odpowiedników plików nagłówkowych języka C, takich
jak stdio.h, stdlib.h, itp.
a Pliki te mogą być włączane za pomocą #include <cxxx>
lub #include <xxx.h>, gdzie xxx oznacza nazwę
biblioteki (np. math, stdlib, itp.)
a Dla pierwszego z tych zapisów wszystkie deklaracje są
umieszczone w przestrzeni nazw std (zalecane)
a W przypadku drugiego z zapisów wszystkie deklaracje są
umieszczone w przestrzeni std i przestrzeni globalnej