Правильно разграничивая строки в C - PullRequest
4 голосов
/ 04 ноября 2010

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

char *str_1 = "foo; for|* 1.234+\"@!`";
char *str_n = "bar; for|* 1.234+%\"@`";

для конечной строки как:

char *str_final = "foo; for|* 1.234+\"@!`bar; for|* 1.234+%\"@`"; // split?

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

Обратите внимание, что может быть более 2 строк для объединения.

Я открыт для предложений.

Спасибо

Ответы [ 6 ]

3 голосов
/ 05 ноября 2010

Поскольку мои комментарии становились все длиннее и длиннее, вот полный ответ:

Ваш буфер char * должен хранить длину строки в первых X байтах (например, как это делает Паскаль). После этой длины идут строковые данные, которые могут содержать любые символы, которые вам нравятся. После этого следующие X байтов сообщают вам длину строки next . И так далее, до конца, который ограничен пустой строкой (т. Е. Последние байты X утверждают, что следующая строка имеет нулевую длину, и ваше приложение воспринимает это как сигнал о прекращении поиска дополнительных строк).

Одним из преимуществ является то, что вам не нужно сканировать строковые данные - поиск следующей строки в начале первой строки занимает O (1) времени, а поиск количества строк в вашем списке занимает O (n). ) время, но все равно будет невероятно быстрым (если O (n) неприемлемо, вы можете обойти это, но я не думаю, что стоит начинать прямо сейчас).

Еще одним преимуществом является то, что строковые данные могут содержать любой символ, который вам нравится. Это может быть мошенничеством - если ваша строка может содержать символ NUL, вы можете безопасно извлечь ее, но вы должны быть осторожны, чтобы не передавать ее в строковую функцию C (например, strlen() или strcat()), которая увидит NUL-символ как конец ваших данных (которые могут быть или не быть). Вам придется полагаться на memcpy() и арифметику указателей.

Проблема заключается в значении X (количество байтов, которое вы используете для хранения длины строки). Самым простым будет 1, который обойдет все проблемы с порядком байтов и выравниванием, но ограничит ваши строки 255 символами. Если это ограничение, с которым вы можете жить, отлично, но 255 мне кажется немного низким.

X может быть 2 или 4 байта, но вам необходимо убедиться, что у вас есть (беззнаковый) тип данных, который по крайней мере столько же байтов (stdint.h uint16_t или uint32_t, либо, возможно, uint_least16_t или uint_least32_t). Лучшим решением было бы сделать X = sizeof(size_t), поскольку тип size_t гарантированно сможет хранить длину любой строки, которую вы захотите сохранить.

Наличие X > 1 вводит выравнивание и, если переносимость сети является проблемой, порядковый номер. Самый простой способ прочитать первые X байтов как переменную size_t - это преобразовать ваши данные char * в size_t * и просто разыменовать. Однако, если вы не можете гарантировать, что ваши char * данные выровнены должным образом, в некоторых системах это будет нарушено. Даже если вы гарантируете выравнивание данных char *, вам придется тратить несколько байтов в конце большинства строк, чтобы убедиться, что значение длины следующей строки выровнено.

Самый простой способ преодоления выравнивания - вручную преобразовать первые sizeof(size_t) байтов в значение size_t. Вам нужно будет решить, хотите ли вы, чтобы данные хранились в порядке байтов. Большинство компьютеров будут иметь непосредственный порядок байтов, но для ручного преобразования это не имеет значения - просто выберите один. Число 65537 (2 ^ 16 + 2), хранящееся в 4 байтах, с прямым порядком байтов, выглядит как { 0, 1, 0, 2 }; little-endian, { 2, 0, 1, 0 }.

Как только вы решили, что (неважно, выберите тот, который вам нравится), вы просто приводите первые X точек данных к unsigned char с, затем к size_t, затем делаете сдвиг битов по соответствующему показателю, чтобы поместить их в нужное место, а затем сложить их все вместе. В приведенных выше примерах 0 будет умножено на 2 ^ 32, 1 на 2 ^ 16, 0 на 2 ^ 8 и 2 на 2 ^ 0 (или 1), что приведет к 0 + 65536 + 0 + 2 или 65537. Там, вероятно, будет нулевая разница в эффективности между старшим и младшим порядком байтов, если вы будете выполнять ручное преобразование - я хочу отметить (снова), насколько я могу судить, выбор совершенно произвольный.

Ручное преобразование позволяет избежать проблем с выравниванием, и полностью обходит проблемы, связанные с порядком байтов между системами, поэтому данные, передаваемые с компьютера с прямым порядком байтов на компьютер с прямым порядком байтов, будут считываться одинаково. Все еще существует потенциальная проблема с передачей данных из системы, где sizeof(size_t) == 4, в систему, где sizeof(size_t) == 8. Если это проблема, вы можете либо а) отказаться от size_t и выбрать инвариантный размер, либо б) кодировать (единственный байт - все, что вам нужно) значение sizeof(size_t) для отправителя в качестве первого байта данных, и пусть получатель внесет необходимые изменения. Вариант а) может быть проще, но может вызвать проблемы (что, если вы выберете размер, слишком малый для учета устаревших компьютеров в вашей сети, и по мере их отказа у вас не хватит места для хранения данных?), Поэтому Я бы предпочел вариант б), так как он масштабируется с любой системой, на которой вы работаете (16-битная, 32-битная, 64-битная, возможно, даже в будущем 128-битная), но вам не понадобятся такие усилия .

</vomit> Я даю читателю разобраться во всем этом беспорядке, который я только что написал.

3 голосов
/ 04 ноября 2010

Возможно, вы могли бы закодировать длину строки, за которой следовал специальный символ перед каждой строкой?Таким образом, вам не нужно беспокоиться о том, что символы в следующих N символов.Хорошей идеей может быть также завершение каждой подстроки нулем.

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

РЕДАКТИРОВАТЬ: еще лучший подход - использовать первые 2-4 байта, как было предложено.Крисом в комментарии ниже вместо закодированной длины + спецсимвола.

2 голосов
/ 04 ноября 2010

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

Когда вы объединяете строки, первым шагом является поиск обоих символов в незашифрованных строках и замена их на экранированную версию:

str1 = "foo;bar;baz";
str2 = "foo/bar/baz";

становится

estr1 = "foo/;bar/;baz";
estr2 = "foo//bar//baz";

Затем они объединяются с разделителем:

res = "foo/;bar/;baz;foo//bar//baz";

Вот и все. Разделение выполняется путем поиска разделителя без ведущего управляющего символа, а затем замены экранированных символов в одиночных строках обратно на неэкранированную версию.

Это хороший выбор, если вы хотите работать со строками с функциями, которые ожидают одну строку с нулевым символом в конце, например, используя функции str или распечатайте их с помощью функций printf. Если вы можете гарантировать, что только ваши собственные функции будут работать с этими строками, то упомянутое разделение нулями \0 более эффективно, особенно если вам не нужно разбивать его, вы можете использовать указатель на полную строку используйте одну частичную строку из нее при использовании функций str или printf.

2 голосов
/ 04 ноября 2010

Если вы знаете, что ваши строки всегда будут действительным текстом UTF-8 (или ASCII), вы можете использовать байт, который не может отображаться в действительном UTF-8 (или ASCII), в качестве разделителя. В UTF-8 байты C0, C1, F5, F6, F7, F8, F9, FA, FB, FC, FD, FE и FF являются недействительными. В ASCII любой байт с установленным старшим битом недопустим.

2 голосов
/ 04 ноября 2010

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

const char* str_final = "foo; for|* 1.234+\"@!`\0bar; for|* 1.234+%\"@`\0";
                                     delimiter ^             delimiter ^ 

Рэймонд Чен дал хороший обзор строки с двойным нулем в конце в своем блоге. Он используется несколькими функциями в Windows API.1006 *

1 голос
/ 04 ноября 2010

2 идеи:

1) Используйте стандартный подход «escape», что-то похожее на определение литерала char * в C.

2) Используйте один символ '\0' в качестве разделителя, идва из них как маркер конца строки.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...