własne biblioteki, struktura STL, kolekcje i iteratory

Download Report

Transcript własne biblioteki, struktura STL, kolekcje i iteratory

C++
wykład 11 (30.05.2013)
Własne biblioteki,
standardowa biblioteka
szablonów STL
Kompilacja i łączenie





Plik jako jednostka kompilacji.
Preprocesing – obsługa makr i dyrektyw
włączających – dostarcza kompilatorowi jednostkę
translacji.
Kompilator analizuje jednostkę translacji w izolacji
od reszty programu.
Fizyczna struktura programu (podział na pliki)
powinna wynikać z logicznej struktury programu.
Rola linkera przy budowaniu programu albo
biblioteki.
Moduły




Każdy większy program składa się z pewnej
liczby oddzielnych części – modułów.
Moduł to kompletny fragment programu
(moduł obliczeniowy, moduł we/wy, moduł
prezentacji, itp).
Podział kodu na moduły porządkuje logikę
programu.
Należy minimalizować zależności między
modułami.
Biblioteki


Moduły, z których może korzystać wiele programów
umieszcza się w oddzielnych skompilowanych
plikach, zwanych bibliotekami.
Typy bibliotek w C++:



biblioteka statyczna jest dołączana do programu
wynikowego na etapie linkowania;
biblioteka współdzielona jest dołączana do programu w
trakcie ładowania programu do pamięci;
biblioteka dynamiczna jest dołączana do uruchomionego
procesu w trakcie działania programu.
Biblioteki



Biblioteka to zbiór klas, funkcji i zmiennych, z
których mogą korzystać różne programy.
Biblioteka ma postać binarną – jej
poszczególne fragmenty są skompilowane
(biblioteka jest kolekcją plików obiektowych).
Korzystanie z bibliotek ułatwia
programowanie (korzystamy z gotowych i
sprawdzonych fragmentów kodu) i
przyspiesza proces rekompilacji.
Biblioteki



Wynikiem samej kompilacji pliku źródłowego
(plik.c albo plik.cpp) jest plik plik.o
pod Linuxem albo plik.obj pod
Windowsem.
Biblioteki statyczne mają nazwy
libmodul.a pod Linuxem albo modul.lib
pod Windowsem.
Biblioteki dynamiczne mają nazwy
libmodul.so pod Linuxem (tak jak
biblioteki współdzielone) albo modul.dll
pod Windowsem.
Biblioteka statyczna




Używając biblioteki statycznej przekazujemy jej
archiwum linkerowi w czasie kompilacji. Linker
wyszukuje w nim tylko tych plików obiektowych,
które są niezbędne do działania naszego programu i
kopiuje je bezpośrednio do programu.
Program wynikowy korzystający z biblioteki
statycznej jest obszerniejszy ale szybciej się ładuje
do pamięci.
Program wynikowy zlinkowany z biblioteką
statyczną jest niezależny od plików zewnętrznych.
Uaktualnienie biblioteki wymaga rekompilacji
programu.
Biblioteka statyczna
lib.cpp
g++
lib.o
prog.cpp
ar
g++
prog.o
static library
linker
g++
a.out
loader
ssh
memory
Biblioteka współdzielona




Programy korzystające biblioteki współdzielonej nie
zawierają bezpośrednio kodu z tej biblioteki a tylko
referencję do niej.
Program wynikowy korzystający z biblioteki
współdzielonej jest chudszy ale wolniej ładuje się do
pamięci (biblioteki współdzielone są odszukiwane i
ładowane do pamięci razem z programem).
Program wynikowy skompilowany z biblioteką
współdzieloną jest zależny od plików zewnętrznych.
Zmodyfikowanie biblioteki współdzielonej spowoduje
zmianę w działaniu programu ale bez jego ponownej
kompilacji.
Biblioteka współdzielona
prog.cpp
g++
prog.o
lib.cpp
g++
lib.o
linker
g++
g++
a.out
shared library
loader
ssh
memory
Biblioteka dynamiczna




Programy korzystające bibliotek dynamicznych nie
zawierają bezpośrednio kodu z tej biblioteki ale
muszą korzystać ze specjalnych metod włączania
takich bibliotek w trakcie działania programu (plik
nagłówkowy <dlfcn.h>).
Program wynikowy korzystający z biblioteki
dynamicznej jest chudszy i szybciej ładuje się do
pamięci, ale działa wolniej (biblioteki dynamiczne
można załadować w dowolnym momencie w trakcie
działania programu).
Program wynikowy skompilowany z biblioteką
dynamiczną jest zależny od plików zewnętrznych.
Zmodyfikowanie biblioteki dynamicznej spowoduje
zmianę w działaniu programu ale bez jego ponownej
kompilacji.
Biblioteka dynamiczna
prog.cpp
g++
prog.o
lib.cpp
linker
g++
lib.o
g++
a.out
g++
loader
dynamic library
a.out
ssh
memory
Tworzenie bibliotek
(pod Linuxem)
Tworzenie programu bez dołączanych bibliotek.





Załóżmy, że mamy pliki src1.cpp, src2.cpp i src3.cpp, które
stanowią moduł obliczeniowy oraz plik prog.cpp, który będzie
korzystał z funkcji i klas zdefiniowanych w module obliczeniowym.
Aby skompilować cały program razem z modułem obliczeniowym
należy wydać polecenie:
$ g++ -Wall -ansi -pedantic src1.cpp src2.cpp
src3.cpp prog.cpp -o calculation
Aby skompilować cały program razem z modułem obliczeniowym i
statycznie zlinkować z innymi bibliotekami (rozmiar programu
wynikowego będzie znacznie większy) należy wydać polecenie:
$ g++ -static …
Aby uruchomić skompilowany program należy wydać polecenie:
$ ./calculation
Aby sprawdzić z jakimi bibliotekami jest linkowany program i jakie
symbole są w nim użyte należy wydać polecenie:
$ ldd calculation
$ nm calculation
Tworzenie bibliotek
(pod Linuxem)
Program korzystający z biblioteki statycznej.





Najpierw kompilujemy pliki źródłowe do plików obiektowych:
$ g++ -c -Wall -ansi -pedantic src1.cpp src2.cpp
src3.cpp
Następnie łączymy pliki obiektowe do jednego pliku bibliotecznego
libsrc.a:
$ ar crs libsrc.a src1.o src2.o src3.o
Na koniec należy skompilować plik z programem i zlinkować go z
biblioteką:
$ g++ -c -Wall -ansi -pedantic prog.cpp
$ g++ -o calculation prog.o –L. –lsrc
Teraz można uruchomić skompilowany program:
$ ./calculation
Wyjaśnienie:


opcja -Lścieżka określa ścieżkę do biblioteki,
opcja -lbiblioteka określa nazwę biblioteki.
Tworzenie bibliotek
(pod Linuxem)
Program korzystający z biblioteki współdzielonej.

Najpierw kompilujemy pliki źródłowe z opcją -fpic do plików
obiektowych:
$ g++ -fpic –c -Wall -ansi -pedantic src1.cpp src2.cpp
src3.cpp




Następnie łączymy pliki obiektowe do jednego pliku bibliotecznego
libsrc.so:
$ g++ –fpic -shared -o libsrc.so src1.o src2.o src3.o
Na koniec należy skompilować plik z programem i wlinkować do niego
informacje o bibliotece:
$ g++ -Wall -ansi -pedantic prog.cpp
$ g++ -o calculation prog.o –L. –lsrc
Przed uruchomieniem programu trzeba zapisać w zmiennej środowiskowej
LD_LIBRARY_PATH ścieżkę do biblioteki:
$ export LD_LIBRARY_PATH="LD_LIBRARY_PATH:ścieżka"
Teraz można uruchomić skompilowany program:
$ ./calculation
Tworzenie bibliotek
(pod Linuxem)
Program korzystający z biblioteki dynamicznej.




Bibliotekę dynamiczną przygotowujemy tak samo jak bibliotekę
współdzieloną libsrc.so:
$ g++ -fpic –c -Wall -ansi -pedantic src1.cpp
src2.cpp src3.cpp
$ g++ –fpic -shared -o libsrc.so src1.o src2.o
src3.o
Na koniec należy skompilować plik z programem i dołączyć do
niego dynamicznego linkera opcją -ldl:
$ g++ -Wall -ansi -pedantic prog.cpp
$ g++ -o calculation prog.o –ldl
Teraz można uruchomić skompilowany program:
$ ./calculation
Wyjaśnienie:


aby skorzystać z dynamicznego linkera należy do programu włączyć
plik nagłówkowy <dlfcn.h>;
aby załadować dynamiczną bibliotekę trzeba skorzystać z funkcji
dlopen, dlsym, dlerror i dlclose.
Nazwy zewnętrzne



Nazwa jest łączona zewnętrznie jeśli można
jej używać w jednostkach translacji innej niż
ta, w której ją zdefiniowano.
Nazwę zewnętrzną deklaruje się za pomocą
słowa extern.
Funkcja wbudowana musi być zdefiniowana
w każdej jednostce translacji za pomocą
identycznej definicji; ta sama reuła odnosi się
do funkcji i klas szablonowych.
Nowości z C++11 – szablony
zewnętrzne i aliasy szablonów



C++ musi stworzyć instancję szablonu zawsze kiedy
napotka w pełni określony szablon w jednostce
translacyjnej. W starszym C++ nie jest bowiem możliwe
wstrzymanie tworzenia instancji szablonu w takiej
sytuacji.
C++11 wprowadza ideę szablonów zewnętrznych w celu
zablokowania tworzenia instancji w jednostce
translacyjnej. Przykład:
extern template class
std::vector<MojaKlasa>;
W C++11 można używać aliasów dla szablonów.
Przykład:
template <typename T>
using Vec = std::vector<T,MyAlloc<T>>;
STL






STL (ang. Standard Template Library) to
standardowa biblioteka wzorców w C++.
Wszystkie składniki STL są wzorcami.
STL jest rdzeniem biblioteki standardowej C++.
Wszystkie identyfikatory w bibliotece standardowej
C++ zdefiniowane są w przestrzeni nazw std.
Zaleta STL – mnóstwo gotowych szablonów klas
pozwalających programować na wyższym poziomie
abstrakcji.
Wada STL – brak oczywistości (koniecznie czytaj
dokumentację)
STL

Biblioteka STL składa się z kilku modułów:







strumienie we/wy,
łańcuchy i wyrażenia regularne,
kontenery i iteratory,
algorytmy i obiekty funkcyjne,
wielowątkowość i przetwarzanie współbieżne,
internacjonalizacja,
klasy narzędziowe.
Pary







Szablon struktury pair<> (zdefiniowany w <utility>) umożliwia
potraktowanie dwóch wartości jako pojedynczego elementu.
Para posiada dwa pola: first i second.
Para posiada konstruktor dwuargumentowy oraz domyślny i kopiujący.
Pary można porównywać (operatory == i <).
Istnieje szablon funkcji make_pair() do tworzenia pary (typy danych
są rozpoznawane przez kompilator po typach argumentów).
Przykłady:
void f (std::pair<int, const char *>);
void g (std::pair<const int, std::string>);
…
std::pair<int, const char *> p(44,”witaj”);
f(p); // wywołuje domyślny konstruktor kopiujący
g(p); // wywołuje konstruktor wzorcowy
g(std::make_pair(44,”witaj”)); // przekazuje dwie
// wartości jako parę z wykorzystaniem konwersji
// typów
Pary są wykorzystywane w kontenerach map i multimap.
Tuple








W C++11 zdefiniowano tuple do przechowywania wielu wartości a nie
tylko dwóch (szablon tuple<> jest analogią do szblony pary pair<>).
Tupla posiada wiele ponumerowanych pól, do których dostęp mamy za
pomocą metody get<i>.
Tupla posiada konstruktor wieloargumentowy oraz domyślny i
kopiujący.
Tuple można porównywać za pomocą operatorów porównań
(porównywanie leksykograficzne).
Istnieje szablon funkcji make_tuple() do tworzenia tupli (typy danych
są rozpoznawane przez kompilator po typach argumentów).
Istnieje szablon funkcji tie() do tworzenia tupli z referencjami (jako
argumenty podaje się zmienne).
Szablon tuple_size<tupletype>::value służy do podania liczby
elementów w tupli.
Szablon tuple_element<idx,tupletype>::type służy do
podania typu elementu o indeksie idx w tupli.
Sprytne wskaźniki




Sprytne wskaźniki są zdefiniowane w pliku
nagłówkowym <memory>.
Zastąpienie szablonu auto_ptr<>.
Szablon klasy shared_pointer<> – wiele takich
sprytnych wskaźników może przechowywać
wskaźnik do tego samego obiektu, tak że obiekt ten
oraz związane z nim zasoby zostaną zwolnione
dopiero po likwidacji ostatniego sprytnego
wskaźnika.
Szablon klasy unique_pointer<> – tylko jeden
sprytny wskaźnik może przechowywać wskaźnik do
tego danego obiektu.
Ograniczenia liczbowe



Typy numeryczne posiadają ograniczenia zależne od platformy i
są zdefiniowane w szablonie numeric_limits<>
(zdefiniowany w <limits>, stałe preprocesora są nadal
dostępne w <climits> i <cfloat>).
Wybrane składowe statyczne szablonu numeric_limits<>:
is_signed, is_integer, is_exact, is_bounded,
is_modulo, has_infinity, has_quiet_NaN,
min(), max(), epsilon().
Przykłady:
numeric_limits<char>::is_signed;
numeric_limits<short>::is_modulo;
numeric_limits<long>::max();
numeric_limits<float>::min();
numeric_limits<double>::epsilon();
Minimum i maksimum


Obliczanie wartości minimalnej oraz maksymalnej:
template <class T>
inline const T& min (const T &a, const
{ return b<a ? b : a; }
template <class T>
inline const T& max (const T &a, const
{ return a<b ? b : a; }
Istnieją też wersje tych szablonów z komparatorami
funkcyjny):
template <class T, class C>
inline const T& min (const T &a, const
{ return comp(b,a) ? b : a; }
template <class T>
inline const T& max (const T &a, const
{ return comp(a,b) ? b : a; }
T &b)
T &b)
(funkcja lub obiekt
T &b, C comp)
T &b, C comp)
Minimum i maksimum


Przykład 1:
bool int_ptr_less (int *p, int *q) {
return *p<*q; }
…
int x = 33, y = 44;
int *px = &x, *py = &y;
int *pmax = std::max(px,py,int_ptr_less);
Przykład 2:
int i;
long l;
…
l = max(i,l); // BŁĄD
// niezgodne typy argumentów
l = std::max<long>(i,l); // OK
Zamiana wartości


Zamiana dwóch wartości:
template <class T>
inline void swap (T &a, T &b)
{ T tmp(a); a = b; b = tmp; }
Przykład:
int x = 33, y = 44;
…
std::swap(x,y);
Operatory porównywania



Cztery funkcje szablonowe (zdefiniowane w <utility>) na podstawie
operatorów == i < definiują operatory porównań !=, <=, >= i >.
Funkcje te są umieszczone w przestrzeni nazw std::rel_ops.
Przykład:
class X {
…
public:
bool operator== (const X &x) const throw();
bool operator< (const X &x) const throw();
…
};
…
void foo () {
using namespace std::rel_ops;
X x1, x2;
…
if (x1!=x2) { … }
…
}
Kontenery


Kontenery służą do przechowywania i
zarządzania kolekcjami danych.
Rodzaje kontenerów:

Kontenery sekwencyjne, gdzie każdy element ma
określoną pozycję. Na przykład: vector, deque,
list.

Kontenery uporządkowane (w tym asocjacyjne),
gdzie pozycja elementu zależy od jego wartości.
Na przykład: set, multiset, map, multimap.
Elementy kontenerów

Elementy kontenerów muszą spełniać wymagania
podstawowe:




element musi być kopiowalny (konstruktor kopiujący),
element musi być przypisywalny (przypisanie kopiujące),
element musi być zniszczalny (publiczny destruktor).
W pewnych sytuacjach elementy kontenerów muszą
spełniać wymagania dodatkowe:



konstruktor domyślny (utworzenie niepustego kontenera),
operator porównywania == (wyszukiwanie),
operator porównywania < (kryterium sortowania).
Semantyka wartości
a semantyka referencji


Kontenery STL realizują semantykę wartości:
tworzą wewnętrzne kopie swoich elementów
oraz zwracają kopie tych elementów.
Semantykę referencji można
zaimplementować samodzielnie za pomocą
inteligentnych wskaźników – wskaźniki te
mają umożliwiać zliczanie referencji dla
obiektów, do których odnoszą się wskaźniki.
Kontenery sekwencyjne
– wektory




Wektor vector<> (zdefiniowany w <vector>) przechowuje
swoje elementy w tablicy dynamicznej.
Uzyskujemy szybki dostęp do każdego elementu za pomocą
indeksowania.
Dołączanie i usuwanie elementów na końcu wektora jest bardzo
szybkie, ale wstawienie lub usunięcie elementu ze środka
zabiera więcej czasu.
Przykład:
vector<int> coll;
…
for (int i=1; i<=6; ++i)
coll.push_back(i);
…
for (int i=0; i<coll.size(); ++i)
cout << coll[i] << ’ ’;
cout << endl;
Kontenery sekwencyjne
– kolejki o dwóch końcach




Kolejka o dwóch końcach deque<> (zdefiniowana w <deque>)
przechowuje swoje elementy w tablicy dynamicznej, która może rosnąć
w dwie strony.
Uzyskujemy szybki dostęp do każdego elementu za pomocą
indeksowania.
Dołączanie i usuwanie elementów na końcu i na początku kolejki jest
bardzo szybkie, ale wstawienie lub usunięcie elementu ze środka
zabiera więcej czasu.
Przykład:
deque<float> coll;
…
for (int i=1; i<=6; ++i)
coll.push_front(i*1.234);
…
for (int i=0; i<coll.size(); ++i)
cout << coll[i] << ’ ’;
cout << endl;
Kontenery sekwencyjne
– listy




Lista list<> (zdefiniowana w <list>) przechowuje swoje
elementy w liście dwukierunkowej.
W listach nie ma swobodnego dostępu do elementów kolekcji.
Dołączanie i usuwanie elementów na końcu i na początku listy
jest bardzo szybkie, ale dostanie się do elementu ze środka
zabiera dużo czasu.
Przykład:
list<char> coll;
…
for (char c=’a’; c<=’z’; ++c)
coll.push_back(c);
…
while (!coll.empty()) {
cout << coll.front() << ’ ’;
coll.pop_front(); }
cout << endl;
Kontenery sekwencyjne
– łańcuchy i tablice


Obiektów klas łańcuchowych, czyli
basic_string<>, string i wstring,
można używać jak kontenerów
sekwencyjnych. Są one podobne w
zachowaniu do wektorów.
Innym rodzajem kontenera może być tablica.
Nie jest to klasa i nie ma żadnych metod ale
konstrukcja STL umożliwia uruchamianie na
tablicach różnych algorytmów (tak jak na
kontenerach).
Kontenery uporządkowane






Kontenery uporządkowane wykonują automatycznie sortowanie
swoich elementów.
Asocjacyjne kontenery uporządkowane przechowują pary kluczwartość (odpowiednio first i second) i sortowanie następuje
po kluczach.
Domyślnie elementy lub klucze są porządkowane przy pomocy
operatora <.
Kontenery uporządkowane są implementowane w postaci
zrównoważonych drzew BST (drzewa czerwono-czarne).
Wszystkie kontenery uporządkowane posiadają domyślny
parametr wzorca służący sortowaniu (domyślnym jest operator
<).
Rodzaje kontenerów: zbiory set<>, wielozbiory multiset<>,
mapy map<> i multimapy multimap<>.
Adaptatory kontenerów


Adaptatory kontenerów to kontenery
wykorzystujące ogólną strukturę innych
kontenerów do realizacji pewnych
specyficznych potrzeb.
Adaptatorami kontenerów są stosy
stack<>, kolejki queue<> i kolejki
priorytetowe priority_queue<>.
Iteratory




Iterator to specjalny obiekt, który potrafi iterować po elementach
kolekcji.
Iterator ma zaimplementowaną semantykę wskaźnika – posiada
operator wyłuskania elementu *, operatory przechodzenia do
elementówsąsiednich ++ i -- oraz operatory porównywania
pozycji == i !=.
Wszystkie kontenery udostępniają funkcje tworzące iteratory do
nawigowania po ich elementach – funkcja begin() zwraca
iterator wskazujący na pozycję z pierwszym elementem w
kolekcji a funkcja end() zwraca iterator wskazujący pozycję za
ostatnim elementem.
Każdy kontener definiuje dwa typy iteratorów –
kontener::iterator przeznaczony do iterowania po
elementach z możliwością odczytu i zapisu oraz
kontener::const_iterator przeznaczony do iterowania po
elementach tylko z możliwością odczytu.
Iteratory


Przykład 1:
list<char> coll;
…
list<char>::const_iterator pos;
for (pos=coll.begin(); pos!=coll.end(); ++pos)
cout << *pos << ’ ’;
cout << endl;
Przykład 2:
list<char> coll;
…
list<char>::iterator pos;
for (pos=coll.begin(); pos!=coll.end(); ++pos)
*pos = toupper(*pos);