Что происходит, когда массив символов инициализируется строковым литералом?

Насколько я понимаю, следующий код работает так:

char* cptr = "Hello World";

«Hello World» находится в разделе .rodata памяти программы. Строковый литерал "Hello World" возвращает указатель на базовый адрес строки или адрес первого элемента в так называемом «массиве», поскольку символы располагаются в памяти последовательно, это будет «H». Это моя маленькая диаграмма, когда я визуализирую, как строковый литерал сохраняется в памяти:

0x4 : 'H'
0x5 : 'e'
0x6 : 'l'
0x6 : 'l'
0x7 : 'o'
0x8 : ' '
0x9 : 'W'
0xa : 'o'
0xb : 'r'
0xc : 'l'
0xd : 'd'
0xe : '\0'

Таким образом, объявление выше становится:

char* cptr = 0x4;

Теперь cptr указывает на строковый литерал. Я просто придумываю адреса.

0xa1 : 0x4

Теперь, как этот код работает?

char cString[] = "Hello World";

Я предполагаю, что, как и в предыдущей ситуации, "Hello World" также деградирует до адреса «H» и 0x4.

char cString[] = 0x4;

Я читаю = как перегруженный оператор присваивания, когда он используется с инициализацией массива символов. Насколько я понимаю, при инициализации только C-строки он копирует символ за символом, начиная с заданного базового адреса, в C-строку, пока не встретит «\ 0» в качестве последнего скопированного символа. Он также выделяет достаточно памяти для всех символов. Поскольку перегруженные операторы на самом деле являются просто функциями, я предполагаю, что их внутренняя реализация аналогична strcpy().

Я хотел бы, чтобы один из более опытных программистов на C подтвердил мои предположения о том, как работает этот код. Это моя визуализация C-строки после того, как в нее скопированы символы из строкового литерала:

0xb4 : 'H'
0xb5 : 'e'
0xb6 : 'l'
0xb6 : 'l'
0xb7 : 'o'
0xb8 : ' '
0xb9 : 'W'
0xba : 'o'
0xbb : 'r'
0xbc : 'l'
0xbd : 'd'
0xbe : '\0'

Опять же, адреса произвольные, дело в том, что C-строка в стеке отлична от строкового литерала в секции .rodata в памяти.

Что я пытаюсь сделать? Я пытаюсь использовать указатель char для временного хранения базового адреса строкового литерала и использовать тот же указатель char (базовый адрес строкового литерала) для инициализации C-строки.

char* cptr = "Hello World";
char cString[] = cptr;

Я предполагаю, что "Hello World" соответствует базовому адресу 0x4. Таким образом, этот код должен выглядеть так:

char* cptr = 0x4;
char cString[] = 0x4;

Я предполагаю, что он не должен отличаться от char cString[] = "Hello World";, поскольку «Hello World» оценивается по его базовому адресу, и именно это хранится в указателе char!

Однако gcc выдает ошибку:

error: invalid initializer
char cString[] = cptr;
                 ^
  1. Почему вы не можете использовать указатель char в качестве временного заполнителя для хранения базового адреса строкового литерала?
  2. Как работает этот код? Верны ли мои предположения?
  3. Возвращает ли использование строкового литерала в коде базовый адрес в «массив», где символы хранятся в памяти?

person Galaxy    schedule 19.06.2018    source источник
comment
Массивы не являются указателями. Здесь, на SO, и в других местах в сети есть много объяснений этого. Да, вы можете назначить указатель, чтобы он указывал на массив. Нет, вы не можете назначить массив так, чтобы он указывал на что-либо (потому что это не указатель). Инициализация char cString[] = "Hello World" не требует никаких указателей — символ cString оказывается адресом массива из 12 символов, инициализированного символами из Hello World.   -  person Steve Summit    schedule 20.06.2018
comment
Вы пишете так называемый «массив», как будто в этом есть что-то сомнительное. Нет. Термин «массив» хорошо определен в C, и стандарт явно указывает, что частью процесса преобразования исходных файлов C в программы является добавление нулевого байта к байтам каждого строкового литерала и использование полученных последовательностей байтов для инициализации статических массивов. достаточно долго, чтобы вместить их. Нет никаких «как если бы» или «подобно» — строковые литералы, появляющиеся в исходном коде C, соответствуют добросовестным массивам в программах.   -  person John Bollinger    schedule 20.06.2018
comment
Я не понимаю добросовестно. Я хочу сказать, что строковый литерал выглядит как массив, хранящийся в разделе .rodata программы. Однако это не массив, поскольку я, программист, объявил бы это явно в коде: char array[];   -  person Galaxy    schedule 20.06.2018


Ответы (3)


Ваше понимание схемы памяти более-менее правильное. Но проблема, с которой вы столкнулись, связана с семантикой инициализации в C.

Символ = в объявлении здесь НЕ является оператором присваивания. Вместо этого это синтаксис, который указывает инициализатор для инстанцируемой переменной. В общем случае T x = y; не совпадает с T x; x = y;.

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

Почему такие правила? «Исторические причины».

person M.M    schedule 19.06.2018

Я предполагаю, что, как и в предыдущей ситуации, "Hello World" также деградирует до адреса 'H' и 0x4.

Не совсем так: cString[] получает совершенно новый адрес в памяти. Компилятор выделяет ему 12 char и инициализирует их содержимым строкового литерала "Hello World".

Я предполагаю, что "Hello World" соответствует базовому адресу 0x4. Возвращает ли использование строкового литерала в коде базовый адрес в «массив», где символы хранятся в памяти?

cString может быть преобразовано в char* позже, что даст его базовый адрес, но в обычном контексте он остается массивом. В частности, если вы вызовете sizeof(cString), вы получите размер массива, а не размер указателя.

Почему вы не можете использовать указатель char в качестве временного заполнителя для хранения базового адреса строкового литерала?

Ты сможешь. Однако, как только строковый литерал присваивается char *, он перестает быть строковым литералом, по крайней мере, с точки зрения компилятора. Он становится указателем char *, ничем не отличающимся от других указателей char *.

Обратите внимание, что современные компиляторы C комбинируют идентичные строковые литералы в качестве оптимизации, поэтому, если вы напишете

#define HELLO_WORLD "Hello World"
...
char* cptr = HELLO_WORLD;
char cString[] = HELLO_WORLD;

и включить оптимизацию, компилятор устранит повторяющиеся копии строкового литерала.

person Sergey Kalinichenko    schedule 19.06.2018

Второе определение char cString[] = "Hello World"; является сокращением для этого эквивалентного определения:

char cString[12] = { 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '\0' };

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

person chqrlie    schedule 19.06.2018
comment
В абстрактной машине есть строковый литерал, который копируется в cString. С точки зрения наблюдаемого поведения два приведенных вами кода одинаковы. Варианты его реализации можно охарактеризовать как вопрос оптимизации. - person M.M; 20.06.2018
comment
@M.M: верно, но копирование строкового литерала несколько неоднозначно, оно не точно описывает семантику другого примера char cString[12] = { "Hello" }; эквивалентен char cString[12] = { 'H', 'e', 'l', 'l', 'o', '\0', '\0', '\0', '\0', '\0', '\0', '\0' };, а не char cString[12]; strcpy(cString, "Hello"); - person chqrlie; 20.06.2018