2009-08-10 2 views
24

Я пишу интерпретатор языка в C, и мой string типа содержит атрибут length, например так:Почему строки с нулевым символом? Или: завершающий нуль против хранения символов + длиной

struct String 
{ 
    char* characters; 
    size_t length; 
}; 

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

Проверка границ встроена, если вы используете «длину» вместо ищет нуль.

Вы должны пройти всю строку, чтобы найти ее длину.

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

Строки с нулевым завершением плохо работают с Unicode.

Строки, не содержащие нуль, могут быть более статичными, то есть символы для «Hello, world» и «Hello» могут храниться в одном и том же месте с разной длиной. Это невозможно сделать с нулевыми строками.

Строковый срез (примечание: строки неизменяемы на моем языке). Очевидно, что вторая медленнее (и более подвержена ошибкам: подумайте о добавлении проверки ошибок begin и end к обеим функциям). не

struct String slice(struct String in, size_t begin, size_t end) 
{ 
    struct String out; 
    out.characters = in.characters + begin; 
    out.length = end - begin; 

    return out; 
} 

char* slice(char* in, size_t begin, size_t end) 
{ 
    char* out = malloc(end - begin + 1); 

    for(int i = 0; i < end - begin; i++) 
     out[i] = in[i + begin]; 

    out[end - begin] = '\0'; 

    return out; 
} 

После всего этого, мое мышление уже не о том, должен ли я использовать строки нулевым символом в конце: Я думаю о том, почему C использует их!

Итак, мой вопрос: есть ли какие-либо преимущества для null-term, которые мне не хватает?

+0

Поскольку malloc() настолько дорог в C, я предлагаю использовать эту структуру: struct String {size_t length; char [1] символов; } Просто выделите строки strlen (s) + 1 + sizeof (size_t) или strlen (s) + sizeof (String) и скопируйте строку в адрес и символы. –

+0

Это просто. В этом польза. –

ответ

15

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

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

+1

Это то, что делает Луа. Это делает взаимодействие с C для нормальных случаев использования очень чистым и по-прежнему поддерживает двоичные буферы произвольной длины. – RBerteig

+3

Это то, что делают большинство! Вам даже не нужно поддерживать нулевой терминатор все время - просто делайте 'str [len] = '\ 0'', когда вам нужно. Это то, что обычно означает 'std :: string :: c_str'' на C++. –

+0

В большинстве случаев я имею в виду большинство строковых классов и большинство представлений строк интерпретатора. Одним из широко используемых примеров в Windows является тип BSTR. –

0

Я думаю, что основная причина заключается в том, что стандарт не говорит ничего конкретного о размере любого типа, кроме символа. Но sizeof (char) = 1, и это определенно недостаточно для размера строки.

+0

Этого достаточно для 2^CHAR_BIT символов в строке. –

+0

Это всего 255 символов. Слишком мало. –

+0

Нет, этого достаточно для 2^CHAR_BIT - 1 символ в строке, и если вам никогда не приходилось иметь дело со строками длиной более 255 символов, вам нужно было только программировать для очень ограниченного проблемного домена.Однако C * делает конкретные вещи о других интегральных типах - например, int должен иметь диапазон от -32767 до +32767. В частности, C говорит, что size_t должен уметь удерживать размер любого объекта, так что это было бы нормально, как стандартизованная длина строки. – caf

7

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

+0

Можете ли вы привести пример строки, в которой вы можете напечатать конец строки? – weiqure

+0

Это можно использовать при конкатенации строк - вы можете захотеть добавить не всю строку, а только ее хвост. Затем вы вызываете strcat (target, source + offset); - и все сделано. – sharptooth

+0

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

1

Хотя я предпочитаю метод массива + len в большинстве случаев, существуют веские причины для использования с нулевым завершением.

Возьмите 32-битную систему.

Для хранения 7 Байт Строки
символа * + size_t + 8 байт = 19 байт

Для хранения 7 нулевых байт-Term строку
символа * +-= 16 байт.

Нулевые массивы не обязательно должны быть неизменяемыми, как ваши строки. Я могу с удовольствием обрезать c-строку, просто помещая нулевой символ. Если вы код, вам нужно будет создать новую строку, которая предполагает выделение памяти.

В зависимости от использования строк ваши строки никогда не смогут сопоставить производительность с помощью c-строк в отличие от ваших строк.

+1

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

+3

Действительно, это путь; Я основал свой ответ на строковой структуре op, которая позволит вам уменьшить длину, но никогда больше не использовать это пространство. Веревки - еще один интересный способ обработки строк. http://en.wikipedia.org/wiki/Rope_(computer_science) –

+0

Несколько вопросов: если байтом является 8 бит, не должна 32-битная система иметь 'sizeof (size_t) == 4' и' sizeof (char *) == 4'? И с помощью моего метода вам не нужно использовать 8 символов для первого метода. Поэтому я получаю: 4 + 4 + 7 = 15 для моего метода и 4 + 8 = 12 для метода нулевого завершения. Я не оспариваю вашу точку зрения, просто математика, которая ведет к вашей точке. – Imagist

5

Длина также имеет свои проблемы.

  • Длина занимает дополнительное пространство (не такая проблема сейчас, но большой фактор 30 лет назад).

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

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

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

+1

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

+1

@Jimmy Моя проблема: на таких встроенных системах, почему вы используете строки? Я не думаю, что когда-либо использовал «char», когда я занимался программированием робототехники.Единственный пример, который я могу придумать, - это программировать светодиодный дисплей (например, прокручивать текстовые файлы или те, что есть на машинах для безалкогольных напитков), но функциональность там настолько проста, что мне все еще сложно представить себе дополнительные 3 байта (4 байта int - 1 байт, так как вам не нужно хранить нулевой символ). – Imagist

+0

То, что вы предлагаете, - это не то, что тривиально. Кто будет владеть буфером? Будет ли созданная временная строка делать это? Я сомневаюсь, что вы этого хотите, а затем вам нужен способ иметь такие не владеющие строки, чтобы избежать копирования. – sharptooth

4

Просто выбросить некоторые гипотетики:

  • нет никакого способа, чтобы получить «неправильное» выполнение нуля строки. Однако стандартизованная структура может иметь специфические для поставщика варианты.
  • никаких структур не требуется. Строки, завершенные нулем, являются «встроенными», так сказать, благодаря частному случаю char *.
29

От Джоэла Back to Basics:

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

Это единственный способ хранения строк? Нет, на самом деле, это один из худших способов хранения строк. Для нетривиальных программ, API, операционных систем, библиотек классов вам следует избегать строк ASCIZ, таких как чума.

+0

+1 Назад к основам - отличный пост. –

+17

Denis Ritchie мнение несколько отличается. BCPL имеет представление длины + содержимого, длина которого содержится в одном байте. B переключился на завершенную строку «частично, чтобы избежать ограничения длины строки, вызванного удерживанием счета в слоте с 8 или 9 бит, а отчасти потому, что поддержание счета казалось, по нашему опыту, менее удобным, чем использование терминатора». («Развитие языка C», http://cm.bell-labs.com/cm/cs/who/dmr/chist.pdf) – AProgrammer

6

Одним из преимуществ NUL байт, что если вы идете через строку символов по-символ, вам нужно только сохранить один указатель на адрес строки:

while (*s) 
{ 
    *s = toupper(*s); 
    s++; 
} 

в то время как для строки без часовых, вы должны держать два бита состояния вокруг: либо указатель и индекс:

while (i < s.length) 
{ 
    s.data[i] = toupper(s.data[i]); 
    i++; 
} 

... или текущий указатель и предел:

s_end = s + length; 
while (s < s_end) 
{ 
    *s = toupper(*s); 
    s++; 
} 

Когда регистры CPU были скудным ресурсом (а компиляторы были хуже при их распределении), это было важно. Теперь не так много.

+4

«Когда регистры CPU были скудным ресурсом» - регистры по-прежнему являются дефицитным ресурсом x86 и x64. – Jimmy

+0

Я не понимаю; если я храню строку в примере 'struct', который я дал, почему я не могу использовать это как предел? – Imagist

+1

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

1

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

Мне понравился способ хранения Delphi строк. Я считаю, что он поддерживает длину/максимальную длину перед строкой (переменной длины). Таким образом, для совместимости строки могут заканчиваться нулями.

Мои проблемы с вашим механизмом: - дополнительный указатель - неизменяемость si в основных частях вашего языка; обычно типы строк не являются неизменяемыми, поэтому, если вы когда-либо пересматриваете, это будет сложно. Вам нужно будет реализовать механизм «создать копию при замене» - использование malloc (вряд ли эффективно, но может быть включено здесь просто для удобства?)

Удачи вам; Написание собственного переводчика может быть очень познавательным в понимании в основном грамматики и синтаксиса языков программирования! (по крайней мере, это для меня)

+2

Языки _Most_ высокого уровня имеют неизменяемые строки. –

4

Немного оффтопный, но есть более эффективный способ делать строки с префиксом длины, чем описанные вами. Создать-структуру, как это (действует в С99 и выше):

struct String 
{ 
    size_t length; 
    char characters[0]; 
} 

Это создает структуру, которая имеет длину в начале, с ЭЛЕМЕНТ "персонажей, используемой в качестве полукокса * так же, как вы бы с током структура. Разница, однако, заключается в том, что вы можете выделить только один элемент в куче для каждой строки вместо двух. Выделяют ваши строки так:

mystr = malloc(sizeof(String) + strlen(cstring)) 

Eg - по длине структуры (который является только size_t) плюс достаточно места, чтобы поставить фактическую строку после нее.

Если вы не хотите использовать C99, вы также можете сделать это с помощью «char characters [1]» и вычесть 1 из длины строки для выделения.