главная->Статьи->Списки в С |
||||||||||||||||
Работа со списками в ССравнение с С++C++ программисты очень гордятся, по-видимому, тем, что у них есть такой инструмент, как STL. Они могут в пять секунд построить и отсортировать вам любой список, создать хэш-таблицу, и т.д. Но есть одно "НО". Лично я глубоко убеждён, что всё, что позволяет сделать С++ можно сделать и на С, ровно таким счетом, как всё, что позволяет сделать С можно сделать на ассемблере. Разница только в том, что делать что-то на ассемблере может оказаться достаточно затруднительным, а на С - уже можно. Для затравки скажу, что в С есть практически всё, что есть в С++. Но на эту тему (как в С реализовать обработку исключений, как разрабатывать приложения в объектно-ориентированном стиле, где в С аналог шаблонов) будет отдельная статья. Сейчас речь пойдет о работе со списками. Я, конечно, не разделяю полностью позицию Линуса Торвальдса об ущербности С++, но просто чистый С мне очень симпатичен, поэтому буду его защищать всеми силами! Стандартный подходСтандартный подход заключается в создании некоей структуры такого типа:
При этом в памяти формируется что-то типа:
Его [этого стандартного подхода] недостаток очевиден: список требует отдельного управления памятью. Т.е. если обычно нам может быть достаточно не сложно работать с динамической памятью (когда у всех объектов мы стараемся четко выделить время жизни, обеспечить упорядоченность выделения памяти под них и их удаление), то в случае со списком не всё так просто: Для каждого объекта у нас должен быть list_entry, этот list_entry надо вовремя создать и вовремя уничтожить. И не дай бог, обратиться к нему после уничтожения - можно получить плавающую ошибку, которую исправить будет очень сложно. Именно из-за этого Java-программисты считают себя существами высшего сорта - у них-то таких проблем почти никогда не бывает - у них же нет указателей! Подход, основанный на использовании макроса offsetofИтак, нам нужны списки. Более того, нам желательно, чтоб одни и те же объекты могли быть в нескольких списках (например: алфавитный список, список по возрастанию рейтинга, список по возрастанию номеров телефонов, и т.д.) В Linux kernel для этого есть замечательный файл: list.h. Основная идея в том, что наш list_entry_t не содержит указателя на объект, а сам содержится в качестве одного из полей объекта. При этом достигается отсутствие необходимости выделять память под элемент списка - память выделяется автоматически вместе с объектом. Отрицательным моментом этого является то, что при уничтожении объекта до удаления его из списка - рушится и весь список тоже, но это не страшно, т.к. потерявши голову по волосам не плачут! Чем быстрее такая ошибка обнаружится - тем лучше.
Итак, видим четыре связанных между собой выражения. Очевидно что, первое из них определяет как раз структуру для поддержания двухсвязного списка второе служит для примерно такой вот инициализации этой структуры:
третье служит для объявления вместе с инициализацией:
ну и четвертое служит для инициализации имеющейся структуры через вызов функции:
Любой из способов инициализации, указанных на последних трёх листингах, подходит. Главное, что у вновь созданной структуры и next и prev должны указыать на неё саму. При работе, происходит следующее: допустим, у нас есть такой объект, к которому мы хотим иметь доступ через список:
таким образом, после такой инициализации поля alpha_list->next и alpha_list->prev указывают на alpha_list, т.е. считаем, что линки next и prev указывают на сам объект, в котором они есть. также нам нужно завести отдельную (вне объекта) структуру типа list_head, которая у нас собственно и будет отождествляться со списком:
Эта структура при такой инициализации фактически также указывает сама на себя, показывая таким образом, что список пуст. дальше мы будем добавлять объекты к alpha_list следующим образом: в list.h есть такой код:
Используя из него list_add либо list_add_tail добавляем наш объект в начало либо в конец списка:
После добавления нескольких объектов получается такая картина:
Вот как ссылаются друг на друга ответственные за создание списка поля: Это очень хорошо, но как получить доступ к самим объектам? Элементы списка ведь ссылаются только на себя, но не на объект. Вот тут и начинают работать некоторые Си-шные трюки. В list.h определён такой макрос:
container_of определён в kernel.h вот так вот:
а offsetof определён в stddef.h вот так вот:
Таким образом, в нашем случае
возвращает указатель на object3. Недостаток только в том, что IDE-шки этого могут не понимать, и, соответственно, не будут помогать вам, высвечивая автоматически список полей my_object_t, после того как вы поставите "->" за закрывающейся скобкой list_entry. Собственно, соль вся не в том, как это устроено, а в том, что работать с такими списками очень удобно. Например, есть в файле list.h специальные макросы, которые обеспечивают перебор всех объектов списка, имея только указатель на list_head, например:
Есть там аналогичные макросы и для перебора в обратную сторону, и вообще много всего, чтобы не загромождать - не буду здесь указывать. Приведу только пример программы, которая используя эту технологию парсит текст, считает количество вхождений каждого слова, выводит таблицу либо по частоте вхождений, либо в алфавитном порядке. Вот ссылка на архив с программой. Для справки надо запустить ./concordance --help Вот ссылка на соответствующий man. Для тех, кто под Linux, можно просто набрать man queue или man 3 queue. Про man queue мне сказал LJ-user afonia. |