Dlaczego dostaję błąd segmentacji, gdy piszę do „char * s” zainicjowanego literałem łańcucha, ale nie „char s []”?

288

Poniższy kod odbiera błąd seg w linii 2:

char *str = "string";
str[0] = 'z';  // could be also written as *str = 'z'
printf("%s\n", str);

Chociaż działa to doskonale:

char str[] = "string";
str[0] = 'z';
printf("%s\n", str);

Testowane z MSVC i GCC.

Markus
źródło
1
To zabawne - ale tak naprawdę kompiluje się i działa idealnie, gdy używasz kompilatora Windows (cl) w wierszu polecenia programisty Visual Studio. Przez chwilę byłem zdezorientowany ...
David Refaeli,

Odpowiedzi:

242

Zobacz C FAQ, pytanie 1.32

P : Jaka jest różnica między tymi inicjalizacjami?
char a[] = "string literal";
char *p = "string literal";
Mój program ulega awarii, jeśli próbuję przypisać nową wartość p[i].

Odp . : Dosłowny ciąg znaków (formalny termin na ciąg cudzysłowu w źródle C) może być użyty na dwa nieco inne sposoby:

  1. Jako inicjator tablicy char a[]znaków , podobnie jak w deklaracji , określa początkowe wartości znaków w tej tablicy (i, jeśli to konieczne, jej rozmiar).
  2. Gdziekolwiek indziej zamienia się w nienazwaną, statyczną tablicę znaków, a ta nienazwana tablica może być przechowywana w pamięci tylko do odczytu, a zatem niekoniecznie musi być modyfikowana. W kontekście wyrażeń tablica jest jak zwykle konwertowana na wskaźnik (patrz sekcja 6), więc druga deklaracja inicjuje p, aby wskazywać pierwszy element tablicy bez nazwy.

Niektóre kompilatory mają przełącznik kontrolujący, czy literały łańcuchowe są zapisywalne, czy nie (do kompilowania starego kodu), a niektóre mogą mieć opcje powodujące, że literały łańcuchowe są formalnie traktowane jako tablice const char (dla lepszego wychwytywania błędów).

matli
źródło
7
Kilka innych punktów: (1) segfault dzieje się zgodnie z opisem, ale jego wystąpienie jest funkcją środowiska uruchomieniowego; jeśli ten sam kod był w systemie osadzonym, zapis może nie przynieść żadnego efektu lub może faktycznie zmienić s na z. (2) Ponieważ literałów łańcuchowych nie można zapisywać, kompilator może zaoszczędzić miejsce, umieszczając dwa wystąpienia „łańcucha” w tym samym miejscu; lub, jeśli gdzieś w kodzie masz „inny ciąg”, to jedna część pamięci może obsługiwać oba literały. Oczywiście, gdyby kod mógł wówczas zmienić te bajty, mogłyby wystąpić dziwne i trudne błędy.
greggo
1
@greggo: Dobra uwaga. Istnieje również sposób, aby to zrobić w systemach z MMU za pomocą mprotectochrony falowej tylko do odczytu (patrz tutaj ).
Więc char * p = "bla" faktycznie tworzy tymczasową tablicę? Dziwne.
rahul tyagi
1
A po 2 latach pisania w C ++ ... TIL
zeboidlund
@rahultyagi co masz na myśli?
Suraj Jain,
105

Zwykle literały łańcuchowe są przechowywane w pamięci tylko do odczytu, gdy program jest uruchomiony. Zapobiega to przypadkowej zmianie stałej ciągu. W pierwszym przykładzie "string"jest przechowywany w pamięci tylko do odczytu i *strwskazuje na pierwszy znak. Segfault zdarza się, gdy próbujesz zmienić pierwszy znak na 'z'.

W drugim przykładzie ciąg "string"jest kopiowany przez kompilator z jego domu tylko do odczytu do str[]tablicy. Następnie zmiana pierwszej postaci jest dozwolona. Możesz to sprawdzić, drukując adres każdego:

printf("%p", str);

Również wydrukowanie rozmiaru strw drugim przykładzie pokaże, że kompilator przydzielił dla niego 7 bajtów:

printf("%d", sizeof(str));
Greg Hewgill
źródło
13
Ilekroć używasz „% p” na printf, powinieneś rzucić wskaźnik na void * jak w printf („% p”, (void *) str); Podczas drukowania size_t za pomocą printf, powinieneś użyć „% zu”, jeśli używasz najnowszego standardu C (C99).
Chris Young,
4
Również nawias z sizeof jest potrzebny tylko przy przyjmowaniu rozmiaru typu (argument wygląda wtedy jak rzutowanie). Pamiętaj, że sizeof jest operatorem, a nie funkcją.
zrelaksuj się
i użyj %zudo drukowaniasize_t
phuclv 11.04.17
34

Większość z tych odpowiedzi jest poprawna, ale dla większej przejrzystości ...

„Pamięć tylko do odczytu”, do której odnoszą się ludzie, to segment tekstowy w kategoriach ASM. To to samo miejsce w pamięci, w którym ładowane są instrukcje. Jest to tylko do odczytu z oczywistych powodów, takich jak bezpieczeństwo. Kiedy tworzysz znak * zainicjowany na łańcuch, dane łańcucha są kompilowane do segmentu tekstowego, a program inicjuje wskaźnik, aby wskazywał na segment tekstowy. Więc jeśli spróbujesz to zmienić, kaboom. Segfault.

Kompilator zapisany jako tablica umieszcza zainicjowane dane łańcuchowe w segmencie danych, czyli w tym samym miejscu, w którym znajdują się zmienne globalne. Pamięć ta jest zmienna, ponieważ w segmencie danych nie ma instrukcji. Tym razem, gdy kompilator inicjuje tablicę znaków (która wciąż jest tylko znakiem *), wskazuje ona raczej na segment danych niż segment tekstowy, który można bezpiecznie zmienić w czasie wykonywania.

Bob Somers
źródło
Ale czy nie jest prawdą, że mogą istnieć implementacje, które pozwalają modyfikować „pamięć tylko do odczytu”?
Pacerier
Podczas zapisywania jako tablica kompilator umieszcza zainicjowane dane ciągu w segmencie danych, jeśli są one statyczne lub globalne. W przeciwnym razie (np. Dla normalnej automatycznej tablicy) umieszcza się na stosie, w ramce stosu funkcji main. Poprawny?
SE
27

Dlaczego dostaję błąd segmentacji podczas zapisu do łańcucha?

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 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";

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.

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 .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
Ciro Santilli
źródło
17

W pierwszym kodzie „ciąg” jest stałą ciąg, a stałych ciąg nigdy nie należy modyfikować, ponieważ często są one umieszczane w pamięci tylko do odczytu. „str” to wskaźnik używany do modyfikowania stałej.

W drugim kodzie „string” to inicjator tablicowy, rodzaj krótkiej ręki

char str[7] =  { 's', 't', 'r', 'i', 'n', 'g', '\0' };

„str” to tablica przydzielona na stosie i może być dowolnie modyfikowana.

Andru Luvisi
źródło
1
Na stosie lub segmencie danych, jeśli strjest globalny lub static.
Gauthier
12

Ponieważ typ "whatever"w kontekście pierwszego przykładu to const char *(nawet jeśli przypiszesz go do non-const char *), co oznacza, że ​​nie powinieneś próbować do niego pisać.

Kompilator wymusił to, umieszczając ciąg znaków w części tylko do odczytu, dlatego zapis do niego generuje błąd segfault.


źródło
8

Aby zrozumieć ten błąd lub problem, powinieneś najpierw poznać różnicę między wskaźnikiem i tablicą, więc tutaj najpierw wyjaśnię ci różnice między nimi

tablica ciągów

 char strarray[] = "hello";

W tablicy pamięci jest przechowywany w ciągłych komórkach pamięci, przechowywany tak samo jak [h][e][l][l][o][\0] =>[]komórka pamięci o rozmiarze 1 bajta, a do tych ciągłych komórek pamięci można uzyskać dostęp pod nazwą o nazwie strarray tutaj. Więc tutaj strarraysama tablica ciągów zawierająca wszystkie znaki łańcucha zainicjowane w tym. przypadku, "hello" abyśmy mogli łatwo zmienić zawartość pamięci, uzyskując dostęp do każdego znaku według jego wartości indeksu

`strarray[0]='m'` it access character at index 0 which is 'h'in strarray

a jego wartość zmieniono na 'm'tak zmienną wartość zmienną na "mello";

należy zwrócić uwagę na to, że możemy zmienić zawartość tablicy łańcuchów, zmieniając znak po znaku, ale nie możemy zainicjować innego łańcucha bezpośrednio na nim, ponieważ strarray="new string"jest nieprawidłowy

Wskaźnik

Jak wszyscy wiemy wskaźnik wskazuje na lokalizację pamięci w pamięci, niezainicjowany wskaźnik wskazuje na losową lokalizację pamięci, a po inicjalizacji wskazuje na konkretną lokalizację pamięci

char *ptr = "hello";

tutaj wskaźnik ptr jest inicjowany na ciąg znaków, "hello"który jest ciągiem stałym przechowywanym w pamięci tylko do odczytu (ROM), więc "hello"nie można go zmienić, ponieważ jest przechowywany w pamięci ROM

a ptr jest przechowywany w sekcji stosu i wskazuje na stały ciąg "hello"

więc ptr [0] = 'm' jest nieprawidłowy, ponieważ nie można uzyskać dostępu do pamięci tylko do odczytu

Ale ptr można zainicjować bezpośrednio na inną wartość ciągu, ponieważ jest to tylko wskaźnik, więc może wskazywać dowolny adres pamięci zmiennej typu danych

ptr="new string"; is valid
Społeczność
źródło
7
char *str = "string";  

Powyższe wskazuje strna wartość literalną"string" która jest zakodowana na stałe w obrazie binarnym programu, który prawdopodobnie jest oznaczony jako tylko do odczytu w pamięci.

Więc str[0]=próbuje pisać na tylko do odczytu kodu aplikacji. Sądzę jednak, że to prawdopodobnie zależy od kompilatora.

DougN
źródło
6
char *str = "string";

przydziela wskaźnik do literału łańcuchowego, który kompilator umieszcza w niemodyfikowalnej części pliku wykonywalnego;

char str[] = "string";

przydziela i inicjuje tablicę lokalną, którą można modyfikować

Rob Walker
źródło
czy możemy pisać int *b = {1,2,3) tak jak my char *s = "HelloWorld"?
Suraj Jain,
6

Często zadawane pytania dotyczące C, które @matli odsyła do tej wzmianki, ale nikt jeszcze tu nie ma, więc dla wyjaśnienia: jeśli dosłowny ciąg znaków (ciąg cudzysłowu w źródle) jest używany w dowolnym miejscu innym niż inicjalizacja tablicy znaków (tj .: @ Drugi przykład Marka, który działa poprawnie), ten ciąg jest przechowywany przez kompilator w specjalnej tabeli ciągów statycznych , która jest podobna do tworzenia globalnej zmiennej statycznej (oczywiście tylko do odczytu), która jest zasadniczo anonimowa (nie ma nazwy zmiennej) „). Część tylko do odczytu jest ważną częścią i dlatego pierwszy przykład kodu @ Mark segfaults.

rpj
źródło
czy możemy pisać int *b = {1,2,3) tak jak my char *s = "HelloWorld"?
Suraj Jain,
4

The

 char *str = "string";

linia definiuje wskaźnik i wskazuje dosłowny ciąg. Ciąg literalny nie jest zapisywalny, więc gdy wykonasz:

  str[0] = 'z';

dostajesz błąd seg. Na niektórych platformach literał może znajdować się w zapisywalnej pamięci, więc nie zobaczysz segfault, ale niezależnie od tego jest to nieprawidłowy kod (skutkujący niezdefiniowanym zachowaniem).

Linia:

char str[] = "string";

przydziela tablicę znaków i kopiuje literalny ciąg do tej tablicy, która jest w pełni zapisywalna, więc kolejna aktualizacja nie stanowi problemu.

Michael Burr
źródło
czy możemy pisać int *b = {1,2,3) tak jak my char *s = "HelloWorld"?
Suraj Jain,
3

Literały łańcuchowe, takie jak „łańcuch”, są prawdopodobnie przydzielane w przestrzeni adresowej twojego pliku wykonywalnego jako dane tylko do odczytu (daj lub weź kompilator). Kiedy go dotkniesz, przestraszy Cię, że jesteś w obszarze kostiumu kąpielowego i powiadomi Cię z błędem seg.

W pierwszym przykładzie otrzymujesz wskaźnik do tych stałych danych. W drugim przykładzie inicjujesz tablicę 7 znaków z kopią stałych danych.

Jurney
źródło
2
// create a string constant like this - will be read only
char *str_p;
str_p = "String constant";

// create an array of characters like this 
char *arr_p;
char arr[] = "String in an array";
arr_p = &arr[0];

// now we try to change a character in the array first, this will work
*arr_p = 'E';

// lets try to change the first character of the string contant
*str_p = 'G'; // this will result in a segmentation fault. Comment it out to work.


/*-----------------------------------------------------------------------------
 *  String constants can't be modified. A segmentation fault is the result,
 *  because most operating systems will not allow a write
 *  operation on read only memory.
 *-----------------------------------------------------------------------------*/

//print both strings to see if they have changed
printf("%s\n", str_p); //print the string without a variable
printf("%s\n", arr_p); //print the string, which is in an array. 
jokeysmurf
źródło
1

Przede wszystkim strjest wskaźnikiem, który wskazuje "string". Kompilator może umieszczać literały łańcuchowe w miejscach w pamięci, w których nie można pisać, ale można tylko czytać. (To naprawdę powinno wywołać ostrzeżenie, ponieważ przypisujesz a const char *do char *. Czy masz wyłączone ostrzeżenia, czy po prostu je zignorowałeś?)

Po drugie, tworzysz tablicę, do której masz pełny dostęp, i inicjujesz ją "string". Tworzysz char[7](sześć dla liter, jeden dla kończącego „\ 0”) i robisz z nim, co chcesz.

David Thornley
źródło
@Ferruccio,? Tak, constprefiks tworzy zmienne Tylko do
odczytu
Literały łańcuchowe w języku C mają typ char [N], nie const char [N], więc nie ma ostrzeżenia. (Możesz to zmienić przynajmniej w gcc, przechodząc -Wwrite-strings.)
melpomene
0

Załóżmy, że ciągi są

char a[] = "string literal copied to stack";
char *p  = "string literal referenced by p";

W pierwszym przypadku literał należy skopiować, gdy „a” wchodzi w zakres. Tutaj „a” jest tablicą zdefiniowaną na stosie. Oznacza to, że ciąg zostanie utworzony na stosie, a jego dane zostaną skopiowane z pamięci kodu (tekstu), która zwykle jest tylko do odczytu (jest to specyficzne dla implementacji, kompilator może również umieścić dane tego programu tylko do odczytu w pamięci do odczytu ).

W drugim przypadku p jest wskaźnikiem zdefiniowanym na stosie (zasięg lokalny) i odnosi się do literału łańcucha (dane programu lub tekst) przechowywanego gdzie indziej. Zwykle modyfikowanie takiej pamięci nie jest dobrą praktyką ani nie jest zalecane.

Venki
źródło
-1

Pierwszy to ciąg stały, którego nie można modyfikować. Drugi to tablica z zainicjowaną wartością, więc można ją modyfikować.

libralhb
źródło
-2

Błąd segmentacji powstaje, gdy próbujesz uzyskać dostęp do pamięci, która jest niedostępna.

char *str jest wskaźnikiem na ciąg, który nie jest modyfikowalny (przyczyna uzyskania segfault).

podczas gdy char str[]jest tablicą i może być modyfikowalny ..

Raghu Srikanth Reddy
źródło