Jaka jest różnica między tablicą znaków a wskaźnikiem znaków w C?
Projekt C99 N1256
Istnieją dwa różne zastosowania literałów ciągów znaków:
Zainicjuj char[]
:
char c[] = "abc";
Jest to „więcej magii” i opisane w 6.7.8 / 14 „Inicjalizacja”:
Tablica typu znaków może być inicjalizowana literałem ciągu znaków, opcjonalnie ujętym w nawiasy klamrowe. Kolejne znaki literału łańcucha znaków (w tym kończący znak null, jeśli jest miejsce lub tablica ma nieznany rozmiar), inicjują elementy tablicy.
To tylko skrót do:
char c[] = {'a', 'b', 'c', '\0'};
Jak każda inna zwykła tablica, c
może być modyfikowana.
Wszędzie indziej: generuje:
Więc kiedy piszesz:
char *c = "abc";
Jest to podobne do:
/* __unnamed is magic because modifying it gives UB. */
static char __unnamed[] = "abc";
char *c = __unnamed;
Zwróć uwagę na niejawne przesłanie od char[]
do char *
, 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”:
5 W fazie tłumaczenia 7 bajt lub kod o wartości zero jest dołączany do każdej wielobajtowej sekwencji znaków wynikającej z literału lub literałów z ciągu. Wielobajtowa sekwencja znaków jest następnie używana do zainicjowania tablicy statycznego czasu przechowywania i długości wystarczającej do przechowywania sekwencji. W przypadku literałów ciągów znaków elementy tablicy mają typ char i są inicjowane pojedynczymi bajtami wielobajtowej sekwencji znaków [...]
6 Nie jest określone, czy tablice te są różne, pod warunkiem że ich elementy mają odpowiednie wartości. Jeśli program spróbuje zmodyfikować taką tablicę, zachowanie jest niezdefiniowane.
6.7.8 / 32 „Inicjalizacja” daje bezpośredni przykład:
PRZYKŁAD 8: Deklaracja
char s[] = "abc", t[3] = "abc";
definiuje „zwykłe” obiekty tablicy znaków s
it
którego elementy są inicjalizowane napisowych charakter.
Ta deklaracja jest identyczna z
char s[] = { 'a', 'b', 'c', '\0' },
t[] = { 'a', 'b', 'c' };
Zawartość tablic można modyfikować. Z drugiej strony deklaracja
char *p = "abc";
definiuje za p
pomocą typu „wskaźnik na char” i inicjuje go, aby wskazywał na obiekt o typie „tablica char” o długości 4, którego elementy są inicjowane literałem ciągu znaków. W przypadku próby p
zmodyfikowania zawartości tablicy zachowanie jest niezdefiniowane.
Implementacja GCC 4.8 x86-64 ELF
Program:
#include <stdio.h>
int main(void) {
char *s = "abc";
printf("%s\n", s);
return 0;
}
Kompiluj i dekompiluj:
gcc -ggdb -std=c99 -c main.c
objdump -Sr main.o
Dane wyjściowe zawierają:
char *s = "abc";
8: 48 c7 45 f8 00 00 00 movq $0x0,-0x8(%rbp)
f: 00
c: R_X86_64_32S .rodata
Wniosek: GCC przechowuje char*
go w .rodata
sekcji, a nie w .text
.
Jeśli zrobimy to samo dla char[]
:
char s[] = "abc";
otrzymujemy:
17: c7 45 f0 61 62 63 00 movl $0x636261,-0x10(%rbp)
więc zostaje zapisany na stosie (względem %rbp
).
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:
readelf -l a.out
który zawiera:
Section to Segment mapping:
Segment Sections...
02 .text .rodata
char p[3] = "hello";
Ciąg inicjalizujący jest zbyt długi dla rozmiaru deklarowanej tablicy. Literówka?