1. Wprowadzenie
Kiedy dopiero zaczynasz programować, łatwo wpaść w iluzję, że „najważniejsze są liczby”. Rzeczywiście: oceny, pieniądze, wiek, liczniki — wszędzie cyfry. Ale gdy zechcesz uczynić program choć trochę przyjaznym dla człowieka, nagle okazuje się, że człowiek komunikuje się słowami. Imię, nazwisko, miasto, komentarz do zamówienia, tytuł filmu — to wszystko tekst, czyli string.
Kiedy C++ powstał, nie było w nim specjalnego typu dla stringów (string) — string traktowano po prostu jako ciąg znaków (typ char). Jednak z czasem okazało się, że to niewygodne, i zdecydowano o wprowadzeniu odrębnego typu string oraz dodaniu go do standardowej (STD) biblioteki C++. Tak powstał w C++ typ std::string.
Można go traktować jak kontener sekwencji znaków: wewnątrz są litery, spacje, znaki interpunkcyjne. I co ważne, standardowa biblioteka daje gotowe operacje na stringach: łączenie, poznawanie długości, dostęp do znaków po indeksie.
Aby przećwiczyć pracę ze std, będziemy rozwijać jedno małe aplikacyjne demo w trakcie kursu: „konsolowa wizytówka”. Na razie będzie po prostu składać ładny string wizytówki i wypisywać go. Dzisiaj dodamy imię i kilka tekstowych detali.
2. Pierwszy string: dołączamy <string>
Teraz moment, który na początku wydaje się drobiazgiem, a potem staje się drugą naturą: aby używać std::string, zwykle trzeba dołączyć nagłówek <string>. Tak, w niektórych kompilatorach „jakoś działa bez niego”, ale to zły nawyk: lepiej dołączać to, z czego korzystasz.
Zacznijmy od prostego: utworzymy string z literału znakowego "..." i wypiszemy go.
#include <iostream>
#include <string>
int main() {
std::string course = "C++ for beginners";
std::cout << course << '\n'; // C++ for beginners
}
Tu ważna idea: std::string to typ, jak int, tylko dla tekstu. Zadeklarowaliśmy zmienną course i włożyliśmy w nią tekst. Potem zachowuje się jak wartość, którą można przekazać do wypisu.
String "A" i znak 'A'
W programowaniu jest dowcip: „dwa cudzysłowy decydują o losie projektu”. I niestety to prawie prawda. W C++ są literały łańcuchowe w podwójnych cudzysłowach "..." i literały znakowe w pojedynczych '...'. Różnica jest zasadnicza: string to sekwencja znaków, a znak to dokładnie jeden znak.
Jeśli potrzebujesz przechować imię "Ada", to jest string. Jeśli potrzebujesz jednego znaku, np. pierwszej litery imienia 'A', to jest znak. std::string przechowuje string, a typ dla pojedynczego znaku to formalnie char, o którym powiemy później.
Porównaj:
#include <iostream>
#include <string>
int main() {
std::string word = "Cat"; // string tekstu
char firstLetter = 'C'; // jeden znak
std::cout << word << '\n'; // Cat
std::cout << firstLetter << '\n'; // C
}
Na pierwszy rzut oka podobne, ale sens inny: word może mieć dowolną długość, a firstLetter to dokładnie jeden znak.
3. Konkatenacja: + i +=
Przechodzimy do bardzo „ludzkiej” operacji: złożenia z kawałków jednej ładnej frazy. W stringach nazywa się to konkatenacją. Wyobraź sobie, że string to konstruktor LEGO, tylko zamiast klocków masz słowa i spacje. Składasz zdanie, które pokażesz użytkownikowi.
Są dwa podstawowe sposoby łączenia:
- a + b tworzy nowy string (jakbyś wziął dwa arkusze i zrobił trzeci, gdzie jest wszystko razem).
- a += b dopisuje do istniejącego stringa (jakbyś kontynuował pisanie na tym samym arkuszu).
Zacznijmy od +:
#include <iostream>
#include <string>
int main() {
std::string first = "Ada";
std::string last = "Lovelace";
std::string full = first + " " + last;
std::cout << full << '\n'; // Ada Lovelace
}
Zwróć uwagę na " ". Spacja też jest znakiem i często ma znaczenie. Jeśli zapomnisz spacji, powstanie AdaLovelace, co wygląda jak nazwa klasy, a nie imię i nazwisko.
Teraz wariant z += — wygodny, gdy budujesz string krok po kroku:
#include <iostream>
#include <string>
int main() {
std::string badge = "Name: ";
badge += "Ada";
badge += " ";
badge += "Lovelace";
std::cout << badge << '\n'; // Name: Ada Lovelace
}
Pod względem czytelności przypomina „składanie stringa krok po kroku”. W przyszłości, gdy pojawią się warunki i pętle, += będzie szczególnie przydatne, ale już teraz dobrze pokazuje ideę „dopisania”.
Mała uwaga: string i liczby nie da się po prostu „dodawać” jak stringi. Na przykład "Age: " + 10 — to nie zadziała, bo to różne typy. Nie będziemy teraz wnikać w konwersje; zapamiętaj: stringi łączy się ze stringami.
4. Długość stringa: size()
Kiedy trzymasz string, często chcesz wiedzieć, ile ma znaków. Dla tego std::string ma metodę size() (czasem spotyka się też length(), ale dziś wystarczy size()). To jak prędkościomierz pokazujący „ile znaków jest teraz w stringu”.
Zobaczmy w praktyce:
#include <iostream>
#include <string>
int main() {
std::string city = "Boston";
std::cout << city.size() << '\n'; // 6
}
Warto przyzwyczaić się: size() to pierwsza rzecz, o której myślisz, zanim zaczniesz indeksować. Bo indeksowanie bez rozumienia długości — to jak iść po schodach po omacku, zakładając, że stopnie są nieskończone. Spoiler: stopnie są skończone, a kolano jedno.
Kolejny przykład, gdzie długość pokazuje, że spacja też się liczy:
#include <iostream>
#include <string>
int main() {
std::string s = "Hi Bob";
std::cout << s.size() << '\n'; // 6 (H i _ B o b)
}
5. Indeksowanie: s[i]
Jak już ustaliliśmy — string to zestaw znaków. A skoro to zestaw, można pobierać pojedyncze znaki po ich numerze.
W programowaniu numeracja zwykle zaczyna się od zera. Numeracja od zera oznacza: pierwszy znak to s[0], drugi — s[1], i tak dalej.
Wyobraź sobie string jako pudełko z przegródkami:
flowchart LR
A["s = 'c a t'"] --> B["indeksy: 0 1 2"]
Spójrzmy na kod:
#include <iostream>
#include <string>
int main() {
std::string animal = "cat";
std::cout << animal[0] << '\n'; // c
std::cout << animal[1] << '\n'; // a
std::cout << animal[2] << '\n'; // t
}
O granicach
Aby się mniej mylić, w programowaniu wygodniej używać terminu indeks zamiast numer. Indeks zawsze zaczyna się od zera.
Indeks powinien zawierać się w przedziale od 0 do s.size() - 1. Jeśli string ma długość 3, dopuszczalne indeksy to 0, 1, 2. Jeśli spróbujesz pobrać animal[3], wychodzisz poza granice i program może się zawiesić.
W C++ jest to niebezpieczne, bo program może zachowywać się nieprzewidywalnie: czasem „jakby działa”, czasem wypisuje dziwne znaki, czasem pada. I to jest przypadek, gdy „czasem działa” jest gorsze niż „od razu pada”, bo błąd ukrywa się i czeka na odpowiedni moment.
Czy można zmieniać znaki w stringu?
Tak, znak o danym indeksie można zamienić: string jest mutowalny, co przydaje się do prostych modyfikacji.
#include <iostream>
#include <string>
int main() {
std::string word = "bat";
word[0] = 'c';
std::cout << word << '\n'; // cat
}
Zwróć uwagę: po prawej stoi 'c' w pojedynczych cudzysłowach, bo przypisujemy jeden znak, a nie string "c".
6. Pierwsze spotkanie z metodami
Mogłeś zauważyć dziwny zapis:
city.size()
Dlaczego nie piszemy po prostu size(city)? Bo size() to metoda.
Metoda to funkcja należąca do konkretnego typu lub obiektu. W naszym przypadku std::string to typ, a city — obiekt tego typu. Stringi mają swoje „wbudowane umiejętności”: poznawać długość, dopisywać tekst, pobierać znak po indeksie i wiele innych.
Gdy funkcja należy do obiektu, wywołujemy ją przez kropkę:
obiekt.metoda()
Kropka czyta się niemal po ludzku: „Weź obiekt city i wywołaj na nim metodę size()”.
Czyli string „wie sam”, jak obliczyć swoją długość. To jest idea metod: zachowanie związane z danymi.
Spójrz na znane już przykłady:
std::string full = first + " " + last;
std::cout << full.size() << '\n';
std::cout << full[0] << '\n';
full.size() — string sam podaje swoją długość. full[0] — odwołujemy się do jego wewnętrznego znaku.
Później zobaczysz, że prawie wszystkie złożone typy w C++ mają swoje metody. Na przykład std::vector ma push_back(), std::string ma substr(), strumienie mają clear(). To jeden z kluczowych zasad języka: dane i operacje na nich są powiązane.
Warto przyzwyczaić się do takiego zapisu. Wszystko, co wygląda jak:
coś.coś()
prawie zawsze oznacza wywołanie metody obiektu.
Dlatego piszemy full.size(), a nie po prostu size(full). String udostępnia narzędzia do pracy ze sobą.
7. Mini-aplikacja: konsolowa wizytówka
Teraz zbierzemy wszystko w jeden mały fragment: zrobimy wizytówkę, która wypisze imię, długość pełnego imienia i pierwszą literę. Na razie dane weźmiemy bezpośrednio z kodu (wejście pojawi się w kolejnym wykładzie), aby nie mieszać nowych tematów.
W tym przykładzie uzyskasz poczucie „aplikacji”: nie tylko pojedyncze operacje, ale mały użyteczny wypis.
#include <iostream>
#include <string>
int main() {
std::string first = "Ada";
std::string last = "Lovelace";
std::string full = first + " " + last;
std::cout << "Full name: " << full << '\n'; // Full name: Ada Lovelace
std::cout << "Length: " << full.size() << '\n'; // Length: 12
std::cout << "First letter: " << full[0] << '\n'; // First letter: A
}
Tu ukrytych jest kilka ważnych nawyków.
Po pierwsze, wyraźnie dodaliśmy spację między imieniem a nazwiskiem, bo komputer nie domyśli się, że „między słowami powinno być miejsce”. Po drugie, wypisaliśmy długość stringa: to pomaga zrozumieć, że spacja też się liczy. Po trzecie, pobraliśmy full[0] i wydobyliśmy pierwszy znak, co daje w przyszłości możliwość tworzenia inicjałów czy prostych skrótów.
Jeśli chcesz trochę „pobawić się”, możesz pobrać ostatnią literę nazwiska. Formalnie ostatnia litera ma indeks size() - 1. Robimy to tylko dla niepustego stringa, by nie ryzykować wyjścia poza granice:
#include <iostream>
#include <string>
int main() {
std::string last = "Lovelace";
char lastChar = last[last.size() - 1];
std::cout << lastChar << '\n'; // e
}
Tak, wyrażenie wygląda nieco „matematycznie”, ale logika prosta: jeśli długość to 8, indeksy idą 0…7, zatem ostatni to 7, czyli size() - 1.
Przypomnienie operacji na stringach
By nie trzymać wszystkiego w głowie naraz, warto mieć małą „mapę”. Poniżej krótka tabela z tym, czego dziś używaliśmy (bez zagłębiania się w bardziej zaawansowane metody stringów — będą później).
| Operacja | Przykład | Co się dzieje |
|---|---|---|
| Utworzyć string | |
W s przechowywany jest tekst |
| Połączyć stringi | |
Tworzony jest nowy string |
| Dopisać do stringa | |
s staje się dłuższy |
| Długość stringa | |
Otrzymujemy liczbę znaków |
| Znak po indeksie | |
Pobieramy jeden znak (albo możemy go zamienić) |
Ta tabela to Twój „minimalny zestaw”, przydatny w prawie każdym zadaniu konsolowym: od wypisywania powitania po wstępną obróbkę tekstu.
8. Typowe błędy przy pracy ze std::string
Błąd nr 1: zapomnieli #include <string>.
Czasem kod „jakby” się kompiluje przez pośrednie dołączenia, a czasem niespodziewanie przestaje. Dobre nawyki są proste: używasz std::string — dołącz <string>. To nie kaprys, to dyscyplina: nawet w przykładach i dokumentach zdarzają się poprawki, które dodają brakujący include, bo bez niego przykład jest niepoprawny.
Błąd nr 2: pomylili "A" i 'A'.
Gdy piszesz word[0] = "b";, kompilator słusznie powie: „hej, po lewej jeden znak, a po prawej string”. Dla jednego znaku użyj pojedynczych cudzysłowów: 'b'. Dla tekstu — podwójnych: "b".
Błąd nr 3: zapomnieli o spacji przy konkatenacji.
Klasyka: first + last daje „AdaLovelace”, a ty potem długo patrzysz i myślisz, że program się zepsuł. W rzeczywistości jest zbyt dosłowny: nie poprosiłeś o spację — nie dodał. Dodawaj " " explicite.
Błąd nr 4: indeks poza granicami stringa.
s[100] dla stringa "Hi" — to nie „pustka”, nie null i nie „nic”. To wyjście poza granice i zachowanie programu staje się nieprzewidywalne. Zanim pojawi się if, nie robiemy losowych indeksów i zawsze opieramy się na size().
Błąd nr 5: oczekiwano, że size() to „liczba liter w dowolnym języku”.
Na początku kursu pracujemy zwykle na prostych przykładach łacińskich, gdzie jeden znak to mniej więcej jedna „litera”. W prawdziwych tekstach (zwłaszcza z znakami wielobajtowymi w UTF‑8) sprawa może być trudniejsza, ale nasza obecna rola to opanowanie podstaw mechaniki stringów, nie rozwiązywanie wszystkich problemów kodowania znaków.
GO TO FULL VERSION