Почему строки с нулевым завершением? Или: с нулевым завершением против символов + длина хранения

Я пишу интерпретатор языка в C, а мой тип string содержит атрибут length, например:

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

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

Проверка границ встроена, если вы используете "длину" вместо поиска нулевого значения.

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

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

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

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

String slice (примечание: строки неизменяемы на моем языке). Очевидно, что второй медленнее (и более подвержен ошибкам: подумайте о добавлении проверки ошибок 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, которые мне не хватает?

Ответ 1

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

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

Ответ 2

От Joel Назад к основам:

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

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

Ответ 3

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

Ответ 4

Одно из преимуществ строк с nul-terminated заключается в том, что если вы проходите по строкам по-символу, вам нужно всего лишь указать один указатель на строку:

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 были скудным ресурсом (и компиляторы были хуже при их распределении), это было важно. Теперь не так много.

Ответ 5

У длин тоже есть свои проблемы.

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

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

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

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

Ответ 6

Просто выкидывайте некоторые гипотезы:

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

Ответ 7

Немного offtopic, но есть более эффективный способ делать строки с префиксом длиной до тех пор, как вы описываете. Создайте такую ​​структуру (действующую на C99 и выше):

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

Это создает структуру, которая имеет длину в начале, с элементом 'characters', который можно использовать как char * так же, как и с вашей текущей структурой. Разница, однако, заключается в том, что вы можете выделить только один элемент в куче для каждой строки вместо двух. Выделите свои строки следующим образом:

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

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

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

Ответ 8

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

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

Чтобы сохранить 7-байтовую строку
char * + size_t + 8 bytes = 19 bytes

Чтобы сохранить нулевую строку длиной 7 байтов
char * + 8 = 16 байт.

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

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

Ответ 9

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

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

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

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

Ответ 10

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