C++ to język, który od dekad łączy wydajność z ogromną elastycznością, a jego siłą niezmiennie pozostaje bogata biblioteka standardowa (STL – Standard Template Library). Choć często kojarzona jest głównie z kontenerami, ale równie ważną część STL stanowią też algorytmy. To właśnie one umożliwiają pisanie zwięzłego, bezpiecznego i wydajnego kodu, eliminując potrzebę ręcznego implementowania wielu podstawowych operacji.
Kontenery
Jeśli pamiętasz, w części pierwszej wspomniałem o tablicach. Tablica to nic innego jak zbiór do przechowywania wielu zmiennych. Jest to najbardziej prymitywny sposób dostarczony przez jezyk C++. W praktyce jednak często potrzebujemy nieco bardziej specjalizowanych zbiorów, które dobieramy odpowiednio do zastosowania. I tu z pomocą przychodza kontenery STL. Kontenery w bibliotece standardowej C++ to struktury danych, które pozwalają na przechowywanie i organizowanie zbiorów obiektów w uporządkowany sposób. Ich główną zaletą jest to, że są uniwersalne, bezpieczne i zoptymalizowane pod kątem zastosowania. Kontenery w bibliotece standardowej C++ dzielimy na kilka głównych kategorii:
Kontenery sekwencyjne (Sequence Containers)
Przechowują elementy w określonej kolejności, umożliwiając dostęp do nich na podstawie pozycji.
std::array
– statyczna tablica – odpowiednik zwykłej tablicy, przy czym dostarcza standardowy interfejs kontenerów, pozwala na pracę jak ze zwykłą tablicą, ale także świetnie współpracuje z algorytmami. Jedynym ograniczeniem jest to, że rozmiar takiej tablicy określamy w kodzie i tego rozmiaru nie można już zmienić.std::vector
– jest to tablica dynamiczna, jej rozmiar można łatwo zmieniać w trakcie działania programu w zalezności od potrzeb. Stosujemy ją przede wszystkim wtedy, gdy potrzebujemy wydajnego sposobu aby aby dostać sie do jej elementów. Dodawanie elementów na końcu jest względnie optymalne, natomiast wstawianie elementów w środku i na początku jest już kosztowne.std::list
– lista dwukierunkowa – ma zastosowanie wtedy, gdy potrzebujemy szybkiego wstawiania elementów w środku kolekcji i efektywnego iterowania po całej kolekcji, niestety nie otrzymujemy tego za darmo. Odbywa się to głównie kosztem szybkiego dostępu do wybranego elementu w środku.std::deque
– kolejka dwustronna – jest pewnego rodzaju hybrydą kontenerówstd::vector
orazstd::list
. Ma zastosowanie głównie w mechanizmach kolejek, czyli tam, gdzie potrzebujemy efektywnie wstawiać oraz pobierać elementy na obu końcach.
#include <iostream>
#include <vector>
#include <queue>
int main() {
// Tworzymy pusty wektor liczb całkowitych
std::vector<int> liczby = {1,2,3,4,5};
// Dodajemy element na końcu
liczby.push_back(10);
// Zmieniamy wartość drugiego elementu (indeks 1)
liczby[1] = 25; // zmieniamy 20 na 25
// Wypisujemy zawartość wektora
for (size_t i = 0; i < liczby.size(); ++i) {
std::cout << "Element " << i << ": " << liczby[i] << std::endl;
}
// Tworzymy kolejke zadan
std::queue<std::string> kolejkaZadan;
// Dodajemy zadania do kolejki
kolejkaZadan.push("Sprawdzic e-maile");
kolejkaZadan.push("Zadzwonic do klienta");
kolejkaZadan.push("Zrobic raport");
// Przetwarzamy zadania po kolei
while (!kolejkaZadan.empty()) {
std::cout << "Wykonuję: " << kolejkaZadan.front() << std::endl;
kolejkaZadan.pop(); // Usuwamy wykonane zadanie
}
std::cout << "Wszystkie zadania zostaly wykonane." << std::endl;
return 0;
}
Kontenery asocjacyjne (Associative Containers)
Przechowują elementy w uporządkowany sposób, zwykle jako zbiory elementów unikalnych lub pary klucz–wartość, z szybkim dostępem do elementów. Elementy w zbiorze są automatycznie sortowane według zadanego predykatu. Te kontenery stosuje sie najczęściej, gdy potrzebujemy często wyszukiwać określone elementy zbioru.
std::set
– zbiór unikalnych elementów, nie pozwala aby elementy sie powtarzały w zbiorzestd::map
– mapa (słownik) z unikalnymi kluczami, pozwala dynamicznie budować słownik oraz efektywne odczytywanie wartości na podstawie kluczastd::multiset
,std::multimap
– pozwalają na powtarzające się wartości i klucze
#include <map>
int main() {
// Tworzymy mapę, gdzie kluczem jest int (np. ID), a wartością string (np. imię)
std::map<int, std::string> osoby;
// Dodajemy dane
osoby[1] = "Alicja";
osoby[2] = "Bartek";
osoby[3] = "Celina";
// Zapytanie o osobę o ID 1002
int szukaneID = 2;
auto it = osoby.find(szukaneID);
if (it != osoby.end()) {
std::cout << "Znaleziono osobę o ID " << szukaneID << ": " << it->second << std::endl;
} else {
std::cout << "Nie znaleziono osoby o ID " << szukaneID << std::endl;
}
// Tworzymy zbiór liczb całkowitych
std::set<int> liczby;
// Dodajemy elementy do zbioru
liczby.insert(5);
liczby.insert(2);
liczby.insert(9);
liczby.insert(2); // duplikat, nie zostanie dodany
// Sprawdzamy, czy liczba 5 znajduje się w zbiorze
int szukana = 5;
if (liczby.find(szukana) != liczby.end()) {
std::cout << "Liczba " << szukana << " znajduje się w zbiorze." << std::endl;
} else {
std::cout << "Liczba " << szukana << " nie zostala znaleziona." << std::endl;
}
return 0;
}
Kontenery nieuporządkowane (Unordered Containers)
Bazują na tablicach haszujących, zapewniają bardzo szybki dostęp do danych przy braku gwarancji kolejności przechowywania. Te kolekcje przechowują elementy w takiej kolejności, aby dostęp do elementów był jak najbardziej wydajny. Nazywają się odpowiednio:
std::unordered_set
std::unordered_map
std::unordered_multiset
std::unordered_multimap
Kontenery adaptery (Container Adapters)
Są to kontenery, które same w sobie nie przechowują danych i potrzebują innych kontenerów do istnienia. Ich zadaniem jest jedynie udostępnienie uproszczonego interfejs do konkretnego sposobu użycia. Odpowiedni kontener programista może dobrać sam. Są to std::stack
czyli stos, std::queue
i std::priority_queue
czyli kolejki.
Najdokładniejsza dokumentacja funkcji kontenerów jest dostępna pod adresem https://en.cppreference.com/w/cpp/container
Iteratory
Iteratory w C++ pełnią rolę pośredników między algorytmami a kontenerami – umożliwiają dostęp do elementów kontenerów w sposób zbliżony do wskaźników. Dzięki nim możemy przemieszczać się po strukturach danych, modyfikować zawartość, porównywać elementy oraz korzystać z algorytmów standardowej biblioteki bez znajomości konkretnej implementacji kontenera. Iteratory są fundamentem działania wielu algorytmów STL i pozwalają pisać kod uniwersalny, niezależny od konkretnego kontenera. Poniżej przykład użycia iteracji po tablicy liczb
#include <iostream>
#include <array>
int main() {
// Tworzymy tablice na 5 liczb całkowitych
std::array<int, 5> liczby = {10, 20, 30, 40, 50};
// Iterujemy po wektorze za pomocą iteratora
for (auto it = liczby.begin(); it != liczby.end(); ++it) {
std::cout << *it << std::endl; // wyłuskanie wartości z iteratora
}
return 0;
}
W tym przykładzie można wyróżnic następujące konstrukcje
liczby.begin()
– pozyskanie iteratora, który wskazuje na pierwszy element konteneraliczby.end()
– pozyskanie iteratora, który symbolizuje umownie element następny za ostatnim++it
– przesunięcie iteratora na następny elementit != liczby.end()
– sprawdzenie, czy dotarliśmy do końca i czy można przeskoczyć na następny element*it
– wyłuskanie wartości elementu na który wskazuje iterator
Spójne API iteratorów sprawia, że programista może utworzyć własny typ kontenera i będzie on wówczas współpracował ze wszystkimi algorytmami biblioteki standardowej.
Tekst
Język C++ pozwala na prace z tekstem za pomocą łańcuchów znakowych typu char*
w stylu języka C. W praktyce każdy taki ciąg jest zakończony specjalnym znakiem końca '\0'
, a to oznacza to zajmuje on w pamięci o jeden bajt wiecej niż ma w sobie znaków. Programiści często o tym zapominali i z tego powodu popełniali drobne błędy, które były często powodem różnych podatności. Dlatego do biblioteki standardowej została dodana klasa std::string
aby praca z tekstem w C++ stała się łatwiejsza i przede wszystkim bezpieczniejsza. Obiekt std::string
pozwala na wygodne tworzenie, modyfikowanie i analizowanie ciągów znaków bez konieczności zarządzania pamięcią, jak ma to miejsce w przypadku klasycznych tablic znaków. std::string
obsługuje wiele przydatnych operacji, takich jak konkatenacja (+
), porównywanie (==
, <
, itd.), wycinanie fragmentów (substr
), znajdowanie podciągów (find
, rfind
), czy modyfikacja zawartości (insert
, replace
, erase
). Można go też łatwo łączyć z innymi komponentami C++, np. konwertować liczby na tekst za pomocą std::to_string
lub czytać dane z pliku i przetwarzać linia po linii. Dzięki przeładowanym operatorom i intuicyjnemu interfejsowi std::string
pozwala na wydajną i przede wszystkim bezpieczną manipulację tekstem w stylu zbliżonym do innych nowoczesnych języków programowania. Ale co tu dużo gadać, zobaczmy sobie przykłady.
#include <iostream>
#include <string>
int main() {
// Tworzenie i inicjalizacja
std::string imie = "John";
std::string nazwisko("Doe");
std::cout << "Imie: " << imie << ", nazwisko: " << nazwisko << std::endl;
// Łaczenie (konkatenacja) lancuchow znakow oprator+()
std::string pelnaNazwa = imie + " " + nazwisko;
std::cout << "Pelna nazwa: " << pelnaNazwa << std::endl;
// Dostęp do wybranych znakow i modyfikacja, operator[]()
imie[0] = 'K'
std::cout << "Imie: " << imie << std::endl; // wyswietli Kohn
// Sprawdzanie dlugosci tekstu length()
std::cout << "Dlugośc tekstu: " << imie.length() << std::endl;
// Wyszukiwanie tekstu find()
std::string zdanie = "Programowanie w C++";
size_t pozycja = zdanie.find("C++");
if (pozycja != std::string::npos) {
std::cout << "Znaleziono 'C++' na pozycji: " << pozycja << std::endl;
} else {
std::cout << "Nie znaleziono." << std::endl;
}
// Wycinanie fragmentu substr()
std::string data = "2025-05-13";
std::string rok = data.substr(0, 4);
std::string miesiac = data.substr(5, 2);
std::string dzien = data.substr(8, 2);
std::cout << "Rok: " << rok << ", Miesiac: " << miesiac << ", Dzien: " << dzien << std::endl;
// Porównywanie łańcuchów znaków operator==()
if(imie == nazwisko) {
std::cout << "Imie i nazwisko jest takie samo" << std::endl;
} else {
std::cout << "Imie i nazwisko rozni sie" << std::endl;
}
// konwersja do char*, ale UWAGA na ograniczenia:
// - tylko const, wiec nie mozna modyfikowac tej pamieci
// - mozna korzystac tylko do poki obiekt string istnieje i nie zostal zmodyfikowany
const char* str = imie.c_str();
return 0;
}
Strumienie i pliki
Klasa std::string
, choć pozwala na wiele, to nalezy pamiętać, że jest przede wszystkim tylko kontenerem. Nie została stworzona z myślą o wydajności, więc należy raczej unikać ich modyfikacji, takie operacje prowadzą do częstych reallokacji pamięci. Jeśli więc mamy potrzebę operowania na długich tekstach, wówczas z pomocą przyjdą nam strumienie.
Strumienie w bibliotece standardowej C++ (iostream
) umożliwiają wygodne i elastyczne operacje wejścia i wyjścia, zarówno z konsolą, jak i z plikami. Klasy takie jak std::cin
, std::cout
, std::cerr
obsługują standardowe wejście i wyjście, pozwalając na czytanie danych od użytkownika i wypisywanie wyników. Dla operacji na plikach dostępne są std::ifstream
, std::ofstream
oraz std::fstream
, które umożliwiają otwieranie plików w trybie odczytu, zapisu lub obu jednocześnie. Strumienie STL obsługują operatory >>
i <<
, co pozwala na naturalne, przypominające składnię języka naturalnego manipulowanie danymi. Co więcej, dzięki klasom takim jak std::stringstream
możemy łatwo przekształcać dane między formatem tekstowym a zmiennymi programu, np. dzieląc ciągi tekstowe lub konwertując liczby z i na tekst. Strumienie w C++ są typami sparametryzowanymi i silnie zintegrowanymi z resztą STL, dzięki czemu oferują bezpieczny, wydajny i rozszerzalny sposób obsługi danych wejściowych i wyjściowych.
Iostream
Zapewne zauwazyłeś, że w wielu przykładach załączałem nagłówek iostream
. Dzieki niemu mamy dostęp do trzech zmiennych globanych w przestrzeni nazw std
. Są to cout
, cin
, cerr
cout
– console output – czyli standardowe wyjście na konsole (stdout), pozwala nam wysyłac informacje tekstowe na terminal użytkownika naszej aplikacjicin
– console input – czyli standardowe wejście z konsoli (stdin), pozwala nam odbierać wpisywane przez użytkownika polecenia terminalacerr
– console error – czyli standardowe wyjście błędów (stderr), osobny, równoległy docout
kanał, służący do przesyłania informacji o błędach, zwykle użytkownik będzie odbierał na terminalu oba kanały, ale w skryptach powłoki istnieje pożliwość rozdzielenia ich i przekierowania np. do pliku lub innej aplikacji
Aby zobrazować działanie tych strumieni napiszemy krótki program „Echo”, który wczytuje linie tekstu z std::cin
i wysyła je spowrotem na std::cout
. Jeśli przesłana linia bedzie pusta, program zakończy działania. Jesli pierwszy znak lini będzie „!”, wówczas prześlemy wyjście na std::cerr
.
#include <iostream>
#include <string>
int main() {
std::string linia;
std::cout << "Wpisz tekst (pusta linia konczy):" << std::endl;
while (true) {
std::getline(std::cin, linia);
// Usuwanie '\r' jeśli występuje na koncu (typowe dla systemu Windows)
if (!linia.empty() && linia.back() == '\r') {
linia.pop_back();
}
if (linia.empty()) {
break; // zakończ petle na pustej linii
}
if (linia[0] == '!') {
std::cerr << "Blad: " << linia << std::endl;
} else {
std::cout << "Echo: " << linia << std::endl;
}
}
std::cout << "Do zobacznia!" << std::endl;
return 0;
}
Algorytmy
Algorytmy w bibliotece standardowej C++ (STL) stanowią potężne narzędzie do przetwarzania danych w kontenerach, oferując szeroki zestaw gotowych funkcji, które upraszczają i skracają kod. Zamiast ręcznego pisania pętli, możemy używać funkcji do przekształcania danych. Algorytmy STL działają w oparciu o iteratory, co oznacza, że są niezależne od konkretnego typu kontenera. Wiele z nich przyjmuje również funkcje lub wyrażenia lambda jako argumenty, co pozwala łatwo dopasować zachowanie algorytmu do konkretnego problemu. Dzięki temu algorytmy STL umożliwiają pisanie kodu bardziej deklaratywnego, czytelnego i łatwiejszego do testowania, jednocześnie zachowując wysoką wydajność. Poniżej przedstawię listę kilku wybranych najpopularniejszych funkcji z tej biblioteki.
std::sort(begin, end)
– sortuje elementy kontenerastd::reverse(begin, end)
– odwraca kolejność elementówstd::copy(begin, end, dest)
– kopiuje elementy z jednej kolekcji do innejstd::copy_if(begin, end, dest, pred)
– kopiuje elementy warunkowostd::fill(begin, end, value)
– wypełnia kontener elementamistd::find(begin, end, value)
– wyszukuje element w kolekcjistd::transform(begin, end, dest, func)
– przekształca zbiór daną funkcjąstd::accumulate(begin, end, init)
– sumuje elementystd::equal(begin1, end1, begin2)
– porównuje kolekcjiestd::shuffle(begin, end, rng)
– permutacja elementówstd::min(a, b)
– zwraca mniejszy argumentstd::max(b, b)
– zwraca większy argumentstd::clamp(value, hi, lo)
– ogranicza wartość do zakresu
Zastosujmy to w praktyce
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
int main() {
std::vector<int> v = {3, 1, 4, 1, 5, 9};
std::sort(v.begin(), v.end());
int sum = std::accumulate(v.begin(), v.end(), 0);
auto found = std::find(v.begin(), v.end(), 4);
if (found != v.end()) {
std::cout << "Znaleziono 4\n";
}
std::cout << "Suma: " << sum << '\n';
return 0;
}
Jeśli chcesz poznać kompletną listę, zapraszam do lektury strony https://en.cppreference.com/w/cpp/algorithm.html
Predykaty
Predykaty to proste funkcje, które przyjmują jeden (unary) lub dwa (binary) argumenty. Ich zadaniem jest zwrócenie wartości bool
(true lub false). Taką funkcję możemy łączyć z algorytmamy, paramtetryzując w ten sposób ich zachowanie. Biblioteka STL dostarcza wielu prymitywnych predykatów, oto lista kilku przykładowych:
std::less(a, b)
– zwraca true gdya
jest mniejsze odb
std::greater(a, b)
– true gdya > b
std::greater_equal(a, b)
– true gdya >= b
std::equal_to(a, b)
– true gdya == b
std::logical_and(a, b)
– true gdya && b
Nic nie stoi na przeszkodzie, aby napisac własny predykat zawierający specyficzną logikę. Możemy to osiągnąć definując własną funkcję, lub wyrażenie lambda. Zaprezentuję to na przykładach
#include <algorithm>
#include <vector>
#include <iostream>
bool jest_parzysta(int x) { return x % 2 == 0; }
int main() {
std::vector<int> liczby = {1, 4, 6, 7, 9, 12};
// Sortuj malejąco uzywając gotowego predykatu
std::sort(liczby.begin(), liczby.end(), std::greater<>());
// Zlicz liczby parzyste
int ile_parzystych = std::count_if(liczby.begin(), liczby.end(), jest_parzysta);
// to samo co wyżej, wykorzysując wyrazenie lambda
ile_parzystych = std::count_if(liczby.begin(), liczby.end(), [](int x) { return x % 2 == 0; });
std::cout << "Liczb parzystych: " << ile_parzystych << '\n';
return 0;
}
Podsumowanie
Standardowa biblioteka C++ STL to potężne narzędzie, które pozwala pisać krótszy, czytelniejszy i bardziej wydajny kod. Dzięki setkom gotowych algorytmów – takich jak std::sort
, std::find_if
, std::count
, std::remove_if
, std::accumulate
czy std::transform
– możesz operować na danych bez ręcznego pisania pętli i warunków. Kluczem do skutecznego korzystania z tych algorytmów są iteratory, zakresy oraz predykaty – czyli funkcje określające warunki działania algorytmu. STL promuje styl programowania funkcyjnego i generycznego, zwiększając elastyczność aplikacji i ułatwiając refaktoryzację kodu. Jeśli jeszcze nie używasz algorytmów STL na co dzień – czas to zmienić. Zyskasz czystszy kod i mniej błędów.