17.11 — Stałe symboliczne w stylu C w stylu C

W poprzedniej lekcji (17.10 -- Ciągi znaków w stylu C), omawialiśmy, jak tworzyć i inicjować obiekty łańcuchowe w stylu C:

#include <iostream>

int main()
{
    char name[]{ "Alex" }; // C-style string
    std::cout << name << '\n';

    return 0;
}

C++ obsługuje dwa różne sposoby tworzenia stałych symbolicznych ciągu w stylu C:

#include <iostream>

int main()
{
    const char name[] { "Alex" };        // case 1: const C-style string initialized with C-style string literal
    const char* const color{ "Orange" }; // case 2: const pointer to C-style string literal

    std::cout << name << ' ' << color << '\n';

    return 0;
}

Wypisuje:

Alex Orange

Podczas gdy powyższe dwie metody dają takie same wyniki, C++ radzi sobie z alokacją pamięci dla nich nieco inaczej.

W przypadku 1 „Alex” jest umieszczany gdzieś w pamięci (prawdopodobnie tylko do odczytu). Następnie program przydziela pamięć dla tablicy w stylu C o długości 5 (cztery jawne znaki plus terminator zerowy) i inicjuje tę pamięć ciągiem „Alex”. W rezultacie otrzymujemy 2 kopie „Alexa” – jedną gdzieś w globalnej pamięci, a drugą posiadaną przez name. Ponieważ name jest stałą (i nigdy nie będzie modyfikowana), tworzenie kopii jest nieefektywne.

W przypadku 2 sposób, w jaki kompilator sobie z tym radzi, jest zdefiniowany przez implementację. Zazwyczaj zdarza się, że kompilator umieszcza ciąg „Orange” gdzieś w pamięci tylko do odczytu, a następnie inicjuje wskaźnik adresem ciągu.

W celach optymalizacji wiele literałów łańcuchowych można połączyć w jedną wartość. Na przykład:

const char* name1{ "Alex" };
const char* name2{ "Alex" };

Są to dwa różne literały łańcuchowe o tej samej wartości. Ponieważ te literały są stałymi, kompilator może zdecydować się na oszczędzanie pamięci, łącząc je w jeden współdzielony literał łańcuchowy, przy czym oba name1 i name2 wskazują ten sam adres.

Dedukcja typu za pomocą stałych ciągów w stylu C

Dedukcja typu przy użyciu literału łańcuchowego w stylu C jest dość prosta:

    auto s1{ "Alex" };  // type deduced as const char*
    auto* s2{ "Alex" }; // type deduced as const char*
    auto& s3{ "Alex" }; // type deduced as const char(&)[5]

Wyprowadzanie wskaźników i stylu C strings

Być może zauważyłeś coś interesującego w sposobie std::cout obsługi wskaźników różnych typów.

Rozważ następujący przykład:

#include <iostream>

int main()
{
    int narr[]{ 9, 7, 5, 3, 1 };
    char carr[]{ "Hello!" };
    const char* ptr{ "Alex" };

    std::cout << narr << '\n'; // narr will decay to type int*
    std::cout << carr << '\n'; // carr will decay to type char*
    std::cout << ptr << '\n'; // name is already type char*

    return 0;
}

Na maszynie autora wydrukowało to:

003AF738
Hello!
Alex

Dlaczego tablica int wydrukowała adres, ale tablice znaków zostały wydrukowane jako ciągi znaków?

Odpowiedź jest taka, że strumienie wyjściowe (np. std::cout) przyjmują pewne założenia dotyczące twoich intencji. Jeśli przekażesz mu wskaźnik inny niż znak, po prostu wydrukuje zawartość tego wskaźnika (adres, który przechowuje wskaźnik). Jeśli jednak przekażesz mu obiekt typu char* lub const char*, zostanie przyjęte założenie, że zamierzasz wydrukować ciąg znaków. W rezultacie zamiast drukować wartość wskaźnika (adres), zamiast tego wydrukuje wskazywany ciąg znaków!

Chociaż jest to pożądane w większości przypadków, może prowadzić do nieoczekiwanych rezultatów. Rozważmy następujący przypadek:

#include <iostream>

int main()
{
    char c{ 'Q' };
    std::cout << &c;

    return 0;
}

W tym przypadku programista zamierza wydrukować adres zmiennej c. Jednak &c ma typ char*, więc std::cout próbuje wydrukować to jako ciąg znaków! A ponieważ c nie jest zakończony zerem, otrzymujemy niezdefiniowane zachowanie.

Na maszynie autora wydrukowało to:

Q╠╠╠╠╜╡4;¿■A

Dlaczego to zrobił? Cóż, najpierw założono, że &c (który ma typ char*) jest ciągiem w stylu C. Wypisał więc „Q” i kontynuował. Następna w pamięci była kupa śmieci. W końcu natrafił na jakąś pamięć zawierającą wartość 0 , którą zinterpretował jako terminator zerowy, więc zatrzymał się. To, co widzisz, może się różnić w zależności od tego, co znajduje się w pamięci po zmiennej c.

Ten przypadek jest raczej mało prawdopodobny w prawdziwym życiu (ponieważ jest mało prawdopodobne, że faktycznie chcesz drukować adresy pamięci), ale ilustruje to, jak wszystko działa pod maską i jak programy mogą przypadkowo zejść z torów.

Jeśli rzeczywiście chcesz wydrukować adres wskaźnika znaku, static_cast go do typ const void*:

#include <iostream>

int main()
{
    const char* ptr{ "Alex" };

    std::cout << ptr << '\n';                           // print ptr as C-style string
    std::cout << static_cast<const void*>(ptr) << '\n'; // print address held by ptr
    
    return 0;
}

Powiązana treść

Omawiamy void* w lekcji 19.5 -- Wskaźniki pustki. Nie musisz wiedzieć, jak to działa, aby go tutaj używać.

Preferuj std::string_view dla stałych symbolicznych ciągu w stylu C

Nie ma powodu, aby używać stałych symbolicznych ciągu w stylu C we współczesnym C++. Zamiast tego preferuj constexpr std::string_view obiekty, które są zwykle równie szybkie (jeśli nie szybsze) i zachowują się bardziej konsekwentnie.

Najlepsza praktyka

Unikaj stałych symbolicznych w stylu C na rzecz constexpr std::string_view.

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