5.7 — Wprowadzenie do std::string

W lekcji 5.2 -- Literały, wprowadziliśmy literały łańcuchowe w stylu C:

#include <iostream>
 
int main()
{
    std::cout << "Hello, world!"; // "Hello world!" is a C-style string literal.
    return 0;
}

O ile literały łańcuchowe w stylu C są w porządku, zmienne łańcuchowe w stylu C zachowują się dziwnie i są trudne w obsłudze (np. nie można ich używać przypisanie w celu przypisania zmiennej łańcuchowej w stylu C nowej wartości) i są niebezpieczne (np. jeśli skopiujesz większy ciąg w stylu C w miejsce przeznaczone na krótszy ciąg w stylu C, spowoduje to niezdefiniowane zachowanie). We współczesnym C++ najlepiej unikać zmiennych łańcuchowych w stylu C.

Na szczęście w C++ wprowadzono do języka dwa dodatkowe typy ciągów, z którymi praca jest znacznie łatwiejsza i bezpieczniejsza: std::string i std::string_view (C++17). W przeciwieństwie do typów, które przedstawiliśmy wcześniej, std::string i std::string_view nie są typami podstawowymi (są typami klasowymi, które omówimy w przyszłości). Jednak podstawowe użycie każdego z nich jest na tyle proste i przydatne, że je tutaj przedstawimy.

Wprowadzenie std::string

Najprostszym sposobem pracy z łańcuchami i obiektami łańcuchowymi w C++ jest użycie typu std::string , który znajduje się w nagłówku <string>.

Możemy tworzyć obiekty typu std::string tak jak inne obiekty:

#include <string> // allows use of std::string

int main()
{
    std::string name {}; // empty string

    return 0;
}

Tylko podobnie jak zwykłe zmienne, możesz inicjalizować lub przypisywać wartości do obiektów std::string, zgodnie z oczekiwaniami:

#include <string>

int main()
{
    std::string name { "Alex" }; // initialize name with string literal "Alex"
    name = "John";               // change name to "John"

    return 0;
}

Zauważ, że ciągi mogą składać się również ze znaków numerycznych:

std::string myID{ "45" }; // "45" is not the same as integer 45!

W formie ciągu liczb są traktowane jak tekst, a nie jak liczby i dlatego nie można nimi manipulować jak liczbami (np. nie można ich pomnożyć). C++ nie będzie automatycznie konwertował ciągów znaków na wartości całkowite lub zmiennoprzecinkowe i odwrotnie (chociaż można to zrobić, omówimy to w przyszłej lekcji).

Wyjście ciągu znaków z std::cout

std::string obiektami może zostać wyprowadzone zgodnie z oczekiwaniami przy użyciu std::cout:

#include <iostream>
#include <string>

int main()
{
    std::string name { "Alex" };
    std::cout << "My name is: " << name << '\n';

    return 0;
}

Wypisuje:

My name is: Alex

Puste ciągi nic nie wypiszą:

#include <iostream>
#include <string>

int main()
{
    std::string empty{ };
    std::cout << '[' << empty << ']';

    return 0;
}

Które wypisuje:

[]

std::string obsługuje ciągi znaków różne długości

Jedną z najfajniejszych rzeczy, które std::string mogą zrobić, to przechowywać ciągi o różnej długości:

#include <iostream>
#include <string>

int main()
{
    std::string name { "Alex" }; // initialize name with string literal "Alex"
    std::cout << name << '\n';

    name = "Jason";              // change name to a longer string
    std::cout << name << '\n';

    name = "Jay";                // change name to a shorter string
    std::cout << name << '\n';

    return 0;
}

Wypisuje:

Alex
Jason
Jay

W powyższym przykładzie name jest inicjowany ciągiem "Alex", który zawiera pięć znaków (cztery znaki jawne i terminator zerowy). Następnie ustawiamy name na większy ciąg, a następnie na mniejszy. std::string nie ma problemu z obsługą tego! W std::string.

można przechowywać naprawdę długie ciągi znaków.Jest to jeden z powodów, dla których std::string jest tak potężny.

Kluczowa informacja

Jeśli std::string nie ma wystarczającej ilości pamięci do przechowywania ciągu, zażąda dodatkowej pamięci (w czasie wykonywania) przy użyciu formy alokacji pamięci znanej jako dynamiczna alokacja pamięci. Ta możliwość pozyskiwania dodatkowej pamięci jest częścią tego, co sprawia, że std::string jest tak elastyczny, ale także stosunkowo powolny.

Dynamiczną alokację pamięci omówimy w przyszłym rozdziale.

Wprowadzanie ciągu za pomocą std::cin

Użycie std::string z std::cin może przynieść pewne niespodzianki! Rozważmy następujący przykład:

#include <iostream>
#include <string>

int main()
{
    std::cout << "Enter your full name: ";
    std::string name{};
    std::cin >> name; // this won't work as expected since std::cin breaks on whitespace

    std::cout << "Enter your favorite color: ";
    std::string color{};
    std::cin >> color;

    std::cout << "Your name is " << name << " and your favorite color is " << color << '\n';

    return 0;
}

Oto wyniki przykładowego uruchomienia tego programu:

Enter your full name: John Doe
Enter your favorite color: Your name is John and your favorite color is Doe

Hmmm, to nieprawda! Co się stało? Okazuje się, że użycie operator>> do wyodrębnienia ciągu z std::cin, operator>> zwróci tylko znaki do pierwszego napotkanego odstępu. Wszelkie inne znaki pozostają wewnątrz std::cin i czekają na następną ekstrakcję.

Więc kiedy użyliśmy operator>> do wyodrębnienia danych wejściowych do zmiennej name, wyodrębniono tylko "John" , pozostawiając " Doe" wewnątrz std::cin. Kiedy następnie użyliśmy operator>> aby wyodrębnić dane wejściowe do zmiennej color, wyodrębniono "Doe" zamiast czekać, aż wprowadzimy kolor. Następnie program się kończy.

Użyj std::getline() aby wprowadzić tekst

Aby wczytać całą linię danych wejściowych do ciągu znaków, lepiej zamiast tego użyć funkcji std::getline() . std::getline() wymaga dwóch argumentów: pierwszy to std::cin, a drugi to zmienna łańcuchowa.

Oto ten sam program co powyżej używając std::getline():

#include <iostream>
#include <string> // For std::string and std::getline

int main()
{
    std::cout << "Enter your full name: ";
    std::string name{};
    std::getline(std::cin >> std::ws, name); // read a full line of text into name

    std::cout << "Enter your favorite color: ";
    std::string color{};
    std::getline(std::cin >> std::ws, color); // read a full line of text into color

    std::cout << "Your name is " << name << " and your favorite color is " << color << '\n';

    return 0;
}

Teraz nasz program działa zgodnie z oczekiwaniami:

Enter your full name: John Doe
Enter your favorite color: blue
Your name is John Doe and your favorite color is blue

Co to do cholery jest std::ws?

W lekcji 4.8 — Liczby zmiennoprzecinkowe, omówiliśmy manipulatory wyjściowe, które pozwalają nam zmieniać sposób wyświetlania danych wyjściowych. Podczas tej lekcji użyliśmy funkcji manipulatora wyjściowego std::setprecision() w celu zmiany liczby wyświetlanych std::cout cyfr dokładności.

C++ obsługuje również manipulatory wejściowe, które zmieniają sposób akceptowania danych wejściowych. Manipulator std::ws input mówi std::cin aby zignorować wszelkie wiodące białe znaki przed ekstrakcją. Wiodące białe znaki to dowolny biały znak (spacje, tabulatory, znaki nowej linii) występujący na początku łańcucha.

Przyjrzyjmy się, dlaczego jest to przydatne. Rozważmy następujący program:

#include <iostream>
#include <string>

int main()
{
    std::cout << "Pick 1 or 2: ";
    int choice{};
    std::cin >> choice;

    std::cout << "Now enter your name: ";
    std::string name{};
    std::getline(std::cin, name); // note: no std::ws here

    std::cout << "Hello, " << name << ", you picked " << choice << '\n';

    return 0;
}

Oto wynik działania tego programu:

Pick 1 or 2: 2
Now enter your name: Hello, , you picked 2

Ten program najpierw prosi o wprowadzenie 1 lub 2 i czeka, aż to zrobisz. Jak dotąd wszystko dobrze. Następnie poprosi Cię o podanie swojego imienia i nazwiska. Jednak tak naprawdę nie będzie czekać, aż wpiszesz swoje imię i nazwisko! Zamiast tego wypisuje ciąg „Hello”, a następnie kończy działanie.

Gdy wprowadzisz wartość za pomocą operator>>, std::cin , nie tylko przechwytuje wartość, ale także przechwytuje znak nowego wiersza ('\n'), który pojawia się po naciśnięciu klawisza Enter. Zatem kiedy wpiszemy 2 i naciśniemy Enter, std::cin przechwytuje ciąg znaków "2\n" jako dane wejściowe. Następnie wyodrębnia wartość 2 do zmiennej choice, pozostawiając znak nowego wiersza na później. Następnie, gdy std::getline() idzie wyodrębnić tekst do name, widzi, że "\n" już czeka w std::cin i okazuje się, że musieliśmy wcześniej wprowadzić pusty ciąg znaków! Zdecydowanie nie to było zamierzone.

Możemy zmodyfikować powyższy program tak, aby używał std::ws manipulatora wejściowego, który powie std::getline() aby ignorować wszelkie początkowe białe znaki:

#include <iostream>
#include <string>

int main()
{
    std::cout << "Pick 1 or 2: ";
    int choice{};
    std::cin >> choice;

    std::cout << "Now enter your name: ";
    std::string name{};
    std::getline(std::cin >> std::ws, name); // note: added std::ws here

    std::cout << "Hello, " << name << ", you picked " << choice << '\n';

    return 0;
}

Teraz ten program będzie działał zgodnie z przeznaczeniem.

Pick 1 or 2: 2
Now enter your name: Alex
Hello, Alex, you picked 2

Najlepsza praktyka

Jeśli używasz std::getline() do odczytu ciągów znaków, użyj std::cin >> std::ws manipulatora wejściowego aby zignorować wiodące białe znaki. Należy to zrobić dla każdego std::getline() wywołania, ponieważ std::ws nie jest zachowywane w przypadku wywołań.

Kluczowa informacja

Podczas wyodrębniania do zmiennej operator wyodrębniania (>>) ignoruje początkowe białe znaki. Zatrzymuje wyodrębnianie w przypadku napotkania białych znaków innych niż wiodące.

std::getline() nie ignoruje wiodących białych znaków. Jeśli chcesz, aby ignorował początkowe spacje, jako pierwszy argument podaj std::cin >> std::ws . Przestaje wyodrębniać po napotkaniu nowej linii.

Długość std::string

Jeśli chcemy wiedzieć, ile znaków zawiera std::string, możemy zapytać std::string obiekt o jego długość. Składnia tej operacji jest inna niż widziałeś wcześniej, ale jest całkiem prosta:

#include <iostream>
#include <string>

int main()
{
    std::string name{ "Alex" };
    std::cout << name << " has " << name.length() << " characters\n";

    return 0;
}

Wypisuje:

Alex has 4 characters

Chociaż std::string wymagane jest, aby łańcuch był zakończony znakiem null (od C++ 11), zwracana długość a std::string nie zawiera ukrytego znaku terminatora zerowego.

Zauważ, że zamiast pytać o długość łańcucha jako length(name), mówimy name.length(). Słowo kluczowe length() funkcja nie jest normalną funkcją samodzielną — jest to specjalny typ funkcji zagnieżdżony w std::string zwany funkcją składową. Ponieważ length() funkcja składowa jest zadeklarowana wewnątrz std::string, czasami jest zapisywana jako std::string::length() w dokumentacji.

Funkcje składowe, w tym sposób pisania własnych, omówimy bardziej szczegółowo później.

Kluczowa informacja

W przypadku normalnych funkcji wywołujemy function(object). W przypadku funkcji składowych wywołujemy object.function().

Pamiętaj również, że std::string::length() zwraca wartość całkowita bez znaku (najprawdopodobniej typu size_t). Jeśli chcesz przypisać długość do zmiennej int zmiennej, powinieneś static_cast uniknąć ostrzeżeń kompilatora o konwersjach ze znakiem/bez znaku:

int length { static_cast<int>(name.length()) };

Dla zaawansowanych czytelników

W C++20 możesz także użyć funkcji std::ssize() , aby uzyskać długość std::string jako duży typ całkowity ze znakiem (zwykle std::ptrdiff_t):

#include <iostream>
#include <string>

int main()
{
    std::string name{ "Alex" };
    std::cout << name << " has " << std::ssize(name) << " characters\n";

    return 0;
}

Ponieważ a ptrdiff_t może być większy niż int, jeśli chcesz zapisać wynik std::ssize() w int zmiennej, powinieneś static_cast wynik int:

int len { static_cast<int>(std::ssize(name)) };

Inicjowanie std::string jest kosztowne

Za każdym razem, gdy std::string jest inicjowany, tworzona jest kopia łańcucha użytego do jego inicjalizacji. Wykonywanie kopii ciągów jest kosztowne, dlatego należy zachować ostrożność, aby zminimalizować liczbę wykonanych kopii.

Nie przekazuj std::string według wartości

Kiedy std::string jest przekazywany do funkcji przez wartość, należy utworzyć instancję parametru funkcji std::string i zainicjować go argumentem. Powoduje to kosztowną kopię. Omówimy, co zamiast tego zrobić (użyj std::string_view) w lekcja 5.8 — Wprowadzenie do std::string_view.

Najlepsza praktyka

Nie przekazuj std::string przez wartość, ponieważ powoduje to kosztowną kopię.

Wskazówka

W większości przypadków zamiast tego użyj parametru std::string_view (opisanego w lekcji 5.8 — Wprowadzenie do std::string_view).

Zwrócenie std::string

Gdy funkcja zwraca wartość do osoby wywołującej, zwracana wartość jest zwykle kopiowana z funkcji z powrotem do osoby wywołującej. Możesz się więc spodziewać, że nie powinieneś tego robić return std::string według wartości, ponieważ spowodowałoby to zwrócenie kosztownej kopii std::string.

Jednak zgodnie z ogólną zasadą można zwrócić std::string według wartości, gdy wyrażenie instrukcji return ma postać dowolnego z poniższych:

  • Zmienna lokalna typu std::string.
  • A std::string , która została zwrócona przez wartość z innego wywołania funkcji lub operatora.
  • A std::string tymczasowa tworzony jako część instrukcji return.

Dla zaawansowanych czytelników

std::string obsługuje funkcję zwaną semantyką przenoszenia, która pozwala zamiast tego zwrócić obiekt, który zostanie zniszczony na końcu funkcji, poprzez wartość bez tworzenia kopii. Sposób działania semantyki przenoszenia wykracza poza zakres tego wprowadzającego artykułu, ale przedstawimy go w lekcji 16.5 — Zwrócenie std::vector i wprowadzenie do semantyki przenoszenia.

W większości innych przypadków wolimy unikać zwracania std::string według wartości, ponieważ będzie to kosztowne copy.

Wskazówka

Jeśli zwracasz literał łańcuchowy w stylu C, użyj zamiast tego std::string_view typu zwracanego (omówionego w lekcji 5.9 — std::string_view (część 2)).

Dla zaawansowanych czytelników

W niektórych przypadkach std::string może zostać zwrócony również przez (const) odwołanie, co jest kolejnym sposobem uniknięcia tworzenia kopii. Omówimy to w dalszej części lekcje 12.12 -- Powrót przez referencję i powrót przez adres i 14.6 — Funkcje dostępu.

Literały dla std::string

Literały łańcuchowe w cudzysłowie (np. „Witaj, świecie!”) są domyślnie ciągami w stylu C (a zatem mają dziwny typ).

Możemy tworzyć literały łańcuchowe z typem std::string używając a s sufiks po literale ciągu w cudzysłowie s musi być pisany małymi literami.

#include <iostream>
#include <string> // for std::string

int main()
{
    using namespace std::string_literals; // easy access to the s suffix

    std::cout << "foo\n";   // no suffix is a C-style string literal
    std::cout << "goo\n"s;  // s suffix is a std::string literal

    return 0;
}

Wskazówka

Sufiks „s” znajduje się w przestrzeni nazw std::literals::string_literals.

Najbardziej zwięzły sposób dostępu do sufiksów literału polega na użyciu dyrektywy using using namespace std::literals Jednakże powoduje to import uniknąć standardowe literały biblioteczne w zakres dyrektywy using, która wprowadza mnóstwo rzeczy, których prawdopodobnie nie będziesz używać.

Polecamy program using namespace std::string_literals, która importuje tylko literały dla std::string.

Dyrektywy using omawiamy na lekcji >7.13 -- Używanie deklaracji i dyrektyw. Jest to jeden z wyjątkowych przypadków, gdzie using cała przestrzeń nazw jest ogólnie w porządku, ponieważ jest mało prawdopodobne, aby zdefiniowane w niej przyrostki kolidowały z żadnymi. swojego kodu. Unikaj takich dyrektyw using poza funkcjami w plikach nagłówkowych.

Prawdopodobnie nie będziesz musiał zbyt często używać std::string literałów (ponieważ można zainicjować std::string obiekt literałem łańcuchowym w stylu C), ale w przyszłych lekcjach (obejmujących dedukcję typów) zobaczymy kilka przypadków, w których zamiast literałów std::string Literały łańcuchowe w stylu C ułatwiają sprawę (patrz 10.8 -- Dedukcja typu dla obiektów korzystających z auto słowo kluczowe przykład).

Dla zaawansowanych czytelników

"Hello"s rozwiązuje się do std::string { "Hello", 5 } która tworzy tymczasowy std::string zainicjowany literałem łańcuchowym w stylu C „Hello” (który ma długość 5, z wyłączeniem niejawnego zakończenia zerowego).

Ciągi Constexpr

Jeśli spróbujesz zdefiniować a constexpr std::string, Twój kompilator prawdopodobnie wygeneruje błąd:

#include <iostream>
#include <string>

int main()
{
    using namespace std::string_literals;

    constexpr std::string name{ "Alex"s }; // compile error

    std::cout << "My name is: " << name;

    return 0;
}

Dzieje się tak, ponieważ constexpr std::string nie jest w ogóle obsługiwany w C++17 lub wcześniejszym i działa tylko w bardzo ograniczonych przypadkach w C++20/23. Jeśli potrzebujesz ciągów constexpr, użyj zamiast tego std::string_view (omówione to w lekcji 5.8 — Wprowadzenie do std::string_view).

Wnioski

std::string jest złożone i wykorzystuje wiele funkcji językowych, które. jeszcze tego nie omówiliśmy. Na szczęście nie musisz rozumieć tych zawiłości, aby używać std::string do prostych zadań, takich jak podstawowe wprowadzanie i wyprowadzanie ciągów. Zachęcamy Cię do rozpoczęcia eksperymentów z ciągami znaków już teraz, a dodatkowe możliwości ciągów omówimy później.

Czas quizu

Pytanie nr 1

Napisz program, który poprosi użytkownika o podanie pełnego imienia i nazwiska oraz wieku. Jako wynik podaj użytkownikowi sumę jego wieku i liczbę znaków w jego imieniu (użyj funkcji członkowskiej std::string::length() , aby uzyskać długość ciągu). Dla uproszczenia wszelkie spacje w nazwie należy traktować jako znak.

Przykładowe wyjście:

Enter your full name: John Doe
Enter your age: 32
Your age + length of name is: 40

Przypomnienie: Należy uważać, aby nie wymieszać wartości ze znakiem i bez znaku. std::string::length() zwraca wartość bez znaku. Jeśli znasz C++ 20, użyj std::ssize() , aby uzyskać długość jako wartość ze znakiem. W przeciwnym razie static_rzuć wartość zwracaną std::string::length() na liczbę typu int.

Pokaż rozwiązanie

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
1.3K Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze