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

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

char s[] = "hello";

или такой:

char *s = "hello";

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

12 ответов


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

char *s = "Hello world";

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

как делать:

char s[] = "Hello world";

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

s[0] = 'J';

юридические.


во-первых, в аргументах функции, они полностью эквивалентны:

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;

обратите внимание, что вы никогда не должны пытаться изменить содержимое этого анонимного массива с помощью этого указателя; эффекты не определены (часто означает crash):

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

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

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

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


это заявление:

char s[] = "hello";

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

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

char *s ="hello";

создает два объекты:

  • a только для чтения массив из 6 chars, содержащий значения 'h', 'e', 'l', 'l', 'o', '', который не имеет имени и имеет статическая продолжительность хранения (это означает, что она живет для вся жизнь программы); и
  • переменная типа указатель на символ, называется s, который инициализируется местоположением первого символа в этом неназванном массиве только для чтения.

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


учитывая заявления

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(), etc.).

в линия

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 чтобы указать на другой массив.


С99 проект N1256

существует два совершенно разных использования литералов массива:

  1. инициализации char[]:

    char c[] = "abc";      
    

    это "больше магии", и описанного в 6.7.8/14 "инициализация":

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

    так что это просто ярлык для:

    char c[] = {'a', 'b', 'c', ''};
    

    как и любой другой обычный массив, c может быть изменен.

  2. везде еще: он генерирует:

    поэтому, когда вы пишете:

    char *c = "abc";
    

    это похоже на:

    /* __unnamed is magic because modifying it gives UB. */
    static char __unnamed[] = "abc";
    char *c = __unnamed;
    

    обратите внимание на неявное приведение от char[] to 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', '' },
t[] = { 'a', 'b', 'c' };

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

char *p = "abc";

определяет p С типом " указатель на char "и инициализирует его, чтобы указать на объект с типом" массив char " длиной 4, элементы которого инициализируются символьным строковым литералом. Если сделана попытка использовать p изменить содержимое массива, поведение неопределено.

GCC 4.8 x86-64 реализация ELF

программа:

#include <stdio.h>

int main() {
    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   x0,-0x8(%rbp)
f:  00 
        c: R_X86_64_32S .rodata

заключение: магазины GCC char* его в , не на .text.

если мы сделаем то же самое для char[]:

 char s[] = "abc";

получаем:

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

поэтому он хранится в стеке (относительно %rbp).

обратите внимание, однако, что сценарий компоновщика по умолчанию ставит .rodata и .text в том же сегменте, который имеет разрешение execute, но без разрешения на запись. Это можно наблюдать с помощью:

readelf -l a.out

, который содержит:

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

char s[] = "hello";

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

char *s = "hello";

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


char s[] = "Hello world";

здесь s - это массив символов, который при желании можно перезаписать.

char *s = "hello";

строковый литерал используется для создания этих символьных блоков где-то в памяти, которые этот указатель s указывает на. Здесь мы можем переназначить объект, на который он указывает, изменив это, но пока он указывает на строковый литерал, блок символов, на который он указывает, не может быть изменен.


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

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

и:

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

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

*(x + 1) = 'a';

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


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

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

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


char *str = "Hello";

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

char str[] = "Hello";

копирует строку во вновь выделенную память в стеке. При этом внесение каких-либо изменений в него допускается и законно.

means str[0] = 'M';

изменит str на "Mello".

Для больше деталей, пожалуйста пройдите аналогичный вопрос:

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


по делу:

char *x = "fred";

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

char x[] = "fred";

x не является lvalue, это rvalue - вы не можете назначить ему.


в свете комментариев здесь должно быть очевидно, что: char * s = " привет" ; Это плохая идея,и ее следует использовать в очень узкой области.

Это может быть хорошей возможностью указать, что" правильность const "- это"хорошая вещь". Когда и где вы можете, используйте ключевое слово " 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