В чем разница между char s [] и char * s?
В C можно использовать строковый литерал в следующем объявлении:
char s[] = "hello";
или вот так:
char *s = "hello";
Так в чем же разница? Я хочу знать, что на самом деле происходит с точки зрения продолжительности хранения, как при компиляции, так и во время выполнения.
Ответы
Ответ 1
Разница здесь в том, что
char *s = "Hello world";
поместит "Hello world"
в части только для чтения в памяти и сделает указатель s
указателем на то, что любая операция записи в этой памяти незаконна.
Выполняя:
char s[] = "Hello world";
помещает литеральную строку в постоянную память и копирует строку в новую выделенную память в стеке. Таким образом, делая
s[0] = 'J';
легальным.
Ответ 2
Во-первых, в аргументах функций они в точности эквивалентны:
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()
указатель на этот массив - вместо этого сделайте копию с помощью strdup()
или аналогичного. Разумеется, если массив выделен в глобальной области, проблем нет.
Ответ 3
Это объявление:
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, называемая
s
, которая инициализируется местоположением первого символа в этом неназванном, доступном только для чтения массиве.
Неименованный доступный только для чтения массив обычно находится в сегменте "текст" программы, что означает, что он загружается с диска в постоянную память вместе с самим кодом. Расположение переменной указателя s
в памяти зависит от того, где появляется объявление (как в первом примере).
Ответ 4
Учитывая объявления
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
в С++) со статической продолжительностью хранения, что означает, что память для него выделяется, когда программа запускается и остается выделенной до тех пор, пока программа завершается. Попытка изменить содержимое строкового литерала вызывает поведение undefined.
Линия
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
, чтобы указать на другой массив.
Ответ 5
Тяга C99 N1256
Существует два различных варианта использования строковых литералов символов:
-
Инициализировать char[]
:
char c[] = "abc";
Это "больше волшебства" и описано в 6.7.8/14 "Инициализация":
Массив символьного типа может быть инициализирован литералом символьной строки, необязательно заключенным в фигурные скобки. Последовательные символы литерала символьной строки (включая завершающий нулевой символ, если есть место или массив имеет неизвестный размер) инициализируют элементы массива.
Так что это просто ярлык для:
char c[] = {'a', 'b', 'c', '\0'};
Как и любой другой обычный массив, c
может быть изменен.
-
Везде: генерирует:
Поэтому, когда вы пишете:
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
в .rodata
и тот же сегмент, который имеет исполняемый файл, но не имеет разрешения на запись. Это можно наблюдать с:
readelf -l a.out
который содержит:
Section to Segment mapping:
Segment Sections...
02 .text .rodata
Ответ 6
char s[] = "hello";
объявляет s
как массив char
, который достаточно длинный, чтобы удерживать инициализатор (5 + 1 char
s) и инициализирует массив, копируя члены данного строкового литерала в массив.
char *s = "hello";
объявляет s
указателем на один или несколько (в этом случае больше) char
и указывает его непосредственно в фиксированном (только для чтения) месте, содержащем литерал "hello"
.
Ответ 7
char s[] = "Hello world";
Здесь s
- это массив символов, который может быть перезаписан, если мы хотим.
char *s = "hello";
Строковый литерал используется для создания этих символьных блоков где-то в памяти, на которую указывает этот указатель s
. Мы можем переназначить объект, на который он указывает, изменив его, но пока он указывает на строковый литерал, блок символов, на которые он указывает, не может быть изменен.
Ответ 8
Просто добавьте: вы также получите разные значения для своих размеров.
printf("sizeof s[] = %zu\n", sizeof(s)); //6
printf("sizeof *s = %zu\n", sizeof(s)); //4 or 8
Как упоминалось выше, для массива '\0'
будет выделен конечный элемент.
Ответ 9
В качестве дополнения учтите, что для целей только для чтения использование обоих одинаково, вы можете получить доступ к char путем индексирования либо с помощью []
, либо *(<var> + <index>)
Формат:
printf("%c", x[1]); //Prints r
и
printf("%c", *(x + 1)); //Prints r
Очевидно, что если вы попытаетесь сделать
*(x + 1) = 'a';
Вероятно, вы получите ошибку сегментации, поскольку вы пытаетесь получить доступ к постоянной памяти.
Ответ 10
char *str = "Hello";
Вышеупомянутое устанавливает str для указания на буквальное значение "Hello", которое жестко закодировано в двоичном образе программы, которое помечено как доступное только для чтения в памяти, означает, что любое изменение в этом строковом литерале является незаконным, и это будет бросать ошибки сегментации.
char str[] = "Hello";
копирует строку в новую выделенную память в стеке. Таким образом, любые изменения в нем разрешены и законны.
means str[0] = 'M';
изменит str на "Mello".
Для получения более подробной информации, пожалуйста, перейдите к аналогичному вопросу:
Почему возникает ошибка сегментации при записи в строку, инициализированную с помощью "char * s" но не "char s []" ,
Ответ 11
В случае:
char *x = "fred";
x является lvalue - ему можно назначить. Но в случае:
char x[] = "fred";
x не является lvalue, это rvalue - вы не можете назначить ему.
Ответ 12
В свете комментариев здесь должно быть очевидно, что: char * s = "hello";
Это плохая идея, и ее следует использовать в очень узком пространстве.
Это может быть хорошей возможностью указать на то, что "const correctness" - это "хорошая вещь". Когда бы и где бы вы ни находились, используйте ключевое слово "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 ant, попадают во время компиляции. Для const не существует пространства времени выполнения или скорости.
(Предполагается, что вы используете компилятор С++, конечно?)
- DBJ
Ответ 13
char s[] = "Hello world";
является массивом символов, которые могут быть изменены, в то время как char *s = "hello";
указатель только для чтения.