Многофункциональная реализация связанного списка в чистом C - PullRequest
28 голосов
/ 10 апреля 2009

Это не совсем технический вопрос, так как я знаю, что C достаточно для того, чтобы делать то, что мне нужно (я имею в виду, не говоря о том, чтобы «позволить языку встать на вашем пути»), так что этот вопрос в основном вопрос «в каком направлении».

Ситуация такова: в настоящее время я прохожу курс продвинутых алгоритмов, и для того, чтобы «вырасти как программисты», мне нужно использовать чистый C для выполнения практических заданий (это работает хорошо: практически любая маленькая ошибка, которую вы make на самом деле заставляет вас полностью понять, что вы делаете, чтобы это исправить). В ходе реализации я, очевидно, столкнулся с проблемой реализации «базовых» структур данных с нуля: фактически не только связанных списков, но и стеков, деревьев и так далее.

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

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

  • Составление списка пустых указателей (вроде не элегантно; сложнее в отладке)
  • Создание только одного списка, но с union в качестве «типа элемента», содержащего все типы элементов, которые я буду использовать в программе (проще в отладке; тратится пространство, если элементы не имеют одинаковый размер)
  • Использование макроса препроцессора для регенерации кода для каждого типа в стиле SGLIB , «имитирующего» STL (творческое решение C ++); не тратит пространство; элементы имеют явный тип, которым они на самом деле являются когда они возвращаются; любое изменение в коде списка может быть очень существенным )
  • Ваша идея / решение

Чтобы прояснить вопрос: что из вышеперечисленного лучше?

PS: Поскольку я в основном в академическом контексте, меня также очень интересует мнение людей, работающих с чистым C в этой отрасли. Я понимаю, что большинство программистов на чистом C находятся в области встроенных устройств, где я не думаю, что такая проблема, с которой я сталкиваюсь, является распространенной. Однако, если кто-нибудь там знает, как это делается «в реальном мире», мне было бы очень интересно ваше мнение.

Ответы [ 9 ]

33 голосов
/ 10 апреля 2009

A void * представляет собой сложную задачу в связанном списке, так как вам приходится управлять его размещением отдельно для самого списка. Один из подходов, которые я использовал в прошлом, - это иметь структуру с переменным размером, такую ​​как:

typedef struct _tNode {
    struct _tNode *prev;
    struct _tNode *next;
    int payloadType;
    char payload[1];  // or use different type for alignment.
} tNode;

Теперь я понимаю, что не выглядит переменного размера, но давайте выделим структуру таким образом:

typedef struct {
    char Name[30];
    char Addr[50];
} tPerson;
tNode *node = malloc (sizeof (tNode) - 1 + sizeof (tPerson));

Теперь у вас есть узел, который для всех намерений и целей выглядит следующим образом:

typedef struct _tNode {
    struct _tNode *prev;
    struct _tNode *next;
    int payloadType;
    char Name[30];
    char Addr[50];
} tNode;

или в графическом виде (где [n] означает n байт):

+----------------+
|    prev[4]     |
+----------------+
|    next[4]     |
+----------------+
| payloadType[4] |                
+----------------+                +----------+
|   payload[1]   | <- overlap ->  | Name[30] |
+----------------+                +----------+
                                  | Addr[50] |
                                  +----------+

То есть, если вы знаете, как правильно обращаться с полезной нагрузкой. Это можно сделать следующим образом:

node->prev = NULL;
node->next = NULL;
node->payloadType = PLTYP_PERSON;
tPerson *person = &(node->payload); // cast for easy changes to payload.
strcpy (person->Name, "Bob Smith");
strcpy (person->Addr, "7 Station St");

Эта строка приведения просто преобразует адрес символа payload (в типе tNode) в адрес фактического tPerson типа полезной нагрузки.

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

union {
    int x;
    char y[100];
} u;

где 96 байтов тратятся впустую каждый раз, когда вы сохраняете целочисленный тип в списке (для 4-байтового целого).

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

#define PAYLOAD_UNKNOWN     0
#define PAYLOAD_MANAGER     1
#define PAYLOAD_EMPLOYEE    2
#define PAYLOAD_CONTRACTOR  3

или (возможно, лучше):

typedef enum {
    PAYLOAD_UNKNOWN,
    PAYLOAD_MANAGER,
    PAYLOAD_EMPLOYEE,
    PAYLOAD_CONTRACTOR
} tPayLoad;
8 голосов
/ 10 апреля 2009

Мои $ .002:

  • Составление списка пустых указателей (вроде нелегально; сложнее в отладке)

Это не такой уж плохой выбор, IMHO, если вы должны писать на языке C. Вы можете добавить методы API, чтобы приложение могло предоставлять метод print () для простоты отладки. Подобные методы могут вызываться, когда (например) элементы добавляются или удаляются из списка. (Для связанных списков это обычно не требуется, но для более сложных структур данных - например, хеш-таблиц) - иногда это может быть спасением.)

  • Создание только одного списка, но с объединением в качестве «типа элемента», содержащего все типы элементов, которые я буду использовать в программе (проще для отладки; тратится пространство, если элементы не одного размера)

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

  • Использование макроса препроцессора для регенерации кода для каждого типа в стиле SGLIB (sglib.sourceforge.net), «имитирующего» C ++ STL (креативное решение; не тратит пространство; элементы имеют явный тип, которым они на самом деле являются когда они возвращаются; любое изменение в коде списка может быть очень существенным)

Интригующая идея, но, поскольку я не знаю SGLIB, я не могу сказать намного больше.

  • Ваша идея / решение

Я бы выбрал первый выбор.

6 голосов
/ 07 июня 2009

Использование макроса препроцессора - лучший вариант. Связанный список с ядром Linux является отличной и эффективной реализацией циклически связанного списка в C. Очень переносим и прост в использовании. Здесь автономная версия заголовка list.h ядра Linux 2.6.29.

FreeBSD / OpenBSD sys / queue - еще один хороший вариант для общего списка макросов

6 голосов
/ 10 апреля 2009

Я делал это в прошлом, в нашем коде (который с тех пор был преобразован в C ++), и в то время выбирал подход void *. Я просто сделал это для гибкости - мы почти всегда сохраняли указатель в списке, а простота решения и удобство его использования перевешивали (для меня) недостатки у других подходов.

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

4 голосов
/ 10 апреля 2009

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

1 голос
/ 10 апреля 2009

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

Скорее, при программировании на языке x вы должны использовать идиомы языка x. Не пишите Java, когда вы используете Python. Не пишите C, когда вы используете схему. Не пишите C ++, когда вы используете C99.

Сам, я бы, вероятно, в конечном итоге использовал что-то вроде предложения Пакса, но на самом деле использовал бы объединение char [1] и void * и int, чтобы сделать общие случаи удобными (и перечислимый флаг типа)

(Я также, вероятно, в конечном итоге реализовал бы дерево Фибоначчи, просто потому что это звучит аккуратно, и вы можете реализовать Деревья RB столько раз, пока он не потерял свой вкус, даже если это лучше для общих случаев использоваться для.)

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

1 голос
/ 10 апреля 2009

Это хорошая проблема. Мне нравятся два решения:

  • Интерфейсы и реализации C Дейва Хансона использует список void * указателей, что мне достаточно.

  • Для моих студентов я написал awk-скрипт для генерации специфичных для типа функций списка . По сравнению с макросами препроцессора, это требует дополнительного этапа сборки, но работа системы гораздо более прозрачна для программистов без большого опыта. И это действительно помогает обосновать параметрический полиморфизм, который они увидят позже в своей учебной программе.

    Вот как выглядит один набор функций:

    int      lengthEL (Explist *l);
    Exp*     nthEL    (Explist *l, unsigned n);
    Explist *mkEL     (Exp *hd, Explist *tl);
    

    Скрипт awk - это ужас из 150 строк; он ищет код C для typedef s и генерирует набор функций списка для каждой из них. Это очень старый; Я мог бы, вероятно, сделать лучше сейчас: -)

Я бы не дал список союзов времени дня (или места на моем жестком диске). Это небезопасно и не расширяемо, поэтому вы можете просто использовать void * и покончить с этим.

0 голосов
/ 10 апреля 2009

Я бы, наверное, сам применил метод void *, но мне пришло в голову, что вы можете хранить свои данные в формате XML. Тогда в списке может быть только char * для данных (который вы будете анализировать по требованию для любых подэлементов, которые вам нужны) ....

0 голосов
/ 10 апреля 2009

Одним из улучшений по сравнению с созданием списка void * было бы создание списка структур, содержащих void * и некоторые метаданные о том, на что указывает void, включая его тип, размер и т. Д.

Другие идеи: встроить интерпретатор Perl или Lisp.

Или идите на полпути: свяжитесь с библиотекой Perl и составьте список SV-файлов Perl или чего-то еще.

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