Почему я получаю ошибку сегментации при записи в "char * s", инициализированный строковым литералом, но не в "char s []"?

Следующий код получает ошибку seg в строке 2:

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

Хотя это прекрасно работает:

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

Протестировано с MSVC и GCC.

Это забавно, но на самом деле он компилируется и отлично работает при использовании компилятора Windows (cl) в командной строке разработчика Visual Studio. На несколько мгновений сбил меня с толку ...

Maverick Meerkat 13.09.2016 11:30
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
305
1
82 252
17
Перейти к ответу Данный вопрос помечен как решенный

Ответы 17

Поскольку тип "whatever" в контексте 1-го примера - const char * (даже если вы назначаете его неконстантному символу *), это означает, что вам не следует пытаться писать в него.

Компилятор добился этого, поместив строку в доступную только для чтения часть памяти, поэтому запись в нее генерирует segfault.

Обычно строковые литералы сохраняются в постоянной памяти при запуске программы. Это сделано для предотвращения случайного изменения строковой константы. В вашем первом примере "string" хранится в постоянной памяти, а *str указывает на первый символ. Ошибка сегментации возникает, когда вы пытаетесь изменить первый символ на 'z'.

Во втором примере строка "string" - это скопировано компилятора из своего дома, доступного только для чтения, в массив str[]. Тогда разрешается изменение первого символа. Вы можете проверить это, распечатав адрес каждого:

printf("%p", str);

Кроме того, печать размера str во втором примере покажет вам, что компилятор выделил для него 7 байтов:

printf("%d", sizeof(str));

Всякий раз, когда вы используете "% p" в printf, вы должны привести указатель к void *, как в printf ("% p", (void *) str); При печати size_t с помощью printf вы должны использовать "% zu", если используете последний стандарт C (C99).

Chris Young 03.10.2008 11:44

Кроме того, круглые скобки с sizeof необходимы только при выборе размера типа (тогда аргумент выглядит как приведение). Помните, что sizeof - это оператор, а не функция.

unwind 25.11.2008 11:45

и используйте %zu для печати size_t

phuclv 11.04.2017 18:36

предупреждение: неизвестный символ типа преобразования 'z' в формате [-Wformat =]: /

john 26.02.2021 13:02

char *str = "string";  

Вышеуказанное устанавливает str для указания на буквальное значение "string", которое жестко закодировано в двоичном образе программы, который, вероятно, помечен как доступный только для чтения в памяти.

Итак, str[0]= пытается писать в код приложения, доступный только для чтения. Я бы предположил, что это, вероятно, зависит от компилятора.

В первом коде «строка» - это строковая константа, и строковые константы никогда не должны изменяться, потому что они часто помещаются в постоянную память. "str" ​​- это указатель, используемый для изменения константы.

Во втором коде строка - это инициализатор массива, что-то вроде сокращения для

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

"str" ​​- это массив, размещенный в стеке, который можно свободно изменять.

В стеке или сегменте данных, если str является глобальным или static.

Gauthier 24.03.2016 12:16

char *str = "string";

выделяет указатель на строковый литерал, который компилятор помещает в неизменяемую часть вашего исполняемого файла;

char str[] = "string";

выделяет и инициализирует локальный массив, который можно изменять

мы можем написать int *b = {1,2,3), как мы пишем char *s = "HelloWorld"?

Suraj Jain 27.12.2016 11:08

Строковые литералы, такие как «строка», вероятно, размещены в адресном пространстве вашего исполняемого файла как данные только для чтения (плюс-минус ваш компилятор). Когда вы собираетесь прикоснуться к нему, он пугается, что вы находитесь в зоне купального костюма, и сообщает вам об этом с ошибкой.

В вашем первом примере вы получаете указатель на эти константные данные. Во втором примере вы инициализируете массив из 7 символов копией константных данных.

В

 char *str = "string";

line определяет указатель и указывает на буквальную строку. Буквальная строка недоступна для записи, поэтому, когда вы это сделаете:

  str[0] = 'z';

вы получаете ошибку сегмента. На некоторых платформах литерал может находиться в доступной для записи памяти, поэтому вы не увидите segfault, но это недопустимый код (что приводит к неопределенному поведению) в любом случае.

Линия:

char str[] = "string";

выделяет массив символов и копии литеральную строку в этот массив, который полностью доступен для записи, поэтому последующее обновление не проблема.

мы можем написать int *b = {1,2,3), как мы пишем char *s = "HelloWorld"?

Suraj Jain 27.12.2016 11:08
Ответ принят как подходящий

См. FAQ по C, Вопрос 1.32

Q: What is the difference between these initializations?
char a[] = "string literal";
char *p = "string literal";
My program crashes if I try to assign a new value to p[i].

A: A string literal (the formal term for a double-quoted string in C source) can be used in two slightly different ways:

  1. As the initializer for an array of char, as in the declaration of char a[] , it specifies the initial values of the characters in that array (and, if necessary, its size).
  2. Anywhere else, it turns into an unnamed, static array of characters, and this unnamed array may be stored in read-only memory, and which therefore cannot necessarily be modified. In an expression context, the array is converted at once to a pointer, as usual (see section 6), so the second declaration initializes p to point to the unnamed array's first element.

Some compilers have a switch controlling whether string literals are writable or not (for compiling old code), and some may have options to cause string literals to be formally treated as arrays of const char (for better error catching).

Пара других моментов: (1) segfault происходит, как описано, но его возникновение является функцией среды выполнения; если такой же код был во встроенной системе, запись может не иметь никакого эффекта или может фактически изменить s на z. (2) Поскольку строковые литералы не доступны для записи, компилятор может сэкономить место, поместив два экземпляра строки в одно и то же место; или, если где-то еще в коде есть «другая строка», то один фрагмент памяти может поддерживать оба литерала. Ясно, что если бы коду было разрешено изменять эти байты, могли бы возникнуть странные и сложные ошибки.

greggo 27.08.2011 03:24

@greggo: Хорошее замечание. Также есть способ сделать это в системах с MMU, используя mprotect для активации защиты только для чтения (см. здесь).

user405725 02.05.2013 17:40

Итак, char * p = "blah" на самом деле создает временный массив?

rahul tyagi 03.12.2014 16:35

И через 2 года написания на C++ ... TIL

zeboidlund 28.12.2014 03:41

@rahultyagi, что ты имеешь в виду?

Suraj Jain 27.12.2016 12:40

@rahul tyagi, не временный массив. Напротив, это самый долгоживущий из массивов. Он создается компилятором и находится в самом исполняемом файле. Из вышесказанного вы должны были понять, что это массив общий, который следует рассматривать как только чтение (и на самом деле он может быть доступен только для чтения).

ikegami 15.11.2019 13:03

Прежде всего, str - это указатель, указывающий на "string". Компилятору разрешено помещать строковые литералы в те места в памяти, которые вы не можете записывать, но можете только читать. (Это действительно должно было вызвать предупреждение, поскольку вы назначаете const char * на char *. Вы отключили предупреждения или просто проигнорировали их?)

Во-вторых, вы создаете массив, представляющий собой память, к которой у вас есть полный доступ, и инициализируете его с помощью "string". Вы создаете char[7] (шесть для букв, один для завершающего '\ 0') и делаете с ним все, что хотите.

@ Ферруччо,? Да префикс const делает переменные доступными только для чтения

EsmaeelE 09.12.2017 16:40

В строковых литералах C тип char [N], а не const char [N], поэтому предупреждения нет. (Вы можете изменить это в gcc, по крайней мере, передав -Wwrite-strings.)

melpomene 13.06.2018 06:55

В часто задаваемых вопросах C, на который ссылается @matli, упоминается это, но здесь еще нет никого, поэтому для пояснения: если строковый литерал (строка в двойных кавычках в вашем источнике) используется где-либо, Кроме как для инициализации массива символов (например: второй пример, который работает правильно), эта строка сохраняется компилятором в специальном статическая таблица строк, что сродни созданию глобальной статической переменной (конечно, только для чтения), которая по сути является анонимной (не имеет «имени» переменной). Часть только чтение является важной частью, и именно поэтому в первом примере кода @Mark происходит сбой.

мы можем написать int *b = {1,2,3), как мы пишем char *s = "HelloWorld"?

Suraj Jain 27.12.2016 11:09

Большинство из этих ответов верны, но просто для большей ясности ...

«Постоянная память для чтения», о которой говорят люди, - это текстовый сегмент в терминах ASM. Это то же самое место в памяти, куда загружаются инструкции. Он доступен только для чтения по очевидным причинам, таким как безопасность. Когда вы создаете char *, инициализированный строкой, строковые данные компилируются в текстовый сегмент, и программа инициализирует указатель, чтобы он указывал на текстовый сегмент. Так что, если вы попытаетесь это изменить, бахвальф. Segfault.

При записи в виде массива компилятор вместо этого помещает инициализированные строковые данные в сегмент данных, который находится в том же месте, что и ваши глобальные переменные и тому подобное. Эта память изменяема, поскольку в сегменте данных нет инструкций. На этот раз, когда компилятор инициализирует массив символов (который по-прежнему является просто символом *), он указывает на сегмент данных, а не на текстовый сегмент, который вы можете безопасно изменить во время выполнения.

Но разве не могут быть реализации, позволяющие изменять «постоянную память»?

Pacerier 21.09.2013 09:07

При записи в виде массива компилятор помещает инициализированные строковые данные в сегмент данных, если они статические или глобальные. В противном случае (например, для обычного автоматического массива) он помещается в стек, в кадр стека функции main. Правильный?

S E 04.12.2019 05:15

@SE Да, я бы предположил, что Боб Сомерс имеет в виду и стек, и кучу, и статические (включая статические и глобальные переменные) при записи «сегмента данных». А в стек кладется локальный массив, значит, в этом вы правы :)

Olov 27.12.2020 04:44

Извините, но вы, вероятно, правы здесь. Сегмент данных - это часть памяти, выделенная для инициализированных глобальных или статических переменных, но массив также может быть помещен в стек, если он локальный, как вы написали.

Olov 27.12.2020 04:53

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

Ошибка сегментации возникает, когда вы пытаетесь получить доступ к недоступной памяти.

char *str - это указатель на строку, которую нельзя изменить (причина получения segfault).

тогда как char str[] является массивом и может быть изменен ..

Во-первых, это одна постоянная строка, которую нельзя изменить. Во-вторых, это массив с инициализированным значением, поэтому его можно изменить.

Чтобы понять эту ошибку или проблему, вы должны сначала узнать разницу между указателем и массивом. Итак, сначала я объясню вам различия между ними

строковый массив

 char strarray[] = "hello";

В памяти массив хранится в ячейках непрерывной памяти, хранится как [h][e][l][l][o][\0] =>[], это ячейка памяти размером 1 байт, и к этим ячейкам непрерывной памяти можно получить доступ по имени с именем strarray здесь. Итак, здесь сам массив строк strarray, содержащий все символы строки, инициализированной для него. в данном случае здесь "hello" поэтому мы можем легко изменить его содержимое памяти, обращаясь к каждому символу по его значению индекса

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

и его значение изменилось на 'm', поэтому значение strarray изменилось на "mello";

здесь следует отметить, что мы можем изменить содержимое массива строк, изменяя символ за символом, но не можем инициализировать другую строку напрямую, например, strarray = "new string" недействителен

Указатель

Как мы все знаем, указатель указывает на место в памяти, неинициализированный указатель указывает на случайную ячейку памяти, поэтому после инициализации указывает на конкретную ячейку памяти

char *ptr = "hello";

здесь указатель ptr инициализируется строкой "hello", которая является постоянной строкой, хранящейся в постоянном запоминающем устройстве (ROM), поэтому "hello" не может быть изменен, поскольку он хранится в ROM

и ptr хранится в разделе стека и указывает на постоянную строку "hello"

поэтому ptr [0] = 'm' недействителен, так как вы не можете получить доступ к памяти только для чтения

Но ptr может быть инициализирован другим строковым значением напрямую, поскольку это просто указатель, поэтому он может указывать на любой адрес памяти переменной своего типа данных.

ptr = "new string"; is valid

Why do I get a segmentation fault when writing to a string?

C99 N1256 тяга

Есть два разных использования литералов символьной строки:

  1. Инициализировать char[]:

    char c[] = "abc";      
    

    Это «больше волшебства», описанное в 6.7.8 / 14 «Инициализация»:

    An array of character type may be initialized by a character string literal, optionally enclosed in braces. Successive characters of the character string literal (including the terminating null character if there is room or if the array is of unknown size) initialize the elements of the array.

    Так что это просто ярлык для:

    char c[] = {'a', 'b', 'c', '\0'};
    

    Как и любой другой обычный массив, c можно модифицировать.

  2. В другом месте: он генерирует:

    Итак, когда вы пишете:

    char *c = "abc";
    

    Это похоже на:

    /* __unnamed is magic because modifying it gives UB. */
    static char __unnamed[] = "abc";
    char *c = __unnamed;
    

    Обратите внимание на неявное приведение от char[] к char *, которое всегда допустимо.

    Затем, если вы измените c[0], вы также измените __unnamed, который является UB.

    Это описано в разделе 6.4.5 «Строковые литералы»:

    5 In translation phase 7, a byte or code of value zero is appended to each multibyte character sequence that results from a string literal or literals. The multibyte character sequence is then used to initialize an array of static storage duration and length just sufficient to contain the sequence. For character string literals, the array elements have type char, and are initialized with the individual bytes of the multibyte character sequence [...]

    6 It is unspecified whether these arrays are distinct provided their elements have the appropriate values. If the program attempts to modify such an array, the behavior is undefined.

6.7.8 / 32 "Инициализация" дает прямой пример:

EXAMPLE 8: The declaration

char s[] = "abc", t[3] = "abc";

defines "plain" char array objects s and t whose elements are initialized with character string literals.

This declaration is identical to

char s[] = { 'a', 'b', 'c', '\0' },
t[] = { 'a', 'b', 'c' };

The contents of the arrays are modifiable. On the other hand, the declaration

char *p = "abc";

defines p with type "pointer to char" and initializes it to point to an object with type "array of char" with length 4 whose elements are initialized with a character string literal. If an attempt is made to use p to modify the contents of the array, the behavior is undefined.

Реализация GCC 4.8 x86-64 ELF

Программа:

#include <stdio.h>

int main(void) {
    char *s = "abc";
    printf("%s\n", s);
    return 0;
}

Скомпилировать и декомпилировать:

gcc -ggdb -std=c99 -c main.c
objdump -Sr main.o

Вывод содержит:

 char *s = "abc";
8:  48 c7 45 f8 00 00 00    movq   $0x0,-0x8(%rbp)
f:  00 
        c: R_X86_64_32S .rodata

Вывод: GCC хранит char* в разделе .rodata, а не в .text.

Если сделать то же самое для char[]:

 char s[] = "abc";

мы получаем:

17:   c7 45 f0 61 62 63 00    movl   $0x636261,-0x10(%rbp)

поэтому он сохраняется в стеке (относительно %rbp).

Однако обратите внимание, что сценарий компоновщика по умолчанию помещает .rodata и .text в один и тот же сегмент, который имеет разрешение на выполнение, но не имеет разрешения на запись. Это можно наблюдать с помощью:

readelf -l a.out

который содержит:

 Section to Segment mapping:
  Segment Sections...
   02     .text .rodata

Предположим, что строки:

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

В первом случае литерал должен быть скопирован, когда "a" входит в область видимости. Здесь «a» - это массив, определенный в стеке. Это означает, что строка будет создана в стеке, и ее данные будут скопированы из памяти кода (текста), которая обычно доступна только для чтения (это зависит от реализации, компилятор может также поместить эти данные программы только для чтения в память с возможностью чтения и записи. ).

Во втором случае p - это указатель, определенный в стеке (локальная область видимости) и ссылающийся на строковый литерал (данные программы или текст), хранящийся в другом месте. Обычно изменение такой памяти не является хорошей практикой и не поощряется.

Другие вопросы по теме