C++/Wersja do druku
Z Wikibooks, biblioteki wolnych podręczników.
Aktualna, edytowalna wersja tego podręcznika jest dostępna w Wikibooks, bibliotece wolnych podręczników pod adresem
http://pl.wikibooks.org/wiki/C%2B%2B
Całość tekstu jest objęta licencją GNU Free Documentation License.
- Język C
Podstawy języka C
[edytuj] Spis treści
[edytuj] Wstęp
- O języku C++

Opis i historia - O podręczniku

Autorzy, źródła - Jak czytać ten podręcznik

Ważne informacje o organizacji podręcznika
[edytuj] Część 1: Podstawy języka
- Przestrzenie nazw

Wprowadzenie pojęcia przestrzeni nazw, przestrzeń nazw std, przykładowy program demonstrujący użycie cin, cout i string - Zmienne

Nowe sposoby deklaracji, kontrola typów w C++, nowe sposoby rzutowania - Referencje

Porównanie ze wskaźnikami, zastosowanie do przekazywania argumentów do funkcji - Funkcje inline

Krótki opis funkcji inline - Przeciążanie funkcji

Po co i jak można przeciążać funkcje i jak tego nie da się robić - Zarządzanie pamięcią

Jak w C++ dynamicznie zarządzać pamięcią z użyciem operatorów new i delete
[edytuj] Część 2: Podstawy programowania obiektowego
- Czym jest obiekt

Wprowadzenie pojęcia klasy i obiektu, autorekursja, kontrola dostępu - Konstruktor i destruktor

Konstruktor, konstruktor kopiujący, destruktor - Dziedziczenie

Dziedziczenie prywatne, publiczne i chronione - Składniki statyczne

Atrybuty i metody statyczne
[edytuj] Część 3: Zaawansowane programowanie obiektowe
- Funkcje wirtualne

Funkcje wirtualne i abstrakcyjne, wyjaśnienie polimorfizmu i dynamic_cast - Programowanie orientowane obiektowo
Wyjaśnienie idei programowanie orientowanego obiektowo - Obiekty stałe
Jak tworzyć, możliwe niebezpieczeństwa, słowo kluczowe mutable - Przeciążanie operatorów

Wprowadzenie przykładu klasy z kompletnym przeciążeniem operatorów - Konwersje obiektów
Przeciążenie operatorów konwersji, konstruktor jako sposób konwersji, konstruktory typu explicit - Klasy i typy zagnieżdżone
Tworzenie klas i typów zagnieżdżonych - Dziedziczenie wielokrotne
Dziedziczenie wielokrotne, dziedziczenie wirtualne oraz problemy z nimi związane
[edytuj] Część 4: Zaawansowane konstrukcje językowe
- Obsługa wyjątków

Obsługa wyjątków w C++, funkcje unexpected() i terminate() - Szablony funkcji

Szablony funkcji - Szablony klas

Szablony klas, programowanie uogólnione - Wskaźniki do elementów składowych
Wykorzystnie wskaźników do elementów składowych klas
[edytuj] Dodatek A: Biblioteka STL
- Filozofia STL

Jak skonstruowana jest biblioteka STL - String

Korzystanie z łańcuchów znaków - Vector

Korzystanie z wektorów - List & Slist

Listy jedno- i dwukierunkowe - Set
Korzystanie ze zbiorów - Map
Korzystanie z odwzorowań - Stack
Korzystanie ze stosu - Iteratory
Korzystanie z iteratorów - Algorytmy w STL

Jak działają algorytmy w STL - Inne klasy STL
Krótkie omówienie pozostałych klas
[edytuj] Dodatek B
- Przykłady
Przykłady kodu z komentarzem - Ćwiczenia

Zadania kontrolne - Różnice między C a C++

Najważniejsze różnice między C a C++
[edytuj] Pozostałe
- Indeks

Indeks najważniejszych terminów - Zasoby

Książki, linki do innych kursów i dokumentacji - Dla autorów
Wskazówki dla osób pragnących pomóc w rozwoju podręcznika - Wersja do druku
Całość książki na jednej stronie, gotowa do druku - Licencja

Pełny tekst GNU Free Documentation license
Wstęp
[edytuj] O języku C++
C++ jest nowoczesnym językiem wieloparadygmatowym, mającym korzenie w popularnym języku C. Na jego rozwój oddziaływało wiele języków, z których należy przede wszystkim wspomnieć Simulę i Adę. Programiści cenią go za połączenie bezkompromisowej wydajności programów wynikowych z zaawansowanymi mechanizmami umożliwiającymi programowanie na wysokim poziomie abstrakcji i kontrolę zależności między komponentami w wielkich projektach. C++ stara się zachować kompatybilność z językiem C, ale jednocześnie udostępnia szeroki wachlarz nowych mechanizmów, m.in: programowanie obiektowe z wielokrotnym dziedziczeniem i kontrolą dostępu, programowanie generyczne dzięki wykorzystaniu szablonów, przeciążanie funkcji i operatorów, automatyczne konwersje, obsługę sytuacji wyjątkowych i zarządzanie przestrzeniami nazw. Od ostatnio powstałych konkurentów, takich jak Java i C#, wyróżnia się traktowaniem typów zdefiniowanych przez użytkownika na równi z typami wbudowanymi. Niestety, to bogactwo możliwości prowadzi do znacznych komplikacji przy implementacji kompilatorów.
Nazwa C++ została wymyślona przez Marka Kosasonia i wywodzi się z faktu, że w C wyrażenie zmienna++ oznacza inkrementację czyli zwiększenie o jeden.
[edytuj] O podręczniku
Podręcznik ten jest tworzonym na Wikibooks kursem języka C++. Początkowo pomyślany jako samodzielna jednostka, po dyskusji utracił część treści na rzecz podręcznika C i odtąd ma być jego w pewnym sensie przedłużeniem, pomijając podstawy, którymi ma zająć się drugi z podręczników.
[edytuj] Tworzenie podręcznika
Niniejszy kurs cały czas jest w fazie rozwojowej. Jako otwarty podręcznik może być edytowany przez każdego i każdy wkład będzie mile widziany. Przed wprowadzaniem poważniejszych zmian warto jednak przeczytać rozdział Dla autorów.
[edytuj] Autorzy
Znaczący wkład w powstanie podręcznika mają:
[edytuj] Bibliografia
[edytuj] Jak czytać ten podręcznik
Język C++ powstał jako wzbogacenie języka C o cechy obiektowe. Ten podręcznik został podobnie pomyślany nie jako osobna całość, ale logiczne przedłużenie podręcznika C. Co prawda oba kursy są niezależne, ale pisząc ten podręcznik staraliśmy się nie powtarzać informacji, które były już zawarte w poprzedniej książce. Nie znajdzie tu się więc objaśnienie, czym jest kompilator, jak działa i który kompilator wybrać, czym jest funkcja i zmienna oraz podobne podstawowe informacje. Czytelnikowi zostawiamy swobodę w tym, jak podejdzie do tego podręcznika, ale zalecamy jeden z dwóch sposobów.
Pierwszy z nich to dokładne przestudiowanie kursu C a następnie zaczęcie od pierwszej części tego podręcznika, gdzie pokazujemy, co C++ zmienia w składni i co oferuje nowego. Idąc dalej, czytelnik dojdzie do programowania obiektowego. Taki sposób jest co prawda bardziej pracochłonny i wymaga większego wysiłku, ale systematyczne podejście do nauki języka powinno zaowocować dobrym uporządkowaniem informacji i większym zrozumieniem filozofii obu języków.
Możliwa jest też droga dla osób bardziej niecierpliwych. Należy wtedy szybko przejrzeć kilka początkowych rozdziałów podręcznika C, ominąć część pierwszą tego kursu i od razu zacząć od programowania obiektowego, w razie kłopotów zaglądając do Indeksu. Może to być dobra metoda dla osób zaznajomionych już z programowaniem, choć w C++ czyha na nieuważnych wiele pułapek, które trzeba nauczyć się omijać.
Część 1
Podstawy języka
[edytuj] Przestrzenie nazw
Jeśli użyjemy dowolnej wyszukiwarki internetowej to powinniśmy bez problemu znaleźć prosty, szablonowy kod napisany w C++, który wyświetla napis „Hello World!”, w tłumaczeniu na polski „Witaj Świecie!”. Spójrzmy na niego:
#include <iostream> using namespace std; int main() { cout << "Hello World!" << endl; return 0; }
Zaleca się używanie znaku nowej linii (\n) zamiast funkcji "endl". Chyba że jest to uzasadnione: endl powoduje opróżnienie bufora, ale na przykład przy wielokrotnym zapisie na dysk może to obciążyć jego pracę.
Osoby, które już znają C, na pewno się domyślą, co mniej więcej się dzieje w tym kodzie. Nasuwa się tylko jedno pytanie -- po co jest ta wstawka using namespace std ? O tym za chwilę. Najpierw, pokrótce omówimy, co ten program właściwie robi.
Za pomocą #include<iostream> dołączyliśmy odpowiedni, tak zwany plik nagłówkowy, który umożliwia nam między innymi wypisywanie pewnych informacji na ekran (ściślej na standardowe wyjście).
Dzięki int main( ) {...} mogliśmy otworzyć funkcję główną, która jest zawsze uruchomiana podczas startu naszego programu.
Operacja cout << umożliwia nam wypisywanie pewnych informacji. W naszym przypadku wypisaliśmy napis „Hello World!”, a następnie „przedłużyliśmy” to polecenie za pomocą znaku << i wstawiliśmy nową linię, która jest zdefiniowana przez endl.
Za pomocą return 0 informujemy system, że program działał prawidłowo i nie wystąpiły żadne problemy.
Powróćmy teraz do tematu zadanego na początku, czyli po co wstawiliśmy linię z kodem using namespace std. W tym celu musimy omówić, czym są przestrzenie nazw.
[edytuj] Przestrzenie nazw
Spójrzmy jeszcze raz na nasz pierwszy program, tylko w trochę zmienionym wydaniu:
#include <iostream> int main( ) { std::cout << "Hello World!" << std::endl; return 0; }
Łatwo zauważyć, że usunęliśmy linię using namespace std, kosztem tego, że cout i endl musieliśmy poprzedzić nazwą std i operatorem ::. Właśnie std jest nazwą pewnej przestrzeni nazw. W przestrzeni nazw std znajdziemy mnóstwo, a wręcz cały arsenał różnych narzędzi, począwszy od pewnych bardzo przydatnych funkcji np. sortowania, wyszukiwania, a kończywszy na tak zwanych pojemnikach, które pozwalają nam w łatwy sposób przechowywać pewne wartości. Oczywiście, aby mieć dostęp do tych narzędzi musimy dołączyć odpowiedni plik nagłówkowy, używając do tego dyrektywy #include.
Pisząc std::cout << informujemy kompilator, że chcemy wejść do przestrzeni nazw std i skorzystać ze znajdującej się tam operacji cout <<.
Poprzedni przykład pokazał nam, że nie musimy za każdym razem odwoływać się do przestrzeni nazw, kiedy chcemy wywołać pewną, należącą do niej funkcję. Używając using namespace przestrzenNazw, powiadamiamy kompilator, aby wszystko co się znajduje w przestrzeni nazw przestrzenNazw przeniósł do „naszego pokoju”. Dzięki temu nie musimy się martwić, w którym „pokoju” (czyli przestrzeni nazw) się co znajduje, ponieważ wszystko mamy pod ręką.
Oczywiście nie musimy naraz „przenosić” wszystkiego, co jest w danej przestrzeni nazw, możemy wykorzystać także pewne wybrane elementy. Używamy do tego operacji using przestrzenNazw::element. Zobaczmy przykład użycia tej operacji:
#include <iostream> using std::endl; int main( ) { std::cout << "Hello World!" << endl; return 0; }
Za pomocą using std::endl poinformowaliśmy kompilator, że używany przez nas znak końca linii (czyli endl), znajduje się w przestrzeni nazw std. Dzięki temu możemy zamiast std::endl spokojnie pisać endl (nie powinno być problemów także wtedy, gdybyśmy napisali std::endl). Nie wykonaliśmy tej analogicznej operacji na elemencie cout (ściślej nie wstawiliśmy instrukcji using std::cout), więc musimy go dalej poprzedzać nazwą tej przestrzeni.
Ale właściwie do czego są używane przestrzenie nazw? Przede wszystkim zapewniają pewien komfort programiście. Pisząc np. jakąś funkcję, nie musimy się martwić jak się ją nazwie, ponieważ raczej nie spowoduje to żadnych kolizji nazw z nazwami innych funkcji, które na przykład znajdują się w innych bibliotekach (no chyba, że dana nazwa jest już zajęta w używanej przestrzeni nazw). Możemy w obrębie różnych przestrzeni nazw tworzyć funkcje o takich samych nazwach i deklaracjach, ale robiące zupełnie inne operacje!
[edytuj] Tworzenie własnej przestrzeni nazw
Przestrzeń nazw tworzymy za pomocą słowa kluczowego namespace, ograniczając zawartość klamrami. Możemy na przykład stworzyć przestrzeń nazw HelloWorld zawierającą funkcję hello( ):
#include <iostream> namespace HelloWorld { void hello( ) { std::cout << "Hello World!" << std::endl; } } int main( ) { HelloWorld::hello( ); return 0; }
Oczywiście, gdybyśmy wstawili using namespace HelloWorld przed funkcją main (a nawet wewnątrz tej funkcji), nie musielibyśmy odwoływać się bezpośrednio do HelloWorld, wystarczyłoby samo hello( ).
Nie musimy zamieszczać od razu wszystkiego, co chcielibyśmy, aby się znalazło w naszej przestrzeni nazw. Możemy rozbić to na wiele części:
namespace Matematyka { int dodaj( int a, int b ) { return a+b; } int odejmij( int a, int b ) { return a-b; } } namespace Matematyka { int pomnoz( int a, int b ) { return a*b; } int podziel( int a, int b ) { return a/b; } }
Wówczas wewnątrz przestrzeni nazw Matematyka znajdziemy wszystkie stworzone przez nas funkcje.
Tworząc funkcję w pewnej przestrzeni nazw możemy wstawić samą deklarację, a potem w innym miejscu podać pełną definicję tej funkcji. Możemy na co najmniej dwa sposoby podać definicję pewnej funkcji -- wewnątrz definicji przestrzeni nazw lub pisząc typ_zwracany przestrzenNazw::nazwaFunkcji( typ0 arg0, typ1 arg1, ... ) na przykład:
#include <iostream> namespace Matematyka { int dodaj( int a, int b ); int odejmij( int a, int b ); } using namespace std; int main( ) { cout << Matematyka::dodaj( 10, 20 ) << endl; return 0; } namespace Matematyka { int dodaj( int a, int b ) { return a+b; } int odejmij( int a, int b ) { return a-b; } }
Ostatnie 12 linii moglibyśmy zapisać także w ten sposób:
int Matematyka::dodaj( int a, int b ) { return a+b; } int Matematyka::odejmij( int a, int b ) { return a-b; }
Zależy to wyłącznie od nas, który sposób będziemy chcieli wykorzystać.
[edytuj] Przestrzeń nazw std
Wróćmy ponownie do standardowej przestrzeni nazw, jaką jest std. Dzięki plikowi nagłówkowemu iostream możemy operować na standardowym wejściu i wyjściu. Poprzednio dowiedzieliśmy się, jak wypisać pewien napis. Teraz zobaczymy jak wczytywać pewne wartości do zmiennych, używając do tego cin.Zobaczmy na przykład:
#include <iostream> int main( ) { std::cout << "Podaj dwie liczby a i b" << std::endl; int a, b; // wypisujemy "a:" na wyjście i czekamy na wpisanie liczby a std::cout << "a:"; std::cin >> a; // wypisujemy "b:" na wyjście i czekamy na wpisanie liczby b std::cout << "b:"; std::cin >> b; // wypisujemy sumę tych dwóch liczb std::cout << "a+b=" << a+b << std::endl; return 0; }
Dzięki std::cin >> możemy wczytać pewną wartość do dowolnej zmiennej. Zmienna ta nie musi być liczbą, może być także pewien tekst. W C++ bardzo często pewien tekst (łańcuch znaków) przechowuje się w zmiennej o typie string (który także znajduje się w std). Aby można było utworzyć taką zmienną trzeba najpierw dołączyć plik string. Zobaczmy na przykład:
#include <iostream> #include <string> using namespace std; int main( ) { string imie; string email; string informacja; // wczytujemy imię cout << "Podaj swoje imie: "; cin >> imie; // wczytujemy email cout << "Podaj swój email: "; cin >> email; informacja=imie+" ("+email+")"; // suma napisów cout << "Witaj " << informacja << endl; informacja+=" czyta ten napis"; cout << informacja << endl; return 0; }
Zauważmy, jak prosto się korzysta zmienną typu string (dla wtajemniczonych jest to pewna klasa). Jeśli chcemy dodać dwa napisy, wystarczy wykorzystać operator +. Możemy także wykorzystywać operator +=, jeśli chcemy dokleić do tekstu dodatkowy napis.
Podając swoje imię jako Zdzichu, a e-mail jako zdzichu@zdzichowo.mars, zobaczymy wynik:
Podaj swoje imie: Zdzichu Podaj swój email: zdzichu@zdzichowo.mars Witaj Zdzichu (zdzichu@zdzichowo.mars) Zdzichu (zdzichu@zdzichowo.mars) czyta ten napis
Więcej o stringach można przeczytać w dodatku opisującym bibliotekę STL.
[edytuj] Korzystanie z biblioteki standardowej C
Ponieważ język C++ jest (w pewnym uproszczeniu) rozwinięciem C, w dalszym ciągu można korzystać z biblioteki standardowej C (tzw. libc). Ze względu na zachowanie wstecznej kompatybilności umożliwiono korzystanie z niej tak jak wcześniej w C.
#include <string.h> int main(int argc, char **argv) { if( argc < 2 ) return -1; return strcmp(argv[0], argv[1]); }
Jednak dostępna jest też wersja libc przygotowana specjalnie dla C++. Pliki nagłówkowe są w niej inaczej nazywane, wszystkie funkcje znajdują się dodatkowo w przestrzeni nazw std. Tak więc powyższy program napisany w sposób właściwy dla C++ wyglądałby następująco:
#include <cstring> // zamiast <string.h> wykorzystujemy <cstring> using namespace std; int main(int argc, char **argv) { if( argc < 2 ) return -1; return strcmp(argv[0], argv[1]); }
Zauważmy, że:
- dołączany plik nagłówkowy ma dodaną na początku literę c
- nie jest używane rozszerzenie .h
Reguła ta dotyczy wszystkich plików, z których składa się biblioteka standardowa C.
W swoich programach lepiej jest używać wersji przygotowanej dla C++. Po pierwsze, dzięki przestrzeniom nazw unikniemy kolizji nazw z własnymi funkcjami. Po drugie, wersja ta ma wbudowaną obsługę wyjątków. Po trzecie, czasami libc przygotowana dla C wywołuje ostrzeżenia lub błędy kompilacji w kompilatorach C++.
[edytuj] Zmienne
Zanim przystąpisz do czytania tego rozdziału upewnij się, że opanowałeś już wiedzę z podręcznika C. Jest tu wykorzystywanych wiele odniesień i pojęć z tego języka.
[edytuj] Deklarowanie zmiennych
W języku C zmienne deklarowało się na początku bloku kodu (zwykle przed pierwszą instrukcją). W przeciwieństwie do C++ nie można było natomiast deklarować zmiennych np. w nagłówku pętli for. Poniższy przykład bez problemu powinien zadziałać w kompilatorach języka C++, natomiast starsze kompilatory C mogą go uznać za błędny:
int main () { for (int i = 0; i <= 10; ++i ) { // instrukcje... } }
W C++ deklaracje zmiennych mogą znajdować się w dowolnym miejscu kodu w funkcji, nie obowiązuje już zasada z C nakazująca ich deklarowanie przed właściwym kodem funkcji:
#include <iostream> using namespace std; int main () { int i; cin >> i; int j = i*i; cout << j; return 0; }
[edytuj] Kontrola typów
W C++ w stosunku do C została zaostrzona kontrola typów. Teraz za każdym razem, gdy przekażemy funkcji zmienną o innym typie dostaniemy błąd od kompilatora. Główna zmiana dotyczy wskaźników na typ void*. W C były one zupełnie bezkarne i można było przydzielać wskaźniki void* do każdych innych, w C++ są na równi z innymi typami. Teoretycznie kod napisany w C powinien zostać bez problemu skompilowany w kompilatorze C++, lecz istnieje kilka rozbieżności, które czasami to uniemożliwiają. Jedna z nich dotyczy właśnie typu void*. Kod w C, bez problemu skompilowany w kompilatorze tegoż języka:
int* wskaznik = malloc(sizeof(int));
nie zostanie skompilowany w kompilatorze C++, z powodu zaostrzonej kontroli typów. Aby sprawić, że ten kod będzie się kompilować musimy go odrobinę zmodyfikować:
int* wskaznik = (int*)malloc(sizeof(int));
Problem został rozwiązany przy użyciu rzutowania. Co to takiego? Odpowiedź znajdziesz w dziale poniżej.
[edytuj] Rzutowanie
W języku C rzutowanie wyglądało w następujący sposób:
int zmienna_calkowita = (int)zmienna_rzeczywista;
W C++ nadal można używać takiego rzutowania, jest ono nazywane "rzutowaniem w stylu C". Oprócz tego C++ oferuje "rzutowanie w stylu funkcyjnym":
int zmienna_calkowita = int(zmienna_rzeczywista);
które działa dokładnie tak samo.
Oba zapisy mają jedną istotną wadę - ciężko wypatrzeć je w kodzie. Każde rzutowanie jest potencjalnym miejscem wystąpienia błędów. Jeśli byśmy chcieli przejrzeć kod źródłowy w poszukiwaniu wszystkich rzutowań, nie byłoby to łatwe, przez co usuwanie błędów z programu w stylu języka C jest utrudnione.
C++ wprowadza inny sposób zapisu, który od razu rzuca się w oczy. Dodatkowo rzutowanie podzielono na cztery typy:
- static_cast
- proste rzutowanie
- const_cast
- rzutowanie ze zmiennych z modyfikatorem const i volatile na zmienne bez tych modyfikatorów
- reinterpret_cast
- niebezpieczne rzutowania, które zmieniają zupełnie sens interpretacji bitów w zmiennych
- dynamic_cast
- rzutowanie wskaźników na obiekty
Ostatnie z tych rzutowań będzie opisane później, w rozdziale Funkcje wirtualne.
Powodem takiego podziału jest potrzeba zwiększenia bezpieczeństwa przez wyeliminowanie pomyłek. Jak to działa? Jeśli chcielibyśmy dokonać pewnego rodzaju rzutowania operatorem, który nie jest do niego przewidziany, kompilator zgłosi nam błąd. Dodatkowo, jeśli podejrzewamy, że jakiś błąd w działaniu programu wynika z rzutowania, najczęściej chodzi nam o rzutowanie konkretnego rodzaju, zatem podział rzutowań ułatwia znajdywanie takich błędów.
Nowych operatorów rzutowania używa się w następujący sposób:
int zmienna_całkowita = static_cast<int>(zmienna_rzeczywista);
podając w nawiasach ostrych typ, na który rzutujemy.
Omówimy teraz dłużej pierwsze trzy z nowych rzutowań.
| Do zrobienia: dopisać dla każdego - jak używać, kiedy stosować, kiedy nie stosować, konsekwencje, przykład |
[edytuj] Static_cast
Operator static_cast zapewnia wysoki poziom bezpieczeństwa, gdyż widząc static_cast kompilator używa całej swojej mądrości, żeby zagwarantować jak najsensowniejszy rezultat rzutowania, w razie potrzeby zmieniając reprezentację wartości poddanej rzutowaniu. Przykładowo przy rzutowaniu zmiennej typu int na float, bity wewnętrznej reprezentacji zostaną zmienione, tak aby reprezentowały tę samą wartość matematyczną, ale według formatu używanego dla float.
[edytuj] Static_cast służy w szczególności do:
- Konwersji podstawowych typów liczbowych, np. int na float.
- Konwersji zdefiniowanych przez użytkownika.
- Konwersji wskaźnika na obiekt klasy pochodnej na wskaźnik na obiekt klasy podstawowej (tak zwane rzutowanie do góry hierarchii dziedziczenia).
- Konwersji wskaźnika na obiekt klasy podstawowej na wskaźnik na obiekt klasy pochodnej (tak zwane rzutowanie w dół hierarchii).
Są też inne zastosowania, np. rzutowanie zmiennej za pomocą wyrażenia static_cast<void>(nazwa_zmiennej), które na niektórych kompilatorach pozwala uniknąć ostrzeżenia o nieużywaniu tej zmiennej.
Nie przejmuj się, jeżeli trzy ostatnie punkty powyższej listy są niezrozumiałe. Staną się zrozumiałe po przeczytaniu rozdziału o dziedziczeniu i definiowaniu konwersji typów. Ważny jest morał z przytoczenia tych zastosowań, a mianowicie fakt, że static_cast służy do najczęściej wykorzystywanych, zdefiniowanych przez standard języka i bezpiecznych rzutowań. Czwarty punkt na powyższej liście przypomina jednak o tym, że nie zawsze rzutowanie static_cast jest bezpieczne w czasie wykonania programu.
Wyjaśnienie dla zaawansowanych:
- Jeśli wykonamy rzutowanie w dół na typ, który nie jest zgodny z rzeczywistym (dynamicznym) typem obiektu, rezultatem może być wysypanie się programu.
[edytuj] Do czego static_cast nie służy:
- Do rzutowania wskaźników na różne typy, jeśli nie ma specjalnie zdefiniowanej konwersji między tymi wskaźnikami. Przykładowo nie skompiluje się static_cast<int*>(i), jeśli zmienna i jest typu unsigned int* Nie uda się też rzutowanie ze wskaźnika na typ stały (z modyfikatorem const) na wskaźnik na typ niestały.
- Do dynamicznego sprawdzania, czy rzutowanie mogłoby się powieźć (czy ma sens). Nie miałoby to sensu, bo dla static_cast sposób rzutowania jest ustalany w czasie kompilacji. Zresztą nie ma żadnej informacji o błędzie, którą można by było sprawdzić.
[edytuj] Przykłady poprawnego użycia static_cast:
| Do zrobienia: przykłady |
[edytuj] Przykłady niepoprawnego użycia static_cast:
| Do zrobienia: przykłady |
[edytuj] Inne Cechy static_cast
Standard języka stwierdza również, że wyrażenia, które nie dokonują żadnej konwersji mogą być również opisane operatorem static_cast, np. int i=static_cast<int>(8);. Takie static_cast może być bezpiecznie usunięte z kodu, należy jednak uważać na usuwanie go z kodu generycznego, korzystającego z szablonów.
W powyższym wstępie i przykładach wszędzie, gdzie jest mowa o wskaźnikach, można by również mówić o referencjach. Obowiązują je te same reguły.
Należy pamiętać, że działanie rzutowania static_cast zależy tylko od takich informacji o typach, które są dostępne czasie kompilacji. Stąd słowo "static" w "static_cast". Kompilator nie dodaje "z własnej inicjatywy" kodu binarnego, więc static_cast można używać również w tzw. wąskich gardłach programu. Poprzednie zdanie celowo używa wyrażenia w cudzysłowie, bo jakiś kod oczywiście jest dodawany przez kompilator. Zazwyczaj jest to jednak tylko zmiana reprezentacji liczby lub wywołanie zdefiniowanej przez użytkownika (czyli z naszej inicjatywy) funkcji konwertującej.
[edytuj] const_cast
[edytuj] reinterpret_cast
[edytuj] Ćwiczenia
#include <stdio.h> int main(int argc, char *argv[]) { int liczba, liczba2; scanf("%d %d", &liczba, &liczba2); double wynik = liczba / liczba2; printf("%d / %d = %0.1f\n", liczba, liczba2, wynik); return 0; }
Po uruchomieniu powyższego programu i podaniu wejścia
5 2
Dlaczego jako wynik wyświetlana jest liczba 2.0 a nie 2.5? Rozwiąż problem przy użyciu rzutowania.
[edytuj] Referencje
[edytuj] Czym jest referencja?
Referencja w swym działaniu przypomina wskaźniki. Różnica polega jednak na tym, że do referencji można przypisać adres tylko raz, a jej dalsze używanie niczym się nie różni od używania zwykłej zmiennej. Operacje jakie wykona się na zmiennej referencyjnej, zostaną odzwierciedlone na zmiennej zwykłej, z której pobrano adres.
Można by pokusić się o stwierdzenie, że:
| Referencja jest inną nazwą zmiennej. |
[edytuj] Deklaracja referencji
Referencje deklaruje się jak zmienne z podaniem znaku &:
TypDanych & referencja;
Po deklaracji, należy przypisać zmiennej adres innej zmiennej (robi się to trochę inaczej niż w wypadku wskaźników):
referencja = innaZmienna;
Od tej pory można używać obu tych zmiennych zamiennie. Poniższe przypisania dadzą więc ten sam efekt:
innaZmienna = 9; referencja = 9;
Zobaczmy działanie referencji na konkretnym przykładzie:
int i=0; int& ref_i=i; cout << i; // wypisuje 0 ref_i = 1; cout << i; // wypisuje 1 cout << ref_i; // wypisuje 1
Porównajmy to z sytuacją, gdybyśmy użyli wskaźników:
int i=0; int * wsk_i=&i; cout << i; // wypisuje 0 *wsk_i = 1; cout << i; // wypisuje 1 cout << *wsk_i; // wypisuje 1
Zauważmy, o ile wygodniejsze jest użycie referencji. Nie musimy ani pobierać adresu zmiennej (&i) by przypisać go do referencji ani też używać gwiazdki by dostać wskazywaną wartość.
Jeszcze jedną różnicą ze wskaźnikami jest ograniczenie, że referencji po przypisaniu nie można przestawić na inną zmienną. Referencja musi też być zainicjalizowana w momencie utworzenia:
int a,b; int *wsk_a=&a,*wsk_b=&b; int &ref_a=a, &ref_b=b; int &ref_c; // kompilator nie zezwoli na to - referencja niezainicjalizowana wsk_b = &a; // ok ref_b = &a; // tak się nie da
Przykład przypomina też, że analogicznie jak w przypadku wskaźników znak & nie łączy się z typem tylko ze zmienną i przy deklarowaniu kilku referencji na raz trzeba wstawiać & przed każdą z nich:
int &ref_x, &ref_y, &z; // referencje char *wsk_1, *wsk2, *wskazniczek; // wskazniki
[edytuj] Stałe referencje
Możliwe jest zadeklarowanie referencji do obiektów stałych - wtedy obiektu, do którego odnosi się referencja nie będzie można zmienić.
int i=0; const int& ref_i=i; cout << ref_i; // wypisze 0 ref_i = 1; // kompilator nie pozwoli na to i zgłosi błąd
Powody, dla jakich możemy chcieć używać stałych referencji są analogiczne jak dla stałych wskaźników.
[edytuj] Przekazywanie argumentów przez referencję
Aby w C zmodyfikować parametr przekazywany do funkcji, musieliśmy używać wskaźników. C++ proponuje bezpieczniejszą i wygodniejszą w użyciu metodę - przekazywanie przez referencję.
Różnica między przekazywaniem przez referencję a przekazywaniem przez wskaźnik jest taka jaka miedzy referencjami i wskaźnikami, nie ma tu żadnej rewolucji. Przykład zastosowania pokazany jest poniżej:
void nie_zwieksz(int i) { ++i; // tak naprawdę funkcja nie robi nic, bo zmieniona zostaje tylko lokalna kopia } void zwieksz_c(int *i) { ++(*i); // ta funkcja jest napisana w stylu C } void zwieksz_cpp(int& i) { ++i; // ta funkcja wykorzystuje możliwości C++ } int main() { int a=0,b=0,c=0; nie_zwieksz(a); zwieksz_c(&b); zwieksz_cpp(c); cout << a << " " << b << " " << c; /* wypisze "0 1 1" */ return 0; }
[edytuj] Funkcje inline
Funkcje inline jak można by się domyśleć z nazwy są funkcjami "w linii" w C++ znaczy to, że kompilator widząc że funkcja jest inline w miejsce jej wywołania nie wstawia jak w normalnym przypadku wskaźnika do tej funkcji w pamięci, lecz wpisuje jej kod w miejsce jej wystąpienia. Takie funkcje dalej jednak występują w pamięci komputera, dzięki czemu możemy tworzyć do nich wskaźniki i używać ich jak w przypadku zwykłych funkcji.
Użycie funkcji inline:
inline int dodaj(int, int); //deklaracja int main() { int a, b, c; c = dodaj(a,b); return 0; } inline int dodaj(int a,int b) //definicja { return a+b; }
Rzeczywiste działanie:
int main() { int a, b, c; { c = a+b; //podstawianie kodu funkcji } return 0; }
Ma to zastosowanie, gdy zależy programiście na szybkości działania programu. Status inline zazwyczaj dodaje się krótkim funkcjom, nie mającym więcej niż kilkanaście linijek kodu. Czasami gdy kompilator uzna, że nasza funkcja jest zbyt długa lub wywołuje się rekurencyjnie ignoruje nasze inline. Gdy chcemy wymusić takie zachowanie to używamy np. __forceinline dla MS Visual C++. Funkcja składowa klasy zostaje natomiast automatycznie uznana za inline jeśli napiszemy jej kod bezpośrednio po jej deklaracji we wnętrzu klasy. Warto dodać że słowo inline jest słowem kluczowym w C++.
Teoretycznie makroinstrukcje języka C mają dość podobne działanie, lecz funkcje inline mają nad nimi kilka przewag:
- czytelność
- makra języka C są znacznie mniej czytelne i są tak jakby "czarną owcą" w kodzie, gdyż są wplecione między różnorakie funkcje, które stanowią większość kodu, od których znacznie różnią się zapisem.
- konwersja argumentów
- jako, że funkcja inline imituje zwykłą funkcję, posiada argumenty z kontrolą typów, dzięki czemu inni programiści mogą z nich łatwiej korzystać w stosunku do makroinstrukcji.
- argumenty jako zmienne
- w przypadku makroinstrukcji argumenty nie są traktowane jako zmienne; to co przekażemy jako argument, jest po prostu kopiowane w miejsca użycia danego argumentu w kodzie makroinstrukcji. Funkcje inline posiadają argumenty, które są zmiennymi, co wyklucza wiele błędów.
Gdzie tu jest haczyk? Otóż jako, że kod funkcji jest wstawiany w miejsce wywołania, to jeśli wywołamy tę funkcję w 3 miejscach, dostaniemy 3 kopie kodu tejże funkcji. Jeśli przesadzimy i będziemy dodawać przedrostek inline do zbyt wielu funkcji (zwłaszcza tych dużych i często wywoływanych), plik wykonywalny może urosnąć do niebotycznych rozmiarów (co dodatkowo przedłuża czas jego uruchamiania).
[edytuj] Przeciążanie funkcji
W języku C++ możliwe jest utworzenie kilku różnych funkcji, które posiadają tę samą nazwę. Takie funkcje muszą różnić się od siebie ilością lub typem argumentów. Dzięki temu kompilator będzie wiedział dokładnie, którą funkcję należy wywołać. Takie funkcje nazywamy przeciążonymi (czasem również – przeładowanymi).
Oto przykłady funkcji przeciążonych:
void funkcja(int argument); void funkcja(char* argument); void funkcja(char* argument, char* argument2); // int funkcja(int argument); //niedozwolone, funkcje różnią się tylko zwracanym typem int funkcja(bool argument); //dozwolone
Czasami kompilator może zabronić przeładowania, gdy uzna, że typy argumentów są zbyt podobne. Może tak się dziać na przykład w przypadku, gdy:
- użyjemy typu const T i T,
- użyjemy argumentów domyślnych.
void funkcja(int arg1, int arg2 = 0); void funkcja(int arg1); //to ta sama funkcja, zostanie zgłoszony błąd
Kompilator obsługuje przeciążanie przez dodanie do nazwy każdej z przeciążonych funkcji specjalnego identyfikatora, który związany jest z liczbą i typem argumentów - tak więc po etapie kompilacji wszystkie funkcje mają unikalne nazwy.
[edytuj] Zastosowanie
Przeciążenie funkcji najczęściej stosuje się przy np. potęgowaniu:
int pot(int podstawa, int wykladnik) { int wynik=1; for(int i = 0; i < wykladnik; ++i) wynik=podstawa*wynik; return wynik; } double pot(double podstawa, int wykladnik) // przeładowana funkcja I: zwraca inny typ danych i są inne parametry { int wynik=1; for(int i = 0; i < wykladnik; ++i) wynik=podstawa*wynik; return wynik; } void pot(int & podstawa, int wykladnik) // przeładowana funkcja II: nie zwraca danych tylko modyfikuje podstawę która jest podana przez referencję { int wynik=1; for(int i = 0; i < wykladnik; ++i) wynik=podstawa*wynik; podstawa = wynik; }
[edytuj] Zarządzanie pamięcią
W języku C++ do alokowania pamięci służy operator new a do zwalniania - delete. W C++ można również stosować funkcje malloc i free, jednak należy być ostrożnym. Najczęstszym błędem jest mieszanie operatorów new i delete z funkcjami malloc i free, np. zwalnianie pamięci zaalokowanej przez new przy pomocy free.
Rozważmy prosty przykład. Załóżmy, że chcemy stworzyć wektor 10 liczb typu całkowitego. Możemy to zrobić na dwa sposoby.W stylu znanym z języka C:
int *Wektor = (int*)malloc(sizeof(int)*10); free(Wektor);
Albo w stylu C++:
int *Wektor = new int[10]; delete [] Wektor;
Od razu widać, że drugi zapis jest zdecydowanie łatwiejszy i przyjemniejszy w użyciu. To jest podstawowa zaleta operatora new - krótszy zapis. Wystarczy wiedzieć jakiego typu ma być obiekt, który chcemy powołać do życia, nie martwiąc się o rozmiar alokowanego bloku pamięci. Za pomocą operatora new można również tworzyć tablice wielowymiarowe:
int **Wektory = new int *[5]; for( int i=0; i<5; ++i ) Wektory[i] = new int [10];
W ten sposób stworzono tablicę dwuwymiarową którą statycznie zadeklarowalibyśmy jako:
int Wektory[5][10];
Jenak w przeciwieństwie do int Wektory[5][10], która jest tablicą dwuwymiarową, nasze int **Wektory jest tablicą tablic i może być rozrzucone po całej pamięci.
Ilość elementów poszczególnych wymiarów nie musi być jednakowa. Można np zadeklarować tablicę taką:
int **Wektory = new int *[2]; Wektory[0] = new int [5]; Wektory[1] = new int;
Przy takiej deklaracji pierwszy wiersz ma 5 elementów (tablica) a drugi to jeden element. Deklaracja tablic o większej ilości wymiarów przebiega podobnie:
int ***Wektory = NULL; // deklarujemy tablicę 3-wymiarową Wektory = new int **[5]; // pierwszy wymiar Wektory[0] = new int *[10]; // pierwszy element pierwszego wymiaru Wektory[1] = new int *[3]; // drugi element pierwszego wymiaru .... Wektory[0][1] = new int [3] // wymiar I = 0 -> wymiar II = 1 -> 3 elementy(tablica) Wektory[0][3] = new int [5] // wymiar I = 0 -> wymiar II = 3 -> 5 elementów(tablica) Wektory[1][2] = new int; // wymiar I = 1 -> wymiar II = 2 -> 1 element ...
Stosując ten algorytm ogólnie można deklarować tablice n-wymiarowe bez większego problemu. Usuwanie tablic wielowymiarowych przebiega podobnie jak jednowymiarowych z tą różnicą, że usuwanie zaczynamy od "najgłębszego" wymiaru:
int ***Wektory = new int **[2]; Wektory[0] = new int *[2]; Wektory[1] = new int *[2]; Wektory[0][0] = new int; // pojedyncza zmienna dynamniczna! nie tablica Wektory[0][1] = new int [5]; // zmienna tablicowa Wektory[1][0] = new int [3]; Wektory[1][1] = new int [2]; ... // Kod programu ... // III wymiar delete Wektory[0][0]; // kasujemy pojedynczą zmienną delete [] Wektory[0][1]; delete [] Wektory[1][0]; delete [] Wektory[1][1]; // II wymiar delete [] Wektory[0]; delete [] Wektory[1]; // I wymiar delete [] Wektory;
Zwrócić uwagę trzeba na dwie rzeczy:
delete []używamy dla zmiennych tablicowych, adeletedla pojedynczych zmiennych- Kolejność zwalniania wymiarów jest odwrotna niż ich tworzenia
Drugą zaletą jest fakt, że przy okazji alokacji pamięci możemy wywołać odpowiedni konstruktor inicjując wartości zmiennych obiektu, np.
Test *test = new Test(1,2);
zakładając, że obiekt Test posiada dwie zmienne typu całkowitego i zdefiniowany konstruktor Test(int,int).
Kolejną korzyścią jest możliwość przeciążania. Jednak to już jest temat na inny rozdział.
Część 2
Podstawy programowania obiektowego
[edytuj] Czym jest obiekt?
Aby odpowiedzieć na pytanie zadane w temacie, zadajmy sobie inne:
- Co nazywamy obiektem w świecie rzeczywistym?
Otóż wszystko może być obiektem! Drzewa, zwierzęta, miasta, auta, ludzie...
W programowaniu również obiektem może być dowolny twór, o jakim pomyślimy. Tworząc "świat programu" można stworzyć obiekt, którego użycie będzie bardziej "namacalne" od szeregu parametrów, porozrzucanych w różnych zmiennych. To różni programowanie strukturalne od programowania obiektowego.
[edytuj] Projekt i twór – klasa i obiekt
Zanim stworzymy jakiś obiekt, trzeba ustalić czym ten obiekt będzie. W zależności od tego, czy chcemy stworzyć wirtualny samochód, czy samolot, należy określić dwie rzeczy:
- jakie właściwości będzie miał ten obiekt,
- jakie będzie miał metody działania.
W związku z tym, przed stworzeniem jakiegokolwiek obiektu należy przedstawić kompilatorowi jego projekt(wzorzec), czyli określić jego klasę.
| Klasa to byt programistyczny określający jakie właściwości i metody będą miały obiekty, które zostaną utworzone na jej podstawie. |
Jednak sam projekt nie sprawi jeszcze, że dostaniemy obiekty (to tak jakby po narysowaniu projektu domu chcieć zamieszkać na kartce papieru :-)). Trzeba jeszcze obiekt utworzyć, co oznacza po prostu deklarację obiektu na podstawie pewnej klasy:
NazwaKlasy MojObiekt;
Wygląda to jak deklaracja zwykłej zmiennej i tak jest w istocie – w C++ tworząc klasę definiuje się nowy typ danych. Podobnie jak w przypadku zmiennych, można utworzyć wiele obiektów danej klasy.
[edytuj] Deklaracja klasy
Ogólny szablon definiowania klas w C++ wygląda następująco:
class NaszaNazwaKlasy { ... // pola i metody składowe klasy };
Po słowie kluczowym class następuje nazwa naszej klasy (prawidła jej nazywania są takie same jak dla zmiennych).
W nawiasach klamrowych umieszcza się definicje składowych klasy: pól i metod określając dla nich specyfikatory dostępu.
Należy pamiętać o średniku za klamerką zamykającą definicję klasy.
Oto przykładowa definicja klasy:
class NazwaKlasy{ public: //pola i metody są publicznie dostępne //definiowanie pól int poleInt; float poleFloat; //definiowanie metod int Metoda1(); void Metoda2(); }; //pamiętaj o średniku!
[edytuj] Użycie klasy
Sama definicja klasy nie wystarczy, aby uzyskać dostęp do jej składowych. Należy stworzyć obiekt. Można przyjąć, że obiekt to zmienna typu klasowego. Deklaracja obiektu:
NazwaKlasy Obiekt;
Dostęp do pól i metod uzyskuje się operatorem wyłuskania (.):
Obiekt.poleInt = 0;//przypisanie wartości polom Obiekt.poleFloat = 9.04; Obiekt.Metoda1();//wywołanie metody obiektu
W przypadku deklaracji wskaźnika do obiektu:
NazwaKlasy *ObiektWsk = new NazwaKlasy;
operatorem wyłuskania staje się ->:
ObiektWsk->poleInt = 0; //przypisanie wartości polom ObiektWsk->poleFloat = 9.04; ObiektWsk->Metoda1(); //wywołanie metody obiektu
Należy pamiętać o zniszczeniu obiektu przed zakończeniem działania programu:
delete ObiektWsk;
[edytuj] Przykład
Stwórzmy klasę kostki do gry:
class Kostka{ public: unsigned int wartosc; unsigned int maks; void Losuj(); };
Po deklaracji klasy, zadeklarujemy jeszcze metodę Losuj() tej klasy:
void Kostka::Losuj() { wartosc = rand()%maks + 1; }
Warto zwrócić uwagę w jaki sposób się to robi. Nazwą metody dowolnej klasy jest NazwaKlasy::NazwaMetody. Poza tym aby uzyskać dostęp do pól klasy, w której istnieje dana metoda nie stosuje się operatora wyłuskania.
Po tym można napisać resztę programu:
int main() { Kostka kostkaSzescienna; //utworzenie obiektu kostkaSzescienna.maks = 6; //określenie maksymalnej ilosci oczek kostkaSzescienna.Losuj(); //losowanie cout << "Wylosowano:" << kostkaSzescienna.wartosc << endl;//wypisanie wyniku return 0; }
[edytuj] Autorekursja
Wskaźnik this umożliwia jawne odwołanie się do pól klasy. Poniższy program wymusza użycie wskaźnika this, gdyż nazwa pola jest taka sama jak nazwa argumentu metody wczytaj:
#include <iostream.h> class KlasaThis { int liczba; public: void wczytaj(int liczba) {this->liczba=liczba;} void wypisz() {cout << liczba <<endl;} }; int main() { KlasaThis ObiektThis; ObiektThis.wczytaj(11); ObiektThis.wypisz(); return 0; }
[edytuj] Kontrola dostępu
Istnieją trzy specyfikatory dostępu do składowych klasy:
- private (prywatny) - dostępne tylko z wnętrza danej klasy i klas/funkcji zaprzyjaźnionych.
- protected (chroniony) - dostępne z wnętrza danej klasy, klas/funkcji zaprzyjaźnionych i klas pochodnych.
- public (publiczny) - dostępne dla każdego.
Jeśli sekwencja deklaracji składowych klasy nie jest poprzedzona żadnym z powyższych specyfikatorów, to domyślnym specyfikatorem (dla kompilatora) będzie private.
Dzięki specyfikatorom dostępu inni programiści mają ułatwione korzystanie z utworzonej przez nas klasy, gdyż metody i pola, których nie powinni modyfikować, bo mogłoby to spowodować niepoprawne działanie obiektu, są oznaczone jako private lub protected i nie mogą z nich korzystać. Funkcje, które zapewniają pełną funkcjonalność klasy oznaczone są jako public i tylko do nich ma dostęp użytkownik klasy (do protected również, ale z ograniczeniami). Oto zmodyfikowany przykład z kostką, który zobrazuje cele kontroli dostępu:
class Kostka{ unsigned int maks; //domyślnym specyfikatorem jest private public: unsigned int wartosc; void Losuj(); void ZmienIloscScian(unsigned int _maks); }; void Kostka::ZmienIloscScian(unsigned int _maks) { if(_maks > 20) maks = 20; else maks = _maks; }
Zmodyfikowana klasa zezwala tylko na kostki maksymalnie dwudziestościenne. Ręczne modyfikacje zmiennej maks są zabronione, można tego dokonać jedynie poprzez funkcję ZmienIloscScian, która zapobiega przydzieleniu większej ilości ścianek niż 20.
[edytuj] Ćwiczenia
1. Napisz klasę reprezentującą człowieka. Musi on być opisany przy pomocy: imienia, nazwiska, płci, wieku, partnerki/partnera(jako wskaźnik).
2. Rozwiń klasę napisaną w 1. ćwiczeniu dodając ograniczenie, między partnerami nie może być większa niż 25% wieku starszej z nich.
3*. Jeśli zaznajomiłeś się z wektorami, dodaj kolejny parametr opisujący ludzi - zainteresowania, dodaj odpowiednią funkcję do dodawania nowych zainteresowań do listy oraz funkcję porównującą zainteresowania obu ludzi i zwracającą procent identycznych zainteresowań.
[edytuj] Kontrola dostępu
[edytuj] Konstruktor i destruktor
[edytuj] Teoria
[edytuj] Wstęp
Pisząc klasy każdy kiedyś dotrze do momentu, w którym będzie odczuwał potrzebę napisania funkcji wykonującej jakieś niezbędne instrukcje na początku lub na końcu istnienia obiektu. W takim momencie programista powinien sięgnąć po dwa niezwykle przydatne narzędzia: konstruktory i destruktory.
[edytuj] Konstruktor
Konstruktor jest to funkcja w klasie, wywoływana w trakcie tworzenia każdej instancji. Funkcja może stać się konstruktorem gdy spełni poniższe warunki
- Ma identyczną nazwę jak nazwa klasy
- Nie zwraca żadnej wartości (nawet void)
- Jest ustawiona w przestrzeni publicznej klasy (sekcja public:)
Należy dodać że każda klasa ma swój konstruktor. Nawet jeżeli nie zadeklarujemy go jawnie zrobi to za nas kompilator (stworzy wtedy konstruktor bezparametrowy).
Mamy na przykład klasę Miesiac. Chcielibyśmy, aby każdy obiekt tej klasy tuż po utworzeniu wygenerował tablicę z nazwami dni tygodnia w zależności od miesiąca i roku. A może dało by się to zrobić w trakcie tworzenia klasy?
Przyjrzyj się poniższej klasie, oraz funkcji konstruktora:
class Miesiac { public: int dni[31]; int liczbaDni; char nazwa[20]; Miesiac();//deklaracja konstruktora }; Miesiac::Miesiac()//definicja konstruktora { /* instrukcje tworzące */ }
Konstruktor może też przyjmować argumenty. Jak?
To zależy od sposobu w jaki tworzymy obiekt:
- jako obiekt
MojaKlasa obiekt(argumenty);
- jako wskaźnik do obiektu:
MojaKlasa* wsk = new MojaKlasa(argumenty);
Teraz powyższa klasa miesiąca może być stworzona z uwzględnieniem numeru miesiąca i roku:
class Miesiac { public: int dni[31]; int liczbaDni; char nazwa[20]; Miesiac(int numer,int rok); }; Miesiac::Miesiac(int numer,int rok) { /* instrukcje tworzące */ }
Aby utworzyć nowy obiekt tej klasy trzeba będzie napisać:
Miesiac styczen2000(1,2000);
lub jako wskaźnik do obiektu:
Miesiac* styczen2000 = new Miesiac(1,2000);
otrzymawszy w ten sposób kalendarz na styczeń.
W przypadku dziedziczenia zawsze najpierw jest wykonywany konstruktor nadklasy. Jeżeli nie podamy jawnie, który konstruktor nadklasy ma się wykonać, zostanie wykonany konstruktor bezparametrowy (jeżeli taki nie istnieje kompilator zwróci informacje o błędzie). Możemy jawnie wywołać inny konstruktor np. w taki sposób:
class Miesiac { public: int numer; int rok; Miesiac(int numer,int rok) { this->numer=numer; this->rok=rok; } }; class Grudzien : public Miesiac { public: Grudzien(int rok) : Miesiac(12, rok) //tutaj wywołujemy konstruktor klasy pierwotnej {} };
Najczęstszą funkcją konstruktora jest inicjalizacja obiektu, oraz alokacja pamięci (np. poprzez stworzenie potrzebnych obiektów).
[edytuj] Konstruktor kopiujący
Konstruktor kopiujący to konstruktor spełniający specyficzne zadanie. Mianowicie może on zostać wywoływany przez kompilator niejawnie jeżeli zachodzi potrzeba stworzenia drugiej instancji obiektu (np. podczas przekazywania obiektu do funkcji przez wartość).
Jeżeli nie zaimplementujemy konstruktora kopiującego, kompilator zrobi to automatycznie. Konstruktor taki będzie po prostu tworzył drugą instancję wszystkich pól obiektu. Możemy go jawnie wywołać np. tak:
Miesiac miesiac(12,2005); Miesiac kopia(miesiac); //tu zostanie wywołany konstruktor kopiujący /* obiekt kopia będzie miał taką samą zawartość jak obiekt miesiąc */
Jeżeli chcemy sami zaimplementować konstruktor kopiujący musimy zadeklarować go jako konstruktor o jednym parametrze będącym referencją na obiekt tej samej klasy.
class Miesiac { public: int numer; int rok; Miesiac(const Miesiac &miesiac) { numer=miesiac.numer; rok=miesiac.rok; } };
[edytuj] Destruktor
Destruktor jest natomiast funkcją, którą wykonuje się w celu zwolnienia pamięci; następuje niszczenie obiektu danej klasy.
Zasady "przemiany" zwykłej funkcji do destruktora, są podobne do tych tyczących się konstruktora. Jedyna zmiana tyczy się nazwy funkcji: Musi się ona zaczynać od znaku tyldy - ~.
class MojaKlasa { MojaKlasa();//to oczywiście jest konstruktor ~MojaKlasa();//a to - destruktor };
Najczęstszą funkcją destruktora jest zwolnienie pamięci (zwykle poprzez zniszczenie wszystkich pól używanych przez ten obiekt).
| Porada Należy pamiętać, że jeżeli zamierzamy implementować dziedziczenie po klasie dla której piszemy destruktor to powinniśmy stworzyć destruktor wirtualny! class MojaKlasa { MojaKlasa(); virtual ~MojaKlasa();//to jest destruktor wirtualny }; Początkujący programiści często o tym zapominają, doprowadzając w ten sposób czasami do tzw. wycieków pamięci. Dobrą praktyką jest tworzenie tylko destruktorów wirtualnych (patrz Funkcje wirtualne). |
[edytuj] Ćwiczenia
[edytuj] Ćwiczenie 1
Napisz definicje konstruktorów do poniższej klasy:
class Vector { public: double x; double y; public: Vector(); Vector(double x, double y); };
Klasa ma reprezentować wektor w przestrzeni dwuwymiarowej, a konstruktory mają realizować inicjalizację tego wektora. Pierwszy konstruktor powinien ustawiać wektor na wartość domyślną (0,0).
[edytuj] Ćwiczenie 2
Dopisz do kodu z poprzedniego ćwiczenia konstruktor kopiujący.
Vector(const Vector &vector);
Po wykonaniu tego ćwiczenia zastanów się, czy napisanie konstruktora kopiującego było konieczne. Jeżeli nie jesteś pewien - napisz program który testuje działanie Twojego konstruktora kopiującego i sprawdź jak program działa bez niego. Wyjaśnij dlaczego konstruktor kopiujący nie jest potrzebny.
[edytuj] Ćwiczenie 3
Poniższa klasa miała implementować dowolnej wielkości tablicę obiektów klasy Vector z poprzednich ćwiczeń. Niestety okazało się że powoduje wycieki pamięci - programista zapomniał o napisaniu destruktora:
class VectorsArray { protected: int size; //ilość wektorów w tablicy public: Vector [] vectors; public: VectorsArray(int size) { this->size = size; vectors = new Vectors[size]; } Vector GetVector(int i) { return Vector[i]; } int GetSize() { return size; } };
Do powyższej klasy dopisz definicję destruktora. Nie zapomnij o dealokacji pamięci!
[edytuj] Dziedziczenie
[edytuj] Wstęp - Co to jest dziedziczenie
Często podczas tworzenia klasy napotykamy na sytuację, w której klasa ta powiększa możliwości innej klasy, nierzadko precyzując jednocześnie jej funkcjonalność. Dziedziczenie daje nam możliwość wykorzystania nowych klas w oparciu o stare klasy. Nie należy jednak traktować dziedziczenia jedynie jako sposobu na współdzielenie kodu między klasami. Dzięki mechanizmowi rzutowania możliwe jest interpretowanie obiektu klasy tak, jakby był obiektem klasy z której się wywodzi. Umożliwia to skonstruowanie szeregu klas wywodzących się z tej samej klasy i korzystanie w przejrzysty i spójny sposób z ich wspólnych możliwości. Należy dodać, że dziedziczenie jest jednym z czterech elementów programowania obiektowego (obok abstrakcji, enkapsulacji i polimorfizmu).
Klasę z której dziedziczymy nazywamy klasą bazową, zaś klasę, która po niej dziedziczy nazywamy klasą pochodną. Klasa pochodna może korzystać z funkcjonalności klasy bazowej i z założenia powinna rozszerzać jej możliwości (poprzez dodanie nowych metod, lub modyfikację metod klasy bazowej).
[edytuj] Składnia
Składnia dziedziczenia jest bardzo prosta. Przy definicji klasy należy zaznaczyć po których klasach dziedziczymy. Należy tu zaznaczyć, że C++ umożliwia Wielodziedziczenie, czyli dziedziczenie po wielu klasach na raz. Jest ono opisane w rozdziale Dziedziczenie wielokrotne.
class nazwa_klasy :[operator_widocznosci] nazwa_klasy_bazowej, [operator_widocznosci] nazwa_klasy_bazowej ... { definicja_klasy };
operator_widoczności może przyjmować jedną z trzech wartości: public, protected, private. Operator widoczności przy klasie, z której dziedziczymy pozwala ograniczyć widoczność elementów publicznych z klasy bazowej.
- public - oznacza, że dziedziczone elementy (np. zmienne lub funkcje) mają taką widoczność jak w klasie bazowej.
- public
public - protected
protected - private
brak dostępu w klasie pochodnej
- protected - oznacza, że elementy publiczne zmieniają się w chronione.
- public
protected - protected
protected - private
brak dostępu w klasie pochodnej
- private - oznacza, że wszystkie elementy klasy bazowej zmieniają się w prywatne.
- public
private - protected
private - private
brak dostępu w klasie pochodnej
- 'brak operatora - oznacza, że niejawnie (domyślnie) zostanie wybrany operator private..
- public
private - protected
private - private
brak dostępu w klasie pochodnej
Dostęp do elementów klasy bazowej można uzyskać jawnie w następujący sposób:
[klasa_bazowa::...]klasa_bazowa::element
Zapis ten umożliwia dostęp do elementów klasy bazowej, które są "przykryte" przez elementy klasy nadrzędnej (mają takie same nazwy jak elementy klasy nadrzędnej). Jeżeli nie zaznaczymy jawnie o który element nam chodzi kompilator uzna że chodzi o element klasy nadrzędnej, o ile taki istnieje (przeszukiwanie będzie prowadzone w głąb aż kompilator znajdzie "najbliższy" element).
[edytuj] Przykład 1
[edytuj] Definicja i sposób wykorzystania dziedziczenia
Najczęstszym powodem korzystania z dziedziczenia podczas tworzenia klasy jest chęć sprecyzowania funkcjonalności jakiejś klasy wraz z implementacją tej funkcjonalności. Pozwala to na rozróżnianie obiektów klas i jednocześnie umożliwia stworzenie funkcji korzystających ze wspólnych cech tych klas. Załóżmy że piszemy program symulujący zachowanie zwierząt. Każde zwierze powinno móc jeść. Tworzymy odpowiednią klasę:
class Zwierze { public: Zwierze(); void jedz(); };
Następnie okazuje się, że musimy zaimplementowac klasy Ptak i Ssak. Każdy ptak i ssak jest zwierzęciem. Oprócz tego ptak może latać, a ssak ssać. Wykorzystanie dziedziczenia wydaje się tu naturalne.
class Ptak : public Zwierze { public: Ptak(); void lec(); }; class Ssak : public Zwierze { public: Ssak(); void ssij(); };
Co istotne tworząc takie klasy możemy wywołać ich metodę pochodzącą z klasy Zwierze:
Ptak ptak; ptak.jedz(); //metoda z klasy Zwierze ptak.lec(); //metoda z klasy Ptak Ssak *ssak=new Ssak(); ssak->jedz(); //metoda z klasy Zwierze ssak->ssij(); //metoda z klasy Ssak
Możemy też zrzutować obiekty klasy Ptak i Ssak na klasę Zwierze:
Ptak *ptak=new Ptak(); Zwierze *zwierze; zwierze=ptak; zwierze->jedz(); Ssak ssak; ((Zwierze)ssak).jedz();
Jeżeli tego nie zrobimy, a rzutowanie jest potrzebne, kompilator sam wykona rzutowanie niejawne:
Zwierze zwierzeta[2]; zwierzeta[0]=Ssak(); //rzutowanie niejawne zwierzeta[1]=Ptak(); //rzutowanie niejawne for (int i=0; i<2; ++i) zwierzeta[i].jedz();
[edytuj] Dostęp do elementów przykrytych
[edytuj] Elementy chronione - operator widoczności protected
Sekcja protected klasy jest ściśle związana z dziedziczeniem - elementy i metody klasy, które się w niej znajdują, mogą być swobodnie używane w klasie dziedzicznej ale poza klasą dziedziczną i klasą bazową nie są widoczne.
[edytuj] Elementy powiązane z dziedziczeniem
Chciałbym zwrócić uwagę na inne, bardzo istotne elementy dziedziczenia, które są opisane w następnych rozdziałach tego podręcznika, a które mogą być wręcz niezbędne w prawidłowym korzystaniu z dziedziczenia (przede wszystkim Funkcje wirtualne).
[edytuj] Funkcje wirtualne
Przykrywanie metod, czyli definiowanie metod w klasie pochodnej o nazwie i parametrach takich samych jak w klasie bazowej, ma zwykle na celu przystosowanie metody do nowej funkcjonalności klasy. Bardzo często wywołanie metody klasy bazowej może prowadzić wręcz do katastrofy, ponieważ nie bierze ona pod uwagę zmian miedzy klasą bazową a pochodną. Problem powstaje, kiedy nie wiemy jaka jest klasa nadrzędna obiektu, a chcielibyśmy żeby zawsze była wywoływana metoda klasy pochodnej. W tym celu język C++ posiada funkcje wirtualne. Są one opisane w rozdziale Funkcje wirtualne.
[edytuj] Wielodziedziczenie - czyli dziedziczenie wielokrotne
Język C++ umożliwia dziedziczenie po wielu klasach bazowych na raz. Proces ten jest opisany w rozdziale Dziedziczenie wielokrotne.
[edytuj] Przykład 2
#include <iostream>
class Zwierze
{
public:
Zwierze()
{ }
void jedz( )
{
for ( int i=0; i<10; ++i )
std::cout << "Om Nom Nom Nom\n";
}
void pij( )
{
for ( int i=0; i<5; ++i )
std::cout << "Hlip, hlip\n";
}
void spij( )
{
std::cout << "Chrr...\n";
}
};
class Pies : public Zwierze
{
public:
Pies()
{ }
void szczekaj()
{
std::cout << "Hał, Hał...\n";
}
void warcz()
{
std::cout << "Wrrrrrr...\n";
}
};
...
Za pomocą
...
class Pies : public Zwierze
{
...
utworzyliśmy klasę Psa, która dziedziczy klasę Zwierze. Dziedziczenie umożliwia przekazanie zmiennych, metod itp. z jednej klasy do drugiej. Możemy funkcję main zapisać w ten sposób:
...
int main()
{
Pies burek;
burek.jedz();
burek.pij();
burek.warcz();
burek.pij();
burek.szczekaj();
burek.spij();
return 0;
}
[edytuj] Składniki statyczne
| Do zrobienia: Pola statyczne muszą być też zadeklarowane poza klasą jako globalne jeśli ciało metod piszemy poza klasą |
[edytuj] Wstęp
Czasami zachodzi potrzeba dodania elementu, który jest zwiazany z klasą, ale nie z konkretną instancją tej klasy. Możemy wtedy stworzyć element statyczny. Element statyczny jest właśnie elementem, który jest powiązany z klasą, a nie z obiektem tej klasy, czyli np. statyczna metoda nie może się odwołać do niestatycznej zmiennej lub funkcji.
[edytuj] Składnia
Elementy statyczne poprzedza się podczas definicji słówkiem static. Statyczne mogą być zarówno funkcje, jak i pola należące do klasy.
class Klasa
{
protected:
static int iloscInstancji; // pole statyczne
public:
Klasa()
{
iloscInstancji++;
}
virtual ~Klasa()
{
iloscInstancji--;
}
static int IloscInstancji()
{
return iloscInstancji;
}
};
int Klasa::iloscInstancji=0;
Jak widać do obiektów statycznych z wewnątrz klasy możemy się odwołać tak samo jak do innych pól. Pole IloscInstancji w powyższym przykładzie nie jest jednak zwykłym polem - jest polem statycznym. Oznacza to, że powstanie tylko jedna instancja tego pola. W powyższym przykładzie iloscInstancji ma za zadanie zliczania ile powstało obiektów klasy Klasa.
W powyższym przykładzie ponadto istnieje metoda statyczna. Z takiej metody nie można się odwołać do niestatycznych elementów klasy. Zarówno do klasy statycznej jak do statycznego pola możemy się odwołać nawet jeżeli nie został stworzony żaden obiekt klasy Klasa.
Odwołanie się do metody statycznej IloscInstancji z programu wymaga następująco:
int i=Klasa::IloscInstancji();
Gdyby zaś pole iloscInstancji było publiczne, a nie chronione, to moglibyśmy się do niego odwołać poprzez:
int i=Klasa::iloscInstancji;
Ponieważ jednak w powyższym przykładzie pole iloscInstancji jest chronione możemy się do niego odwołać jedynie z klasy Klasa bądź z klas które po niej dziedziczą.
Oczywiscie metody statyczne nie mogą być wirtualne.
Część 3
Zawansowane programowanie
obiektowe
[edytuj] Funkcje wirtualne
| Do zrobienia: przykład naprawdę polimorficznego i praktycznego programu |
[edytuj] Wstęp
Funkcje wirtualne to specjalne funkcje składowe, które przydają się szczególnie, gdy używamy obiektów posługując się wskaźnikami lub referencjami do nich. Dla zwykłych funkcji z identycznymi nazwami to, czy zostanie wywołana funkcja z klasy podstawowej, czy pochodnej, zależy od typu wskaźnika, a nie tego, na co faktycznie on wskazuje. Dysponując funkcjami wirtualnymi będziemy mogli użyć prawdziwego polimorfizmu - używać metod klasy pochodnej wszędzie tam, gdzie spodziewana jest klasa podstawowa. W ten sposób będziemy mogli korzystać z metod klasy pochodnej korzystając ze wskaźnika, którego typ odnosi się do klasy podstawowej. W tej chwili może się to wydawać niepraktyczne, lecz za chwilę przekonasz się, że funkcje wirtualne niosą naprawdę sporo nowych możliwości.
[edytuj] Opis
Na początek rozpatrzymy przykład, który pokaże, dlaczego zwykłe, niewirtualne funkcje składowe nie zdają egzaminu gdy posługujemy się wskaźnikiem, który może wskazywać i na obiekt klasy podstawowej i na obiekt dowolnej z jej klas pochodnych.
Mając klasę bazową wyprowadzamy od niej klasę pochodną:
class Baza { public: void pisz() { std::cout << "Tu funkcja pisz z klasy baza" << std::endl; } }; class Baza2 : public Baza { public: void pisz() { std::cout << "Tu funkcja pisz z klasy Baza2" << std::endl; } };
Jeżeli teraz w funkcji main stworzymy wskaźnik do obiektu typu Baza, to możemy ten wskaźnik ustawiać na dowolne obiekty tego typu. Można też ustawić go na obiekt typu pochodnego, czyli Baza2:
int main() { Baza *wsk; Baza objB; Baza2 objB2; wsk = &objB; wsk -> pisz(); // Teraz ustawiamy wskaźnik wsk na obiekt typu pochodnego wsk = &objB2; wsk -> pisz(); return 0; }
Po skompilowaniu na ekranie zobaczymy dwa wypisy: "Tu funkcja pisz z klasy Baza". Stało się tak dlatego, że wskaźnik jest do typu Baza. Gdy ustawiliśmy wskaźnik na obiekt typu pochodnego (wolno nam), a następnie wywołaliśmy funkcję składową, to kompilator "na ślepo" sięgnął po funkcję pisz z klasy bazowej (bo wskaźnik wskazuje na klasę bazową).
Można jednak określić żeby kompilator nie sięgał po funkcję z klasy bazowej, ale sam się zorientował na co wskaźnik pokazuje. Do tego służy przydomek virtual, a funkcja składowa nim oznaczona nazywa się wirtualną. Różnica polega tylko na dodaniu słowa kluczowego virtual, co wygląda tak:
class Baza { public: virtual void pisz() { std::cout << "Tu funkcja pisz z klasy baza" << std::endl; } }; class Baza2 : public Baza { public: virtual void pisz() { std::cout << "Tu funkcja pisz z klasy Baza2" << std::endl; } };
[edytuj] Konsekwencje
Gdy funkcja jest oznaczona jako wirtualna, kompilator nie przypisuje na stałe wywołania funkcji z tej klasy, na którą pokazuje wskaźnik, już podczas kompilacji. Pozostawia decyzję co do wyboru właściwej wersji funkcji aż do momentu wykonania programu - jest to tzw. późne wiązanie. Wtedy program skorzysta z krótkiej informacji zapisanej w obiekcie a określającej klasę, do jakiej należy dany obiekt. Dopiero po odczytaniu informacji o klasie danego obiektu wybierana jest właściwa metoda.
Jeśli klasa ma choć jedną funkcję wirtualną, to do każdego jej obiektu dopisywany jest identyfikator tej klasy a do wywołania funkcji dopisywany jest kod, który ten identyfikator czyta i odnajduje odpowiednią funkcję. Gdy klasa funkcji wirtualnych nie posiada, takie informacje nie są dodawane, bo nie są potrzebne.
Zauważmy też, że nie zawsze decyzja o wyborze funkcji jest dokonywana dopiero na etapie wykonania. Gdy do obiektów odnosimy się przez zmienną a nie przez wskaźnik lub referencję to kompilator już na etapie kompilacji wie, jaki jest typ (klasa) danej zmiennej (bo do zmiennej w przeciwieństwie do wskaźnika lub referencji nie można przypisać klasy pochodnej). Tak więc wirtualność nie gra roli gdy nie używamy wskaźników; kompilator generuje wtedy taki sam kod, jakby wszystkie funkcje były niewirtualne. Przy wskaźnikach musi orientować się czytając informację o klasie obiektu, na który wskazuje wskaźnik, bo moglibyśmy np. losować, czy do wskaźnika przypiszemy klasę bazową czy jej pochodną - wtedy przy każdym uruchomieniu programu byłaby wywoływana inna funkcja.
Jak widać, za wirtualność się płaci - zarówno drobnym narzutem pamięciowym na każdy obiekt (identyfikator klasy), jak i drobnym narzutem czasowym (odnajdywanie przy każdym wywołaniu odpowiedniej klasy i jej funkcji składowej). Jednak zyskujemy możliwośc płynnego rozwoju naszego programu przez zastępowanie klas ich podklasami, co bez wirtualności jest niewykonalne. Przy możliwościach obecnych komputerów koszt wirtualności jest zaniedbywalny, ale wciąż warto przemyśleć, czy potrzebujemy wirtualności dla wszystkich funkcji.
[edytuj] Programowanie orientowane obiektowo
[edytuj] Obiekty stałe
[edytuj] Przeciążanie operatorów
Przeładowanie (przeciążanie) operatorów polega na nadaniu im nowych funkcji.
[edytuj] Użycie
Nie możemy przeładowywać operatorów dla typów wbudowanych (int, char, float).
Przeciążanie polega na zdefiniowaniu tzw. funkcji operatorowej. Identyfikatorem funkcji operatorowej jest zawsze słowo kluczowe operator, bezpośrednio po którym następuje symbol operatora
typ_zwracany operator@ (argumenty)
{
// operacje
}
np.: operator+, operator-, operator<< itd. Co najmniej jeden argument tej funkcji musi być obiektem danej klasy.
[edytuj] Operatory które można przeciążać
+ - * / % & | ^ << << ~ && || ! == != < <= > >= += -= *= /= %= &= |= ^= <<= >>= ++ -- = -> ->* () [] new delete ,
Trzeba zapamiętać że można przeciążać TYLKO te operatory. Próba przeciążania innych (tudzież tworzenia nowych jak np @) skończy się błędem kompilacji.
[edytuj] Przykład zastosowania
Mamy daną klasę Student
class Student
{
int nr_indeksu;
float srednia_ocen;
public:
Student(int nr=0, float sr=0) {nr_indeksu=nr; srednia_ocen=sr;} // konstruktor
};
i chcemy przeładować operator wyjścia <<
Robimy to w następujący sposób:
class Student
{
int nr_indeksu;
float srednia_ocen;
public:
Student(int nr=0, float sr=0) {nr_indeksu=nr; srednia_ocen=sr;} // konstruktor
friend ostream & operator<< (ostream &wyjscie, const Student &s);
};
ostream & operator<< (ostream &wyjscie, const Student &s)
{
wyjscie << "Nr indeksu : " <<s.nr_indeksu << endl << "Srednia ocen : " <<s.srednia_ocen<<endl;
return wyjscie;
}
Aby zobaczyć, jak to działa, wystarczy że funkcja main() będzie miała następującą postać:
int main()
{
Student st, stu(10,5);
cout << st; // wypisze nr indexu = 0, srednia ocen=0,
// poniewaz st to wartosci konstruktora domyślnego :)
cout << stu; // wypisze nr indexu = 10, srednia ocen=5
return 0;
}
W powyższym przykładzie wprowadzone zostało także nowe pojęcie - zaprzyjaźnianie (friend). Należy uzyć go wtedy, kiedy chcemy używać typów wprowadzonych przez inną bibliotekę. (tu przez iostream).
Ale weźmy przykład nieco prostszy. Chcemy sprawdzić czy to jest ten sam student - przeciążamy operator:
class Student
{
//...
public:
int operator==(Student & s, Student &q) {return s.nr_indeksu==q.nr_indeksu}; //to jest przeciez zle, w ciele klasy przeladowanie jest jedno argumentowe!
int operator==(Student & s, int &q) {return s.nr_indeksu==q}; //jw
};
I niby wszystko jest pięknie, ale tu zacznają się schody... My, jako twórcy klasy wiemy, że porównanie dotyczy się tylko i wyłącznie numeru indeksu. Przy różnych średnich i tych samych indeksach dostaniemy wynik pozytywny. A nuż niech ktoś sobie ubzdura, że == odnosi się do wszystkich składowych...
Dalsze zamieszanie wprowadzą kolejne zaproponowane przeze mnie operatory.
class Student
{
//...
public:
int operator< (Student & s, Student &q) {return s.srednia < q.srednia};
int operator< (Student & s, int &q) {return s.srednia < q};
// itd dla kolejnych operatorów.
};
Samo w sobie nie jest groźne. Dopiero przy konfrontacji z poprzednim operatorem zaczyna wprowadzać zamieszanie. Wszystko jest dobrze kiedy pamiętamy, jakie operacje dane operatory wykonują. Ale pamiętajmy: pamięć ludzka jest ulotna i ktoś inny (albo my) może spędzić kilka dni zanim dojdzie do tego, dlaczego to działa nie tak jak powinno.
Ale powyższy przykład wygląda naprawdę blado w porównaniu z tym:
class Student
{
//...
public:
int operator+ (Student & s, Student &q) {return (s.srednia + q.srednia +11) };
int operator+ (Student & s, int &q) {return (s.srednia - q / 30) };
};
Jak widzicie operator + wcale nas nie zmusza do wykonywania operacji dodawania. Możemy równie dobrze wewnątrz odejmować. A przy odejmowaniu porównywać. Lecz takie postępowanie nie jest intuicyjne. Takie postępowanie jest dozwolone jedynie w przypadkach, kiedy startujecie w konkursie na najbardziej nieczytelny kod.
Ale dość już straszenia. Teraz należy pokazać jak prawidłowo przeciążać operatory jednoargumentowe (++, --) oraz jak prawidłowo zwracać obiekt np. przy dodawaniu.
Oprócz operatorów arytmetycznych oraz działających na strumieniach można przeciążać również operatory logiczne.
[edytuj] Operatory "bool" i "!"
W języku C++ jest również możliwość przeciążania operatorów bool i !. Dzięki temu możemy w instrukcji warunkowej używać nazwy obiektu do testowania, czy spełnia on jakieś określone kryteria. Poniżej znajduje się prosty przykład, który to ilustruje:
#include <iostream> using namespace std; class TablicaInt { public: TablicaInt(int el) : Tab(new int[el]), L_elementow(el) {} operator bool() const {return (L_elementow != 0);} bool operator!() const {return (L_elementow == 0);} private: int * Tab; int L_elementow; }; int main() { int n = 5; TablicaInt tab(n); if(tab) cout << "Tablica nie jest pusta." << endl; if(!tab) cout << "Tablica jest pusta." << endl; TablicaInt tab2(0); if(tab2) cout << "Tablica nie jest pusta." << endl; if(!tab2) cout << "Tablica jest pusta." << endl; return 0; }
W efekcie na ekranie otrzymamy dwa wpisy:
Tablica nie jest pusta. Tablica jest pusta.
W pierwszym przypadku tablica zawierała niezerową liczbę elementów i prawdę zwrócił operator bool. W drugim natomiast liczba elementów wynosiła zero i prawdę zwrócił operator !.
[edytuj] Operator "[]"
W niektórych przypadkach bardzo przydatny jest operator indeksu []. Można go przeciążyć oczywiście w dowolny sposób, ale chyba najbardziej intuicyjne jest przypisanie mu funkcji dostępu do konkretnego elementu np. w tablicy.
Posługując się przykładem klasy TablicaInt możemy dopisać do niej następujące dwa operatory:
int & operator[](int el) {return Tab[el];} const int & operator[](int el) const {return Tab[el];}
oraz testową funkcję main
int main() { int n = 5; TablicaInt tab(n); for(int i = 0; i < n; ++i) { tab[i] = i; cout << tab[i] << endl; } return 0; }
W ogólności te operatory nie muszą wykonywać dokładnie tych samych czynności. Można sobie wyobrazić przykład w którym operator do zapisu zapisuje coś do tablicy, a w przypadku braku miejsca alokuje dodatkową pamięć. Operator stały nie będzie posiadał takiej funkcjonalności ponieważ nie może zmieniać obiektu na rzecz którego został wywołany.
[edytuj] Operator "()"
Ten operator służy do tworzenia tzw. funktorów czyli klas które naśladują funkcje:
class Foo { public: int operator() (int a, int b) { return (a+b); } };
Nie jest to może najmądrzejszy przykład gdyż jest dostępny do przeciążania operator "+" ale oddaje zasadę działania. Trzeba zaznaczyć że ten operator może zwracać dowolną wartość oraz przyjmować dowolną liczbę parametrów dowolnego typu. Niestety musi być zadeklarownay jako niestatyczna metoda klasy (gdyż inne operatory które mogą być statyczne zwracają obiekt (&Foo operator...), choć możliwość udawania przez klasę funkcji z pewnością to wynagrodzi.
[edytuj] Konwersje obiektów
[edytuj] Klasy i typy zagnieżdżone
[edytuj] Dziedziczenie wielokrotne
Dziedziczenie wielokrotne stanowi właściwość niektórych obiektowych języków programowania w których klasa może dziedziczyć metody i pola z więcej niż jednej klasy bazowej.
Języki wspierające dziedziczenie wielokrotne to: Eiffel, C++, Python, Perl, Curl, Common Lisp (via CLOS).
Część 4
Zawansowane konstrukcje językowe
[edytuj] Obsługa wyjątków
[edytuj] Wstęp
Wyjątki pozwalają reagować na różne sytuacje wyjątkowe. Używa się ich tam gdzie istnieje ryzyko wystąpienia wyjątku.
[edytuj] Zarys wyjątków
Jeżeli w jakimś miejscu programu zajdzie nieoczekiwana sytuacja, programista piszący ten kod powinien zasygnalizować o tym. Dawniej polegało to na zwróceniu specyficznej wartości, co nie było zbyt szczęśliwym rozwiązaniem, bo sygnał musiał być taki jak wartość zwracana przez funkcję. W przypadku obsługi sytuacji wyjątkowej mówi się o obiekcie sytuacji wyjątkowej, co często zastępowane jest słowem "wyjątek". W C++ wyjątki się "rzuca", służy do tego instrukcja throw.
[edytuj] Szkielet obsługi wyjątków
Tam gdzie spodziewamy się wyjątku umieszczamy blok try, w którym umieszczamy "podejrzane" instrukcje. Za tym blokiem muszą (tzn. musi przynajmniej jedna) pojawić się bloki catch. Wygląda to tak:
//jakaś zwykła funkcja, lub funkcja main
try // w instrukcjach poniżej może coś się nie udać
{
fun();
fun2(); //podejrzane funkcje
}
catch(char obj)
{
//tu coś robimy, na przykład piszemy o błędzie
}
W instrukcji catch umieszczamy typ jakim będzie wyjątek. Rzucić możemy wyjątek typu int, char i inne, dlatego tu określamy co nas interesuje. Nazwa tego obiektu nie jest konieczna, ale jeżeli chcemy znać wartość musimy ten obiekt nazwać. Bloków catch może być więcej, najczęściej tyle ile możliwych typów do złapania. Co ważne jeżeli rzucimy wyjątek konkretnego typu to "wpadnie" on do pierwszego dobrego catch nawet jeżeli inne nadają się lepiej (podobnie jak z instrukcjami if else). Dotyczy to zwłaszcza klas dziedziczonych. Przykładowo, jeżeli mamy klasę Pies, która dziedziczy z klasy Zwierze, to jeśli pierwszy pojawi się blok:
catch(Zwierze obj)
to on zostanie użyty do obsługi wyjątku.
Zawsze dobrze jest się zabezpieczyć blokiem
catch(...)
Taki blok łapie wszystko. Dlatego kompilator nie dopuści by wszystko łapiący catch był przed innymi instrukcjami catch.
[edytuj] Rzucanie wyjątku
Pisząc funkcję możemy stwierdzić że coś poszło nie tak i chcemy zasygnalizować wyjątek. Jak to zrobic przedstawia kod:
double Dziel(double a, double b) //funkcja zwraca iloraz a / b
{
if(b == 0) //przez zero się nie dzieli
throw "dzielenie przez zero!"; //rzucamy wyjątek
return a / b;
}
Po instrukcji throw umieszczamy obiekt który chcemy rzucić (u nas jest to char*). W tym miejscu działanie funkcji jest natychmiast przerywane i nasz łańcuch znaków wędruje do bloków catch.
Pełny program ilustrujący wyjątki:
#include<iostream>
using namespace std;
double Dziel(double a, double b) //funkcja zwraca iloraz a / b
{
if(b == 0) //przez zero się nie dzieli
throw "dzielenie przez zero!"; //rzucamy wyjątek
return a / b;
}
int main()
{
try
{
Dziel(10, 0);
}
catch(const char* w)
{
cout<<"Wyjatek: "<<w;
}
cin.get();
return 0;
}
Jak widać, utworzony jest tylko jeden blok catch, a to dlatego że funkcja Dziel rzuca tylko wyjątki typu const char*. Dobrym zwyczajem jest pisanie obok deklaracji funkcji jakie wyjątki może ona rzucać:
void fun(int aa) throw(char)
zapis ten oznacza że funkcja fun może zwrócić wyjątek typu char.
[edytuj] Szablony funkcji
[edytuj] Szablon funkcji
Szablon funkcji pozwala stworzyć wiele funkcji różniących się tylko typem argumentów przyjmowanych. Załóżmy, że chcielibyśmy napisać funkcję pisz, której jedynym argumentem byłaby zmienna, którą chcemy wypisać. Aby móc wypisywać wiele typów zmiennych możemy skorzystać z przeładowania (inna nazwa na przeciążenie) funkcji.
void pisz(char a)
{
cout<<a;
}
void pisz(double a)
{
cout<<a;
}
void pisz(int a)
{
cout<<a;
}
Można tak wiele razy przeładowywać funkcję pisz, ale można też wskazać kompilatorowi w jaki sposób może stworzyć nowe funkcje. Są to właśnie szablony. Szablon naszej funkcji wyglądałby tak:
template <class typ> void pisz(typ a)
{
cout<<a;
}
Pierwszą linijkę można też złamać po drugim nawiasie '>':
template <class typ>
void pisz(typ arg)
Pierwszym słowem jest template czyli szablon. Następnie w ostrych nawiasach umieszcza się słowo class - które może oznaczać dowolny typ danych. Można także stosować słowo typedef przy zagnieżdżaniu szablonów, tak aby uniknąć nieprecyzyjności. Można także stosować nazwy typów, takich jak int czy char Nazwa użyta po słowach kluczowych zastępuje w deklaracji funkcji typ argumentu.
[edytuj] Szablony klas
[edytuj] Czym są?
Szablony (wzorce) są czymś podobnym do makr, tyle że wykonywane są przez kompilator, a nie przez preprocesor. Cechują je pewne własności, których jednak nie ma preprocesor, np. można tworzyć rekurencyjne wywołania. Ale zobaczmy najpierw taką sytuację: chcemy utworzyć klasę o nazwie Punkt o trzech współrzędnych: x, y, z. Potrzebujemy trzy różne implementacje. Jedna ma działać na liczbach typu unsigned int, druga na liczbach typu int, a trzecia na float. Pierwsze, o czym pomyślimy, to napisać coś takiego (jeśli dostrzeżesz błąd, to się nie przejmuj):
// ... class PunktUInt { public: PunktUInt( unsigned _x, unsigned _y, unsigned _z ) : x(_x), y(_y), z(_x) { } unsigned x, y, z; }; // ...
I potem co? kopiujemy i zmieniamy unsigned na int:
// ... class PunktInt { public: PunktInt( int _x, int _y, int _z ) : x(_x), y(_y), z(_x) { } int x, y, z; }; // ...
Następnie zamieniamy na float:
// ... class PunktFloat { public: PunktFloat( float _x, float _y, float _z ) : x(_x), y(_y), z(_x) { } float x, y, z; }; // ...
Uff! Wreszcie napisaliśmy - pomyślisz sobie. Jednak pisanie wymaga trochę wysiłku. No dobrze, teraz tworzymy funkcję main:
int main(void) { PunktInt A(0,-10,0); PunktUInt B(0,10,5); std::cout << "A(" << A.x << "," << A.y << "," << A.z << ")" << std::endl; std::cout << "B(" << B.x << "," << B.y << "," << B.z << ")" << std::endl; }
I oto po naszych ciężkich staraniach otrzymujemy dość ciekawy i niespodziewany wynik:
A(0,-10,0) B(0,10,0)
Ojej! - zapewne krzykniesz. Musiał gdzieś się tu zjawić błąd. Trzeba znaleźć go. Aha, mamy go:
// ... PunktUInt( unsigned _x, unsigned _y, unsigned _z ) : x(_x), y(_y), '''z(_x)''' // ...
Zamiast z(_x) powinno być z(_z). I trzeba teraz wszystko poprawiać, w tym także resztę funkcji... Dobrze, że to tylko tyle. Ale na szczęście C++ daje nam prostszy sposób.
[edytuj] Wykorzystywanie szablonów
Napiszmy teraz jeszcze raz nasz program. Tym razem z wykorzystaniem szablonów:
// ... template <class Typ> class Punkt { public: Punkt( Typ _x, Typ _y, Typ _z ) : x(_x), y(_y), z(_x) { } Typ x, y, z; }; // ...
Za pomocą template<class Typ> tworzymy nasz szablon. Parametrem jest typ, jaki chcemy użyć, tworzymy go poprzez <class Typ>. Teraz, aby utworzyć nasze punkty możemy zapisać:
Punkt<int> A(0,-10,0); Punkt<unsigned> A(0,10,5);
Czyli nasz main będzie wyglądał tak:
int main(void) { Punkt<int> A(0,-10,0); Punkt<unsigned> B(0,10,5); std::cout << "A(" << A.x << "," << A.y << "," << A.z << ")" << std::endl; std::cout << "B(" << B.x << "," << B.y << "," << B.z << ")" << std::endl; }
Ale nie podoba nam się ta notacja, bo na przykład program za bardzo zaczyna nam przypominać HTML. Co mamy zrobić? Nic innego, jak tylko przed funkcją main skorzystać z typedef:
typedef Punkt<int> PunktInt; typedef Punkt<unsigned> PunktUInt; typedef Punkt<float> PunktFloat;
I tyle. Main zamieni nam się wtedy w pierwotną formę:
int main(void) { PunktInt A(0,-10,0); PunktUInt B(0,10,5); std::cout << "A(" << A.x << "," << A.y << "," << A.z << ")" << std::endl; std::cout << "B(" << B.x << "," << B.y << "," << B.z << ")" << std::endl; }
- "No dobra, ale jak teraz uruchomię ten program to nadal mam ten sam, zły wynik" - powiesz. I masz rację, bo zobaczysz:
A(0,-10,0) B(0,10,0)
Powód jest ten sam co poprzedni.
Punkt( Typ _x, Typ _y, Typ _z )
: x(_x), y(_y), z(_x)
{ }
Musimy zamienić z(_x) na z(_z) i będzie wszystko w porządku. Tylko tyle. Nie wierzysz? Ale to jest prawda. To zobacz cały nasz program powinien wyglądać w ten sposób:
#include <iostream> template <class Typ> class Punkt { public: Punkt( Typ _x, Typ _y, Typ _z ) : x(_x), y(_y), z(_z) { } Typ x, y, z; }; typedef Punkt<int> PunktInt; typedef Punkt<unsigned> PunktUInt; typedef Punkt<float> PunktFloat; int main(void) { PunktInt A(0,-10,0); PunktUInt B(0,10,5); std::cout << "A(" << A.x << "," << A.y << "," << A.z << ")" << std::endl; std::cout << "B(" << B.x << "," << B.y << "," << B.z << ")" << std::endl; }
[edytuj] Szablony z wieloma parametrami
Szablon może także posiadać więcej niż jeden parametr. Na przykład chcielibyśmy napisać klasę Para, zawierającą dwa elementy pierwszy o nazwie pierwszy, a drugi o nazwie drugi, jednakże nie wiemy jakie mają one mieć typ. Możemy to zrobić w ten sposób:
#include <iostream> template <class T1, class T2> class Para { public: Para() { } Para( T1 _a, T2 _b ) : pierwszy(_a), drugi(_b) { } T1 pierwszy; T2 drugi; }; int main(void) { // tworzymy nasz obiekt Para<char*,int> zmienna("Liczba",10); std::cout << zmienna.pierwszy << " " << zmienna.drugi << std::endl; return 0; }
Za pomocą template<class T1, class T2> utworzyliśmy szablon o dwóch parametrach.
[edytuj] Można też liczby
Szablonem może być także liczba. Zilustrujmy to przykładem:
#include <iostream> template <class T, int N> class Tablica { public: Tablica() { } T &operator[]( int i ) { return tabl[i]; } private: T tabl[N]; }; int main(void) { Tablica<int,10> A; for ( int i=0; i<10; ++i ) { A[i]=100+i; } for ( int i=0; i<10; ++i ) { std::cout << "A[" << i << "]=" << A[i] << std::endl; } return 0; }
[edytuj] Wskażniki do elementów składowych
Dodatek A
Biblioteka STL
[edytuj] Filozofia
[edytuj] String
[edytuj] String
Napisy w stylu języka C są częstą przyczyną błędów a na dodatek ich używanie jest dosyć kłopotliwe. Nic wiec dziwnego że biblioteka standardowa posiada zaimplementowaną uogólnioną klasę napisów zwaną string. Taka klasa daje jednolity, niezależny od systemu i bezpieczny interfejs do manipulowania napisami.
Aby móc korzystać z klasy string należy dołączyć plik nagłówkowy:
#include <string>
Tworzyć nowe obiekty tego typu możemy następująco:
string napis1;
napis1 = "text";
string napis2( "text" );
string napis3 = "text";
cout << napis1 << endl
<< napis2 << endl
<< napis3 << endl;
string napis4(10,'X');
cout << napis4;
Uwaga: Aby kompilator widział typ "string" należy powiadomić go, w jakiej przestrzeni nazw ten typ się znajduje, np. w ten sposób:
using namespace std;
lub bardziej elegancko i bezpieczniej:
using std::string;
Powyższy kod da w wyniku:
text text text XXXXXXXXXX
Klasa string ma zdefiniowanych bardzo wiele operatorów, co ułatwia niektóre działania na napisach. Dla przykładu dawniej aby skopiować napis do innego z napisu trzeba było używać funkcji strcpy(). W implementacji klasy string wystarczy operator przypisania '=' :
string a, b; a = '1'; b = '2'; a = b; cout << a;
Mamy też takie operatory jak ==, !=, + i indeksowy []:
string a,b,c;
a = "Europa";
b = "Afryka";
c = "Europa";
if (a == c)
cout << "takie same\n";
if (a > b)
cout << "napis a jest wiekszy\n";
else
cout << "napis a nie jest wiekszy\n";
b = b + 'a';
if (a > b)
cout << "napis a jest wiekszy\n";
else
cout << "napis a nie jest wiekszy\n";
if (a != b)
cout << "rozne\n" ;
b[0] = '_';
cout << b;
Po czym w konsoli zobaczymy:
takie same napis a jest wiekszy napis a jest wiekszy rozne _frykaa
Jak widać manipulacje obiektami string są bardzo wygodne. Oprócz wygodnych w stosowaniu operatorów klasa string posiada jeszcze więcej metod.
| Metoda | Opis |
| empty() | Zwraca wartość true jeżeli napis jest pusty. |
| size(),length() | Zwraca ilość znaków w napisie. |
| at() | Zwraca znak o podanym położeniu, podobnie jak operator [] z tym że ta metoda jest bezpieczniejsza, zapobiega wyjściu poza zakres. |
| clear(),erase() | Usuwa wszystkie znaki z napisu, erase() może usuwać wybrane znaki. |
| find() | Znajduje podciąg w ciągu, są też bardziej rozbudowane funkcje tego typu. |
| swap() | Zamienia miejscami dwa stringi, a staje się b, a b staje się a. |
| substr( int start_index, int długość ) | Zwraca podciąg na podstawie indeksu początkowego i długości podciągu. |
| append() | Dodaje zadany napis na końcu istniejącego ciągu. |
| c_str() | Zwraca napis w stylu języka C (stały wskaźnik do pierwszego elementu). |
Omówione dotychczas operatory i metody to tylko część dostępnych, tu wymieniłem tylko te najczęściej używane. Teraz przedstawię różnice jakie występują między bibliotekami C a C++ w obsłudze napisów.
| C | C++ |
| strcpy(a,b) | a = b |
| strcmp(a,b) | a == b |
| strcat(a,b) | a += b |
| strlen(a) | a.size() |
| strstr(a,b) | a.find(b) |
[edytuj] Vector
[edytuj] Vector
Klasa vector reprezentuje tablicę standardową. Dzięki niej możemy tworzyć tablice dowolnych typów. Tak jak int a[10] jest tablicą dziesięciu elementów typu int tak vector może być dowolną tablicą, dowolnego elementu i dowolnej jej liczby. Aby łatwiej sobie przyswoić klasę vector porównam ją do działań wykonywanych na normalnej tablicy znaków.
char tab[10]; tab[0] = 'a'; for (short i = 1; i < 10; i++) tab[i] = 'b';
Tak to by wyglądało w stylu języka C, a tak używając klasy vector. Musimy jeszcze tylko dołączyć plik nagłówkowy <vector>. Korzystając z możliwości tej klasy teraz stworzymy tablice elementów typu string a nie char, ponieważ ten typ nas bardzo ogranicza.
vector< string > tab(10); tab[0] = "a"; for (vector< string >::size_type i = 1; i < tab.size(); ++i) tab[i] = "dluzsze napisy";
Składowa vector<T>::size_type jest aliasem do typu właściwego dla indeksowania elementów wektora - gwarantuje, że typ będzie miał wystarczający zakres, żeby obsłużyć całą długość wektora. Innymi słowy - rozmiar wektora (vector<T>::size()) jest właśnie typu size_type. Użycie size() zamiast stałej gwarantuje nam, że zmieniając rozmiar bufora zawsze przeiterujemy go do końca.
Obiekt vector ma kilka odmian konstruktorów. Możemy stworzyć wektor pusty, a później dodawać do niego elementy. Wtedy wektor sam będzie sie powiększał. Nie musimy znać rozmiaru podczas tworzenia obiektu.
vector < typ_elemetow > nazwa_tablicy;
Możemy też podać wielkość, co wcale nas nie ogranicza do tej wielkości. Jeżeli zabraknie miejsca obiekt sam sie powiększy.
vector < int > tab(20);
Myślę że może sie przydać jeszcze wersja konstruktora która podaje liczbę elementów i wartość jaką ma mieć każdy z nich.
vector < string > tablica( 20, "przykladowy tekst dla kazdego elementu");
Ta tablica będzie miała dwadzieścia elementów z czego wszystkie mają wartość: "przykladowy tekst dla kazdego elementu". Jeżeli podamy ilość elementów wtedy należy do nich odwoływać sie za pomocą operatora indeksowego [], lub czuwającą żeby nie wyjść poza zakres elementów metodą at(). Natomiast jeżeli nie podamy ilości wtedy trzeba najpierw utworzyć każdy kolejny element, czynimy to metodą push_back(). Ta metoda dodaje nowy element na koniec tablicy. Po dodaniu nowych elementów możemy sie do nich odwoływać indeksowo []. Możemy też sprawdzić ile obecnie jest elementów metodą size(), a metoda empty() powie nam czy przypadkiem wektor nie jest pusty. Dalsze wnikanie w możliwości klasy vector wymaga poznania iteratorów – inaczej wskaźniki. Jak wiemy do poruszania sie po elementach tablicy takich jak int a[10] można używać wskaźnika jakim jest a. Tak samo operacje na obiektach wektora można dokonać używając iteratorów i algorytmów uogólnionych. Rzućmy wiec okiem na te iteratory. Deklaracja iteratora wygląda podobnie do deklaracji wektora. Zadeklarujemy wektor 10 elementów typu string i dwa iteratory do niego. Pierwszy będzie wskazywał na pierwszy element, a drugi na pierwszy po ostatnim. Do uzyskania takich iteratorów służą metody begin() i end(), które zwracają iterator odpowiednio do pierwszego i następnego po ostatnim (nie ostatniego) elementu.
vector<string> tabStr(10,"0"); vector<string>::iterator iter1; vector<string>::iterator iter2; iter1 = tabStr.begin(); iter2 = tabStr.end();
Mając takie iteratory możemy sie nimi posługiwać na wzór standardowych wskaźników. Można go wyłuskać, używać operatorów arytmetycznych ++, -- i logicznych. Poniższy program to ilustruje.
#include <string>
#include <iostream>
#include <vector>
using namespace std;
int main(int argc, char *argv[])
{
vector<string> tabStr(10,"0");
vector<string>::iterator iter1;
vector<string>::iterator iter2;
iter1 = tabStr.begin();
iter2 = tabStr.end();
for (int a = 0; iter1 != iter2; ++iter1, ++a )
{
if (a % 2 == 0)
*iter1 = "1";
cout << *iter1 << endl;
}
cin.get();
return 0;
}
Wynik działania:
1 0 1 0 1 0 1 0 1 0
Ale iteratory są o wiele bardziej skomplikowane niż tu zostało to przedstawione. Jest pięć rodzajów iteratorów ogólnych, bo każda klasa taka jak vector ma w sobie zdefiniowany iterator. Iteratory będziemy wykorzystywać prawie w każdym kontenerze (struktura do przechowywania danych) jakim jest np. vector. Teraz, kiedy już wiemy, co to są iteratory, możemy przejść do metod klasy vector i algorytmów uogólnionych, które działają najlepiej na tych iteratorach. Klasa vector nie ma sama w sobie zbyt wiele metod do ciekawych operacji, ale tego braku uzupełnieniem jest zbiór algorytmów uogólnionych, o których będę pisał później.
[edytuj] Metody
Lista metod klasy vector (gdzie iterator to vector<T>::iterator - podmiana dla czytelności):
| prototyp | opis działania |
|---|---|
| void swap(vector vec) | zamienia zawartości dwóch wektorów miejscami (wykonywane szybko, w stałym czasie) |
| size_t size() | zwraca obecną ilość elementów wektora. |
| size_t capacity() | zwraca ilość elementów, którą wektor jest w stanie pomieścić przed przeniesieniem go do większego obszaru pamięci. |
| size_t max_size() | zwraca ilość elementów, którą maksymalnie może pomieścić wektor |
| bool empty() | zwraca true jeśli wektor nie przechowuje żadnych zmiennych |
| void reserve(size_t n) | rezerwuje pamięć na n elementów, co zapobiega przenoszeniu wektora w pamięci przed osiągnięciem tej liczby |
| void resize(size_t n, T obj) | zmienia rozmiar wektora do n; jeśli jest większy od obecnego, dodawane są nowe elementy będące kopiami obj |
| void resize(size_t n) | zmienia rozmiar wektora do n; jeśli jest większy od obecnego, dodawane są nowe elementy o przypadkowych wartościach |
| void push_back(const T obj) | dodaje na końcu wektora kopię przekazanego argumentu |
| void pop_back() | usuwa ostatni element z wektora (wykonywane w stałym czasie) |
| void clear() | usuwa wszystkie elementy z wektora |
| void assign(size_t n, const T obj) | czyści wektor i wypełnia go n kopiami argumentu obj |
| iterator assign(iterator poczatek, iterator koniec) | czyści wektor i wypełnia go elementami z innego wektora z przedziału <poczatek;koniec> |
| iterator insert(iterator pos, T obj) | wstawia element obj przed wskazywaną przez iterator pos pozycją i zwraca iterator do dostawionego elementu (powolna funkcja) |
| void insert(iterator pos, size_t n, const T obj) | wstawia n kopii argumentu obj przed pozycją wskazywaną przez iterator pos |
| void insert(iterator pos, iterator poczatek, iterator koniec) | wstawia przed pozycją wskazywaną przez iterator pos elementy między iteratorami początek i koniec (włącznie) |
| iterator erase(iterator pos) | usuwa element wskazywany przez pos i zwraca iterator do następnego elementu |
| iterator erase(iterator poczatek, iterator koniec) | usuwa elementy z przedziału <poczatek;koniec> i zwraca iterator do elementu za nimi |
| T& front() | zwraca referencję do pierwszego elementu wektora |
| T& back() | zwraca referencję do ostatniego elementu wektora |
| iterator begin() | zwraca iterator do pierwszego elementu wektora (często mylone z front()) |
| iterator end() | zwraca iterator ustawiony za ostatnim elementem wektora |
| iterator rbegin() | zwraca odwrócony iterator do pierwszego elementu |
| iterator rend() | zwraca odwrócony iterator do ostatniego elementu |
[edytuj] Set
[edytuj] Opis
Zbiory są jednym z kontenerów biblioteki STL, których struktura oparta jest na drzewach. Elementy które są w nich przechowywane są posortowane, według pewnego klucza. Zarówno typ wartosci tych elementów jak i typ wartości klucza są takie same. Drzewiasta struktura zapewnia szybkie wyszukiwanie, jednak są z tym związane także pewne mankamenty, mianowicie modyfikacja elementu jest możliwa tylko w taki sposób, że kasujemy stary element, a następnie wstawiamy w to miejsce nowy. Korzystając z terminologii STL-a zbiory sa tzw. kontenerami asocjacyjnymi (o zmiennej długości, pozwalającymi na operowanie elementami przy użyciu kluczy):
| Rodzaj kontenera | Opis | |
|---|---|---|
| 1 | prostymi | key_type i value_type są tego samego typu |
| 2 | posortowanymi | rosnąco względem klucza |
| 3 | unikalnymi | w danym zbiorze nie mogą wsytępowac dwa elementy o tym samym kluczu |
[edytuj] Parametry
| Parametr | Opis |
|---|---|
| Key | Klucz zbioru ten sam co typ wartości. Zdefiniowane także jako: set::key_type i set::value_type |
| Compare | Funkcja porównująca klucze, według słabo rosnącego porządku którego argumenty są typu key_type;
zwraca true jeśli pierwszy argument jest wcześniejszy od drugiego, w przeciwnym razie zwraca false. Zdefiniowana także jako: set::key_compare i set::value_compare. |
| Alloc | Alokator zbioru, używany do wewnętrznego zarządzania pamięcią. |
[edytuj] Prosty Przykład
#include <iostream>
#include <string>
#include <set>
using namespace std;
int main()
{
set<string> mapa; //deklaracja zbioru mapa
mapa.insert("lublin");
mapa.insert("lodz");
mapa.insert("warszawa");
mapa.insert("krakow"); //dodawanie elem
set<string>::iterator marker = mapa.find("warszawa"); //szuka elementu "warszawa"
if (marker!=mapa.end())
{
cout<<"znalazlem! "<<*marker<<endl;
}
for (marker=mapa.begin(); marker!=mapa.end(); marker++)
cout << *marker << endl;
system("pause");
return 0;
}
[edytuj] Składniki
| Składnik | Opis działania |
|---|---|
| value_type | Typ obiektu, T, przechowywany w zbiorze(secie). |
| key_type | Typ klucza zbioru, powiązany z value_type. |
| key_compare | Funkcjonał porównujący dwa klucze w celu uporządkowania. |
| value_compare | Funkcjonał porównujący dwie wartości w celu uporządkowania. |
| pointer | Wskaźnik na T. |
| reference | Referencja do T. |
| const_reference | Stała referencja do T. |
| size_type | Integralny typ bez znaku. |
| difference_type | Integralny typ ze znaku. |
| iterator | Iterator używany do 'chodzenia' po zbiorze. |
| const_iterator | Stały iterator używany do 'chodzenia'(iteracji) po zbiorze(tego samego typu co zwykły iterator). |
| reverse_iterator | Iterator używany do 'chodzenia do tyłu'(iteracji wstecznej) po zbiorze. |
| const_reverse_iterator | Stały iterator używany do 'chodzenia do tyłu'(iteracji wstecznej) po zbiorze. |
| iterator begin() | Zwraca iterator wskazujący na początek zbioru. |
| iterator end() | Zwraca iterator wskazujący na koniec zbioru. |
| reverse_iterator rbegin() | Zwraca reverse_iterator wskazujący na początek odwróconego zbioru. |
| reverse_iterator rend() | Zwraca reverse_iterator wskazujący na koniec odwróconego zbioru. |
| size_type size() | Zwraca rozmiar zbioru. |
| size_type max_size() | Zwraca największy możliwy rozmiar zbioru. |
| bool empty() | Zwraca true gdy rozmiar zbioru wynosi 0. |
| key_compare key_comp() | Zwraca key_compare obiektu używanego przez zbiór. |
| value_compare value_comp() | Zwraca value_compare obiektu używanego przez zbiór. |
| set() | Tworzy pusty zbiór. |
| set(const key_compare& comp) | Tworzy pusty zbiór, używając comp jako key_compare obiektu(klucza porównującego). |
| template <class InputIterator>
set(InputIterator f, InputIterator l) |
Tworzy zbiór zawierający kopie zakresu. |
| template <class InputIterator>
set(InputIterator f, InputIterator l, const key_compare& comp) |
Tworzy zbiór zawierający kopie zakresu, używając comp jako key_compare obiektu. |
| set(const set&) | Konstruktor kopiujący. |
| set& operator=(const set&) | Operator przypisania. |
| void swap(set&) | Zamienia zawartość dwóch zbiorów. |
| pair<iterator, bool>
insert(const value_type& x) |
Wstawia do zbioru element x. |
| iterator insert(iterator pos, const value_type& x) | Wstawia do zbioru element x. Używa pos do wskazania miejsca, w którym ma być wstawiony. |
| template <class InputIterator>
void insert(InputIterator, InputIterator) |
Wstawia do zbioru zakres. |
| void erase(iterator pos) | Kasuje element wskazywany przez pos. |
| size_type erase(const key_type& k) | Kasuje element którego kluczem jest k. |
| void erase(iterator first, iterator last) | Kasuje wszystkie elementy z danego zakresu. |
| void clear() | Kasuje wszystkie elementy zbioru. |
| iterator find(const key_type& k) const | Znajduje element, którego klucz wynosi k. |
| size_type count(const key_type& k) const | Zwraca liczbę elementów, których klucz wynosi k. |
| iterator lower_bound(const key_type& k) const | Znajduje pierwszy element, którego klucz jest nie mniejszy od k. |
| iterator upper_bound(const key_type& k) const | Znajduje pierwszy element, którego klucz jest większy od k. |
| pair<iterator, iterator>
equal_range(const key_type& k) const |
Znajduje zakres zawierający wszystkie elementy o kluczu k. |
| bool operator==(const set&, const set&) | Sprawdza dwa zbiory pod względem równości. Jest to funkcja globalna, nie składnikowa. |
| bool operator<(const set&,const set&) | Porównanie leksykograficzne. Jest to funkcja globalna, nie składnikowa. |
[edytuj] Map
[edytuj] Opis
Mapa to posortowany kontener asocjacyjny, czyli zbiornik o zmiennej długości gromadzący dane, które można dodawać i usuwać. Nie można jednak dodawać danych na konkretną pozycje, ponieważ kolejność ustalana jest wg danej klucz. Mapa jest również parowym zbiornikiem asocjacyjnym, czyli jej elementami są pary wartości klucz i dana. Pierwszej wartości key_type, czyli klucza mapy, nie można zmieniać, natomiast druga wartość danej jest przypisywalna (np.(*i).second=2). Mapa jest w końcu unikalnym kontenerem asocjacyjnym, co oznacza, że żadne dwa elementy nie mają tego samego klucza.
Mapa zdefiniowana jest w standardowym nagłówku map oraz w niestandardowym, wstecznie kompatybilnym nagłówku map.h.
| Parametr | Opis działania | Domyślny |
|---|---|---|
| Key | Typ klucza mapy. Jest też definiowany jako map::key_type. | |
| Data | Typ danych mapy. Jest też definiowany jako map::data_type. | |
| Compare | Funkcja porównująca klucze, w porządku ostro malejącym którego typ argumentu to key_type;
zwraca true jeśli pierwszy argument jest mniejszy od drugiego argumentu, w przeciwnym razie zwraca false. Jest też definiowany jako map::key_compare. |
less<Key> |
| Alloc | Alokator mapy, wykorzystywany do zarządzania pamięcią wewnętrzną. | alloc |
- Data jest zgodna w sensie przypisania.
- Compare jest ostro malejącym porządkiem, którego argumenty są typu Key.
- Alloc jest alokatorem.
[edytuj] Przykład
#include<iostream>
#include<map>
using namespace std;
int main()
{
map<const int, string> tygodnie;
tygodnie[1] = "niedziela";
tygodnie[2] = "poniedzialek";
tygodnie[3] = "wtorek";
tygodnie[4] = "sroda";
tygodnie[5] = "czwartek";
tygodnie[6] = "piatek";
tygodnie[7] = "sobota";
cout << "trzeci dzien tygodnia: " << tygodnie[3] << endl;
map<const int, string>::iterator cur = tygodnie.find(3);
map<const int, string>::iterator prev = cur;
map<const int, string>::iterator next = cur;
++next;
--prev;
cout << "Wczesniejszy: " << prev->second << endl;
cout << "Nastepny: " << next->second << endl;
}
[edytuj] Składniki
| Składnik | Opis działania |
|---|---|
| key_type | Typ klucza mapy, Key. |
| data_type | Typ obiektów powiązanych z kluczem. |
| value_type | Typ obiektu, pair<const key_type, data_type>, magazynowanego w mapie. |
| key_compare | Obiekt funkcyjny, który porównuje dwa klucze w celu ustalenia porządku. |
| value_compare | Obiekt funkcyjny, który porównuje dwa klucze w celu ustalenia porządku. |
| pointer | Wskaźnik na T. |
| reference | Referencja do T. |
| const_reference | Stała referencja do T. |
| size_type | Integralny typ bez znaku. |
| difference_type | Integralny typ ze znakiem. |
| iterator | Iterator używany do iteracji poprzez mapę. |
| const_iterator | Stały iterator używany do iteracji poprzez mapę. |
| reverse_iterator | Iterator używany do iteracji wstecznej poprzez mapę. |
| const_reverse_iterator | Stały iterator używany do iteracji wstecznej poprzez mapę. |
| iterator begin() | Zwraca iterator wskazujący na początek mapy. |
| iterator end() | Zwraca iterator wskazujący na koniec mapy. |
| const_iterator begin() const | Zwraca const_iterator wskazujący na początek mapy. |
| const_iterator end() const | Zwraca const_iterator wskazujący na koniec mapy. |
| reverse_iterator rbegin() | Zwraca reverse_iterator wskazujący na początek wstecznej mapy. |
| reverse_iterator rend() | Zwraca reverse_iterator wskazujący na koniec wstecznej mapy. |
| const_reverse_iterator rbegin() const | Zwraca const_reverse_iterator wskazujący na początek wstecznej mapy. |
| const_reverse_iterator rend() const | Zwraca const_reverse_iterator wskazujący na koniec wstecznej mapy. |
| size_type size() const | Zwraca rozmiar mapy. |
| size_type max_size() const | Zwraca najwiekszy możliwy rozmiar mapy. |
| bool empty() const | Zwraca true jeśli rozmiar mapy to 0. |
| key_compare key_comp() const | Zwraca key_compare obiekt używany przez mapę. |
| value_compare value_comp() const | Zwraca value_compare obiekt używany przez mapę. |
| map() | Tworzy pustą mapę. |
| map(const key_compare& comp) | Tworzy pustą mapę, używając comp jako obiektu key_compare. |
| template <class InputIterator>
map(InputIterator f, InputIterator l) |
Tworzy mapę z kopią zakresu. |
| template <class InputIterator>
map(InputIterator f, InputIterator l, const key_compare& comp) |
Tworzy mapę z kopią zakresu, używając comp jako obiektu key_compare. |
| map(const map&) | Konstruktor kopiujący. |
| map& operator=(const map&) | Operator przypisania. |
| void swap(map&) | Zamienia zawartość dwóch map. |
| pair<iterator, bool>
insert(const value_type& x) |
Wstawia x w mapę. |
| iterator insert(iterator pos,
const value_type& x) |
Wstawia x w mapę, używając pos jako wskazówki gdzie ma być wstawione. |
| template <class InputIterator>
void insert(InputIterator, InputIterator) |
Wstawia zakres w mapę. |
| void erase(iterator pos) | Usuwa element wskazywany przez pos. |
| size_type erase(const key_type& k) | Usuwa element, którego kluczem jest k. |
| void erase(iterator first, iterator last) | Usuwa wszystkie elementy z zakresu. |
| void clear() | Usuwa wszystkie elementy. |
| iterator find(const key_type& k) | Znajduje element, którego kluczem jest k. |
| const_iterator find(const key_type& k) const | Znajduje element, którego kluczem jest k. |
| size_type count(const key_type& k) | Zlicza elementy, których kluczem jest k. |
| iterator lower_bound(const key_type& k) | Znajduje pierwszy element, którego klucz jest nie mniejszy niż k. |
| const_iterator lower_bound(const key_type& k) const | Znajduje pierwszy element, którego klucz jest nie mniejszy niż k. |
| iterator upper_bound(const key_type& k) | Znajduje pierwszy element, którego klucz jest wiekszy niż k. |
| const_iterator upper_bound(const key_type& k) const | Znajduje pierwszy element, którego klucz jest wiekszy niż k. |
| pair<iterator, iterator>
equal_range(const key_type& k) |
Znajduje zakres zawierający wszystkie elementy o kluczu k. |
| pair<const_iterator, const_iterator>
equal_range(const key_type& k) const |
Znajduje zakres zawierający wszystkie elementy o kluczu k. |
| data_type&
operator[](const key_type& k) |
Patrz niżej |
| bool operator==(const map&,
const map&) |
Porównuje dwie mapy pod względem równości. Jest to funkcja globalna, a nie składnikowa. |
| bool operator<(const map&,
const map&) |
Porównanie leksykograficzne. Jest to funkcja globalna, a nie składnikowa. |
[edytuj] Nowe składniki
Te składniki nie są zdefiniowane w wymaganiach Unikalnego Posortowanego Kontenera Asocjacyjnego i
Parze Kontenerów Asocjacyjnych, ale są unikalne dla map:
| Funkcja składnikowa | Opis działania |
|---|---|
| data_type&
operator[](const key_type& k) |
Zwraca referencje do obiektu, który jest powiązany ze danym kluczem. Jeśli mapa nie zawiera
takiego obiektu operator [] wstawia domyślny obiekt typu data_type(). |
Na stronie wykorzystano materiały z dokumentacji SGI
Permission to use, copy, modify, distribute and sell this software and its documentation for any purpose is hereby granted without fee, provided that the below copyright notice appears in all copies and that both the copyright notice and this permission notice appear in supporting documentation. Silicon Graphics makes no representations about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty.
Copyright © 1994 Hewlett-Packard Company
[edytuj] Interatory
[edytuj] Algorytmy w STL
Cóż znaczą biblioteki bez <algorithm>? Na pewno mniej, ponieważ każde modyfikacje na wektorach czy ciągach znaków są bardziej uciążliwe i wymagają od użytkownika dodatkowego wkładu pracy na napisanie algorytmu do wykonania określonego problemu. Weźmy pod uwagę przykładowo problem sortowania. Poniżej przedstawiona jest funkcja sortująca bąbelkowo n-elementową tablicę 1-wymiarową.
void sortowanie_babelkowe(int tab[], int n) { for (int j=n-1; j>0; --j) { for (int i=0; i<j; ++i) if (tab[i]>tab[i+1]) { int temp=tab[i]; tab[i]=tab[i+1]; tab[i+1]=temp; } } }
Kod nie jest długi, ale wygodniej jest napisać:
sort(tab,tab+n);
[edytuj] Lista algorytmów zawartych w bibliotece <algorithm>
[edytuj] Inne klasy w STL
Dodatek B
[edytuj] Przykłady programów w C++
// Arithmetic - proste działania matematyczne
#include <iostream>
int main()
{
using namespace std;
int Liczba1 = 0;
int Liczba2 = 0;
int Wynik = 0;
// koniec linii to '\n'
cout << "Podaj pierwsza liczbe calkowita\n";
cin >> Liczba1;
// czasem mozna linie konczyc uzywajac ::std::endl
// ale nalezy pamietac, ze endl oczyszcza bufor
// co wiaze sie ze spadkiem wydajnosci
// kiedy wykonywanych jest duzo takich operacji
cout << "Podaj druga liczbe calkowita" << endl;
cin >> Liczba2;
Wynik = Liczba1 + Liczba2;
cout << '\n' << Liczba1 << '+' << Liczba2 << '=' << Wynik << '\n';
return 0;
}
[edytuj] Ćwiczenia
[edytuj] Ćwiczenie 1
Napisać program, który czyta ze strumienia standardowego wejścia i zmienia małe litery na wielkie i odwrotnie (nie bierzemy pod uwagę polskich znaków), wypisując wynik do strumienia standardowego wyjścia. Nastepnie zmodyfikować go tak, aby pracował na plikach: wejściowym oraz wyjściowym, podanych jako parametry programu.
[edytuj] Ćwiczenie 2
Mamy tablice kwadratowa, której wymiarem jest liczba nieparzysta. Napisz program, który wypełni ja zerami i jedynkami w następujący sposób:
0 0 1 0 0 0 1 0 1 0 1 0 0 0 1 0 1 0 1 0 0 0 1 0 0
[edytuj] Ćwiczenie 3
Dany jest plik:
+-----------+----+-----------+--------------+ |PESEL |Płeć|Imię |Nazwisko | +-----------+----+-----------+--------------+ 45679815845 M Tomasz Buczakowski 48491848438 M Andzej Krzemień 81871681861 K Karolina Cubisz 78168181348 M Aleksander Buczykowski 48618481389 M Jerzy Apostoł 56884864684 K Magdalena Koczkowska 84184864889 M Patryk Rutkowski 93818984869 M Daniel Dworkowski 48498386181 K Karina Puszkowski 78681864864 M Jakub Buczakowski 48648848646 M Stefan Buczakowski 84681846844 K Anna Stepaniuk 45784698943 M Grzegorz Warszawski
- Wypisać wszystkie nazwiska kończące się na "ski" w porządku alfabetycznym, bez powtórzeń.
- Wypisać w odwrotnym porządku alfabetycznym osobno imiona żeńskie i męskie.
[edytuj] Ćwiczenie 4
Napisz funkcje wypisującą liczby pierwsze (lub złożone) z podanego zakresu liczb.
- Podpowiedź: Należy użyć algorytmu sita Eratostenesa.
[edytuj] Ćwiczenie 5
Mamy taki oto plik źrodłowy:
#include <iostream>
using namespace std;
class wydatkiMiesieczne
{
public:
//w tym miejscu wpisz brakujace pola klasy
private:
int cenaChleb; //
int cenaMleko; //ceny produktow (przypisz je w konstruktorze)
int cenaGazeta; //
bool zakupChleba; //
bool zakupMleka; //zmienne true/false
bool zakupGazety; //
int pensja; //pensja obiektu (człowieka)
int sumaWydatkowMiesiecznych; //zmienna ktora sumuje wydatki
bool czyWystarczaNaMiesiac; //zmienna przechowuje true jesli pensja jest wieksza niz wydatki; jesli jest odwrotnie - false
};
int main(int argc, char** argv)
{
wydatkiMiesieczne Marian;
Marian.zarobek(200);
Marian.czyKupujeChleb(true);
Marian.czyKupujeMleko(true);
Marian.czyKupujeGazete(true);
Marian.obliczanieWydatkowMiesiecznych();
cout << "Pensja Mariana " << (Marian.czyWystarczaPieniedzyNaWydatkiMiesieczne() ? "jest wystarczajaca" : "nie jest wystarczajaca");
cout << " na pokrycie kosztow codziennych zakupow" << endl;
wydatkiMiesieczne* Jadwiga = new wydatkiMiesieczne;
Jadwiga->zarobek(12000);
Jadwiga->czyKupujeChleb(true);
Jadwiga->czyKupujeMleko(true);
Jadwiga->czyKupujeGazete(false);
Jadwiga->obliczanieWydatkowMiesiecznych();
cout << "Pensja Jadwigi " << (Jadwiga->czyWystarczaPieniedzyNaWydatkiMiesieczne() ? "jest wystarczajaca" : "nie jest wystarczajaca");
cout << " na pokrycie kosztow codziennych zakupow" << endl;
delete Jadwiga;
getchar();
return 0;
}
Program ten oblicza czy pensja, ktorą dostaje człowiek zdefiniowany jako obiekt klasy wydatkiMiesieczne, jest w stanie pokryc uproszczone wydatki miesięczne. Jednak program nie jest kompletny. Uzupełnij brakujące pola klasy w sekcji public stosując się do poniższych instrukcji:
-
- Użyj konstruktora do przypisania wartosci polom danych:
+-----------+-----------------+ |nazwa pola |wartosc ktora ma | | danych |zostac przypisana| +-----------+-----------------+ | cenaChleb | 2 | | cenaMleko | 3 | | cenaGazeta| 2 | | | | | sumaWydat-| | | kowMiesie-| 0 | | cznych | (opcjonalnie) | +-----------+-----------------+
-
- Pamiętaj że zmienne cenaChleb, cenaMleko i cenaGazeta reprezentują cene za jedną sztukę danego produktu, a my bedziemy potrzebować ceny za zakup tych towarów przez cały miesiąc (przyjmijmy ze miesiąc ma 30 dni).
- Nie tworz innych funkcji w sekcji public niż te, które zostały wywołane w funkcji main().
- Funkcja zarobek() ma pobierac liczbe reprezentującą zarobek danej osoby (w zł) i wpisywac tą wartość w zmienną pensja.
- Funkcje z przedrostkami czyKupuje- mają przypisywac wartość (true - jeśli kupuje dany produkt; false - jeśli nie) do swoich odpowiednikow z sekcji private (np. funkcja czyKupujeMleko() przypisze wartość zmiennej zakupMleka).
- Funkcja obliczanieWydatkowMiesiecznych() ma obliczyć kwotę jaka bedzie wydana przez miesiąc kupowania ustalonych przez obiekt produktów i przypisać wynik zmiennej sumaWydatkowMiesiecznych.
- Funkcja czyWystarczaPieniedzyNaWydatkiMiesieczne() ma obliczyc czy pensja danego obiektu jest wystarczająca na pokrycie kosztów zakupów, przekazać wynik true, albo false do zmiennej czyWystarczaNaMiesiac i zwrocić go.
[edytuj] Różnice między C a C++
[edytuj] Komentarze
W ANSI C (C89) nie jest dozwolone używanie komentarzy zaczynających się od //. Zostały jednak dodane w standardzie C99.
[edytuj] Stałe
Stałe w C++ (obiekty zadeklarowane ze słowem const) są stałe w pełnym tego słowa znaczeniu. Np. stałe typów całkowitych mogą być stosowane tam, gdzie wymaga się stałych wyrażeń (tzn. jako etykiety case, jako rozmiar tablic itp.). W C już nie i tam do takich stałych trzeba stosować dyrektywę preprocesora #define. W C stała jest dokładnie tym samym co zmienna, z tym tylko zastrzeżeniem, że nie można jej jawnie modyfikować (ale można zmodyfikować zawartość wskaźnika do adresu stałej, czyli de facto zmodyfikować stałą).
Konsekwencją tego po części jest fakt, że globalnie deklarowane stałe w języku C mają to samo wiązanie (ang. linkage) co zmienne, czyli zewnętrzne. W języku C++ stałe mają domyślnie wiązanie lokalne i aby były one zewnętrzne (dzielone między jednostkami kompilacji), muszą być zadeklarowane razem z inicjalizacją i słowem extern.
[edytuj] Zmienne
- możliwość deklarowania zmiennych np. w instrukcji sterującej petli for
- możliwość mieszania instrukcji i deklaracji zmiennych w jednym bloku kodu (w ANSI C zmienne muszą być deklarowane przed pierwszą instrukcją)
[edytuj] Wiązanie (ang. linkage) i obiekty niejawne
W języku C wiązanie symboli z obiektami, które są przez nie oznaczane, czyli odwoływanie się w jednych jednostkach kompilacji do obiektów lub funkcji z innych jednostek kompilacji, jest opisane luźniejszymi regułami, niż w C++.
W języku C obowiązuje "słabe wiązanie" (ang. vague linkage), przy czym nie istnieją w tym języku żadne obiekty niejawne. To oznacza, że funkcja lub zmienna globalna o określonej nazwie może wystąpić dowolną ilość razy w całym zbiorze kompilacji (zbiorze jednostek kompilacji składających się na jeden plik wykonywalny lub bibliotekę dynamiczną). Podczas procesu wiązania wybierany jest w takim wypadku "pierwszy lepszy" ze wszystkich takich obiektów. Język C pozwala również na wielokrotne definicje zmiennej globalnej w tym samym pliku - definicje te, jak też definicje zmiennej o tej samej nazwie w innych jednostkach kompilacji będą się odnosić do dokładnie tej samej zmiennej. Właściwość ta pochodzi prawdopodobnie z czasów, gdy w C nie było słowa extern, więc deklarację zmiennej globalnej można było bez dodatkowych oznaczeń zamieścić w pliku nagłówkowym.
W języku C++ obowiązuje "silne wiązanie" (ang. strict linkage) dla obiektów jawnych, natomiast słabe dla obiektów niejawnych. Obiekty niejawne w C++ to są tablice metod wirtualnych tworzonych dla określonej klasy oraz funkcje inline. Silne wiązanie oznacza, że jeśli w zbiorze kompilacji zostaną znalezione dwa obiekty o tej samej nazwie, to linker zgłosi błąd i odmówi wiązania.
[edytuj] Typ stałej znakowej
W języku C literał znakowy (stała znakowa), np. 'a' jest traktowana jako int, natomiast w C++ jest uważana za char.
[edytuj] Typ bool
W C++ istnieje oficjalny typ bool i dwie stałe tego typu true i false, które służą do przechowywania wartości logicznych. Jest typem zwracanym operatorów porównawczych i relacji oraz typem przyjmowanym i zwracanym przez operatory && i ||. Ten typ musi mieć również wyrażenie podawane do if, while i drugiego wyrażenia w for.
Ze względu na wsteczną zgodność jednak pozostawiono domyślną konwersję typu bool na int, przy czym false i true są konwertowane odpowiednio na 0 i 1, natomiast w drugą stronę 0 konwertuje się na false i każda inna wartość całkowita na true.
[edytuj] Typy wskaźników
W języku ANSI C dozwolone są niejawne konwersje pomiędzy różnymi typami wskaźnikowymi oraz pomiędzy typami wskaźnikowymi i typami całkowitymi. Co prawda wiele kompilatorów zgłasza ostrzeżenia przy próbach dokonania takiej konwersji bez jawnego rzutowania (za wyjątkiem konwersji, w których uczestniczy void*), nie jest ona jednak w ANSI C błędem.
W języku C++ niejawne konwersje pomiędzy wskaźnikami i referencjami do różnych typów są możliwe tylko w przypadku typów spokrewnionych, tzn. wskaźnik do klasy pochodnej może być niejawnie konwertowany na wskaźnik do klasy bazowej (w tym również niejako uważa się "typ void" za bazę dla wszystkich typów, zatem każdy wskaźnik na dane można niejawnie konwertować na void*). Wszelkie inne konwersje pomiędzy wskaźnikami do danych różnych typów oraz wskaźnikami i typami całkowitymi muszą być jawnie zrzutowane.
Warto zaznaczyć, że rzutowanie pomiędzy typami klasowymi, które są shierarchizowane (np. rzutowanie wskaźnika do klasy bazowej na wskaźnik do klasy pochodnej) mogą się odbywać wyłącznie poprzez operator static_cast lub dynamic_cast. Użycie do tego celu rzutowania ogólnego "(typ)obiekt" lub reinterpret_cast może spowodować niezdefiniowane zachowanie.
[edytuj] Alternatywne słowa kluczowe
W języku C++ dodano dodatkowe słowa kluczowe opisujące niektóre operatory. Operatory &&, || i ! możemy też zapisywać jako and, or i not. Istnieją także słowa dla operatorów bitowych &, | i ^: bitand, bitor i xor. Podobnie również dla operatorów połączonych z przypisaniem: and_eq , or_eq i xor_eq . Istnieje także, choć szczerze nie wiem po co, not_eq .
[edytuj] Biblioteka standardowa
C++ używa innych nazw plików nagłówkowych dla biblioteki standardowej odziedziczonej z języka C - np. cstdio zamiast stdio.h. Zobacz też rozdział Przestrzenie nazw.
[edytuj] Funkcje
W języku C pusta lista argumentów: () oznacza, że prototyp nie precyzuje argumentów przyjmowanych przez funkcję, natomiast deklaracja funkcja(void) oznacza, że funkcja nie przyjmuje argumentów. W języku C++ puste nawiasy są tożsame z (void).
W języku C++ można jednak uzyskać ten sam efekt, co puste nawiasy w C poprzez (...), czyli zmienną listę argumentów, ale bez określania argumentów początkowych (to z kolei nie jest dostępne w języku C).
Należy zwrócić szczególną uwagę, że jest to w istocie dość uciążliwe ułatwienie, że () jest tożsame z (void). W konsekwencji bowiem o ile wyrażenie (a) (gdzie 'a' jest jakąś zmienną) można odróżnić od nazwy typu w nawiasach, np. (int), to w przypadku () jest to nie do odróżnienia. Stąd mała niekonsekwencja w deklarowaniu obiektów wraz z argumentami konstruktora:
Klasa x( arg1, arg2 );
ale bez argumentów musi być deklarowane jako
Klasa x;
czyli bez nawiasów.
Należy też pamiętać, że odróżnianie argumentu w nawiasach od typu w nawiasach źle działa w przypadku obiektów tymczasowych:
Klasa1 obiekt( Klasa2() );
które, wbrew pozorom, nie deklaruje obiektu klasy Klasa1 z podaniem obiektu tymczasowego typu Klasa2 do konstruktora, lecz deklaruje funkcję o nazwie 'obiekt', która przyjmuje typ Klasa2 i zwraca typ Klasa1.
[edytuj] Manglowanie nazw funkcji
W związku z przeciążaniem funkcji, każda funkcja w C++ ma unikalną identyfikację, niezależną od jej nazwy. Ten identyfikator służy również do rozpoznania odpowiedniej wersji funkcji na poziomie wiązania - nawet jeśli istnieje prototyp funkcji o określonej nazwie, ale z innymi parametrami, niż te, z którymi została ta funkcja zdefiniowana, to błąd przy próbie wywołania takiej funkcji zostanie wykryty na etapie wiązania (w przypadku języka C nie zostałby wykryty w ogóle).
To spowodowało niezgodność sposobu wiązania funkcji z językiem C. Żeby móc w C++ użyć funkcji zdefiniowanej w języku C, to jej prototyp musi być poprzedzony extern "C". Taka funkcja nie może wtedy podlegać przeciążaniu (tzn. może być wiele funkcji o takiej nazwie, ale tylko jedna z nich może być extern "C").
[edytuj] Struktury
Jeśli mamy strukturę Struktura to w C zmienne definiujemy struct Struktura s1, s2;. W C++ możemy pominąć słowo kluczowe struct (i podobnie jest z union, enum i class). Dla zgodności z językiem C jednak nie jest zabronione ponowne użycie nazwy struktury w innym znaczeniu (np. funkcji, co ma miejsce w przypadku standardowej funkcji z języka C stat), tyle że jeśli się tak stanie, to wtedy nie można już pominąć słowa struct, jeśli się ma na myśli określony typ strukturalny.
Pozostałe
[edytuj] Indeks
| Spis treści |
|---|
Zobacz też: C/Indeks
[edytuj] C
[edytuj] D
[edytuj] E
[edytuj] F
[edytuj] I
[edytuj] K
kompilator
konwersja
[edytuj] L
[edytuj] Ł
[edytuj] M
[edytuj] N
[edytuj] O
operator
- dekerementacji --
- inkrementacji ++
- lista operatorów
- dzielenia modulo %
- pobrania adresu &
- sizeof
- wyłuskania *
- wyrażenia warunkowego ?:
[edytuj] P
przeciążanie
przekazywanie argumentów do funkcji
[edytuj] R
[edytuj] S
szablon
[edytuj] T
typ
[edytuj] U
[edytuj] W
[edytuj] Z
[edytuj] Zasoby
[edytuj] Linki zewnętrzne
- Thinking in C++ - darmowy, bardzo dobry podęcznik Bruce'a Eckela
- http://www.cppreference.com/ - bardzo zwięzły opis najważniejszych elementów C++
- http://it.hk.pl - kurs C++ od podstaw z przykładami
- C++ bez cholesterolu
- [1] Tutoriale, zasoby, artykuły o c++
- Oficjalna dokumentacja STL na stronie SGI
[edytuj] Książki
- Jerzy Grębosz, Symfonia C++
- Jerzy Grębosz, Pasja C++
[edytuj] Licencja
Version 1.2, November 2002
Copyright (C) 2000,2001,2002 Free Software Foundation, Inc.
51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[edytuj] 0. PREAMBLE
The purpose of this License is to make a manual, textbook, or other functional and useful document "free" in the sense of freedom: to assure everyone the effective freedom to copy and redistribute it, with or without modifying it, either commercially or noncommercially. Secondarily, this License preserves for the author and publisher a way to get credit for their work, while not being considered responsible for modifications made by others.
This License is a kind of "copyleft", which means that derivative works of the document must themselves be free in the same sense. It complements the GNU General Public License, which is a copyleft license designed for free software.
We have designed this License in order to use it for manuals for free software, because free software needs free documentation: a free program should come with manuals providing the same freedoms that the software does. But this License is not limited to software manuals; it can be used for any textual work, regardless of subject matter or whether it is published as a printed book. We recommend this License principally for works whose purpose is instruction or reference.
[edytuj] 1. APPLICABILITY AND DEFINITIONS
This License applies to any manual or other work, in any medium, that contains a notice placed by the copyright holder saying it can be distributed under the terms of this License. Such a notice grants a world-wide, royalty-free license, unlimited in duration, to use that work under the conditions stated herein. The "Document", below, refers to any such manual or work. Any member of the public is a licensee, and is addressed as "you". You accept the license if you copy, modify or distribute the work in a way requiring permission under copyright law.
A "Modified Version" of the Document means any work containing the Document or a portion of it, either copied verbatim, or with modifications and/or translated into another language.
A "Secondary Section" is a named appendix or a front-matter section of the Document that deals exclusively with the relationship of the publishers or authors of the Document to the Document's overall subject (or to related matters) and contains nothing that could fall directly within that overall subject. (Thus, if the Document is in part a textbook of mathematics, a Secondary Section may not explain any mathematics.) The relationship could be a matter of historical connection with the subject or with related matters, or of legal, commercial, philosophical, ethical or political position regarding them.
The "Invariant Sections" are certain Secondary Sections whose titles are designated, as being those of Invariant Sections, in the notice that says that the Document is released under this License. If a section does not fit the above definition of Secondary then it is not allowed to be designated as Invariant. The Document may contain zero Invariant Sections. If the Document does not identify any Invariant Sections then there are none.
The "Cover Texts" are certain short passages of text that are listed, as Front-Cover Texts or Back-Cover Texts, in the notice that says that the Document is released under this License. A Front-Cover Text may be at most 5 words, and a Back-Cover Text may be at most 25 words.
A "Transparent" copy of the Document means a machine-readable copy, represented in a format whose specification is available to the general public, that is suitable for revising the document straightforwardly with generic text editors or (for images composed of pixels) generic paint programs or (for drawings) some widely available drawing editor, and that is suitable for input to text formatters or for automatic translation to a variety of formats suitable for input to text formatters. A copy made in an otherwise Transparent file format whose markup, or absence of markup, has been arranged to thwart or discourage subsequent modification by readers is not Transparent. An image format is not Transparent if used for any substantial amount of text. A copy that is not "Transparent" is called "Opaque".
Examples of suitable formats for Transparent copies include plain ASCII without markup, Texinfo input format, LaTeX input format, SGML or XML using a publicly available DTD, and standard-conforming simple HTML, PostScript or PDF designed for human modification. Examples of transparent image formats include PNG, XCF and JPG. Opaque formats include proprietary formats that can be read and edited only by proprietary word processors, SGML or XML for which the DTD and/or processing tools are not generally available, and the machine-generated HTML, PostScript or PDF produced by some word processors for output purposes only.
The "Title Page" means, for a printed book, the title page itself, plus such following pages as are needed to hold, legibly, the material this License requires to appear in the title page. For works in formats which do not have any title page as such, "Title Page" means the text near the most prominent appearance of the work's title, preceding the beginning of the body of the text.
A section "Entitled XYZ" means a named subunit of the Document whose title either is precisely XYZ or contains XYZ in parentheses following text that translates XYZ in another language. (Here XYZ stands for a specific section name mentioned below, such as "Acknowledgements", "Dedications", "Endorsements", or "History".) To "Preserve the Title" of such a section when you modify the Document means that it remains a section "Entitled XYZ" according to this definition.
The Document may include Warranty Disclaimers next to the notice which states that this License applies to the Document. These Warranty Disclaimers are considered to be included by reference in this License, but only as regards disclaiming warranties: any other implication that these Warranty Disclaimers may have is void and has no effect on the meaning of this License.
[edytuj] 2. VERBATIM COPYING
You may copy and distribute the Document in any medium, either commercially or noncommercially, provided that this License, the copyright notices, and the license notice saying this License applies to the Document are reproduced in all copies, and that you add no other conditions whatsoever to those of this License. You may not use technical measures to obstruct or control the reading or further copying of the copies you make or distribute. However, you may accept compensation in exchange for copies. If you distribute a large enough number of copies you must also follow the conditions in section 3.
You may also lend copies, under the same conditions stated above, and you may publicly display copies.
[edytuj] 3. COPYING IN QUANTITY
If you publish printed copies (or copies in media that commonly have printed covers) of the Document, numbering more than 100, and the Document's license notice requires Cover Texts, you must enclose the copies in covers that carry, clearly and legibly, all these Cover Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on the back cover. Both covers must also clearly and legibly identify you as the publisher of these copies. The front cover must present the full title with all words of the title equally prominent and visible. You may add other material on the covers in addition. Copying with changes limited to the covers, as long as they preserve the title of the Document and satisfy these conditions, can be treated as verbatim copying in other respects.
If the required texts for either cover are too voluminous to fit legibly, you should put the first ones listed (as many as fit reasonably) on the actual cover, and continue the rest onto adjacent pages.
If you publish or distribute Opaque copies of the Document numbering more than 100, you must either include a machine-readable Transparent copy along with each Opaque copy, or state in or with each Opaque copy a computer-network location from which the general network-using public has access to download using public-standard network protocols a complete Transparent copy of the Document, free of added material. If you use the latter option, you must take reasonably prudent steps, when you begin distribution of Opaque copies in quantity, to ensure that this Transparent copy will remain thus accessible at the stated location until at least one year after the last time you distribute an Opaque copy (directly or through your agents or retailers) of that edition to the public.
It is requested, but not required, that you contact the authors of the Document well before redistributing any large number of copies, to give them a chance to provide you with an updated version of the Document.
[edytuj] 4. MODIFICATIONS
You may copy and distribute a Modified Version of the Document under the conditions of sections 2 and 3 above, provided that you release the Modified Version under precisely this License, with the Modified Version filling the role of the Document, thus licensing distribution and modification of the Modified Version to whoever possesses a copy of it. In addition, you must do these things in the Modified Version:
- A. Use in the Title Page (and on the covers, if any) a title distinct from that of the Document, and from those of previous versions (which should, if there were any, be listed in the History section of the Document). You may use the same title as a previous version if the original publisher of that version gives permission.
- B. List on the Title Page, as authors, one or more persons or entities responsible for authorship of the modifications in the Modified Version, together with at least five of the principal authors of the Document (all of its principal authors, if it has fewer than five), unless they release you from this requirement.
- C. State on the Title page the name of the publisher of the Modified Version, as the publisher.
- D. Preserve all the copyright notices of the Document.
- E. Add an appropriate copyright notice for your modifications adjacent to the other copyright notices.
- F. Include, immediately after the copyright notices, a license notice giving the public permission to use the Modified Version under the terms of this License, in the form shown in the Addendum below.
- G. Preserve in that license notice the full lists of Invariant Sections and required Cover Texts given in the Document's license notice.
- H. Include an unaltered copy of this License.
- I. Preserve the section Entitled "History", Preserve its Title, and add to it an item stating at least the title, year, new authors, and publisher of the Modified Version as given on the Title Page. If there is no section Entitled "History" in the Document, create one stating the title, year, authors, and publisher of the Document as given on its Title Page, then add an item describing the Modified Version as stated in the previous sentence.
- J. Preserve the network location, if any, given in the Document for public access to a Transparent copy of the Document, and likewise the network locations given in the Document for previous versions it was based on. These may be placed in the "History" section. You may omit a network location for a work that was published at least four years before the Document itself, or if the original publisher of the version it refers to gives permission.
- K. For any section Entitled "Acknowledgements" or "Dedications", Preserve the Title of the section, and preserve in the section all the substance and tone of each of the contributor acknowledgements and/or dedications given therein.
- L. Preserve all the Invariant Sections of the Document, unaltered in their text and in their titles. Section numbers or the equivalent are not considered part of the section titles.
- M. Delete any section Entitled "Endorsements". Such a section may not be included in the Modified Version.
- N. Do not retitle any existing section to be Entitled "Endorsements" or to conflict in title with any Invariant Section.
- O. Preserve any Warranty Disclaimers.
If the Modified Version includes new front-matter sections or appendices that qualify as Secondary Sections and contain no material copied from the Document, you may at your option designate some or all of these sections as invariant. To do this, add their titles to the list of Invariant Sections in the Modified Version's license notice. These titles must be distinct from any other section titles.
You may add a section Entitled "Endorsements", provided it contains nothing but endorsements of your Modified Version by various parties--for example, statements of peer review or that the text has been approved by an organization as the authoritative definition of a standard.
You may add a passage of up to five words as a Front-Cover Text, and a passage of up to 25 words as a Back-Cover Text, to the end of the list of Cover Texts in the Modified Version. Only one passage of Front-Cover Text and one of Back-Cover Text may be added by (or through arrangements made by) any one entity. If the Document already includes a cover text for the same cover, previously added by you or by arrangement made by the same entity you are acting on behalf of, you may not add another; but you may replace the old one, on explicit permission from the previous publisher that added the old one.
The author(s) and publisher(s) of the Document do not by this License give permission to use their names for publicity for or to assert or imply endorsement of any Modified Version.
[edytuj] 5. COMBINING DOCUMENTS
You may combine the Document with other documents released under this License, under the terms defined in section 4 above for modified versions, provided that you include in the combination all of the Invariant Sections of all of the original documents, unmodified, and list them all as Invariant Sections of your combined work in its license notice, and that you preserve all their Warranty Disclaimers.
The combined work need only contain one copy of this License, and multiple identical Invariant Sections may be replaced with a single copy. If there are multiple Invariant Sections with the same name but different contents, make the title of each such section unique by adding at the end of it, in parentheses, the name of the original author or publisher of that section if known, or else a unique number. Make the same adjustment to the section titles in the list of Invariant Sections in the license notice of the combined work.
In the combination, you must combine any sections Entitled "History" in the various original documents, forming one section Entitled "History"; likewise combine any sections Entitled "Acknowledgements", and any sections Entitled "Dedications". You must delete all sections Entitled "Endorsements."
[edytuj] 6. COLLECTIONS OF DOCUMENTS
You may make a collection consisting of the Document and other documents released under this License, and replace the individual copies of this License in the various documents with a single copy that is included in the collection, provided that you follow the rules of this License for verbatim copying of each of the documents in all other respects.
You may extract a single document from such a collection, and distribute it individually under this License, provided you insert a copy of this License into the extracted document, and follow this License in all other respects regarding verbatim copying of that document.
[edytuj] 7. AGGREGATION WITH INDEPENDENT WORKS
A compilation of the Document or its derivatives with other separate and independent documents or works, in or on a volume of a storage or distribution medium, is called an "aggregate" if the copyright resulting from the compilation is not used to limit the legal rights of the compilation's users beyond what the individual works permit. When the Document is included in an aggregate, this License does not apply to the other works in the aggregate which are not themselves derivative works of the Document.
If the Cover Text requirement of section 3 is applicable to these copies of the Document, then if the Document is less than one half of the entire aggregate, the Document's Cover Texts may be placed on covers that bracket the Document within the aggregate, or the electronic equivalent of covers if the Document is in electronic form. Otherwise they must appear on printed covers that bracket the whole aggregate.
[edytuj] 8. TRANSLATION
Translation is considered a kind of modification, so you may distribute translations of the Document under the terms of section 4. Replacing Invariant Sections with translations requires special permission from their copyright holders, but you may include translations of some or all Invariant Sections in addition to the original versions of these Invariant Sections. You may include a translation of this License, and all the license notices in the Document, and any Warranty Disclaimers, provided that you also include the original English version of this License and the original versions of those notices and disclaimers. In case of a disagreement between the translation and the original version of this License or a notice or disclaimer, the original version will prevail.
If a section in the Document is Entitled "Acknowledgements", "Dedications", or "History", the requirement (section 4) to Preserve its Title (section 1) will typically require changing the actual title.
[edytuj] 9. TERMINATION
You may not copy, modify, sublicense, or distribute the Document except as expressly provided for under this License. Any other attempt to copy, modify, sublicense or distribute the Document is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
[edytuj] 10. FUTURE REVISIONS OF THIS LICENSE
The Free Software Foundation may publish new, revised versions of the GNU Free Documentation License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. See http://www.gnu.org/copyleft/.
Each version of the License is given a distinguishing version number. If the Document specifies that a particular numbered version of this License "or any later version" applies to it, you have the option of following the terms and conditions either of that specified version or of any later version that has been published (not as a draft) by the Free Software Foundation. If the Document does not specify a version number of this License, you may choose any version ever published (not as a draft) by the Free Software Foundation.
[edytuj] How to use this License for your documents
To use this License in a document you have written, include a copy of the License in the document and put the following copyright and license notices just after the title page:
Copyright (c) YEAR YOUR NAME.
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.2
or any later version published by the Free Software Foundation;
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
A copy of the license is included in the section entitled "GNU
Free Documentation License".
If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts, replace the "with...Texts." line with this:
with the Invariant Sections being LIST THEIR TITLES, with the
Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST.
If you have Invariant Sections without Cover Texts, or some other combination of the three, merge those two alternatives to suit the situation.
If your document contains nontrivial examples of program code, we recommend releasing these examples in parallel under your choice of free software license, such as the GNU General Public License, to permit their use in free software.