Как массивы символов должны использоваться в качестве строк?

10

Я понимаю, что строки в C - это просто символьные массивы. Поэтому я попробовал следующий код, но он дает странные результаты, такие как вывод мусора или сбой программы:

#include <stdio.h>

int main (void)
{
  char str [5] = "hello";
  puts(str);
}

Почему это не работает?

Он компилируется чисто с gcc -std=c17 -pedantic-errors -Wall -Wextra.


Примечание. Этот пост предназначен для использования в качестве канонического FAQ для проблем, связанных с невозможностью выделить место для терминатора NUL при объявлении строки.

Lundin
источник

Ответы:

12

Строка AC - это массив символов, заканчивающийся нулевым терминатором .

Все символы имеют значение таблицы символов. Терминатор null - это значение символа 0(ноль). Используется для обозначения конца строки. Это необходимо, поскольку размер строки нигде не сохраняется.

Поэтому каждый раз, когда вы выделяете место для строки, вы должны включать достаточно места для нулевого символа-терминатора. Ваш пример не делает этого, он только выделяет место для 5 символов "hello". Правильный код должен быть:

char str[6] = "hello";

Или эквивалентно, вы можете написать самодокументируемый код для 5 символов плюс 1 нулевой терминатор:

char str[5+1] = "hello";

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

char input[n] = ... ;
...
char* str = malloc(strlen(input) + 1);

Если вы не добавляете нулевой терминатор в конец строки, библиотечные функции, ожидающие строку, не будут работать должным образом, и вы получите ошибки «неопределенного поведения», такие как вывод мусора или сбой программы.

Наиболее распространенный способ , чтобы написать нулевой терминатор символ в C является использование так называемой «восьмеричное последовательность», глядя , как это: '\0'. Это на 100% эквивалентно написанию 0, но \служит самодокументированным кодом, утверждающим, что ноль явно является нулевым терминатором. Код, такой как if(str[i] == '\0'), проверит, является ли определенный символ нулевым терминатором.

Обратите внимание, что термин «нулевой терминатор» не имеет ничего общего с нулевыми указателями или NULLмакросом! Это может сбивать с толку - очень похожие имена, но очень разные значения. Вот почему нулевой терминатор иногда называют NULодним L, не путать с NULLнулевыми указателями. См. Ответы на этот вопрос SO для получения дополнительной информации.

В "hello"вашем коде называется строковым литералом . Это следует рассматривать как строку только для чтения. В ""синтаксическом означает , что компилятор добавит нулевой терминатор в конце строки буквальных автоматически. Таким образом, если вы распечатаете, sizeof("hello")вы получите 6, а не 5, потому что вы получите размер массива, включая нулевой терминатор.


Он компилируется чисто с GCC

Действительно, даже не предупреждение. Это происходит из-за тонкой детализации / недостатка в языке C, который позволяет инициализировать символьные массивы строковым литералом, который содержит ровно столько символов, сколько есть места в массиве, а затем молча отбросить нулевой терминатор (C17 6.7.9 / 15). Язык преднамеренно ведет себя так по историческим причинам, см. Подробности в разделе Непоследовательная диагностика gcc для инициализации строки . Также обратите внимание, что здесь C ++ отличается и не позволяет использовать этот трюк / недостаток.

Lundin
источник
1
Вы должны упомянуть char str[] = "hello";случай.
Джаббервоки
@Jabberwocky Это вики сообщества, не стесняйтесь редактировать и вносить свой вклад.
Лундин
1
... а может и проблема char *str = "hello";... str[0] = foo;
Джаббервоки
Возможно, распространите смысл использования sizeofна его использование в параметре функции, особенно когда он определен как массив.
Флюгер
@WeatherVane Должен быть покрыт другим FAQ здесь: stackoverflow.com/questions/492384/…
Lundin
4

Из стандарта C (7.1.1 Определения терминов)

1 Строка - это непрерывная последовательность символов, оканчивающаяся первым нулевым символом и включающая его. Термин многобайтовая строка иногда используется вместо этого, чтобы подчеркнуть специальную обработку, данную многобайтовым символам, содержащимся в строке, или избежать путаницы с широкой строкой. Указатель на строку - это указатель на ее начальный (наименее адресованный) символ. Длина строки - это число байтов, предшествующих нулевому символу, а значение строки - это последовательность значений содержащихся символов в порядке.

В этой декларации

char str [5] = "hello";

строковый литерал "hello"имеет внутреннее представление типа

{ 'h', 'e', 'l', 'l', 'o', '\0' }

поэтому он имеет 6 символов, включая завершающий ноль. Его элементы используются для инициализации массива символов, strкоторый резервирует пространство только для 5 символов.

Стандарт C (противоположный стандарту C ++) позволяет такую ​​инициализацию массива символов, когда конечный ноль строкового литерала не используется в качестве инициализатора.

Однако в результате массив символов strне содержит строку.

Если вы хотите, чтобы массив содержал строку, вы можете написать

char str [6] = "hello";

или просто

char str [] = "hello";

В последнем случае размер массива символов определяется из числа инициализаторов строкового литерала, равного 6.

Влад из Москвы
источник
0

Можно ли считать все строки массивом символов ( Да ), можно ли считать все массивы символов строками ( Нет ).

Почему бы нет? и почему это важно?

В дополнение к другим ответам, объясняющим, что длина строки нигде не хранится как часть строки, и ссылкам на стандарт, где определяется строка, оборотной стороной является «Как функции библиотеки C обрабатывают строки?»

Хотя массив символов может содержать одинаковые символы, он представляет собой просто массив символов, если за последним символом не стоит завершающий нулем символ. Этот завершающий нуль символ - это то, что позволяет массиву символов считаться (обрабатываться как) строкой.

Все функции в C, которые ожидают строку в качестве аргумента, ожидают, что последовательность символов будет оканчиваться нулем . Почему?

Это связано с тем, как работают все строковые функции. Поскольку длина не включена как часть массива, строковые функции сканируют в массиве до тех пор, пока не будет найден нуль-символ (например, '\0'- эквивалент десятичного числа 0). Смотрите таблицу ASCII и описание . Независимо от того, используете ли вы strcpy, strchr, strcspnи т.д .. Все строковые функции полагаются на NUL оконечного характера присутствуя определить , где конец этой строки есть.

Сравнение двух похожих функций из string.hподчеркивает важность нулевого завершающего символа. Взять, к примеру:

    char *strcpy(char *dest, const char *src);

strcpyФункция просто копирует байты из srcк destдо NUL оконечного символ не найдено рассказывая , strcpyгде остановить копирование символов. Теперь возьмите похожую функцию memcpy:

    void *memcpy(void *dest, const void *src, size_t n);

Функция выполняет аналогичную операцию, но не считает или требует, чтобы srcпараметр был строкой. Так как memcpyпри srcкопировании байтов не удается просто выполнить перемотку вперед до destтех пор, пока не будет достигнут нулевой завершающий символ, ему требуется явное количество байтов для копирования в качестве третьего параметра. Этот третий параметр обеспечивает memcpyту же информацию о размере, которую strcpyможно получить, просто сканируя вперед, пока не будет найден нулевой завершающий символ.

(который также подчеркивает, что идет не так strcpy(или любую функцию, ожидающую строку), если вы не можете обеспечить функцию строкой с нулевым символом в конце - он понятия не имеет, где остановиться, и с удовольствием прыгнет через оставшуюся часть вашего сегмента памяти вызывая неопределенное поведение до тех пор, пока nul-символ просто не будет найден где-то в памяти - или не произойдет ошибка сегментации)

Вот почему функциям, ожидающим строку с нулевым символом в конце, должна быть передана строка с нулевым символом в конце и почему это важно .

Дэвид С. Ранкин
источник
0

Наглядно ...

Думайте о массиве как о переменной (содержит вещи) и о строке как о значении (может быть помещено в переменную).

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

Однако возможно сохранить строку в массиве, который намного больше, чем строка.

Обратите внимание, что обычные операторы присваивания и сравнения ( = == <и т. Д.) Не работают так, как вы могли бы ожидать. Но strxyzсемейство функций подходит довольно близко, когда вы знаете, что делаете. Смотрите C FAQ по строкам и массивам .

Artelius
источник