В чем разница между 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;
Обратите внимание, что вы никогда не должны пытаться изменить содержимое этого анонимного массива с помощью этого указателя; эффекты не определены (часто означают сбой):
x[1] = 'O'; // BAD. DON'T DO THIS.
Использование синтаксиса массива напрямую выделяет его в новую память. Таким образом, модификация безопасна:
char x[] = "Foo";
x[1] = 'O'; // No problem.
Тем не менее, массив живет только до тех пор, пока его окружающая область, поэтому, если вы делаете это в функции, не возвращайте и не просачивайте указатель на этот массив - вместо этого сделайте копию с strdup()
или похожие. Если массив размещен в глобальной области видимости, конечно, проблем нет.
Эта декларация:
char s[] = "hello";
Создает один объект - char
массив размером 6, называется s
, инициализируется со значениями 'h', 'e', 'l', 'l', 'o', '\0'
, От того, где этот массив размещен в памяти, и как долго он живет, зависит от того, где появляется объявление. Если объявление находится внутри функции, оно будет жить до конца блока, в котором оно объявлено, и почти наверняка будет размещено в стеке; если он находится вне функции, он, вероятно, будет храниться в "инициализированном сегменте данных", который загружается из исполняемого файла в доступную для записи память при запуске программы.
С другой стороны, это объявление:
char *s ="hello";
Создает два объекта:
- доступный только для чтения массив из 6
char
s, содержащие значения'h', 'e', 'l', 'l', 'o', '\0'
, который не имеет имени и имеет статическую продолжительность хранения (это означает, что он живет в течение всей жизни программы); а также - переменная типа указатель на символ, называется
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()
, так далее.).
Линия
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
указать на другой массив.
C99 N1256 осадка
Существует два различных варианта использования строковых литералов символов:
инициализировать
char[]
:char c[] = "abc";
Это "больше волшебства", и описано в 6.7.8/14 "Инициализация":
Массив символьного типа может быть инициализирован литералом символьной строки, необязательно заключенным в фигурные скобки. Последовательные символы литерала символьной строки (включая завершающий нулевой символ, если есть место или массив имеет неизвестный размер) инициализируют элементы массива.
Так что это просто ярлык для:
char c[] = {'a', 'b', 'c', '\0'};
Как и любой другой обычный массив,
c
можно изменить.Везде: генерирует:
- неназванный
- массив символов Какого типа строковые литералы в C и C++?
- со статическим хранилищем
- который дает UB, если модифицирован
Поэтому, когда вы пишете:
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
в том же сегменте, который имеет выполнить, но нет разрешения на запись. Это можно наблюдать с помощью:
readelf -l a.out
который содержит:
Section to Segment mapping:
Segment Sections...
02 .text .rodata
char s[] = "hello";
объявляет s
быть массивом char
что достаточно долго, чтобы держать инициализатор (5 + 1 char
s) и инициализирует массив путем копирования членов данного строкового литерала в массив.
char *s = "hello";
объявляет s
быть указателем на один или несколько (в данном случае больше) char
s и указывает его непосредственно на фиксированное (только для чтения) местоположение, содержащее литерал "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
Как уже упоминалось выше, для массива '\0'
будет выделен как последний элемент.
Пример различия:
printf("hello" + 2); //llo
char a[] = "hello" + 2; //error
В первом случае работает арифметика указателей (переданные в функцию массивы распадаются на указатели).
char *str = "Hello";
Выше указано, что str указывает на буквальное значение "Hello", которое жестко запрограммировано в двоичном изображении программы, которое помечено как доступное только для чтения в памяти, означает, что любое изменение в этом строковом литерале является недопустимым, и это приведет к ошибкам сегментации.
char str[] = "Hello";
копирует строку во вновь выделенную память в стеке. Таким образом, любое изменение в нем разрешено и законно.
means str[0] = 'M';
изменит улицу на "Мелло".
Для более подробной информации, пожалуйста, перейдите к аналогичному вопросу:
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
В случае:
char *x = "fred";
x является lvalue - его можно назначить. Но в случае:
char x[] = "fred";
x не lvalue, это rvalue - вы не можете присвоить ему.
В свете комментариев здесь должно быть очевидно, что: 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