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

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

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

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

Bounds-проверка встроена, если вы используете "длина" вместо поиска null.

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

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

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

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

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] = '';

    return out;
}

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

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

10 ответов


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

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


от вернуться к основам:

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

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


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


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

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

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

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

...или текущий указатель и ограничение:

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

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


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

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

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

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

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

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


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

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

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

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

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


просто выбрасываю гипотетически:

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

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

возьмите 32-разрядную систему.

для хранения строки 7 байт
char * + size_t + 8 байт = 19 байт

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

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

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


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

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

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

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


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