Большинство языков дают вам тип string, знающий собственную длину. C — не даёт. В C строка — это просто массив char с одним дополнительным правилом: за текстом следует байт со значением ноль, который записывают как '\0' и называют завершающим нулём. Всё остальное про строки в C вытекает из этого единственного соглашения.
Строка — это массив char плюс '\0'
Строка объявляется инициализацией массива char литералом:
В массиве оказывается четыре байта, а не три:
name: +-----+-----+-----+------+
| 'A' | 'd' | 'a' | '\0' |
+-----+-----+-----+------+
индекс: 0 1 2 3
sizeof(name) равен 4. Компилятор посчитал символы, добавил один под завершающий ноль и сам определил размер массива. Из-за этого лишнего байта char name[3] = "Ada"; — это готовая беда: текст помещается, а завершающий ноль нет, поэтому всё дальнейшее не сможет понять, где строка кончается.
Когда нужно место, чтобы менять содержимое позже, задайте размер сами и оставьте запас:
char name[32] = "Ada"; /* 3 символа, завершающий ноль и 28 свободных байт */
Неиспользованные байты заполняются нулями инициализатором, и это безвредно.
Почему завершающий ноль так важен
Ни одна функция C не получает длину. printf("%s", s) получает один адрес и идёт вперёд, печатая байты, пока не встретит '\0'. Поэтому массив char без завершающего нуля не является строкой, а обращение с ним как со строкой читает всё, что случайно лежит следом.
Правило для запоминания: sizeof даёт хранилище, strlen даёт текст. Для char buf[32] = "Ada" sizeof buf равен 32, а strlen(buf) — 3. Они отвечают на разные вопросы, и путаница между ними — один из самых частых багов в C.
Строковые литералы против массивов char
Эти две строки выглядят похоже, а ведут себя очень по-разному:
char a[] = "hello"; /* ваш собственный массив, инициализированный литералом */
char *p = "hello"; /* указатель, нацеленный на сам литерал */
a — массив из 6 байт с собственной копией текста. Его можно изменять. p указывает в область строковых данных программы, доступную только для чтения; литерал общий, и запись через p — неопределённое поведение, которое обычно роняет программу во время выполнения, а не на компиляции.
Пишите const char * всякий раз, когда указатель нацелен на литерал. Тогда компилятор отвергнет запись ещё на этапе компиляции, вместо того чтобы дать ей превратиться в падение. Выбор между двумя формами — по сути вопрос про указатели и массивы, разобранный в статье указатели и массивы.
Ещё одно различие, о котором стоит знать: sizeof a равен 6, а sizeof p — это размер указателя (8 на большинстве систем) независимо от длины текста.
Вывод и ввод строк
%s печатает строку, %c — одиночный символ.
Для ввода полностью избегайте gets — его убрали из языка, потому что пользоваться им безопасно невозможно. Используйте fgets, который принимает размер буфера и не переполнится:
fgets сохраняет нажатый вами перевод строки, а в самой строке он почти никогда не нужен. Строка с strcspn находит первый '\n' и записывает поверх него завершающий ноль — компактный стандартный способ его срезать.
Проход по строке символ за символом
Поскольку конец помечен, а не сосчитан, идиоматический цикл проверяет сам символ:
Условие text[i] != '\0' часто сокращают просто до text[i], поскольку '\0' — это ноль и, значит, ложь. Оба варианта верны; явная форма читается легче, пока вы учитесь.
Идти можно указателем, а не индексом, и это столь же идиоматичный C:
Приведение к unsigned char перед вызовом toupper — не украшение: функции из <ctype.h> не определены для отрицательных значений, а обычный char может быть отрицательным для байтов больше 127.
Строку нельзя скопировать через =
В эту стену упирается каждый новичок:
char a[10];
a = "hello"; /* ошибка: присваивание выражению типа массива */
char b[10] = "hi";
char c[10];
c = b; /* ошибка - по той же причине */
Имя массива не является изменяемым значением, поэтому = нечего делать. Единственное место, где краткая запись работает, — инициализация при объявлении, а это другая операция, выполняемая компилятором.
Чтобы скопировать во время выполнения, копируйте байты через strcpy из <string.h>:
По той же логике == не сравнивает строки. if (a == b) сравнивает два адреса, поэтому для двух разных массивов с одинаковым текстом результат ложен. Используйте вместо этого strcmp(a, b) == 0. Обе функции и их более безопасные родственники разобраны в статье функции для строк.
Массивы строк
Список строк — это либо массив указателей (для неизменного текста, который вы только читаете), либо двумерный массив char (когда каждый элемент должен быть изменяемым):
Массив указателей хранит три адреса литералов только для чтения — компактно, но неизменяемо. Форма char[3][16] резервирует 48 байт вашей собственной памяти, так что каждую строку можно переписать.
Частые ошибки
- Забытый байт под завершающий ноль. В
char s[5] = "hello"для'\0'места нет. Отводитеstrlen + 1. sizeofтам, где имелся в видуstrlen.sizeofот указателя даёт размер указателя, а не длину текста.- Запись через
char *в литерал. Объявляйте такие указатели какconst char *. - Сравнение через
==. Оно сравнивает адреса. Используйтеstrcmp. - Чтение через
scanf("%s", buf). Он не ограничен размером и с удовольствием переполняет буфер; предпочитайтеfgets.
Часто задаваемые вопросы
Как объявить строку в C?
Как массив char: char name[] = "Ada";. Встроенного строкового типа в C нет — этот массив хранит 'A', 'd', 'a' и четвёртый скрытый байт '\0', помечающий конец. Если нужно место, чтобы менять текст позже, задайте размер явно: char name[32] = "Ada";.
Что такое завершающий нулевой байт в C?
Это байт '\0' (значение ноль), помечающий конец строки. Каждая стандартная функция — printf("%s"), strlen, strcpy — читает вперёд, пока не встретит этот байт. Массив char без него не является строкой, и передача такого массива этим функциям приводит к чтению за пределами вашей памяти.
Почему в C нельзя присвоить строку через =?
Потому что массив — не значение, которое копируется через =. char a[10]; a = "hi"; не скомпилируется. Копируйте символы через strcpy(a, "hi") из <string.h> или инициализируйте при объявлении — char a[10] = "hi";, — это единственное место, где такая краткая запись работает.
Чем отличаются char * и char [] в C?
char s[] = "hi" создаёт изменяемый массив с собственной копией текста. char *p = "hi" создаёт указатель на строковый литерал, который лежит в памяти только для чтения: читать через него можно, а запись роняет программу. Массив — когда собираетесь менять, указатель — когда только читаете.