В чем разница между char s [] и char * s?

506

В C можно использовать строковый литерал в объявлении, например:

char s[] = "hello";

или вот так:

char *s = "hello";

Так в чем же разница? Я хочу знать, что на самом деле происходит с точки зрения продолжительности хранения, как во время компиляции, так и во время выполнения.

Рассказчик - Unslander Monica
источник
8
char * s = "hello", здесь s может указывать любую другую строку во время выполнения. Я имею в виду, что это не константный указатель, вы можете присвоить другое значение во время выполнения p = "Nishant", в то время как s [] здесь s - постоянный указатель .. ..it нельзя переназначить другую строку, но мы можем назначить другое символьное значение в s [index].
Нишант Кумар

Ответы:

541

Разница здесь в том, что

char *s = "Hello world";

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

При этом:

char s[] = "Hello world";

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

s[0] = 'J';

законны.

Рикард
источник
22
Буквенная строка "Hello world"находится в «доступной только для чтения части памяти» в обоих примерах. Пример с массивом указывает на него, пример с массивом копирует символы в элементы массива.
pmg
28
pmg: во втором случае буквальная строка вовсе не обязательно существует в памяти как отдельный смежный объект - это всего лишь инициализатор, компилятор вполне резонно может генерировать серию инструкций «загрузить немедленный байт», которые содержат значения символов, встроенные в их.
Кафе
10
Пример массива char не обязательно помещает строку в стек - если она появляется на уровне файла, она, вероятно, будет находиться в каком-то сегменте инициализированных данных.
Кафе
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()или аналогичным образом. Если массив размещен в глобальной области видимости, конечно, проблем нет.

bdonlan
источник
72

Эта декларация:

char s[] = "hello";

Создает один объект - charмассив размером 6, называемый s, инициализированный значениями 'h', 'e', 'l', 'l', 'o', '\0'. От того, где этот массив размещен в памяти, и как долго он живет, зависит от того, где появляется объявление. Если объявление находится внутри функции, оно будет жить до конца блока, в котором оно объявлено, и почти наверняка будет размещено в стеке; если он находится вне функции, он, вероятно, будет храниться в «инициализированном сегменте данных», который загружается из исполняемого файла в доступную для записи память при запуске программы.

С другой стороны, это объявление:

char *s ="hello";

Создает два объекта:

  • доступный только для чтения массив из 6 charс, содержащий значения 'h', 'e', 'l', 'l', 'o', '\0', который не имеет имени и имеет статическую продолжительность хранения (что означает, что он живет в течение всей жизни программы); а также
  • переменная типа pointer-to-char, named s, которая инициализируется расположением первого символа в этом безымянном массиве только для чтения.

Безымянный массив только для чтения обычно находится в «текстовом» сегменте программы, что означает, что он загружается с диска в постоянную память вместе с самим кодом. Расположение sпеременной-указателя в памяти зависит от того, где появляется объявление (как в первом примере).

кафе
источник
1
В обеих декларациях для "hello" память выделяется во время компиляции? = "привет", он также будет сначала сохранять в части текстового сегмента, и во время выполнения он будет копироваться в стеке, как сказал Рикард в ответе. пожалуйста, уточните этот момент.
Нишант Кумар
2
@Nishant: В данном char s[] = "hello"случае, "hello"это просто инициализатор, сообщающий компилятору, как должен быть инициализирован массив. Это может приводить или не приводить к соответствующей строке в текстовом сегменте - например, если sимеет статическую продолжительность хранения, тогда, вероятно, что единственный экземпляр "hello"будет в инициализированном сегменте данных - сам объект s. Даже если он sимеет автоматическую продолжительность хранения, он может быть инициализирован последовательностью литеральных хранилищ, а не копией (например, movl $1819043176, -6(%ebp); movw $111, -2(%ebp)).
Кафе
Более точно, GCC 4.8 помещает его в .rodata, который скрипт линкера затем сбрасывает в том же сегменте, .text. Смотри мой ответ .
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
@caf В первом ответе Рикарда написано, что текстовая char s[] = "Hello world";строка помещается в постоянную память и копируется в новую выделенную память в стеке. Но ваш ответ говорит только о строковом положить в памяти только для чтения и пропускает вторую часть предложения , которая говорит: copies the string to newly allocated memory on the stack. Итак, является ли ваш ответ неполным, если не указать вторую часть?
KPMG
1
@AjaySinghNegi: Как я уже говорил в других комментариях (к этому ответу и ответу Рикарда), строка in char s[] = "Hellow world";является только инициализатором и вовсе не обязательно хранится как отдельная копия только для чтения. Если sимеет статическую длительность хранения, то единственная копия строки, скорее всего, будет находиться в сегменте чтения-записи в месте s, и даже если нет, то компилятор может выбрать инициализацию массива с помощью инструкций немедленной загрузки или аналогичного, а не копирования из строки только для чтения. Дело в том, что в этом случае сама строка инициализатора не присутствует во время выполнения.
кафе
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
Я думаю, что гипотетическая карта памяти облегчает понимание!
MidnightBlue
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";

определяет понятие «простые» объекты массив символов 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.

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

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
источник
15
char s[] = "hello";

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

char *s = "hello";

объявляет sуказатель на один или несколько (в данном случае больше) charи указывает его непосредственно на фиксированное (только для чтения) местоположение, содержащее литерал "hello".

CB Bailey
источник
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указывает этот указатель . Здесь мы можем переназначить объект, на который он указывает, изменив его, но пока он указывает на строковый литерал, блок символов, на который он указывает, не может быть изменен.

Sailaja
источник
@bo Persson Почему блок символов не может быть изменен во втором случае?
Панкадж Махато
3

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

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

А также:

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

Очевидно, если вы попытаетесь сделать

*(x + 1) = 'a';

Вы, вероятно, получите ошибку сегментации, так как вы пытаетесь получить доступ к постоянной памяти.

Ник Лулудакис
источник
Это ни в коем случае не отличается от того, x[1] = 'a';что будет происходить и с segfault (конечно, в зависимости от платформы).
glglgl
3

Просто добавлю: вы также получите разные значения для их размеров.

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

Как уже упоминалось выше, для массива '\0'будет выделен последний элемент.

Muzab
источник
2
char *str = "Hello";

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

char str[] = "Hello";

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

means str[0] = 'M';

изменит улицу на "Мелло".

Для более подробной информации, пожалуйста, перейдите к аналогичному вопросу:

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

Мохит
источник
0

На случай, если:

char *x = "fred";

x является lvalue - его можно назначить. Но в случае:

char x[] = "fred";

x не lvalue, это rvalue - вы не можете присвоить ему.

Lee-Man
источник
3
Технически, xэто неизменяемое значение lvalue. Тем не менее, почти во всех контекстах он будет указывать на свой первый элемент, и это значение является значением r.
Кафе
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 = "hello"; Это плохая идея, и ее следует использовать в очень узких рамках.

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

Достаточно мелодрамы, вот чего можно добиться, украсив указатели «const». (Примечание. Нужно читать объявления указателей справа налево.) Вот 3 различных способа защитить себя при игре с указателями:

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-ant, обнаруживаются во время компиляции. Для const нет места во время выполнения или скорости.

(Предполагается, что вы используете компилятор C ++, конечно?)

--DBJ


источник
Это все правильно, но это не имеет ничего общего с вопросом. Что касается вашего предположения о компиляторе C ++, вопрос помечен как C, а не как C ++.
Фабио говорит восстановить Монику
В char * s = "const string" нет ничего плохого;
Пол Смит