Menu

Строки в C: массивы char, нулевой байт и примеры

В C нет типа «строка»: строка — это массив char, заканчивающийся байтом '\0'. Здесь выстраивается эта модель, а затем разбираются литералы, печать, перебор символов и то, почему строку нельзя скопировать через =.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Большинство языков дают вам тип 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" создаёт указатель на строковый литерал, который лежит в памяти только для чтения: читать через него можно, а запись роняет программу. Массив — когда собираетесь менять, указатель — когда только читаете.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ