Как массивы символов должны использоваться в качестве строк? - PullRequest
10 голосов
/ 23 октября 2019

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

#include <stdio.h>

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

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

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


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

Ответы [ 4 ]

11 голосов
/ 23 октября 2019

Строка 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 - это использование так называемой «восьмеричной 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 ++ отличается и не позволяет использовать этот трюк / недостаток.

4 голосов
/ 23 октября 2019

Из стандарта 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 голосов
/ 24 октября 2019

Интуитивно ...

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

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

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

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

0 голосов
/ 23 октября 2019

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

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

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

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

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

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

Сравнение двух похожих функций из string.h подчеркнет важность символа nul-terminating . Возьмем, к примеру:

    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 (или любой функции, ожидающей строку), если вы не предоставите функции строку с нулевым символом в конце - она ​​не знает, где остановиться, и с радостью прыгнет через остальную частьваш сегмент памяти вызывает неопределенное поведение до тех пор, пока нуль-символ просто не будет найден где-то в памяти - или не произойдет ошибка сегментации)

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

...