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

288

Следующий код получает ошибку сегмента в строке 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.

Markus
источник
1
Это забавно, но на самом деле это прекрасно компилируется и работает при использовании компилятора Windows (cl) в командной строке разработчика Visual Studio. Я запутался на несколько мгновений ...
Дэвид Рафаэли

Ответы:

242

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

Q : В чем разница между этими инициализациями?
char a[] = "string literal";
char *p = "string literal";
Моя программа падает, если я пытаюсь присвоить новое значение p[i].

A : Строковый литерал (формальный термин для строки в двойных кавычках в C-источнике) может использоваться двумя слегка отличающимися способами:

  1. В качестве инициализатора для массива char, как в объявлении char a[], он указывает начальные значения символов в этом массиве (и, при необходимости, его размер).
  2. В любом другом месте он превращается в безымянный статический массив символов, и этот безымянный массив может храниться в постоянной памяти и поэтому не может быть изменен. В контексте выражения массив обычно преобразуется сразу в указатель, как обычно (см. Раздел 6), поэтому второе объявление инициализирует p, чтобы указывать на первый элемент безымянного массива.

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

matli
источник
7
Пара других моментов: (1) segfault происходит, как описано, но его возникновение является функцией среды выполнения; если тот же код был во встроенной системе, запись может не иметь никакого эффекта, или это может фактически изменить s на z. (2) Поскольку строковые литералы недоступны для записи, компилятор может сэкономить место, поместив два экземпляра «string» в одном месте; или, если где-то еще в коде у вас есть «другая строка», то один кусок памяти может поддерживать оба литерала. Понятно, что если бы в коде было разрешено изменять эти байты, могли возникнуть странные и сложные ошибки.
Грегго
1
@greggo: Хорошая мысль. Существует также способ сделать это в системах с MMU, используя mprotectзащиту только для чтения (см. Здесь ).
Так что char * p = "blah" на самом деле создает временный массив - странно.
Рахул Тяги
1
И после 2 лет написания на C ++ ... TIL
zeboidlund
@rahultyagi что ты имеешь ввиду?
Сурадж Джейн
105

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

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

printf("%p", str);

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

printf("%d", sizeof(str));
Грег Хьюгилл
источник
13
Всякий раз, когда вы используете «% p» в printf, вы должны привести указатель к void *, как в printf («% p», (void *) str); При печати size_t с помощью printf вы должны использовать «% zu», если используете последний стандарт C (C99).
Крис Янг
4
Кроме того, круглые скобки с sizeof необходимы только при получении размера типа (тогда аргумент выглядит как приведение). Помните, что sizeof - это оператор, а не функция.
расслабиться
34

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

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

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

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

Почему я получаю ошибку сегментации при записи в строку?

Тяга C99 N1256

Существует два различных варианта использования строковых литералов символов:

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

    char c[] = "abc";      

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

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

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

    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 На этапе перевода 7 байт или код нулевого значения добавляются к каждой многобайтовой последовательности символов, полученной из строкового литерала или литералов. Последовательность многобайтовых символов затем используется для инициализации массива статической продолжительности и длины хранения, достаточных для того, чтобы содержать последовательность. Для строковых литералов символов элементы массива имеют тип char и инициализируются отдельными байтами многобайтовой последовательности символов [...]

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

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

ПРИМЕР 8: декларация

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

определяет "простые" объекты массива символов sиt чьи элементы инициализируются с символьной строки литералов.

Эта декларация идентична

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

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

char *p = "abc";

определяется pс типом «указатель на символ» и инициализирует его, чтобы указывать на объект с типом «массив символа» длиной 4, элементы которого инициализируются литералом символьной строки. Если предпринята попытка использовать pдля изменения содержимого массива, поведение не определено.

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
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
источник
17

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

Во втором коде «строка» является инициализатором массива, своего рода сокращение для

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

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

Андру Лувизи
источник
1
На стеке, или сегмент данных, если strявляется глобальным или static.
Готье
12

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

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


источник
8

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

массив строк

 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'так, что значение изменилось на"mello" ;

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

Указатель

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

char *ptr = "hello";

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

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

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

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

ptr="new string"; is valid
Сообщество
источник
7
char *str = "string";  

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

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

DougN
источник
6
char *str = "string";

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

char str[] = "string";

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

Роб Уокер
источник
мы можем писать int *b = {1,2,3) как мы пишем char *s = "HelloWorld"?
Сурадж Джейн
6

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

гн
источник
мы можем писать int *b = {1,2,3) как мы пишем char *s = "HelloWorld"?
Сурадж Джейн
4

 char *str = "string";

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

  str[0] = 'z';

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

Линия:

char str[] = "string";

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

Майкл Берр
источник
мы можем писать int *b = {1,2,3) как мы пишем char *s = "HelloWorld"?
Сурадж Джейн
3

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

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

Jurney
источник
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
источник
1

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

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

Дэвид Торнли
источник
@ Ферруччо,? Да constпрефикс делает переменные
доступными
В строковых литералах C типа char [N]нет const char [N], поэтому предупреждения нет. (Вы можете изменить это в gcc хотя бы мимоходом -Wwrite-strings.)
melpomene
0

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

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

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

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

Venki
источник
-1

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

libralhb
источник
-2

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

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

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

Рагу Срикант Редди
источник