Какое обоснование для строк с нулевым завершением?

Насколько я люблю C и С++, я не могу не почесать голову при выборе нулевых завершенных строк:

  • Строки длиной до префикса (т.е. Паскаль) существовали до C
  • Длина префиксных строк делает несколько алгоритмов быстрее, обеспечивая постоянный поиск длины.
  • Длина префиксных строк делает сложнее вызвать ошибки переполнения буфера.
  • Даже на 32-битной машине, если вы разрешаете строке быть размером доступной памяти, длина префиксной строки всего на три байта шире, чем строка с нулевым завершением. На 16-битных машинах это один байт. На 64-битных машинах 4 ГБ является разумным пределом длины строки, но даже если вы хотите расширить его до размера машинного слова, 64-разрядные машины обычно имеют достаточную память, что делает лишние семь байтов вроде нулевого аргумента. Я знаю, что оригинальный C-стандарт был написан для безумно бедных машин (с точки зрения памяти), но аргумент эффективности не продает меня здесь.
  • Практически любой другой язык (например, Perl, Pascal, Python, Java, С# и т.д.) использует префиксные строки длины. Эти языки обычно били C в тестах обработки строк, потому что они более эффективны со строками.
  • С++ исправил это немного с помощью шаблона std::basic_string, но простые массивы символов, ожидающие нулевых завершенных строк, все еще распространены. Это также несовершенно, потому что для этого требуется выделение кучи.
  • Строки с нулевым завершением должны зарезервировать символ (а именно, null), который не может существовать в строке, а строки с префиксом длины могут содержать внедренные нули.

Некоторые из этих вещей появились совсем недавно, чем C, поэтому было бы полезно, чтобы C не знал о них. Тем не менее, некоторые из них были хорошо известны до того, как C стал. Почему были выбраны нулевые завершенные строки вместо префикса явно превосходящей длины?

РЕДАКТИРОВАТЬ: поскольку некоторые из вас попросили факты (и мне не понравились те, которые я уже предоставил) по моей эффективности выше, они вытекают из нескольких вещей:

  • Конкат, использующий нуль-завершенные строки, требует O (n + m) временной сложности. Для префикса длины часто требуется только O (m).
  • Длина с использованием нулевых завершенных строк требует O (n) временной сложности. Префикс длины - O (1).
  • Длина и concat являются наиболее распространенными строковыми операциями. Существует несколько случаев, когда нулевые завершаемые строки могут быть более эффективными, но они встречаются гораздо реже.

Из приведенных ниже ответов, это некоторые случаи, когда строки с нулевым завершением являются более эффективными:

  • Когда вам нужно отключить начало строки и передать ее некоторому методу. Вы не можете сделать это в постоянное время с префиксом длины, даже если вам разрешено уничтожить исходную строку, потому что префикс длины, вероятно, должен следовать правилам выравнивания.
  • В некоторых случаях, когда вы просто перебираете символ строки по символу, вы можете сохранить регистр CPU. Обратите внимание, что это работает только в том случае, если вы не динамически выделили строку (потому что тогда вам придется освободить ее, что потребовало бы использовать этот регистр процессора, который вы сохранили, чтобы удерживать указатель, который вы изначально получили от malloc и друзей).

Ни один из вышеперечисленных не является столь же общим, как длина и concat.

В ответах ниже сказано следующее:

  • Вам нужно отрезать конец строки

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

Ответ 1

Из лошадиного устья

Нет поддержки BCPL, B или C символьных данных в язык; каждый трактует строки много как векторы целых чисел и дополняет общие правила несколькими конвенций. Как в BCPL, так и в B a Строковый литерал обозначает адрес статическая область, инициализированная символы строки, упакованные в клетки. В BCPL первый упакованный байт содержит количество символов в Струна; в B нет счета и строки заканчиваются специальный символ, который B пишется *e. Это изменение было сделано частично во избежание ограничения длины строки, вызванной счет в 8- или 9-битном слоте и отчасти потому, что поддержание счета казалось, по нашему опыту, меньше удобно, чем использование терминатора.

Деннис М Ричи, разработка языка C

Ответ 2

C не содержит строку как часть языка. "Строка" в C - это просто указатель на char. Так что, возможно, вы задаете неправильный вопрос.

"Какое обоснование для исключения типа строки" может быть более актуальным. Для этого я хотел бы указать, что C не является объектно-ориентированным языком и имеет только базовые типы значений. Строка представляет собой концепцию более высокого уровня, которая должна быть реализована путем объединения значений других типов. C находится на более низком уровне абстракции.

в свете бушующего шквала ниже:

Я просто хочу указать, что я не пытаюсь сказать, что это глупый или плохой вопрос, или что способ представления строк - это лучший выбор. Я пытаюсь уточнить, что вопрос будет более лаконичным, если учесть тот факт, что C не имеет механизма для дифференциации строки как типа данных из массива байтов. Это лучший выбор в свете обработки и памяти сегодняшних компьютеров? Возможно нет. Но задним числом всегда 20/20, и все это:)

Ответ 3

Вопрос задается как вещь Length Prefixed Strings (LPS) vs zero terminated strings (SZ), но в основном раскрывает преимущества префиксных строк длины. Это может показаться ошеломляющим, но, честно говоря, мы также должны учитывать недостатки LPS и преимущества SZ.

Как я понимаю, вопрос может быть даже понят как предвзятый способ спросить "в чем преимущества Zero Terminated Strings?".

Преимущества (я вижу) строк с нулевым завершением:

  • очень просто, не нужно вводить новые понятия в язык, char массивы / char могут делать указатели.
  • основной язык включает минимальный синтаксический сахар для преобразования что-то между двойными кавычками куча символов (на самом деле это куча байт). В некоторых случаях его можно использовать полностью инициализировать вещи не связанный с текстом. Например, xpm формат файла изображения является допустимым источником C который содержит данные изображения, закодированные как строка.
  • Кстати, вы можете поместить нуль в строковый литерал, компилятор будет просто добавьте еще один в конец литерала: "this\0is\0valid\0C". Это строка? или четыре строки? Или куча байтов...
  • плоская реализация, без скрытой косвенности, без скрытого целого.
  • не задействовано скрытое выделение памяти (ну, некоторые постыдные не стандартные функции, такие как strdup выполнять распределение, но в основном источник проблемы).
  • нет конкретной проблемы для небольшого или большого оборудования (представьте себе управлять длиной бита 32 бит на 8 бит микроконтроллеров или ограничения ограничения размера строки до менее 256 байт, это была проблема, с которой я действительно сталкивался с Turbo Pascal eons назад).
  • реализация строковых манипуляций - всего лишь несколько очень простая функция библиотеки
  • эффективен для основного использования строк: чтение постоянного текста последовательно от известного старта (в основном сообщения для пользователя).
  • завершающий нуль даже не является обязательным, все необходимые инструменты манипулировать символами как кучу байты. При выполнении инициализация массива в C, вы можете даже избегайте терминатора NUL. Просто установите правильный размер. char a[3] = "foo"; имеет значение C (не С++) и не ставит конечный ноль в.
  • согласованный с точкой unix "все есть файл", в том числе "файлы", которые не имеют внутренней длины как stdin, stdout. Вы должны помнить, что открытые примитивы чтения и записи реализованы на очень низком уровне. Это не вызовы библиотеки, а системные вызовы. И используется тот же API для двоичных или текстовых файлов. Элементы чтения файлов получают адрес буфера и размер и возвращают новый размер. И вы можете использовать строки в качестве буфера для записи. Использование другого типа строки представление подразумевает, что вы не можете легко использовать литеральную строку в качестве буфера для вывода или вам придется сделать это очень странно, когда вы набрасываете его на char*. а именно не возвращать адрес строки, а вместо этого возвращать фактические данные.
  • очень легко манипулировать текстовыми данными, считываемыми из файла на месте, без бесполезной копии буфера, просто вставьте нули в нужные места (ну, на самом деле, с современными C, поскольку строки с двойными кавычками представляют собой const char массивы, которые в настоящее время обычно хранятся в не изменяемом сегменте данных).
  • Предполагая, что некоторые значения int любого размера будут подразумевать проблемы выравнивания. Начальный длина должна быть выровнена, но нет причин делать это для символов (и снова, заставляя выравнивание строк будет подразумевать проблемы, рассматривая их как кучу байт).
  • длина известна во время компиляции для постоянных строк литерала (sizeof). Так зачем кто-нибудь хочет сохранить его в памяти, добавляя его к фактическим данным?
  • таким образом, что C делает (почти) все остальные, строки рассматриваются как массивы char. Поскольку длина массива не управляется C, логическая длина не управляется ни для строк. Единственное, что удивительно, это то, что в конце добавлен 0 элемента, но только на уровне основного языка при вводе строки между двойными кавычками. Пользователи могут прекрасно вызывать функции манипуляции строкой, проходящие по длине, или даже использовать вместо них простое замещение. SZ - всего лишь объект. В большинстве других языков длина массива управляется, это логично, что для строк является одинаковым.
  • в наше время все равно 1 байтовый набор символов недостаточно, и вам часто приходится иметь дело с закодированными строками unicode, где количество символов сильно отличается от числа байтов. Это означает, что пользователи, вероятно, захотят больше, чем "просто размер", но также и другие сведения. Сохраняя длину, не используйте ничего (особенно естественное место для их хранения) в отношении этих других полезных фрагментов информации.

Тем не менее, нет необходимости жаловаться в редком случае, когда стандартные строки C действительно неэффективны. Доступны либы. Если бы я следил за этой тенденцией, я должен был бы пожаловаться, что стандарт C не включает никаких функций поддержки регулярных выражений... но на самом деле все знают, что это не настоящая проблема, поскольку для этой цели существуют библиотеки. Поэтому, когда требуется эффективная манипуляция строкой, почему бы не использовать библиотеку, например bstring? Или даже строки С++?

EDIT. Недавно я взглянул на строки D. Достаточно интересно видеть, что выбранное решение не является ни префиксом размера, ни нулевым завершением. Как и в C, литеральные строки, заключенные в двойные кавычки, являются короткой рукой для неизменяемых массивов char, а язык также имеет ключевое слово string, которое означает (неизменяемый массив char).

Но массивы D намного богаче C-массивов. В случае статических массивов длина известна во время выполнения, поэтому нет необходимости хранить длину. У компилятора есть его во время компиляции. В случае динамических массивов длина доступна, но в документации D не указано, где она хранится. Насколько нам известно, компилятор мог бы сохранить его в каком-либо регистре или в некоторой переменной, хранящейся далеко от данных символов.

В обычных char массивах или нелиберальных строках нет конечного нуля, поэтому программист должен сам поставить его, если он хочет вызвать некоторую функцию C из D. В частном случае литеральных строк, однако компилятор D все еще поместите нуль в конце каждой строки (чтобы упростить приведение к строкам C, чтобы упростить вызов функции C?), но этот ноль не является частью строки (D не учитывает ее в размере строки).

Единственное, что меня несколько разочаровывало в том, что строки должны быть utf-8, но длина, по-видимому, все еще возвращает количество байтов (по крайней мере, это правда в моем компиляторе gdc) даже при использовании многобайтовых символов. Мне непонятно, если это ошибка компилятора или по назначению. (ОК, я, наверное, выяснил, что произошло. Чтобы сказать компилятору D, что ваш источник использует utf-8, вы должны сначала поместить некоторый глупый порядок байтов. Я пишу глупо, потому что знаю, что не редактор делает это, особенно для UTF- 8, который должен быть совместим с ASCII).

Ответ 4

Я думаю, он имеет исторические причины и нашел это в википедии:

В то время C (и языки, которые он был получен из) были разработаны, память была крайне ограничена, поэтому использование только один байт накладных расходов для хранения длина строки была привлекательной. только популярная альтернатива в то время, обычно называемый "строкой Паскаля", (хотя также используется ранними версиями BASIC), используется старший байт для хранения длина строки. Это позволяет строка, содержащая NUL и сделанная найти длину нужно только один доступ к памяти (время O (1) (постоянное)). Но один байт ограничивает длину до 255. Это ограничение длины было намного больше чем проблемы с C, так что строка C вообще выиграл.

Ответ 5

Calavera , но поскольку люди, похоже, не понимают, Приведем примеры кода.

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

Если C имел тип string, например int или char, это был бы тип, который не вписывался в регистр или в стек, и требовал бы выделения памяти (со всей своей поддерживающей инфраструктурой ) для обработки любым способом. Все это противоречит основным принципам C.

Итак, строка в C:

char s*;

Итак, допустим, что это было префиксом длины. Давайте напишем код, чтобы объединить две строки:

char* concat(char* s1, char* s2)
{
    /* What? What is the type of the length of the string? */
    int l1 = *(int*) s1;
    /* How much? How much must I skip? */
    char *s1s = s1 + sizeof(int);
    int l2 = *(int*) s2;
    char *s2s = s2 + sizeof(int);
    int l3 = l1 + l2;
    char *s3 = (char*) malloc(l3 + sizeof(int));
    char *s3s = s3 + sizeof(int);
    memcpy(s3s, s1s, l1);
    memcpy(s3s + l1, s2s, l2);
    *(int*) s3 = l3;
    return s3;
}

Другой альтернативой может быть использование структуры для определения строки:

struct {
  int len; /* cannot be left implementation-defined */
  char* buf;
}

В этот момент для всех манипуляций с строками потребуются два распределения, которые на практике означают, что вы проходите через библиотеку, чтобы справиться с ней.

Самое смешное, что такие структуры существуют в C! Они просто не используются для ежедневного отображения сообщений для обработки пользователей.

Итак, вот точка, которую Calavera делает: в C. нет строкового типа. Чтобы что-то сделать с ней, вам нужно будет взять указатель и декодировать его как указатель на два разных типа, а затем он станет очень важно, каков размер строки, и ее нельзя просто оставить как "реализованную реализацию".

Теперь C может обрабатывать память в любом случае, а функции mem в библиотеке (в <string.h>, даже!) предоставляют все инструменты, необходимые для обработки памяти как пары указателя и размера. Так называемые "строки" на C были созданы только для одной цели: показ сообщений в контексте написания операционной системы, предназначенной для текстовых терминалов. И для этого нулевого завершения достаточно.

Ответ 6

Очевидно, что для повышения производительности и безопасности вы должны будете поддерживать длину строки во время работы с ней, а не многократно выполнять strlen или эквивалент на ней. Тем не менее, сохранение длины в фиксированном месте непосредственно перед содержимым строки является невероятно плохим дизайном. Как отметил Йорген в комментариях к ответе Санджита, это исключает обработку хвоста строки в виде строки, которая, например, делает невозможным множество обычных операций, таких как path_to_filename или filename_to_extension, без выделения новой памяти (и при этом возникает возможность ошибок и ошибок). И тогда, конечно, существует проблема, по которой никто не может согласиться с тем, сколько байтов должно занимать поле длины строки (много плохих "языковых строк Pascal" используют 16-битные поля или даже 24-битные поля, которые исключают обработку длинных строк).

C дизайн, позволяющий программисту выбрать, будет ли/где/как хранить длину, намного более гибким и мощным. Но, конечно, программист должен быть умным. C наказывает глупость программами, которые выходят из строя, останавливаются, или дают вашим врагам корень.

Ответ 7

Lazyness, регистрируйте бережливость и переносимость, учитывая сборку кишки любого языка, особенно C, которая на один шаг выше сборки (таким образом, наследует много устаревшего кода сборки). Вы согласитесь, что null char был бы бесполезен в те ASCII-дни, он (и, вероятно, такой же хороший, как EOF-контроль char).

см. в псевдокоде

function readString(string) // 1 parameter: 1 register or 1 stact entries
    pointer=addressOf(string) 
    while(string[pointer]!=CONTROL_CHAR) do
        read(string[pointer])
        increment pointer

всего 1 использование регистра

случай 2

 function readString(length,string) // 2 parameters: 2 register used or 2 stack entries
     pointer=addressOf(string) 
     while(length>0) do 
         read(string[pointer])
         increment pointer
         decrement length

всего 2 используемых регистра

Это может показаться недальновидным в то время, но, учитывая бережливость кода и регистра (которые были в то время PREMIUM, время, когда вы знаете, они используют перфокарту). Таким образом, будучи быстрее (когда скорость процессора может быть подсчитана в кГц), этот "Hack" был довольно неплохим и портативным для безрезультатного процессора.

Для аргументации я реализую 2 операции с общей строкой

stringLength(string)
     pointer=addressOf(string)
     while(string[pointer]!=CONTROL_CHAR) do
         increment pointer
     return pointer-addressOf(string)

сложность O (n), где в большинстве случаев строка PASCAL является O (1), поскольку длина строки предварительно привязана к строковой структуре (что также означает, что эта операция должна быть перенесена на более раннюю стадию).

concatString(string1,string2)
     length1=stringLength(string1)
     length2=stringLength(string2)
     string3=allocate(string1+string2)
     pointer1=addressOf(string1)
     pointer3=addressOf(string3)
     while(string1[pointer1]!=CONTROL_CHAR) do
         string3[pointer3]=string1[pointer1]
         increment pointer3
         increment pointer1
     pointer2=addressOf(string2)
     while(string2[pointer2]!=CONTROL_CHAR) do
         string3[pointer3]=string2[pointer2]
         increment pointer3
         increment pointer1
     return string3

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

С другой стороны, если вы используете строку PASCAL, вам придется переконфигурировать ваш API для учета длины регистра и битовой сущности, строка PASCAL получила известное ограничение 255 char (0xFF), поскольку длина была сохранена в 1 байт (8 бит), и вам нужна более длинная строка (16 бит → что угодно), которую вам нужно будет учитывать архитектуру на одном уровне вашего кода, что в большинстве случаев будет несовместимым строковым API, если вы хотите более длинную строку,

Пример:

Один файл был написан с вашей добавленной строкой api на 8-битном компьютере, а затем должен быть прочитан на 32-битном компьютере, что бы ленивая программа считала, что ваши 4 байта - это длина строки, а затем выделяют много памяти затем попытаются прочитать это много байтов. Другим случаем будет чтение строки в байтах PPC 32 (little endian) на x86 (big endian), конечно, если вы не знаете, что один написан другим, это будет проблемой. 1 байтовая длина (0x00000001) станет 16777216 (0x0100000), что составляет 16 МБ для чтения 1 байтовой строки. Конечно, вы бы сказали, что люди должны согласиться на один стандарт, но даже 16-битный юникод получил малое и большое значение.

Конечно, C тоже будет иметь свои проблемы, но будет очень мало затронут затронутыми здесь проблемами.

Ответ 8

Во многих отношениях C был примитивным. И мне это понравилось.

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

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

Оглядываясь назад, это не кажется удобным. Но я использовал ассемблерный язык еще в 80-х годах, и в то время он казался очень удобным. Я просто думаю, что программное обеспечение постоянно развивается, и платформы и инструменты постоянно становятся все более сложными.

Ответ 9

Предполагая на мгновение, что C реализованные строки, путь Pascal, путем префикса их по длине: это длинная строка длиной 7 char того же ТИПА ДАННЫХ, как строка 3 char? Если да, то какой код должен генерировать компилятор, когда я назначаю первое последнему? Должна ли строка быть усечена или автоматически изменяться? Если изменить размер, следует ли защищать эту операцию блокировкой, чтобы сделать ее безопасной? Сторона подхода С сделала все эти проблемы, вроде этого или нет:)

Ответ 10

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

#define PREFIX_STR(s) ((prefix_str_t){ sizeof(s)-1, (s) })

typedef struct { int n; char * p; } prefix_str_t;

int main() {
    prefix_str_t string1, string2;

    string1 = PREFIX_STR("Hello!");
    string2 = PREFIX_STR("Allows \0 chars (even if printf directly doesn't)");

    printf("%d %s\n", string1.n, string1.p); /* prints: "6 Hello!" */
    printf("%d %s\n", string2.n, string2.p); /* prints: "48 Allows " */

    return 0;
}

Это, однако, не будет иметь проблем, так как вам нужно быть осторожным, когда специально освобождать этот указатель на строку и когда он статически назначен (литерал char array).

Изменить:. Как более прямой ответ на вопрос, я считаю, что это способ, которым C мог поддерживать как имеющую длину строки (как постоянную времени компиляции), если она вам нужна, но все еще без накладных расходов памяти, если вы хотите использовать только указатели и нулевое завершение.

Конечно, кажется, что работа с нулевыми строками была рекомендуемой практикой, поскольку стандартная библиотека вообще не принимает длину строки в качестве аргументов, а так как извлечение длины не является таким простым кодом, как char * s = "abc", как показывает мой пример.

Ответ 11

Нулевое завершение позволяет выполнять операции с быстрым указателем.

Ответ 12

"Даже на 32-битной машине, если вы разрешаете строке быть размером доступной памяти, длина префиксной строки всего на три байта шире, чем строка с нулевым завершением".

Во-первых, дополнительные 3 байта могут быть значительными накладными расходами для коротких строк. В частности, строка с нулевой длиной теперь занимает в 4 раза больше памяти. Некоторые из нас используют 64-битные машины, поэтому нам нужно 8 байтов для хранения строки нулевой длины, или формат строки не может справиться с самыми длинными строками, поддерживаемыми платформой.

Также могут возникать проблемы с выравниванием. Предположим, у меня есть блок памяти, содержащий 7 строк, например "solo\0second\0\0four\0five\0\0seventh". Вторая строка начинается со смещения 5. Аппаратное обеспечение может требовать, чтобы 32-разрядные целые числа были выровнены по адресу, кратное 4, поэтому вам нужно добавить отступы, увеличив накладные расходы еще больше. Представление C очень экономично для сравнения. (Эффективность работы с памятью хороша, например, она позволяет работать с кешем.)

Ответ 13

Одна точка, о которой еще не упоминалось: когда C был спроектирован, было много машин, где "char" не было восьми бит (даже сегодня есть платформы DSP, где это не так). Если вы решите, что строки должны быть префиксом длины, то сколько префиксов длины char стоит использовать один? Используя два, накладывается искусственный предел длины строки для машин с 8-разрядным char и 32-разрядным адресным пространством, в то же время теряя пространство на машинах с 16-разрядным char и 16-разрядным адресным пространством.

Если бы хотелось, чтобы строки произвольной длины были эффективно сохранены, а если "char" всегда были 8 бит, можно было бы - за некоторые расходы по скорости и размеру кода - определить схему - это строка с префиксом четного числа N будет длиной в N/2 байта, строка с префиксом нечетного значения N и четное значение M (чтение назад) может быть ((N-1) + M * char_max)/2 и т.д. и т.д. требуют, чтобы любой буфер, который, как утверждается, предлагал определенное количество места для хранения строки, должен позволять достаточным байтам, предшествующим этому пространству, обрабатывать максимальную длину. Однако тот факт, что "char" не всегда является 8 битами, может усложнить такую ​​схему, поскольку число "char", необходимое для хранения длины строки, будет зависеть от архитектуры ЦП.

Ответ 14

Многие проектные решения, связанные с C, связаны с тем, что, когда он был первоначально реализован, передача параметров была несколько дорогой. Учитывая выбор между, например,

void add_element_to_next(arr, offset)
  char[] arr;
  int offset;
{
  arr[offset] += arr[offset+1];
}

char array[40];

void test()
{
  for (i=0; i<39; i++)
    add_element_to_next(array, i);
}

против

void add_element_to_next(ptr)
  char *p;
{
  p[0]+=p[1];
}

char array[40];

void test()
{
  int i;
  for (i=0; i<39; i++)
    add_element_to_next(arr+i);
}

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

Хотя существует множество разумных способов, в которых C может иметь кодированные длины строк, подходы, которые были изобретены до того времени, будут иметь все необходимые функции, которые должны иметь возможность работать с частью строки, чтобы принять базовый адрес строка и желаемый индекс как два отдельных параметра. Использование обхода нулевого байта позволило избежать этого требования. Хотя другие подходы были бы лучше с сегодняшними машинами (современные компиляторы часто передают параметры в регистрах, а memcpy можно оптимизировать способами, которые не могут быть реализованы с помощью strcpy() - эквивалентов). В достаточном производственном коде используются строки с нулевым байтом, которые трудно изменить ни на что другое.

PS. В обмен на небольшое ограничение скорости на некоторые операции и крошечный бит дополнительных накладных расходов на более длинных строках, было бы возможно иметь методы, которые работают со строками, принимают указатели непосредственно на строки, bounds-checked string буферов или структур данных, идентифицирующих подстроки другой строки. Функция типа "strcat" выглядела бы как [современный синтаксис]

void strcat(unsigned char *dest, unsigned char *src)
{
  struct STRING_INFO d,s;
  str_size_t copy_length;

  get_string_info(&d, dest);
  get_string_info(&s, src);
  if (d.si_buff_size > d.si_length) // Destination is resizable buffer
  {
    copy_length = d.si_buff_size - d.si_length;
    if (s.src_length < copy_length)
      copy_length = s.src_length;
    memcpy(d.buff + d.si_length, s.buff, copy_length);
    d.si_length += copy_length;
    update_string_length(&d);
  }
}

Немного больше, чем метод K & R strcat, но он будет поддерживать проверку границ, которую не использует метод K & R. Кроме того, в отличие от текущего способа, можно было бы легко конкатенировать произвольную подстроку, например.

/* Concatenate 10th through 24th characters from src to dest */

void catpart(unsigned char *dest, unsigned char *src)
{
  struct SUBSTRING_INFO *inf;
  src = temp_substring(&inf, src, 10, 24);
  strcat(dest, src);
}

Обратите внимание, что время жизни строки, возвращаемой temp_substring, будет ограничено значениями s и src, которые когда-либо были короче (поэтому метод требует, чтобы inf был передан - если он был local, он умрет, когда метод вернется).

С точки зрения стоимости памяти, строки и буферы до 64 байтов имеют один байт служебных данных (так же, как строки с нулевым завершением); более длинные строки будут иметь немного больше (независимо от того, разрешено ли количество накладных расходов между двумя байтами и максимально необходимым, это компромисс между временем/пространством). Специальное значение байта длины/режима будет использоваться, чтобы указать, что строковой функции была предоставлена ​​структура, содержащая байт-указатель, указатель и длину буфера (которые затем могут произвольно индексироваться в любую другую строку).

Конечно, K & R не реализовал такую ​​вещь, но это, скорее всего, потому, что они не хотели тратить много усилий на обработку строк - область, где даже сегодня многие языки кажутся довольно анемичными.

Ответ 15

По словам Джоэла Спольского в этом сообщении в блоге,

Это потому, что микропроцессор PDP-7, на котором был изобретен язык программирования UNIX и C, имел тип строки ASCIZ. ASCIZ означало "ASCII с Z (ноль) в конце".

После просмотра всех других ответов здесь я убежден, что даже если это так, это лишь часть причины, когда C имеет "строки" с нулевым символом. Этот пост достаточно освещает, как простые вещи, такие как строки, могут быть довольно сложными.

Ответ 16

gcc принять следующие коды:

char s [4] = "abcd";

и это нормально, если мы рассматриваем это как массив символов, но не строку. То есть мы можем получить к нему доступ с помощью s [0], s [1], s [2] и s [3] или даже с memcpy (dest, s, 4). Но мы будем получать беспорядочные символы, когда мы пытаемся использовать puts (s), или хуже, с помощью strcpy (dest, s).