Как следует использовать символьные массивы в качестве строк?
Я понимаю, что строки в C - это просто массивы символов. Итак, я попробовал следующий код, но он дает странные результаты, такие как вывод мусора или сбой программы:
#include <stdio.h>
int main (void)
{
char str [5] = "hello";
puts(str);
}
Почему это не работает?
Он компилируется чисто с gcc -std=c17 -pedantic-errors -Wall -Wextra
.
Примечание: этот пост предназначен для использования в качестве канонического FAQ по проблемам, возникающим из-за неспособности выделить место для терминатора NUL при объявлении строки.
4 ответа
Строка A C - это массив символов, который заканчивается нулевым ограничителем.
Все символы имеют значение таблицы символов. Нулевой терминатор - это значение символа.0
(нуль). Он используется для обозначения конца строки. Это необходимо, поскольку размер строки нигде не хранится.
Следовательно, каждый раз, когда вы выделяете место для строки, вы должны включать достаточно места для нулевого символа терминатора. В вашем примере этого не происходит, он выделяет место только для 5 символов"hello"
. Правильный код должен быть:
char str[6] = "hello";
Или, что то же самое, вы можете написать самодокументированный код из 5 символов плюс 1 нулевой терминатор:
char str[5+1] = "hello";
Но вы также можете использовать это и позволить компилятору использовать это для ваших:
char str[] = "hello"; // Will allocate 6 bytes automatically
При динамическом распределении памяти для строки во время выполнения вам также необходимо выделить место для нулевого терминатора:
char input[n] = ... ;
...
char* str = malloc(strlen(input) + 1);
Если вы не добавите нулевой терминатор в конце строки, то библиотечные функции, ожидающие строку, не будут работать должным образом, и вы получите ошибки "неопределенного поведения", такие как вывод мусора или сбои программы.
Наиболее распространенный способ записать нулевой символ-ограничитель в C - использовать так называемую "восьмеричную escape-последовательность", которая выглядит следующим образом: '\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++ здесь отличается и не позволяет использовать этот трюк / недостаток.
Из стандарта 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.
Могут ли все строки считаться массивом символов (Да), все ли массивы символов считаться строками (Нет).
Почему бы нет? и почему это важно?
В дополнение к другим ответам, объясняющим, что длина строки нигде не сохраняется как часть строки, и ссылки на стандарт, в котором определена строка, обратной стороной является "Как функции библиотеки C обрабатывают строки?"
Хотя массив символов может содержать одни и те же символы, это просто массив символов, если за последним символом не следует завершающий символ нулю. Этот завершающий нуль символ - это то, что позволяет рассматривать массив символов (обрабатывать как) строку.
Все функции в C, которые ожидают строку в качестве аргумента, ожидают, что последовательность символов будет завершена нулем. Почему?
Это связано с тем, как работают все строковые функции. Поскольку длина не входит в состав массива, строковые функции просматривают массив вперед до нулевого символа (например,'\0'
- эквивалент десятичной дроби 0
) найден. См. Таблицу и описание ASCII. Независимо от того, используете ли выstrcpy
, strchr
, strcspn
и т. д. Все строковые функции полагаются на наличие завершающего нулю символа, чтобы определить, где находится конец этой строки.
Сравнение двух похожих функций из string.h
подчеркнет важность завершающего нуля символа. Взять, к примеру:
char *strcpy(char *dest, const char *src);
В strcpy
функция просто копирует байты из src
к dest
пока не будет найден завершающий нулем символ, говорящийstrcpy
где перестать копировать символы. Теперь возьмем аналогичную функциюmemcpy
:
void *memcpy(void *dest, const void *src, size_t n);
Функция выполняет аналогичную операцию, но не учитывает и не требует src
параметр должен быть строкой. посколькуmemcpy
не может просто сканировать вперед в src
копирование байтов в dest
до тех пор, пока не будет достигнут завершающий нулевой символ, требуется явное количество байтов для копирования в качестве третьего параметра. Этот третий параметр обеспечиваетmemcpy
с той же информацией о размере strcpy
может быть получен простым сканированием вперед, пока не будет найден завершающий нуль символ.
(что также подчеркивает, что идет не так в strcpy
(или любая функция, ожидающая строку), если вы не можете предоставить функции строку с завершающим нулем - она не знает, где остановиться, и с радостью будет гоняться по остальной части вашего сегмента памяти, вызывая Undefined Behavior, пока не появится нулевой символ просто случайно находится где-то в памяти - или возникает Ошибка сегментации)
Вот почему функциям, ожидающим строку с завершающим нулем, должна передаваться строка с завершающим нулем, и почему это важно.
Интуитивно...
Думайте о массиве как о переменной (содержит вещи) и о строке как о значении (может быть помещено в переменную).
Конечно, это не одно и то же. В вашем случае переменная слишком мала, чтобы удерживать строку, поэтому строка обрезается. ("Строки в кавычках" в C имеют в конце неявный нулевой символ.)
Однако можно сохранить строку в массиве, который намного больше, чем строка.
Обратите внимание, что обычные операторы присваивания и сравнения (=
==
<
и т. д.) не работают так, как вы могли ожидать. Ноstrxyz
семейство функций становится довольно близким, если вы знаете, что делаете. См. FAQ по C о строках и массивах.