Введение

Структура данных

В компьютерном программировании, строка с нулевым завершением — это последовательность символов, хранящаяся в виде массива, содержащего символы и завершающаяся нулевым символом (символ с внутренним значением нуля, называемый "NUL" в этой статье, не путать с графическим символом нуля). Другие названия — C-строка, отсылающая к языку программирования C, и ASCIIZ (хотя C может использовать кодировки, отличные от ASCII). Длина строки определяется путем поиска (первого) NUL. Это может быть медленным, так как требует O(n) (линейного времени) относительно длины строки. Также это означает, что строка не может содержать NUL (в памяти NUL присутствует, но он располагается после последнего символа, а не внутри строки).

История

Нулевые строки создавались директивой ASCIZ языков ассемблера PDP 11 и директивой ASCIZ макроассемблера MACRO 10 для PDP 10. Они предшествовали разработке языка программирования C, но часто использовались и другие форматы строк. Когда C (и языки, от которых он произошел) разрабатывался, память была крайне ограничена, поэтому использование всего одного байта дополнительных затрат для хранения длины строки было привлекательным решением. Единственной популярной альтернативой в то время, обычно называемой «паскалевской строкой» (более современный термин – «строка с префиксом длины»), был ведущий байт, используемый для хранения длины строки. Это позволяло строке содержать символ NUL и требовало всего одного обращения к памяти для определения длины (O(1) – постоянное время), но ограничивало длину строки до 255 символов. Разработчик C, Деннис Ричи, выбрал соглашение о нулевом завершении, чтобы избежать ограничения длины строки и потому, что, по его опыту, ведение подсчета казалось менее удобным, чем использование терминатора. Это оказало определенное влияние на проектирование наборов инструкций процессоров. Некоторые процессоры 1970-х и 1980-х годов, такие как Zilog Z80 и DEC VAX, имели специализированные инструкции для работы со строками с префиксом длины. Однако, по мере распространения нулевых строк, разработчики процессоров стали учитывать и их, что проявилось, например, в решении IBM добавить инструкции "Logical String Assist" в ES/9000 520 в 1992 году и векторные строковые инструкции в IBM z13 в 2015 году. Разработчик FreeBSD, Пол Хеннинг Камп, в статье для ACM Queue назвал победу нулевых строк над двухбайтовым (а не однобайтовым) префиксом длины «самой дорогой ошибкой в один байт» в истории.

Ограничения

Хотя это представление легко реализовать, оно склонно к ошибкам и проблемам с производительностью. Нуль-терминирование исторически создавало уязвимости в системе безопасности. Символ NUL, вставленный в середину строки, неожиданно обрезает её. Распространенной ошибкой было недостаточное выделение памяти для завершающего нуля, из-за чего он перезаписывал соседние области памяти. Другой ошибкой было полное отсутствие записи завершающего нуля, что часто оставалось незамеченным при тестировании, поскольку блок памяти уже содержал нули. Из-за затрат на определение длины многие программы пренебрегали этим перед копированием строки в буфер фиксированного размера, что приводило к переполнению буфера, если строка была слишком длинной. Невозможность хранения нуля требует раздельного хранения текстовых и бинарных данных и использования для их обработки различных функций (для бинарных данных при этом необходимо указывать и длину). Это может приводить к дублированию кода и ошибкам при использовании неверной функции. Проблемы с производительностью при определении длины обычно можно решить, объединив эту операцию с другой, имеющей сложность O(n), например, как в strlcpy. Однако это не всегда обеспечивает интуитивно понятный API.

Кодировки символов

Строки с нулевым терминатором требуют, чтобы кодировка не использовала нулевой байт (0x00) нигде; следовательно, невозможно сохранить каждую возможную строку ASCII или UTF-8. Однако обычно подмножество ASCII или UTF-8 – все символы, кроме NUL – хранится в строках с нулевым терминатором. Некоторые системы используют "модифицированный UTF-8", который кодирует NUL как два ненулевых байта (0xC0, 0x80) и таким образом позволяет хранить все возможные строки. Это не допускается стандартом UTF-8, поскольку это избыточно длинная кодировка, и это считается угрозой безопасности. Вместо этого в качестве конца строки могут использоваться другие байты, например 0xFE или 0xFF, которые не используются в UTF-8. UTF-16 использует 2-байтовые целые числа, и поскольку любой байт может быть нулевым (фактически, каждый второй байт является нулевым при представлении текста ASCII), его нельзя хранить в строке байтов с нулевым терминатором. Однако некоторые языки реализуют строку из 16-битных символов UTF-16, завершаемую 16-битным NUL (0x0000).

Улучшения

Было предпринято множество попыток сделать обработку строк в C менее подверженной ошибкам. Одна из стратегий заключается в добавлении более безопасных функций, таких как strdup и strlcpy, и одновременном отказе от использования небезопасных функций, например, gets. Другая – добавить объектно-ориентированную оболочку вокруг строк C, чтобы разрешить только безопасные вызовы. Однако, небезопасные функции все равно можно вызывать напрямую. Большинство современных библиотек заменяют строки C структурой, содержащей значение длины размером 32 бита или больше (что значительно превышает размеры, рассматриваемые для строк с префиксом длины), и часто добавляют дополнительный указатель, счетчик ссылок и даже нулевой символ для ускорения преобразования обратно в строку C. Объем памяти сейчас значительно больше, поэтому, если добавление 3 (или 16, или более) байтов к каждой строке действительно является проблемой, программное обеспечение должно будет работать с таким количеством коротких строк, что другой метод хранения позволит сэкономить еще больше памяти (например, если существует большое количество дубликатов, хэш-таблица может использовать меньше памяти). Примеры включают std::string из C++ Standard Template Library, QString из Qt, CString из MFC, а также реализацию CFString на основе C из Core Foundation и ее аналог на Objective-C – NSString из Foundation, обе разработаны Apple. Для хранения строк также могут использоваться более сложные структуры, такие как rope (веревка).