Насколько я люблю C и С++, я не могу не почесать голову при выборе нулевых завершенных строк:
std::basic_string
, но простые массивы символов, ожидающие нулевых завершенных строк, все еще распространены. Это также несовершенно, потому что для этого требуется выделение кучи.Некоторые из этих вещей появились совсем недавно, чем C, поэтому было бы полезно, чтобы C не знал о них. Тем не менее, некоторые из них были хорошо известны до того, как C стал. Почему были выбраны нулевые завершенные строки вместо префикса явно превосходящей длины?
РЕДАКТИРОВАТЬ: поскольку некоторые из вас попросили факты (и мне не понравились те, которые я уже предоставил) по моей эффективности выше, они вытекают из нескольких вещей:
Из приведенных ниже ответов, это некоторые случаи, когда строки с нулевым завершением являются более эффективными:
Ни один из вышеперечисленных не является столь же общим, как длина и concat.
В ответах ниже сказано следующее:
но это неверно - это такое же количество времени для строк с нулевым завершением и длиной префикса. (Строки с нулевым завершающим строком просто вставляют нуль, где вы хотите, чтобы новый конец был, префиксы длины просто вычитают из префикса.)
Нет поддержки BCPL, B или C символьных данных в язык; каждый трактует строки много как векторы целых чисел и дополняет общие правила несколькими конвенций. Как в BCPL, так и в B a Строковый литерал обозначает адрес статическая область, инициализированная символы строки, упакованные в клетки. В BCPL первый упакованный байт содержит количество символов в Струна; в B нет счета и строки заканчиваются специальный символ, который B пишется
*e
. Это изменение было сделано частично во избежание ограничения длины строки, вызванной счет в 8- или 9-битном слоте и отчасти потому, что поддержание счета казалось, по нашему опыту, меньше удобно, чем использование терминатора.
Деннис М Ричи, разработка языка C
C не содержит строку как часть языка. "Строка" в C - это просто указатель на char. Так что, возможно, вы задаете неправильный вопрос.
"Какое обоснование для исключения типа строки" может быть более актуальным. Для этого я хотел бы указать, что C не является объектно-ориентированным языком и имеет только базовые типы значений. Строка представляет собой концепцию более высокого уровня, которая должна быть реализована путем объединения значений других типов. C находится на более низком уровне абстракции.
Я просто хочу указать, что я не пытаюсь сказать, что это глупый или плохой вопрос, или что способ представления строк - это лучший выбор. Я пытаюсь уточнить, что вопрос будет более лаконичным, если учесть тот факт, что C не имеет механизма для дифференциации строки как типа данных из массива байтов. Это лучший выбор в свете обработки и памяти сегодняшних компьютеров? Возможно нет. Но задним числом всегда 20/20, и все это:)
asciiz
тип asciiz
или символьные массивы с нулевым символом в asciiz
.
char *temp = "foo bar";
является действительным утверждением на C ... эй! разве это не строка? разве это не завершено?
Вопрос задается как вещь Length Prefixed Strings (LPS)
vs zero terminated strings (SZ)
, но в основном раскрывает преимущества префиксных строк длины. Это может показаться ошеломляющим, но, честно говоря, мы также должны учитывать недостатки LPS и преимущества SZ.
Как я понимаю, вопрос может быть даже понят как предвзятый способ спросить "в чем преимущества Zero Terminated Strings?".
Преимущества (я вижу) строк с нулевым завершением:
"this\0is\0valid\0C"
.
Это строка? или четыре строки? Или куча байтов...char a[3] =
"foo";
имеет значение C (не С++) и
не ставит конечный ноль в.char*
. а именно
не возвращать адрес строки, а вместо этого возвращать фактические данные.Тем не менее, нет необходимости жаловаться в редком случае, когда стандартные строки 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).
Я думаю, он имеет исторические причины и нашел это в википедии:
В то время C (и языки, которые он был получен из) были разработаны, память была крайне ограничена, поэтому использование только один байт накладных расходов для хранения длина строки была привлекательной. только популярная альтернатива в то время, обычно называемый "строкой Паскаля", (хотя также используется ранними версиями BASIC), используется старший байт для хранения длина строки. Это позволяет строка, содержащая NUL и сделанная найти длину нужно только один доступ к памяти (время O (1) (постоянное)). Но один байт ограничивает длину до 255. Это ограничение длины было намного больше чем проблемы с C, так что строка C вообще выиграл.
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 были созданы только для одной цели: показ сообщений в контексте написания операционной системы, предназначенной для текстовых терминалов. И для этого нулевого завершения достаточно.
strlen
и друзей. Что касается проблемы с «оставлением на усмотрение реализации», вы можете сказать, что префикс - это любой short
в целевом блоке. Тогда все ваши кастинги все равно будут работать. 3. Я могу придумывать надуманные сценарии в течение всего дня, которые делают ту или иную систему плохой.
short
эффективно ограничивает размер строки, что, похоже, является одной из причин, которой они не увлекались. Я сам, работая с 8-битными строками BASIC и Pascal, строками COBOL фиксированного размера и подобными вещами, быстро стал большим поклонником C-строк неограниченного размера. В настоящее время 32-битный размер будет обрабатывать любую практическую строку, но добавление этих байтов на ранних этапах было проблематичным.
Очевидно, что для повышения производительности и безопасности вы должны будете поддерживать длину строки во время работы с ней, а не многократно выполнять strlen
или эквивалент на ней. Тем не менее, сохранение длины в фиксированном месте непосредственно перед содержимым строки является невероятно плохим дизайном. Как отметил Йорген в комментариях к ответе Санджита, это исключает обработку хвоста строки в виде строки, которая, например, делает невозможным множество обычных операций, таких как path_to_filename
или filename_to_extension
, без выделения новой памяти (и при этом возникает возможность ошибок и ошибок). И тогда, конечно, существует проблема, по которой никто не может согласиться с тем, сколько байтов должно занимать поле длины строки (много плохих "языковых строк Pascal" используют 16-битные поля или даже 24-битные поля, которые исключают обработку длинных строк).
C дизайн, позволяющий программисту выбрать, будет ли/где/как хранить длину, намного более гибким и мощным. Но, конечно, программист должен быть умным. C наказывает глупость программами, которые выходят из строя, останавливаются, или дают вашим врагам корень.
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 тоже будет иметь свои проблемы, но будет очень мало затронут затронутыми здесь проблемами.
Предполагая на мгновение, что C реализованные строки, путь Pascal, путем префикса их по длине: это длинная строка длиной 7 char того же ТИПА ДАННЫХ, как строка 3 char? Если да, то какой код должен генерировать компилятор, когда я назначаю первое последнему? Должна ли строка быть усечена или автоматически изменяться? Если изменить размер, следует ли защищать эту операцию блокировкой, чтобы сделать ее безопасной? Сторона подхода С сделала все эти проблемы, вроде этого или нет:)
Во многих отношениях C был примитивным. И мне это понравилось.
Это был шаг выше языка ассемблера, давая вам почти такую же производительность с языком, который гораздо проще писать и поддерживать.
Нулевой терминатор прост и не требует специальной поддержки языка.
Оглядываясь назад, это не кажется удобным. Но я использовал ассемблерный язык еще в 80-х годах, и в то время он казался очень удобным. Я просто думаю, что программное обеспечение постоянно развивается, и платформы и инструменты постоянно становятся все более сложными.
Как-то я понял, что вопрос подразумевает отсутствие поддержки компилятора строк с префиксом длины в 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"
, как показывает мой пример.
Нулевое завершение позволяет выполнять операции с быстрым указателем.
"Даже на 32-битной машине, если вы разрешаете строке быть размером доступной памяти, длина префиксной строки всего на три байта шире, чем строка с нулевым завершением".
Во-первых, дополнительные 3 байта могут быть значительными накладными расходами для коротких строк. В частности, строка с нулевой длиной теперь занимает в 4 раза больше памяти. Некоторые из нас используют 64-битные машины, поэтому нам нужно 8 байтов для хранения строки нулевой длины, или формат строки не может справиться с самыми длинными строками, поддерживаемыми платформой.
Также могут возникать проблемы с выравниванием. Предположим, у меня есть блок памяти, содержащий 7 строк, например "solo\0second\0\0four\0five\0\0seventh". Вторая строка начинается со смещения 5. Аппаратное обеспечение может требовать, чтобы 32-разрядные целые числа были выровнены по адресу, кратное 4, поэтому вам нужно добавить отступы, увеличив накладные расходы еще больше. Представление C очень экономично для сравнения. (Эффективность работы с памятью хороша, например, она позволяет работать с кешем.)
Одна точка, о которой еще не упоминалось: когда 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", необходимое для хранения длины строки, будет зависеть от архитектуры ЦП.
sizeof(char)
.
sizeof(char)
один. Всегда. Можно иметь префикс, определяемый размером реализации, но это будет неудобно. Кроме того, нет никакого реального способа узнать, каким должен быть «правильный» размер. Если один содержит много 4-символьных строк, заполнение нулями будет накладывать 25% накладных расходов, в то время как префикс длины в четыре байта будет накладывать 100% накладных расходов. Кроме того, время, потраченное на упаковку и распаковку четырехбайтовых префиксов длины, может превысить стоимость сканирования 4-байтовых строк на нулевой байт.
По словам Джоэла Спольского в этом сообщении в блоге,
Это потому, что микропроцессор PDP-7, на котором был изобретен язык программирования UNIX и C, имел тип строки ASCIZ. ASCIZ означало "ASCII с Z (ноль) в конце".
После просмотра всех других ответов здесь я убежден, что даже если это так, это лишь часть причины, когда C имеет "строки" с нулевым символом. Этот пост достаточно освещает, как простые вещи, такие как строки, могут быть довольно сложными.
Многие проектные решения, связанные с 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 не реализовал такую вещь, но это, скорее всего, потому, что они не хотели тратить много усилий на обработку строк - область, где даже сегодня многие языки кажутся довольно анемичными.
char* arr
указывать на структуру вида struct { int length; char characters[ANYSIZE_ARRAY] };
или подобный, который все еще мог бы быть проходимым как единственный параметр.
gcc принять следующие коды:
char s [4] = "abcd";
и это нормально, если мы рассматриваем это как массив символов, но не строку. То есть мы можем получить к нему доступ с помощью s [0], s [1], s [2] и s [3] или даже с memcpy (dest, s, 4). Но мы будем получать беспорядочные символы, когда мы пытаемся использовать puts (s), или хуже, с помощью strcpy (dest, s).
"abcd"
требует пять байтов (из-за завершающего нулевого байта) и не помещается в char[4]
.