Język C++

Transkrypt

Język C++
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
• CPU
Standard 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ątkowa
C++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 Document
www.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 Eckel
Thinking 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)
Siddhartha Rao
C++. Dla każdego.
Wydanie VII
Nicholas A. Solter,
Scott J. Kleper
C++ Zaawansowane
programowanie
Wydanie III po angielsku
Stephen Prata
Język C++. Szkoła
programowania.
Wydanie VI
C++ literatura (4)
Anthony Williams
Język C++
i przetwarzanie
współbieżne w akcji
D. Ryan Stephens
C++ Receptury
(O’Reilly)
Nicolai M. Josuttis
C++. Biblioteka
standardowa.
Podręcznik
programisty
David Vandevoorde,
Nicolai M. Josuttis
C++ szablony.
Vademecum
profesjonalisty
Wydanie II po angielsku
Aktualizacja w roku 2015
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
KURSY DOSTĘPNE ON-LINE
Karol „Xion” Kuczmarski – Kurs C++ (Megatutorial)
Sektor van Skijlen – C++ bez cholesterolu
Piotr Białas, Wojciech Palacz – Zaawansowane C++
pl.wikibooks.org/wiki/C++ – niekompletny jeszcze…
Frank B. Brokken – C++ Annotations Ver. 9.8.x
– „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)
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:
.C, .cc, .cpp, .CPP, .c++, .cp, .cxx
– 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!
Nie utrudniajmy sobie życia i używajmy g++
W przypadku Dev-C++ chodzi o zapisanie
pliku ź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 standardu
Wsparcie kompilatora dla standardu C++11 (C++14)
wymaga dodatkowej opcji (flagi):
g++ -std=c++11 …
g++ -std=c++14 …
Przykładowo (linux):
Program jest w katalogu /usr/bin
Pliki nagłówkowe w katalogu /usr/include/c++/4.8
Biblioteki w katalogu /usr/lib/gcc/i486-linux-gnu/4.8
Na pracowni komputerowej kompilator g++ 4.92 i nowszy jest
dostępny tylko w nowych instalacjach (Dev-C++ 5.11)
Kompilowanie i linkowanie (konsolidacja)
Prosty program o nazwie myprog z pliku prog1.cc
g++ -std=c++11 -o myprog prog1.cc
// to samo: g++ -std=c++11 prog1.cc -o myprog
Plik obiektowy (bez konsolidacji do programu)
g++ -std=c++11 -c -o prog1.o prog1.cc
Konsolidacja do programu wykonalnego
g++ -std=c++11 -o myprog prog1.o
Uruchomienie programu (linux)
./myprog
gdzie „ . ” (kropka) oznacza pełną nazwę ścieżki,
chyba że ścieżka do katalogu z programem jest w zmiennej PATH
Kompilatory – kilka uwag
Można mieć zainstalowane kilka wersji g++ oraz biblioteki standardowej.
Napisz: g++ i postukaj „tab” (pokażą się wszystkie programy zaczynające się na g++)
Zwykle g++ to link symboliczny do jednej z wersji. Sprawdzenie wersji:
g++ -v
// lub: g++ --version
Inne kompilatory warte uwagi:
clang ver. 3.7, Intel C++ ver. 15, Microsoft Visual Studio C++ 2015
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”)
Pierwszy program
Program wymaga napisania funkcji:
auto main() -> int { }
lub
int main() { }
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() {
return 0;
}
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.
int fun3() { Nie stawiaj średnika za nawiasem kończącym
return 2; definicję funkcji – jest niepotrzebny!
};
#include ”mojplik.h” ;
#define FLAGA ;
class Klasa { } ;
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ę
for (int i=0; i<10; ++i);
kodu, w którym przez pomyłkę
// tutaj instrukcja, którą może ktoś
mamy niezamierzone działanie…
// zamierzał wykonać 10 razy…
Pierwszy program – który coś robi
#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
}
#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ć konkretne
using 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>
extern istream cin;
extern ostream cout;
extern ostream cerr;
extern ostream clog;
Deklaracje obiektów odpowiadających
za pracę na strumieniu wejście / wyjście.
Obiekty konstruowane przed main()
Hierarchia klas
odpowiedzialnych za
pracę na strumieniach
#include <ostream>
#include <istream>
cin – obiekt odpowiedzialny za obsługę standardowego strumienia wejściowego
(zwykle powiązanego z klawiaturą), wywołanie powoduje opróżnienie buforu cout
cout – 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ń niebuforowany
clog – 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’ oraz
wykonuje flush – opróżnienie buforu
ends – wkłada znak kończący łańcuch
znakowy, czyli symbol zerowy ’\0’
flush – opróżnia bufor
ws – czyta i ignoruje białe znaki
Kod 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 zapytania
KONIECZNE 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
void fun( double d, short n, int );
nie ma definicji (ciała) funkcji, czyli części ujętej
w nawiasy { } to jest to tylko deklaracja
• deklaracja poprzedzona jest specyfikatorem extern, w znaczeniu obiektu
zdefiniowanego w innym pliku
extern double d;
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ę!
• deklaracja z użyciem extern jako sposób konsolidacji (linkowania) kodu
extern ”C” int fun ( float );
extern ”C” { /* tutaj lista deklaracj */ }
gdy chcemy „zlinkować” z kodem z innego
języka, musimy zadeklarować nazwy
obiektów tam zdefiniowanych
Deklaracje (2)
• deklarujemy statyczną składową w definicji klasy
class Foo {
static int n;
};
zmienna statyczna w definicji klasy to dopiero jej deklaracja – jak się
później dowiemy, taką zmienną definiuje się dopiero poza ciałem klasy
• deklarujemy nazwę klasy (bez jej definiowania): class Foo;
• deklarujemy (silny) typ wyliczeniowy (C++11)
enum class EColor;
enum struct EShape : char;
„klasa wyliczeniowa” (albo „silny typ wyliczeniowy”)
pozwala na uprzednią deklarację wraz ze specyfikacją
typy danych wyliczeniowych (typ musi być całkowity)
Deklaracjami nazywamy również:
• deklarację z użyciem typedef
typedef int Calkowity, *PtrCalkowity;
Calkowity n1; PtrCalkowity ptr1;
n1 jest typu int, zaś
ptr1 jest typu „wskaźnik do int”
• deklarację użycia using lub dyrektywę using
using std::cout;
using namespace std;
deklaracja użycia czegoś
dyrektywa użycia którejś przestrzeni nazw
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.
class Foo;
struct Foo* ptr1;
Foo *ptr2;
One ring to rule them all,
one ring to find them,
One ring to bring them all
and in the darkness bind them.
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 inline
DEFINICJE klas, DEFINICJE szablonów
plik nagłówkowy ( .h )
Cytat z „Megatutorial-u” Karola Kuczmarskiego
(Państwa niewiele starszego kolegi…)
Dyrektywa #include jest głupia jak cały preprocesor.
Wielokrotne włączenie tego samego
nagłówka (#include) – wielokrotna definicja – pogwałcenie reguły ODR – błąd!
Aby temu zapobiec, w plikach nagłówkowych zawsze korzystamy
z dyrektyw preprocesora (blokada, tzw. header guard)
#ifndef FIGURA_H
#define FIGURA_H
// tutaj cała zawartość pliku
#endif // FIGURA_H
DEFINICJE zmiennych, funkcji, metod klas
plik źródłowy ( .cc )
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
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
plik h1.h
plik h2.h
plik test.cc
plik main.cc
#ifndef H1_H
#define H1_H
void fun() { }
#endif
#ifndef H2_H
#define H2_H
#include ”h1.h”
// coś jeszcze
#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 here
collect2: ld returned 1 exit status
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 std
Konieczna 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 globalnej
albo (zagnieżdżone) wewnątrz innej przestrzeni
tym samym
namespace nie można zagnieździć (definiować)
wewnątrz definicji funkcji (również main) ani
nie można zdefiniować wewnątrz klasy
using – deklaracja/dyrektywa użycia czegoś z przestrzeni nazw
void f();
namespace A {
void g(); namespace X {
}
using ::f; // globalne f
}
using A::g; // g z A
void h() {
X::f(); // woła ::f
X::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
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) istnieje
void bar() {
using A::f; // f jest synonimem A::f;
// zarówno A::f(int) i A::f(char)
f(’a’); // woła f(char)
}
Można dodawać kolejną zawartość
przestrzeni nazw (nawet w kolejnych
plikach) ale musi być to zrobione w
tym samym zasięgu znaczeniowym.
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()
}
}
Jeśli w dwóch różnych przestrzeniach
te same nazwy – konflikt w momencie użycia
Typy danych oraz specyfikatory
Podstawowe typy wbudowane:
char, int, float, double
wchar_t – rozszerzony typ znakowy (wielkość zależna od implementacji)
char16_t i char32_t – do reprezentacji znaków standardu Unicode
Specyfikatory (rozszerzają lub zawężają, ze znakiem lub bez znaku)
short – long, signed – unsigned
•
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 double
Typ
bool
true – odpowiednik wartości całkowitej 1
false - odpowiednik wartości całkowitej 0
(dwa stany logiczne: true, false – to są stałe)
nie nadawać stanu logicznego za pomocą operacji arytmetycznej (+ lub -)
→ niejawna konwersja typów
• operatory: && || ! < > <= >= == !=
• komendy sterujące: if, for, while, do, ? :
• kompilator przekształca int w bool
typedef, using
typedef – synonim typu istniejącego (nie żadna nowa
definicja), najczęściej używany do uproszczenia zapisu
(wiele razy w bibliotece standardowej) np.
typedef basic_fstream<char> fstream; // w nagłówku fstream
typedef basic_string<char> string; // w nagłówku string
using – może być użyte zamiennie jako typedef
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 globalna
zakomentowanie globalnej zmiennej i próba
namespace mojeKlocki
odwołania się do niej spowoduje błąd kompilacji
{ 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ę instrukcja
cout << "a lokalne = "<< a <<endl; // 3
using namespace mojeKlocki;
cout << "a lokalne = "<< a <<endl; // 3
cout << "a z mojeKlocki = "<< mojeKlocki::a <<endl; // 7
cout << "b z mojeKlocki = "<< b <<endl; // 8
int b = 12;
cout << "b lokalne = "<< b <<endl; // 12
cout << "a nielokalne = "<< ::a <<endl; // 1
cout << "c z nienazwanej przestrzeni " << ::c << endl; // 99, to też jest zmienna globalna
}
cout << "a lokalne = "<< a <<endl; // 2
}
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
pamięć statyczna
Rodzaje obiektów i ich cechy (static – global)
Obiekty globalne i statyczne (globalne) – przykłady
// w przestrzeni nazw lub przestrzeni globalnej
int i; // domyślnie łączenie zewnętrzne
const int ci = 0; // domyślnie globalny const jest static (łączony wewnętrznie)
extern const int eci; // jawna deklaracja łączenia zewnętrznego
static int si; // jawnie static
// podobnie funkcje – uwaga – nie ma globalnych funkcji stałych (const)
int foo(); // domyślnie łączenie zewnętrzne
static int bar(); // jawna deklaracja static
// nienazwana przestrzeń nazw jako polecany sposób na ograniczenie zakresu
// widzialności nazw do danej jednostki translacji
namespace {
int i; // mimo łączenia zewnętrznego niedostępne
// w innych jednostkach translacji
class niewidoczna_dla_innych { };
}
sterta
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
Rodzaje obiektów i ich cechy (stack, heap)
Stałe (const) a preprocesor
• za pomocą preprocesora
#define PI 3.1415
od miejsca zdefiniowana do końca pliku
• modyfikator const
const float pi = 3.1415;
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):
#define PRINT (STR, VAR) cout << STR ” = ” << VAR << endl
#define PR (x) cout << #x ” = ” << x << ”\n”
wtedy gdzieś w kodzie: PRINT(”wartosc”, a );
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ę
auto i = 7; // typ int
auto x = wyrażenie // x będzie typu zwracanego przez wyrażenie
• 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)
template<class T>
int whatever(T t) {
T x; // równoważne do auto x poza szablonem
};
auto – zastosowania ( C++11 )
• przykłady
auto a = 7; // a jest typu int
const auto *ptr = &a, b = 5; // ptr typu const int*, b typu const int
static auto d = 3.14; // d typu double
auto x = { 1, 2, 3 }; // x typu std::initializer_list<int>
• działa również z operatorem new
new auto(1); // alokowanym typem jest int
auto z = new auto(’a’); // alokowanym typem jest char, z jest typu char*
• szczególnie wygodne do dedukcji typów iteratorów
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>&
• niestety, wewnątrz wyrażeń lambda auto nie działa
auto – zastosowania ( C++11 )
• zmienne zadeklarowane za pomocą auto są nadal wielkościami
statycznymi, stąd niemożliwe jest:
void fun( auto arg ) { } // źle: autodedukcja typu argumentu niemożliwa
class 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
• możliwe jest
class Foo {
static const auto n = 0; // static tak
};
• uwaga
auto s = ”hello world”; // jest typu const char*
auto& s = ”hello world”; // jest typu referencja do const char[12] czyli tablicy
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()
auto ci = m.cbegin(); // ci typu std::map<int, std::string>::const_iterator
Nowa składnia dla pętli for (tzw. range-based loop)
vector<int> v { 1,2,3,4,5 };
for ( int i : v ) cout << i << endl; // i bezpośrednio każdym elementem wektora
for ( auto i : v ) cout << i << endl; // to samo co powyżej
for ( int& i : v ) cout << ++i; // może być też referencją i zmieniać zawartość!
for ( auto& i : v ) cout << ++i; // to samo co powyżej
for (const int i : v ) jakasMetoda( i ); // const/volatile też możliwe
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ę?
w C++11 nie ma
problemu zagnieżdżonych
nawiasó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:
const vector<int> w;
auto v1 = w; // v1 typu vector<int>, const zignorowane
auto& v2 = w; // v2 typu const vector<int>& - ale jeśli przez referencję, to ok
Tablice i nazwy funkcji redukują się do wskaźników:
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ę
Jeżeli const/volatile nie na najwyższym poziomie, to zostają:
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>&
Za pomocą auto można deklarować więcej zmiennych w linii:
auto zmienna = s, *ptr_zmienna = &s; // dedukcja typu inicjalizatora – ten sam typ
auto i = 3, d = 3.14; // błąd – rożne typy inicjalizatorów
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:
int a = --b ? b : (b = -10); // jeśli b=1, to a=-10
• operator , zwraca wartość ostatniego z wyrażeń
• operator sizeof
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
++ -- ~ ! sizeof new
delete
unary (prefix)
*&
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
3
Right-to-left
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 volatile
do 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
enum EPozycja {
eAsystent, // 0
eAdiunkt, // 1
eProfesor // 2
};
definiowanie zmiennych podobnie
jak dla typu wbudowanego:
EPozycja pracownik = eAsystent;
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)
int a = eAsystent; // ok, konwersja!
pracownik = 3; // bez rzutowania to jest błąd
można też zadać wartość
enum EPozycja {
eAsystent = 5,
eAdiunkt = eAsystent + 2,
eProfesor
};
nie można robić
inkrementacji:
pracownik++;
„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)
sizeof( EPozycja ) = ?
• nie można określić typu, na jakim zbudowane sa identyfikatory
… pewnie 4 ale…
może być mniej
• niemożliwa jest uprzedzająca deklaracja typu wyliczeniowego
nienazwany 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 { jeden = 1, dwa = 2, cztery = 4 };
•
enum – silny typ wyliczeniowy (C++11)
enum class nazwa { lista identyfikatorów };
zamiast class może być struct
• nazwy z listy wyliczeniowej nie wyciekają na zewnątrz
• nie następuje niejawna automatyczna konwersja na int
enum Alert { green, yellow, election, red }; // standardowy, stary typ wyliczeniowy
enum class Color { red, blue }; // nowy, silny, identyfikatory nieznane na zewnątrz
enum struct TrafficLight { red, yellow, green }; // jak widać, nie koliduje z niczym
Alert a = 7; // błąd: zwykły przypadek, nie ma konwersji z int na enum
Color c = 7; // błąd: nie ma konwersji int->Color
int a2 = red; // ok: możliwa konwersja Alert::red->int
int a3 = Alert::red; // błąd w C++98, ok w C++11
int a4 = blue; // błąd: blue nieznane w tym zakresie
int a5 = Color::blue; // błąd: brak konwersji Color->int
Color a6 = Color::blue; // ok
•
•
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ść
enum class Color : char { red, blue }; // sizeof( Color ) taki sam jak sizeof( char )
możliwa jest deklaracja wyprzedzająca
enum class Color : char; // deklaracja
void foo(Color* p); // teraz można już użyć
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ć
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
• 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
Referencje – lewe ( T &, const T & )
Terminologia wprowadzająca
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
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.
Nie istnieją:
• referencje do referencji
Niestała referencja ( T & ) może
• tablice referencji
wskazywać na l-wartość. Stała
• wskaźniki do referencji
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.
Referencje – zakazane cv, prawe ( T && ) (C++11)
Nie istnieje:
T & const
Przykład
Kwalifikatory cv dla referencji, są niedopuszczalne.
Wprowadzone przez typedef, albo argument szablonu, są zignorowane.
pamiętajmy, że w c++ funkcjonuje pojęcie kwalifikatora cv, czyli
const i/lub volatile, zatem to co piszemy o const, dotyczy też volatile
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 int
napisanie 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
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” (move
assignment operator). Więcej o tym – w dalszej części wykładu.
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 obiektu
Ponownie 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
int *vInt = n; // wcześniej int n[10];
vInt = &n[0]; // to samo
można nimi operować jakby były tablicą, vInt[2] to samo co n[2]
* tu jako
operator
wyłuskania
zmiennej
ze wskaźnika
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
• 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 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…
double f1 = 0.;
const double pi = 3.14;
double *vZmienna = &f1;
const double *vStala1 = &pi;
const double *vStala2; // wskaźnik do stałego obiektu, jeszcze nie ustawiony
vStala2 = vZmienna;
•*vZmienna = 25.;
double * const vStalyZmienna = const_cast<double * const>( vStala1 );
vZmienna = vStalyZmienna;
• nie wszystkie zmiany możliwe…
nie można usunąć przydomka const z żadnego obiektu (można tylko rzutować)
T * & - referencja do wskaźnika na typ T
T & * - takie coś nie istnieje!
przydaje się jako argument funkcji,
wtedy wskaźnik – argument, można
wewnątrz funkcji przestawić na inny adres
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)
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
• 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;
Funkcje – sposoby przekazania parametrów
• sposoby przekazywania parametrów do funkcji
void 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 parametr
void 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 przenoszenia
void fun(const float&& f); // zwykle bez sensu, bo blokuje przenoszenie
void 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:
float mojaLiczba = 0.;
fun(mojaLiczba); // przez referencję, tak samo jak przez wartość
fun(&mojaLiczba); // przez wskaźnik, trzeba podać adres obiektu za pomocą &
• na temat dedukcji typu zwracanego przez funkcję:
auto f(); // zwracany typ nieznany
auto f() { return 5; } // zwracany typ int
auto f(); // redeklaracja – ok
int f(); // błąd – traktowane jako deklaracja inne funkcji
auto f() { return f(); } // błąd, dopóki typ zwracany jest nieznany,
// nie można wołać rekurencyjnie
auto suma(int i) {
if (i==1) return i; // zwracany typ teraz znany
else 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
Funkcje – tablice argumentami, inline
• tablice jako argumenty funkcji nie są przekazywane przez wartość
void func1(int a[], int rozmiar); // musimy podać rozmiar
void func2(int *a, int rozmiar); // array-to-pointer decay
void func3(int (&a) [10]); // tylko 10-elementowa tablica
void func4(int macierz[][3], int rozmiar);
• 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
void fun();
definicja w plku źródłowym poprzedzona specyfikatorem inline
inline void fun() { /* definicja */ }
• 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)
Funkcje – wartości domyślne
argumenty domniemane (od prawej do lewej) tylko w deklaracji
void fun(int a, void*, float = 3.14, char znak= '\0');
• 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ślnymi
void 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!
Funkcje – wartości domyślne, przykłady
void g(int = 0, ...); // ok, bo … (wielokropek) to nie argument, tylko ich lista
void 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ślnych
f(4); // błąd: niepoprawna liczba argumentów
void f(int, int = 5); // OK
f(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
… - 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 )
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, argument
int 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 0
return sum;
}
int main() {
cout << sum(3, 1, 1, 1, 1, 1) << endl; // OK, możemy mniej liczyć, 3
cout << sum(8, 1, 1, 1, 1, 1, 1) << endl; // śmieci, wyszliśmy poza listę
}
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ów
pierwszym zawsze jest ścieżka i nazwa programu
argv[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”
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ących
komfortowo i bezpiecznie z punktu widzenia kontroli typów.
Nie musimy się też martwić o wielkość wypełnianego buforu!
#include <string>
// konwertuje zmienną typu int na łańcuch znakowy std::string
std::string to_string( int value );
// taki sam, gdy działało sprintf o odpowiednio dużym buforze
std::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 );
Pomiędzy „łańcuchem znakowym” a innymi typami
Podobnie w drugą stronę, jeśli mamy łańcuchy znakowe (np. parametry programu), możemy
teraz 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.
#include <string>
// konwertuje łańcuch znakowy std::string na typ całkowity
Int 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 zmiennoprzecinkowy
float 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 );
Klasa std::string
Utworzenie obiektu typu std::string odbywa się podobnie jak dowolnej zmiennej
typu wbudowanego. Jednak w tym przypadku można też stworzyć obiekt
zainicjalizowany danymi – obiekt budowany jest przez specjalną metodę składową,
konstruktor. Konstruktorów może być dowolnie wiele, muszą różnić się
argumentami.
Zbadajmy jaki jest rozmiar
i bufor obiektu s1:
#include <iostream>
#include <string>
using namespace std;
auto main() -> int {
string s1; // pusty string
}
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 false
s1.shrink_to_fit();
s1.reserve(57); // jakie capacity() ?
Tworzymy kolejne obiekty std::string
Oto kilka sposobów na utworzenie / przypisanie obiektu typu std::string
const char *t = ”tekst do inicjalizacji”;
s1 = t;
string s2( s1); // obiekt „na wzór” istniejącego wcześniej
string s3( t, 8 ); // pierwsze 8 znaków
string s4( s2, 6, 8 ); // od 6-tego do 6+8 -mego, czyli…
string s5( 100, ’*’ ); // chcę mieć sto gwiazdek
string s6 = ”konstrukcja”;
string s7 = { ”uniwersalna inicjalizacja” }; // = opcjonalnie
Działania na stringach bez problemu:
s1 = s1 + ” drugi ” + s2;
s1 += s6;
Rozmiary, usuwanie…
Maksymalny rozmiar i pewna stała:
max_size() // zwykła metoda składowa
string().max_size(); // string „w locie”
Sprawdźcie jaka jest wartość tej stałej:
std::string::npos
Wielkie usuwanie (erase – metoda składowa):
erase( nr_od, nr_ile ); // zwraca „referencję do”
erase( adres_od, adres_do ); // zwraca „adres” nast. znaku
Specjalne funkcje adresowe (zwracające tzw. iteratory czyli obiekty „udające”
wskaźniki – przechowalniki adresu i wiedzy o typie):
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ńca
std::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;
Bufor cin nadal trzyma starą zawartość, tu poczytajcie jak to wyczyścić
http://cpp0x.pl/kursy/Kurs-C++/Poziom-1/Obsluga-strumienia-wejsciowego/12
ifstream plik("tekst.txt"); // np. wziąć z: pl.lipsum.com
while ( ! plik.eof() ) {
getline (plik, str);
s10 += str; // czego tu brakuje? Znak końca linii… + ’\n’
}
// wypiszcie na ekran… cout << s10;
Przebiegamy po stringu…
String to forma kontenera sekwencyjnego… jakby tablicy znaków…
s1 = "wlazl kotek na plotek i mruga";
for ( auto c : s1 ) cout << c << " "; // range-based loop
for ( 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] << " ";
ITERATOR – inteligentny „pośrednik” pomiędzy kontenerami (zasobnikami),
„wskaźnik” z adresem do operacji na konkretnych typach, strumieniach…
string::iterator it; // na razie pusty
auto it = s1.begin();
it = s1.begin(); // początek … end() koniec
while ( it != s1.end() ) { cout << *it << endl; ++it; }
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>
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
Czy potrafisz określić cel klasy w jednym zdaniu?
– 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
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
abstrakcja danych
• hermentyzacja
wynik definiowania klas,
• hierarchia
koncentrujemy się na zewnętrznym
hierarchia
sposób tworzenia
wzajemnych relacji
pomiędzy abstrakcjami
danych
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
Typy hierarchii
"jest-czymś",
realizowane poprzez
dziedziczenie, umożliwia stosowanie relacji
ogólne-specyficzne
RACHUNEK BANKOWY
jest:
ROR
LOKATA
"ma-coś",
budowanie z
elementów składowych,
wprowadza stosunek
część-całość
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
KLASA
podstawowa 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();
Skąd zwykła (niestatyczna)
metoda wie, na jakim
komplecie danych (na
jakim obiekcie) pracuje?
Otrzymuje niejawnie
specjalny wskaźnik: this
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();
zawiera adres konkretnego
obiektu danego typu
this is it
(kilka słów o „tym” wskaźniku)
(stały) wskaźnik this – niejawnie zdefiniowana składowa
każ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ą
void Prostokat::ustawParam(double x, double y) {
this->bokX = x; // można jawnie zapisać, ale nie trzeba
this->bokY = y;
}
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)
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 =)
void Prostokat::kopiuj(const Prostokat& figura) {
if (this != &figura) { // tu sprawdzamy czy nie to samo
bokX = figura.bokX;
bokY = figura.bokY;
}
}
• 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
Klasa – prawa dostępu
public:
protected:
private:
w dowolnej kolejności
etykiety mogą się powtarzać
protected
• tak jak private,
plus dostęp dla
domyślny
• dostęp bez
klas pochodnych
ograniczeń (z
(dziedziczenie) private
wnętrza i poza
• dostęp tylko z
zakresem klasy)
wnętrza klasy (z
• tutaj jest interfejs
zewnątrz dla klas lub
• składniki to funkcje
fukcji - przyjaciół)
• tutaj szczegóły
implementacji
public
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) { /* … */ }
lista inicjatorów konstruktora,
„miejsce”, gdzie powstają i są
inicjalizowane obiekty
otwarcie { oznacza
skonstruowanie obiektu
„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!
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ślne
Foo::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)
Foo nowy = stary; // stary też klasy Foo
Foo nowy = Foo(stary);
// ale nie: nowy = stary; tu pracuje operator =
– przekazanie jako argument funkcji przez wartość
– zwrócenie wartości funkcji (obiekt tymczasowy
inicjalizowany konstruktorem kopiującym – zależy
od optymalizacji kompilatora)
Konstruktor kopiujący – kiedy konieczny?
class A {
A::A(const A& src) {
// klasa bez konstruktora kopiującego
numer = src.numer;
int numer;
nazwa = new char[src.strlen()+1];
char* nazwa;
strcpy(nazwa, src.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 a1
a2.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
Zbudujemy klasę
Definicję klasy zapiszmy w pliku tstring.h
#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
pola
dostępu
do klasy
z zewnątrz
Zapiszmy też prosty plik main.cc
#include ”tstring.h”
#include <iostream>
using namespace std;
int main () {
TString s1;
}
size_t jest nazwą (typedef) na bezznakowy
typ całkowity wystarczająco pojemny aby
opisać wielkość dowolnego obiektu
[ m.in. zwracany przez sizeof ]
class TString {
protected:
private:
}; // pamiętaj o średniku
#endif
};
// póki nie będziemy dziedziczyć,
// to pole nas nie interesuje
char* ptr;
std::size_t size;
Zdefiniujmy konstruktor
Zdeklarujmy konstruktor (c-tor) :
class TString {
public:
TString( const char* s = nullptr );
};
Metody definiujemy w tstring.cc
#include ”tstring.h”
#include <iostream>
using namespace std;
TString::TString( const char* s ) :
ptr(nullptr), size(0) {
if (s > 0) {
Kompilujemy dodatkowo dodając w linii
size = strlen(s);
ptr = new char[ size + 1 ]; opcję –D definiowananazwa
czyli –D DEBUG (może być bez spacji), przykładowo:
strcpy( ptr, s );
g++4.8 –std=c++11 –DDEBUG
}
main.cc tstring.cc –o prog
#ifdef DEBUG
cout << "TString c-tor " << size << " - " << ( ptr ? ptr : "pusty" ) << endl;
#endif
Możemy teraz dopisać w main kolejny obiekt, np.
}
TString s2("inicjalizacja slowem");
Zdefiniujmy destruktor
Zdeklarujmy destruktor (d-tor):
class TString {
public:
TString( const char* s = nullptr );
~TString();
};
TString::~TString() {
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ęść
#ifdef DEBUG
cout << "TString d-tor " << size << " - " << ( ptr ? ptr : "pusty" ) << endl;
#endif
delete [] ptr;
}
Śledzenie pokrokowe programu (debuger) gdb
Kod 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
Zdefiniujmy konstruktor kopiujący
Zdeklarujmy konstruktor kopiujący (cc-tor):
// poniżej to nie jest przypisanie
class TString {
public:
TString( const char* s = 0 );
TString( const TString& s );
~TString();
};
if (size > 0) {
ptr = new char[ size + 1 ];
strcpy( ptr, s.ptr );
}
Możemy dopisać w main.cc
TString 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ęść
Operacja podobna do tej z konstruktora.
#ifdef DEBUG
cout << "TString cc-tor " << size << " - " << ( ptr ? ptr : "pusty" ) << endl;
#endif
}
Zdefiniujmy operator przypisania kopiujący
Zdeklarujmy operator= kopiujący:
class TString { public:
TString& operator=
( const TString& s );
};
Możemy dopisać w main.cc
// poniżej jest przypisanie, bo obiekt
// po lewej już istnieje
s3 = ”alfa beta”;
s3 = s2;
TString& TString::operator=
(const TString& s ) {
if ( this != &s ) { // if ( *this != s ) {
delete [] ptr; ptr = nullptr; size = s.size;
if ( size > 0 ) {
this – specjalny wskaźnik, który otrzymuje
ptr = new char[ size + 1 ];
każda niestatyczna składowa klasy, a w którym
strcpy( ptr, s.ptr );
zapisany jest adres bieżącego obiektu, na
}
którego argumentach działać ma metoda
}
#ifdef DEBUG
cout << "TString copy operator= " << size << " - " << ( ptr ? ptr : "pusty" ) << endl;
#endif
return *this; // nie zapomnij zwrócić obiektu!
}
Zdefiniujmy konstruktor przenoszący
Konstruktor przenoszący (mvc-tor):
class TString {
public:
};
TString( TString&& s );
Możemy dopisać w main.cc
// move „maskuje” tożsamość obiektu
TString 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) {
// 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;
Operacja przenoszenia dzieje się automatycznie
s.size = 0;
wtedy, gdy obiekt źródłowy „nie ma nazwy” i
„nie ma adresu”
#ifdef DEBUG
cout << "TString mvc-tor " << size << " - " << ( ptr ? ptr : "pusty" ) << endl;
#endif
}
Zdefiniujmy operator przypisania przenoszący
Zdeklarujmy operator= przenoszący:
class TString { public:
TString& operator=
( TString&& s );
Możemy dopisać w main.cc
// ponownie „ukrywamy obiekt”
// za pomocą std::move
s3 = std::move( s1 );
TString& TString::operator=
};
( TString&& s ) {
if ( this != &s ) {
delete [] ptr; // usuń dotychczasowy zasób
size = s.size; // typy proste się tylko (po prostu) kopiuje
ptr = s.ptr; // tu zabieramy adres wskaźnika (przeniesienie praw własności)
s.size = 0; // obiekt, któremu zabraliśmy, zerujemy
s.ptr = nullptr; // wskaźnik również zerujemy
}
#ifdef DEBUG
cout << "TString move operator= " << size << " - " << ( ptr ? ptr : "pusty" ) << endl;
#endif
return *this; // nie zapomnij zwrócić obiektu!
}
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 A
B 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 kluczowym
explicit 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 kluczowym
explicit 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ść
•
•
•
"przeciążenie wyjścia"
class A { /* … */ };
class B { /* … */ };
class C {
public:
operator A() const;
operator B() const;
};
// tu się zaczyna problem
// przeładowane wersje fun
void fun(A a);
void fun(B b);
// gdzieś w programie:
C c;
fun(c); // 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
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 dziedziczenia – znaczenie
• 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ć traktowana
jako klasa bazowa (w dziedziczeniu publicznym),
oznacza to, że:
– można wskaźnikiem (referencją) klasy bazowej pokazywać na obiekty klas
pochodnych i nie jest to operacja powodująca utratę części wskazywanego
obiektu
– dziedziczenie prywatne nie jest prawdziwym dziedziczeniem
sposoby dziedziczenia (public, protected, private)
klasa bazowa A klasa pochodna B
private
public
protected
protected
public
public
dziedziczenie interfejsu
• dostęp do części prywatnej klasy bazowej A
tylko przez jej interfejs
• mamy dostęp do części public i protected
z tym że protected na zewnątrz niedostępny
(tak samo jak private)
klasa bazowa A klasa pochodna B
private
protected
protected
protected
public
klasa bazowa A klasa pochodna B
private
private
private
protected
public
dziedziczenie implementacji
• domyślny, niepodanie specyfikacji
oznacza dziedziczenie private
class B : A { /* ... */ };
• stosujemy gdy chcemy ukryć fakt dziedziczenia
deklaracja dostępu (using)
•
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 dziedziczeniu
wielokrotnym (najpierw zawsze jest rozstrzygana
wieloznaczność)
dziedziczenie kilkupokoleniowe i inicjalizacja
A
B
C
• klasa B jest dla klasy C klasą podstawową bezpośrednią, zaś
klasa A – klasą podstawową pośrednią
• inicjalizowanie klasy podstawowej poprzez wywołanie jej
konstruktora
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)
kompozycja i dziedziczenie
// wcześniej definiujemy klasy: MW, MX, MY, MZ oraz klasę A
class B : public A {
MY my;
• kolejność wywołania konstruktorów
MX mx;
elementów składowych jest związana
public:
z kolejnością ich wystąpienia
B(int i) : mx(), my(), A() { /*...*/ }
w definicji klasy, a nie z kolejnością
~B();
na liście inicjatorów
};
class C : public B {
• w przykładzie po lewej,
MW mw;
kolejność konstrukcji:
MZ mz;
A, MY, MX, B, MW, MZ, C
public:
C() : mw(3.14), B(45) { /*...*/ }
• kolejność destrukcji
~C();
jest dokładnie odwrotna:
};
C, MZ, MW, B, MX, MY, A
ukrywanie nazw w klasach pochodnych
• przedefiniowanie (redefining) w przypadku
zwykłych funkcji składowych klasy bazowej
• zasłanianie (overriding) w przypadku funkcji wirtualnych klasy bazowej
class 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 przypadkach
niewidoczne (zasłonięte)
stają się również funkcje
przeciąż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
czego się nie dziedziczy (C++98)
• konstruktory (patrz C++11)
• operator=
• destruktor
KONSTRUKTOR KOPIUJĄCY
•
•
• trzeba je zdefiniować samemu
(lub zostaną wygenerowane automatycznie!)
• można jednak we własnych definicjach wywołać wersje
z klas podstawowych do obsłużenia odziedziczonej części obiektu
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ć
jawne wywołanie konstruktora
class A { public:
kopiującego klasy A, inaczej zostałby
A(const A& a);
wywołany zwykły konstruktor
};
domyślny klasy A
class B : public A {
public:
B(const B& b) : A(b) { /* ... */ }
};
dziedziczenie konstruktorów ( C++11 )
Deklaracja using może być użyta z konstruktorami klasy bazowej
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 niepotrzebne
using Foo::Foo; // powoduje niejawną deklarację Bar::Bar(int);
// taki konstruktor zdefiniowany/wygenerowany tylko w przypadku użycia
void fun(); // nadpisuje Foo::fun()
Bar( int, int ); // tu już samemu napisany konstruktor, bez dziedziczenia
};
Bar b1( 7 ); // ok w C++11 dzięki dziedziczeniu konstruktora
Bar b2( 3, 5 ); // normalne wywołanie Bar::Bar(int, int);
Dziedziczone konstruktory zachowują swoją specyfikację (tzn. są np.
explicit lub są wyrażeniem stałym constexpr).
dziedziczenie konstruktorów – dostępność, inicjalizacja składników ( C++11 )
Może się okazać, że odziedzczony konstruktor jest prywatny
class Foo { private:
explicit Foo(int);
};
class Bar : public Foo {
public:
using Foo::Foo;
błąd objawia się
private:
w momencie próby użycia
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
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: string s = ”niezainicjalizowany”;
int x = 0, y = 0;
czego się nie dziedziczy
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
musi być podany zakres ( A:: )
• 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; }
};
ponieważ nowodefiniowany
B::operator= przesłania funkcję
operatora klasy bazowej
alternatywnie mozna tak:
(*this).A::operator=(b);
lub
A *wsk = this; // możemy wskaźnikiem klasy bazowej
(*wsk) = b; // pokazać na obiekt pochodny
lub
A &ref = *this; // możemy referencji do klasy bazowej
ref = b;
// przypisać obiekt klasy pochodnej
co jest dziedziczone i warto wspomnieć
• składniki statyczne i oczywiście definiujemy je dla klasy
w 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 A
static 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 sygnatury
funkcji
co jeszcze jest dziedziczone
• operatory konwersji typów – bo w klasach pochodnych mamy komplet
informacji do wykonania konwersji
• konstruktory konwersji nie są dziedziczone, ale…
class C { public:
C(int n) : c(n) {}
int c;
};
class D : public C { public:
D(int n) : C(n+3), c(n) {}
int c;
};
class A { public:
A(int n) : a(n) {}
A (const C& c) : a(c.c) {}
int a;
};
class B : public A { public:
B(int n) : A(n+2), a(n) {}
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 typu
fun(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 A
jest widziany jako obiekt klasy A,
więc wypisana jest część
obiektu z klasy A (tu zasłonięta
w klasie B)
obiekt klasy D jest również
obiektem typu klasy C, więc
możliwa jest konwersja obiektu
typu D na obiekt typu A, wypisana
jest ta część obiektu z klasy C
(tu zasłonięta w klasie D)
rzutowanie w górę (upcasting) i w dół
• jest bezpieczne bo od typu bardziej wyspecjalizowanego
przechodzimy do typu bardziej ogólnego
• jest naturalne: wskaźnikiem (referencją) typu bazowego
pokazujemy na typ pochodny
class A { public: int a; };
class B : public A { public: int b; };
// gdzieś w programie…
B b;
A *wskA = &b;
A &refA = b;
– poprzez wskA i refA oczywiście nie mamy dostępu do części zdefiniowanej w klasie B (tzn. int
b), ale np. poprzez jawne rzutowanie (w dół !) można się tam dostać
• co jeśli przez wartość?
A a = b;
– to też dopuszczalne, ale następuje nieodwracalna strata części obiektu klasy B
(tu zadziała konstruktor kopiujący z klasy A, który nic nie wie o dodatkowej części
z klasy B)
polimorfizm – czego oczekujemy?
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 programie
B 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, bo
przecież pokazywane są
obiekty klas pochodnych
chcielibyśmy, żeby wskaźnik
(referencja) inteligentnie
reagowały na typ obiektu
na który pokazują, wołając
jego funkcję…
polimorfizm - rozwiązanie
class A { public:
w klasie bazowej
virtual void getMe() { cout << "Jestem A/n"; }
(tutaj klasie A)
};
musimy w deklaracji
class B : public A { public:
funkcji dodać
void getMe() { cout << "Jestem B/n"; }
virtual
};
class C : public B { public:
void getMe() { cout << "Jestem C/n"; }
funkcja getMe() jest wirtualna w każdej
};
klasie pochodnej, można (ale nie trzeba
// …gdzieś w programie
bo jest to mylące) dopisać "virtual"
również w klasie B i C…
B b;
C c;
• ściśle rzecz biorąc polimorficzne jest
A *ptrA = &b;
wywołanie funkcji, a nie funkcja
A &refA = c;
• klasa, w której jest zdefiniowana
A a = b;
lub odziedziczona funkcja wirtualna,
ptrA->getMe(); // "Jestem B"
nazywa się klasą polimorficzną
refA.getMe(); // "Jestem C"
a.getMe(); // "Jestem A" – nieodwracalne "przycięcie" do A
funkcje wirtualne – kilka szczegółów
•
•
•
•
•
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 referencji
class A { public:
virtual void f() { cout << "Jestem A" << endl; }
};
class B : public A { private:
void f() { cout << "Jestem B" << endl; }
dostęp rozstrzygany
};
na poziomie wiedzy
int main()
wyniesionej z klasy
{
bazowej A, bo pokazujemy
A *ptrA = new B;
wskaźnikiem klasy bazowej
ptrA->f(); // "Jestem B"
B &refB = dynamic_cast<B&>(*ptrA);
refB.f(); // błąd - virtual void B::f() is private
}
konstruktor, destruktor – wirtualny
• 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
dziedziczenie – klasa abstrakcyjna
•
•
•
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ła
virtual 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 kontra zawieranie – przykład uniwersytecki
TOsoba
TOsoba
nazwisko
adres
data urodzenia
TStudent
status
wydział
kursy
nazwisko
adres
data urodzenia
TNauczyciel
funkcja
kursy
TOsoba
nazwisko
adres
data urodzenia
TStudent
TNauczyciel
funkcja
kursy prowadzone
status
wydział
kursy
TDoktorant
TDoktorant
nie może się zapisywać
na kursy podstawowe
TDoktorantNaucz
wielokrotne dziedziczenie spowoduje
zapewne pojawienie się konfliktu
niejednoznaczności, np. funkcja print() odziedziczona podwójnie…
doktorant z obowiązkiem
prowadzenia zajęć
dydaktycznych
dziedziczenie wielokrotne – alternatywa 1
1
•
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
TDoktorantNaucz
1
TDoktorant
class TDoktorantNaucz {
private:
TNauczyciel nauczycielProxy;
TDoktorant doktorantProxy;
// sporo kodu do napisania
};
dziedziczenie i zawieranie – alternatywa 2
•
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
TDoktorantNaucz
1
TNauczyciel
class TDoktorantNaucz
: public TDoktorant {
private:
TNauczyciel nauczycielProxy;
// trochę kodu do napisania
};
dziedziczenie wielokrotne – alternatywa 3
•
TOsoba
nazwisko
adres
data urodzenia
TStudent
wirtualna
klasa
bazowa
TNauczyciel
•
funkcja
kursy prowadzone
status
wydział
kursy
TDoktorant
•
TDoktorantNaucz
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.)
dziedziczenie wielokrotne - koszty
TOsoba
nazwisko
adres
data urodzenia
TStudent
wirtualna
klasa
bazowa
TNauczyciel
funkcja
kursy prowadzone
status
wydział
kursy
TOsoba
TNauczyciel
TDoktorant
TStudent
TDoktorantNaucz
TDoktorant
istnienie konstruktora w klasie (np. domyślnego)
zależy wyłącznie od projektu interfejsu, nie należy
dodawać funkcji składowych tylko po to,
aby uniknąć błędów kompilacji
TDoktorantNaucz
dziedziczenie – statyczna relacja
• dziedziczenie jest relacją
statyczną - trudno ją zmienić
• kiedy relacje między klasami
zmieniają się, przydatność
dziedziczenia jest ograniczona
• relacje w hierarchii dziedziczenia
są określone i zakodowane na stałe
•
•
•
TOsoba
{ virtual }
TAsystentBadan
•
TStudent
TNauczyciel
•
TDoktorant
TDoktorantNaucz
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
klasa mieszana – mix-in-class
• 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
MozeBycStudentem
TOsoba
•
•
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
};
•
TStudent
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
w klasie TStudent trzeba zaimplementować
wszystkie wirtualne metody dziedziczone po
MozeBycStudentem, w której można też
zdefiniować jakąś domyślną implementację
klasy mieszane - dyskusja
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
MozeWykBadania
MozeNauczac
TOsoba
•
•
•
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
MozeBycStudentem
TStudent
TAsystentBadan
TNauczyciel
TDoktorant
TDoktorantBadacz
TDoktorantNaucz
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
dynamiczna zmiana sytuacji – czyli co po studiach?
Wiemy już, że należy unikać
niepotrzebnego powielania danych
(wirtualne klasy bazowe) - bo powoduje
to utratę zasobów i problemy z
zarządzaniem tymi danymi
Warto też do minimum ograniczyć ilość
kopiowanych danych kiedy
przekształcamy lub kopiujemy obiekt
•
•
•
•
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?
Widzimy brak elastyczności dziedziczenia
wielokrotnego w dynamicznie zmieniających się
sytuacjach - często ma miejsce w bazach danych
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
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 zmienia
dynamiczna zmiana sytuacji – role
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
uniwersytetu
Każ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
rola
Obiekt TOsoba przechowuje listę
wszystkich możliwych ról pełnionych
przez daną osobę – nie powiela się
danych osobowych
Role 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)
pełni rolę
0 .. n
TOsoba
TCzlonekUniwersytetu
do kogo
TStudent
TNauczyciel
TBadacz
TDoktorant
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)
role i ich konsekwencje
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
Role są przenośne
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)
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…
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
MozeWykBadania
MozeNauczac
TAsystentBadan
TOsoba
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
MozeBycStudentem
TStudent
TNauczyciel
TDoktorantBadacz
TStudentBadacz
Obiekty pełniące rolę to
lepsze rozwiązanie w
dynamicznie zmieniających
się sytuacjach
TDoktorant
TNauczycielDoksztalc
TDoktorantNaucz
Można utworzyć obiekt
TOsoba bez żadnych ról,
które przypisze się później
klasy mieszane a role – przypadki zastosowań
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
Klasy mieszane a role
role – lepsze gdy istnieje zbyt wiele
możliwych kombinacji ról i kombinacje
te mogą się zmieniać dynamicznie
klasy mieszane – gdy kombinacja ról jest
mała i jedna osoba może pełnić tylko
jedną rolę danego rodzaju
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
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

Podobne dokumenty