Чим відрізняється char s [] від char * s?


506

У C можна використовувати літеральний рядок у декларації, як це:

char s[] = "hello";

або так:

char *s = "hello";

То в чому різниця? Хочу знати, що насправді відбувається з точки зору тривалості зберігання, як під час компіляції, так і під час виконання.



8
char * s = "привіт", тут s може вказувати будь-який інший рядок під час виконання. Я маю на увазі, що це не постійний покажчик, ви можете призначити інше значення під час виконання p = "Nishant", тоді як s [] тут s - постійний покажчик .. .. це не може бути повторно призначити інший рядок, але ми можемо призначити інше значення символу в s [index].
Нішант Кумар

Відповіді:


541

Різниця тут полягає в тому, що

char *s = "Hello world";

розміщуватиметься "Hello world"в пам'яті лише для читання і робить sвказівник на це, що робить будь-яку операцію запису на цій пам'яті незаконною.

Під час виконання:

char s[] = "Hello world";

поміщає буквальний рядок у пам'ять лише для читання та копіює рядок у щойно виділену пам'ять на стек. Таким чином роблячи

s[0] = 'J';

юридичний.


22
У "Hello world"обох прикладах буквальна рядок знаходиться в "частинах пам'яті, які доступні лише для читання". Приклад з точками масиву є, приклад з масивом копіює символи в елементи масиву.
pmg

28
pmg: У другому випадку буквальна рядок зовсім не обов'язково існує в пам'яті як єдиний суміжний об'єкт - це лише ініціалізатор, компілятор міг би досить резонно випромінювати серію інструкцій "завантажувати негайний байт", що містять знаки символів, вкладені в їх.
caf

10
Приклад масиву char не обов'язково розміщує рядок у стеці - якщо він з'являється на рівні файлу, він, ймовірно, буде натомість у якомусь ініціалізованому сегменті даних.
caf

9
Я хотів би зазначити, що char s = "xx" не повинен знаходитися в пам'яті лише для читання (наприклад, у деяких реалізаціях немає MMU). У чернетці n1362 c1x просто зазначено, що зміна такого масиву спричиняє не визначене поведінку. Але +1 у будь-якому разі, оскільки покладатися на таку поведінку - дурно.
paxdiablo

3
Я отримую чисту компіляцію у файлі, що містить лише char msg[] = "hello, world!"; рядок, що закінчується в ініціалізованому розділі даних. Після оголошення char * constв розділі даних, доступних лише для читання. gcc-4.5.3
gcbenison

152

По-перше, у аргументах функцій вони рівнозначні:

void foo(char *x);
void foo(char x[]); // exactly the same in all respects

В інших контекстах char *виділяє вказівник, а char []виділяє масив. Куди йдеш рядок у колишньому випадку, запитаєш ти? Компілятор таємно виділяє статичний анонімний масив для утримання рядкового літералу. Тому:

char *x = "Foo";
// is approximately equivalent to:
static const char __secret_anonymous_array[] = "Foo";
char *x = (char *) __secret_anonymous_array;

Зауважте, що ви ніколи не повинні намагатися змінювати вміст цього анонімного масиву за допомогою цього вказівника; наслідки не визначені (часто означає збій):

x[1] = 'O'; // BAD. DON'T DO THIS.

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

char x[] = "Foo";
x[1] = 'O'; // No problem.

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


72

Ця декларація:

char s[] = "hello";

Створює один об’єкт - charмасив розміром 6, що називається s, ініціалізований зі значеннями 'h', 'e', 'l', 'l', 'o', '\0'. Де цей масив розміщений у пам'яті та як довго він живе, залежить від місця появи декларації. Якщо декларація знаходиться в межах функції, вона проживе до кінця блоку, в якому вона оголошена, і майже напевно буде виділена на стек; якщо вона знаходиться поза функцією, вона, ймовірно, зберігатиметься в "ініціалізованому сегменті даних", який завантажується з виконуваного файлу в пам'ять, що записується, коли програма запускається.

З іншого боку, ця декларація:

char *s ="hello";

Створює два об'єкти:

  • тільки для читання масив з 6 charз , що містить значення 'h', 'e', 'l', 'l', 'o', '\0', які не мають імені і мають статичну тривалість зберігання ( це означає , що він живе в протягом усього терміну дії програми); і
  • змінна типу вказівника на char, що називається s, яка ініціалізується з розташуванням першого символу в цьому неназваному масиві, доступному лише для читання.

Безіменний масив лише для читання, як правило, розташований у сегменті програми "текст", що означає, що він завантажується з диска в пам'ять, доступну лише для читання, разом із самим кодом. Розташування sзмінної вказівника в пам'яті залежить від того, де з’являється декларація (як і в першому прикладі).


1
В обох деклараціях для "привіт" пам'ять виділяється в загальний час? = "Привіт", він також зберігатиметься спочатку в частині сегменту тексту, а під час виконання він буде копіювати у стек, як відповів Рікард. уточніть, будь ласка, цей пункт.
Нішант Кумар

2
@Nishant: У цьому char s[] = "hello"випадку, "hello"це просто ініціалізатор, який повідомляє компілятору, як масив повинен бути ініціалізований. Це може або не може призвести до відповідного рядка в текстовому сегменті - наприклад, якщо у sнього є статична тривалість зберігання, то, ймовірно, єдиний екземпляр "hello"буде в ініціалізованому сегменті даних - сам об'єкт s. Навіть якщо sавтоматична тривалість зберігання може бути ініціалізована послідовністю буквальних сховищ, а не копією (наприклад, movl $1819043176, -6(%ebp); movw $111, -2(%ebp)).
caf

Точніше, GCC 4.8 розміщує його, в .rodataякому сценарій посилання потім скидається в той же сегмент, що і .text. Дивіться мою відповідь .
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

@caf У першій відповіді Рікарда написано, що char s[] = "Hello world";вводить буквальний рядок у пам'ять лише для читання та копіює рядок у щойно виділену пам'ять на стек. Але ваша відповідь говорить тільки про строковому покласти в пам'яті тільки для читання і пропускає другу частину пропозиції , яка говорить: copies the string to newly allocated memory on the stack. Отже, чи є ваша відповідь неповною для невказання другої частини?
KPMG

1
@AjaySinghNegi: Як я вже заявляв в інших коментарях (до цієї відповіді та відповіді Рікарда), рядок char s[] = "Hellow world";є лише ініціалізатором і не обов'язково зберігається як окрема копія лише для читання. Якщо sмає статичну тривалість зберігання, то, можливо, єдина копія рядка знаходиться в сегменті читання-запису в місці розташування s, і навіть якщо ні, тоді компілятор може вибрати ініціалізацію масиву з безпосередніми інструкціями завантаження або подібними, а не копіюванням. з рядка лише для читання. Справа в тому, що в цьому випадку рядок ініціалізатора сам по собі не має часу виконання.
caf

60

З огляду на декларації

char *s0 = "hello world";
char s1[] = "hello world";

припустимо наступну гіпотетичну карту пам'яті:

                    0x01 0x02 0x03 0x04
        0x00008000: 'h' 'e' 'l' 'l'
        0x00008004: 'o' '' 'w' 'o
        0x00008008: 'r' 'l' 'd' 0x00
        ...
s0: 0x00010000: 0x00 0x00 0x80 0x00
s1: 0x00010004: 'h' 'e' 'l' 'l'
        0x00010008: 'o' '' 'w' 'o
        0x0001000C: 'r' 'l' 'd' 0x00

Літеральний рядок "hello world"- це 12-елементний масив char( const charу C ++) зі статичною тривалістю зберігання, тобто пам'ять для нього виділяється при запуску програми і залишається виділеною до завершення програми. Спроба змінити вміст рядкового літералу викликає невизначене поведінку.

Лінія

char *s0 = "hello world";

визначає s0як покажчик на charтривалість автоматичного зберігання (означає, що змінна s0існує лише для області, в якій вона оголошена) і копіює адресу рядкового літералу ( 0x00008000у цьому прикладі) до нього. Слід зазначити , що , оскільки s0вказує на рядок литерала, вона не повинна бути використана в якості аргументу для будь-якої функції , яка буде намагатися змінити його (наприклад, strtok(), strcat(), strcpy()і т.д.).

Лінія

char s1[] = "hello world";

визначає s1як 12-елементний масив char(довжина взята з рядкового літералу) з тривалістю автоматичного зберігання та копіює вміст літералу в масив. Як видно з карти пам'яті, у нас є дві копії рядка "hello world"; Різниця полягає в тому, що ви можете змінити рядок, що міститься в s1.

s0і s1є взаємозамінними у більшості контекстів; ось винятки:

sizeof s0 == sizeof (char*)
sizeof s1 == 12

type of &s0 == char **
type of &s1 == char (*)[12] // pointer to a 12-element array of char

Ви можете перепризначити змінну s0для вказівки на інший рядковий літерал або іншу змінну. Ви не можете перепризначити змінну s1для вказівки на інший масив.


2
Я думаю, що гіпотетична карта пам’яті дозволяє легко зрозуміти!
опівночіБлаки

32

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.

Однак зауважте, що сценарій посилання за замовчуванням розміщує .rodataі .textв тому ж сегменті , який має виконати, але не має дозволу на запис. Це можна спостерігати за:

readelf -l a.out

який містить:

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

Якщо ми робимо те саме для char[]:

 char s[] = "abc";

ми отримуємо:

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

тому він зберігається в стеку (щодо %rbp).


15
char s[] = "hello";

оголошує sмасив, charякий достатньо довгий, щоб утримувати ініціалізатор (5 + 1 charс), і ініціалізує масив, копіюючи члени даного рядкового літералу в масив.

char *s = "hello";

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


1
Який метод краще використовувати у функціях, якщо s не буде змінено, f (const char s []) або f (const char * s)?
психоделія

1
@psihodelia: У оголошенні функції немає різниці. В обох випадках sвказівник на const char.
CB Bailey

4
char s[] = "Hello world";

Тут sпредставлений масив символів, який за бажанням можна перезаписати.

char *s = "hello";

Строковий літерал використовується для створення цих символьних блоків десь у пам'яті, на яку вказує цей вказівник s. Тут ми можемо перепризначити об'єкт, на який він вказує, змінивши це, але доки він вказує на рядковий буквал, блок символів, на який він вказує, не може бути змінений.


@bo Persson Чому блок символів не можна змінити у другому випадку?
Панкай Махато

3

Як додаток, врахуйте, що, як для цілей, лише для читання, використання обох ідентичне, ви можете отримати доступ до знака, індексуючи []або *(<var> + <index>) формат:

printf("%c", x[1]);     //Prints r

І:

printf("%c", *(x + 1)); //Prints r

Очевидно, якщо ви намагаєтеся зробити це

*(x + 1) = 'a';

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


Це ні в чому не відрізняється від того, x[1] = 'a';що буде також сегментарно (залежно від платформи, звичайно).
glglgl

3

Просто додам: ви також отримуєте різні значення для їх розмірів.

printf("sizeof s[] = %zu\n", sizeof(s));  //6
printf("sizeof *s  = %zu\n", sizeof(s));  //4 or 8

Як було сказано вище, для масиву '\0'буде виділено як завершальний елемент.


2
char *str = "Hello";

Вищезазначене встановлює str на вказівку на буквальне значення "Hello", яке жорстко закодирується у бінарному зображенні програми, яке позначено як пам'ять лише для читання, означає, що будь-яка зміна цього літерального рядка є незаконною і може призвести до помилок сегментації.

char str[] = "Hello";

копіює рядок у щойно виділену пам'ять у стеку. Таким чином, внесення будь-яких змін до неї дозволено та законно.

means str[0] = 'M';

змінить str на "Mello".

Для отримання детальної інформації, будь ласка, перегляньте подібне запитання:

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


0

У випадку:

char *x = "fred";

x - значення - воно може бути призначене. Але у випадку:

char x[] = "fred";

x - не значення, це значення, яке ви не можете призначити.


3
Технічно xце не змінне значення. Практично у всіх контекстах вона буде оцінюватись вказівником на її перший елемент, і це значення є оцінкою.
caf

0
char *s1 = "Hello world"; // Points to fixed character string which is not allowed to modify
char s2[] = "Hello world"; // As good as fixed array of characters in string so allowed to modify

// s1[0] = 'J'; // Illegal
s2[0] = 'J'; // Legal

-1

У світлі коментарів тут повинно бути очевидно, що: char * s = "привіт"; Це погана ідея, і її слід використовувати в дуже вузьких межах.

Це може бути гарною можливістю зазначити, що "правильність конкуренції" - це "добра річ". Коли і де можна, використовуйте ключове слово "const", щоб захистити свій код, від "розслаблених" абонентів або програмістів, які зазвичай найбільш "розслаблені", коли вказівники вступають у гру.

Досить мелодрами, ось чого можна досягти, прикрашаючи покажчики «const». (Примітка. Треба читати декларації вказівників праворуч ліворуч.) Ось три різні способи захистити себе під час гри з покажчиками:

const DBJ* p means "p points to a DBJ that is const" 

- тобто об'єкт DBJ неможливо змінити через p.

DBJ* const p means "p is a const pointer to a DBJ" 

- тобто ви можете змінити об’єкт DBJ через p, але ви не можете змінити сам вказівник p.

const DBJ* const p means "p is a const pointer to a const DBJ" 

- тобто ви не можете змінити сам вказівник p, а також не можете змінити об'єкт DBJ через p.

Помилки, пов’язані зі спробами мутацій протистояння, виявляються під час компіляції. Для const немає місця для виконання або штрафу за швидкість.

(Припущення, звичайно, ви використовуєте компілятор C ++?)

--DBJ


Це все правильно, але це не має нічого спільного з питанням. Що стосується ваших припущень щодо компілятора C ++, питання позначається як C, а не як C ++.
Фабіо каже: Відновити Моніку

Немає нічого поганого в char * s = "const string";
Пол Сміт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.