Чому я отримую помилку сегментації під час запису до "char * s", ініціалізованого рядковим буквалом, але не "char s []"?


288

Наступний код отримує seg fault у рядку 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.


1
Це смішно - але це насправді компілюється та працює ідеально при використанні компілятора Windows (cl) у візуальному командному рядку розробника візуальної студії. Мене розгубилось на кілька моментів ...
Девід Рефаелі

Відповіді:


242

Дивіться C FAQ, питання 1.32

З : Яка різниця між цими ініціалізаціями?
char a[] = "string literal";
char *p = "string literal";
Моя програма виходить з ладу, якщо я спробую призначити нове значення p[i].

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

  1. Як ініціалізатор масиву char, як у декларації char a[], він вказує початкові значення символів у цьому масиві (і, якщо потрібно, його розмір).
  2. У будь-якому іншому місці він перетворюється на безіменний, статичний масив символів, і цей безіменний масив може зберігатися в пам'яті, доступній лише для читання, і тому не обов'язково може бути змінений. У контексті вираження масив одразу перетворюється на покажчик, як зазвичай (див. Розділ 6), тому друге оголошення ініціалізує p, щоб вказувати на перший елемент безіменного масиву.

Деякі компілятори мають перемикач, який керує тим, чи строкові літерали можуть бути записані чи ні (для компіляції старого коду), а деякі можуть мати параметри, щоб змусити рядкові літерали формально трактуватись як масиви const char (для кращого виявлення помилок).


7
Пара інших моментів: (1) сегментація за замовчуванням відбувається так, як описано, але її виникнення є функцією середовища виконання; якщо той самий код був у вбудованій системі, запис може не мати ефекту, або він може фактично змінити s на z. (2) Оскільки літеральні рядки не записуються, компілятор може заощадити простір, поставивши два екземпляри "string" в одне і те ж місце; або, якщо десь ще в коді у вас є "інший рядок", то один шматок пам'яті може підтримувати обидва літерали. Зрозуміло, що якщо коду було дозволено змінити ці байти, можуть виникнути дивні та складні помилки.
greggo

1
@greggo: Добре. Існує також спосіб зробити це в системах з MMU, використовуючи mprotectхвильовий захист лише для читання (див. Тут ).

Отже char * p = "blah" насправді створює тимчасовий масив? Дивно.
rahul tiagi

1
І через 2 роки написання на C ++ ... TIL
zeboidlund

@rahultyagi, що ти маєш на увазі?
Сурай Джайн

105

Зазвичай рядкові літерали зберігаються в пам'яті лише для читання при запуску програми. Це запобігає випадковому зміні рядкової константи. У вашому першому прикладі "string"він зберігається в пам'яті лише для читання і *strвказує на перший символ. Сегмент за замовчуванням відбувається, коли ви намагаєтесь змінити перший символ на 'z'.

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

printf("%p", str);

Також друк розміру strу другому прикладі покаже вам, що компілятор виділив для нього 7 байтів:

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

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

4
Також дужки з sizeof потрібні лише при прийнятті розміру типу (аргумент тоді виглядає як літ). Пам'ятайте, що sizeof - це оператор, а не функція.
розмотуємось


34

Більшість із цих відповідей є правильними, але просто для додання трохи більше ясності ...

"Пам'ять лише для читання", на яку люди посилаються, - це текстовий сегмент в термінах ASM. Це те саме місце в пам'яті, де завантажуються інструкції. Це доступно лише для читання з очевидних причин, таких як безпека. Коли ви створюєте char *, ініціалізований до рядка, рядкові дані збираються в текстовий сегмент і програма ініціалізує вказівник, щоб вказати на текстовий сегмент. Тож якщо ви спробуєте це змінити, kaboom. За замовчуванням.

Якщо він записується як масив, компілятор замість цього розміщує ініціалізовані рядкові дані у сегменті даних. Це місце, де перебувають ваші глобальні змінні та подібні. Ця пам'ять є вичерпною, оскільки в сегменті даних немає інструкцій. Цього разу, коли компілятор ініціалізує масив символів (який все ще є лише символом *), він вказує на сегмент даних, а не на текстовий сегмент, який ви можете сміливо змінювати під час виконання.


Але чи не правда, що можуть бути реалізації, які дозволяють змінювати "пам'ять лише для читання"?
Pacerier

Якщо він записується як масив, компілятор розміщує ініціалізовані рядкові дані у сегменті даних, якщо вони статичні чи глобальні. В іншому випадку (наприклад, для звичайного автоматичного масиву) він розміщується на стеку, в кадрі стека основної функції. Правильно?
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";

визначає "прості" об'єкти масиву char sта tелементи яких ініціалізуються літеральними рядками символів.

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

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

Вміст масивів може змінюватися. З іншого боку, декларація

char *p = "abc";

визначає pз типом "покажчик на char" та ініціалізує його для вказівки на об'єкт типу "масив char" довжиною 4, елементи якого ініціалізовані літеральним рядком символів. Якщо буде здійснена спроба pзмінити вміст масиву, поведінка не визначена.

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

Програма:

#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 * (навіть якщо ви призначаєте його до non-con char *), це означає, що не слід намагатися писати на нього.

Компілятор застосував це, поставивши рядок у частину пам'яті, лише для читання, і, отже, запис до неї генерує сегмент за замовчуванням.


8

Щоб зрозуміти цю помилку або проблему, слід спершу знати різницю b / w вказівник і масив, тому тут по-перше, я поясню вам відмінності, б / в їх

рядковий масив

 char strarray[] = "hello";

У масиві пам’яті зберігається в комірках безперервної пам’яті, зберігається так, як [h][e][l][l][o][\0] =>[]це осередок пам’яті розміром 1 байт, і до цієї комірки безперервної пам’яті можна отримати доступ за ім’ям, названим тут strarraystrarray. У цьому випадку "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"не може бути змінений, оскільки він зберігається в ПЗУ

і ptr зберігається в розділі стека і вказує на постійний рядок "hello"

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

Але ptr можна ініціалізувати безпосередньо на іншому рядковому значенні, оскільки це просто вказівник, тому він може вказувати на будь-яку адресу пам'яті змінної свого типу даних

ptr="new string"; is valid

7
char *str = "string";  

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

Так str[0]=робиться спроба записати код, доступний лише для читання. Я б припустив, що це, мабуть, залежить від компілятора.


6
char *str = "string";

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

char str[] = "string";

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


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

6

Поширений запит на C, який @matli посилається на нього, згадує, але тут ще ніхто не має, тому для уточнення: якщо рядковий літерал (рядок з подвійним цитуванням у вашому джерелі) використовується деінде, ніж для ініціалізації масиву символів (тобто: @ Другий приклад Марка, який працює правильно), що рядок зберігається компілятором у спеціальній таблиці статичних рядків , яка схожа на створення глобальної статичної змінної (звичайно лише для читання), яка по суті є анонімною (не має змінної "ім'я "). Частина лише для читання є важливою частиною, і тому перший приклад коду @ Марк segfaults.


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

4

The

 char *str = "string";

рядок визначає вказівник і вказує його на буквальний рядок. Буквальний рядок не піддається запису, тому коли ви робите:

  str[0] = 'z';

ви отримуєте провину в сегменті. На деяких платформах літерал може знаходитися в пам'яті, що записується, тому ви не побачите сегментації, але це недійсний код (що призводить до невизначеної поведінки) незалежно.

Лінія:

char str[] = "string";

виділяє масив символів і копіює буквальний рядок у цей масив, який є повністю записаним, тому подальше оновлення не є проблемою.


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

3

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

У першому прикладі ви отримуєте вказівник на ці дані const. У другому прикладі ви ініціалізуєте масив із 7 символів з копією даних const.


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. 

1

По-перше, strце вказівник, на який вказує "string". Компілятору дозволено розміщувати рядкові літерали в місцях пам'яті, до яких ви не можете записати, а лише читати. (Це дійсно повинно викликати попередження, оскільки ви призначаєте const char *аchar * . Хіба ви відключили попередження, або ж ви просто ігнорувати їх?)

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


@Ferruccio,? Так, constпрефікс робить змінні лише для читання
EsmaeelE

У лінійних рядках C є тип char [N], ні const char [N], тому немає попередження. (Ви можете змінити це в gcc принаймні, проходячи -Wwrite-strings.)
Мельпомена

0

Припустимо, рядки є,

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

У першому випадку літерал повинен бути скопійований, коли «a» потрапляє в область застосування. Тут 'a' - це масив, визначений у стеці. Це означає, що рядок буде створений на стеку, а його дані скопійовано з кодової (текстової) пам’яті, яка, як правило, є лише для читання (це специфічно для реалізації, компілятор може розмістити ці програмні дані лише для читання в пам'яті, що читається для запису ).

У другому випадку, p - покажчик, визначений у стеці (локальна область) та посилається на рядковий літерал (програмні дані чи текст), який зберігається в іншому місці. Зазвичай модифікація такої пам'яті не є хорошою практикою і не рекомендується.


-1

По-перше, це одна постійна рядок, яку неможливо змінити. Другий - це масив з ініціалізованим значенням, тому його можна модифікувати.


-2

Помилка сегментації виникає при спробі отримати доступ до пам'яті, яка недоступна.

char *str є вказівником на рядок, який неможливо змінити (причина отримання segfault).

тоді char str[]як це масив і може бути змінений ..

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.