Почему я получаю ошибку сегментации при записи в строку, инициализированную "char *s", но не "char s[]"?
Следующий код получает ошибку сегмента в строке 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.
20 ответов
См. C FAQ, Вопрос 1.32
Q: В чем разница между этими инициализациями?
char a[] = "string literal";
char *p = "string literal";
Моя программа падает, если я пытаюсь присвоить новое значениеp[i]
,A: Строковый литерал (формальный термин для строки в двойных кавычках в C-источнике) может использоваться двумя слегка отличающимися способами:
- В качестве инициализатора для массива char, как в объявлении
char a[]
, он определяет начальные значения символов в этом массиве (и, если необходимо, его размер).- В любом другом месте он превращается в безымянный статический массив символов, и этот безымянный массив может храниться в постоянной памяти и поэтому не может быть изменен. В контексте выражения массив обычно преобразуется сразу в указатель, как обычно (см. Раздел 6), поэтому второе объявление инициализирует p, чтобы указывать на первый элемент безымянного массива.
Некоторые компиляторы имеют переключатель, управляющий тем, доступны ли строковые литералы для записи или нет (для компиляции старого кода), а некоторые могут иметь параметры, позволяющие формально обрабатывать строковые литералы как массивы const char (для лучшего обнаружения ошибок).
Обычно строковые литералы хранятся в постоянной памяти только при запуске программы. Это предотвращает случайное изменение строковой константы. В вашем первом примере "string"
хранится в постоянной памяти и *str
указывает на первый символ. Segfault происходит, когда вы пытаетесь изменить первый символ на 'z'
,
Во втором примере строка "string"
копируется компилятором из его доступного только для чтения дома в str[]
массив. Тогда изменение первого символа разрешено. Вы можете проверить это, напечатав адрес каждого:
printf("%p", str);
Кроме того, печать размером str
во втором примере покажет, что компилятор выделил для него 7 байт:
printf("%d", sizeof(str));
Большинство из этих ответов верны, но для большей ясности...
"Память только для чтения", на которую ссылаются люди, - это текстовый сегмент в терминах ASM. Это то же самое место в памяти, куда загружаются инструкции. Это только для чтения по очевидным причинам, таким как безопасность. Когда вы создаете char *, инициализированный в строку, строковые данные компилируются в текстовый сегмент, и программа инициализирует указатель так, чтобы он указывал на текстовый сегмент. Так что, если вы попытаетесь изменить это, kaboom. Segfault.
При записи в виде массива компилятор помещает инициализированные строковые данные в сегмент данных, который находится в том же месте, где живут ваши глобальные переменные и тому подобное. Эта память изменчива, поскольку в сегменте данных нет инструкций. На этот раз, когда компилятор инициализирует массив символов (который все еще является просто символом *), он указывает на сегмент данных, а не на текстовый сегмент, который можно смело изменять во время выполнения.
Почему при записи в строку возникает ошибка сегментации?
C99 N1256 осадка
Существует два различных варианта использования строковых литералов символов:
инициализировать
char[]
:char c[] = "abc";
Это "больше волшебства", и описано в 6.7.8/14 "Инициализация":
Массив символьного типа может быть инициализирован литералом символьной строки, необязательно заключенным в фигурные скобки. Последовательные символы литерала символьной строки (включая завершающий нулевой символ, если есть место или массив имеет неизвестный размер) инициализируют элементы массива.
Так что это просто ярлык для:
char c[] = {'a', 'b', 'c', '\0'};
Как и любой другой обычный массив,
c
можно изменить.Везде: генерирует:
- неназванный
- массив символов Какого типа строковые литералы в C и C++?
- со статическим хранилищем
- который дает UB, если модифицирован
Поэтому, когда вы пишете:
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
В первом коде "строка" является строковой константой, и строковые константы никогда не должны изменяться, потому что они часто помещаются в постоянную память. "str" - указатель, используемый для изменения константы.
Во втором коде "строка" является инициализатором массива, своего рода сокращение для
char str[7] = { 's', 't', 'r', 'i', 'n', 'g', '\0' };
"str" - это массив, размещенный в стеке, и его можно свободно изменять.
Потому что тип "whatever"
в контексте 1-го примера const char *
(даже если вы назначите его неконстантному символу *), что означает, что вы не должны пытаться писать в него.
Компилятор ввел это в действие, поместив строку в часть памяти, доступную только для чтения, и, следовательно, запись в нее генерирует ошибку segfault.
char *str = "string";
Вышеуказанные наборы str
указать на буквальное значение "string"
который жестко запрограммирован в двоичном образе программы, который, вероятно, помечен как доступный только для чтения в памяти.
Так str[0]=
пытается записать в код приложения только для чтения. Я думаю, это, вероятно, зависит от компилятора.
Чтобы понять эту ошибку или проблему, вы должны сначала знать разницу ч / б указателя и массива, так что здесь сначала я объясню вам различия ч / б
массив строк
char strarray[] = "hello";
В памяти массив хранится в непрерывных ячейках памяти, хранится как [h][e][l][l][o][\0] =>[]
равен 1 ячейке памяти размером в 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
char *str = "string";
выделяет указатель на строковый литерал, который компилятор помещает в неизменяемую часть вашего исполняемого файла;
char str[] = "string";
выделяет и инициализирует локальный массив, который можно изменить
Часто задаваемые вопросы о C, на которые ссылается @matli, упоминают об этом, но здесь еще никто об этом не упоминает, поэтому для пояснения: если строковый литерал (строка в двойных кавычках в вашем источнике) используется где-либо, кроме как для инициализации массива символов (то есть: @ Второй пример Марка, который работает правильно), эта строка хранится компилятором в специальной таблице статических строк, которая похожа на создание глобальной статической переменной (конечно, только для чтения), которая по сути является анонимной (имя переменной не имеет) "). Часть только для чтения - важная часть, и именно поэтому первый пример кода @Mark содержит ошибки.
char *str = "string";
Строка определяет указатель и указывает на литеральную строку. Буквенная строка недоступна для записи, поэтому, когда вы делаете:
str[0] = 'z';
Вы получаете ошибку сегмента. На некоторых платформах литерал может находиться в доступной для записи памяти, поэтому вы не увидите segfault, но это неверный код (приводящий к неопределенному поведению) независимо.
Линия:
char str[] = "string";
выделяет массив символов и копирует буквенную строку в этот массив, который полностью доступен для записи, поэтому последующее обновление не представляет проблем.
Строковые литералы, такие как "строка", вероятно, размещаются в адресном пространстве вашего исполняемого файла как данные только для чтения (дайте или возьмите ваш компилятор). Когда вы дотрагиваетесь до него, он пугается, что вы находитесь в зоне купального костюма, и дает вам знать, что он неисправен.
В первом примере вы получаете указатель на эти постоянные данные. Во втором примере вы инициализируете массив из 7 символов с копией данных const.
// 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.
Постоянная память
Поскольку строковые литералы предназначены только для чтения, они хранятся в постоянной части памяти. Хранящиеся там данные неизменяемы, т. Е. Не могут быть изменены. Таким образом, все строковые литералы, определенные в коде C, получают здесь адрес памяти только для чтения.
Стековая память
В стековой части памяти находятся адреса локальных переменных, например переменных, определенных в функциях.
Как следует из ответа @matli, есть два способа работы со строкой с этими постоянными строками.
1. Указатель на строковый литерал
Когда мы определяем указатель на строковый литерал, мы создаем переменную-указатель, живущую в памяти стека . Он указывает на доступный только для чтения адрес, где находится базовый строковый литерал.
#include <stdio.h>
int main(void) {
char *s = "hello";
printf("%p\n", &s); // Prints a read-only address, e.g. 0x7ffc8e224620
return 0;
}
Если мы попытаемся изменить
s
вставив
s[0] = 'H';
мы получаем
Segmentation fault (core dumped)
. Мы пытаемся получить доступ к памяти, к которой у нас нет доступа. Мы пытаемся изменить значение адреса только для чтения.
2. Массив символов
Для примера предположим, что строковый литерал
"Hello"
хранится в постоянной памяти, имеет адрес памяти только для чтения, идентичный указанному выше,
0x7ffc8e224620
.
#include <stdio.h>
int main(void) {
// We create an array from a string literal with address 0x7ffc8e224620.
// C initializes an array variable in the stack, let's give it address
// 0x7ffc7a9a9db2.
// C then copies the read-only value from 0x7ffc8e224620 into
// 0x7ffc7a9a9db2 to give us a local copy we can mutate.
char a[] = "hello";
// We can now mutate the local copy
a[0] = 'H';
printf("%p\n", &a); // Prints the Stack address, e.g. 0x7ffc7a9a9db2
printf("%s\n", a); // Prints "Hello"
return 0;
}
Примечание. При использовании указателей на строковые литералы, как в 1., лучше всего использовать
const
ключевое слово, напримерconst *s = "hello"
. Это более читабельно, и компилятор лучше поможет в случае нарушения. Затем он выдаст ошибку, напримерerror: assignment of read-only location ‘*s’
вместо ошибки сегмента. Линтеры в редакторах также, вероятно, обнаружат ошибку до того, как вы вручную скомпилируете код.
На первом месте, str
указатель, который указывает на "string"
, Компилятору разрешено помещать строковые литералы в места в памяти, в которые вы не можете писать, но можете только читать. (Это действительно должно было вызвать предупреждение, так как вы назначаете const char *
к char *
, У вас были отключены предупреждения или вы их просто игнорировали?)
Во-вторых, вы создаете массив, то есть память, к которой у вас есть полный доступ, и инициализируете его "string"
, Вы создаете char[7]
(шесть для букв, один для завершающего '\0'), и вы делаете с ним все, что захотите.
Предположим, что строки
char a[] = "string literal copied to stack";
char *p = "string literal referenced by p";
В первом случае, литерал должен быть скопирован, когда "а" входит в область видимости. Здесь "a" - это массив, определенный в стеке. Это означает, что строка будет создана в стеке, а ее данные скопированы из кодовой (текстовой) памяти, которая обычно доступна только для чтения (это зависит от реализации, компилятор может также поместить эти программные данные только для чтения в доступную для чтения память).).
Во втором случае p - это указатель, определенный в стеке (локальная область) и ссылающийся на строковый литерал (данные программы или текст), хранящиеся где-то еще. Обычно изменение такой памяти не является хорошей практикой и не поощряется.
Section 5.5 Character Pointers and Functions
из
K&R
также обсуждает эту тему:
Между этими определениями есть важное различие:
char amessage[] = "now is the time"; /* an array */
char *pmessage = "now is the time"; /* a pointer */
представляет собой массив, достаточно большой, чтобы содержать последовательность символов и
'\0'
это инициализирует его. Отдельные символы в массиве могут быть изменены, ноamessage
всегда будет относиться к одному и тому же хранилищу. С другой стороны,pmessage
указатель, инициализированный, чтобы указывать на строковую константу; указатель впоследствии может быть изменен, чтобы указывать в другом месте, но результат не определен, если вы попытаетесь изменить содержимое строки.
Первое означает нечто иное, чем вы думаете:
char *str = "string";
Это значит,
- Сохраните мою строку символов -> «строка»
- по адресу (который выбирается компилятором)
- Сохраните адрес, по которому хранится «строка», в переменной с именем
str
.
Затем вы пытаетесь изменить адрес , по которому хранится ваша строка «строка»:
str[0] = 'z'; // could be also written as *str = 'z'
Попробуйте простую программу
Вы можете доказать, что это правда, просто немного изменив свою программу.
Попробуйте скомпилировать и запустить следующее:
#include <stdio.h>
int main()
{
char *str = "string";
// str[0] = 'z'; // could be also written as *str = 'z'
// PRINT the value contained in str (it's an address - a pointer which points to "string")
printf("%p\n", str);
}
На моей машине результат:
Когда вы пытаетесь написатьstr[0]
с str[0] = 'z' вы пытаетесь сохранить адрес 'z' по адресу0x55e789c1d004
Ошибка сегментации возникает, когда вы обращаетесь к памяти, которая недоступна.
char *str
указатель на строку, которая не модифицируется (причина получения ошибки seg)..
в то время как char str[]
является массивом и может быть изменяемым
Первая - это одна постоянная строка, которую нельзя изменить. Второй - это массив с инициализированным значением, поэтому его можно изменить.