Jaka jest różnica między char [s] a char * s?

506

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.

StoryTeller - Unslander Monica
źródło
8
char * s = "witaj", tutaj można wskazać dowolny ciąg w czasie wykonywania Mam na myśli, że nie jest to stały wskaźnik, możesz przypisać inną wartość w czasie wykonywania p = "Nishant", podczas gdy s [] tutaj jest stałym wskaźnikiem .. .. nie można ponownie przypisać innego ciągu, ale możemy przypisać inną wartość znaku na s [indeks].
Nishant Kumar

Odpowiedzi:

541

Różnica polega na tym, że

char *s = "Hello world";

umieści "Hello world"w częściach tylko do odczytu , a utworzenie swskaźnika spowoduje, że wszelkie operacje zapisu w tej pamięci będą nielegalne.

Podczas wykonywania:

char s[] = "Hello world";

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

s[0] = 'J';

prawny.

Rickard
źródło
22
Dosłowny ciąg "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.
pmg
28
pmg: W drugim przypadku ciąg dosłowny niekoniecznie musi istnieć w pamięci jako pojedynczy ciągły obiekt - to tylko inicjator, kompilator może dość rezonansowo emitować serię instrukcji „ładuj natychmiastowy bajt”, które zawierają wartości znaków osadzone w im.
caf
10
Przykład tablicy char ma nie koniecznie umieścić napis na stosie - jeżeli okaże się na poziomie plików, to najprawdopodobniej będzie w jakimś zainicjowane zamiast segmentu danych.
caf
9
Chciałbym zaznaczyć, że char s = "xx" nie musi znajdować się w pamięci tylko do odczytu (na przykład niektóre implementacje nie mają MMU). Szkic n1362 c1x po prostu stwierdza, że ​​modyfikacja takiej tablicy powoduje niezdefiniowane zachowanie. Ale i tak +1, ponieważ poleganie na tym zachowaniu jest głupotą.
paxdiablo
3
Otrzymuję czystą kompilację na pliku zawierającym tylko 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.3
gcbenison
152

Po pierwsze, w argumentach funkcji są one dokładnie równoważne:

void foo(char *x);
void foo(char x[]); // exactly the same in all respects

W innych kontekstach char *przydziela wskaźnik, a char []przydziela tablicę. Gdzie idzie sznur w pierwszym przypadku, pytasz? Kompilator potajemnie przydziela statyczną anonimową tablicę do przechowywania literału łańcucha. Więc:

char *x = "Foo";
// is approximately equivalent to:
static const char __secret_anonymous_array[] = "Foo";
char *x = (char *) __secret_anonymous_array;

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ę):

x[1] = 'O'; // BAD. DON'T DO THIS.

Zastosowanie składni tablicowej powoduje bezpośrednie przydzielenie jej do nowej pamięci. Dlatego modyfikacja jest bezpieczna:

char x[] = "Foo";
x[1] = 'O'; // No problem.

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.

bdonlan
źródło
72

Ta deklaracja:

char s[] = "hello";

Tworzy jeden obiekt - chartablicę 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:

char *s ="hello";

Tworzy dwa obiekty:

  • 6 -sekundowa tablica tylko do odczytuchar 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); i
  • wywoływana zmienna typu wskaźnik-na-char, sktó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 szmiennej wskaźnika w pamięci zależy od tego, gdzie pojawia się deklaracja (tak jak w pierwszym przykładzie).

caf
źródło
1
W obu deklaracjach dla „cześć” pamięć jest przydzielana w czasie kompletnym? A kolejna rzecz char * p = „cześć” tutaj „cześć” jest przechowywana w segmencie tekstowym, jak podano w odpowiedzi ... a co z char [] = „cześć”, zapisze również najpierw w części segmentu tekstowego, aw czasie wykonywania skopiuje na stos, jak Rickard stwierdził w odpowiedzi. proszę wyjaśnić ten punkt.
Nishant Kumar
2
@Nishant: W tym char s[] = "hello"przypadku "hello"jest to tylko inicjator informujący kompilator o sposobie inicjalizacji tablicy. Może, ale nie smusi, 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 - ssam obiekt . Nawet jeśli sma 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)).
caf
Mówiąc dokładniej, GCC 4.8 umieszcza go w tym .rodata, w którym skrypt linkera zrzuca ten sam segment co .text. Zobacz moją odpowiedź .
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
@caf W pierwszej odpowiedzi Rickarda napisano, że zapisuje 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?
KPMG
1
@AjaySinghNegi: Jak napisałem w innych komentarzach (do tej odpowiedzi i odpowiedzi Rickarda), ciąg znaków char s[] = "Hellow world";jest tylko inicjatorem i niekoniecznie jest przechowywany jako osobna kopia tylko do odczytu. Jeśli sma statyczny czas przechowywania, wówczas jedyna kopia ciągu prawdopodobnie znajduje się w segmencie odczytu i zapisu w miejscu s, 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.
caf
60

Biorąc pod uwagę deklaracje

char *s0 = "hello world";
char s1[] = "hello world";

przyjmij następującą hipotetyczną mapę pamięci:

                    0x01 0x02 0x03 0x04
        0x00008000: „h” „e” „l” „l”
        0x00008004: „o” „w” „o”
        0x00008008: „r” „l” „d” 0x00
        ...
s0: 0x00010000: 0x00 0x00 0x80 0x00
s1: 0x00010004: „h” „e” „l” „l”
        0x00010008: „o” „w” „o”
        0x0001000C: „r” „l” „d” 0x00

Dosłowny ciąg znaków "hello world"to 12-elementowa tablica char( const charw 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

char *s0 = "hello world";

definiuje s0jako wskaźnik charz automatycznym czasem przechowywania (co oznacza, że ​​zmienna s0istnieje tylko dla zakresu, w którym została zadeklarowana) i kopiuje do niej adres literału łańcucha ( 0x00008000w tym przykładzie). Należy zauważyć, że ponieważ s0wskazuje na ciągiem znaków, nie powinno być wykorzystywane jako argument do dowolnej funkcji, które starają się je modyfikować (np strtok(), strcat(), strcpy()itd.)

Linia

char s1[] = "hello world";

definiuje s1jako 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 w s1.

s0i s1są wymienne w większości kontekstów; oto wyjątki:

sizeof s0 == sizeof (char*)
sizeof s1 == 12

type of &s0 == char **
type of &s1 == char (*)[12] // pointer to a 12-element array of char

Możesz ponownie przypisać zmienną, s0aby wskazywała inny literał łańcuchowy lub inną zmienną. Nie można ponownie przypisać zmiennej, s1aby wskazywała inną tablicę.

John Bode
źródło
2
Myślę, że hipotetyczna mapa pamięci ułatwia zrozumienie!
midnightblue
32

Projekt C99 N1256

Istnieją dwa różne zastosowania literałów ciągów znaków:

  1. 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, cmoże być modyfikowana.

  2. 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 ukrytą rzutowanie z char[]na 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 znaków. Wielobajtowa sekwencja znaków jest następnie używana do inicjalizacji 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ą odrębne, 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";

określa „zwykły” obiektów tablicy char si tktó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 ppomocą 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 pzmodyfikowania 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 .rodatasekcji, a nie w .text.

Zauważ jednak, że domyślny skrypt linkera umieszcza .rodatai .textw 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

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).

Ciro Santilli
źródło
15
char s[] = "hello";

deklaruje, sże jest tablicą, charktóra jest wystarczająco długa, aby pomieścić inicjator (5 + 1 chars) i inicjuje tablicę, kopiując elementy danego literału łańcucha do tablicy.

char *s = "hello";

deklaruje, sże jest wskaźnikiem do jednego lub więcej (w tym przypadku więcej) chars i wskazuje go bezpośrednio na stałą (tylko do odczytu) lokalizację zawierającą literał "hello".

CB Bailey
źródło
1
Jakiej metody najlepiej użyć w funkcjach, jeśli s nie zostanie zmieniony, f (const char s []) lub f (const char * s)?
psihodelia,
1
@psihodelia: W deklaracji funkcji nie ma różnicy. W obu przypadkach sjest wskaźnikiem do const char.
CB Bailey,
4
char s[] = "Hello world";

Oto stablica znaków, które można zastąpić, jeśli chcemy.

char *s = "hello";

Literał łańcuchowy służy do tworzenia bloków znaków gdzieś w pamięci, na którą swskazuje 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.

Sailaja
źródło
@bo Persson Dlaczego nie można zmienić bloku znaków w drugim przypadku?
Pankaj Mahato
3

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:

printf("%c", x[1]);     //Prints r

I:

printf("%c", *(x + 1)); //Prints r

Oczywiście, jeśli spróbujesz

*(x + 1) = 'a';

Prawdopodobnie wystąpi błąd segmentacji, gdy próbujesz uzyskać dostęp do pamięci tylko do odczytu.

Nick Louloudakis
źródło
Nie różni się to w żaden sposób od tego, x[1] = 'a';co spowoduje awarię (oczywiście w zależności od platformy).
glglgl
3

Wystarczy dodać: otrzymujesz różne wartości dla ich rozmiarów.

printf("sizeof s[] = %zu\n", sizeof(s));  //6
printf("sizeof *s  = %zu\n", sizeof(s));  //4 or 8

Jak wspomniano powyżej, tablica '\0'zostanie przydzielona jako ostatni element.

Muzab
źródło
2
char *str = "Hello";

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.

char str[] = "Hello";

kopiuje ciąg do nowo przydzielonej pamięci na stosie. W związku z tym dokonywanie jakichkolwiek zmian jest dozwolone i legalne.

means str[0] = 'M';

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 []”?

Mohit
źródło
0

W przypadku:

char *x = "fred";

x jest lwartość - może być przypisany do. Ale w przypadku:

char x[] = "fred";

x nie jest wartością, jest wartością - nie można do niej przypisać.

Lee-Man
źródło
3
Technicznie xjest to niemodyfikowalna wartość. Jednak w prawie wszystkich kontekstach będzie wskazywał na wskaźnik do pierwszego elementu, a ta wartość jest wartością.
caf
0
char *s1 = "Hello world"; // Points to fixed character string which is not allowed to modify
char s2[] = "Hello world"; // As good as fixed array of characters in string so allowed to modify

// s1[0] = 'J'; // Illegal
s2[0] = 'J'; // Legal
Atul
źródło
-1

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:

const DBJ* p means "p points to a DBJ that is const" 

- to znaczy, że obiektu DBJ nie można zmienić za pomocą p.

DBJ* const p means "p is a const pointer to a DBJ" 

- to znaczy, możesz zmienić obiekt DBJ za pomocą p, ale nie możesz zmienić samego wskaźnika p.

const DBJ* const p means "p is a const pointer to a const DBJ" 

- 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
To wszystko prawda, ale nie ma to nic wspólnego z pytaniem. I jeśli chodzi o twoje założenie dotyczące kompilatora C ++, pytanie jest oznaczone jako C, a nie jako C ++.
Fabio mówi Przywróć Monikę
Nie ma nic złego w char * s = "const string";
Paul Smith