17.10 — ciągi znaków w stylu C

W lekcji 17.7 -- Wprowadzenie do tablic w stylu C wprowadziliśmy tablice w stylu C, które pozwalają nam zdefiniować sekwencyjny zbiór elementów:

    int testScore[30] {}; // an array of 30 ints, indices 0 through 29

W lekcji 5.2 -- Literały zdefiniowaliśmy ciąg jako zbiór kolejnych znaków (takich jak „Witaj, świecie!”) i wprowadziliśmy literały łańcuchowe w stylu C. Zauważyliśmy również, że literał łańcuchowy w stylu C „Hello, world!” ma typ const char[14] (13 jawnych znaków plus 1 ukryty znak zakończenia zerowego).

Jeśli wcześniej nie połączyłeś kropek, powinno być teraz oczywiste, że ciągi znaków w stylu C są po prostu tablicami w stylu C, których typ elementu to char lub const char!

Chociaż literały ciągów w stylu C można używać w naszym kodzie, obiekty łańcuchowe w stylu C wypadły z łask we współczesnych czasach C++, ponieważ są trudne w użyciu i niebezpieczne ( std::string i std::string_view będą nowoczesnymi zamiennikami). Niezależnie od tego, w starszym kodzie nadal możesz natknąć się na zastosowania obiektów łańcuchowych w stylu C, a my nie omówiliśmy ich w ogóle.

Dlatego w tej lekcji przyjrzymy się najważniejszym punktom dotyczącym obiektów łańcuchowych w stylu C we współczesnym C++.

Definiowanie ciągów w stylu C

Aby zdefiniować zmienną łańcuchową w stylu C, po prostu zadeklaruj zmienną tablicową w stylu C of char (lub const char / constexpr char):

char str1[8]{};                    // an array of 8 char, indices 0 through 7

const char str2[]{ "string" };     // an array of 7 char, indices 0 through 6
constexpr char str3[] { "hello" }; // an array of 6 const char, indices 0 through 5

Pamiętaj, że potrzebujemy dodatkowego znaku dla niejawnego terminatora zerowego.

Definiując ciągi w stylu C za pomocą inicjatora, zdecydowanie zalecamy pominięcie długości tablicy i pozostawienie kompilatorowi obliczenia długości. Dzięki temu, jeśli inicjator zmieni się w przyszłości, nie będziesz musiał pamiętać o aktualizowaniu długości i nie ma ryzyka, że zapomnisz o dołączeniu dodatkowego elementu do przechowywania terminator zerowy.

Ciągi w stylu C będą zanikać

W lekcji 17.8 -- Zanikanie tablicy w stylu C omówiliśmy, jak tablice w stylu C będą w większości przypadków rozpadać się na wskaźnik. Ponieważ ciągi w stylu C są tablicami w stylu C, ulegną rozpadowi -- Litery ciągu w stylu C rozpadają się na a const char*, oraz Tablice ciągów w stylu C rozpadają się na a const char* lub char* w zależności od tego, czy tablica jest stała. A kiedy ciąg w stylu C rozpada się na wskaźnik, długość ciągu (zakodowana w informacjach o typie) zostaje utracona.

Ta informacja o utracie długości jest powodem, dla którego ciągi w stylu C mają terminator zerowy. Długość łańcucha może być (nieefektywnie) regenerowana przez zliczanie liczba elementów pomiędzy początkiem łańcucha a terminatorem zerowym.

Wyprowadzanie ciągu w stylu C

Podczas wysyłania ciągu w stylu C, std::cout wyprowadza znaki aż do napotkania terminatora zerowego. Ten terminator zerowy oznacza koniec ciągu, tak że zepsute ciągi (które utraciły informację o długości) nadal mogą być drukowane.

#include <iostream>

void print(char ptr[])
{
    std::cout << ptr << '\n'; // output string
}

int main()
{
    char str[]{ "string" };
    std::cout << str << '\n'; // outputs string

    print(str);

    return 0;
}

Jeśli spróbujesz. aby wydrukować ciąg znaków, który nie ma terminatora zerowego (np. ponieważ terminator zerowy został w jakiś sposób nadpisany), wynikiem będzie niezdefiniowane zachowanie. Najbardziej prawdopodobnym rezultatem w tym przypadku będzie wydrukowanie wszystkich znaków w ciągu, a następnie drukowanie będzie po prostu wszystkiego z sąsiednich komórek pamięci (interpretowanych jako znak), aż trafi się bajt pamięci zawierający 0 (co zostanie zinterpretowane jako terminator zerowy)!

Wprowadzanie Ciągi w stylu C

Rozważmy przypadek, w którym prosimy użytkownika o rzucenie kostką tyle razy, ile chce i wprowadzenie wyrzuconych liczb bez spacji (np. 524412616 Ile znaków wprowadzi użytkownik. Nie mamy pojęcia.

Ponieważ ciągi znaków w stylu C są tablicami o stałym rozmiarze, rozwiązaniem jest zadeklarowanie tablicy większej, niż kiedykolwiek będziemy potrzebować:

#include <iostream>

int main()
{
    char rolls[255] {}; // declare array large enough to hold 254 characters + null terminator
    std::cout << "Enter your rolls: ";
    std::cin >> rolls;
    std::cout << "You entered: " << rolls << '\n';

    return 0;
}

Przed C++20 std::cin >> rolls wyodrębnialiśmy jak najwięcej znaków do rolls (zatrzymując się na pierwszym niewiodącym białym znaku). Nic nie stoi na przeszkodzie, aby użytkownik wprowadził więcej niż 254 znaki (nieumyślnie lub złośliwie). Jeśli tak się stanie, dane wejściowe użytkownika przepełnią rolls tablicę, co spowoduje niezdefiniowane zachowanie.

Kluczowa informacja

Przepełnienie tablicy lub przepełnienie bufora to problem związany z bezpieczeństwem komputera, który występuje, gdy do pamięci masowej kopiuje się więcej danych, niż może ona pomieścić. W takich przypadkach pamięć znajdująca się tuż za pamięcią zostanie nadpisana, co prowadzi do niezdefiniowanego zachowania. Złośliwi aktorzy mogą potencjalnie wykorzystać takie wady do nadpisania zawartości pamięci, mając nadzieję na zmianę zachowania programu w jakiś korzystny sposób.

W C++20 operator>> zostało to zmienione tak, że działa tylko przy wprowadzaniu niezniszczonych ciągów w stylu C. Pozwala to operator>> wyodrębnić tylko tyle znaków, na ile pozwala długość łańcucha w stylu C, zapobiegając przepełnieniu. Ale oznacza to również, że nie można już używać operator>> do wprowadzania danych do zniszczonych ciągów w stylu C.

Zalecany sposób czytania ciągów w stylu C przy użyciu std::cin jest następujący:

#include <iostream>
#include <iterator> // for std::size

int main()
{
    char rolls[255] {}; // declare array large enough to hold 254 characters + null terminator
    std::cout << "Enter your rolls: ";
    std::cin.getline(rolls, std::size(rolls));
    std::cout << "You entered: " << rolls << '\n';

    return 0;
}

To wywołanie cin.getline() wczyta do rolls do 254 znaków (łącznie ze spacjami). Wszelkie nadmiarowe znaki zostaną odrzucone. Ponieważ getline() zajmuje dużo czasu, możemy podać maksymalną liczbę akceptowanych znaków. W przypadku tablicy, która nie uległa rozkładowi, jest to łatwe — możemy użyć std::size() , aby uzyskać długość tablicy. W przypadku tablicy z rozkładem musimy określić długość w inny sposób. A jeśli podamy niewłaściwą długość, nasz program może działać nieprawidłowo lub mieć problemy z bezpieczeństwem.

We współczesnym C++, podczas przechowywania tekstu wprowadzonego przez użytkownika, bezpieczniej jest używać std::string, ponieważ std::string dostosuje się automatycznie tak, aby pomieścić tyle znaków, ile potrzeba.

Modyfikowanie ciągów w stylu C

Warto zauważyć, że po ciągach w stylu C te same zasady, co tablice w stylu C. Oznacza to, że możesz zainicjować ciąg podczas tworzenia, ale nie możesz później przypisać do niego wartości za pomocą operatora przypisania!

char str[]{ "string" }; // ok
str = "rope";           // not ok!

To sprawia, że używanie ciągów w stylu C jest nieco niewygodne.

Ponieważ ciągi w stylu C są tablicami, możesz użyć operatora [], aby zmienić poszczególne znaki w ciągu:

#include <iostream>

int main()
{
    char str[]{ "string" };
    std::cout << str << '\n';
    str[1] = 'p';
    std::cout << str << '\n';

    return 0;
}

Ten program wypisuje:

string
spring

Uzyskiwanie długości łańcucha w stylu C string

Ponieważ ciągi w stylu C są tablicami w stylu C, możesz użyć std::size() (lub w C++20, std::ssize()), aby uzyskać długość łańcucha jako tablicę. Są tu dwa zastrzeżenia:

  1. To nie działa na uszkodzonych ciągach.
  2. Zwraca rzeczywistą długość tablicy w stylu C, a nie długość łańcucha.
#include <iostream>

int main()
{
    char str[255]{ "string" }; // 6 characters + null terminator
    std::cout << "length = " << std::size(str) << '\n'; // prints length = 255

    char *ptr { str };
    std::cout << "length = " << std::size(ptr) << '\n'; // compile error

    return 0;
}

Alternatywne rozwiązanie polega na użyciu funkcji strlen() , która znajduje się w nagłówku <cstring> . strlen() będzie działać na uszkodzonych tablicach, i zwraca długość przechowywanego łańcucha, z wyłączeniem terminatora zerowego:

#include <cstring> // for std::strlen
#include <iostream>

int main()
{
    char str[255]{ "string" }; // 6 characters + null terminator
    std::cout << "length = " << std::strlen(str) << '\n'; // prints length = 6

    char *ptr { str };
    std::cout << "length = " << std::strlen(ptr) << '\n';   // prints length = 6

    return 0;
}

Jednak std::strlen() jest powolny, ponieważ musi przejść przez całą tablicę, zliczając znaki, aż trafi na terminator zerowy.

Inne funkcje manipulacji ciągami w stylu C

Ponieważ ciągi w stylu C są podstawowym typem ciągów w C, język C zapewnia wiele funkcji dla manipulowanie ciągami znaków w stylu C. Funkcje te zostały odziedziczone przez C++ jako część nagłówka <cstring> .

Oto kilka najbardziej przydatnych funkcji, które możesz zobaczyć w starszym kodzie:

  • strlen() -- zwraca długość łańcucha w stylu C
  • strcpy(), strncpy(), strcpy_s() -- zastępuje jeden ciąg w stylu C inny
  • strcat(), strncat() -- Dołącza jeden ciąg w stylu C na koniec innego
  • strcmp(), strncmp() -- Porównuje dwa ciągi w stylu C (zwraca 0 jeśli jest równy)

Z wyjątkiem strlen(), generalnie zalecamy ich unikanie.

Unikaj niestałych obiektów łańcuchowych w stylu C

Jeśli nie masz konkretnego, ważnego powodu, aby używać niestałych ciągów w stylu C, najlepiej ich unikać, ponieważ są niewygodne w obsłudze i są podatne na przekroczenia, co powoduje niezdefiniowane zachowanie (i stanowią potencjalne bezpieczeństwo

W rzadkich przypadkach, gdy musisz pracować z ciągami w stylu C lub stałymi rozmiarami buforów (np. w przypadku urządzeń o ograniczonej pamięci), zalecamy użycie dobrze przetestowanej biblioteki ciągów o stałej długości innej firmy, zaprojektowanej do tego celu.

Najlepsza praktyka

Unikaj niestałych obiektów łańcuchowych w stylu C na korzyść std::string.

Czas quizu

Pytanie nr 1

Napisz funkcję, która drukuje ciąg znaków w stylu C znak po znaku. Użyj wskaźnika i arytmetyki wskaźników, aby przejść przez każdy znak ciągu i wydrukować ten znak. Napisz main funkcję testującą funkcję za pomocą literału łańcuchowego „Hello, world!”.

Pokaż rozwiązanie

Pytanie nr 2

Powtórz quiz nr 1, ale tym razem funkcja powinna wyświetlić ciąg znaków od tyłu.

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:  
406 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze