ใช้รายการเชื่อมโยงทั่วไปซึ่งมีเขตข้อมูลค่าเดียว

เมื่อเร็ว ๆ นี้เมื่อเขียนโปรแกรม 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 - ผนัง linked_list.c llist_test.c -o a.out

ดำเนินการ:

./a.ออก


คำถาม:

  • การหล่อมีความซับซ้อน มีแนวทางใดที่ทำให้ง่ายขึ้นหรือไม่?
  • ในวิธีทดสอบ 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

    ความแตกต่างคือการร่ายพอยน์เตอร์ เหตุใดจึงทำให้ผลลัพธ์แตกต่างออกไป


@อัปเดต - เกี่ยวกับคำถามที่ 2

ตามที่ @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 เพื่อให้มันใช้งานได้ ตอนนี้ขอคำแนะนำเกี่ยวกับการออกแบบและการใช้งาน และคำถามที่ 2 เป็นเพียงปัญหาการใช้พอยน์เตอร์พอยน์เตอร์ที่ฉันไม่สามารถหาได้ใน gdb   -  person user218867    schedule 23.01.2016
comment
@BLUEPIXY นั่นสำคัญ ฉันเกือบลืมไปแล้ว จะตรวจสอบตอนนี้   -  person user218867    schedule 23.01.2016
comment
ขออภัย เหตุผลที่ต้องมีการแคสต์จำนวนมากคือคุณใช้ที่อยู่ของออบเจ็กต์ (เช่น &something) เมื่อคุณใช้ที่อยู่ของวัตถุด้วยตัวดำเนินการ urnary & ผลลัพธ์ก็คือ ที่อยู่ ที่อยู่ไม่มีประเภท -- มันเป็นเพียงตำแหน่งหน่วยความจำ ดังนั้น เพื่อที่จะใช้ที่อยู่ของออบเจ็กต์อย่างเหมาะสม คุณต้อง ส่งที่อยู่ ของออบเจ็กต์ให้เป็น ประเภท ที่เหมาะสม   -  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 ดูเหมือนว่าจำเป็นต้องจัดสรรหน่วยความจำออกจากไซต์ของ struct เอง ซึ่งซับซ้อนในการบำรุงรักษา มิฉะนั้นจะได้รับความผิดส่วน - person user218867; 23.01.2016
comment
การบำรุงรักษาไม่ซับซ้อนไปกว่าโซลูชันก่อนหน้านี้ เมื่อลบโหนด เพียงลบค่า (ว่าง) อีกทางหนึ่ง แทนที่จะทำการ mallocing/คัดลอกค่า คุณสามารถบันทึกค่า (ตัวชี้) จากผู้ใช้ได้ ดังนั้นคุณไม่จำเป็นต้องจัดสรรหน่วยความจำให้ ขึ้นอยู่กับฟังก์ชันที่คุณต้องการนำเสนอให้กับผู้ใช้ของคุณ: เขาต้องการให้คุณเก็บสำเนาหรือเพียงแค่ตัวชี้? ในกรณีแรก การเปลี่ยนแปลงสำเนาของผู้ใช้จะไม่ปรากฏในสำเนาที่จัดเก็บไว้ในรายการ ในกรณีที่สองผู้ใช้ดำเนินการกับสำเนาเดียว - 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