Кольцевые двусвязные списки на С – проще не бывает
Возвращаясь к теме хранения данных, затронутой ранее, хотелось бы поделиться ещё одной полезностью, которую я часто использую в своих программах, написанных на С.
Сразу оговорюсь, это не моё изобретение, мне подсказал идею мой друг, а он, в свою очередь, добыл эти знания где-то в недрах сетевого ядра FreeBSD.
Итак, имеем проект, написанный на С. Задача – хранить некоторое количество структур данных таким образом, чтобы имелась возможность вставки и удаления произвольной структуры при сохранении адресов всех прочих.
Одним их подходящих решений для данной задачи является кольцевой двусвязный список (я надеюсь, мне не нужно объяснять вам, что это такое). Но в С нет такого встроенного типа, нельзя решить задачу и через шаблоны, как это сделано, например, в STL. На помощь нам приходит близость языка к «железу» и такое мощное (в умелых руках) средство, как приведение типов.
Почему я упомянул здесь про умелые руки? Бездумное приведение типов есть отличный источник трудноуловимых ошибок. Хрестоматийным случаем является приведение беззнакового целого к знаковому и наоборот. Не зря в С++ это действие сопровождается ритуальными плясками с static_cast и прочими cast-ами. Поэтому пользоваться им необходимо осмотрительно, точно зная, что делаешь.
Сначала нам необходимо провести подготовительные работы. Знакомьтесь - базовая структура для кольцевого двусвязного списка:
struct dl_node {
struct dl_node *next; // Указатель на следующий
// элемент
struct dl_node *prev; // Указатель на предыдущий
// элемент
};
У пустого списка, представленного своим головным элементом, оба указателя содержат адрес самой структуры «головы».

При добавлении элемента его элементы next и prev будут указывать на «голову» списка, а те же элементы «головы» - на добавленный элемент.

Далее можно добавлять элементы в конец и начало списка, то есть до и после головного элемента. Получаем такую структуру:

Для того, чтобы удалить элемент из списка, достаточно правильно перелинковать его предыдущего и последующего соседей.
Все эти операции осуществляют функции, находящиеся в файлах list.h и list.c. Архив с ними лежит здесь.
Теперь пришла ваша очередь задать вопрос: «Конечно, всё вышеизложенное весьма интересно, но где же сами данные, которые мы будем хранить при помощи двунаправленного списка?».
Замечание справедливо, поэтому переходим к практическому применению полученного кода.
Пример: необходим список целых знаковых значений.
Для реализации сначала опишем следующую структуру:
struct int_list {
struct dl_node node; // Служебная структура для
// реализации кольцевого
// двусвязного списка
int value; // Собственно, данные
};
Далее определяем переменные.
struct dl_node our_list; // «голова» списка. Её тип
// одинаков для любых типов
// хранимых значений - это
// очень удобно
struct int_list *element; // Это указатель на текущий
// элемент списка
Инициализируем список
list_init (&our_list);
Аллокируем и добавляем первый элемент
element = (struct int_list *)malloc (sizeof (int_list));
element->value = 45;
list_append (&our_list, (struct dl_node*)element);
Здесь как раз используется приведение типов. Почему такое возможно? Потому, что структура dl_node есть первый элемент структуры int_list, а значит, их адреса совпадают. Поэтому мы можем свободно приводить их адреса друг к другу, не опасаясь за последствия.
Мы можем добавлять сколько угодно членов любых типов в структуру int_list, и она будет оставаться двусвязным списком, при условии первенства элемента node.
А теперь пробежимся по списку и распечатаем его значения. Сделать это можно многими способами. Мне больше всего нравится такой:
for (element = (struct int_list *)our_list.node.next;
element != (struct int_list *)&dev->protocols;
element = (struct int_list *) element ->node.next) {
printf ("Element is %d\n", element->value);
}
Пришло время собирать камни. Функции list_remove, служащей для удаления элемента из списка, требуется только один параметр – адрес этого элемента. А бремя освобождения памяти, занятой элементами списка ложится целиком на ваши плечи.
Но вы ведь не какие-нибудь там неженки, ожидающие, что весь мусор за вами соберет система? Eсли вы дочитали до этого места, вы – программисты от сохи, привыкшие всё делать сами, обладающие навыками кодинга без страховки и набором собственных приемов написания правильного кода.
Надеюсь, этот набор сегодня пополнился.
P.S. Спасибо Ивану Сагалаеву за его highlight.js
Рекомендуйте на news2.ru
Занесите в del.icio.us
Читайте также:
No related posts
Март 7th, 2007 at 20:24
Ох уж эти высокоуровневые программисты, страдающие от отсутствия двусвязанного списка в STL
А если серьезно, то C хорош как раз тем, что там можно сделать все что угодно и как угодно, причем алгоритм работы будет весьма прозрачен, в отличии хотя бы от С++. Возможно поэтому он и является самым распространенным языком для микроконтроллеров.
Март 8th, 2007 at 8:19
2 Sergey: Соглашаюсь с Вами. Хотя, как видно, посетители Берлоги больше склоняются всё же к C++:
http://beta.delta-z.com/cgi-bin/yabb2/YaBB.pl?num=1159537441
Кстати, там в комментариях я помещал ссылки на другие рейтинги (и коммерческие и статистические).
Позволю отвлечься от темы, да простит меня автор статьи.
Да, статья подтверждает красоту, простоту и изящество применения C. Реализация приведённой в статье конструкции на каком-либо другом языке повлечёт за собой увеличение либо объёмов исходных текстов, либо кода (если этот язык не имеет уже встроенного аппарата для работы со списками.
).
Сделаю одно замечание. C конечно же хорош, но в среде контроллеров и DSP к нему относятся неоднозначно. Многое зависит от компилятора. Часто ассемблерные команды контроллера или DSP настолько ёмки, что трудно найти в C эквивалент (а с упаковкой команд - вообще проблема - только ASM).
Да, наверное большая часть текстов для MCU&DSP пишется на C. Но этот инструмент обеспечивает лишь простоту и функциональность. Настоящую же скорость и мощь даст лишь специализированный инструмент (ASM или, опять же, библиотека, написанная на том же ASM’е).
Март 8th, 2007 at 21:17
Я конечно дико извеняюсь, но о связном списке знает любой хоть как то грамотный программист и то что его открыл некий друг в недрах ФриБСд … хм… ну не буду конечно критикавать, но всё же забавно +) А так статья хорошая.
Присоединясь к оффтопу:
Си хорошь и я пишу например исключительно на нём ибо пока ещё не было задачи в которой надо было использывать что то другое ( я имею ввиду более высокоуровневое ) С++ тоже хорошо даже отлично очень хорошая структура программы получаеться легко читаемая если невдаваться в реализацию классов…. Но многие почему то начинают изучать язык с Си++ только потому что это “типа крута” причем в 90% случаев используют 2-3% языка =)
Март 13th, 2007 at 18:15
2Vladimir:
> посетители Берлоги больше склоняются всё же к C++:
Склоняться-то они склоняются, но в реальной жизни слишком много т.н. программистов на С++, которые используют его лишь как некое расширение С, позволяющее некоторые вольности в написании кода, и при этом практически не применяют объектно-ориентированный подход в проектировании.
2Роман:
Не могу не согласиться с Вашим комментарием, но хотелось бы заметить, что данный ресурс читают не только опытные программисты, да и не все, кто занимается программированием, получили хорошее образование по этому профилю - жизнь заставляет
В статье речь шла, если Вы заметили, не о двусвязных списках вообще, а об одной из их конкретных реализаций на языке С, о которой кто-то мог и не знать.
Если есть желание написать статью о чем-то более “продвинутом” - милости просим в авторы.
Март 14th, 2007 at 3:01
Я конечно не супер программист и в программирование не так уж и давно. Но мне кажется что это не просто Двунаправленный спиок, а кольцевой список. Так как последний элемент указывает на следующий как на головной ( последний на первый). Тут конечно половина было не понятно потому как написано на чистом Си. А разве нельзя на С++ сделать примерно тоже самое через new(). Не пользуясь тем же STL.
Или я что то не правильно делаю? Если разъясните большое спасибо, а то я только учусь :-))
Март 14th, 2007 at 14:52
2Maverick:
Насчёт того, что список кольцевой, абсолютно согласен. Сейчас поправлю.
Теперь о С и С++. Суть данного решения на С состоит в том, чтобы всего лишь добавив в начало любой структуры данных элемент типа struct dl_node получить возможность организовывать эти структуры в кольцевой двусвязный список. Это достигается при помощи приведения типов.
Тот же самый результат можно получить в С++ при помощи шаблонов или наследования.
Результат получаем один и тот же - при помощи один раз написанного кода осуществлять манипуляцию с различными двусвязными списками.
Март 24th, 2007 at 12:15
Кто сказал, что stl-ный list однонаправленный? Я хочу плюнуть ему в лицо
Май 28th, 2007 at 8:57
Не являюсь профессиональным программистом. Статья симпатичная. Но думаю, что о списках лучше не подсматривать во FreeBSD, а почитать старую добрую книгу Никлауса Вирта “Алгориты и структуры данных”, а уж потом смотреть перевод на С:)