Podstawy języka C++ – część 7 – biblioteka standardowa STL

cze 6, 2025 | Język C++

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ów std::vector oraz std::list. Ma zastosowanie głównie w mechanizmach kolejek, czyli tam, gdzie potrzebujemy efektywnie wstawiać oraz pobierać elementy na obu końcach.
C++
#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 zbiorze
  • std::map – mapa (słownik) z unikalnymi kluczami, pozwala dynamicznie budować słownik oraz efektywne odczytywanie wartości na podstawie klucza
  • std::multiset, std::multimap – pozwalają na powtarzające się wartości i klucze
C++
#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

C++
#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 kontenera
  • liczby.end() – pozyskanie iteratora, który symbolizuje umownie element następny za ostatnim
  • ++it – przesunięcie iteratora na następny element
  • it != 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.

C++
#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 aplikacji
  • cin – console input – czyli standardowe wejście z konsoli (stdin), pozwala nam odbierać wpisywane przez użytkownika polecenia terminala
  • cerr – console error – czyli standardowe wyjście błędów (stderr), osobny, równoległy do cout 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.

C++
#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 kontenera
  • std::reverse(begin, end) – odwraca kolejność elementów
  • std::copy(begin, end, dest) – kopiuje elementy z jednej kolekcji do innej
  • std::copy_if(begin, end, dest, pred) – kopiuje elementy warunkowo
  • std::fill(begin, end, value) – wypełnia kontener elementami
  • std::find(begin, end, value) – wyszukuje element w kolekcji
  • std::transform(begin, end, dest, func) – przekształca zbiór daną funkcją
  • std::accumulate(begin, end, init) – sumuje elementy
  • std::equal(begin1, end1, begin2) – porównuje kolekcjie
  • std::shuffle(begin, end, rng) – permutacja elementów
  • std::min(a, b) – zwraca mniejszy argument
  • std::max(b, b) – zwraca większy argument
  • std::clamp(value, hi, lo) – ogranicza wartość do zakresu

Zastosujmy to w praktyce

C++
#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 gdy a jest mniejsze od b
  • std::greater(a, b) – true gdy a > b
  • std::greater_equal(a, b) – true gdy a >= b
  • std::equal_to(a, b) – true gdy a == b
  • std::logical_and(a, b) – true gdy a && 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

C++
#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.