Подтвердить что ты не робот

Практика обработки строк в C

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

С этим пониманием, каковы некоторые лучшие практики, gotchas, трюки или другие методы, которые могут помочь сделать строчную обработку проекта более надежной и безопасной?

4b9b3361

Ответ 1

Без дополнительной информации о том, что делает ваш код, я бы рекомендовал разработать все ваши интерфейсы следующим образом:

size_t foobar(char *dest, size_t buf_size, /* operands here */)

с семантикой типа snprintf:

  • dest указывает на буфер размером не менее buf_size.
  • Если buf_size равно нулю, нулевые/недопустимые указатели допустимы для dest, и ничего не будет записано.
  • Если buf_size отличное от нуля, dest всегда завершено нуль.
  • Каждая функция foobar возвращает длину полного не усеченного вывода; выход был усечен, если buf_size меньше или равно возвращаемому значению.

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

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

Ответ 2

Некоторые мысли от давнего встроенного разработчика, большинство из которых подробно описывают ваше требование для простоты и не являются C-специфическими:

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

  • Следуйте рекомендациям R., чтобы определить четкий интерфейс, который согласован для всех обработчиков строк. Строгий, малый, но подробный набор правил позволяет использовать сопоставление образцов в качестве инструмента для отладки: вы можете быть подозрительными к любому коду, который отличается от остальных.

  • Как отметил Барт ван Инген Шенау, отслеживайте длину буфера независимо от длины строки. Если вы всегда будете работать с текстом, безопасно использовать стандартный нулевой символ для указания конца строки, но вам нужно убедиться, что текст + null будет помещаться в буфер.

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

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

  • По возможности используйте безопасные встроенные функции (например, memmove() для Джонатана Леффлера), чтобы сделать тяжелый подъем. Но проверяйте их, чтобы убедиться, что они делают то, что, по вашему мнению, делают!

  • Проверяйте ошибки как можно скорее. Нераспределенные переполнения буфера могут привести к ошибкам "ricochet", которые, как известно, трудно найти.

  • Напишите тесты для каждой функции, чтобы убедиться, что она удовлетворяет его контракту. Обязательно закройте граничные случаи (выключите на 1, пустые/пустые строки, перекрытие источника/места и т.д.). И это может показаться очевидным, но убедитесь, что вы понимаете, как создавать и обнаруживать переполнение/переполнение буфера, а затем записывать тесты которые явно генерируют и проверяют эти проблемы. (Мои люди QA, вероятно, устали от моих инструкций, чтобы "не просто проверить, чтобы убедиться, что это работает, проверьте, чтобы он не сломался".)

Вот несколько методов, которые сработали для меня:

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

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

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

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

Большое спасибо за задание этого вопроса! Я хочу, чтобы разработчики подумали об этих проблемах, особенно перед тем, как начать кодирование. Удачи и наилучших пожеланий для надежного, успешного продукта!

Ответ 3

Посмотрите strlcpy и strlcat, см. original paper для деталей.

Ответ 4

Два цента:

  • Всегда используйте "n" версию строковых функций: strncpy, strncmp, (или wcsncpy, wcsncmp и т.д.).
  • Всегда выделяйте с помощью идиомы +1: например. char * str [MAX_STR_SIZE + 1], а затем передать MAX_STR_SIZE как размер для "n" версии строковых функций и завершить с помощью str [MAX_STR_SIZE] = '\ 0'; чтобы убедиться, что все строки правильно финализированы.

Последний шаг важен, так как "n" версия строковых функций не добавит "\ 0" после копирования, если достигнут максимальный размер.

Ответ 5

  • Работа с массивами в стеке когда это возможно, и правильно их инициализировать. Вам не нужно отслеживать распределения, размеры и инициализации.

    char myCopy[] = { "the interesting string" };
    
  • Для строк среднего размера C99 имеет VLA. Они немного менее полезны, поскольку вы не могут их инициализировать. Но у вас все еще есть первые два из вышеуказанных Преимущества.

    char myBuffer[n];
    myBuffer[0] = '\0';
    

Ответ 6

Некоторые важные ошибки:

  • В C нет никакой связи между длиной строки и размером буфера. Строка всегда запускает (и включает) первый '\0' -character. Вы отвечаете за программиста, чтобы убедиться, что этот символ можно найти в зарезервированном буфере для этой строки.
  • Всегда явно отслеживать размеры буфера. Компилятор отслеживает размеры массива, но эта информация будет потеряна для вас, прежде чем вы ее узнаете.

Ответ 7

Когда дело доходит до времени и пространства, не забудьте выбрать стандартную битку из здесь

Во время моих ранних проектов прошивки я использовал таблицы поиска, чтобы подсчитать бит, установленный в эффективности операции O (1).