Почему я получаю ошибку сегментации при записи в строку, инициализированную "char * s", но не "char s []"? - PullRequest
268 голосов
/ 02 октября 2008

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

Ответы [ 17 ]

220 голосов
/ 02 октября 2008

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

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

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

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

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

93 голосов
/ 02 октября 2008

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

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

printf("%p", str);

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

printf("%d", sizeof(str));
31 голосов
/ 03 октября 2008

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

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

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

20 голосов

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

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 голосов
/ 02 октября 2008

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

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

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

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

12 голосов
/ 02 октября 2008

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

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

8 голосов
/ 07 декабря 2013

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

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

 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", которая является константной строкой, хранящейся в постоянном запоминающем устройстве (ПЗУ), поэтому "hello" нельзя изменить, поскольку она хранится в ПЗУ

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

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

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

ptr="new string"; is valid
7 голосов
/ 02 октября 2008
char *str = "string";  

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

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

6 голосов
/ 03 октября 2008

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

6 голосов
/ 02 октября 2008
char *str = "string";

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

char str[] = "string";

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

...