C: различия между указателем на символ и массивом
Рассматривать:
char amessage[] = "now is the time";
char *pmessage = "now is the time";
Я прочитал из The C Programming Language, 2nd Edition, что два приведенных выше утверждения не делают одно и то же.
Я всегда думал, что массив - это удобный способ манипулировать указателями для хранения некоторых данных, но это явно не тот случай. Каковы "нетривиальные" различия между массивами и указателями в C?
14 ответов
Правда, но это тонкая разница. По сути, первый:
char amessage[] = "now is the time";
Определяет массив, члены которого находятся в пространстве стека текущей области, тогда как:
char *pmessage = "now is the time";
Определяет указатель, который находится в пространстве стека текущей области, но который ссылается на память в другом месте (в этом "сейчас время" хранится в другом месте в памяти, обычно это строковая таблица).
Кроме того, обратите внимание, что поскольку данные, принадлежащие второму определению (явный указатель), не хранятся в пространстве стека текущей области, они не определены точно, где они будут храниться, и их не следует изменять.
Редактировать: Как отмечают Марк, GMan и Павел, есть также разница, когда оператор адреса используется для любой из этих переменных. Например, &pmessage возвращает указатель типа char** или указатель на указатель на символы, тогда как &amessage возвращает указатель типа char(*)[16], или указатель на массив из 16 символов (который, как символ **, должен быть разыменован в два раза, как указывает символ).
Вот гипотетическая карта памяти, показывающая результаты двух объявлений:
0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
0x00008000: 'n' 'o' 'w' ' ' 'i' 's' ' ' 't'
0x00008008: 'h' 'e' ' ' 't' 'i' 'm' 'e' '\0'
...
amessage:
0x00500000: 'n' 'o' 'w' ' ' 'i' 's' ' ' 't'
0x00500008: 'h' 'e' ' ' 't' 'i' 'm' 'e' '\0'
pmessage:
0x00500010: 0x00 0x00 0x80 0x00
Строковый литерал "сейчас время" хранится в виде 16-элементного массива char по адресу памяти 0x00008000. Эта память не может быть доступна для записи; лучше предположить, что это не так. Вы никогда не должны пытаться изменить содержимое строкового литерала.
Декларация
char amessage[] = "now is the time";
выделяет 16-элементный массив char по адресу памяти 0x00500000 и копирует содержимое строкового литерала в него. Эта память доступна для записи; Вы можете изменить содержание сообщения на свое сердце:
strcpy(amessage, "the time is now");
Декларация
char *pmessage = "now is the time";
выделяет один указатель на символ в адресе памяти 0x00500010 и копирует адрес строкового литерала в него.
Поскольку pmessage указывает на строковый литерал, его не следует использовать в качестве аргумента для функций, которым необходимо изменить содержимое строки:
strcpy(amessage, pmessage); /* OKAY */
strcpy(pmessage, amessage); /* NOT OKAY */
strtok(amessage, " "); /* OKAY */
strtok(pmessage, " "); /* NOT OKAY */
scanf("%15s", amessage); /* OKAY */
scanf("%15s", pmessage); /* NOT OKAY */
и так далее. Если вы изменили pmessage, чтобы указать на amessage:
pmessage = amessage;
тогда это может использоваться везде, где можно использовать amessage.
Массив содержит элементы. Указатель указывает на них.
Первая - это краткая форма высказывания.
char amessage[16];
amessage[0] = 'n';
amessage[1] = 'o';
...
amessage[15] = '\0';
То есть это массив, содержащий все символы. Специальная инициализация инициализирует его для вас и автоматически определяет его размер. Элементы массива могут быть изменены - вы можете перезаписывать символы в нем.
Вторая форма - это указатель, который просто указывает на символы. Он хранит символы не напрямую. Поскольку массив является строковым литералом, вы не можете взять указатель и записать туда, куда он указывает
char *pmessage = "now is the time";
*pmessage = 'p'; /* undefined behavior! */
Этот код, вероятно, вылетит на вашем компьютере. Но он может делать все что угодно, потому что его поведение не определено.
Если массив определен так, что его размер доступен во время объявления, sizeof(p)/sizeof(type-of-array)
вернет количество элементов в массиве.
Я не могу с пользой добавить к другим ответам, но отмечу, что в " Секретах глубокой С" Питер ван дер Линден подробно описывает этот пример. Если вы задаете такие вопросы, я думаю, вам понравится эта книга.
PS Вы можете присвоить новое значение pmessage
, Вы не можете присвоить новое значение amessage
; это неизменно.
различия между указателем на символ и массивом
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
Первая форма (amessage
) определяет переменную (массив), которая содержит копию строки "now is the time"
,
Вторая форма (pmessage
) определяет переменную (указатель), которая находится в другом месте, чем любая копия строки "now is the time"
,
Попробуйте эту программу:
#include <inttypes.h>
#include <stdio.h>
int main (int argc, char *argv [])
{
char amessage [] = "now is the time";
char *pmessage = "now is the time";
printf("&amessage : %#016"PRIxPTR"\n", (uintptr_t)&amessage);
printf("&amessage[0]: %#016"PRIxPTR"\n", (uintptr_t)&amessage[0]);
printf("&pmessage : %#016"PRIxPTR"\n", (uintptr_t)&pmessage);
printf("&pmessage[0]: %#016"PRIxPTR"\n", (uintptr_t)&pmessage[0]);
printf("&\"now is the time\": %#016"PRIxPTR"\n",
(uintptr_t)&"now is the time");
return 0;
}
Вы увидите, что пока &amessage
равно &amessage[0]
это не верно для &pmessage
а также &pmessage[0]
, На самом деле, вы увидите, что строка хранится в amessage
живет в стеке, а строка, на которую указывает pmessage
живет в другом месте.
Последний printf показывает адрес строкового литерала. Если ваш компилятор выполняет "пул строк", тогда будет только одна копия строки "сейчас самое время" - и вы увидите, что его адрес не совпадает с адресом amessage
, Это потому что amessage
получает копию строки при инициализации.
В конце концов, дело в том, что amessage
сохраняет строку в своей собственной памяти (в этом примере в стеке), а pmessage
указывает на строку, которая хранится в другом месте.
Наряду с памятью для строки "сейчас время", выделяемой в двух разных местах, следует также помнить, что имя массива действует как значение указателя, а не как переменная- указатель, которой является pmessage. Основное отличие состоит в том, что переменная-указатель может быть изменена так, чтобы указывать куда-то еще, а массив - нет.
char arr[] = "now is the time";
char *pchar = "later is the time";
char arr2[] = "Another String";
pchar = arr2; //Ok, pchar now points at "Another String"
arr = arr2; //Compiler Error! The array name can be used as a pointer VALUE
//not a pointer VARIABLE
Указатель - это просто переменная, которая содержит адрес памяти. Обратите внимание, что вы играете с "строковыми литералами", что является еще одной проблемой. Различия объяснены в строке: В основном:
#include <stdio.h>
int main ()
{
char amessage[] = "now is the time"; /* Attention you have created a "string literal" */
char *pmessage = "now is the time"; /* You are REUSING the string literal */
/* About arrays and pointers */
pmessage = NULL; /* All right */
amessage = NULL; /* Compilation ERROR!! */
printf ("%d\n", sizeof (amessage)); /* Size of the string literal*/
printf ("%d\n", sizeof (pmessage)); /* Size of pmessage is platform dependent - size of memory bus (1,2,4,8 bytes)*/
printf ("%p, %p\n", pmessage, &pmessage); /* These values are different !! */
printf ("%p, %p\n", amessage, &amessage); /* These values are THE SAME!!. There is no sense in retrieving "&amessage" */
/* About string literals */
if (pmessage == amessage)
{
printf ("A string literal is defined only once. You are sharing space");
/* Demostration */
"now is the time"[0] = 'W';
printf ("You have modified both!! %s == %s \n", amessage, pmessage);
}
/* Hope it was useful*/
return 0;
}
Второй размещает строку в некотором доступном только для чтения разделе ELF. Попробуйте следующее:
#include <stdio.h>
int main(char argc, char** argv) {
char amessage[] = "now is the time";
char *pmessage = "now is the time";
amessage[3] = 'S';
printf("%s\n",amessage);
pmessage[3] = 'S';
printf("%s\n",pmessage);
}
и вы получите ошибку по второму назначению (pmessage[3]='S').
Ответы выше должны были ответить на ваш вопрос. Но я хотел бы предложить вам прочитать параграф "Эмбриональный Си" в книге " Развитие языка Си", автором которой является сэр Деннис Ритчи.
Для этой строки: char amessage[] = "сейчас время";
Компилятор будет оценивать использование amessage как указатель на начало массива, содержащего символы "сейчас время". Компилятор выделяет память для "сейчас - время" и инициализирует ее строкой "сейчас - время". Вы знаете, где хранится это сообщение, потому что amessage всегда ссылается на начало этого сообщения. amessage не может быть присвоено новое значение - это не переменная, это имя строки "сейчас время".
Эта строка: char * pmessage = "сейчас время";
объявляет переменную, pmessage, которая инициализируется (учитывая начальное значение) начального адреса строки "сейчас время". В отличие от amessage, pmessage можно придать новое значение. В этом случае, как и в предыдущем случае, компилятор также хранит "сейчас самое время" в другом месте в памяти. Например, это приведет к тому, что pmessage укажет на "i", которое начинается "время". pmessage = pmessage + 4;
Вот мое резюме ключевых различий между массивами и указателями, которые я сделал для себя:
//ATTENTION:
//Pointer depth 1
int marr[] = {1,13,25,37,45,56}; // array is implemented as a Pointer TO THE FIRST ARRAY ELEMENT
int* pmarr = marr; // don't use & for assignment, because same pointer depth. Assigning Pointer = Pointer makes them equal. So pmarr points to the first ArrayElement.
int* point = (marr + 1); // ATTENTION: moves the array-pointer in memory, but by sizeof(TYPE) and not by 1 byte. The steps are equal to the type of the array-elements (here sizeof(int))
//Pointer depth 2
int** ppmarr = &pmarr; // use & because going one level deeper. So use the address of the pointer.
//TYPES
//array and pointer are different, which can be seen by checking their types
std::cout << "type of marr is: " << typeid(marr).name() << std::endl; // int* so marr gives a pointer to the first array element
std::cout << "type of &marr is: " << typeid(&marr).name() << std::endl; // int (*)[6] so &marr gives a pointer to the whole array
std::cout << "type of pmarr is: " << typeid(pmarr).name() << std::endl; // int* so pmarr gives a pointer to the first array element
std::cout << "type of &pmarr is: " << typeid(&pmarr).name() << std::endl; // int** so &pmarr gives a pointer to to pointer to the first array elelemt. Because & gets us one level deeper.
Массив является константным указателем. Вы не можете обновить его значение и указать его где-либо еще. Пока за указатель можно сделать.