Przejdź do zawartości

C++/Wersja do druku

Z Wikibooks, biblioteki wolnych podręczników.
< C++


Programowanie w C++




 
 
Podręcznik języka C.
Podstawy języka C, jednocześnie wstęp do niniejszej książki

Spis treści

[edytuj]

Wstęp

[edytuj]
  1. O języku C++ Etap rozwoju: 50% (w dniu 04.01.2005)
    Opis i historia
  2. O podręczniku Etap rozwoju: 75% (w dniu 12.02.2005)
    Autorzy, źródła, jak czytać ten podręcznik

Część 1: Podstawowe mechanizmy C++

[edytuj]
  1. Przestrzenie nazw Etap rozwoju: 75% (w dniu 13.04.2006)
    Wprowadzenie pojęcia przestrzeni nazw, przestrzeń nazw std
  2. Zmienne Etap rozwoju: 50% (w dniu 12.02.2006)
    Nowe sposoby deklaracji, kontrola typów w C++, nowe sposoby rzutowania
  3. Referencje Etap rozwoju: 50% (w dniu 12.02.2006)
    Porównanie ze wskaźnikami, zastosowanie do przekazywania argumentów do funkcji
  4. Rvalue-referencje i przenoszenie wartości Etap rozwoju: 00% (w dniu 14.11.2014)
    Wprowadzony w C++11 mechanizm przenoszenia zawartości obiektów
  5. Funkcje inline Etap rozwoju: 50% (w dniu 11.03.2006)
    Krótki opis funkcji inline
  6. Przeciążanie funkcji Etap rozwoju: 00% (w dniu 04.01.2005)
    Po co i jak można przeciążać funkcje i jak tego nie da się robić
  7. Zarządzanie pamięcią Etap rozwoju: 25% (w dniu 11.03.2006)
    Jak w C++ dynamicznie zarządzać pamięcią z użyciem operatorów new i delete
  8. Strumienie
    Obsługa strumieni wejścia i wyjścia, czytanie i pisanie do plików, obiekty std::istream i std::ostream
  9. C++11 - wybrane nowe elementy standardu
    Pętla for po kolekcji, typy wyliczeniowe, typy całkowitoliczbowe

Część 2: Podstawy programowania obiektowego

[edytuj]
  1. Czym jest obiekt Etap rozwoju: 75% (w dniu 29.09.2007)
    Wprowadzenie pojęcia klasy i obiektu, autorekursja, kontrola dostępu
  2. Konstruktor i destruktor Etap rozwoju: 100% (w dniu 04.01.2005)
    Konstruktor, konstruktor kopiujący, destruktor
  3. Dziedziczenie Etap rozwoju: 50% (w dniu 04.01.2005)
    Dziedziczenie prywatne, publiczne i chronione
  4. Składniki statyczne Etap rozwoju: 00% (w dniu 04.01.2005)
    Atrybuty i metody statyczne

Część 3: Zaawansowane programowanie obiektowe

[edytuj]
  1. Funkcje wirtualne Etap rozwoju: 25% (w dniu 30.12.2006)
    Funkcje wirtualne i abstrakcyjne, wyjaśnienie polimorfizmu i dynamic_cast
  2. Programowanie zorientowane obiektowo
    Wyjaśnienie idei programowania zorientowanego obiektowo
  3. Obiekty stałe
    Jak tworzyć, możliwe niebezpieczeństwa, słowo kluczowe mutable
  4. Przeciążanie operatorów Etap rozwoju: 25% (w dniu 04.01.2005)
    Wprowadzenie przykładu klasy z kompletnym przeciążeniem operatorów
  5. Konwersje obiektów
    Przeciążenie operatorów konwersji, konstruktor jako sposób konwersji, konstruktory typu explicit
  6. Klasy i typy zagnieżdżone
    Tworzenie klas i typów zagnieżdżonych
  7. Dziedziczenie wielokrotne
    Dziedziczenie wielokrotne, dziedziczenie wirtualne oraz problemy z nimi związane

Część 4: Zaawansowane konstrukcje językowe

[edytuj]
  1. Obsługa wyjątków Etap rozwoju: 00% (w dniu 30.12.2006)
    Obsługa wyjątków w C++, funkcje unexpected() i terminate()
  2. Funkcje anonimowe (lambdy) Etap rozwoju: 50% (w dniu 12.11.2014)
    Funkcje anonimowe wprowadzone w C++11
  3. Szablony funkcji Etap rozwoju: 00% (w dniu 30.12.2006)
    Szablony funkcji
  4. Szablony klas Etap rozwoju: 00% (w dniu 14.11.2014)
    Szablony klas, programowanie uogólnione
  5. Metaprogramowanie Etap rozwoju: 00% (w dniu 14.11.2014)
    Zaawansowanie użycie szablonów, informacje o typach, SFINAE
  6. Wskaźniki do elementów składowych
    Wykorzystanie wskaźników do elementów składowych klas

Dodatek A: Biblioteka STL

[edytuj]
  1. Filozofia STL Etap rozwoju: 00% (w dniu 30.12.2006)
    Jak skonstruowana jest biblioteka STL
  2. String Etap rozwoju: 00% (w dniu 30.12.2006)
    Korzystanie z łańcuchów znaków
  3. Vector Etap rozwoju: 75% (w dniu 30.12.2006)
    Korzystanie z wektorów
  4. List & Slist Etap rozwoju: 50% (w dniu 12.07.2007)
    Listy jedno- i dwukierunkowe
  5. Set
    Korzystanie ze zbiorów
  6. Map
    Korzystanie z odwzorowań
  7. Unordered_set
    Korzystanie ze zbiorów
  8. Unordered_map
    Korzystanie ze zbiorów
  9. Stack
    Korzystanie ze stosu
  10. Iteratory
    Korzystanie z iteratorów
  11. Algorytmy w STL Etap rozwoju: 50% (w dniu 24.03.2011)
    Jak działają algorytmy w STL
  12. Inne klasy STL
    Krótkie omówienie pozostałych klas

Dodatek B

[edytuj]
  1. Przykłady
    Przykłady kodu z komentarzem
  2. Ćwiczenia Etap rozwoju: 00% (w dniu 04.01.2005)
    Zadania kontrolne
  3. Różnice między C a C++ Etap rozwoju: 25% (w dniu 25.05.2006)
    Najważniejsze różnice między C a C++

Pozostałe

[edytuj]
  1. Indeks Etap rozwoju: 00% (w dniu 04.01.2005)
    Indeks najważniejszych terminów
  2. Zasoby Etap rozwoju: 50% (w dniu 04.01.2005)
    Książki, linki do innych kursów i dokumentacji
  3. Dla autorów
    Wskazówki dla osób pragnących pomóc w rozwoju podręcznika
  4. Wersja do druku Etap rozwoju: 100% (w dniu 11.02.2005) (edytuj)
    Całość książki na jednej stronie, gotowa do druku
  5. Licencja Etap rozwoju: 100% (w dniu 04.01.2005)
    Pełny tekst GNU Free Documentation license

Wstęp

O języku C++

[edytuj]

C++ jest 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, dynamiczną kontrolę typów i precyzyjne rzutowanie, programowanie generyczne (uogólnione) dzięki wykorzystaniu szablonów, przeciążanie funkcji i operatorów, 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 skutkuje rozbudowaną składnią języka, stanowiąc problem nawet dla kompilatorów (żaden popularny kompilator nie jest w pełni zgodny z obowiązującym standardem języka; stan na 2010 rok).

Nazwa C++ została zaproponowana przez Ricka Mascitti i wywodzi się z faktu, że w C wyrażenie zmienna++ oznacza inkrementację, czyli zwiększenie o jeden.

Autorem i twórcą języka C++ jest duński programista Bjarne Stroustrup, pierwsza wersja pojawiła się w 1979 roku.


O podręczniku

[edytuj]

O podręczniku

[edytuj]

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, bez powielania występujących tam podstaw.

Tworzenie podręcznika

[edytuj]

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.

Autorzy

[edytuj]

Znaczący wkład w powstanie podręcznika mają:

Bibliografia

[edytuj]
  • ISO/IEC 14882:2003 Programming Languages – C++

Jak czytać ten podręcznik

[edytuj]

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 przedstawionych dalej trzech 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ć.

Trzecia możliwość to zapoznanie się z językiem C++ w kursie internetowym, zapewniam że jeżeli wpiszemy w Google hasło "kurs C++" wyświetli się nam bardzo dużo propozycji. Po skończeniu owego kursu przypomnimy sobie podstawy języka w tej książce, a następnie zapoznamy się z bardziej zaawansowanymi komendami.



Część 1
Podstawy języka

Przestrzenie nazw

[edytuj]

Słowem wstępu

[edytuj]

Załóżmy, że w grupie znajomych masz dwie osoby o tym samym imieniu, powiedzmy „Tomek”. Teraz za każdym razem, gdy wołasz Tomka, oboje się mylą - nie wiedzą o którego ci chodzi. Aby rozwiązać ten problem, zaczynasz używać ich pełnych nazwisk. Przestrzenie nazw w C ++ działają w ten sam sposób. (Źródło - Namespaces)

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 manipulatora wyjścia "endl". Chyba że jest to uzasadnione: endl wymusza 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. Najpierw, pokrótce omówimy, co ten program właściwie robi.

Za pomocą #include <iostream> dołączyliśmy plik nagłówkowy do obsługi strumieni I/O, dzięki czemu możemy wypisywać dane na ekran (ściślej: na standardowe wyjście). Dodatkowo istnieje plik nagłówkowy iostream.h. Jest to jednak nagłówek niestandardowy, pomagający zachować wsteczną zgodność.

int main( ) {...} służy zdefiniowaniu funkcji głównej, która jest zawsze uruchomiana podczas startu naszego programu.

Wyrażenie 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ą operatora <<, i użyliśmy endl, który m.in. dodaje znak nowej linii.

Za pomocą return 0 informujemy system, że program może zakończyć działanie bez zgłaszania błędów.

Na koniec zostawiliśmy linię z kodem using namespace std. Aby wyjaśnić jej znaczenie, musimy omówić, czym są przestrzenie nazw.

Przestrzenie nazw

[edytuj]

Podczas pracy nad dużymi projektami, w których używa się wielu bibliotek z licznymi deklaracjami, możemy w końcu natknąć się na problem konfliktu nazw - gdy kilka obiektów, typów czy funkcji ma tę samą nazwę. Rozwiązaniem może być np. zamknięcie nazw w "zakresach", w celu oddzielenia ich. Z pomocą przychodzi nam mechanizm przestrzeni nazw.

Przestrzeń nazw jest zatem zbiorem obiektów, która ogranicza dostęp do nich - oprócz nazwy obiektu niezbędne jest też wspomnienie, z której przestrzeni nazw chcemy go użyć, obchodząc tym samym problem konfliktu nazw.

Spójrzmy na kolejny program, zmienioną wersję poprzedniego:

#include <iostream>

int main ()
{
   std::cout << "Hello World!" << std::endl;
   return 0;
}

Widzimy tu wyrażenie std:: pojawiające się przed cout i endl. Zapis ten oznacza, że wspomniane obiekty chcemy zaczerpnąć z przestrzeni std, a przy okazji nie obchodzi nas, czy są jakieś inne obiekty o takich nazwach. Jeśli jednak pominiemy wzmiankę o std::, pojawi się informacja o błędzie.

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 (kolekcjach), 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.

Przykład pierwszy ze wstępu pokazał nam, że nie musimy za każdym razem odwoływać się do przestrzeni nazw, kiedy chcemy użyć znajdujących się w niej rzeczy. Używając using namespace PrzestrzenNazw, podpowiadamy kompilatorowi, w którym miejscu może szukać używanych przez nas obiektów i funkcji, abyśmy mogli swobodnie używać wszystkiego co się znajduje w danej przestrzeni nazw, tzn. bez dodatkowej wzmianki jak np. std::.

Oczywiście nie musimy naraz "udostępniać" 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 będziemy mogli używać w kodzie endl i będziemy mieli na myśli właśnie to pochodzące z przestrzeni std. Nie wykonaliśmy tej operacji na elemencie cout (nie wstawiliśmy instrukcji using std::cout), więc musieliśmy go dalej poprzedzić nazwą przestrzeni.

Tworzenie własnej przestrzeni nazw

[edytuj]

Przestrzeń nazw tworzymy za pomocą słowa kluczowego namespace, ograniczając jej 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 HelloWorld::hello lub ogólnie using namespace HelloWorld przed funkcją main (a nawet wewnątrz tej funkcji), nie musielibyśmy odwoływać się jawnie do HelloWorld, wystarczyłoby samo hello( ).

Co ciekawe, nie musimy zamieszczać zawartości naszej przestrzeni nazw w jednym, ciągłym bloku. Możemy rozbić to na kilka 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 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 przestrzeni nazw lub poza nią, pisząc typ_zwracany PrzestrzenNazw::nazwa_funkcji( ), 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;
   }
}

Jak wspomniano wcześniej, ostatnie dwie definicje funkcji moglibyśmy zapisać także w ten sposób:

int Matematyka::dodaj (int a, int b)
{...}

int Matematyka::odejmij (int a, int b)
{...}

Przestrzeń nazw std

[edytuj]

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. Zobaczmy jak wczytywać pewne wartości do zmiennych, używając do tego cin:

#include <iostream>

int main ()
{
   int a, b;
   std::cout << "Podaj dwie liczby a i b" << std::endl;

   // wypisujemy komunikat i czekamy na wpisanie liczby a
   std::cout << "podaj a: ";
   std::cin >> a;

   // wypisujemy komunikat na wyjście i czekamy na wpisanie liczby b
   std::cout << "podaj 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 zmiennej. Zmienna ta nie musi być liczbą, może być też np. napisem. W C++ tekst (łańcuch znaków) będziemy często przechowywali w obiektach typu string (który także znajduje się w std). Do jego obsługi będziemy musieli dołączyć do projektu bibliotekę <string>. Spójrzmy na przykład:

#include <iostream>
#include <string>

using std::cout;
using std::cin;
using std::endl;

int main ()
{
   std::string imie;
   std::string email;
   std::string informacja;

   // wczytujemy imię
   cout << "Podaj swoje imie: "; 
   cin >> imie;

   // wczytujemy email
   cout << "Podaj swój email: ";
   cin >> email;

   informacja = imie + " (" + email + ")";  // suma (konkatenacja) 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.

Korzystanie z biblioteki standardowej C

[edytuj]

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++ mógłby wyglądać następująco:

#include <cstring>  // zamiast <string.h>
  
int main (int argc, char **argv)
{
   if (argc < 2)
      return -1;
   return std::strcmp( argv[0], argv[1]);
}

Zauważmy, że:

  1. dołączany plik nagłówkowy ma dodaną na początku literę c
  2. dostęp do funkcji jest możliwy przez pośrednictwo przestrzeni nazw std

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++: #include <cxxxxx>. 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++.


Zmienne

[edytuj]

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.

Deklarowanie zmiennych

[edytuj]

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;
}

Kontrola typów

[edytuj]

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.

Rzutowanie

[edytuj]

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ą istotne wady: ciężko wypatrzeć je w kodzie oraz możliwe jest za pomocą ich dowolne rzutowanie. 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 cztery nowe operatory rzutowania typu, które są bardziej restrykcyjne i mogą być wykorzystywane tylko w określonych operacjach rzutowania:

const_cast
rzutowanie ze zmiennych z modyfikatorem const i volatile na zmienne bez tych modyfikatorów.
static_cast
rzutowanie w którym typ obiektu musi być znany w momencie kompilacji.
dynamic_cast
rzutowanie wskaźników na obiekty. Umożliwia sprawdzenie, czy można bezpiecznie przypisać adres obiektu do wskaźnika danego typu. Typ obiektu jest dynamicznie określany, w czasie wykonywania programu. Jest do tego używany mechanizm dynamicznej identyfikacji typu RTTI (ang. runtime type identification).
reinterpret_cast
niebezpieczne rzutowania, które zmieniają zupełnie sens interpretacji bitów w zmiennych. Rzutowanie to nie pozwala na zdjęcie modyfikatora const.

Rzutowanie dynamic_cast jest opisane 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 znajdowanie 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ń.

static_cast

[edytuj]

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.

static_cast służy w szczególności do:

[edytuj]
  • 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.

Do czego static_cast nie służy:

[edytuj]
  • 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ć.

Przykłady poprawnego użycia static_cast:

[edytuj]
#include <iostream>

int main ()
{
   int liczba = 5, liczba2 = 2;
   std::cout << "5/2 int(bez rzutowania): " << liczba/liczba2 << std::endl;
   std::cout << "5/2 float(static_cast): " 
      << static_cast<float>(liczba)/static_cast<float>(liczba2) << std::endl;
   return 0;
}

Przykłady niepoprawnego użycia static_cast:

[edytuj]
#include <iostream>

int main ()
{
   std::string str = "ciag";
   std::cout << "string --> char: " << static_cast<char>(str) << std::endl;
   return 0;
}

Inne cechy static_cast

[edytuj]

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.

const_cast

[edytuj]

Rzutowanie służy do usunięcia klasyfikatorów const (patrz Obiekty stałe) lub volatile z typu, którym mogą być jedynie: referencje, wskaźniki oraz wskaźniki do składowych klasy. Można zatem dokonać konwersji:

  • const_cast<const Typ&>(wyrażenie) na Typ&,
  • const_cast<const Typ*>(wyrażenie) na Typ*.

Trzeba jednak pamiętać, że usuwanie klasyfikatora const zwykle nie jest dobrym pomysłem i konieczność jego zastosowania świadczy o jakimś problemie w intefejsie klas. Jedynym w miarę sensownym użyciem const_cast jest rzutowanie wskaźników, tak żeby później posługiwać się operatorami post- i preinkrementcji, co może być wygodniejsze, np.:

int policz_literke(const char* napis, char literka) {

    int n = 0;
    char* c = const_cast<char*>(napis);
    while (*c) {
        if (*c++ == literka) n++;
    }

    return n;
}

Ale zamiast rzutowania można w powyższej funkcji wprowadzić dodatkowy licznik i dowoływać się do znaków napisu operatorem indeksowania.

Poniżej jeszcze jeden przykład, tym razem ilustrujący negatywne skutki stosowania const_cast - obiekt, który teoretycznie powinien być niezmienny, zmienia swój stan. Wnioskowanie o działaniu programu zawierającego tego typu sztuczki jest trudne.

#include <iostream>

class Klasa {
    
    int liczba;
public:
    Klasa() : liczba(0) {}

    void zmien() {
        liczba += 1;
    }

    int jaka_liczba() const {
        return liczba;
    }
};

void wywolaj_zmien(const Klasa& obiekt) {

    Klasa& tmp = const_cast<Klasa&>(obiekt);

    tmp.zmien();
}

int main() {
    const Klasa obiekt;

    std::cout << obiekt.jaka_liczba() << '\n';  // wyświetli 0

    //obiekt.zmien(); // wywołanie niemożliwe, obiekt jest stały
    wywolaj_zmien(obiekt);

    std::cout << obiekt.jaka_liczba() << '\n';  // wyświetli 1
}

reinterpret_cast

[edytuj]
#include <iostream>
using namespace std;

int main(void)
{
    typedef unsigned long long ULL;
    typedef unsigned int UI;
    
    ULL a = 137438953600;
    //Liczba a w pamięci komputera:
    //00000000000000000000000000100000
    //00000000000000000000000010000000
    
    ULL* wsk_a_ll = &a;
    //ULL* wsk_a_int = static_cast<UI*>(&a); //błąd kompilatora - niedozwolone rzutowanie static_cast
    UI* wsk_a_int = reinterpret_cast<UI*>(&a);

    cout << *wsk_a_ll << "\n" << wsk_a_int[0] << " " << wsk_a_int[1] << "\n";
    
    return 0;
}

Wyjście programu:

137438953600
128 32

W powyższym przykładzie próbujemy udowodnić, że dowolny zaalokowany obszar pamięci możemy potraktować jako tablicę, a interpretacja danych zależy od tego, jaki jest typ wskaźnika, którym się posługujemy; tutaj kolejno (long long *) oraz (int *).

Stosowane jest rzutowanie typu "reinterpret_cast", ponieważ "static_cast" skutkuje błędem kompilacji. Samo rzutowanie jest niecodzienne i udowadnia, że "reinterpret_cast" należy używać jedynie w uzasadnionych okolicznościach.

Dedukcja typów - auto (C++11)

[edytuj]

W C++ aby zadeklarować zmienną należy podać pełną nazwę typu. O ile te nazwy są krótkie nie jest to wielkim problemem, jednak wraz ze wzrostem złożoności programu pojawia się wiele przestrzeni nazw oraz klas, które również definiują typy. Wówczas należy przy każdy wystąpieniu podać pełną nazwę, tak jak w przykładzie:

namespace konfiguracja_programu {

    class PlikINI {

        typedef std::string nazwa_pliku;

        nazwa_pliku domyslny_plik();

    };
}

int main() {

    konfiguracja_programu::PlikINI k;

    konfiguracja_programu::PlikINI::nazwa_pliku konf = k.domyslny_plik();
}

To może wydawać się przesadzone, ale już korzystając z biblioteki standardowej natrafimy na różne rozwlekłe nazwy typów, np. w iteratorach. Ponadto w C++ coraz powszechniej korzysta się z tzw. metaprogramowania, czyli szablonów funkcji i klas, gdzie często operuje się na nieznanych z nazwy typach. Dlatego od C++11 wprowadzono słowo kluczowe auto które zastępuje nazwę typu i jeśli kompilator potrafi wydedukować typ na podstawie prawej strony przypisania, zmienna jest deklarowana. Czyli przykładowy program uprości się do:

int main() {

    konfiguracja_programu::PlikINI k;

    auto konf = k.domyslny_plik();
}

Deklaracji auto można również używać dla typów prostych, np.

int main() {

    auto a = 0;     // a jest typu int
    auto b = 0.0;   // b jest typu double
    auto c = true;  // c jest typu bool
    auto d = '?';   // d jest typu char

    for (auto i=0; i < 5; i++) {
        // ...
    }
}

Słowo auto zastępuje typ, więc można je poprzedzić np. słowem const czyniąc taką zmienną niezmienialną, albo uczynić z niej referencję, np.

int main() {

    konfiguracja_programu::PlikINI k;

    const auto  plik     = k.domyslny_plik();
    const auto& plik_ref = k.domyslny_plik();
}

Ćwiczenia

[edytuj]
#include <iostream>

int main (int argc, char *argv[])
{
  int liczba, liczba2;
  std::cin >> liczba >> liczba2;
  double wynik = liczba / liczba2;
  std::cout << wynik << std::endl;

  return 0;
}

Po uruchomieniu powyższego programu i podaniu wejścia

5 2

Otrzymamy

2

Dlaczego jako wynik wyświetlana jest liczba 2 a nie 2.5? Rozwiąż problem przy użyciu rzutowania.


Referencje

[edytuj]

Czym jest referencja?

[edytuj]

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:

Deklaracja referencji

[edytuj]

Referencje deklaruje się jak zmienne z podaniem znaku &:

TypDanych & referencja

Taki zapis byłby możliwy w liście argumentów funkcji, jednak w ciele funkcji referencja musi być od razu zainicjalizowana. Zapisujemy do niej adres innej zmiennej (robi się to trochę inaczej niż w wypadku wskaźników):

TypDanych & 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 = x, &ref_y = y;           // referencje
char *wsk_1, *wsk2; // wskazniki

Stałe referencje

[edytuj]

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.

Przekazywanie argumentów przez referencję

[edytuj]

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;
}


Rvalue-referencje i przenoszenie wartości

[edytuj]

C++/Rvalue-referencje i przenoszenie wartości

Funkcje inline

[edytuj]

Funkcje inline jak można by się domyślić 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 (może tak zrobić lecz nie musi). 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:[1]

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).


Przeciążanie funkcji

[edytuj]

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 liczbą 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);
void funkcja (std::string);
void funkcja (std::string, std::string);
// int funkcja (int);  //niedozwolone, funkcje różnią się tylko zwracanym typem
int funkcja (bool);  //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.

Zastosowanie

[edytuj]

Przeciążenie funkcji stosuje się przy np. potęgowaniu:

int pot (int, int);
double pot (double, int);
void pot (int&, int);

int pot (int podstawa, int wykladnik)
{
   int wynik = 1;
   for (int i = 0; i < wykladnik; ++i)
      wynik = podstawa*wynik;
   return wynik;
}

// przeładowana funkcja I: zwraca inny typ danych i są inne parametry
double pot (double podstawa, int wykladnik)
{
   double wynik = 1;
   for (int i = 0; i < wykladnik; ++i)
      wynik = podstawa*wynik;
   return wynik;
}

// przeładowana funkcja II: nie zwraca danych tylko modyfikuje podstawę która jest podana przez referencję
void pot (int& podstawa, int wykladnik)
{
   int wynik = 1;
   for (int i = 0; i < wykladnik; ++i)
      wynik = podstawa*wynik;
   podstawa = wynik;
}

Argumenty domyślne

[edytuj]

Pierwszym sposobem przeładowania są argumenty domyślne. Deklaracja funkcji wygląda tak:

int potega (int podstawa, int wykładnik = 2);

W tym przypadku, kiedy funkcje wywołamy poprzez potega(2), zostanie dodany parametr domyślny. Będzie to więc znaczyło to samo, co potega(2, 2).

Nie możemy już jednak przeciążyć tej funkcji poniższą:

int potega (int podstawa)
{
  return podstawa*podstawa;
}

W tym przypadku, gdy podamy jeden argument, kompilator nie będzie mógł określić o którą funkcję nam chodzi - dwuargumentową z jednym argumentem domyślnym, czy zwykłą jednoargumentową.

Typ argumentów

[edytuj]

Czasem możemy chcieć, by funkcja zachowywała się zależnie od tego, jakie argumenty jej dano. Załóżmy, że piszemy własną bibliotekę do obsługi standardowego wyjścia stdout. Chcemy zrobić funkcję wypisującą różne typy danych, w tym typ łańcuchów C++.

void pisz (char);
void pisz (std::string);
void pisz (void);

void pisz (char a){
   printf ("%c", a);
}
void pisz (std::string a)
{
   printf ("%s", a.c_str());
}
void pisz ()
{
   printf ("\n");
}

Szablony funkcji

[edytuj]
 Zobacz głównie: Szablony funkcji.

W C++ dostępne są szablony. Wyobraź sobie sytuację: programista pisze funkcję obsługującą sporo typów danych. Możemy rozwiązać to przez kopiowanie funkcji w kodzie. jednak byłoby to dość uciążliwe. Możemy to zrobić krócej:

template <typename T>
T nazwaFunkcji (argumenty typu T)
{
   //do funkcji można przekazać dowolny typ danych
}


Zarządzanie pamięcią

[edytuj]

W języku C++ do alokowania pamięci na stercie 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 ł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];

Jednak 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;             // 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][0] = new int [3] // wymiar I = 0 -> wymiar II = 1 -> 3 elementy(tablica)
wektory[0][1] = new int [5] // wymiar I = 0 -> wymiar II = 3 -> 5 elementów(tablica)
wektory[1][0] = new int;    // wymiar I = 1 -> wymiar II = 2 -> 1 element
...


Stosując ten sposób, 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:

delete  wektory[1][0];        // kasujemy pojedynczą zmienną
delete [] wektory[0][1];
delete [] wektory[0][0];
// 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, a delete dla 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ł.

Działanie w przypadku braku pamięci

[edytuj]

1. Domyślnie gdy przydział pamięci jest niemożliwy operator new zgłasza wyjątek std::bad_alloc, np.

#include <new>      // wyjątek std::bad_alloc
#include <cstdio>

int main() {

    try {
        char* p = new char[1000000000000];
    } catch (std::bad_alloc& e) {
        // std::bad_alloc::what
        std::printf("Błąd alokacji: %s\n", e.what()); 
    }

    return 0;
}

2. Można wyłączyć zgłaszanie wyjątku, zamiast tego w przypadku braku pamięci zostanie zwrócony pusty wskaźnik (nullptr). W tym celu po słowie kluczowym new trzeba podać symbol std::nothrow, np.

#include <new>      // symbol std::nothrow
#include <cstdio>

int main() {
    
    char* p = new (std::nothrow) char[1000000000000];

    if (p == nullptr) {
        std::puts("Brak pamięci");
    }

    return 0;
}

Placement new

[edytuj]

Jak zostało powiedziane operator new wykonuje dwie operacje:

  • zaalokowanie pamięci o żądanym rozmiarze,
  • wywołanie konstruktorów (domyślnych lub wyspecyfikowanych).

Można jednak użyć specjalnego wywołania new, tzw. "placement new", które jedynie wywołuje konstruktory na pamięci już zaalokowanej w innym miejscu; trzeba być jednak pewnym, że wskazany obszar pamięci jest odpowiedniego rozmiaru.

Jest to niezbyt powszechne użycie, ma też jedną wadę: nie działa operator delete, trzeba ręcznie wywoływać destruktory obiektów. Zastosowanie tego mechanizmu ma głównie sens, gdy samodzielnie zarządzamy pamięcią, np. zawczasu rezerwujemy pamięć dla dużej liczby obiektów i w miarę potrzeb ją przydzielamy, oszczędzając tym samym czas na każdorazowe odwołanie do alokatora pamięci.

#include <new>
#include <cstdlib>  // malloc

class Klasa {
    int numer;
};

int main() {

    void* wskaznik = malloc(sizeof(Klasa));

    Klasa* obiekt = new (wskaznik) Klasa;

    obiekt->~Klasa();

    return 0;
}

Inteligentne wskaźniki

[edytuj]

Klasy nazywane "inteligentnymi wskaźnikami" pomagają ominąć część problemów związanych z czasem życia wskazywanych obiektów i ze współdzieleniem wskaźników. Często występujące problemy to:

  • wycieki pamięci,
  • przedwczesne kasowanie wskazywanych obiektów,
  • wielokrotne kasowanie obiektów (przez delete).

W standardzie zdefiniowano trzy klasy szablonowe:

  • std::unique_ptr,
  • std::shared_ptr,
  • std::weak_ptr.

unique_ptr

[edytuj]

Klasa unique_ptr (unikalny wskaźnik) gwarantuje, że w systemie istnieć będzie dokładnie jedna aktywna instancja wskaźnika. Nie tylko nie istnieje możliwość skopiowania unique_ptr, ale nawet nie można dokonać prostego przypisania, tzn. a = b;. Dostępne jest jedynie przenoszenie wartości (std::move) ewentualnie należy wprost zwolnić wskaźnik metodą release i zastąpić przez reset. Np.

#include <memory>

int main() {
    std::unique_ptr<int> a(new int(5));
    std::unique_ptr<int> b;

    b.reset(a.release());
    a = std::move(b);
}

Kiedy unique_ptr posiadający wskaźnik jest niszczony, niszczony jest również obiekt na który wskazuje. Usunięcie obiektu wykonuje tzw. deleter, jest to klasa będąca drugim argumentem szablonu. Domyślnie to std::default_deleter i nie trzeba jej wprost podawać; dopiero gdy potrzebujemy dodatkowych działań w chwili usuwania obiektu należy taką klasę zaimplementować, co jest raczej rzadko potrzebne.

Przykładowy program:

#include <memory>       // unique_ptr
#include <iostream>

class Klasa {
public:
    Klasa()  { std::cout << "Konstrukor" << '\n'; }
    ~Klasa() { std::cout << "Destruktor" << '\n'; }
};

int main() {

    std::unique_ptr<Klasa> p1(new Klasa());
    std::unique_ptr<Klasa> p2;

    std::cout << "p1 = " << p1.get() << '\n';
    std::cout << "p2 = " << p2.get() << '\n';

    p2 = std::move(p1);

    std::cout << "p1 = " << p1.get() << '\n';
    std::cout << "p2 = " << p2.get() << '\n';
}

Wypisze:

Konstrukor
p1 = 0x894b008
p2 = 0
p1 = 0
p2 = 0x894b008
Destruktor

Warto zwrócić uwagę, że nigdzie w programie nie ma bezpośredniego wywołania delete, mimo to destruktor klasy jest wołany. Z tej cechy można korzystać we własnych klasach: zamiast przechowywać wskaźniki do jakiś obiektów wykorzystywanych wewnętrznie, łatwiej mieć unique_ptr, dzięki czemu nie trzeba pamiętać o zwalnianiu pamięci w destruktorze. Analogicznie w przypadku funkcji unique_ptr załatwia za nas zwalnianie pamięci, także w przypadku wystąpienia wyjątku. Na przykład:

void funkcja() {
    
    char* dane = new char[100000];      // wyjątek może wystąpić tutaj
    char* tymczasowe = new char[5000];  // i tutaj też

    // obliczenia ...                   // podczas obliczeń również

    delete[] dane;
    delete[] tymczasowe;
}

Ta realizacja ma oczywistą wadę: w przypadku jakiegokolwiek wyjątku nastąpi wyciek pamięci (no, chyba, że nie uda się zaalokować pamięci na dane). Oprócz tego programista jest odpowiedzialny za zwolnienie zasobów. Czasem można zapomnieć, szczególnie gdy rozwija się już istniejącą funkcję.

Drugim podejściem jest złapanie wszystkich możliwych wyjątków:

void funkcja_poprawiona() {
    char* dane = nullptr;
    char* tymczasowe = nullptr;

    try {
        dane = new char[100000];
        tymczasowe = new char[5000];

        // obliczenia ...

        delete[] dane;
        delete[] tymczasowe;
    } catch (...) {
        delete[] dane;
        delete[] tymczasowe;

        throw;
    }
}

To podejście jest lepsze, jednak również ma wadę z poprzedniego rozwiązania: ręczne zwalnianie pamięci, do tego powielone. Użycie unique_ptr skraca i znacząco upraszcza kod, eliminując wszystkie wymienione mankamenty:

#include <memory>

void funkcja_najlepsza() {
    
    std::unique_ptr<char> dane(new char[100000]);
    std::unique_ptr<char> tymczasowe(new char[5000]);

    // obliczenia ...
}


shared_ptr i weak_ptr

[edytuj]

Klasa shared_ptr (współdzielony wskaźnik) umożliwia istnienie wielu wskaźników do tego samego obiektu, podtrzymując go przy życiu tak długo, jak istnieje przynajmniej jeden shared_ptr, który by go zawierał. Klasa shared_ptr w istocie realizuje odśmiecanie pamięci ze zliczaniem referencji. Z każdym obiektem związana jest liczba odwołań (odczyt metodą use_count), która jest automatycznie aktualizowana. Kiedy osiągnie zero obiekt jest niszczony.

Standard C++11 gwarantuje bezpieczeństwo w środowisku wielowątkowym.

#include <memory>
#include <iostream>

class Klasa {
public:
    Klasa()  {std::cout << "Konstruktor" << '\n';}
    ~Klasa() {std::cout << "Destruktor" << '\n';}
};

void przyklad() {

    std::shared_ptr<Klasa> p(new Klasa());  
    std::cout << p.use_count() << '\n';         // licznik referencji = 1
    {
        std::shared_ptr<Klasa> p1(p);
        std::cout << p.use_count() << '\n';     // licznik referencji = 2

        {
            std::shared_ptr<Klasa> p2(p);       // licznik referencji = 3
            std::cout << p.use_count() << '\n';
        }                                       // licznik referencji = 2 - p2 jest niszczony

        std::shared_ptr<Klasa> p3(p1);          // licznik referencji = 3
        std::cout << p.use_count() << '\n';
    }                                           // licznik referencji = 1 - p1 i p3 są niszczone

    std::cout << p.use_count() << '\n';

}                                               // licznik referencji = 0 - p jest niszczony, niszczona jest też instancja Klasa

int main() {
    przyklad();
}

Program wyświetli:

Konstruktor
1
2
3
3
1
Destruktor

Używając shared_ptr należy mieć jednak na uwadze pewne niedogodności:

  • Zliczanie referencji nie radzi sobie z cyklami w zależnościach, tzn. jeśli obiekt A wskazuje na B i jednocześnie B wskazuje na A, to nigdy nie zostaną zwolnione, nawet jeśli oba są zbędne.
  • W środowisku wieloprocesorowym zliczanie referencji nie jest najszybsze, ze względu na konieczność synchronizacji pamięci między procesorami.

O ile drugi problem dotyczy wąskiej grupy programów, które intensywnie wykorzystują wątki (gry, bazy danych, programy naukowe), tak pierwszy może wystąpić wszędzie. Oto ilustracja cyklu zależności:

#include <memory>
#include <iostream>

class Klasa {
private:
    std::shared_ptr<Klasa> sasiad;

public:
    Klasa()  {std::cout << "Konstruktor" << '\n';}
    ~Klasa() {std::cout << "Destruktor" << '\n';}

    void ustaw_sasiada(std::shared_ptr<Klasa> s) {
        sasiad = s;
    }
};

void przyklad() {

    std::shared_ptr<Klasa> A(new Klasa());  // licznik A = 1
    std::shared_ptr<Klasa> B(new Klasa());  // licznik B = 1 

    A->ustaw_sasiada(B);                    // licznik B = 2
    B->ustaw_sasiada(A);                    // licznik A = 2
}                                           // licznik A, B = 1

int main() {
    przyklad();
}

Kiedy skompilujemy i uruchomimy powyższy program, na ekranie zostaną wypisane tylko dwa wiersze

Konstrukor
Konstrukor

pochodzące z konstruktorów obiektów A i B. Mimo że po wyjściu z funkcji wskaźniki shared_ptr są niszczone, to obiekty już nie, ponieważ same posiadają dodatkowe shared_ptr podbijające licznik do niezerowej wartości. W tym przypadku doszło do wycieku pamięci.

Jeśli to możliwe należy nie dopuszczać do tworzenia cykli, bo jak widzimy prowadzi to do kłopotów. Pół biedy kiedy jesteśmy ich świadomi, ale gdy taki cykl powstanie przypadkowo, trudniej będzie dojść przyczyny wycieków.

Kiedy jednak cykle są nie do uniknięcia można użyć klasy weak_ptr, która również trzyma wskaźnik do obiektu, jednak nie wpływa na licznik referencji, mówi się, że "przerywa" cykl. Nie istnieje też możliwość dereferencji tego wskaźnika, należy tymczasowo skonwertować weak_ptr na shared_ptr, który podtrzyma przy życiu obiekt - robi to metoda lock. Instancja weak_ptr może istnieć dłużej niż wskazywany obiekt, dlatego konieczne jest stwierdzenie, czy obiekt jest jeszcze ważny – służy do tego funkcja expired.

Wzorce użycia tej klasy są dwa, można albo 1) najpierw testować metodą expired, a następnie użyć lock, albo 2) prościej od razu użyć lock, sprawdzając czy wynikowy wskaźnik nie będzie pusty.

weak_ptr<int> wp;

// ...

// 1)
if (!wp.expired()) {
    auto shared = wp.lock();
    // tu działania na shared
}

// 2)
if (auto shared = wp.lock()) {
    // tu działania na shared
}

Poniżej kompletny przykład.

#include <memory>
#include <iostream>

void wyswietl(std::weak_ptr<int> ptr) {
    if (ptr.expired()) {
        std::cout << "<brak danych>" << '\n';
    } else {
        auto shared = ptr.lock();
        std::cout << "wartość = " << *shared << ", "
                  << "licznik referencji = " << shared.use_count() << '\n';
    }
}

void przyklad() {

    std::weak_ptr<int> wp;

    {
        std::cout << ">>>>" << '\n';
        std::shared_ptr<int> p(new int(71));
        wp = p;
        std::cout << "licznik referencji = " << p.use_count() << '\n';

        wyswietl(wp);
        std::cout << "<<<<" << '\n';
    }

    wyswietl(wp);
}

int main() {
    przyklad();
}

Po uruchomieniu na ekranie wyświetli się:

>>>>
licznik referencji = 1
wartość = 71, licznik referencji = 2
<<<<
<brak danych>

Jak widać po przypisaniu shared_ptr do weak_ptr nie zmienia licznika, dopiero w funkcji wyświetl jest on zmieniany. Po wyjściu z zakresu, p jest niszczone i wskaźnik staje się nieważny.


Strumienie

[edytuj]

Czym są strumienie?

[edytuj]

Najprościej mówiąc jest to ciąg bajtów o nieokreślonej długości. Strumień danych jest to szereg danych przesyłanych po sobie, który może być skończony lub nieskończony.

Przykładowo: film to szereg zdjęć (kadrów, klatek) wyświetlanych po sobie z określoną częstotliwością (podstawa to 25 zdjęć wyświetlanych na sekundę), Jeżeli zdjęcia będą odpowiednio szybko wyświetlane z odpowiednią kolejnością, to mamy wrażenie, że wszystko działa płynnie, a sam film ogląda się przyjemnie, Dzięki temu mówimy, że zdjęcia z filmu są przesyłane strumieniowo.

Zarządzać strumieniami możemy tak samo, jak w języku C, za pomocą struktur typu FILE i poleceń fopen() i fclose(), lecz daje to małe możliwości, o czym się przekonamy podczas nauki programowania obiektowego. Dlatego w C++ utworzono dużo wygodniejszy mechanizm, z którego już skorzystaliśmy. Wyróżniamy trzy rodzaje strumieni:

Strumienie "konsoli"

[edytuj]

Zapewne każdy uważny czytelnik wie już, jak pobierać oraz wyświetlać dane na ekranie konsoli. Dla przypomnienia napiszę. Do wczytywania danych ze strumienia wejścia służy operator >>, a wysyłania danych do strumienia wyjścia służy operator <<. Jednak metody, które do tej pory poznałeś nie zawsze spełnią twoje oczekiwania. Jak myślisz, co wyświetli poniższy program?

#include <iostream>
#include <string>

int main ()
{
   std::string x;
   std::cout << "Podaj swoje imie i nazwisko: ";
   std::cin >> x;
   std::cout << x << std::endl;
   return 0;
}

Prawdopodobnie Cię rozczaruję - wyświetli tylko i wyłącznie imię! Operator >> "wyciąga" pojedyncze słowo oddzielone białymi znakami oraz zapisuje je do zmiennej x. Musimy stworzyć kolejną zmienną typu string i zapisać w niej nazwisko i użyć kaskadowej operacji wstawiania danych do strumienia. Wystarczy dokonać kilka modyfikacji tego programu:

  • zmienić linijkę:
std::string x;

na:

std::string a, b;
  • zmienić linijkę:
std::cin >> x;

na

std::cin >> a >> b;
  • zmienić linijkę:
std::cout << x << std::endl;

na

std::cout << a << ' ' << b << std::endl;

Obiekty tego typu dziedziczą po klasie ostream dla strumieni wyjścia i istream dla wejścia. Plik nagłówkowy iostream sprawia, że mamy od początku otwarte 3 strumienie:

  • std::cin - standardowe wejście
  • std::cout - standardowe wyjście
  • std::cerr - gdy coś złego się stanie (wyjście)

Funkcja "getline"

[edytuj]

Funkcja ta umożliwia pobranie z klawiatury tekstu zawierającego spacje (obiekt "cin" przestaje wczytywać tekst po napotkaniu pierwszej spacji, tabulatora lub znaku końca wiersza). Oto przykład użycia funkcji "getline":

#include <iostream>
#include <string>

using namespace std;

int main()
{
   cout << "Podaj tekst: ";
   string tekst;
   getline(cin, tekst);
   cout << tekst << endl;
}

Jeśli przed użyciem funkcji "getline" użyjemy obiektu "cin", ten ostatni pozostawia zwykle znak końca wiersza '\n' w buforze klawiatury. Funkcja "getline" napotykając ten znak natychmiast kończy działanie, więc żeby uniknąć błędnego działania programu, należy wywołać funkcję cin.ignore(). Zostało pokazane to w poniższym przykładzie:

#include <iostream>
#include <string>

using namespace std;

int main()
{

    cout << "Podaj liczbę: ";
    int liczba;
    cin >> liczba;
    
    cout << "Podaj tekst: ";
    string tekst;
    cin.ignore(); // to wywołanie usunie z bufora znak '\n' pozostawiony przez obiekt "cin"
    getline(cin, tekst);
    
    cout << liczba << ' ' << tekst << endl;

    return 0;
}

Strumienie plikowe

[edytuj]
Program w C++ zapisujący dane do pliku graficznego

Za pomocą strumieni możemy czytać i zapisywać do plików:

#include <iostream>
#include <fstream>
#include <string>

int main()
{
   std::string a;
   std::cout << "Nacisnij Enter aby zakonczyc zapis.\n";
   std::ofstream f ("log.txt");
   std::cin >> a;
   if (f.good())
   {
      f << a;
      f.close();
   }
   return 0;
}

Program zapisuje łańcuch znaków do pliku. Pobiera go do momentu naciśnięcia Enter.


Inny przykład :

Jak utworzyć plik ppm za pomocą strumienia plikowego
/*
https://commons.wikimedia.org/wiki/File:XOR_texture.png

g++ p.cpp -Wall
./a.out

*/


#include <fstream>

int main()
{
	std::ofstream file;
	file.open("xor.ppm");
	file << "P2 256 256 255\n";
	for (int i = 0; i < 256; i++)
		for (int j = 0; j < 256; j++)
			file << (i ^ j) << ' ';
	file.close();
	return 0;
}

Strumienie napisów

[edytuj]

Wyróżniamy jeszcze jeden rodzaj strumieni - stringstream. Dzięki niemu jesteśmy w stanie operować na napisach tak, jak na zwykłym strumieniu. Wyobraźmy sobie sytuację, gdy musimy zamienić liczbę całkowitą na napis. Język C umożliwiał nam dokonywanie takich operacji za pomocą funkcji sprintf() bądź niestandardowej funkcji itoa(). Jednak zaprezentowane poniżej rozwiązanie jest o wiele czytelniejsze.

#include <iostream>
#include <sstream>

int main ()
{
   long x;   // Zmienna do przechowania liczby
   std::string napis;   // Zmienna do przechowania napisu
   std::stringstream ss;  // Strumień do napisów

   std::cout << "Podaj dowolna liczbe calkowita: ";
   std::cin >> x;

   ss << x;   // Do strumienia 'wysyłamy' podaną liczbę
   napis = ss.str();   // Zamieniamy zawartość strumienia na napis

   std::cout << "Dlugosc napisu wynosi " << napis.size() << " znakow." << std::endl;
   return 0;
}


C++11 - wybrane nowe elementy standardu

[edytuj]

Standard C++ z roku 2011, w skrócie nazywany C++11, dodał do języka wiele nowości, które powodują, że programy pisane zgodnie z nowym standardem są niekompatybilne ze starymi kompilatorami. W tym rozdziale opiszemy jedynie niektóre zmiany, pozostałe są wyjaśnione w innych rozdziałach.

Iterowanie po kolekcji

[edytuj]

Iterowanie po kolekcji pętlą for jest możliwe na kilka sposobów.

1. Jeśli kolekcja posiada operator indeksowania i funkcję zwracającą rozmiar można przejść po wszystkich indeksach:

#include <vector>
#include <iostream>

int main() {
    std::vector<int> liczby = {1, 2, 3, 4, 5};

    for (auto i=0u; i < liczby.size(); i++) {
        std::cout << liczby[i] << '\n';   
    }
}

2. Gdy kolekcja wspiera iteratory i metody begin/end (lub ich odpowiedniki):

int main() {
    std::vector<int> liczby = {1, 2, 3, 4, 5};

    for (auto it = liczby.begin(); it != liczby.end(); ++it) {
        std::cout << *it << '\n';   
    }
}

3. Od C++11 możliwe jest skrócenie powyższego zapisu do

int main() {
    std::vector<int> liczby = {1, 2, 3, 4, 5};

    for (auto x: liczby) {
        std::cout << x << '\n';
    }
}

Składnia jest następująca:

for (typ nazwa_zmiennej: nazwa_kolekcji) {
    // działanie na zmiennej
}

Klasa, która implementuje kolekcję musi posiadać metody begin i end, obie muszą zwracać iterator, który implementuje trzy operatory:

  • porównanie (!=)
  • dereferencję (*)
  • preinkrementację (++).

Poniżej kompletny przykład.

#include <iostream>

class Zakres {

    int start, stop;

    class Iterator {
        int liczba;

    public:
        Iterator(int liczba) : liczba(liczba) {}

        bool operator!=(const Iterator& iter) const {
            return liczba != iter.liczba;
        }

        int operator*() const {
            return liczba;
        }

        Iterator operator++() {
            liczba += 1;

            return *this;
        }
    };

public:
    Zakres() : start(0), stop(5) {};

    Iterator begin() const {
        return Iterator(start);
    };

    Iterator end() const {
        return Iterator(stop);
    };
};

int main() {

    Zakres liczby;

    for (auto x: liczby) {
        std::cout << x << '\n';
    }
}

Typy wyliczeniowe

[edytuj]

W C++, podobnie jak w C, funkcjonują typy wyliczeniowe. Wprowadza się je słowem kluczowym enum, po którym następuje nazwa typu, a w nawiasach klamrowych lista wartości. np.:

enum Kolor {
    Czerwony,
    Zielony,
    Niebieski
};

Problem związany z typami wyliczeniowymi jest dodawanie w zakresie widoczności nazw wszystkich wartości. Jeśli w tym samym zakresie potrzebujemy inny typ wyliczeniowy i powinien on zawierać pewne wartości o tych samych nazwach, kompilator nie dopuści do tego ze względu na powtórzenia nazw. Np. gdybyśmy chcieli typ dla kolorów świateł na skrzyżowaniach, to wartościami powinny być: Czerwony, Zielony, Zolty; dwie pierwsze są już zajęte przez Kolor.

W praktyce rozwiązuje się tego typu konflikty na kilka sposobów:

  • dodając do nazw jakieś prefiksy, np. "kol_Czerwony" i "swiatla_Czerwony";
  • umieszczając typy w przestrzeni nazw lub w klasie.

W C++11 wprowadzono nowy sposób definiowania typów wyliczeniowych enum class, deklaracja jest następująca:

enum class Nazwa {
    Wartosc1,
    Wartosc2,
    ...
    WartoscN
};

lub

enum class Nazwa: typ_wartości {
    Wartosc1,
    Wartosc2,
    ...
    WartoscN
};

Dwie główne różnice:

  1. Nazwy wartości nie pojawiają się w zakresie definicji typu wyliczeniowego, muszą być zawsze kwalifikowane jego nazwą, np. Nazwa::Wartosc2.
  2. Możliwe jest podanie typu, na jakich zapisywane są wartości. W ten sposób można precyzyjnie sterować rozmiarem klas lub struktur.
enum class Kolor {
    Czerwony,
    Zielony,
    Niebieski
};

enum class SygnalizacjaSwietlna {
    Czerwony,
    Zielony,
    Zolty
};

int main() {
    Kolor kolor;
    SygnalizacjaSwietlna sygn;

    // kolor = Czerwony;   // błąd: symbol 'Czerwony' niezadeklarowany
    // kolor = SygnalizacjaSwietlna::Zielony; // błąd: konwersja niemożliwa

    kolor = Kolor::Czerwony;
    sygn  = SygnalizacjaSwietlna::Czerwony;
}

Typy całkowite o określonym rozmiarze

[edytuj]

Standardowe typy całkowite, tj. int, long, short mogą mieć (w uproszczeniu) dowolny zakres, zależnie od systemu operacyjnego, architektury komputera, a nawet kompilatora. Np. nic nie stoi na przeszkodzie aby wszystkie trzy miały ten sam zakres. Pisząc przenośny program to na programistę spada obowiązek sprawdzenia, z których typów może bezpiecznie korzystać, np. badając stałe w rodzaju MAX_INT.

W C++11, wzorem standardu języka C, dodano typy całkowite o predefiniowanych rozmiarach, dzięki czemu zakresy liczb są gwarantowane. Jednocześnie trzeba mieć na uwadze, że zależnie od architektury procesora obsługa tych typów może być różnie realizowana. Np. na procesorach 32-bitowych wykonywanie działań na 64-bitowych typach wymaga większej liczby instrukcji, co przekłada się negatywnie na wydajność.

typ liczba bitów wartość minimalna wartość maksymalna
uint8_t 8 0 255
uint16_t 16 0 65 535
uint32_t 32 0 4 294 967 295
uint64_t 64 0 18 446 744 073 709 551 615
int8_t 8 -128 127
int16_t 16 -32 768 32 767
int32_t 32 -2 147 483 648 2 147 483 647
int64_t 64 -9 223 372 036 854 775 808 9 223 372 036 854 775 807


Część 2
Podstawy programowania obiektowego

Czym jest obiekt?

[edytuj]

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.

Projekt i twór – klasa i obiekt

[edytuj]

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ę.

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.

Definicja klasy

[edytuj]

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;

         //deklarowanie metod
         int Metoda1();
         void Metoda2();

    }; //pamiętaj o średniku!

Użycie klasy

[edytuj]

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 (.):

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;

Analogicznie jak w przypadku wskaźników na struktury operatorem dostępu do pola/metody klasy poprzez wskaźnik do obiektu 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 (lub kiedy nie jest nam już potrzebny):

delete ObiektWsk;

Przykład

[edytuj]

Stwórzmy klasę kostki do gry:

class Kostka{
      public:
       unsigned int wartosc;
       unsigned int maks;
       void Losuj();
     };

Po definicji klasy, zdefiniujmy jeszcze metodę Losuj() zadeklarowaną w tej klasie:

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;
}

Autorekursja

[edytuj]

Wskaźnik this umożliwia jawne odwołanie się zarówno do atrybutów, jak i metod klasy. Poniższy program wymusza użycie wskaźnika this, gdyż nazwa pola jest taka sama jak nazwa argumentu metody wczytaj:

#include <iostream>
using namespace std;

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;  
}

Kontrola dostępu

[edytuj]

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{
public :
       void Losuj();
       void Wypisz();
       int DajWartosc();
       void ZmienIloscScian(unsigned argMax);
protected:
       unsigned wartosc;
       unsigned max;
     };

int Kostka::DajWartosc()
{
return this->wartosc;
}

void Kostka::ZmienIloscScian(unsigned argMax)
{
  if(argMax> 20)
    max = 20;
  else
    max = argMax;
}

Zmodyfikowana klasa zezwala tylko na kostki maksymalnie dwudziestościenne. Ręczne modyfikacje zmiennej max są zabronione, można tego dokonać jedynie poprzez funkcję ZmienIloscScian, która zapobiega przydzieleniu większej ilości ścianek niż 20. Prywatny jest też atrybut wartość. Przecież nie chcemy aby była ona ustawiona inaczej niż przez losowanie! Dlatego możemy udostępnić jej wartość do odczytu poprzez metodę DajWartosc(), ale modyfikowana może być tylko na skutek działania metody Losuj().

Ćwiczenia

[edytuj]

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ń.

Kontrola dostępu

[edytuj]

C++/Kontrola dostępu

Konstruktor i destruktor

[edytuj]

Teoria

[edytuj]

Wstęp

[edytuj]

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.

Konstruktor

[edytuj]

Konstruktor jest to funkcja w klasie, wywoływana w trakcie tworzenia każdego obiektu danej klasy. 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)

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 i pusty).

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;
             string nazwa;
             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;
             string nazwa;
             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ń.


Najczęstszą funkcją konstruktora jest inicjalizacja obiektu oraz alokacja pamięci dla dodatkowych zmiennych (w tym celu lepiej jest użyć instrukcji inicjujących, które poznasz już za chwilę).

Instrukcje inicjalizujące

[edytuj]

Instrukcje inicjalizujące to instrukcje konstruktora spełniające specyficzne zadanie. Mianowicie mogą one zostać wywołane przez kompilator zaraz po utworzeniu klasy. Służą do inicjalizowania pól klasy, w tym stałych i referencji.

Jeśli nie zaimplementujemy instrukcji inicjalizujących, niczego nie będą one robiły.

Jeżeli chcemy zaimplementować instrukcje inicjalizujące, musimy po liście argumentów konstruktora, użyć dwukropka, podać nazwę pola, które chcemy zainicjalizować i jego wartość ujętą w nawiasy okrągłe.

 Rok()
 : miesiace(new Miesiac[12])
 , liczbaDni(7)
 /*
 zamiast średników stosuje się przecinki
 przy ostatniej instrukcji przecinka nie stosuje się
 */
 {}

Działa to podobnie jak użycie inicjalizowania w konstruktorze, jednak w przypadku instrukcji inicjalizujących pola będą zainicjalizowane w trakcie tworzenia klasy, a nie po utworzeniu jej obiektu.

Konstruktor kopiujący

[edytuj]

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;
             }
   };

Delegacja konstruktorów (C++11)

[edytuj]

W przypadku wielu wariantów konstruktorów często zdarza się, że muszą one powielać różne testy poprawności argumentów lub jakieś szczególne operacje konieczne do inicjalizacji obiektu. Niekiedy taki wspólny kod wyciąga się do osobnych prywatnych lub chronionych metod.

W C++11 dodano możliwość użycia na liście inicjalizacyjnej innych konstruktorów klasy.

Przyjrzyjmy się poniższej klasie - ma ona za zadanie przechowywać szczegóły dotyczące błędów składniowych np. w pliku konfiguracyjnym. Przechowuje numer linii, nazwę pliku i komunikat. Niekiedy jednak pliku nie ma, bo dane czytamy ze standardowego wejścia; czasem numer linii też nie jest dostępny, bo np. dopiero po przeczytaniu całego pliku wiadomo, że jest coś nie tak w strukturze.

Klasa zrealizowana bez delegacji konstruktorów.

class BladSkladniowy {
    
    int         numer_linii;
    std::string plik;
    std::string komunikat;         

    BladSkladniowy(int numer_linii, const std::string& plik, const std::string& komunikat)
        : numer_linii(numer_linii)
        , plik(plik)
        , komunikat(komunikat) {

        if (numer_linii < 0)   throw "niepoprawny numer linii";
        if (plik.empty())      throw "nazwa pliku nie może być pusta";
        if (komunikat.empty()) throw "komunikat nie może być pusty";
    }

    BladSkladniowy(int numer_linii, const std::string& komunikat)
        : numer_linii(numer_linii)
        , plik("<standardowe wejście>")
        , komunikat(komunikat) {

        if (numer_linii < 0)   throw "niepoprawny numer linii";
        if (komunikat.empty()) throw "komunikat nie może być pusty";
    }

    BladSkladniowy(const std::string& komunikat)
        : numer_linii(0)
        , plik("<standardowe wejście>")
        , komunikat(komunikat) {

        if (komunikat.empty()) throw "komunikat nie może być pusty";
    }
};

Przy użyciu delegacji kod skraca się znacząco:

class BladSkladniowy {
    
    int         numer_linii;
    std::string plik;
    std::string komunikat;         

    BladSkladniowy(int numer_linii, const std::string& plik, const std::string& komunikat)
        : numer_linii(numer_linii)
        , plik(plik)
        , komunikat(komunikat) {

        if (numer_linii < 0)   throw "niepoprawny numer linii";
        if (plik.empty())      throw "nazwa pliku nie może być pusta";
        if (komunikat.empty()) throw "komunikat nie może być pusty";
    }

    BladSkladniowy(int numer_linii, const std::string& komunikat)
        : BladSkladniowy(numer_linii, "<standardowe wejście>", komunikat) {}

    BladSkladniowy(const std::string& komunikat)
        : BladSkladniowy(0, komunikat) {}
};

Konstruktor explicit - zabronienie niejawnych konwersji (C++)

[edytuj]

Czasem niepożądane jest, żeby można było "przez przypadek" utworzyć klasę bądź przypisać do niej wartość. Jeśli klasa posiada konstruktor konwertujący, to kompilator jest w stanie wydedukować sposób na przekształcenie jednego typu w drugi, tj. dokonać niejawnej konwersji. Takie zachowanie nie zawsze jest pożądane i w dużych systemach jest dość trudne do przewidzenia i rozpoznania przez programistę.

Zobaczmy na przykład:

class Klasa {
public:
    Klasa(int x) {}
};

int main() {

    Klasa k(42);

    k = -1;
}

Ostatnie przypisanie choć wygląda dziwnie, nie jest błędem. Kompilator widzi, że z typu int może utworzyć Klasę, a także dla Klasy istnieje domyślny operator przypisania, więc ostatnia linijka zostanie zainterpretowana jako:

    k.operator=(Klasa(-1));

Do rozwiązania tego typu problemów w C++11 wprowadzono nowy klasyfikator dla konstruktorów explicit. Jeśli istnieje konstruktor z explicit wówczas utworzenie klasy, która byłaby wynikiem konwersji niejawnej stanie się niemożliwe.

class Klasa {
public:
    explicit Klasa(int x) {}
};

int main() {

    Klasa k(42);

    // k = -1; // błąd: kompilator nie wie jak skonwertować int na Klasa
}

Dopiero wprowadzenie jawnej konwersji operatorem static_cast umożliwia zastosowania konstruktora konwertującego. Taki operator jest dobrze widoczny w kodzie źródłowym i jasno oddaje intencje użycia:

    k = static_cast<Klasa>(-1);


Destruktor

[edytuj]

Destruktor jest natomiast funkcją, którą wykonuje się w celu zwolnienia pamięci przydzielonej dodatkowym obiektom lub innych zasobów.

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).

Ćwiczenia

[edytuj]

Ćwiczenie 1

[edytuj]

Napisz definicje instrukcji inicjujących do poniższej klasy:

   class Vector
   {
        private:
             double x;
             double y;
        public:
             Vector();
             Vector(double, double);
   };

Klasa ma reprezentować wektor w przestrzeni dwuwymiarowej, a instrukcje inicjujące mają realizować inicjalizację tego wektora. Pierwsze instrukcje inicjujące powinny ustawiać wektor na wartość domyślną (0,0).

Ćwiczenie 2

[edytuj]

Dopisz do kodu z poprzedniego ćwiczenia konstruktor kopiujący.

   Vector(const 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.

Ćwiczenie 3

[edytuj]

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
   {
        public:
             Vector* vectors;

             VectorsArray(size_t);
             Vector GetVector(size_t);
             size_t GetSize();
             size_t size;
   };

   VectorsArray::VectorsArray(size_t argSize)
   : size(argSize)
   , vectors(new Vector[argSize])
   {
   }
   Vector VectorsArray::GetVector(size_t i)
   {
        return vectors[i];
   }
   size_t VectorsArray::GetSize()
   {
      return size;
   }

Do powyższej klasy dopisz definicję destruktora. Nie zapomnij o dealokacji pamięci!


Dziedziczenie

[edytuj]

Wstęp - Co to jest dziedziczenie

[edytuj]

Często podczas tworzenia klasy napotykamy na sytuację, w której nowa klasa powiększa możliwości innej (wcześniejszej) 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).

Składnia

[edytuj]

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).

Przykład 1

[edytuj]

Definicja i sposób wykorzystania dziedziczenia

[edytuj]

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 Ryba. Każdy ptak i ryba jest zwierzęciem. Oprócz tego ptak może latać, a ryba płynąć. Wykorzystanie dziedziczenia wydaje się tu naturalne.

   class Ptak : public Zwierze
   {
        public:
             Ptak();
             void lec();
   };
 
   class Ryba : public Zwierze
   {
        public:
             Ryba();
             void plyn();
   };

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
 
   Ryba *ryba=new Ryba();
   ryba->jedz(); //metoda z klasy Zwierze
   ryba->plyn(); //metoda z klasy Ryba

Możemy też zrzutować obiekty klasy Ptak i Ryba na klasę Zwierze:

   Ptak *ptak=new Ptak();
   Zwierze *zwierze;
   zwierze=ptak;
   zwierze->jedz();
 
   Ryba ryba;
   ((Zwierze)ryba).jedz();

Jeżeli tego nie zrobimy, a rzutowanie jest potrzebne, kompilator sam wykona rzutowanie niejawne:

   Zwierze zwierzeta[2];
   zwierzeta[0] = Ryba(); //rzutowanie niejawne
   zwierzeta[1] = Ptak(); //rzutowanie niejawne
   for (int i = 0; i < 2; ++i)
        zwierzeta[i].jedz();

Dostęp do elementów przykrytych

[edytuj]

Elementy chronione - operator widoczności protected

[edytuj]

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 dziedziczonej ale poza klasą dziedziczoną i klasą bazową nie są widoczne.

Elementy powiązane z dziedziczeniem

[edytuj]

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).

Funkcje wirtualne

[edytuj]

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.

Wielodziedziczenie - czyli dziedziczenie wielokrotne

[edytuj]

Język C++ umożliwia dziedziczenie po wielu klasach bazowych na raz. Proces ten jest opisany w rozdziale Dziedziczenie wielokrotne.

== Przykład 2==efewfewqfewfewfewfcsdc

 #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 << "Chlip, chlip\n";
       }
 
       void spij( )
       {
           std::cout << "Chrr...\n";
       }
 };
 
 class Pies : public Zwierze
 {
    public:
      Pies()
      { }
 
      void szczekaj()
      {
         std::cout << "Hau! hau!...\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;
 }

Zabronienie dziedziczenia

[edytuj]

Niekiedy zachodzi potrzeba uniemożliwienia dziedziczenia po podanej klasie. Przed C++11 można to było uzyskać trochę naokoło:

  • utworzyć prywatny konstruktor,
  • dodać do klasy statyczną metodę tworzącą instancję.

Tu przykład:

class Klasa {
private:
    Klasa();

public:
    static Klasa* utworz() {
        return new Klasa();
    }
};

class Pochodna: public Klasa {};


int main() {
    // Pochodna p; // błąd kompilacji: konstruktor jest prywatny

    Klasa* k = Klasa::utworz();
    delete k;

    return 0;
}

Problemy z tym podejściem są co najmniej trzy:

  • Po pierwsze jest to mocno nieczytelne,
  • Po drugie jeśli klasa ma więcej konstruktorów trzeba dla każdego pisać nową wersję metody utworz.
  • Po trzecie błąd kompilacji pojawi się dopiero przy instantacji klasy. Powyższy program się kompiluje, dopiero odkomentowanie pierwszego wiersza main powoduje błąd, który jedynie stwierdzi, że konstruktor klasy jest prywatny.

W C++11 problem ten został usunięty, wprowadzono słowo kluczowe final, które dodane po nazwie klasy powoduje, że dziedziczenie stanie się w ogóle niemożliwe. Poniższy program nie kompiluje się, a kompilator powinien jasno podać przyczynę:

class Klasa final {
public:
    Klasa();
};

class Pochodna: public Klasa {};


Składniki statyczne

[edytuj]

Wstęp

[edytuj]

Czasami zachodzi potrzeba dodania elementu, który jest związany 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.

Składnia

[edytuj]

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 liczbaInstancji; // pole statyczne
        public:
             Klasa() 
             {
                  liczbaInstancji++;
             }
             virtual ~Klasa() 
             {
                  liczbaInstancji--;
             }
             static int LiczbaInstancji()
             {
                  return liczbaInstancji;
             }
   };
 
   int Klasa::liczbaInstancji = 0;

Jak widać do obiektów statycznych z wewnątrz klasy możemy się odwołać tak samo jak do innych pól. Pole liczbaInstancji 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 liczbaInstancji 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 LiczbaInstancji z programu wymaga następująco:

  int i=Klasa::LiczbaInstancji();

Gdyby zaś pole liczbaInstancji było publiczne, a nie chronione, to moglibyśmy się do niego odwołać poprzez:

  int i=Klasa::liczbaInstancji;

Ponieważ jednak w powyższym przykładzie pole liczbaInstancji 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

Funkcje wirtualne

[edytuj]

Wstęp

[edytuj]

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.

Chyba największą zaletą polimorfizmu jest to, że stanowi on „lepszą wersję” instrukcji warunkowych. Lepszą, bo wszelkie decyzje zależne od typu obiektu podejmowane są przez programistę tylko jeden raz: podczas tworzenia obiektu. W klasycznym podejściu, znanym z języka C, typ obiektu przechowuje się np. jako typ wyliczeniowy. Jest to proste i szybkie, ale zarazem kłopotliwe, bo przy każdej operacji zależnej od typu programista musi wstawić np. nowy blok 'if' lub 'case'. Gdy zachodzi potrzeba zmodyfikowania programu, np. dodania nowego typu obiektu, trzeba też znaleźć wszystkie miejsca, gdzie wstawiony był jakiś 'if', sprawdzający typ obiektu! Przy odpowiednio dużym programie wprowadzenie modyfikacji staje się w ten sposób bardzo trudne. Korzystając z polimorfizmu, programista tworzy po prostu obiekt odpowiedniej klasy, a wszystkie późniejsze decyzje, do której konkretnie funkcji skoczyć, podejmowane są już automatycznie, na podstawie informacji dostarczonych przez kompilator. Dzięki temu programista unika wielu możliwości popełnienia błędu, a dodanie nowego typu obiektu sprowadza się do napisania nowej klasy – nie trzeba już przekopywać się przez cały kod.

Opis

[edytuj]

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 sięgnął po funkcję pisz z klasy bazowej.

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;
   }
};

Konsekwencje

[edytuj]

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ść 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.

Przykład

[edytuj]

Poniższy program zawiera deklaracje 3 klas: Figura, Kwadrat i Kolo. W klasie Figura została zadeklarowana metoda wirtualna (słowo kluczowe virtual) virtual float pole(). Każda z klas pochodnych od klasy Figura ma zaimplementowane swoje metody float pole(). Następnie (w funkcji main) znajdują się deklaracje obiektów każdej z klas i wskaźnika mogącego pokazywać na obiekty klasy bazowej Figura.

 #include <iostream>

 const float pi = 3.14159;
 class Figura 
 {
   public:
     virtual float pole() const 
     {
       return -1.0;
     }
 };

 class Kwadrat : public Figura 
 {
   public:
     Kwadrat( const float bok ) : a( bok ) {}

     float pole() const 
     {
       return a * a;
     }
   private:
     float a; // bok kwadratu
 };

 class Kolo : public Figura 
 {
   public:
     Kolo( const float promien ) : r( promien ) {}

     float pole() const 
     {
       return pi * r * r;
     }
   private:
     float r; // promien kola
 };

 void wyswietlPole( Figura &figura ) 
 {
   std::cout << figura.pole() << std::endl;
   return;
 }

 int main() 
 {
   // deklaracje obiektow:
   Figura jakasFigura;
   Kwadrat jakisKwadrat( 5 );
   Kolo jakiesKolo( 3 );
   Figura *wskJakasFigura = 0; // deklaracja wskaźnika

   // obiekty -------------------------------
   std::cout << jakasFigura.pole() << std::endl; // wynik: -1
   std::cout << jakisKwadrat.pole() << std::endl; // wynik: 25
   std::cout << jakiesKolo.pole() << std::endl; // wynik: 28.274...

   // wskazniki -----------------------------
   wskJakasFigura = &jakasFigura;
   std::cout << wskJakasFigura->pole() << std::endl; // wynik: -1
   wskJakasFigura = &jakisKwadrat;
   std::cout << wskJakasFigura->pole() << std::endl; // wynik: 25
   wskJakasFigura = &jakiesKolo;
   std::cout << wskJakasFigura->pole() << std::endl; // wynik: 28.274...
 
   // referencje -----------------------------
   wyswietlPole( jakasFigura ); // wynik: -1
   wyswietlPole( jakisKwadrat ); // wynik: 25
   wyswietlPole( jakiesKolo ); // wynik: 28.274...

   return 0;
 }

Wywołanie metod składowych dla każdego z obiektów powoduje wykonanie metody odpowiedniej dla klasy danego obiektu. Następnie wskaźnikowi wskJakasFigura zostaje przypisany adres obiektu jakasFigura i zostaje wywołana metoda float pole(). Wynikiem jest "-1" zgodnie z treścią metody float pole() w klasie Figura. Następnie przypisujemy wskaźnikowi adres obiektu klasy Kwadrat - możemy tak zrobić ponieważ klasa Kwadrat jest klasą pochodną od klasy Figura - jest to tzw. rzutowanie w górę. Wywołanie teraz metody float pole() dla wskaźnika nie spowoduje wykonania metody zgodnej z typem wskaźnika - który jest typu Figura* lecz zgodnie z aktualnie wskazywanym obiektem, a więc wykonana zostanie metoda float pole() z klasy Kwadrat (gdyż ostatnie przypisanie wskaźnikowi wartości przypisywało mu adres obiektu klasy Kwadrat). Analogiczna sytuacja dzieje się gdy przypiszemy wskaźnikowi adres obiektu klasy Kolo. Następnie zostaje wykonana funkcja void wyswietlPole(Figura&) która przyjmuje jako parametr obiekt klasy Figura przez referencję. Tutaj również zostały wykonane odpowiednie metody dla obiektów klas pochodnych a nie metoda zgodna z obiektem jaki jest zadeklarowany jako parametr funkcji czyli float Figura::pole(). Takie działanie jest spowodowane przez przyjmowanie obiektu klasy Figura przez referencję. Gdyby obiekty były przyjmowane przez wartość (parametr bez &) zostałaby wykonana 3 krotnie metoda float Figura::pole() i 3 krotnie wyświetlona wartość -1.

Wyżej opisane działanie zostało spowodowane przez określenie metody w klasie bazowej jako wirtualnej. Gdyby zostało usunięte słowo kluczowe virtual w deklaracji metody w klasie bazowej, zostałyby wykonane metody zgodne z typem wskaźnika lub referencji, a więc za każdym razem zostałaby wykonana metoda float pole() z klasy Figura.

Rzutowanie dynamiczne - dynamic_cast

[edytuj]

Rzutowanie dynamiczne pozwala w czasie wykonywania konwertować wskaźniki lub referencje klas bazowych do klas pochodnych - jest to tzw. rzutowanie w dół (hierarchii). Rzutowanie to realizuje operator dynamic_cast, jednak dostępny jest jedynie dla klas posiadających metody wirtualne (klasy polimorficzne). Ogólnie C++ pozwala na odczytywanie informacji o zależnościach między klasami polimorficznymi, jest to tzw. RTTI (ang. RunTime Type Information), dynamic_cast korzysta z tych danych.

Jakie jest zastosowanie takiego rzutowania? Wyobraźmy, że posiadamy listę figur z przykładu. Figura jednak udostępnia jedynie swój interfejs, a my np. chcielibyśmy wykonać jakieś działanie wyłącznie na obiektach typu Kwadrat. Dzięki dynamic_cast możemy sprawdzić, czy figura jest odpowiedniego typu, dokonać konwersji i używać obiektu Kwadrat w żądany sposób.

Figura* figura = new NazwaFigury(...);

Kwadrat* kwadrat = dynamic_cast<Kwadrat*>(figura);
if (kwadrat)
{
    // działania na kwadracie
}
else 
{
    std::cout << "figura nie jest kwadratem" << '\n';
}

Wynikiem poprawnego rzutowania wskaźników jest niepusty wskaźnik. Jeśli rzutowanie jest niemożliwe wskaźnik jest pusty.

Z kolei wynikiem rzutowania referencji może być tylko referencja, niemożliwość konwersji sygnalizowana jest wyjątkiem std::bad_cast.

Metody i klasy abstrakcyjne

[edytuj]

Niekiedy tworząc klasy nie wiadomo, jak jedną lub więcej metod zrealizować. Np. są to metody mające zapisywać wyniki - a one mogą być zapisywane do pliku, na konsolę, wysyłane przez sieć, być może użytkownik będzie chciał dostać dodatkowe podsumowanie itp. Czyli dana metoda musi się wykonać, ale z punktu widzenia projektu klasy nie chcemy bądź nie możemy wnikać w szczegóły jej działania.

Wówczas można użyć metod abstrakcyjnych, które posiadają jedynie deklarację (zakończoną dodatkowo "= 0"); takie metody można wywoływać w innych metodach. Klasa posiadająca przynajmniej jedną metodę abstrakcyjną staje się klasą abstrakcyjną i nie można utworzyć instancji takiej klasy. Jedynym sposobem na utworzenie instancji jest odziedziczenie po takiej klasie i dodanie definicji wszystkich metod abstrakcyjnych. Oczywiście możliwe jest dziedziczenie, gdy nie definiuje się wszystkich metod wirtualnych, wówczas taka klasa pochodna nadal jest abstrakcyjna.

Przykład deklaracji:

class KlasaAbstrakcyjna
{
    virtual int wyswietl() = 0;
};

Nadpisywanie metod wirtualnych - override (C++11)

[edytuj]

Dodanie do klasy pochodnej metody wirtualnej o tej samej nazwie co metoda w klasie bazowej, ale innym zestawie argumentów jest jak najbardziej możliwe - mamy wówczas do czynienia z przeciążeniem nazw funkcji i to od parametrów wywołania zależy, która metoda zostanie uruchomiona.

Jednak dodawanie metody o tej samej nazwie ma w 99% przypadków jeden cel - nadpisanie metody w klasie pochodnej. Problemem jest gdy lista parametrów się nie zgadza (na skutek pomyłki, zmian w klasie bazowej, itp.), wtedy wbrew intencjom wprowadzona jest nowa metoda. Aby zapobiec takim problemom od wersji C++11 dostępny jest nowy kwalifikator metod override, który jasno mówi kompilatorowi, że metodę o podanej nazwie chcemy nadpisać. Jeśli metody o tej nazwie nie ma w klasie bazowej, bądź posiada inną sygnaturę, wówczas zgłaszany jest błąd kompilacji.

class Bazowa
{
    virtual void wyswietl(int);
};

class Pochodna: public Bazowa
{
    virtual void wyswietl(int) override;

    // błąd: różne sygnatury
    // virtual void wyswietl(double) override

    // błąd: brak metody
    // virtual void drukuj() override
};


Programowanie orientowane obiektowo

[edytuj]

Sekcja „C++/Programowanie orientowane obiektowo” znajduje się w budowie

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

Obiekty stałe

[edytuj]

Obiekty stałe to takie, których stan - z punktu widzenia interfejsu klasy - nie może się zmienić; obiekt stały można rozumieć jako widok na dane, które można jedynie czytać. To rozróżnienie, wspierane wprost przez język, ma jeden cel: uniemożliwić modyfikację, także przypadkową; dodatkowo kompilatory potrafią wykorzystać te informacje przy optymalizacji kodu.

Obiekty mogą być stałe już w chwili deklaracji (np. napisy), albo stać się takie w obrębie funkcji do której zostały przekazane jako argument. Aby zadeklarować obiekt stały należy poprzedzić nazwę typu słowem kluczowym const:

const Klasa obiekt;
const std::string = "wikibooks.pl";

Analogicznie przy przekazywaniu argumentu do funkcji:

void funkcja(const Klasa obiekt) {
    // działania na obiekcie
}

Można bardziej formalnie powiedzieć, że typy Klasa oraz const Klasa są różne; co więcej, konwersja z typu Klasa na const Klasa jest dopuszczalna, odwrotna jest zabroniona.

Na obiekcie stałym można wywołać jedynie metody oznaczone jako stałe oraz wyłącznie czytać pola, jeśli takie są publicznie dostępne. Metoda jest stała jeśli została zadeklarowana z klasyfikatorem const - w przykładowej klasie poniżej taką metodą jest wartosc.

Metody stałe mogą wywoływać tylko inne metody stałe i odczytywać pola (z małym wyjątkiem, o czym w kolejnych sekcjach) - zapis wartości do pól obiektu oraz wołanie nie-stałych metod jest zabronione. UWAGA: To ograniczenie nie zależy od tego, czy sam obiekt na którym wołana metoda jest stały.

class Klasa {
private:
    int liczba;

public:
    Klasa() : liczba(0) {}

    void dodaj(int x) {
        liczba += x;
    }

    void zmien_znak() {
        liczba = -liczba;
    }

    int wartosc() const {
        return liczba;
    }
};

int main() {

    const Klasa obiekt;

    // obiekt.dodaj(42);    // niemożliwe, metoda zmienia obiekt
    // obiekt.zmien_znak()  // niemożliwe, metoda zmienia obiekt
    
    return obiekt.wartosc(); // wartosc jest const, tylko odczyt
}

Istotne jest, że już istniejący obiekt może być używany jako stały. Funkcja wyswietl nie zmieni szerokości ani wysokości, jednak legalnie wywołuje inną funkcję, która również jedynie czyta parametry prostokąta.

#include <iostream>

class Prostokat {
public:
    int szerokosc;
    int wysokosc;
};

int pole(const Prostokat& p) {
    return p.szerokosc * p.wysokosc;
}

void wyswietl(const Prostokat& p) {
    std::cout << p.szerokosc << " x " << p.wysokosc << ", pole = " << pole(p) << '\n';
}

int main() {
    Prostokat p;

    p.szerokosc = 12;
    p.wysokosc  = 5;

    wyswietl(p);

    p.wysokosc  = 6;

    wyswietl(p);
}

Stałe pola klasy

[edytuj]

Pola klasy również mogą być zadeklarowana jako stałe, ich wartości muszą zostać ustawione na liście inicjalizacyjnej.

class Terminal {
    const int kolumny;
    const int wiersze;

public:
    Terminal() : kolumny(80), wiersze(25) {

        //kolumny = 80; // mimo, że w konstruktorze,
        //wiersze = 25; // to przypisanie niemożliwe
    }
};

Pola mutable

[edytuj]

Niekiedy istnieje potrzeba, aby nawet stały obiekt mógł zmieniać swój wewnętrzny, niepubliczny stan. Można pomyśleć o algorytmach ze spamiętywaniem (ang. memoization), częstym przykładem jest też cache dla niezmieniającej się kolekcji. Metoda wyszukująca istotnie nie ma prawa zmienić samej kolekcji, ale mogłaby zapisywać wynik kilku ostatnich wyszukiwań i szybciej dawać odpowiedź. Z punktu widzenia użytkownika klasy nic się nie zmienia, ponieważ wyniki metody będą zawsze takie same, niezależnie od tego, czy zapytanie trafi w cache, czy nie (zakładając oczywiście bezbłędną implementację całości).

Zacznijmy od klasy bez pamięci podręcznej:

#include <vector>

class Kolekcja {

    std::vector<int> wartosci;
    
public:
    int indeks(int wartosc) const {
        for (auto i=0; i < wartosci.size(); i++) {
            if (wartosc == wartosci[i]) {
                return i;
            }
        }

        return -1; // brak danych
    }
};

(Celowo został tu użyty nieoptymalny algorytm wyszukiwania liniowego, żeby wykazać potrzebę zastosowania pamięci podręcznej. Normalnie należałoby użyć typu std::map lub std::unordered_map albo jakiejś własnej, lepszej struktury danych.)

Teraz klasa, która ma cache. Najistotniejsze są tutaj dwa pola: ostatnia_wartosc i ostatni_wynik, oba zostały poprzedzone słowem kluczowym mutable, to znaczy, że zgadzamy się, żeby metody stałe je modyfikowały.

class KolekcjaZCache: public Kolekcja {

    mutable int ostatni_wynik;
    mutable int ostatnia_wartosc;
    
public:
    int indeks(int wartosc) const {
        if (wartosc == ostatnia_wartosc) {
            return ostatni_wynik;
        }

        ostatnia_wartosc = wartosc;
        ostatni_wynik = Kolekcja::indeks(wartosc);

        return ostatni_wynik;
    }
};

Przeciążanie operatorów

[edytuj]

Przeładowanie (przeciążanie) operatorów polega na nadaniu im nowych funkcji.

Trochę teorii na wstępie

[edytuj]

Przeładowywanie operatorów, jest to definiowanie operatorów dla własnych typów. Można tego dokonać w większości przypadków jako metodę składową lub jako metodę globalną. Przeładowywać możemy następujące operatory: (pełne i wyczerpujące zestawienie operatorów)

+ // operator dodawania, może być jedno lub dwuargumentowy
- // operator odejmowania, może być jedno lub dwuargumentowy
* // operator mnożenia (dwuargumentowy) lub operator
/
% // operator modulo (dwuargumentowy)
^
& // operator and logiczne (dwuargumentowy), lub operator uzyskania adresu (jednoargumentowy) gdy go nie zdefiniujemy robi to za nas kompilator
~
!
= // operator przypisania, gdy go nie zdefiniujemy robi to za nas kompilator
<
>
+=
-=
*=
/=
%=
^=
&=
|=
<<
>>
>>=
<<=
==
!=
<=
>=
&&
||
++
--
,  // przecinek, gdy go nie zdefiniujemy robi to za nas kompilator
->*
->
() // operator wywołania funkcji (ile-chcemy-argumentowy)
[]
new      // ponizsze operatory gdy ich nie zdefiniujemy robi to za nas kompilator
new[]
delete
delete[]

Nie można przeładowywać:

.  // odniesienie do składowej klasy
.* // tak wybieramy składnik wskaźnikiem
:: // operator zakresu
?: // operator zwracający wartość zależnie od spełnienia warunku
static_cast, dynamic_cast, reinterpret_cast, const_cast
sizeof // pobranie rozmiaru danego typu, lub jego wewnętrznej składowej

Parę uwag co do operatorów: Nie można zmienić ich priorytetów, argumentowości, argumenty operatorów nie mogą być domniemane, redefiniować operatory można gdy co najmniej jeden argument jest typu zdefiniowanego przez użytkownika. operatory =, [], (), -> muszą być niestatycznymi funkcjami składowymi w danej klasie.

Nie wszystkie operatory mogą być zdefiniowane jako oddzielna funkcja, oto operatory, które mogą być zdefiniowane wyłącznie jako metody:

=
[]
->

Użycie

[edytuj]

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.


Przykład zastosowania

[edytuj]

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) {}
 };

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) {}
       friend ostream & operator<< (ostream &wyjscie, const Student &s);
 };
 
 ostream & operator<< (ostream &wyjscie, const Student &s) {
   return wyjscie << "Nr indeksu: " <<s.nr_indeksu << endl << "Srednia ocen: " <<s.srednia_ocen<<endl;
 }

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,
               // ponieważ są to wartosci domyślne konstruktora :)
  
   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). Funkcję F() deklarujemy jako zaprzyjaźnioną z klasą X, jeśli chcemy, aby F() miała dostęp do prywatnych lub chronionych danych składowych klasy X.

Ale weźmy przykład nieco prostszy. Chcemy sprawdzić czy to jest ten sam student - przeciążamy operator:

 class Student {
    //...
    public:
       bool operator==(const Student &q) {return nr_indeksu==q.nr_indeksu;}
       bool operator==(const int &q) {return nr_indeksu==q;}
 };

I niby wszystko jest pięknie, ale tu zaczynają się schody... My, jako twórcy klasy wiemy, że porównanie dotyczy 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:
      bool operator< ( Student const &q) const {return srednia_ocen < q.srednia_ocen;}
      bool operator< (int const &q) const {return srednia_ocen < 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 &q) {return (srednia + q.srednia +11) };
       int operator+ ( int &q) {return (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.

Operatory "bool" i "!"

[edytuj]

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;
template <typename T, int el>
class Tablica {
       public:
         Tablica() : L_elementow(el) {}
         operator bool() const {return (L_elementow != 0);}
         bool operator!() const {return (L_elementow == 0);}

       private:
         T Tab[el];
         size_t L_elementow;
    };

int main() {
  const int n = 5;
  Tablica <short, n> tab;

  if(tab)
    cout << "Tablica nie jest pusta." << endl;
  if(!tab)
    cout << "Tablica jest pusta." << endl;

  Tablica <short, 0> tab2;

  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 !.

Oczywiście ładniej by było, gdybyśmy sprawdzali rozmiar przy użyciu if-else, zamiast dwóch if-ów, ale chciałem pokazać wywołanie obu operatorów.

Operator "[]"

[edytuj]

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 Tablica możemy dopisać do niej następujące dwa operatory:

  T & operator[](size_t el) {return Tab[el];}
  const T & operator[](size_t el) const {return Tab[el];}

oraz testową funkcję main

int main() {
  const n = 5;
  TablicaInt <short, n> tab;

  for(int i = 0; i < n; ++i) {
      tab[i] = i;
      cout << tab[i] << endl;
    }
  return 0;
}

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. Zwróć uwagę na słowo kluczowe const w definicji funkcji składowej klasy: " operator[] (int el) const ". Modyfikator const stanowi część sygnatury funkcji. const zapewnia nam nie tylko właściwości funkcji składowej const, ale również umożliwia przeładowanie (przeciążenie) operatora.

Operator "()"

[edytuj]

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ć zadeklarowany 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.

New i delete

[edytuj]

Są to operatory traktowane jako metody statyczne klas (niezależnie czy napiszemy słówko static czy nie), przykład:

class Foo {
  int i;
public:
    void * operator new(size_t rozmiar) { // słówka static nie muszę umieszczać, zrobi to za mnie kompilator
      return (new char[rozmiar]); // zawarte tutaj new skorzysta z GLOBALNEGO (dla wszystkiego tworzonego za jego pomocą) operatora new
    }
    void operator delete(void* wsk) {
      delete wsk;
    }
    Foo(int j = 0) : i(j) {}
    void* operator new[](size_t rozmiar) {
      return (new char[rozmiar]); // należy pamiętać aby nasz obiekt miał koniecznie konstruktor bezargumentowy
    }
    void operator delete[](void* wsk) {
      delete[] wsk;
    }
};

Kilka uwag: 1. Operatory new i new[], oraz delete i delete[] nie są przemienne, czyli jak zdefiniujemy własny new to new[] jest nadal domyślny. 2. Mimo zdefiniowania własnego operatora new i delete możemy nadal używać globalnych wersji:

Foo *f1 = new Foo(); // wywoła naszą wersję
Foo *f2 = ::new Foo(); // wywoła wersję globalną

Kiedy może się nam przydać własny operator new i delete? M. in. jak chcemy alokować za pierwszym razem większą pamięć a potem tylko zwracać wskaźnik do następnego jej fragmentu. Może być też przydatne gdy chcemy mieć kontrolę utworzenia jednej instancji obiektu.


W powyższym przykładzie pokazałem jak przeciążyć ten operator dla własnej klasy, natomiast jest jeszcze możliwość przeciążenia globalnego, czyli dla każdego obiektu w programie od jego uruchomienia do wyłączenia -odpowiedzialność jest więc wielka. Musimy się też liczyć z tym że w obrębie definicji nie możemy zrobić wszystkiego, czyli m.in. nie możemy używać strumieni cout (one korzystają z operatora new), oczywiście kompilator nie zaprotestuje natomiast podczas wykonania programu pojawi się problem. Przykład:

#include <cstdlib> // biblioteka zawierająca funkcje malloc() i free()

void* operator new(size_t rozmiar) {
  void* wsk = malloc(rozmiar);
  return wsk;
}
void operator delete(void* wsk) {
  free(wsk);
}
void* operator new[](size_t rozmiar) {
  return wsk;
}
void operator delete[](void* wsk) {
  free(wsk);
}

Autor "Symfonii C++", na której się opieram pisząc o new i delete, stanowczo odradza przeciążanie tego operatora globalnie

Operatory post- i pre- inkrementacji, oraz dekrementacji

[edytuj]

Tutaj umieszczę same przykłady:

class Foo {
  int i;
public:
  Foo(int j): i(j) {}
  
  Foo & operator++() { // preinkrementacje, czyli najpierw  zwiększamy a potem zwracamy
    ++i;
    return *this;
  }
  Foo operator++(int) {  // specjalny zapis do postinkrementacji
    Foo kopia = (*this);
    ++i;
    return kopia; // zwracamy kopię, a nie oryginał
  }
};

Operatory dekrementacji analogicznie do inkrementacji. Należy mieć na uwadze, że przy własnych operatorach potrzebny jest "działający jak chcemy" operator= lub konstruktor kopiujący, jeśli go nie napiszemy kompilator wygeneruje go automatycznie, natomiast jeśli nasza klasa ma wewnątrz siebie wskaźniki tak w skopiowanym obiekcie będą one wskazywały na ten sam adres co wskaźniki oryginału

Konwersje obiektów

[edytuj]

C++ pozwala na przeciążanie operatorów konwersji, co pozwala na niejawną konwersję klasy na inny typ. Istnieją dwa typy operatorów konwersji: operator konwersji na typ i operator konwersji na bool.

Operator konwersji na typ jest funkcją członkowską, która konwertuje obiekt klasy na określony typ. Składnia tego operatora to:

operator type() const;

Rozważmy na przykład klasę MyClass, która reprezentuje liczbę zespoloną. Operator konwersji na podwójne można zdefiniować w następujący sposób:

class MyClass {
public:
    operator double() const {
        return real_;
    }
private:
    double real_;
    double imag_;
};

Pozwala to na niejawną konwersję obiektu MyClass na obiekt typu double, gdy jest to konieczne.

Operator konwersji na bool jest funkcją członkowską, która określa, czy obiekt klasy jest uważany za prawdziwy, czy fałszywy. Składnia tego operatora to:

explicit operator bool() const;

Słowo kluczowe explicit służy do zapobiegania niejawnym konwersjom na bool. Na przykład:

class MyClass {
public:
    explicit operator bool() const {
        return (real_ != 0.0 || imag_ != 0.0);
    }
private:
    double real_;
    double imag_;
};

W takim przypadku obiekt MyClass zostanie uznany za prawdziwy, jeśli jego część rzeczywista lub urojona jest różna od zera.

Konstruktora można również użyć jako metody konwersji. Jest to znane jako jawny konstruktor. Konstruktor jawny to konstruktor, którego można używać tylko do jawnych konwersji. Składnia jawnego konstruktora to:

explicit ClassName(parameters);

Na przykład:

class MyClass {
public:
    explicit MyClass(int x) : value_(x) {}
private:
    int value_;
};

W takim przypadku konstruktora MyClass można użyć do jawnej konwersji typu int na obiekt MyClass.

Korzystanie z jawnych konstruktorów i operatorów konwersji może poprawić bezpieczeństwo i przejrzystość kodu, utrudniając przypadkowe wykonanie niechcianych konwersji.

Klasy i typy zagnieżdżone

[edytuj]

W C++ klasy służą do definiowania typów zdefiniowanych przez użytkownika. Klasa to zbiór elementów danych i funkcji składowych, które działają na tych elementach danych. Aby utworzyć klasę w C++, możesz użyć słowa kluczowego class, po którym następuje nazwa klasy i treść klasy, która zawiera składowe danych i funkcje składowe:

class MyClass {
public:
    // member functions
private:
    // data members
};

Słowo kluczowe public określa, że członkowie zadeklarowani po nim mogą być dostępni spoza klasy. Słowo kluczowe private określa, że członkowie zadeklarowani po nim mogą być dostępni tylko z poziomu klasy.

Oprócz funkcji członkowskich i członków danych

Dziedziczenie wielokrotne

[edytuj]

Język C++, w odróżnieniu od wielu popularnych języków, np. Javy, dopuszcza dziedziczenie wielobazowe (dziedziczenie wielokrotne), tj. klasa może dziedziczyć po więcej niż jednej klasie. To powoduje, że w ogólnym przypadku nie mamy do czynienia z drzewiastą hierarchią klas, lecz skierowanym grafem acyklicznym dziedziczenia.

Klasa pochodna ma dostęp do wszystkich pól z klas bazowych oraz udostępnia pola i metody zgodnie z podanymi regułami widoczności (private/protected/public). Jeśli pola lub metody w klasach bazowych powtarzają się, wówczas konieczna jest dodatkowa klasyfikacja nazwą klasy, jak w przykładzie poniżej.

#include <iostream>

class BazowaA {
protected:
    int licznik;
public:
    void wyswietl() {
        std::cout << "A::wyswietl()" << '\n';
    }
};

class BazowaB {
protected:
    int licznik;
public:
    void wyswietl() {
        std::cout << "B::wyswietl()" << '\n';
    }
};

class Klasa: public BazowaA, public BazowaB {
public:
    void wyzeruj() {
        BazowaA::licznik = 0;
        BazowaB::licznik = 0;
    }
};

int main() {
    Klasa k;

    k.BazowaA::wyswietl();
    k.BazowaB::wyswietl();
}


Dziedziczenie wirtualne

[edytuj]

Dziedzicznie wirtualne jest specjalnym przypadkiem w dziedziczeniu wielobazowym, które stosuje się, gdy z jakiegoś powodu jedna z klas staje się wielokrotnie przodkiem (bezpośrednio lub pośrednio) innej klasy. Np.

class Bazowa {
protected:
    void wyswietl() {};
};

class Posrednia: public Bazowa {};

class Klasa: public Bazowa, public Posrednia {};

Tutaj to klasa Bazowa jest przodkiem Klasy: raz bezpośrednio, raz poprzez klasę Pośrednią. Oczywiście nie jest to błąd, ale niesie ze sobą następujące niedogodności:

  • Pola z klasy Bazowa powtarzają się 2 razy, skutkiem czego sizeof(Klasa) > 2 * sizeof(Bazowa). To na pierwszy rzut oka może wydawać się mało istotne, bo zwykle rozmiar klasy jest niewielki. Jednak co gdy przechowujemy tysiące lub miliony instancji? Wówczas ten narzut może okazać się znaczący.
  • Zwykle też chcemy, aby to właśnie klasa Bazowa dostarczała pewnych metod lub pól dla klas pochodnych, a w tym przypadku odwołanie do metody Wyswietl musi być kwalifikowane nazwą klasy nadrzędnej, co jest mało wygodne.

Zatem jeśli chcemy wyraźnie wskazać, że jakaś klasa jest bazową dla wszystkich potomków, to przy w deklaracji musimy dodać słowo kluczowe virtual przy nazwie klasy z której dziedziczymy. Wówczas zniknie niejednoznaczność odwołań do jej składowych, natomiast jej pola zostaną umieszczone w klasie pochodnej tylko raz. W przykładzie poniżej rozmiar Klasy to 1kB + kilka bajtów, bez dziedziczenia wirtualnego byłoby to ponad 2kB, oraz oczywiście wywołanie metody wyswietl (używając takiego zapisu) byłoby niemożliwe.

class Bazowa {
    char bufor1kB[1024];
public:
    void wyswietl() {}
};

class Posrednia1: public virtual Bazowa {};

class Posrednia2: public virtual Bazowa {};

class Klasa: public Posrednia1, public Posrednia2 {};

int main() {
    Klasa k;

    std::cout << sizeof(Klasa) << '\n';

    k.wyswietl();
}
Część 4
Zawansowane konstrukcje językowe

Obsługa wyjątków

[edytuj]

Wstęp

[edytuj]

Wyjątki pozwalają zareagować na sytuacje, w których istnieje ryzyko niewykonania określonego zadania.

Zarys wyjątków

[edytuj]

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.

Szkielet obsługi wyjątków

[edytuj]

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(std::string 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, std::string 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.

Rzucanie wyjątku

[edytuj]

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)    {
                 std::string wyjatek = "dzielenie przez zero!"; //przez zero się nie dzieli
                 throw wyjatek;  //rzucamy wyjątek
             }
             return a / b;
         }

Po instrukcji throw umieszczamy obiekt który chcemy rzucić (u nas jest to std::string). 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>
        #include<string>
        #include<cmath>
        using namespace std;
        double Dziel(double, double);
        int main()
        {
            try
            {
                Dziel(10, 0);
            }
            catch(string w)
            {
                cout<<"Wyjatek: "<<w;
            }
            cin.get();
            return 0;
         }
         double Dziel(double a, double b) //funkcja zwraca iloraz a / b
        {
            if (b == 0)    {
                 string wyjatek = "dzielenie przez zero!";
                 throw wyjatek;
            }
            return a / b;
        }

Jak widać, utworzony jest tylko jeden blok catch, a to dlatego że funkcja Dziel rzuca tylko wyjątki typu std::string. Możliwe jest pisanie obok deklaracji funkcji jakie wyjątki może ona rzucać:

       void fun(int) throw(std::string)

zapis ten oznacza, że funkcja fun może zwrócić wyjątek typu std::string. Stanowi to jednak zły zwyczaj i zaleca się jego unikanie.


Funkcje anonimowe (lambdy)

[edytuj]

Od początku istnienia C++, możliwy był zapis programów w postaci funkcji wolnych lub składowych klas. Nie istniała jednak możliwość tworzenia funkcji w innych formach, np. funkcji lokalnych. Można było posiłkować się klasami zagnieżdżonymi, ale było zwykle dalekie od wygody.

Standard C++11 dodał bardzo ważną możliwość tworzenia funkcji anonimowych, czasem nazywanych lambdami. Funkcje anonimowe mogą być przekazywane jako parametry, mogą być przypisywane do zmiennych itp. Regułą jest, że funkcje tego typu są krótkie.

Składnia

[edytuj]

Funkcja anonimowa składa się z czterech elementów:

  1. definicji domknięcia (ang. closure),
  2. listy parametrów,
  3. opcjonalnego typu zwracanego,
  4. bloku kodu.
[domknięcie](parametry) -> typ zwracany {
    
    kod
}

Domknięcie określa jakie zmienne i w jakim trybie są dostępne bezpośrednio w danej funkcji. W najprostszym przypadku domknięcie może być puste, wówczas funkcja nie ma dostępu do żadnych obiektów zdefiniowanych w zakresie, który ją zawiera. Najprościej można wskazać, że wszystkie obiekty są dostępne przez referencję, pisząc [&]; można również wskazać, że wszystkie dostępne są to kopie, [=]. Można też wyliczyć tylko te zmienne, które są rzeczywiście potrzebne, np. [x, y]. Zmienne domyślnie są dostępne tylko do odczytu, ale jeśli poprzedzimy je znakiem & zostaną przekazane przez referencję i będzie można je zmieniać.

Lista parametrów w niczym nie różni się od listy parametrów zwykłej funkcji.

Typ zwracany jest opcjonalny, jeśli nie zostanie podany kompilator wydedukuje typ funkcji na podstawie instrukcji return w kodzie.

Blok kodu może zawierać dowolne instrukcje, dostępne są w nim wszystkie obiekty zdefiniowane w domknięciu i na liście parametrów.

#include <iostream>
#include <string>

template <typename Funkcja>
double policz(Funkcja f) {

    return f(7, 3);
}

int main() {

    double wspolczynnik = 1.25;
    std::string napis = "test";

    double wynik = policz(
        [wspolczynnik, &napis](int a, int b) {
            napis = "zmieniony";
            return (a+b) * wspolczynnik;
        }
    );

    std::cout << "wynik = " << wynik << '\n';
    std::cout << "napis = " << napis << '\n';
}

Wynikiem będzie:

wynik = 12.5
napis = zmieniony

W kolejnym przykładzie funkcja anonimowa jest przypisywana do zmiennej i wielokrotnie wywoływana w kodzie funkcji.

#include <iostream>

int main() {

    int liczba_wywolan = 0;

    auto wyswietl = [&](const std::string& napis) {
        std::cout << "'" << napis << "'" <<  '\n';

        liczba_wywolan += 1;
    };

    wyswietl("witaj");
    wyswietl("wiki");
    wyswietl("books");

    std::cout << "liczba wywołań = " << liczba_wywolan << '\n';
}

Szablony funkcji

[edytuj]

Szablon funkcji

[edytuj]

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 <typename T> void pisz(T a)
       {
          cout<<a;
       }

Pierwszą linijkę można też złamać po nawiasie '>':

       template <typename T>
       void pisz(T arg)

Pierwszym słowem jest template czyli szablon. Następnie w ostrych nawiasach umieszcza się słowo typename (zamiennie struct lub 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.

Nie można stosować szablonów dla metod wirtualnych.


Szablony klas

[edytuj]

Czym są?

[edytuj]

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). Mamy przykładową klasę:

 // ...
 class PunktUInt
 {
    public:
       PunktUInt( unsigned argX, unsigned argY, unsigned argZ )
       : x(argX), y(argY), z(argX)
       { }
 
       unsigned x, y, z;
 };
 // ...

I potem co? kopiujemy i zmieniamy unsigned na int:

 // ...
 class PunktInt
 {
    public:
       PunktInt( int argX, int argY, int argZ )
       : x(argX), y(argY), z(argZ)
       { }
 
       int x, y, z;
 };
 // ...

Następnie zamieniamy na float:

 // ...
 class PunktFloat
 {
    public:
       PunktFloat( float argX, float argY, float argZ )
       : x(argX), y(argY), z(argX)
       { }
 
       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 argX, unsigned argY, unsigned argZ )
       : x(argX), y(argY), '''z(argX)'''
 // ...

Zamiast z(argX) powinno być z(argZ). 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.

Wyróżniamy różne możliwości szablonów:


Wykorzystywanie szablonów

[edytuj]

Napiszmy teraz jeszcze raz nasz program. Tym razem z wykorzystaniem szablonów.


 // ...
 template <typename T>
 class Punkt
 {
    public:
       Punkt( T argX, T argY, T argZ )
       : x(argX), y(argY), z(argX)
       { }
 
       T x, y, z;
 };
 // ...

Za pomocą template<typename T> tworzymy nasz szablon. Parametrem jest typ, jaki chcemy użyć, tworzymy go poprzez <typename T>. 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;
 }

Powiesz:

No dobra, ale jak teraz uruchomię ten program to nadal mam ten sam, zły wynik.

I masz rację, bo zobaczysz:

A(0,-10,0)
B(0,10,0)

Powód jest ten sam co poprzedni.

       Punkt( T argX, T argY, T argZ )
       : x(argX), y(argY), '''z(argX)'''
       { }

Musimy zamienić z(argX) na z(argZ) 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 <typename T>
 class Punkt
 {
    public:
       Punkt( T argX, T argY, T argZ )
       : x(argX), y(argY), z(argZ)
       { }
 
       T 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;
 }

Szablony z wieloma parametrami

[edytuj]

Szablon może także mieć więcej niż jeden parametr. Na przykład chcielibyśmy posługiwać się parami obiektów. Należy więc napisać klasę Para, zawierającą dwa elementy: pierwszy o nazwie pierwszy, a drugi o nazwie drugi, jednakże nie wiemy z góry, jakie mają one mieć typy. Możemy to zrobić w ten sposób:

 #include <iostream>
 #include <string>
 template <typename T1, typename 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<std::string,int> zmienna("Liczba",10);

     std::cout << zmienna.pierwszy << " " << zmienna.drugi << std::endl;
     return 0;
 }

Za pomocą template<typename T1, typename T2> utworzyliśmy szablon o dwóch parametrach.

Deklaracja zmiennej zmienna określa jej typ poprzez skonkretyzowanie typów w szablonie, pośrednio więc określa też, jakie typy będą miały składowe tej zmiennej.

Można też liczby

[edytuj]

Parametrem szablonu może być także liczba. Zilustrujmy to przykładem:

 #include <iostream>
 #include <cstddef>
 template <typename T, std::size_t N>
 class Tablica
 {
     public:
 
        T &operator[]( std::size_t 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;
 }

W powyższym przykładzie użyto typu std::size_t, zadeklarowanego w pliku nagłówkowym dołączanym dyrektywą #include <cstddef>.

Metaprogramowanie

[edytuj]

Metaprogramowanie to technika, która pozwala programistom pisać programy generujące kod lub manipulujące kodem w czasie kompilacji. W C++ metaprogramowanie odbywa się przede wszystkim za pomocą szablonów. Szablony to potężne narzędzie, które umożliwia programowanie ogólne w C++ i może być używane do pisania kodu, który jest zarówno wydajny, jak i elastyczny.

Jedną z kluczowych zalet metaprogramowania jest to, że umożliwia ponowne wykorzystanie kodu i zmniejszenie ilości kodu, który należy napisać. Pisząc szablony, których można używać z różnymi typami danych, programiści mogą uniknąć pisania powtarzalnego kodu i uczynić swój kod bardziej elastycznym i dającym się dostosować.

Jedną z zaawansowanych technik, których można użyć w metaprogramowaniu C++, jest wykorzystanie informacji o typie. Szablony C++ są oparte na informacjach o typie i mogą być używane do wykonywania operacji i podejmowania decyzji na podstawie typu używanych danych. Na przykład można napisać szablon, który działa inaczej w zależności od tego, czy typem danych jest liczba całkowita, czy łańcuch.

SFINAE (ang. Substitution Failure Is Not An Error) to kolejna technika powszechnie stosowana w metaprogramowaniu C++. SFINAE umożliwia pisanie szablonów, których kompilacja nie powiedzie się, jeśli nie zostaną spełnione określone warunki. Może to być przydatne do pisania szablonów, które działają na określonych typach danych lub mają określone wymagania.

Na przykład można napisać szablon, który działa tylko z klasami, które mają określoną metodę. Jeśli używana klasa nie ma tej metody, kompilacja szablonu nie powiedzie się. Może to pomóc w zapobieganiu błędom i zwiększeniu niezawodności kodu.

Inną zaawansowaną techniką, której można użyć w metaprogramowaniu C++, jest specjalizacja szablonów. Specjalizacja szablonów umożliwia dostarczenie określonych implementacji szablonu dla określonych typów danych lub warunków.

Wskażniki do elementów składowych

[edytuj]

Szablon:C++/Wskaźniki do elementów składowych

Dodatek A
Biblioteka STL

Filozofia STL

[edytuj]

Biblioteka standardowa języka C++ jest jego częścią i należy do standardu. Uzupełnia sam język logicznymi strukturami czyniąc go bardziej użytecznym. STL (ang. Standard Template Library) jest chyba pierwszą rzeczą jaką trzeba się nauczyć zaraz po samej nauce języka C++. STL jest pewną częścią biblioteki standardowej należącą do języka C++, a nie całą. Dlaczego warto używać STL? Żeby nie odkrywać koła na nowo mówiąc w skrócie, tworzyć kod przenośny między platformami i wiele innych przyczyn wynikających ze stopnia zaawansowania twojego programowania. W tym artykule postaram się opisać bibliotekę standardową wzorców dla początkujących i najwięcej uwagi poświęcić tym (z mojego punktu widzenia) częściom najbardziej użytecznym i wykorzystywanym do amatorskiego i nie tylko programowania. Zanim zaczniemy poznawać poszczególne kategorie STL musimy poznać tak zwany generalny konspekt przygotowania jej do użycia. Najważniejszymi i niezbędnymi są: dołączanie odpowiednich plików nagłówkowych i korzystanie z przestrzeni nazw. Jeżeli chodzi o pliki nagłówkowe to używamy następującej składni:

#include <iostream>
#include <string>
#include <vector>
 
// zamiast niepoprawnych:
#include <iostream.h>
#include <string.h>
#include <vector.h>
 
// aby użyć starej biblioteki z języka C, zapisując to w stylu C++, robimy to tak:
#include <cstdlib>   // zamiast <stdlib.h>
#include <cstring>   // zamiast <string.h>
 
// przedrostek 'c' oznacza bibliotekę języka C

Teraz kwestia dotycząca przestrzeni nazw. Zawartość biblioteki standardowej została "włożona" do przestrzeni nazw po to, aby używane tam nazwy, np. metod klas, nie zastrzegały sobie wyłączności na daną nazwę w obrębie całego programu. Przez to, albo dzięki temu, możemy powiadomić kompilator o tym jakiej części chcemy używać. Składnia została przedstawiona poniżej:

// załączamy całą przestrzeń nazw:
using namespace std;
 
// lub: załączmy tylko wybrane elementy cout i endl:
using std::cout;
using std::endl;

Oczywiście możemy nie używać dyrektywy using. Wówczas przed każdym użyciem elementu z biblioteki standardowej, np. obiektu cout, dopisujemy z jakiej przestrzeni nazw pochodzi dany element:

std::cout << "Nowa linia i wymuszenie przepływu z bufora" << std::endl;


String

[edytuj]

String

[edytuj]

Łańcuchy znaków w stylu języka C są częstą przyczyną błędów programu, a na dodatek ich używanie jest dosyć kłopotliwe. Nic więc 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>

Tworzenie nowych obiektów tego typu wygląda następująco:

string napis1;
napis1 = "text";
 
//inicjalizowanie łańcucha znaków w miejscu jego tworzenia
//jawne wywołanie konstruktora
string napis2( "text" );
//operator przypisania
string napis3 = "text"; // string nie jest zakończony znakiem null, jak w przypadku C-stringa
     
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:

using std::string;

lub ogólnie:

using namespace std;

Klasa string ma zdefiniowanych wiele operatorów, co ułatwia niektóre działania na napisach. Dla przykładu, dawniej aby skopiować napis z jednej zmiennej do drugiej, trzeba było używać dodatkowej funkcji strcpy(). W przypadku klasy string wystarczy operator przypisania '=' :

string a, b;
a = '1';
b = '2';
a = b;
cout << a;

Możemy z powodzeniem używać także operatorów: ==, !=, +, <, > oraz indeksowego []:

string a,b,c;
a = "gosia";
b = "iza";
c = "gosia";

// porównywanie napisów
if (a == c) cout << "a i c sa takie same\n";

if (a != b) cout << "a i b sa rozne\n" ;

// porządek leksykograficzny
cout << "napis a ("<<a<<") poprzedza napis b("<<b<<"): ";
if (a < b) cout << "prawda\n";
else cout << "nieprawda\n";

// łączenie łańcuchów
a = "mal"+ a;

cout << "napis a ("<<a<<") poprzedza napis b("<<b<<"): ";
if (a < b) cout << "prawda\n";
else cout << "nieprawda\n";

// modyfikacja
b[0] = '_';

cout << "zmieniony wyraz b: "<<b<<'\n';

Po czym w konsoli zobaczymy:

a i c sa takie same
a i b sa rozne
napis a (gosia) poprzedza napis b(iza): prawda
napis a (malgosia) poprzedza napis b(iza): nieprawda
zmieniony wyraz b: _za

Jak widać, manipulacje obiektami string są intuicyjne. 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, tak jak operator [], z tym że ta metoda jest bezpieczniejsza - wyrzuca wyjątek w przypadku wyjścia poza zakres stringa.
clear() Usuwa wszystkie znaki z napisu.
erase(...) 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(...) 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 typu const char*).

Omówione dotychczas operatory i metody to tylko część dostępnych; wymienione zostały tylko te najczęściej używane. Teraz przedstawię różnice jakie występują między C a C++ w obsłudze napisów. Po lewej zmienne a i b są typu (const char *), a po prawej - (std::string).

C C++
strcpy(a,b) a = b
!strcmp(a,b) a == b
strcat(a,b) a += b
strlen(a) a.size(), a.length()
strstr(a,b) a.find(b)


Vector

[edytuj]

Vector

[edytuj]

Przed zanurzeniem się głęboko w wektorach przeczytaj różnicę między vector and array

Klasa vector reprezentuje obudowaną, zwykłą tablicę znaną z C, wyposażoną w kilka dodatkowych mechanizmów. Elementy wektora mogą być dowolnego typu.

Obiekt vector ma kilka odmian konstruktorów. Z reguły będziemy tworzyć wektor pusty.

vector<typ_elementow> nazwa_tablicy;

Możemy też podać wielkość, co wcale nas nie ogranicza do tej wielkości, aby zarezerwować pamięć na kilka elementów od razu. Może to być zabieg optymalizacyjny.

 vector<int> tab(20);

Dodatkowo istnieje konstruktor przyjmujący liczbę elementów oraz wartość, jaką ma mieć każdy z nich.

 vector<string> tablica( 20, "przykladowy tekst" );

Ta tablica będzie miała dwadzieścia elementów, z czego wszystkie mają wartość: "przykladowy tekst".

Dodawanie elementów

[edytuj]

Dodawanie elementów umożliwia metoda push_back(). Dodaje ona nowy element na koniec tablicy. Po dodaniu nowych elementów możemy się do nich odwoływać indeksowo [] lub metodą at(). Możemy też sprawdzić ile obecnie jest elementów metodą size(), a metoda empty() powie nam czy wektor jest pusty.

include <iostream>
include <vector>
using namespace std;
int main(){
    vector<int> tab;
    int n;
    cin >> n;
    for( int i=0; i<n; ++i )
    {
       int element;
       cin >> element;
       tab.push_back(element);
    }
}

Jak działa powiększanie się tablicy vector?

[edytuj]

Poniższy akapit dotyczy szczegółów technicznych, jeśli nie jesteś nimi zainteresowany, możesz go pominąć.

Metoda push_back() dodając nowy element do tablicy, dba o to, aby tablica była odpowiedniego rozmiaru. Za każdym razem, gdy brakuje miejsca, tablica jest powiększana - rezerwowana jest nowa, większa przestrzeń, stare elementy są kopiowane, aby do większej tablicy móc dodać dany element. Z reguły rezerwowana jest pamięć dwa razy większa od poprzedniej. W skrajnej sytuacji, może się zdarzyć, że zarezerwowana pamięć jest prawie dwa razy większa niż ilość elementów znajdujących się w niej!

Jeśli zależy nam na szybkości działania, powinniśmy zarezerwować przy pomocy odpowiedniego konstruktora pamięć na pewną ilość elementów. Przykładowo, jeśli wiemy że w tablicy będzie około 50 elementów, możemy konstruować wektor o wielkości 50, dzięki czemu unikniemy kilku kopiowań całej tablicy podczas dodawania elementów. Warto też szukać złotego środka, aby przypadkiem nie marnować pamięci. Na przykład, aby nie stworzyć wektora o rozmiarze 100, jeśli dodane do niego będą tylko 2 elementy.

Ręczne zarezerwowanie pamięci dla wektora jest możliwe dzięki metodzie reserve(size_t n), natomiast metoda capacity() zwróci nam wielkość aktualnie zarezerwowanego miejsca.

Ponieważ kopiowanie tablicy jest powolnym procesem, w przypadku tablicy dużych struktur, na przykład klas, warto zastanowić się nad utworzeniem tablicy wskaźników na te struktury.

Iteratory

[edytuj]

Jak wiemy, do poruszania się po elementach zwykłej tablicy takiej jak int a[10] można używać wskaźnika. Podobnie, operacje na obiekcie vector można dokonać używając iteratorów działających podobnie jak wskaźniki. Więcej o iteratorach znajduje się w dalszych rozdziałach.

Metody

[edytuj]

Lista metod klasy vector. Użyte słowo iterator zastępuje poprawne vector<T>::iterator, podmienione zostało dla zwiększenia czytelności.

Modyfikacja

[edytuj]
prototyp opis działania złożoność czasowa
void swap(vector<T>& vec) zamienia zawartości dwóch wektorów miejscami stała
void push_back(const T obj) dodaje na końcu wektora kopię przekazanego argumentu stała, czasem liniowa*
void pop_back() usuwa ostatni element z wektora stała
void clear() usuwa wszystkie elementy z wektora liniowa (destruktory)
void assign(size_t n, const T obj) czyści wektor i wypełnia go n kopiami argumentu obj liniowa (jak clear) + liniowa względem wstawianych elementów
void assign(iterator poczatek, iterator koniec) czyści wektor i wypełnia go elementami z innego wektora z przedziału <poczatek;koniec> jw.
iterator insert(iterator pos, T obj) wstawia element obj przed wskazywaną przez iterator pos pozycją i zwraca iterator do dostawionego elementu liniowa (przenoszenie elementów między pos a ostatnim elementem tablicy)
void insert(iterator pos, size_t n, const T obj) wstawia n kopii argumentu obj przed pozycją wskazywaną przez iterator pos jw. + liniowa względem ilości dodanych elementów
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) jw.**
iterator erase(iterator pos) usuwa element wskazywany przez pos i zwraca iterator do następnego elementu liniowa względem ilości elementów za usuwanym elementem
iterator erase(iterator poczatek, iterator koniec) usuwa elementy z przedziału <poczatek;koniec> i zwraca iterator do elementu za nimi liniowa względem ilości usuwanych elementów + przenoszenie elementów za końcem

* może występować kopiowanie wektora, gdy rozmiar jest zbyt mały
** w rzadkim przypadku (dla iteratorów najniższego typu w hierarchi: Input lub Output) złożoność bliższa kwadratowej (ilość wstawianych elementów razy ilość elementów od pozycji do końca tablicy)

Dostęp

[edytuj]
prototyp opis działania
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

Inne

[edytuj]
prototyp opis działania
size_t size() zwraca obecną liczbę 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


List & Slist

[edytuj]

Lista

[edytuj]

Kolejnym kontenerem udostępnianym przez STL, nieco mniej popularnym od wektorów, jest klasa list będąca listą dwukierunkową. Oto główne różnice między wektorami a listami:

aspekt vector list
dostęp przez podanie indeksu lub przez iteratory tylko przez iteratory
dodawanie/usuwanie jeśli nie chodzi o końcowy element - powolne dodawanie i usuwanie elementów jest bardzo szybkie i odbywa się w stałym czasie
adresy elementów ulegają zmianom, wskaźniki często tracą ważność (należy używać wyłącznie indeksów) niezmienne

Kiedy więc lepszym rozwiązaniem są wektory, a kiedy listy? Jeśli nasza kolekcja raz wprowadzona nie zmienia się, lub rzadko się zmienia (np. tylko dodajemy elementy na koniec), odpowiedni do tego będzie wektor. Jeżeli często wprowadzamy zmiany w kontenerze, np. dodajemy/usuwamy elementy, listy będą tutaj szybszym rozwiązaniem. Wektor będzie bardziej odpowiedni dla operacji na pojedynczych elementach, wskazywanych numerem indeksu. W przypadku listy najlepiej jest przeprowadzać operacje po kolei, np. od pierwszego do ostatniego elementu. W przeciwieństwie do wektora, dostęp do losowego elementu listy jest kosztowną operacją.

Sposób korzystania z list jest niemal identyczny jak w przypadku wektorów. Opis użytych tu iteratorów znajduje się w następnych rozdziałach. Oto przykład wykorzystania list:

#include <list>
#include <iostream>
#include <cstddef>


int main()
{
   std::list<int> lista;
   int liczba;

   std::cout << "Podaj kolejne elementy listy, podaj zero aby zakonczyc:\n";
   while(std::cin >> liczba && liczba != 0)
      lista.push_back(liczba);

   size_t rozmiar = lista.size();
 
   liczba = 0;
   for( std::list<int>::iterator iter=lista.begin(); iter != lista.end(); iter++ )
      liczba += *iter;

   std::cout << "Srednia liczb wystepujacych w liscie wynosi " << static_cast<double>(liczba) / static_cast<double>(lista.size()) << '\n';

   // usuniecie liczb ujemnych
   for( std::list<int>::iterator iter=lista.begin(); iter != lista.end(); )
      if (*iter < 0)
         iter=lista.erase(iter);
      else
         iter++;
      

   liczba = 0;
   for( std::list<int>::iterator iter=lista.begin(); iter != lista.end(); ++iter )
      liczba += *iter;
 
   std::cout << "Srednia dodatnich liczb wynosi " << static_cast<double>(liczba) / static_cast<double>(lista.size()) << '\n';
   
   return 0;
}

W zaprezentowanym powyżej programie, został użyty nowy zapis: while (cin >> liczba && liczba != 0). Wywołuje on pętlę, która kończy działanie gdy użytkownik wpisze 0, gdy program dojdzie do końca pliku, lub gdy użytkownik wpisze coś co nie jest liczbą całkowitą.

Metody

[edytuj]

Spis metod klasy list (gdzie iterator to std::list<T>::iterator - podmiana dla czytelności).

Modyfikacja wtyczki

[edytuj]
prototyp opis działania
void push_back(const T obj) dodaje na końcu listy kopię przekazanego argumentu
void pop_back() usuwa ostatni element z listy
void push_front(const T obj) dodaje na początku listy kopię przekazanego argumentu
void pop_front() usuwa pierwszy element listy
void clear() usuwa wszystkie elementy z listy
void remove(const T& wartosc) usuwa wszystkie elementy równe argumentowi wartosc
void remove_if(Functor func) usuwa wszystkie elementy dla których func (bool funkcja(T arg)) zwróci true (patrz: sort)

Modyfikacja - pozostałe

[edytuj]
prototyp opis działania
void merge(std::list<T> ls) dostawia zawartość ls do obecnej listy i całość sortuje rosnąco
void merge(std::list<T> ls, Functor func) dostawia zawartość ls do obecnej listy i całość sortuje przy użyciu func (patrz: funkcja sort poniżej)
void splice(iterator pos, std::list<T>& ls) wstawia zawartość listy ls przed elementem wskazywanym przez pos (ls staje się pusta)
void splice(iterator pos, std::list<T>& ls, iterator i) usuwa element wskazywany przez i w liście ls i wstawia go przed elementem pos w obecnej liście
void splice(iterator pos, std::list<T>& ls, iterator poczatek, iterator koniec) usuwa elementy z przedziału <poczatek;koniec> i wstawia przed elementem pos w obecnej liście
void unique() usuwa wszystkie następujące po sobie elementy o równych wartościach poza pierwszym spośród nich
void unique(Functor func) usuwa wszystkie następujące po sobie elementy, dla których func zwróci true (bool funkcja(T arg1, T arg2) poza pierwszym spośród nich
void assign(size_t n, const T obj) czyści listę i wypełnia ją n kopiami argumentu obj
iterator assign(iterator poczatek, iterator koniec) czyści listę i wypełnia ją elementami 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 (stały czas wykonania)
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
void reverse() odwraca kolejność wszystkich elementów (wykonywane w stałym czasie)
void sort() sortuje elementy listy
void sort(Functor func) sortuje elementy listy przy użyciu przekazanej funkcji (bool funkcja(T arg1, T arg2)), może to być wskaźnik na funkcję lub obiekt ze zdefiniowanym operatorem(); zwracana wartość tejże funkcji ma określać czy arg1 < arg2
void swap(std::list<T> ls) zamienia zawartości dwóch list miejscami (wykonywane szybko, w stałym czasie)

Dostęp

[edytuj]
prototyp opis działania
T& front() zwraca referencję do pierwszego elementu listy
T& back() zwraca referencję do ostatniego elementu listy
iterator begin() zwraca iterator do pierwszego elementu listy (często mylone z front())
iterator end() zwraca iterator ustawiony za ostatnim elementem listy (określa tym samym element "niepoprawny", nieistniejący w liście)
iterator rbegin() zwraca odwrócony (reverse) iterator, wskazuje ostatni element i służy do iterowania w odwrotnym kierunku (od ostatniego do pierwszego elementu)
iterator rend() zwraca odwrócony iterator (podobnie jak end wskazuje element za listą, rend wskazuje teoretyczny element "przed" listą)

Inne

[edytuj]
prototyp opis działania
size_t size() zwraca obecną ilość elementów listy (działa w czasie liniowym)
size_t max_size() zwraca ilość elementów, którą maksymalnie może pomieścić lista
bool empty() zwraca true jeśli lista nie przechowuje żadnych zmiennych
void resize(size_t n, T obj) zmienia rozmiar listy 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 listy do n; jeśli jest większy od obecnego, dodawane są nowe elementy o przypadkowych wartościach

Listy jednokierunkowe

[edytuj]

Listy jednokierunkowe są odmianą list dwukierunkowych i nazwane jako slist. Główna różnica między nimi a zwykłymi listami leży w ogólnym mechanizmie działania. Każdy element zwykłych list posiada wskaźniki na poprzedni i następny element w liście, podczas gdy w kontenerze slist każdy element posiada wskaźnik jedynie na kolejny element w liście, skąd też wzięły się ich nazwy. Jakie wynikają z tego różnice dla korzystającego z nich programisty?

Zalety list jednokierunkowych:

  • są szybsze w działaniu
  • zużywają znacznie mniej pamięci (zwłaszcza, gdy przechowujemy sporo małych elementów, wtedy różnice są bardzo duże)

Wady list jednokierunkowych:

  • iteratory mogą przesuwać się jedynie do przodu
  • brak odwrotnych iteratorów i funkcji z nimi powiązanych
  • funkcje insert, erase oraz splice działają bez porównania wolniej (opis problemu poniżej)

Funkcje insert i splice działają wolniej, gdyż wstawiają elementy przed pozycją wskazywaną przez podany w argumencie iterator. Element, na który wskazuje iterator "nie zna" pozycji poprzedniego elementu, więc aby dostawić element przed nim algorytm listy jednokierunkowej musi przeszukać listę od początku w celu znalezienia poprzedniego elementu.

Funkcja erase z kolei działa wolniej, gdyż po usunięciu danego elementu wskaźnik tego stojącego przed wykasowanym elementem, musi wskazywać na element stojący za usuniętym elementem.

Stąd też postanowiono poszerzyć arsenał list jednokierunkowych względem list dwukierunkowych o nowe metody, które mogą zastąpić owe powolne funkcje, gdyż mają podobne działanie a wykonują się znacznie szybciej. Oto lista nowych metod:


prototyp opis działania
void splice_after(iterator pos, std::list<T>& ls) wstawia zawartość listy ls za elementem wskazywanym przez pos (ls staje się pusta)
void splice_after(iterator pos, iterator poczatek, iterator koniec) usuwa elementy z przedziału (poczatek;koniec+1> i wstawia za elementem pos w obecnej liście
iterator insert_after(iterator pos) dostawia nowy element za pos
iterator insert_after(iterator pos, T obj) wstawia element obj za wskazywaną przez iterator pos pozycją i zwraca iterator do dostawionego elementu (stały czas wykonania), przy czym pos != end()
void insert_after(iterator pos, size_t n, const T obj) wstawia n kopii argumentu obj za pozycją wskazywaną przez iterator pos, przy czym pos != end()
void insert_after(iterator pos, InputIterator poczatek, InputIterator koniec) wstawia za pozycją wskazywaną przez iterator pos elementy z przedziału <poczatek;koniec>, przy czym pos != end()
void insert_after(iterator pos, T* poczatek, T* koniec) wstawia za pozycją wskazywaną przez iterator pos elementy z przedziału <poczatek;koniec>, przy czym pos != end()
iterator erase_after(iterator pos) usuwa następny element za pos i zwraca iterator do następnego elementu (stały czas wykonania)
iterator erase_after(iterator poczatek, iterator koniec) usuwa elementy z przedziału (poczatek;koniec> i zwraca iterator do elementu za nimi


Set

[edytuj]

Opis

[edytuj]

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 wartości 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 są tzw. kontenerami asocjacyjnymi (o zmiennej długości, pozwalającymi na operowanie elementami przy użyciu kluczy).

Prosty przykład

[edytuj]

Opis użytych tu iteratorów znajduje się w rozdziale Iteratory.

#include <iostream>
#include <string>
#include <set>
using namespace std;
 
int main()
{
   set<string> mapa;
   mapa.insert("Lublin");
   mapa.insert("Łódź");
   mapa.insert("Warszawa");
   mapa.insert("Kraków");
 
   set<string>::iterator result, it;

   // szuka elementu "Warszawa"
   result = mapa.find("Warszawa");
   if( result!=mapa.end() )
      cout << "Znalazłem! " << *result<< '\n';

   // wyświetlenie zawartości
   for( it=mapa.begin(); it!=mapa.end(); ++it)
      cout << *it<< '\n';

   return 0;
}

Map

[edytuj]

Opis

[edytuj]

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ą pozycję, ponieważ kolejność ustalana jest według danego klucza. 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 każde dwa elementy mają różny klucz.

Mapa zdefiniowana jest w standardowym nagłówku map oraz w niestandardowym, wstecznie kompatybilnym nagłówku map.h.

Przykład

[edytuj]
#include<iostream>
#include<map>
using namespace std;

int main()
{
   map<int, string> tydzien;
   tydzien[1] = "niedziela";
   tydzien[2] = "poniedzialek";
   tydzien[3] = "wtorek";
   tydzien[4] = "sroda";
   tydzien[5] = "czwartek";
   tydzien[6] = "piatek";
   tydzien[7] = "sobota";

   cout << "trzeci dzien tygodnia:  " << tydzien[3] << '\n';

   map<int, string>::iterator cur;

   // zwrocenie elementu o kluczu 3
   cur = tydzien.find(3);

   // elementy o kluczach większych i mniejszych
   map<int, string>::iterator prev = cur;
   map<int, string>::iterator next = cur;    
   ++next;
   --prev;

   cout << "Wczesniejszy:  " << prev->second << '\n';
   cout << "Nastepny:  " << next->second << '\n';
}

mmap

[edytuj]

Funkcja w c/c++, która allokuje pamięć tak jak malloc. APUE zaleca używanie mmap dla alokowania dużych ilości pamięci, bo może to być zauważalnie szybsze od malloc. Jako programiści zwykle używamy funkcji malloc (), free () i podobnych do przydzielania pamięci. Są one dostarczane przez bibliotekę glibc (). Rzeczywista praca jest wykonywana przez mmap () i munmap (), który jest wywołaniem systemowym Linuksa. Funkcja mmap () lub wywołanie systemowe utworzy mapowanie w wirtualnej pamięci bieżącego procesu. Przestrzeń adresowa składa się z wielu stron, a każdą stronę można zmapować jakimś zasobem. Możemy utworzyć mapowanie dla zasobów, których chcemy użyćFunkcje mmap () i munmap () są dostarczane przez bibliotekę sys / mman.h. więc w celu użycia musimy je uwzględnić, jak poniżej.

  1. include <sys / mman.h>

void * mmap (void * addr, size_t lengthint "prot", int "flags,

           int fd, off_t offset)
 void * addr to adres, od którego chcemy rozpocząć mapowanie
 size_t lengthint to rozmiar, który chcemy zmapować jako liczbę całkowitą
 PROT_READ | PROT_WRITE | PROT_EXEC opcje dotyczące strony
 MAP_ANON | MAP_PRIVATE opcje dotyczące stronyWe have two option about memory mapping for sharing.

Mamy dwie opcje dotyczące mapowania pamięci do udostępniania.

MAP_SHARED zmapuje daną stronę i będzie to widoczne również w innych procesach. MAP_PRIVATE zmapuje daną stronę i nie będzie to widoczne dla innych procesów.

Przykład

[edytuj]
/* 
 * tiny.c - a minimal HTTP server that serves static and
 *          dynamic content with the GET method. Neither 
 *          robust, secure, nor modular. Use for instructional
 *          purposes only.
 *          Dave O'Hallaron, Carnegie Mellon
 *trochę ten kod przerobiłem ale nie wiele
 *  thc
 *          usage: tiny <port>
 */

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <fcntl.h>
#include <sys/types.h> 
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define BUFSIZE 1024
#define MAXERRS 16

extern char **environ; /* the environment */

/*
 * error - wrapper for perror used for bad syscalls
 */
void error(char *msg) {
  perror(msg);
  exit(1);
}

/*
 * cerror - returns an error message to the client
 */
void cerror(FILE *stream, char *cause, char *errno, 
	    char *shortmsg, char *longmsg) {
  fprintf(stream, "HTTP/1.1 %s %s\n", errno, shortmsg);
  fprintf(stream, "Content-type: text/html\n");
  fprintf(stream, "\n");
  fprintf(stream, "<html><title>Tiny Error</title>");
  fprintf(stream, "<body bgcolor=""ffffff"">\n");
  fprintf(stream, "%s: %s\n", errno, shortmsg);
  fprintf(stream, "<p>%s: %s\n", longmsg, cause);
  fprintf(stream, "<hr><em>The Tiny Web server</em>\n");
}

int main(int argc, char **argv) {

  /* variables for connection management */
  int parentfd;          /* parent socket */
  int childfd;           /* child socket */
  int portno;            /* port to listen on */
  int clientlen;         /* byte size of client's address */
  struct hostent *hostp; /* client host info */
  char *hostaddrp;       /* dotted decimal host addr string */
  int optval;            /* flag value for setsockopt */
  struct sockaddr_in serveraddr; /* server's addr */
  struct sockaddr_in clientaddr; /* client addr */

  /* variables for connection I/O */
  FILE *stream;          /* stream version of childfd */
  char buf[BUFSIZE];     /* message buffer */
  char method[BUFSIZE];  /* request method */
  char uri[BUFSIZE];     /* request uri */
  char version[BUFSIZE]; /* request method */
  char filename[BUFSIZE];/* path derived from uri */
  char filetype[BUFSIZE];/* path derived from uri */
  char cgiargs[BUFSIZE]; /* cgi argument list */
  char *p;               /* temporary pointer */
  int is_static;         /* static request? */
  struct stat sbuf;      /* file status */
  int fd;                /* static content filedes */
  int pid;               /* process id from fork */
  int wait_status;       /* status from wait */

  /* check command line args */

  portno = 80;

  /* open socket descriptor */
  parentfd = socket(AF_INET, SOCK_STREAM, 0);
  if (parentfd < 0) 
    error("ERROR opening socket");

  /* allows us to restart server immediately */
  optval = 1;
  setsockopt(parentfd, SOL_SOCKET, SO_REUSEADDR, 
	     (const void *)&optval , sizeof(int));

  /* bind port to socket */
  bzero((char *) &serveraddr, sizeof(serveraddr));
  serveraddr.sin_family = AF_INET;
  serveraddr.sin_addr.s_addr = inet_addr("127.0.0.2");
  serveraddr.sin_port = htons((unsigned short)portno);
  if (bind(parentfd, (struct sockaddr *) &serveraddr, 
	   sizeof(serveraddr)) < 0) 
    error("ERROR on binding");

  /* get us ready to accept connection requests */
  if (listen(parentfd, 5555) < 0) /* allow 5 requests to queue up */ 
    error("ERROR on listen\n");

  /* 
   * main loop: wait for a connection request, parse HTTP,
   * serve requested content, close connection.
   */
  clientlen = sizeof(clientaddr);
  while (1) {

    /* wait for a connection request */
    childfd = accept(parentfd, (struct sockaddr *) &clientaddr, &clientlen);
    if (childfd < 0) 
      error("ERROR on accept");
    
    /* determine who sent the message */
    hostp = gethostbyaddr((const char *)&clientaddr.sin_addr.s_addr, 
			  sizeof(clientaddr.sin_addr.s_addr), AF_INET);
    if (hostp == NULL)
      error("ERROR on gethostbyaddr");
    hostaddrp = inet_ntoa(clientaddr.sin_addr);
    if (hostaddrp == NULL)
      error("ERROR on inet_ntoa\n");
    
    /* open the child socket descriptor as a stream */
    if ((stream = fdopen(childfd, "r+")) == NULL)
      error("ERROR on fdopen");

    /* get the HTTP request line */
    fgets(buf, BUFSIZE, stream);
    printf("%s", buf);
    sscanf(buf, "%s %s %s\n", method, uri, version);

    /* tiny only supports the GET method */
    if (strcasecmp(method, "GET")) {
      cerror(stream, method, "501", "Not Implemented", 
	     "Tiny does not implement this method");
      fclose(stream);
      close(childfd);
      continue;
    }

    /* read (and ignore) the HTTP headers */
    fgets(buf, BUFSIZE, stream);
    printf("%s", buf);
    while(strcmp(buf, "\r\n")) {
      fgets(buf, BUFSIZE, stream);
      printf("%s", buf);
    }

    /* parse the uri [crufty] */
    if (!strstr(uri, "cgi-bin")) { /* static content */
      is_static = 1;
      strcpy(cgiargs, "");
      strcpy(filename, ".");
      strcat(filename, uri);
      if (uri[strlen(uri)-1] == '/') 
	strcat(filename, "index.html");
    }
  
      else {
	strcpy(cgiargs, "");      
      strcpy(filename, ".");
      strcat(filename, uri);
    }

    /* make sure the file exists */
    if (stat(filename, &sbuf) < 0) {
      cerror(stream, filename, "404", "Not found", 
	     "Tiny couldn't find this file");
      fclose(stream);
      close(childfd);
      continue;
    }

    /* serve static content */
    if (is_static) {
      if (strstr(filename, ".html"))
	strcpy(filetype, "text/html");
      else if (strstr(filename, ".gif"))
	strcpy(filetype, "image/gif");
      else if (strstr(filename, ".jpg"))
	strcpy(filetype, "image/jpg");
      else 
	strcpy(filetype, "text/plain");

      /* print response header */
      fprintf(stream, "HTTP/1.1 200 OK\n");
      fprintf(stream, "Server: Tiny Web Server\n");
      fprintf(stream, "Content-length: %d\n", (int)sbuf.st_size);
      fprintf(stream, "Content-type: %s\n", filetype);
      fprintf(stream, "\r\n"); 
  fflush(stream);

      /* Use mmap to return arbitrary-sized response body */
      fd = open(filename, O_RDONLY);
void*  p=mmap(0, sbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
      fwrite(p, 1, sbuf.st_size, stream);
      munmap(p, sbuf.st_size);
    }

    /* serve dynamic content */
    else {
      /* make sure file is a regular executable file */
      if (!(S_IFREG & sbuf.st_mode) || !(S_IXUSR & sbuf.st_mode)) {
	cerror(stream, filename, "403", "Forbidden", 
	       "You are not allow to access this item");
	fclose(stream);
	close(childfd);
	continue;
      }

      /* a real server would set other CGI environ vars as well*/
      setenv("QUERY_STRING", cgiargs, 1); 
      /* print first part of response header */
      sprintf(buf, "HTTP/1.1 200 OK\r\n");
   
      sprintf(buf, "Server: Tiny Web Server\r\n");

      /* create and run the child CGI process so that all child
         output to stdout and stderr goes back to the client via the
         childfd socket descriptor */
      pid = fork();
      if (pid < 0) {
	perror("ERROR in fork");
	exit(1);
      }
      else if (pid > 0) { /* parent process */
	wait(&wait_status);
      }
      else { /* child  process*/
	close(0); /* close stdin */
	dup2(childfd, 1); /* map socket to stdout */
	dup2(childfd, 2); /* map socket to stderr */
	if (execve(filename, NULL, environ) < 0) {
	  perror("ERROR in execve");
	}
      }
    }
   /* clean up */
    fclose(stream);
    close(childfd);
  }
}

Unordered_set

[edytuj]

W C++ unordered_set to implementacja zestawu, który używa tablicy skrótów do przechowywania swoich elementów, zapewniając szybszy czas dostępu niż standardowa implementacja zestawu. Oto przykład użycia unordered_set w standardowej bibliotece szablonów C++ (STL):

#include <iostream>
#include <unordered_set>

using namespace std;

int main() {
    unordered_set<int> mySet; // create an empty unordered set

    // insert elements
    mySet.insert(3);
    mySet.insert(5);
    mySet.insert(1);
    mySet.insert(7);
    mySet.insert(9);

    // check if an element exists in the set
    if (mySet.find(5) != mySet.end()) {
        cout << "5 is present in the set" << endl;
    }

    // erase an element
    mySet.erase(7);

    // print the elements
    for (const auto& element : mySet) {
        cout << element << " ";
    }
    cout << endl;

    // clear the set
    mySet.clear();

    return 0;
}

W tym przykładzie najpierw tworzymy pusty nieuporządkowany zbiór liczb całkowitych o nazwie mySet. Następnie używamy metody insert, aby wstawić elementy do zestawu. Zauważ, że elementy nie są przechowywane w określonej kolejności, ponieważ jest to zbiór nieuporządkowany.

Następnie używamy metody find, aby sprawdzić, czy element jest obecny w zbiorze, oraz metody wymazywania, aby usunąć element ze zbioru. Następnie używamy pętli for opartej na zakresie, aby wydrukować elementy zestawu, oraz metody clear, aby usunąć wszystkie elementy z zestawu.

Należy zauważyć, że unordered_set ma podobną funkcjonalność do innych kontenerów C++ STL, takich jak zestaw, wektor i mapa, ale ma inną charakterystykę wydajności. Na przykład unordered_set zapewnia stałą średnią złożoność operacji wstawiania, usuwania i wyszukiwania, ale zużywa więcej pamięci niż implementacja zestawu.

Unordered_map

[edytuj]

W języku C++ unordered_map jest implementacją mapy, która używa tablicy skrótów do przechowywania par klucz-wartość, zapewniając szybszy czas dostępu niż standardowa implementacja mapy. Oto przykład użycia unordered_map w standardowej bibliotece szablonów C++ (STL):

#include <iostream>
#include <unordered_map>

using namespace std;

int main() {
    unordered_map<string, int> myMap; // create an empty unordered map

    // insert elements
    myMap["apple"] = 5;
    myMap["banana"] = 7;
    myMap["cherry"] = 3;
    myMap["date"] = 9;

    // check if a key exists in the map
    if (myMap.count("banana") > 0) {
        cout << "There are " << myMap["banana"] << " bananas" << endl;
    }

    // iterate over the key-value pairs
    for (const auto& pair : myMap) {
        cout << pair.first << ": " << pair.second << endl;
    }

    // clear the map
    myMap.clear();

    return 0;
}

W tym przykładzie najpierw tworzymy pustą mapę unordered_map, która odwzorowuje ciągi znaków na liczby całkowite o nazwie myMap. Następnie używamy operatora [], aby wstawić pary klucz-wartość do mapy.

Następnie używamy metody count, aby sprawdzić, czy klucz istnieje na mapie, oraz ponownie operatora [], aby pobrać wartość powiązaną z kluczem. Następnie używamy opartej na zakresie pętli for do iteracji par klucz-wartość na mapie oraz metody clear do usunięcia wszystkich par z mapy.

Należy zauważyć, że unordered_map ma podobną funkcjonalność do innych kontenerów C++ STL, takich jak mapa, wektor i zestaw, ale ma inną charakterystykę wydajności. Na przykład unordered_map zapewnia stałą średnią złożoność operacji wstawiania, usuwania i wyszukiwania, ale zużywa więcej pamięci niż implementacja mapy.

Stack

[edytuj]

W języku C++ stos jest implementacją struktury danych typu last-in, first-out (LIFO), w której elementy są wstawiane i usuwane tylko z jednego końca stosu. Oto przykład użycia stosu w standardowej bibliotece szablonów C++ (STL):

#include <iostream>
#include <stack>

using namespace std;

int main() {
    stack<int> myStack; // create an empty stack

    // push elements onto the stack
    myStack.push(3);
    myStack.push(5);
    myStack.push(1);
    myStack.push(7);
    myStack.push(9);

    // check the top element
    cout << "Top element: " << myStack.top() << endl;

    // pop the top element
    myStack.pop();

    // check the new top element
    cout << "New top element: " << myStack.top() << endl;

    // print the elements in the stack
    while (!myStack.empty()) {
        cout << myStack.top() << " ";
        myStack.pop();
    }
    cout << endl;

    return 0;
}

W tym przykładzie najpierw tworzymy pusty stos liczb całkowitych o nazwie myStack. Następnie używamy metody push, aby wstawić elementy na górę stosu. Zauważ, że ostatni element włożony na stos staje się elementem górnym.

Następnie używamy metody top do sprawdzenia wartości górnego elementu oraz metody pop do usunięcia górnego elementu ze stosu. Następnie używamy pętli, aby wydrukować elementy ze stosu, które są usuwane ze stosu metodą pop, aż stos będzie pusty.

Należy zauważyć, że stos zapewnia ograniczony zestaw operacji w porównaniu z innymi kontenerami C++ STL, takimi jak vector, list i deque, ale jest przydatny do implementowania algorytmów wymagających struktury danych LIFO, takich jak przeszukiwanie w głąb i ocena notacji przyrostkowej.

Iteratory

[edytuj]

Wstęp

[edytuj]

Idea iteratorów opiera się na tym, by ułatwić i usprawnić pracę na kontenerach. Daje możliwość dotarcia do danego składnika pojemnika bez konieczności znajomości jego struktury. Słusznie używanie iteratora przypomina pracę przy pomocy zwykłych wskaźników. Iterator umożliwia wygodny dostęp sekwencyjny do wszystkich elementów kontenera.

Można używać gotowych iteratorów dla kontenerów z STL przez dołączenie biblioteki <iterator>.

Istnieją pewne analogie między iteratorem a wskaźnikiem. Przede wszystkim znajomo wyglądają wyrażenia:

*nasz_iterator   // wartością jest element pojemnika wskazywany przez iterator nasz_iterator
wart = *nasz_iter   // podstawienie wartości elementu pod zmienną wart
*nasz_iterator = wart   // podstawienie wartości zmiennej w miejsce pojemnika wskazane przez nasz_iterator

Umożliwia nam to przeciążony operator*, dzięki któremu mamy dostęp do obiektu wskazywanego przez iterator jak również możliwość modyfikowania jego zawartości.

Podział iteratorów

[edytuj]
  • Iteratory wejścia:

Taki iterator może odczytać wartość elementu, na który wskazuje, o ile nie wskazuje przekroczenia zakresu – end(). Musi posiadać domyślny konstruktor, konstruktor kopiujący oraz operatory =, ==, !=, ++. Odczytać wartość można:

wart = *iterator;    // poprawne użycie iteratora wejścia
*iterator = wartosc;    // niepoprawne użycie iteratora wejścia- nie może zapisywać

Można inkrementować iterator – aby wskazywał następny składnik:

iterator++  lub  ++iterator


  • Iteratory wyjścia:

Ten typ iteratora umożliwia tylko zapis wartości do danego składnika – odczyt jest niemożliwy. Musi posiadać domyślny konstruktor, konstruktor kopiujący oraz operatory =, ++. np.

*iterator = wartosc;  // poprawnie
wartosc = *iterator;  // błędnie – próbujemy odczytać


  • Iteratory przejścia w przód:

Jest to połączenie operatora wejścia i wyjścia, posiada domyślny konstruktor, konstruktor kopiujący oraz operatory =, ==, != ,++. Możliwy jest zarówno zapis jak i odczyt. Przesuwanie się po kolejnych składnikach tak jak poprzednio jest możliwe tylko w przód poprzez inkrementacje iteratora.


  • Iteratory dwukierunkowe:

Różnią się tylko możliwością dekrementacji iteratora od iteratorów przejścia w przód.


  • Iteratory bezpośredniego dostępu:

Można powiedzieć, że ten typ z kolei dziedziczy wszystko po iteratorach dwukierunkowych, przy czym posiada możliwość dostępu bezpośrednio do wybranego składnika bez potrzeby skanowania struktury (w najgorszym wypadku całej). Z racji tego, że można przeskoczyć o większą liczbę składników niż jeden, iteratory te posiadają operatory: +, +=, -, -=, []. A także dodatkowo operatory <, >, <=, >=.

Sposób użycia iteratora bezpośredniego dostępu: (załóżmy ze nasz iterator wskazuje już składnik n-ty)

iterator += 5;   // teraz wskazuje (n+5)-ąty składnik
iterator++;   // teraz wskazuje (n+6)-ty składnik 
*iterator[n] = wartosc;   // przypisujemy n-temu składnikowi wartosc

Użycie w poszczególnych kontenerach (z przykładami)

[edytuj]

Przedstawimy teraz metody poszczególnych kontenerów, które umożliwiają operowanie na iteratorach. Należy podkreślić, że nazwy niektórych metod powtarzają się w różnych pojemnikach a także pełnią te same funkcje, dlatego nie będziemy ich za każdym razem dokładnie opisywać - podobnie jak przeładowanych operatorów.

Poruszanie się za pomocą iteratorów

[edytuj]

Poruszanie się za pomocą iteratorów po kolejnych składowych może odbywać się na dwa sposoby:
1) w przód - czyli od początku do końca np.:

kontener<typ kontenera>::iterator iter;   // iterator do przodu

początek struktury wyznacza metoda begin(); zaś koniec end();

2) od końca - czyli od końca do początku np.:

kontener<typ kontenera>::reverse_iterator iter;   // iterator od końca

skanować możemy od rbegin(); do rend();, co można utożsamiać z odwróconym początkiem i odwróconym końcem.

Użycie zostanie zobrazowane na przykładzie kontenera vector.

Wektor

[edytuj]

Przed użyciem iteratora należy najpierw nadać mu wartość. Dlatego gdy chcemy powtarzać pewną operacje w pętli dla każdego elementu wektora – od początku do końca – inicjujemy iterator wartością bezparametrowej funkcji begin(). Metoda ta zwraca iterator wskazujący na pierwszy element pojemnika wektor. Jest to iterator bezpośredniego dostępu. Nie mamy jednak możliwości porównywania iteratora z wartością NULL (bo iterator zwykłym wskaźnikiem nie jest) musi istnieć metoda która pokazuje koniec naszego wektora. Tak też jest – metoda end() zwraca iterator za ostatnim elementem. To także jest iterator bezpośredniego dostępu.

Przykład:

#include <iostream>
#include <vector>
using namespace std;
int main ()
{
   vector<int> tab;

   // inicjujemy wektor kolejnymi liczbami naturalnymi
   tab.push_back(1);
   tab.push_back(2);
   tab.push_back(3);
  
   // wyswietlenie skladnikow wektora tab w petli przy pomocy iteratora
   vector<int>::iterator it;
   for( it=tab.begin(); it!=tab.end(); ++it )
   {
     cout<< *it <<'\n';
   }
   return 0;
}

Program spowoduje wyświetlenie kolejnych elementów pojemnika:

1
2 
3

Metody begin() i end() skonstruowane są do przeglądania wektora od początku do końca. Co jeśli chcemy działać na składowych wektora w odwrotnej kolejności? Nie ma problemu. Istnieje bowiem metoda rbegin(), która zwraca odwrócony iterator wskazujący na ostatni element pojemnika (mówi się także, że jest to odwrócony początek). Odwołuje się on do elementu bezpośrednio poprzedzającego iterator wskazywany przez end. Jest to odwrócony iterator bezpośredniego dostępu. Mamy także metodę rend(), która zwraca odwrócony iterator do elementu odwołującego się do elementu bezpośrednio poprzedzającego pierwszy element kontenera wector (zwany także odwróconym końcem). rend() wskazuje miejsce bezpośrednio poprzedzające składnik do którego odwoływałby się begin(). Oto przykład:

#include <iostream>
#include <vector>
using namespace std;
int main ()
{
   vector<int> tab;
   // inicjujemy wektor kolejnymi liczbami naturalnymi
   tab.push_back(1);
   tab.push_back(2);
   tab.push_back(3);
  
   // wyświetlenie skladników wektora tab w pętli przy pomocy odwróconego iteratora
   vector<int>::reverse_iterator it;
   for( it=tab.rbegin(); it!=tab.rend(); ++it )
   {
      cout<<*it<<'\n';
   }
   return 0;
}

Wykonanie programu spowoduje wyświetlenie składników kontenera w odwrotnej kolejności:

3
2
1

Należy jeszcze podkreślić, że przy użyciu odwróconego iteratora, by przejrzeć wszystkie elementy pojemnika nie używamy -– a ++! Rzeczywiście, chcemy przejrzeć wektor od końca do początku i właśnie ku temu służy odwrócony iterator – zdefiniowany w nim porządek strukturalny jest odwrotny do porządku w zwykłym iteratorze.

Powyższe przykłady pokazują jak dużym ułatwieniem dla korzystania z iteratorów są przeładowane operatory inkrementacji i dekrementacji: operator++ i operator--, których używamy tu jak na zwykłej liczbie int. Mamy tu także operator= podstawienia, jak i porównanie iteratorów - operator!=.

Listy jedno- i dwukierunkowe

[edytuj]

Na liście dwukierunkowej działamy bardzo podobnie jak na wektorze. Tu także dysponujemy metodami begin() i end() zwracającymi wartość iteratora odpowiednio: na początek listy i tuż za koniec. Jest to iterator dwukierunkowy i możemy na nim działać zarówno do przodu (dzięki ++) jak i do tyłu (przez --). Mamy także analogiczne rbegin() i rend() zwracające odwrócony iterator do listy.

Przykład:

#include <iostream>
#include <list>
using namespace std;
 
int main ()
{
   // tworzymy i inicjujemy nowa listę znaków
   list<char> lista;
   lista.push_back('a');
   lista.push_back('b');
   lista.push_back('c');

   // i wyświetlamy ja w pętli do przodu
   list<char>::iterator it;
   cout<<"lista po kolei: ";
   for( it=lista.begin(); it!=lista.end(); ++it )
   {
      cout<<*it<<" ";  
   }

   // oraz do tylu (ale czy poprawnie?)
   cout<<"\nLista od tylu: ";
   for( it=lista.end(); it!=lista.begin(); --it )
   {
      cout<<*it<<" ";  
   }

   // znow wyświetlamy listę - teraz przy pomocy odwróconego iteratora
   list<char>::reverse_iterator it2;
   cout<<"\nLista od tylu z odwróconym iteratorem: ";
   for( it2=lista.rbegin(); it2!=lista.rend(); it2++ )
   {
      cout<<*it2<<" ";  
   }
   cout<<'\n'; 
   return 0;
}

Wynikiem działania programu będzie:

lista po kolei: a b c
lista od tylu: � c b
lista od tylu z odwróconym iteratorem: c b a

Przykład ten pokazuje nam dlaczego do przeglądania kontenera od końca należy używać iteratora odwróconego. Wyświetlenie elementu równego lista.end() przyniosło niespodziewany skutek - losowy symbol spoza naszej listy.

W liście jednokierunkowej nie mamy już tak szerokiego zakresu działania na iteratorach. Funkcje begin() i end() zwracają wartości iteratora (jednokierunkowego) „do przodu”, zaś odwrotny iterator wcale nie istnieje.

Zbiory

[edytuj]

Iterator w zbiorach działa jak w innych kontenerach. Iterator działający na zbiorze jest dwukierunkowy – możemy więc korzystać ze wszystkich operatorów przeciążonych dla iteratora dwukierunkowego. Dodatkowo (jak z własności zbioru wynika) należy wspomnieć, że metoda begin() daje dostęp iteratorowi do pierwszego elementu zbioru, który równocześnie posiada najniższy klucz. W zbiorach możemy także korzystać z odwróconych iteratorów.

#include<iostream>
#include<set>
using namespace std;
int main()
{
   set<int> zbior;
   zbior.insert(5);
   zbior.insert(40);
   zbior.insert(1);
   zbior.insert(11);

   set<int>::iterator it;   // teraz it jest wskaznikiem do zbioru
   for( it=zbior.begin(); it!=zbior.end(); ++it )
      cout<<*it<<'\n';

   return 0;
}

Wynikiem pracy programu będzie wypisanie cyfr:

1
5
11
40


Algorytmy w STL

[edytuj]

Wstęp

[edytuj]

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);

Lista funkcji zawartych w bibliotece <algorithm>

[edytuj]

Lista tematyczna

[edytuj]
  • for_each — wykonuje operację na każdym elemencie ciągu
  • count — liczy ilość wystąpień danej wartości w ciągu
  • count_if — zlicza w ciągu ilość wystąpień wartości spełniających warunek
  • equal — określa czy dwa zbiory elementów są takie same
  • mismatch — znajduje pierwszą parę różnych elementów dwóch ciągów
  • find — znajduje pierwsze wystąpienie wartości w ciągu
  • find_if — znajduje w ciągu pierwsze wystąpienie wartości spełniającej warunek
  • find_end — znajduje ostatnie wystąpienie ciągu jako podciągu
  • find_first_of — znajduje jakikolwiek element ze zbioru w danym ciągu
  • adjacent_find — znajduje sąsiadującą parę wartości
  • search — znajduje pierwsze wystąpienie ciągu jako podciągu
  • search_n — znajduje pierwsze wystąpienie ciągu n-tu wartości w ciągu
  • copy — kopiuje elementy jednego ciągu do drugiego
  • copy_backward — kopiuje ciąg do drugiego ciągu, wstawiając elementy na jego koniec
  • fill — zastępuje elementy ciągu podaną wartością
  • fill_n — zastępuje n elementów ciągu podaną wartością
  • generate — zastępuje elementy ciągu wartościami będącymi wynikiem funkcji
  • generate_n — zastępuje n elementów ciągu wartościami będącymi wynikiem funkcji
  • transform — wykonuje podaną funkcję dla argumentów ze zbioru i zapisuje wyniki w nowym ciągu
  • remove — usuwa elementy o podanej wartości
  • remove_if — usuwa elementy spełniające warunek
  • remove_copy — kopiuje ciąg, usuwając elementy o podanej wartości
  • remove_copy_if — kopiuje ciąg, usuwając elementy spełniające warunek
  • replace — zastępuje elementy o danej wartości inną wartością
  • replace_if — zastępuje elementy spełniające warunek
  • replace_copy — kopiuje ciąg, zastępując elementy o danej wartości inną wartością
  • replace_copy_if — kopiuje ciąg, zastępując elementy spełniające warunek
  • partition — umieszcza elementy spełniające warunek przed tymi które go nie spełniają
  • stable_partition — umieszcza elementy spełniające warunek przed tymi które go nie spełniają, zachowuje wzajemną kolejność
  • random_shuffle — w losowy sposób zmienia kolejność elementów ciągu
  • reverse — odwraca kolejność elementów w ciągu
  • reverse_copy — kopiuje ciąg, odwracając kolejność elementów
  • rotate — dokonuje rotacji elementów ciągu
  • rotate_copy — kopiuje ciąg, przesuwając elementy (rotacja)
  • unique — usuwa powtórzenia, w taki sposób że wśród sąsiadujących elementów nie ma dwóch takich samych
  • unique_copy — kopiuje ciąg, w taki sposób że wśród sąsiadujących elementów nie ma dwóch takich samych
  • swap — zamienia ze sobą dwa elementy
  • swap_ranges — zamienia ze sobą dwa zbiory elementów
  • iter_swap — zamienia ze sobą dwa elementy wskazywane przez iteratory
  • sort — sortuje ciąg rosnąco
  • partial_sort — sortuje pierwsze N najmniejszych elementów w ciągu
  • partial_sort_copy — tworzy kopię N najmniejszych elementów ciągu
  • stable_sort — sortuje ciąg zachowując wzajemną kolejność dla równych elementów
  • nth_element — ciąg jest podzielony na dwie nieposortowane części elementów mniejszych i większych od wybranego elementu

Operacje na posortowanych ciągach

  • lower_bound — zwraca iterator do pierwszego elementu równego lub większego od podanego
  • upper_bound — zwraca iterator do pierwszego elementu większego od podanego
  • binary_search — stwierdza czy element występuje w ciągu
  • equal_range — zwraca parę określającą przedział wewnątrz którego występuje dana wartość (lub ich ciąg).

Operacje na posortowanych ciągach

  • is_heap — zwraca prawdę jeśli ciąg tworzy kopiec
  • make_heap — przekształca ciąg elementów tak aby tworzyły kopiec
  • push_heap — dodaje element do kopca
  • pop_heap — usuwa element ze szczytu kopca
  • sort_heap — przekształca ciąg o strukturze kopca w ciąg posortowany
  • max — zwraca większy z dwóch elementów
  • max_element — zwraca największy z elementów w ciągu
  • min — zwraca mniejszy z elementów
  • min_element — zwraca najmniejszy z elementów w ciągu
  • lexicographical_compare — sprawdza czy jeden ciąg poprzedza leksykograficznie drugi ciąg
  • next_permutation — przekształca ciąg elementów w leksykograficznie następną permutację
  • prev_permutation — przekształca ciąg elementów w leksykograficznie poprzedzającą permutację

Zdefiniowane w nagłówku <numeric>


Operacje niemodyfikujące

[edytuj]

Poniższe przykłady wymagają dołączenia bibliotek i przestrzeni nazw

#include <iostream>
#include <vector>
#include <string>
using namespace std;


for_each()

[edytuj]
for_each( iterator początek, iterator koniec, funkcja )
Działanie
wykonuje operację na każdym elemencie ciągu.
Przykład
poniższy program wywołuje dla każdego elementu dodanego do vectora funkcję echo.
void echo(short num)
{
   cout << num << endl;
}

int main()
{
   vector<short> vect;

   vect.push_back(5);
   vect.push_back(4);
   vect.push_back(3);

   for_each(vect.begin(), vect.end(), echo);
   return 0;
}

Na wyjściu pojawi się:

5
4
3

count()

[edytuj]
count( iterator początek, iterator koniec, wartość )
Działanie
liczy ilość wystąpień danej wartości w ciągu.
Przykład
program przekształca tablicę liczb w wektor i zlicza ilość znajdujących się w nim dwójek.
int main()
{
   int tablica[] = { 2, 5, 7, 9, 2, 9, 2 };
   vector<int> v(tablica, tablica+7);
   cout << "Ilosc dwojek w tablicy: " << count( v.begin(), v.end(), 2 );
   return 0;
}


count_if()

[edytuj]
count_if( iterator początek, iterator koniec, funkcja f )
Działanie
liczy w ciągu ilość wystąpień wartości spełniających warunek
Przykład
program przekształca tablicę liczb w wektor i zlicza ilość znajdujących się w nim liczb parzystych.
bool czyParzysta(int n){
  return (n%2 ? false : true);
}

int main(){
  int tablica[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
  vector<int> v(tablica, tablica+10);
  cout << "Ilosc parzystych: " << count_if(v.begin(), v.end(), czyParzysta); 
  return 0;
}


equal()

[edytuj]
bool equal( iterator początek, iterator koniec, iterator początek_drugiego )
bool equal( iterator początek, iterator koniec, iterator początek_drugiego, funkcja_porownująca )
Działanie
porównywany jest pierwszy zakres elementów (początek, koniec) z drugim zakresem (zaczynającym się w początek_drugiego).
Przykład
program porównuje łańcuch str z napis oraz z napis2. Porównanie ogranicza się do 2 znaków (długość str).
int main()
{
   string str= "bc";
   string napis= "abcde";
   string napis2= "bcde";
 
   if( equal(str.begin(), str.end(), napis.begin()) )
      cout << "Takie same\n";
   else
      cout << "Rozne\n";

   if( equal(str.begin(), str.end(), napis2.begin()) )
      cout << "Takie same";
   else
      cout << "Rozne";
   return 0;
}

Wynikiem jest kolejno Rozne oraz Takie same.

Operacje niemodyfikujące - szukanie

[edytuj]

mismatch()

[edytuj]
pair<> mismatch( iterator1 początek, iterator1 koniec, iterator2 początek_drugi )
pair<> mismatch( iterator1 początek, iterator1 koniec, iterator2 początek_drugi, funkcja )

znajduje pierwsze różne elementy dwóch ciągów

Wartość zwracana
para znalezionych różniących się wartości, typu pair<iterator1, iterator2>
Działanie
porównuje kolejne elementy w dwóch zbiory (pierwszy określony przez początek, koniec oraz drugi zaczynający się w początek_drugi). Zwraca pierwsze wystąpienie dwóch różnych elementów, lub parę <koniec, odpowiedni_koniec_drugi> jeśli nie znajdzie.

find()

[edytuj]
iterator find( iterator początek, iterator koniec, wartość )
Działanie
znajduje pierwsze wystąpienie wartości w ciągu i zwraca iterator do niej, lub jeśli nie zostanie znaleziona, zwraca iterator koniec.
Przykład
program tworzy tablicę liczb 0, 10, 20, ..., 90 i sprawdza, czy znajdują się w nim liczby 30 i 33.
int main()
{
   vector<int> zbior;
   vector<int>::iterator wynik1, wynik2;

   for( int i = 0; i < 10; ++i )
      zbior.push_back(i*10);

   wynik1 = find(zbior.begin(), zbior.end(), 30);
   wynik2 = find(zbior.begin(), zbior.end(), 33);
   if( wynik1 != zbior.end() )
      cout << "Znaleziono 30.";
   if( wynik2 != zbior.end() )
      cout << "Znaleziono 33.";
}

Wynikiem jest: Znaleziono 30.

find_if()

[edytuj]
iterator find_if( iterator początek, iterator koniec, funkcja  )
Działanie
znajduje w ciągu pierwsze wystąpienie wartości spełniającej warunek


find_end()

[edytuj]
iterator find_end( iterator początek, iterator koniec, iterator początek_szukany, iterator koniec_szukany )
iterator find_end( iterator początek, iterator koniec, iterator początek_szukany, iterator koniec_szukany, funkcja )
Działanie
znajduje ostatnie wystąpienie ciągu (początek_szukany, koniec_szukany) jako podciągu w przedziale (początek, koniec). Można dostarczyć własną funkcję do porównywania elementów.

find_first_of()

[edytuj]
iterator find_first_of( iterator początek, iterator koniec, iterator początek_zbiór, iterator koniec_zbiór)
iterator find_first_of( iterator początek, iterator koniec, iterator początek_zbiór, iterator koniec_zbiór, funkcja )
Działanie
znajduje choć jeden element ze zbioru (początek_zbiór, koniec_zbiór) w podanym ciągu (początek, koniec). Można dostarczyć własną funkcję do porównywania elementów.

adjacent_find()

[edytuj]
iterator adjacent_find( iterator początek, iterator koniec )
iterator adjacent_find( iterator początek, iterator koniec, funkcja )
Działanie
porównuje kolejne wartości w obu ciągach, aż znajdzie parę tych samych - zwraca wówczas iterator do tej wartości, lub koniec jeśli nie ma pary identycznych wartości. Można dostarczyć własną funkcję do porównywania elementów.

search()

[edytuj]
iterator search( iterator początek, iterator koniec, iterator początek_szukany, iterator koniec_szukany )
iterator search( iterator początek, iterator koniec, iterator początek_szukany, iterator koniec_szukany, funkcja )
Działanie
znajduje pierwsze wystąpienie ciągu (początek_szukany, koniec_szukany) jako podciągu w przedziale (początek, koniec). Można dostarczyć własną funkcję do porównywania elementów.

search_n()

[edytuj]
iterator search_n( iterator początek, iterator koniec, n, wartość )
iterator search_n( iterator początek, iterator koniec, n, wartość, funkcja )
Działanie
znajduje pierwsze wystąpienie ciągu złożonego z n-tu wartości jako podciągu w przedziale (początek, koniec). Można dostarczyć własną funkcję do porównywania elementów.

Przykładowy program

[edytuj]
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;

void wypisz(int key)
{
	cout<<key<<" ";
}

bool dodatnia(int key)
{
	return (key>0?true:false);
}

bool porownanie(int jeden, int dwa)
{
	return(jeden+1==dwa?true:false);
}

int main()
{
	int tab[]={-9,-6,-1,9,-2,0,0,-1,5,0,-8,9,10,-6,10};
	int tab2[]={-8,-5,0,10,-1,1,1,0,6,1,-7,10,11,-5,11};
	vector<int> wektorek(tab,tab+15);
	vector<int> wektorek2(tab2,tab2+15);
	for_each(wektorek.begin(),wektorek.end(),wypisz);
	cout<<endl;
	for_each(wektorek2.begin(),wektorek2.end(),wypisz);
	pair<vector<int>::iterator, vector<int>::iterator > para;
	para=mismatch(wektorek.begin(),wektorek.end(),wektorek2.begin());
	cout<<"\nPierwsza niezgodnosc w wektorkach (mismatch): "<<*para.first;
	cout<<" i "<<*para.second<<"\nPozycja, na ktorej wylapano niezgodnosc: ";
	cout<<(para.first-wektorek.begin())<<" i "<<(para.second-wektorek2.begin());
	vector<int>::iterator dycha=find(wektorek.begin(),wektorek.end(),10);
	cout<<"\nPozycja dziesiatki w wektorku pierwszym(find): "<<(dycha-wektorek.begin());
	dycha=find_if(wektorek.begin(),wektorek.end(),dodatnia);
	cout<<"\nPierwsza dodatnia z wektorka(find_if): "<<*dycha<<". Znajduje sie na pozycji: "<<(dycha-wektorek.begin());
	vector<int>::iterator findend=find_end(wektorek.begin(),wektorek.end(),wektorek.begin(),wektorek.begin()+4);
	cout<<"\nPoczatek ostatniego wystapienia ciagu wektorka 0-4 w wektorku zaczyna sie na pozycji: "<<(findend-wektorek.begin());
	vector<int>::iterator findfirstof;
	findfirstof=find_first_of(wektorek.begin(),wektorek.end(),wektorek2.begin(),wektorek2.begin()+5);
	cout<<"\nPierwszy element z przedzialu wektorka2 0-5 znajduje sie na pozycji (find_first_of): "<<(findfirstof-wektorek.begin());
	vector<int>::iterator adjacentfind;
	adjacentfind=adjacent_find(wektorek.begin(),wektorek.end());
	cout<<"\nPierwsza para w wektorku zaczyna sie na pozycji (adjacent_find): "<<(adjacentfind-wektorek.begin());
	vector<int>::iterator sercz;
	sercz=search_n(wektorek2.begin(),wektorek2.end(),2,1);
	cout<<"\nPoczatek ciagu zlozonego z dwoch jedynek w wektorku 2 znajduje sie na pozycji (search_n): "<<(sercz-wektorek2.begin());
	return 0;
}

Wynik działania powyższego programu:

-9 -6 -1 9 -2 0 0 -1 5 0 -8 9 10 -6 10 
-8 -5 0 10 -1 1 1 0 6 1 -7 10 11 -5 11 
Pierwsza niezgodnosc w wektorkach (mismatch): -9 i -8
Pozycja, na ktorej wylapano niezgodnosc: 0 i 0
Pozycja dziesiatki w wektorku pierwszym(find): 12
Pierwsza dodatnia z wektorka(find_if): 9. Znajduje sie na pozycji: 3
Poczatek ostatniego wystapienia ciagu wektorka 0-4 w wektorku zaczyna sie na pozycji: 0
Pierwszy element z przedzialu wektorka2 0-5 znajduje sie na pozycji (find_first_of): 2
Pierwsza para w wektorku zaczyna sie na pozycji (adjacent_find): 5
Poczatek ciagu zlozonego z dwoch jedynek w wektorku 2 znajduje sie na pozycji (search_n): 5

Operacje modyfikujące

[edytuj]

copy()

[edytuj]
copy( iterator początek, iterator koniec, iterator początek_kopia )
Działanie
kopiuje ciąg (początek, koniec) do drugiego ciągu
Przykład
tworzy ciąg 10 liczb, po czym kopiuje je do drugiego (pustego) ciągu
int main()
{
   vector<int> ciag;
   for (int i = 0; i < 10; i++) 
      ciag.push_back(i);
 
   vector<int> kopia(10);
 
   copy( ciag.begin(), ciag.end(), kopia.begin() );
}

copy_backward()

[edytuj]
copy_backward( iterator początek, iterator koniec, iterator koniec_kopia )
Działanie
kopiuje ciąg (początek, koniec) do drugiego ciągu, tak aby kończyły się razem z jego końcem
Przykład
tworzy ciąg 10 liczb, po czym kopiuje je do drugiego ciągu o długości 13 (elementy będą przesunięte, aby wyrównać do końca)
int main()
{
   vector<int> ciag;
   for( int i = 0; i < 10; i++ )
      ciag.push_back(i);
 
   vector<int> kopia(13);
 
   copy_backward( ciag.begin(), ciag.end(), kopia.end() );

   for( int i = 0; i < kopia.size(); i++)
      cout << kopia[i] << " ";
}

Wynikiem będzie 0 0 0 0 1 2 3 4 5 6 7 8 9.

fill()

[edytuj]
fill( iterator początek, iterator koniec, wartość )
Działanie
zastępuje elementy ciągu podaną wartością.

fill_n()

[edytuj]
fill_n( iterator początek, n, wartość )
Działanie
zastępuje n pierwszych elementów ciągu podaną wartością.

generate()

[edytuj]
generate( iterator początek, iterator koniec, funkcja )
Działanie
zastępuje elementy ciągu wartościami będącymi wynikiem funkcji.

generate_n()

[edytuj]
generate_n( iterator początek, n, funkcja )
Działanie
zastępuje n elementów ciągu wartościami będącymi wynikiem funkcji

transform()

[edytuj]
transform( iterator początek, iterator koniec, iterator nowy_początek, funkcja )
transform( iterator początek, iterator koniec, iterator początek_drugiego, iterator nowy_początek, funkcja_dwuargumentowa )
Działanie
wykonuje podaną funkcję dla argumentów ze zbioru (początek, koniec) i zapisuje wyniki do zbioru zaczynającego się w nowy_początek. Druga wersja wykonuje funkcję dla pary argumentów, korzystając z drugiego zbioru (wskazywanego przez początek_drugiego).
Przykład
program wykonuje funkcję sqrt dla każdego z 5 elementów ciągu, zapisując wyniki w nowym ciągu. Niezbędne było określenie którego przeładowania funkcji sqrt uzywamy.
int main()
{
   double tablica[5] = {2, 3, 9, 16, 25};
   vector<double> v(tablica, tablica+5);
   vector<double> wyniki(5);

   transform(v.begin(), v.end(), wyniki.begin(), (double (*) (double)) sqrt );
   for( int i=0; i<5; i++ )
      cout << wyniki[i] << '\n';
}

remove()

[edytuj]
Działanie
usuwa elementy o podanej wartości, np. spacje.

remove_if()

[edytuj]
Działanie
usuwa elementy spełniające warunek

remove_copy()

[edytuj]
Działanie
kopiuje ciąg, usuwając elementy o podanej wartości

remove_copy_if()

[edytuj]
Działanie
kopiuje ciąg, usuwając elementy spełniające warunek

replace()

[edytuj]
Działanie
zastępuje elementy o danej wartości inną wartością

replace_if()

[edytuj]
Działanie
zastępuje elementy spełniające warunek

replace_copy()

[edytuj]
Działanie
kopiuje ciąg, zastępując elementy o danej wartości inną wartością

replace_copy_if()

[edytuj]
Działanie
kopiuje ciąg, zastępując elementy spełniające warunek

Operacje zmieniające kolejność

[edytuj]

partition()

[edytuj]
partition( iterator początek, iterator koniec, funkcja )
Działanie
umieszcza elementy spełniające warunek przed tymi które go nie spełniają

stable_partition()

[edytuj]
stable_partition( iterator początek, iterator koniec, funkcja )
Działanie
umieszcza elementy spełniające warunek przed tymi które go nie spełniają, zachowuje wzajemną kolejność

random_shuffle()

[edytuj]
random_shuffle( iterator początek, iterator koniec )
random_shuffle( iterator początek, iterator koniec, generator_liczb_pseudolosowych )
Działanie
w losowy sposób zmienia kolejność elementów ciągu

reverse()

[edytuj]
reverse( iterator początek, iterator koniec )
Działanie
odwraca kolejność elementów w ciągu

reverse_copy()

[edytuj]
reverse_copy( iterator początek, iterator koniec, iterator początek_kopia )
Działanie
kopiuje ciąg do drugiego ciągu, odwracając kolejność elementów

rotate()

[edytuj]
rotate( iterator początek, iterator nowy_początek, iterator koniec )
Działanie
przesuwac elementy w taki sposób aby pierwszym elementem był nowy_początek; element go poprzedzający staje się ostatnim

rotate_copy()

[edytuj]
rotate_copy( iterator początek, iterator nowy_początek, iterator koniec, iterator początek_kopia )
Działanie
kopiuje ciąg do drugiego ciągu, przesuwając elementy w taki sposób aby pierwszym elementem był nowy_początek; element go poprzedzający staje się ostatnim

unique()

[edytuj]
Działanie
usuwa powtórzenia, w taki sposób że wśród sąsiadujących elementów nie ma dwóch takich samych

unique_copy()

[edytuj]
iterator unique_copy( iterator początek, iterator koniec, iterator początek_kopia )
iterator unique_copy( iterator początek, iterator koniec, iterator początek_kopia, funkcja )
Działanie
kopiuje ciąg do drugiego ciągu, w taki sposób że wśród sąsiadujących elementów nie ma dwóch takich samych

swap()

[edytuj]
swap( element1, element2 )
Działanie
zamienia ze sobą dwa elementy.

swap_ranges()

[edytuj]
swap_ranges( iterator początek, iterator koniec, iterator początek_drugiego )
Działanie
zamienia ze sobą dwa zbiory elementów: zbiór (początek, koniec) z drugim (o podanym początku).

iter_swap()

[edytuj]
iter_swap( iterator element1, iterator element2 )
Działanie
zamienia ze sobą dwa elementy wskazywane przez iteratory.

Operacje sortujące

[edytuj]

sort()

[edytuj]

void sort( RandomAccessIterator start, RandomAccessIterator end ) void sort( RandomAccessIterator start, RandomAccessIterator end, Compare cmp )

Działanie
sortuje ciąg rosnąco

Jak używać:

#include<algorithm>
...
sort( iterator start, iterator koniec );

//albo

sort( iterator start, iterator koniec, cmp ); //cmp - funkcja porównująca
...

W pierwszym przypadku algorytm sort() ustawia elementy w zakresie [start,koniec) w porządku niemalejącym. Gdy wywołujemy sortowanie z trzema parametrami to sortowanie odbywa się względem funkcji porównującej, którą definiujemy.

Przykładowe kody źródłowe
vector<int> v;
 v.push_back( 23 );
 v.push_back( -1 );
 v.push_back( 9999 );
 v.push_back( 0 );
 v.push_back( 4 );              

 cout << "Przed sortowaniem: ";
 for( int i = 0; i < v.size(); ++i ) {
   cout << v[i] << " ";
 }
 cout << endl;            

 sort( v.begin(), v.end() );            

 cout << "Po sortowaniu: ";
 for( int i = 0; i < v.size(); ++i ) {
   cout << v[i] << " ";
 }
 cout << endl;   

Efektem działania tego programu będzie:

Przed sortowaniem: 23 -1 9999 0 4
Po sortowaniu: -1 0 4 23 9999

Inny przykład, tym razem z funkcją definiowaną przez programistę:

bool cmp( int a, int b ) {
   return a > b;
 }              

 ...            

 vector<int> v;
 for( int i = 0; i < 10; ++i ) {
   v.push_back(i);
 }              

 cout << "Przed: ";
 for( int i = 0; i < 10; ++i ) {
   cout << v[i] << " ";
 }
 cout << endl;            

 sort( v.begin(), v.end(), cmp );               

 cout << "Po: ";
 for( int i = 0; i < 10; ++i ) {
   cout << v[i] << " ";
 }
 cout << endl; 

Wyniki działania takiego programu będą następujące:

Przed: 0 1 2 3 4 5 6 7 8 9
Po: 9 8 7 6 5 4 3 2 1 0


partial_sort()

[edytuj]
void partial_sort( random_access_iterator start, random_access_iterator middle, random_access_iterator end )
void partial_sort( random_access_iterator start, random_access_iterator middle, random_access_iterator end, StrictWeakOrdering cmp )
Działanie
sortuje pierwsze N najmniejszych elementów w ciągu

partial_sort_copy()

[edytuj]
random_access_iterator partial_sort_copy( input_iterator start, input_iterator end, random_access_iterator result_start, random_access_iterator result_end )
random_access_iterator partial_sort_copy( input_iterator start, input_iterator end, random_access_iterator result_start, random_access_iterator result_end, StrictWeakOrdering cmp )
Działanie
tworzy kopię N najmniejszych elementów ciągu

stable_sort()

[edytuj]
void stable_sort( random_access_iterator start, random_access_iterator end )
void stable_sort( random_access_iterator start, random_access_iterator end, StrictWeakOrdering cmp )
Działanie
sortuje ciąg zachowując wzajemną kolejność dla równych elementów

nth_element()

[edytuj]
void nth_element( random_access_iterator start, random_access_iterator nth, random_access_iterator end )
void nth_element( random_access_iterator start, random_access_iterator nth, random_access_iterator end, StrictWeakOrdering cmp )
Działanie
ciąg jest podzielony na nieposortowane grupy elementów mniejszych i większych od wybranego elementu Nth (odpowiednio po lewej i prawej jego stronie)

Operacje wyszukiwania binarnego

[edytuj]

lower_bound()

[edytuj]
lower_bound( iterator początek, iterator koniec, wartość )
lower_bound( iterator początek, iterator koniec, wartość, funkcja_porównująca )
Działanie
zwraca iterator do pierwszego elementu równego lub większego od wartość. Jest to pierwsze miejsce, w które można wstawić wartość aby zachować uporządkowanie ciągu.

upper_bound()

[edytuj]
upper_bound( iterator początek, iterator koniec, wartość )
upper_bound( iterator początek, iterator koniec, wartość, funkcja_porównująca )
Działanie
zwraca iterator do pierwszego elementu większego od wartość. Jest to ostatnie miejsce, w które można wstawić wartość aby zachować uporządkowanie ciągu.

binary_search()

[edytuj]
bool binary_search( iterator początek, iterator koniec, wartość )
bool binary_search( iterator początek, iterator koniec, wartość, funkcja_porównująca )
Działanie
zwraca prawdę jeśli wartość znajduje się w ciągu (działa w czasie logarytmicznym).

equal_range()

[edytuj]
pair<> equal_range( iterator początek, iterator koniec, wartość )
pair<> equal_range( iterator początek, iterator koniec, wartość, funkcja_porównująca )
Działanie
zwraca parę określającą przedział wewnątrz którego występuje dana wartość (lub ich ciąg), para złożoną z wartości odpowiednio lower_bound i upper_bound.

Operacje na zbiorze

[edytuj]

merge()

[edytuj]
merge( iterator początek, iterator koniec, iterator początek_drugiego, iterator koniec_drugiego, iterator wynik_początek )
merge( iterator początek, iterator koniec, iterator początek_drugiego, iterator koniec_drugiego, iterator wynik_początek, funkcja_porównująca )
Działanie
łączy dwa posortowane ciągi w nowy, posortowany ciąg.

inplace_merge()

[edytuj]
inplace_merge( iterator początek, iterator środek, iterator koniec )
inplace_merge( iterator początek, iterator środek, iterator koniec, funkcja_porównująca )
Działanie
łączy dwie posortowane części ciągu, rozdzielone elementem środek, tak że cały ciąg staje się posortowany.

includes()

[edytuj]
includes( iterator początek, iterator koniec, iterator początek_drugiego, iterator koniec_drugiego )
includes( iterator początek, iterator koniec, iterator początek_drugiego, iterator koniec_drugiego, funkcja_porównująca )
Działanie
zwraca prawdę jeśli pierwszy ciąg jest podciągiem drugiego.

set_difference()

[edytuj]
set_difference( iterator początek, iterator koniec, iterator początek_drugiego, iterator koniec_drugiego, iterator wynik )
set_difference( iterator początek, iterator koniec, iterator początek_drugiego, iterator koniec_drugiego, iterator wynik, funkcja_porównująca )
Działanie
tworzy różnicę zbiorów - posortowany ciąg elementów pierwszego ciągu, które nie występują w drugim.

set_intersection()

[edytuj]
set_intersection( iterator początek, iterator koniec, iterator początek_drugiego, iterator koniec_drugiego, iterator wynik )
set_intersection( iterator początek, iterator koniec, iterator początek_drugiego, iterator koniec_drugiego, iterator wynik, funkcja_porównująca )
Działanie
tworzy przecięcie dwóch zbiorów (zbiór złożony z elementów występujących w obu zbiorach).

set_symmetric_difference()

[edytuj]
set_symmetric_difference( iterator początek, iterator koniec, iterator początek_2, iterator koniec_2, iterator wynik )
set_symmetric_difference( iterator początek, iterator koniec, iterator początek_2, iterator koniec_2, iterator wynik, funkcja_porównująca )
Działanie
tworzy zbiór złożony z elementów występujących w tylko jednym z dwóch ciągów.

set_union()

[edytuj]
set_union( iterator początek, iterator koniec, iterator początek_drugiego, iterator koniec_drugiego, iterator wynik )
set_union( iterator początek, iterator koniec, iterator początek_drugiego, iterator koniec_drugiego, iterator wynik, funkcja_porównująca )
Działanie
tworzy sumę zbiorów (posortowany zbiór elementów z obu zbiorów, bez powtórzeń).

Operacje na kopcu

[edytuj]

is_heap()

[edytuj]
bool is_heap( iterator początek, iterator koniec )
bool is_heap( iterator początek, iterator koniec, funkcja_porównująca )
Działanie
zwraca prawdę jeśli ciąg tworzy kopiec.

make_heap()

[edytuj]
make_heap( iterator początek, iterator koniec )
make_heap( iterator początek, iterator koniec, funkcja_porównująca )
Działanie
przekształca ciąg elementów tak aby tworzyły kopiec.

push_heap()

[edytuj]
push_heap( iterator początek, iterator koniec )
push_heap( iterator początek, iterator koniec, funkcja_porównująca )
Działanie
ostatni element w ciągu zostaje dołączony do struktury kopca.

pop_heap()

[edytuj]
pop_heap( iterator początek, iterator koniec )
pop_heap( iterator początek, iterator koniec, funkcja_porównująca )
Działanie
usuwa element ze szczytu kopca (o największej wartości), zostaje on przenoszony poza nową strukturę kopca (na koniec ciągu).

sort_heap()

[edytuj]
sort_heap( iterator początek, iterator koniec )
sort_heap( iterator początek, iterator koniec, funkcja_porównująca )
Działanie
przekształca ciąg o strukturze kopca w ciąg posortowany

Operacje min max

[edytuj]

max()

[edytuj]
wartość max( element1, element2 )
wartość max( element1, element2, funkcja_porównująca )
Działanie
zwraca większy z dwóch elementów

max_element()

[edytuj]
iterator max_element( iterator początek, iterator koniec )
iterator max_element( iterator początek, iterator koniec, funkcja_porównująca )
Działanie
zwraca największy z elementów w ciągu

min()

[edytuj]
wartość min( element1, element2 )
wartość min( element1, element2, funkcja_porównująca )
Działanie
zwraca mniejszy z dwóch elementów

min_element()

[edytuj]
iterator min_element( iterator początek, iterator koniec )
iterator min_element( iterator początek, iterator koniec, funkcja_porównująca )
Działanie
zwraca najmniejszy z elementów w ciągu

lexicographical_compare()

[edytuj]
bool lexicographical_compare( iterator początek, iterator koniec, iterator początek_drugiego, iterator koniec_drugiego )
bool lexicographical_compare( iterator początek, iterator koniec, iterator początek_drugiego, iterator koniec_drugiego, funkcja )
Działanie
sprawdza czy jeden ciąg poprzedza leksykograficznie drugi ciąg, zwraca prawdę jeśli poprzedza.

next_permutation()

[edytuj]
bool next_permutation( iterator początek, iterator koniec )
bool next_permutation( iterator początek, iterator koniec, funkcja_porównująca )
Działanie
przekształca ciąg elementów w leksykograficznie następną permutację. Zwraca prawdę przy powodzeniu.

prev_permutation()

[edytuj]
bool prev_permutation( iterator początek, iterator koniec )
bool prev_permutation( iterator początek, iterator koniec, funkcja_porównująca )
Działanie
przekształca ciąg elementów w leksykograficznie poprzedzającą permutację. Zwraca prawdę przy powodzeniu.

Operacje numeryczne

[edytuj]

accumulate()

[edytuj]
accumulate( iterator początek, iterator koniec, wartość )
accumulate( iterator początek, iterator koniec, wartość, funkcja )
Działanie
sumuje ciąg elementów

inner_product()

[edytuj]
inner_product( iterator początek, iterator koniec, iterator początek_wyniku, wartość )
inner_product( iterator początek, iterator koniec, iterator początek_wyniku, wartość, funkcja_dodawania, funkcja_mnozenia )
Działanie
oblicza iloczyn skalarny na elementach dwóch ciągów

adjacent_difference()

[edytuj]
adjacent_difference( iterator początek, iterator koniec, iterator początek_wyniku)
adjacent_difference( iterator początek, iterator koniec, iterator początek_wyniku, funkcja )
Działanie
oblicza różnice pomiędzy sąsiadującymi elementami w ciągu

partial_sum()

[edytuj]
partial_sum( iterator początek, iterator koniec, iterator początek_wyniku)
partial_sum( iterator początek, iterator koniec, iterator początek_wyniku, funkcja )
Działanie
oblicza sumy częściowe ciągu elementów

Inne klasy STL

[edytuj]

C++/Inne klasy STL

Inne klasy w STL

[edytuj]

Sekcja „C++/Inne klasy w STL” znajduje się w budowie

Jeżeli chcesz rozszerzyć ten podręcznik o tę sekcję, kliknij na ten link.

Dodatek B

Przykłady programów w C++

[edytuj]

proste działania matematyczne

[edytuj]
  // 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;
 }

Łańcuchy

[edytuj]

Łańcuchy znaków korzystające z biblioteki standardowej, w której zaimplementowano uogólnioną klasę napisów zwaną string

#include <iostream>
#include <string>


using namespace std;

int main(void) {
  string greeting = "Hello";
  cout << greeting<< std::endl;
  return 0;
}

Kompilacja

g++ s.cpp -Wall -Wextra

Uruchomienie

./a.out


Wynik:

Hello


konwersja liczby dziesiętnej do dwójkowej

[edytuj]

Liczba dwójkowa ( binarna) jest w postaci łańcucha

/*
g++ b.cpp -Wall -Wextra -lm

https://stackoverflow.com/questions/8222127/changing-integer-to-binary-string-of-digits

*/

#include <iostream> // cout
#include <cmath>   // log
#include <string>   // 
#include <bitset>   //

using namespace std;


#define LENGTH 
int main(void) {

	int number = 2356;
	int length = floor(log(number)/log(2)) + 1; // https://www.exploringbinary.com/number-of-bits-in-a-decimal-integer/
	string binary_string = "";
	
	const int c_length = 12; // floor(3.4* length_of_decimal_number )
	
	binary_string = bitset< c_length > (number).to_string(); // string conversion
	cout << "decimal number = "<< number << " is binary base = " << binary_string << endl;
	cout << "length of binary digit is  = "<< length << endl;
	return 0;
}


Kompilacja

g++ b.cpp -Wall -Wextra -lm

Uruchomienie

./a.out


Wynik:

decimal number = 2356 is binary base = 100100110100
length of binary digit is  = 12


vector bool

[edytuj]

Vector bool zachowuje się jak dynamiczny bitset. Długość jest zmienną[2]

/*

g++ v.cpp -Wall -Wextra

*/

#include <iostream>
#include <vector>

using namespace std;

int main(){

    vector<bool> tab;
    int n;
    cout << "Podaj długość tablicy " << endl;
    cin >> n;

    for( int i=0; i<n; ++i )
    {
       int element;
       cout << "Podaj element (0 lub 1) " << endl ;
       cin >> element;
       tab.push_back(element);
    }
    
    cout << "wyświetl elementy tablicy  " << endl;
    
    // https://www.kompikownia.pl/index.php/2019/04/18/poznaj-nowoczesna-tablice-vector-w-c/
    for(auto i : tab) { // range-based loop od  C++11

    	cout<<i<<endl;
	}
    
    return 0;
}

Przekroczenie limitu liczb całkowitych

[edytuj]
#include <iostream>
#include <math.h>       /* pow */

int main()
{	unsigned long long int r;
	unsigned long long int t;

	 for ( r = 2; r < 70; ++r){
	 
	 	t = pow(2,r);
	 	std::cout << r << "\t 2^r = " << t << "\n"; //
	 }

	return 0;
}

wynik:

./a.out
2	 2^r = 4
3	 2^r = 8
4	 2^r = 16
5	 2^r = 32
6	 2^r = 64
7	 2^r = 128
8	 2^r = 256
9	 2^r = 512
10	 2^r = 1024
11	 2^r = 2048
12	 2^r = 4096
13	 2^r = 8192
14	 2^r = 16384
15	 2^r = 32768
16	 2^r = 65536
17	 2^r = 131072
18	 2^r = 262144
19	 2^r = 524288
20	 2^r = 1048576
21	 2^r = 2097152
22	 2^r = 4194304
23	 2^r = 8388608
24	 2^r = 16777216
25	 2^r = 33554432
26	 2^r = 67108864
27	 2^r = 134217728
28	 2^r = 268435456
29	 2^r = 536870912
30	 2^r = 1073741824
31	 2^r = 2147483648
32	 2^r = 4294967296
33	 2^r = 8589934592
34	 2^r = 17179869184
35	 2^r = 34359738368
36	 2^r = 68719476736
37	 2^r = 137438953472
38	 2^r = 274877906944
39	 2^r = 549755813888
40	 2^r = 1099511627776
41	 2^r = 2199023255552
42	 2^r = 4398046511104
43	 2^r = 8796093022208
44	 2^r = 17592186044416
45	 2^r = 35184372088832
46	 2^r = 70368744177664
47	 2^r = 140737488355328
48	 2^r = 281474976710656
49	 2^r = 562949953421312
50	 2^r = 1125899906842624
51	 2^r = 2251799813685248
52	 2^r = 4503599627370496
53	 2^r = 9007199254740992
54	 2^r = 18014398509481984
55	 2^r = 36028797018963968
56	 2^r = 72057594037927936
57	 2^r = 144115188075855872
58	 2^r = 288230376151711744
59	 2^r = 576460752303423488
60	 2^r = 1152921504606846976
61	 2^r = 2305843009213693952
62	 2^r = 4611686018427387904
63	 2^r = 9223372036854775808
64	 2^r = 0
65	 2^r = 0
66	 2^r = 0
67	 2^r = 0
68	 2^r = 0
69	 2^r = 0



Ćwiczenia

[edytuj]

Ćwiczenie 1

[edytuj]

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. Następnie zmodyfikować go tak, aby pracował na plikach: wejściowym oraz wyjściowym, podanych jako parametry programu..

Ćwiczenie 2

[edytuj]

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

Ćwiczenie 3

[edytuj]

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
  1. Wypisać wszystkie nazwiska kończące się na "ski" w porządku alfabetycznym, bez powtórzeń.
  2. Wypisać w odwrotnym porządku alfabetycznym osobno imiona żeńskie i męskie.

Ćwiczenie 4

[edytuj]

Napisz funkcje wypisującą liczby pierwsze (lub złożone) z podanego zakresu liczb.

Podpowiedź: Należy użyć algorytmu sita Eratostenesa.

Ćwiczenie 5

[edytuj]

Mamy taki oto plik źródł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, którą dostaje człowiek zdefiniowany jako obiekt klasy wydatkiMiesieczne, jest w stanie pokryć 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 wartości polom danych:
    +-----------+-----------------+
    |nazwa pola |wartość która ma |
    |  danych   |zostać przypisana|
    +-----------+-----------------+
    | cenaChleb |         2       |
    | cenaMleko |         3       |
    | cenaGazeta|         2       |
    |           |                 |
    | sumaWydat-|                 |
    | kowMiesie-|         0       |
    | cznych    |   (opcjonalnie) |
    +-----------+-----------------+
  • Pamiętaj że zmienne cenaChleb, cenaMleko i cenaGazeta reprezentują cenę za jedną sztukę danego produktu, a my będziemy potrzebować ceny za zakup tych towarów przez cały miesiąc (przyjmijmy ze miesiąc ma 30 dni).
  • Nie twórz innych funkcji w sekcji public niż te, które zostały wywołane w funkcji main().
  • Funkcja zarobek() ma pobierać liczbę reprezentującą zarobek danej osoby (w zł) i wpisywać tą wartość w zmienną pensja.
  • Funkcje z przedrostkami czyKupuje- mają przypisywać wartość (true - jeśli kupuje dany produkt; false - jeśli nie) do swoich odpowiedników z sekcji private (np. funkcja czyKupujeMleko() przypisze wartość zmiennej zakupMleka).
  • Funkcja obliczanieWydatkowMiesiecznych() ma obliczyć kwotę jaka będzie wydana przez miesiąc kupowania ustalonych przez obiekt produktów i przypisać wynik zmiennej sumaWydatkowMiesiecznych.
  • Funkcja czyWystarczaPieniedzyNaWydatkiMiesieczne() ma obliczyć czy pensja danego obiektu jest wystarczająca na pokrycie kosztów zakupów, przekazać wynik true, albo false do zmiennej czyWystarczaNaMiesiac i zwrócić go.


Różnice między C a C++

[edytuj]
"Hello World" program w C i C++.

Komentarze

[edytuj]

W ANSI C (C4) nie jest dozwolone używanie komentarzy zaczynających się od // Zostały jednak dodane w standardzie C4

Stałe

[edytuj]

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.

Zmienne

[edytuj]
  • możliwość deklarowania zmiennych np. w instrukcji sterującej petli while
  • możliwość mieszania instrukcji i deklaracji zmiennych w jednym bloku kodu (w ANSI C zmienne muszą być deklarowane przed pierwszą instrukcją)

Wiązanie (ang. linkage) i obiekty niejawne

[edytuj]

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.

Typ stałej znakowej

[edytuj]

W języku C literał znakowy (stała znakowa), np. 'a' jest traktowana jako int, natomiast w C++ jest uważana za char.

Typ bool

[edytuj]

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.

Typy wskaźników

[edytuj]

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ą zhierarchizowane (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.

Alternatywne słowa kluczowe

[edytuj]

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 , xor_eq i not_eq . W obecnych czasach nie ma potrzeby ich używania ani pamiętania.

Biblioteka standardowa

[edytuj]

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.

Funkcje

[edytuj]

W języku C pusta lista argumentów: funkcja() 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) - nie przyjmowanie argumentów, natomiast efekt taki, co puste nawiasy w C można uzyskać 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 funkcję (bezparametrową, zwracającą Klasa2) i zwraca typ Klasa1. Rozwiązaniem jest dodanie nawiasów, więcej światła na ten problem rzucą poniższe przykłady:

Klasa1 o1( Klasa2 funkcja() ); // funkcja (przyjmująca funkcję)
Klasa1 o2( Klasa2 (int) );  // funkcja (przyjmująca funkcję przyjmującą jeden argument)
Klasa1 o3( Klasa2 (10) );   // obiekt (z podaniem tymczasowego obiektu, z podaniem wartości)
Klasa1 o4( (Klasa2()) );    // obiekt (z podaniem tymczasowego obiektu)

Dekorowanie (mangling) nazw funkcji

[edytuj]

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").

Struktury

[edytuj]

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

Zasoby

[edytuj]

Linki zewnętrzne

[edytuj]

Książki

[edytuj]

Licencja

[edytuj]

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.

0. PREAMBLE

[edytuj]

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.

1. APPLICABILITY AND DEFINITIONS

[edytuj]

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.

2. VERBATIM COPYING

[edytuj]

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.

3. COPYING IN QUANTITY

[edytuj]

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.

4. MODIFICATIONS

[edytuj]

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.

5. COMBINING DOCUMENTS

[edytuj]

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."

6. COLLECTIONS OF DOCUMENTS

[edytuj]

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.

7. AGGREGATION WITH INDEPENDENT WORKS

[edytuj]

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.

8. TRANSLATION

[edytuj]

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.

9. TERMINATION

[edytuj]

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.

10. FUTURE REVISIONS OF THIS LICENSE

[edytuj]

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.

How to use this License for your documents

[edytuj]

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.


  1. C++ standard: A function declaration [...] with an inline specifier declares an inline function
  2. poznaj-nowoczesna-tablice-vector-w-c Autor Karol Ślusarz