Реализовать общий связанный список с одним полем значения.

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

Подход:

  • определить структуру с указателями, затем закончить полем значения типа char[], использовать его как общую структуру,
  • определить и внедрить метод в связанный список, используя общую структуру,
  • определить новый тип структуры, который имеет поле значения другого типа,
  • при вызове функций просто приведите их к общему типу,

Код: (черновая версия)

linked_list.h

#ifndef _LINKED_LIST
#define _LINKED_LIST

// common list for any type,
struct llist_item {
    struct llist_item *next;
    char value[1];
};

// int list
struct llist_int {
    struct llist_int *next;
    int value;
};

/**
 * append item to end of list,
 * 
 * @param headp
 *  pointer to head pointer,
 * @param valuep
 *  pointer set value of deleted item into,
 * @param value_size
 *  size of value,
 * @param struct_size
 *  size of actual struct,
 * 
 * @return
 *  pointer to head,
 */
extern struct llist_item *llist_append(struct llist_item **headp, void *valuep, ssize_t value_size, ssize_t struct_size);

/**
 * delete head,
 * 
 * @param headp
 *  pointer to head pointer,
 * @param valuep
 *  pointer set value of deleted item into,
 * 
 * @return
 *  pointer to new head,
 */
extern struct llist_item *llist_del_head(struct llist_item **headp, char *valuep);

#endif

linked_list.c

// linked_list utility
#include <stdio.h>
#include <string.h>
#include <errno.h>

#include <stdlib.h>

#include "linked_list.h"

/*
   printf("error while linked_list: %s\n", strerror(errno));
   printf("linked_list succeed\n");
 */

struct llist_item *llist_append(struct llist_item **headp, void *valuep, ssize_t value_size, ssize_t struct_size) {
    struct llist_item *head = *headp;

    // create new item
    struct llist_item *new_item = (struct llist_item*) malloc(struct_size);
    new_item->next = NULL;
    memcpy(&(new_item->value), valuep, value_size);

    // append new item
    if(head == NULL) { // empty queue,
        head = new_item;
        *headp = head;
    } else {
        // find last item
        struct llist_item *tail = head;
        while(tail->next != NULL) {
            tail = tail->next;
        }   

        tail->next = new_item;
    }

    return head;
}

struct llist_item *llist_del_head(struct llist_item **headp, char *valuep) {
    struct llist_item *head = *headp;

    if(head == NULL) {
        return NULL;
    } else {
        memcpy(valuep, &(head->value), sizeof(*valuep));
        *headp = head->next;
        free(head);
        return *headp;
    }
}

llist_test.c

// linked_list test
#include <stdio.h>
#include <string.h>
#include <errno.h>

#include <stdlib.h>

#include "linked_list.h"

int linked_list_test() {
    struct llist_int *int_list = NULL; // it's important to initialize this pointer as NULL explicitly,
    int i;

    for(i=1; i<=5; i++) {
        llist_append((struct llist_item **) &int_list, (void *) &i, sizeof(int), sizeof(struct llist_int));
    }

    struct llist_int *int_item;
    int value;
    if(int_list != NULL) {
        do {
            (struct llist_int *)llist_del_head((struct llist_item **) &int_list, (char *) &value);
            printf("%d\n", value);
        } while (int_list!= NULL);
    }

    return 0;
}

int main(int argc, char * argv[]) {
    return linked_list_test();
}

Скомпилировать и запустить

список кодов:

  • linked_list.h, заголовок,
  • linked_list.c, реализация,
  • llist_test.c, выполните тест,

компилировать — для тестирования:

gcc -Wall linked_list.c llist_test.c -o a.out

выполнить:

./a.out


Вопросы:

  • Кастинг сложный, есть ли способ упростить его?
  • В методе тестирования linked_list_test():

    если изменить:

        do {
            int_item = (struct llist_int *)llist_del_head((struct llist_item **) &int_list, (char *) &value);
            printf("%d\n", value);
        } while (int_item != NULL);
    

    to

        do {
            (struct llist_int *)llist_del_head((struct llist_item **) &int_list, (char *) &value);
            printf("%d\n", value);
        } while (int_list!= NULL);
    

    Затем результат используется вместо вывода:

    1 2 3 4 5

    он выводит:

    32513 32514 32515 32516 32517

    Разница заключается в приведении указателя, почему это делает результат другим?


@Update — О втором вопросе

Как описано в комментарии @BLUEPIXY, действительно sizeof(*valuep) вызвало проблему, теперь я изменил llist_del_head() и явно указал размер в списке параметров, и проблема исправлена.

Теперь функция выглядит так:

extern struct llist_item *llist_del_head(struct llist_item **headp, char *valuep, ssize_t value_size);

person user218867    schedule 23.01.2016    source источник
comment
Пробовали ли вы выполнять код построчно в отладчике?   -  person Some programmer dude    schedule 23.01.2016
comment
примечание sizeof(*valuep) это 1   -  person BLUEPIXY    schedule 23.01.2016
comment
@JoachimPileborg Теперь код хорош, до этого я отлаживал его через GDB, чтобы заставить его работать. Теперь хочу предложить некоторые предложения по дизайну и реализации, а второй вопрос - это просто проблема приведения указателя, которую я не могу понять в gdb.   -  person user218867    schedule 23.01.2016
comment
@BLUEPIXY Это важно, я почти забыл, сейчас проверю.   -  person user218867    schedule 23.01.2016
comment
Eirc, причина, по которой требуются многие приведения, заключается в том, что вы используете адреса объектов (например, &something). Когда вы берете адрес объекта с помощью урнарного оператора &, результатом будет просто адрес. адрес не имеет типа — это просто ячейка памяти. Следовательно, чтобы правильно использовать адрес объекта, вы должны привести адрес объекта к соответствующему типу.   -  person David C. Rankin    schedule 23.01.2016
comment
@BLUEPIXY эй, с вашим советом, действительно, это sizeof(*valuep) причина проблемы, теперь я изменил llist_del_head(), чтобы явно указать размер в списке параметров, и проблема исправлена, спасибо.   -  person user218867    schedule 23.01.2016
comment
@David Когда оператор адреса (&) применяется к любому допустимому объекту, результатом всегда является типизированное значение. Если применяется к int, тип будет int *. Если применить к char [5], результатом будет char (*)[5]. Каждое выражение в C имеет тип, будь то указатель или что-то другое.   -  person Tom Karzes    schedule 23.01.2016
comment
Том, я согласен с этим, мысль, которую я пытался подчеркнуть, возможно, неискусно, заключалась в том, что после того, как вы берете адрес, результатом является голый адрес, который вы должны преобразовать, чтобы правильно использовать (если не присваивать/передавать как тот же тип). Да, компилятор выдаст предупреждение/ошибку типа, если вы попытаетесь выполнить назначение без надлежащего приведения к объекту другого типа.   -  person David C. Rankin    schedule 23.01.2016
comment
@David Однако такой актерский состав должен быть редкостью. Обычно, если вы берете адрес чего-то и передаете его функции или назначаете его указателю, типы совпадают, и приведение не требуется (в этом, в конце концов, и заключается вся суть строгой типизации). Единственные случаи, когда приведение требуется, - это необычная ситуация, когда вам нужно выполнить арифметику с голыми адресами (в обход системы типов), или если вы хотите принудительно использовать что-то вроде квалификатора const в ситуации, которая не нравится компилятору.   -  person Tom Karzes    schedule 23.01.2016


Ответы (2)


Этот подход имеет несколько проблем:

  • Пространство, которое вы отводите в структуре, составляет всего 1 байт, вы не можете хранить в нем типы больше 1 байта.
  • Если бы вы освободили место для больших типов, у вас все равно возникла бы проблема с выравниванием: для больших типов может потребоваться другое выравнивание, чем предполагается компилятором для массива char после struct llist_item *. Таким образом, даже использование конечного массива переменного размера в структуре не дает жизнеспособного решения.

Вы можете использовать void * и выделять значения отдельно, но это расточительно, или использовать макросы для реализации шаблонов исходного кода для разных типов, но это громоздко и чревато ошибками.

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

person chqrlie    schedule 23.01.2016
comment
Хороший совет для части выравнивания. - person user218867; 23.01.2016

Я не уверен, что вы хотите сделать, но я предлагаю:

  1. Не экспортируйте определение списка в файл .h. Пользователю не нужно знать реализацию.

  2. Обратите внимание, что ваш struct llist_item может содержать только один символ. Измените его на void *, чтобы он мог содержать указатель на любой тип значения.

  3. необходимость передавать размер структуры в llist_append кажется очень странной, поскольку это размер структуры списка плюс размер сохраняемого значения. Но это связано с тем, как вы хотите хранить «любое значение»

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

/* append pointer valuep to the list */
void *llist_append(void **list, void *valuep, ssize_t value_size);

/* delete the list element that contains pointer valuep */
void *llist_del_head(void **list, void *valuep);

и ваша реализация:

// common list for any type,
struct llist_item {
    struct llist_item *next;
    void *value;
};

и добавить:

// create new item
struct llist_item *new_item = malloc(sizeof(struct llist_item));
new_item->next = NULL;
new_item->value= valuep;
person Paul Ogilvie    schedule 23.01.2016
comment
0) Причина написания этого кода заключается в повторном использовании кода для разных типов значений. 1) Хороший совет, могу удалить их, если отдам другим. 2) Я рассмотрел тип void * для поля value, но базовый тип никогда не будет фактически использоваться, когда нужен новый тип, затем определите новую структуру, я использовал char[1] в основном потому, что видел поле unsigned char __pad[X]; из struct sockaddr_in при написании программ сокетов. 3) Это единственное известное мне решение, чтобы убедиться, что размер правильный, я видел подобный подход в inet_ntop(). - person user218867; 23.01.2016
comment
Я только что попробовал void * value, кажется, ему нужно выделить память на месте самой структуры, которую сложно поддерживать. В противном случае будет получена ошибка сегмента. - person user218867; 23.01.2016
comment
Его не сложнее поддерживать, чем ваше предыдущее решение. При удалении узла просто удалите (бесплатно) значение. В качестве альтернативы, вместо переноса/копирования значения вы можете сохранить значение (указатель) от пользователя, чтобы вам не нужно было выделять для него память. Это зависит от функциональности, которую вы хотите предложить своему пользователю: он хочет, чтобы вы сохранили копию или просто указатель? В первом случае изменения в пользовательской копии не будут отражаться в копии, хранящейся в списке, во втором случае пользователь работает с одной копией. - person Paul Ogilvie; 23.01.2016
comment
Хорошо, кажется, что в c действительно сложно написать общее решение для этого, различное использование предпочитает другое решение и структуру. - person user218867; 23.01.2016
comment
Эрик, в C это несложно. В общем, пользователь захочет, чтобы вы поддерживали указатель, а не копию. Пользователь передает указатель на что-то, что ваш модуль списка обрабатывает как void *. Что это такое, зависит от пользователя, и вашему модулю списка не нужно ничего знать о том, что он хранит. Он просто поддерживает список. Я обновлю решение для этого. - person Paul Ogilvie; 23.01.2016
comment
Если это просто указатель, это легко. Но в некоторых случаях это проще для пользователя, если ему не нужно выделять дополнительную память пользователю, например, пользователь хочет поддерживать список сокетов fd от клиента, fd имеет тип int, пользователь, вероятно, не хочет выделять место для int и освобождать его позже вручную каждый раз, когда я думаю. - person user218867; 23.01.2016
comment
Затем пользователь передает int, и список сохраняет int в пространстве для void *, потому что в целом sizeof(int)==sizeof(void *) (примечание: стандарт не требует, чтобы они были одинакового размера, но почти на всех платформах они есть. Так что формально вы должны проверить, что они имеют одинаковый размер на вашей платформе). - person Paul Ogilvie; 24.01.2016