W C można użyć literału łańcuchowego w deklaracji takiej jak ta:
char s[] = "hello";
lub tak:
char *s = "hello";
Jaka jest różnica? Chcę wiedzieć, co faktycznie dzieje się pod względem czasu przechowywania, zarówno podczas kompilacji, jak i wykonywania.
Odpowiedzi:
Różnica polega na tym, że
umieści
"Hello world"
w częściach tylko do odczytu , a utworzenies
wskaźnika spowoduje, że wszelkie operacje zapisu w tej pamięci będą nielegalne.Podczas wykonywania:
umieszcza dosłowny ciąg w pamięci tylko do odczytu i kopiuje ciąg do nowo przydzielonej pamięci na stosie. W ten sposób czyniąc
prawny.
źródło
"Hello world"
znajduje się w „częściach pamięci tylko do odczytu” w obu przykładach. Przykład z tablicą wskazuje , przykład z tablicą kopiuje znaki do elementów tablicy.char msg[] = "hello, world!";
łańcuch, który kończy się w zainicjowanej sekcji danych. Po zadeklarowaniu,char * const
że skończą w sekcji danych tylko do odczytu. gcc-4.5.3Po pierwsze, w argumentach funkcji są one dokładnie równoważne:
W innych kontekstach
char *
przydziela wskaźnik, achar []
przydziela tablicę. Gdzie idzie sznur w pierwszym przypadku, pytasz? Kompilator potajemnie przydziela statyczną anonimową tablicę do przechowywania literału łańcucha. Więc:Zauważ, że nigdy nie wolno próbować modyfikować zawartości tej anonimowej tablicy za pomocą tego wskaźnika; efekty są niezdefiniowane (często oznacza awarię):
Zastosowanie składni tablicowej powoduje bezpośrednie przydzielenie jej do nowej pamięci. Dlatego modyfikacja jest bezpieczna:
Jednak tablica żyje tylko tak długo, jak jej zakres, więc jeśli zrobisz to w funkcji, nie zwracaj ani nie wyciekaj wskaźnika do tej tablicy - zamiast tego wykonaj kopię za pomocą
strdup()
lub podobną. Jeśli tablica jest przydzielona w zasięgu globalnym, oczywiście nie ma problemu.źródło
Ta deklaracja:
Tworzy jeden obiekt -
char
tablicę o rozmiarze 6, wywoływanąs
, inicjalizowaną wartościami'h', 'e', 'l', 'l', 'o', '\0'
. To, gdzie tablica jest przydzielona w pamięci i jak długo trwa, zależy od tego, gdzie pojawia się deklaracja. Jeśli deklaracja znajduje się w funkcji, będzie istnieć do końca bloku, w którym została zadeklarowana, i prawie na pewno zostanie przydzielona na stos; jeśli znajduje się poza funkcją, prawdopodobnie zostanie zapisany w „zainicjowanym segmencie danych”, który jest ładowany z pliku wykonywalnego do pamięci do zapisu, gdy program jest uruchomiony.Z drugiej strony ta deklaracja:
Tworzy dwa obiekty:
char
zawierająca wartości'h', 'e', 'l', 'l', 'o', '\0'
, która nie ma nazwy i ma statyczny czas przechowywania (co oznacza, że żyje przez całe życie programu); is
która jest inicjowana lokalizacją pierwszego znaku w tej nienazwanej tablicy tylko do odczytu.Nienazwana tablica tylko do odczytu zazwyczaj znajduje się w segmencie „tekstowym” programu, co oznacza, że jest ładowana z dysku do pamięci tylko do odczytu wraz z samym kodem. Lokalizacja
s
zmiennej wskaźnika w pamięci zależy od tego, gdzie pojawia się deklaracja (tak jak w pierwszym przykładzie).źródło
char s[] = "hello"
przypadku"hello"
jest to tylko inicjator informujący kompilator o sposobie inicjalizacji tablicy. Może, ale nies
musi, powodować powstanie odpowiedniego ciągu w segmencie tekstowym - na przykład, jeśli ma statyczny czas przechowywania, wówczas prawdopodobne jest, że jedynym wystąpieniem"hello"
będzie w zainicjowanym segmencie danych -s
sam obiekt . Nawet jeślis
ma automatyczny czas przechowywania, może być zainicjowany przez sekwencję dosłownych magazynów, a nie przez kopię (np.movl $1819043176, -6(%ebp); movw $111, -2(%ebp)
)..rodata
, w którym skrypt linkera zrzuca ten sam segment co.text
. Zobacz moją odpowiedź .char s[] = "Hello world";
dosłowny ciąg w pamięci tylko do odczytu i kopiuje ciąg do nowo przydzielonej pamięci na stosie. Ale twoja odpowiedź mówi tylko o dosłownym put ciągu w pamięci tylko do odczytu i przeskakuje na drugą część zdania, które mówi:copies the string to newly allocated memory on the stack
. Czy twoja odpowiedź jest niepełna za nieokreślenie drugiej części?char s[] = "Hellow world";
jest tylko inicjatorem i niekoniecznie jest przechowywany jako osobna kopia tylko do odczytu. Jeślis
ma statyczny czas przechowywania, wówczas jedyna kopia ciągu prawdopodobnie znajduje się w segmencie odczytu i zapisu w miejscus
, a nawet jeśli nie, to kompilator może zdecydować się na zainicjowanie tablicy za pomocą instrukcji natychmiastowego ładowania lub podobnych zamiast kopiowania z ciągu tylko do odczytu. Chodzi o to, że w tym przypadku sam łańcuch inicjujący nie jest obecny w środowisku wykonawczym.Biorąc pod uwagę deklaracje
przyjmij następującą hipotetyczną mapę pamięci:
Dosłowny ciąg znaków
"hello world"
to 12-elementowa tablicachar
(const char
w języku C ++) ze statycznym czasem przechowywania, co oznacza, że pamięć jest przydzielana podczas uruchamiania programu i pozostaje przydzielona do czasu zakończenia programu. Próba modyfikacji zawartości literału łańcuchowego wywołuje niezdefiniowane zachowanie.Linia
definiuje
s0
jako wskaźnikchar
z automatycznym czasem przechowywania (co oznacza, że zmiennas0
istnieje tylko dla zakresu, w którym została zadeklarowana) i kopiuje do niej adres literału łańcucha (0x00008000
w tym przykładzie). Należy zauważyć, że ponieważs0
wskazuje na ciągiem znaków, nie powinno być wykorzystywane jako argument do dowolnej funkcji, które starają się je modyfikować (npstrtok()
,strcat()
,strcpy()
itd.)Linia
definiuje
s1
jako 12-elementową tablicęchar
(długość jest pobierana z literału łańcucha) z automatycznym czasem przechowywania i kopiuje zawartość literału do tablicy. Jak widać z mapy pamięci, mamy dwie kopie ciągu"hello world"
; Różnica polega na tym, że możesz zmodyfikować ciąg znaków zawarty ws1
.s0
is1
są wymienne w większości kontekstów; oto wyjątki:Możesz ponownie przypisać zmienną,
s0
aby wskazywała inny literał łańcuchowy lub inną zmienną. Nie można ponownie przypisać zmiennej,s1
aby wskazywała inną tablicę.źródło
Projekt C99 N1256
Istnieją dwa różne zastosowania literałów ciągów znaków:
Zainicjuj
char[]
:Jest to „więcej magii” i opisane w 6.7.8 / 14 „Inicjalizacja”:
To tylko skrót do:
Jak każda inna zwykła tablica,
c
może być modyfikowana.Wszędzie indziej: generuje:
Więc kiedy piszesz:
Jest to podobne do:
Zwróć uwagę na ukrytą rzutowanie z
char[]
nachar *
, co jest zawsze legalne.Następnie, jeśli zmodyfikujesz
c[0]
, zmodyfikujesz również__unnamed
, czyli UB.Jest to udokumentowane w 6.4.5 „Literały łańcuchowe”:
6.7.8 / 32 „Inicjalizacja” daje bezpośredni przykład:
Implementacja GCC 4.8 x86-64 ELF
Program:
Kompiluj i dekompiluj:
Dane wyjściowe zawierają:
Wniosek: GCC przechowuje
char*
go w.rodata
sekcji, a nie w.text
.Zauważ jednak, że domyślny skrypt linkera umieszcza
.rodata
i.text
w tym samym segmencie , który wykonał, ale nie ma uprawnień do zapisu. Można to zaobserwować przy:który zawiera:
Jeśli zrobimy to samo dla
char[]
:otrzymujemy:
więc zostaje zapisany na stosie (względem
%rbp
).źródło
deklaruje,
s
że jest tablicą,char
która jest wystarczająco długa, aby pomieścić inicjator (5 + 1char
s) i inicjuje tablicę, kopiując elementy danego literału łańcucha do tablicy.deklaruje,
s
że jest wskaźnikiem do jednego lub więcej (w tym przypadku więcej)char
s i wskazuje go bezpośrednio na stałą (tylko do odczytu) lokalizację zawierającą literał"hello"
.źródło
s
jest wskaźnikiem doconst char
.Oto
s
tablica znaków, które można zastąpić, jeśli chcemy.Literał łańcuchowy służy do tworzenia bloków znaków gdzieś w pamięci, na którą
s
wskazuje ten wskaźnik . Możemy tutaj ponownie przypisać obiekt, na który wskazuje, zmieniając go, ale dopóki wskazuje on dosłowny ciąg znaków, blok znaków, na który wskazuje, nie może zostać zmieniony.źródło
Dodatkowo należy wziąć pod uwagę, że ponieważ do celów tylko do odczytu użycie obu jest identyczne, można uzyskać dostęp do znaku, indeksując za pomocą
[]
lub*(<var> + <index>)
format:I:
Oczywiście, jeśli spróbujesz
Prawdopodobnie wystąpi błąd segmentacji, gdy próbujesz uzyskać dostęp do pamięci tylko do odczytu.
źródło
x[1] = 'a';
co spowoduje awarię (oczywiście w zależności od platformy).Wystarczy dodać: otrzymujesz różne wartości dla ich rozmiarów.
Jak wspomniano powyżej, tablica
'\0'
zostanie przydzielona jako ostatni element.źródło
Powyższe ustawienia str wskazują na literalną wartość „Hello”, która jest zakodowana na stałe w obrazie binarnym programu, który jest oznaczony w pamięci jako „tylko do odczytu”, co oznacza, że jakakolwiek zmiana w tym dosłownym łańcuchu znaków jest nielegalna i spowodowałaby błędy segmentacji.
kopiuje ciąg do nowo przydzielonej pamięci na stosie. W związku z tym dokonywanie jakichkolwiek zmian jest dozwolone i legalne.
zmieni str na „Mello”.
Aby uzyskać więcej informacji, przejdź przez podobne pytanie:
Dlaczego dostaję błąd segmentacji podczas zapisu do łańcucha zainicjowanego przez „char * s”, ale nie „char s []”?
źródło
W przypadku:
x jest lwartość - może być przypisany do. Ale w przypadku:
x nie jest wartością, jest wartością - nie można do niej przypisać.
źródło
x
jest to niemodyfikowalna wartość. Jednak w prawie wszystkich kontekstach będzie wskazywał na wskaźnik do pierwszego elementu, a ta wartość jest wartością.źródło
W świetle komentarzy tutaj powinno być oczywiste, że: char * s = "hello"; To zły pomysł i powinien być stosowany w bardzo wąskim zakresie.
To może być dobra okazja do wskazania, że „stała poprawność” jest „dobrą rzeczą”. Kiedykolwiek i gdziekolwiek możesz, użyj słowa kluczowego „const”, aby chronić swój kod, przed „zrelaksowanymi” dzwoniącymi lub programistami, którzy zwykle są najbardziej „zrelaksowani”, gdy pojawiają się wskaźniki.
Dość melodramatu, oto co można osiągnąć, ozdabiając wskaźniki „const”. (Uwaga: należy przeczytać deklaracje wskaźnika od prawej do lewej.) Oto 3 różne sposoby ochrony się podczas gry ze wskaźnikami:
- to znaczy, że obiektu DBJ nie można zmienić za pomocą p.
- to znaczy, możesz zmienić obiekt DBJ za pomocą p, ale nie możesz zmienić samego wskaźnika p.
- to znaczy, nie można zmienić samego wskaźnika p, ani nie można zmienić obiektu DBJ za pomocą p.
Błędy związane z próbami mutacji stałych są wychwytywane podczas kompilacji. Dla const nie ma miejsca na czas wykonywania ani ograniczenia prędkości.
(Zakłada się, że używasz oczywiście kompilatora C ++?)
--DBJ
źródło