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.

