Подтвердить что ты не робот

Строковые литералы против массива char при инициализации указателя

Вдохновленный этот вопрос.

Мы можем инициализировать указатель char строковым литералом:

char *p = "ab";

И это прекрасно. Можно подумать, что это эквивалентно следующему:

char *p = {'a', 'b', '\0'};

Но, по-видимому, это не так. И не только потому, что строковые литералы хранятся в памяти только для чтения, но кажется, что даже через строковый литерал имеет тип массива char, а инициализатор {...} имеет тип массива char, два объявления обрабатываются по-разному, поскольку компилятор дает предупреждение:

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

во втором случае. Каково объяснение такого поведения?

Update:

Кроме того, в последнем случае указатель p будет иметь значение 0x61 (значение первого элемента массива 'a') вместо места памяти, так что компилятор, как предупреждал, принимает только первый элемент инициализатора и присвоение ему p.

4b9b3361

Ответ 1

Я думаю, вы сбиты с толку, потому что char *p = "ab"; и char p[] = "ab"; имеют схожую семантику, но разные значения.

Я считаю, что последний случай (char p[] = "ab";) лучше всего рассматривать как короткую нотацию для char p[] = {'a', 'b', '\0'}; (инициализирует массив размером, определяемым инициализатором). Фактически, в этом случае вы могли бы сказать, что "ab" на самом деле не используется как строковый литерал.

Однако первый случай (char *p = "ab";) отличается тем, что он просто инициализирует указатель p, чтобы указать на первый элемент литерала строки только для чтения "ab".

Надеюсь, вы видите разницу. Хотя char p[] = "ab"; представляется в качестве инициализации, такой как вы описали, char *p = "ab"; нет, поскольку указатели, ну, а не массивы, и инициализация их инициализатором массива делает что-то совершенно другое (а именно, дать им значение первого элемента, 0x61 в вашем случае).

Короче говоря, компиляторы C только "заменяют" строковый литерал инициализатором массива char, если он подходит для этого, т.е. он используется для инициализации массива char.

Ответ 2

Второй пример синтаксически неверен. В C, {'a', 'b', '\0'} может использоваться для инициализации массива, но не для указателя.

Вместо этого вы можете использовать составной литерал C99 (также доступный в некоторых компиляторах как расширение, например GCC) следующим образом:

char *p = (char []){'a', 'b', '\0'};

Обратите внимание, что он более мощный, поскольку инициализатор не обязательно завершает нуль.

Ответ 3

Строковые литералы имеют "магический" статус в C. Они не похожи ни на что другое. Чтобы понять, почему, полезно подумать об этом с точки зрения управления памятью. Например, спросите себя: "Где хранится строковый литерал в памяти? Когда он освобождается из памяти?" и все начнет иметь смысл.

Они отличаются от числовых литералов, которые легко переводятся в машинные инструкции. Для упрощенного примера, примерно так:

int x = 123;

... может перевести что-то вроде этого на уровне машины:

mov ecx, 123

Когда мы делаем что-то вроде:

const char* str = "hello";

... теперь у нас есть дилемма:

mov ecx, ???

Нет необходимости в некоем собственном понимании аппаратного обеспечения того, что на самом деле представляет собой многобайтная строка переменной длины. Он в основном знает о битах и ​​байтах и ​​числах и имеет регистры, предназначенные для хранения этих вещей, но строка - это блок памяти, содержащий несколько из них.

Поэтому компиляторы должны генерировать инструкции для хранения этого блока памяти строки где-то, и поэтому они обычно генерируют инструкции при компиляции вашего кода для хранения этой строки где-то в глобально доступном месте (обычно это сегмент памяти только для чтения или сегмент данных). Они могут также объединяться с несколькими буквальными строками, которые идентичны для хранения в той же области памяти, чтобы избежать избыточности. Теперь он может сгенерировать инструкцию mov/load для загрузки адреса в литеральную строку, и вы можете затем работать с ней косвенно через указатель.

Другой сценарий, который мы могли бы запустить, таков:

static const char* some_global_ptr = "blah";

int main()
{
    if (...)
    {
        const char* ptr = "hello";
        ...
        some_global_ptr = ptr;
    }
    printf("%s\n", some_global_ptr);
}

Естественно, что ptr выходит за пределы области видимости, но нам нужна эта буквальная строка, чтобы задерживаться, чтобы эта программа имела четко определенное поведение. Таким образом, литеральные строки преобразуют не только адреса в глобально доступные блоки памяти, но также не освобождаются, если ваша бинарная/программа загружена/запущена, так что вам не нужно беспокоиться об управлении их памятью. [Edit: исключая потенциальную оптимизацию: для программиста C нам никогда не придется беспокоиться об управлении памятью строковой строки, поэтому эффект такой же, как и всегда).

Теперь о массивах символов, литеральные строки не обязательно являются символьными массивами, как таковые. Ни в коем случае в программном обеспечении мы не можем привязать их к массиву r-значение, которое может дать нам количество байтов, выделенных с помощью sizeof. Мы можем указать только на память через char*/const char*

Этот код фактически дает нам дескриптор такого массива без привлечения указателя:

char str[] = "hello";

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

Первый блок будет постоянным на протяжении всей программы и будет содержать эту литеральную строку "hello". Второй блок будет для этого фактического массива str, и он не обязательно будет постоянным. Если бы мы написали такой код внутри функции, он будет выделять память в стеке, скопировать эту литеральную строку в стек и освободить память из стека, когда str выходит за рамки. Адрес str не будет соответствовать литеральной строке, иначе говоря.

Наконец, когда мы пишем что-то вроде этого:

char str[] = {'h', 'e', 'l', 'l', 'o', '\0'};

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

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

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

Ответ 4

Из C99 имеем

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

Итак, во втором определении нет строкового литерала, поскольку он не находится в двойных кавычках. Указателю следует выделить память перед записью чего-либо, или если вы хотите перейти по списку инициализаторов, то

char p[] = {'a','b','\0'};

- это то, что вы хотите. В основном оба являются разными объявлениями.