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

240

Насколько я люблю 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.

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

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

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

  • 103
    Я всегда думал, что для всех программистов C ++ это был обряд, когда они пишут свою собственную библиотеку строк.
  • 1
    @Juliet: Lol - это правда. Но это не значит, что они должны использовать свою библиотеку строк в производственном коде. Я буду придерживаться стандартных битов TYVM :)
Показать ещё 45 комментариев
Теги:
string
null-terminated

16 ответов

184
Лучший ответ

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

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

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

  • 11
    Другая релевантная цитата: «... семантика строк полностью подчиняется более общим правилам, регулирующим все массивы, и в результате язык проще описать ...»
142

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

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

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

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

  • 0
    @calavera это не неправильный вопрос. это asciiz тип asciiz или символьные массивы с нулевым символом в asciiz .
  • 25
    char *temp = "foo bar"; является действительным утверждением на C ... эй! разве это не строка? разве это не завершено?
Показать ещё 39 комментариев
88

Вопрос задается как вещь 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).

  • 1
    @kriss: очень хороший ответ. Я ценю, что кто-то еще признает, что исходный вопрос имеет некоторую редакционную актуальность и не совсем то, чем кажется.
  • 1
    @kriss: Мой вопрос: «почему были выбраны строки с нулевым символом в конце». Я знаю, что есть лучшие способы решения проблем с использованием библиотек. Но всякий раз, когда вы обращаетесь к решению библиотеки, как эта проблема, большая часть того, что вы получаете, теряется из-за необходимости склеивать вашу библиотеку, используя код к существующему коду. Учитывая, что стандарт использует строки с нулевым символом в конце, это то, что вы застряли. (И иногда мне все еще приходится писать этот вид клея, потому что существующий код не поддерживает i18n GRRR). Кроме того, я думаю, что некоторые из ваших пунктов в равной степени применимы к префиксу длины (то есть библиотечные функции).
Показать ещё 52 комментария
60

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

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

  • 0
    Но это было давно! Почему стандарт не меняется, так что строка имеет 4-байтовый «заголовок Паскаля»?
  • 1
    @muntoo Хм ... совместимость?
Показать ещё 6 комментариев
28

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 были созданы только для одной цели: показ сообщений в контексте написания операционной системы, предназначенной для текстовых терминалов. И для этого нулевого завершения достаточно.

  • 2
    1. +1. 2. Очевидно, что если бы поведение языка по умолчанию было выполнено с использованием префиксов длины, были бы другие способы сделать это проще. Например, все ваши приведения были бы скрыты при вызовах strlen и друзей. Что касается проблемы с «оставлением на усмотрение реализации», вы можете сказать, что префикс - это любой short в целевом блоке. Тогда все ваши кастинги все равно будут работать. 3. Я могу придумывать надуманные сценарии в течение всего дня, которые делают ту или иную систему плохой.
  • 5
    @Billy Суть библиотеки достаточно верна, за исключением того факта, что C был разработан для минимального использования библиотеки или вообще без него. Например, использование прототипов не было обычным делом на ранних этапах. Сказать, что префикс short эффективно ограничивает размер строки, что, похоже, является одной из причин, которой они не увлекались. Я сам, работая с 8-битными строками BASIC и Pascal, строками COBOL фиксированного размера и подобными вещами, быстро стал большим поклонником C-строк неограниченного размера. В настоящее время 32-битный размер будет обрабатывать любую практическую строку, но добавление этих байтов на ранних этапах было проблематичным.
Показать ещё 4 комментария
17

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

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

  • 1
    да, наиболее важным моментом, вероятно, является распределение памяти.
  • 0
    +1. Было бы неплохо иметь стандартное место для хранения длины, хотя бы тем из нас, кто хочет что-то вроде префикса длины, не приходилось везде писать тонны «клеевого кода».
Показать ещё 1 комментарий
10

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 тоже будет иметь свои проблемы, но будет очень мало затронут затронутыми здесь проблемами.

  • 0
    Тогда почему манипулирование строками в C менее эффективно, чем где бы то ни было?
  • 0
    @Billy ONeal: Можете ли вы определить, что вы подразумеваете под «эффективнее, чем где-либо еще»?
Показать ещё 21 комментарий
6

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

  • 2
    Эээ .. нет, это не так. Подход C вообще не позволяет назначать строку длиной 7 символов для строки длиной 3 символа.
  • 0
    @Billy ONeal: почему бы и нет? Насколько я понимаю, в этом случае все строки имеют одинаковый тип данных (char *), поэтому длина не имеет значения. В отличие от Паскаля. Но это было ограничением Паскаля, а не проблемой строк с префиксом длины.
Показать ещё 3 комментария
6

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

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

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

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

  • 0
    Я не вижу ничего более примитивного в строках с нулевым символом в конце, чем все остальное. Паскаль предшествует C и использует префикс длины. Конечно, он был ограничен 256 символами на строку, но простое использование 16-битного поля решило бы проблему в подавляющем большинстве случаев.
  • 0
    Тот факт, что количество символов ограничено, является именно тем типом проблем, о которых вам нужно подумать, когда вы делаете что-то подобное. Да, вы можете сделать это длиннее, но тогда байты имели значение. И будет ли 16-битное поле достаточно длинным для всех случаев? Да ладно, вы должны признать, что нулевой термин является концептуально примитивным.
Показать ещё 2 комментария
5

Как-то я понял, что вопрос подразумевает отсутствие поддержки компилятора строк с префиксом длины в 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", как показывает мой пример.

  • 0
    Проблема в том, что библиотеки не знают о существовании вашей структуры и по-прежнему неправильно обрабатывают такие вещи, как встроенные нули. Кроме того, это на самом деле не отвечает на вопрос, который я задал.
  • 1
    Это правда. Таким образом, большая проблема заключается в том, что нет лучшего стандартного способа предоставления интерфейсов со строковыми параметрами, чем простые старые строки с нулевым окончанием. Я бы по-прежнему утверждал, что есть библиотеки, которые поддерживают подачу пар длины указателя (ну, по крайней мере, вы можете создать из них строку std :: string C ++).
Показать ещё 5 комментариев
4

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

  • 5
    А? Какие «быстрые операции с указателями» не работают с префиксом длины? Что еще более важно, другие языки, которые используют префикс длины, работают быстрее, чем манипуляции со строками.
  • 12
    @billy: Со строками с префиксом длины вы не можете просто взять указатель на строку и добавить 4 к нему, и ожидать, что он все еще будет действительной строкой, потому что у нее нет префикса длины (в любом случае, не является действительным).
Показать ещё 12 комментариев
3

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

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

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

  • 0
    Я полагаю, что обратился ко всему этому в вопросе. Да, на платформах x64 32-битный префикс не может вместить все возможные строки. С другой стороны, вам никогда не нужно, чтобы строка была такой же большой, как строка с нулевым символом в конце, потому что для того, чтобы что-то сделать, вам нужно изучить все 4 миллиарда байтов, чтобы найти конец почти для каждой операции, которую вы можете захотеть сделать с ней. Кроме того, я не говорю, что строки с нулевым символом в конце всегда являются злом - если вы строите одну из этих блочных структур, и ваше конкретное приложение ускоряется подобной конструкцией, продолжайте. Я просто хотел бы, чтобы поведение языка по умолчанию не делало этого.
  • 2
    Я процитировал эту часть вашего вопроса, потому что, на мой взгляд, он недооценил проблему эффективности. Удвоение или увеличение требований к памяти (на 16-битной и 32-битной памяти соответственно) может привести к значительным потерям производительности. Длинные строки могут быть медленными, но, по крайней мере, они поддерживаются и продолжают работать. Мой другой вопрос, о выравнивании, вы вообще не упоминаете.
Показать ещё 2 комментария
1

Одна точка, о которой еще не упоминалось: когда 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", необходимое для хранения длины строки, будет зависеть от архитектуры ЦП.

  • 0
    Префикс может легко иметь размер, определенный реализацией, так же как и sizeof(char) .
  • 0
    @BillyONeal: sizeof(char) один. Всегда. Можно иметь префикс, определяемый размером реализации, но это будет неудобно. Кроме того, нет никакого реального способа узнать, каким должен быть «правильный» размер. Если один содержит много 4-символьных строк, заполнение нулями будет накладывать 25% накладных расходов, в то время как префикс длины в четыре байта будет накладывать 100% накладных расходов. Кроме того, время, потраченное на упаковку и распаковку четырехбайтовых префиксов длины, может превысить стоимость сканирования 4-байтовых строк на нулевой байт.
Показать ещё 8 комментариев
0

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

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

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

  • 2
    Смотри, я уважаю Джоэла за многие вещи; но это то, что он спекулирует. Ответ Ханса Пассанта исходит непосредственно от изобретателей Си.
  • 1
    Да, но если то, что говорит Спольский, вообще верно, то это было бы частью «удобства», о котором они говорили. Отчасти поэтому я включил этот ответ.
Показать ещё 2 комментария
0

Многие проектные решения, связанные с 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 не реализовал такую ​​вещь, но это, скорее всего, потому, что они не хотели тратить много усилий на обработку строк - область, где даже сегодня многие языки кажутся довольно анемичными.

  • 0
    Нет ничего, что могло бы помешать char* arr указывать на структуру вида struct { int length; char characters[ANYSIZE_ARRAY] }; или подобный, который все еще мог бы быть проходимым как единственный параметр.
  • 0
    @BillyONeal: две проблемы с этим подходом: (1) Это позволило бы только передать строку целиком, тогда как настоящий подход также позволяет передавать хвост строки; (2) он будет тратить значительное пространство при использовании с небольшими струнами. Если бы K & R захотели потратить некоторое время на струны, они могли бы сделать вещи намного более надежными, но я не думаю, что они предполагали, что их новый язык будет использоваться через десять лет, а тем более - сорок.
Показать ещё 26 комментариев
-4

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

char s [4] = "abcd";

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

  • 1
    Это просто неправильно. "abcd" требует пять байтов (из-за завершающего нулевого байта) и не помещается в char[4] .

Ещё вопросы

Сообщество Overcoder
Наверх
Меню