การวางแนววัตถุใน C


157

อะไรจะเป็นชุดของแฮ็ก preprocessor ดี (เข้ากันได้กับ ANSI C89 / ISO C90) ซึ่งเปิดใช้การวางแนววัตถุที่น่าเกลียด (แต่ใช้งานได้) ใน C?

ฉันคุ้นเคยกับภาษาเชิงวัตถุต่าง ๆ ดังนั้นโปรดอย่าตอบด้วยคำตอบเช่น "เรียนรู้ C ++!" ฉันได้อ่าน " การเขียนโปรแกรมเชิงวัตถุด้วย ANSI C " (ระวัง: รูปแบบ PDF ) และวิธีแก้ปัญหาที่น่าสนใจอื่น ๆ แต่ฉันสนใจ :-)!


ดูเพิ่มเติมคุณสามารถเขียนโค้ดเชิงวัตถุใน C ได้หรือไม่


1
ฉันสามารถตอบสนองต่อการเรียนรู้ D และการใช้ค ABI เข้ากันได้กับที่คุณต้องการจริงๆซีdigitalmars.com/d
ทิมแมตทิวส์

2
@Dinah: ขอบคุณสำหรับ "ดูด้วย" โพสต์นั้นน่าสนใจ

1
คำถามที่น่าสนใจน่าจะเป็นเหตุผลที่คุณจะต้องการสับก่อนประมวลผลของ OOP ในซี
Calyth

3
@Calyth: ฉันพบว่า OOP มีประโยชน์และ "ฉันทำงานกับระบบฝังตัวบางตัวที่มีคอมไพเลอร์ C เท่านั้น" (จากด้านบน) ยิ่งกว่านั้นคุณไม่พบแฮ็ก preprocessor ที่น่าสนใจที่จะดู?

คำตอบ:


31

C Object System (COS) ให้เสียงที่มีแนวโน้ม (ยังอยู่ในรุ่นอัลฟ่า) มันพยายามที่จะรักษาแนวคิดที่มีอยู่ให้น้อยที่สุดเพื่อความเรียบง่ายและความยืดหยุ่น: การเขียนโปรแกรมเชิงวัตถุแบบชุดรวมถึงคลาสที่เปิด, metaclasses, metaclasses คุณสมบัติ, generics, วิธีการ, การมอบหมาย, การเป็นเจ้าของ, ข้อยกเว้น, สัญญาและการปิด มีกระดาษร่าง (PDF) ที่อธิบาย

ข้อยกเว้นใน Cคือการนำ C89 ไปใช้กับ TRY-CATCH-FINALLY ที่พบในภาษา OO อื่น ๆ มันมาพร้อมกับ Testuite และตัวอย่างบางส่วน

ทั้งสององค์ Deniau ซึ่งเป็นคนที่ทำงานมากในOOP ใน C


@vonbrand COS ย้ายไปยัง GitHub ที่การคอมมิทล่าสุดคือฤดูร้อนที่แล้ว วุฒิภาวะสามารถอธิบายการขาดความมุ่งมั่น
philant

185

ฉันอยากจะแนะนำให้ใช้ preprocessor (ab) เพื่อลองและทำให้ไวยากรณ์ C เหมือนกับภาษาเชิงวัตถุอีกมากขึ้น ในระดับพื้นฐานที่สุดคุณเพียงแค่ใช้โครงสร้างแบบธรรมดาเป็นวัตถุและผ่านพวกมันไปด้วยพอยน์เตอร์:

struct monkey
{
    float age;
    bool is_male;
    int happiness;
};

void monkey_dance(struct monkey *monkey)
{
    /* do a little dance */
}

เพื่อให้ได้สิ่งต่าง ๆ เช่นการสืบทอดและพหุนิยมคุณต้องทำงานหนักขึ้นเล็กน้อย คุณสามารถทำการสืบทอดแบบแมนนวลได้โดยให้สมาชิกรายแรกของโครงสร้างเป็นตัวอย่างของซูเปอร์คลาสจากนั้นคุณสามารถส่งพอยน์เตอร์ไปยังฐานและคลาสที่ได้รับอย่างอิสระ:

struct base
{
    /* base class members */
};

struct derived
{
    struct base super;
    /* derived class members */
};

struct derived d;
struct base *base_ptr = (struct base *)&d;  // upcast
struct derived *derived_ptr = (struct derived *)base_ptr;  // downcast

ในการรับ polymorphism (เช่นฟังก์ชั่นเสมือนจริง) คุณต้องใช้พอยน์เตอร์ของฟังก์ชั่นและฟังก์ชั่นตารางตัวชี้หรือที่เรียกว่าตารางเสมือนหรือ vtables:

struct base;
struct base_vtable
{
    void (*dance)(struct base *);
    void (*jump)(struct base *, int how_high);
};

struct base
{
    struct base_vtable *vtable;
    /* base members */
};

void base_dance(struct base *b)
{
    b->vtable->dance(b);
}

void base_jump(struct base *b, int how_high)
{
    b->vtable->jump(b, how_high);
}

struct derived1
{
    struct base super;
    /* derived1 members */
};

void derived1_dance(struct derived1 *d)
{
    /* implementation of derived1's dance function */
}

void derived1_jump(struct derived1 *d, int how_high)
{
    /* implementation of derived 1's jump function */
}

/* global vtable for derived1 */
struct base_vtable derived1_vtable =
{
    &derived1_dance, /* you might get a warning here about incompatible pointer types */
    &derived1_jump   /* you can ignore it, or perform a cast to get rid of it */
};

void derived1_init(struct derived1 *d)
{
    d->super.vtable = &derived1_vtable;
    /* init base members d->super.foo */
    /* init derived1 members d->foo */
}

struct derived2
{
    struct base super;
    /* derived2 members */
};

void derived2_dance(struct derived2 *d)
{
    /* implementation of derived2's dance function */
}

void derived2_jump(struct derived2 *d, int how_high)
{
    /* implementation of derived2's jump function */
}

struct base_vtable derived2_vtable =
{
   &derived2_dance,
   &derived2_jump
};

void derived2_init(struct derived2 *d)
{
    d->super.vtable = &derived2_vtable;
    /* init base members d->super.foo */
    /* init derived1 members d->foo */
}

int main(void)
{
    /* OK!  We're done with our declarations, now we can finally do some
       polymorphism in C */
    struct derived1 d1;
    derived1_init(&d1);

    struct derived2 d2;
    derived2_init(&d2);

    struct base *b1_ptr = (struct base *)&d1;
    struct base *b2_ptr = (struct base *)&d2;

    base_dance(b1_ptr);  /* calls derived1_dance */
    base_dance(b2_ptr);  /* calls derived2_dance */

    base_jump(b1_ptr, 42);  /* calls derived1_jump */
    base_jump(b2_ptr, 42);  /* calls derived2_jump */

    return 0;
}

และนั่นคือวิธีที่คุณทำ polymorphism ใน C. มันไม่สวย แต่ทำงานได้ดี มีปัญหาบางอย่างที่เกี่ยวข้องกับตัวชี้ casts ระหว่างคลาสพื้นฐานและคลาสที่ได้รับซึ่งมีความปลอดภัยตราบใดที่คลาสพื้นฐานเป็นสมาชิกแรกของคลาสที่ได้รับ การรับมรดกหลายรายการนั้นยากกว่ามากในกรณีนี้ในกรณีระหว่างคลาสฐานอื่นที่ไม่ใช่คลาสแรกคุณจำเป็นต้องปรับพอยน์เตอร์ด้วยตนเองตามออฟเซ็ตที่เหมาะสมซึ่งมีความยุ่งยากและผิดพลาดได้ง่าย

อีกสิ่งหนึ่ง (หากิน) ที่คุณสามารถทำได้คือการเปลี่ยนประเภทของวัตถุแบบไดนามิกที่รันไทม์! คุณเพิ่งกำหนดใหม่ตัวชี้ vtable คุณสามารถเลือกเปลี่ยนฟังก์ชั่นเสมือนบางส่วนได้ในขณะที่ยังคงรักษาคุณสมบัติอื่น ๆ อยู่โดยสร้างประเภทไฮบริดใหม่ เพียงแค่ระมัดระวังในการสร้าง vtable ใหม่แทนการปรับเปลี่ยน vtable ทั่วโลกมิฉะนั้นคุณจะส่งผลกระทบต่อวัตถุทั้งหมดของประเภทที่กำหนด


6
อดัมความสนุกสนานของการเปลี่ยนแปลง vtable โลกประเภทคือการจำลองเป็ดพิมพ์ใน C. :)
jmucchiello

ตอนนี้ฉันเสียดาย C ++ ... แน่นอนว่าไวยากรณ์ของ C ++ นั้นชัดเจนกว่า แต่เนื่องจากไม่ใช่ไวยากรณ์ที่ไม่สำคัญฉันจึงลดน้อยลง ฉันสงสัยว่าบางสิ่งบางอย่างผสมผสานระหว่าง C ++ และ C สามารถทำได้ดังนั้น void * จะยังคงเป็นประเภทที่สามารถใช้งานได้ ส่วนที่struct derived {struct base super;};เห็นได้ชัดคือการเดาว่ามันทำงานได้อย่างไรเนื่องจากตามลำดับไบต์จะถูกต้อง
jokoon

2
+1 สำหรับโค้ดที่หรูหราเขียนได้ดี นี่คือสิ่งที่ฉันกำลังมองหา!
Homunculus Reticulli

3
ทำได้ดี. นี่คือสิ่งที่ฉันทำและเป็นวิธีที่ถูกต้องเช่นกัน แทนที่จะต้องการตัวชี้ไปที่ struct / object คุณควรส่งตัวชี้ไปยังเลขจำนวนเต็ม (ที่อยู่) สิ่งนี้จะช่วยให้คุณสามารถผ่านวัตถุชนิดใด ๆ สำหรับการเรียกเมธอด polymorphic แบบไม่ จำกัด นอกจากนี้สิ่งเดียวที่ขาดหายไปคือฟังก์ชั่นเพื่อเริ่มต้น structs ของคุณ (วัตถุ / คลาส) ซึ่งจะรวมถึงฟังก์ชั่น malloc และส่งกลับตัวชี้ บางทีฉันจะเพิ่มชิ้นส่วนของวิธีการส่งข้อความผ่าน (

1
นี่คือฟางทำลายฉันของ C ++ และใช้ C มากขึ้น (ก่อนที่ฉันจะใช้ C ++ สำหรับการสืบทอดเท่านั้น) ขอบคุณ
Anne Quinn

31

ฉันเคยทำงานกับห้องสมุด C ที่นำมาใช้ในทางที่ทำให้ฉันค่อนข้างสง่างาม พวกเขาเขียนใน C เป็นวิธีการกำหนดวัตถุจากนั้นสืบทอดจากพวกมันเพื่อให้สามารถขยายได้เหมือนกับวัตถุ C ++ แนวคิดพื้นฐานคือ:

  • แต่ละวัตถุมีไฟล์ของตัวเอง
  • ฟังก์ชั่นสาธารณะและตัวแปรที่กำหนดไว้ในไฟล์. h สำหรับวัตถุ
  • ตัวแปรและฟังก์ชั่นส่วนตัวอยู่ในไฟล์. c เท่านั้น
  • เมื่อต้องการ "รับช่วง" โครงสร้างใหม่จะถูกสร้างขึ้นโดยมีสมาชิกรายแรกของโครงสร้างเป็นวัตถุที่สืบทอดมา

การสืบทอดเป็นการยากที่จะอธิบาย แต่โดยพื้นฐานแล้วมันคือ:

struct vehicle {
   int power;
   int weight;
}

จากนั้นในไฟล์อื่น:

struct van {
   struct vehicle base;
   int cubic_size;
}

จากนั้นคุณสามารถสร้างรถตู้ในหน่วยความจำและใช้รหัสที่รู้เกี่ยวกับยานพาหนะเท่านั้น:

struct van my_van;
struct vehicle *something = &my_van;
vehicle_function( something );

มันทำงานได้อย่างสวยงามและไฟล์. h กำหนดไว้อย่างชัดเจนว่าคุณควรทำอะไรกับแต่ละวัตถุ


ฉันชอบวิธีนี้มากยกเว้นว่า internals ของ "object" ทั้งหมดเป็นสาธารณะ
Lawrence Dol

6
@ ซอฟต์แวร์ Monkey: C ไม่มีการควบคุมการเข้าถึง วิธีเดียวที่จะซ่อนรายละเอียดการใช้งานคือการโต้ตอบผ่านตัวชี้ทึบซึ่งอาจเจ็บปวดมากเนื่องจากฟิลด์ทั้งหมดจะต้องเข้าถึงได้ผ่านวิธีการเข้าถึงซึ่งอาจไม่สามารถ inline
Adam Rosenfield

1
@Adam: คอมไพเลอร์สนับสนุนการเพิ่มประสิทธิภาพการเชื่อมโยงเวลาจะ inline พวกเขาเพียงแค่ปรับ ...
คริสโต

9
หากคุณทำเช่นนี้คุณควรตรวจสอบให้แน่ใจว่าฟังก์ชั่นทั้งหมดในไฟล์. c ที่ไม่ได้กำหนดเป็นสาธารณะจะถูกกำหนดเป็นสแตติกดังนั้นพวกเขาจะไม่ได้รับฟังก์ชั่นที่ตั้งชื่อในไฟล์วัตถุของคุณ ที่ทำให้แน่ใจว่าไม่มีใครสามารถค้นหาชื่อของพวกเขาในขั้นตอนการเชื่อมโยง
jmucchiello

2
@Marcel: C ถูกใช้เนื่องจากมีการปรับใช้รหัสบนบอร์ดระดับต่ำที่ใช้โปรเซสเซอร์ที่หลากหลายสำหรับระบบอัตโนมัติ พวกเขาทั้งหมดได้รับการสนับสนุนรวบรวมจาก C ถึงไบนารีพื้นเมืองของตน วิธีการนี้ทำให้อ่านง่ายมากเมื่อคุณรู้ว่าพวกเขากำลังพยายามทำอะไร
Kieveli

18

เดสก์ท็อป GNOME สำหรับ Linux เขียนด้วยวัตถุเชิงวัตถุ C และมีแบบจำลองวัตถุที่เรียกว่า " GObject " ซึ่งรองรับคุณสมบัติการสืบทอดพหุสัณฐานรวมถึงสารพัดอื่น ๆ เช่นการอ้างอิงการจัดการเหตุการณ์ (เรียกว่า "สัญญาณ") รันไทม์ การพิมพ์ข้อมูลส่วนตัว ฯลฯ

มันมีแฮ็กตัวประมวลผลล่วงหน้าเพื่อทำสิ่งต่างๆเช่น typecasting รอบ ๆ ในลำดับชั้นของชั้นเป็นต้นนี่คือตัวอย่างชั้นที่ฉันเขียนให้ GNOME (สิ่งที่ชอบ gchar เป็น typedefs):

แหล่งที่มาของคลาส

ส่วนหัวของคลาส

ภายในโครงสร้าง GObject มีจำนวนเต็ม GType ซึ่งใช้เป็นเลขเวทสำหรับระบบการพิมพ์แบบไดนามิกของ GLib (คุณสามารถแปลงโครงสร้างทั้งหมดเป็น "GType" เพื่อค้นหาประเภทของมัน)


น่าเสียดายที่ไฟล์ read me / tutorial (ลิงค์ wiki) ไม่ทำงานและมีคู่มืออ้างอิงเท่านั้นสำหรับเรื่องนั้น (ฉันกำลังพูดถึง GObject และไม่ใช่ GTK) โปรดให้ไฟล์กวดวิชาบางอย่างสำหรับเดียวกัน ...
FL4SOF

ลิงค์ได้รับการแก้ไขแล้ว
James Cape

4
ลิงก์เสียอีกครั้ง
SeanRamey

6

ฉันเคยทำสิ่งนี้ใน C ก่อนที่ฉันจะรู้ว่า OOP คืออะไร

ต่อไปนี้เป็นตัวอย่างซึ่งใช้ data-buffer ซึ่งเติบโตตามความต้องการกำหนดขนาดขั้นต่ำเพิ่มและขนาดสูงสุด การนำไปใช้งานนี้โดยเฉพาะคือ "องค์ประกอบ" ซึ่งกล่าวได้ว่ามันถูกออกแบบมาเพื่ออนุญาตให้มีการรวบรวมรายการที่คล้ายกับประเภท C ใด ๆ ไม่ใช่แค่ความยาวตัวแปรไบต์บัฟเฟอร์

แนวคิดคือวัตถุนั้นสร้างอินสแตนซ์โดยใช้ xxx_crt () และลบโดยใช้ xxx_dlt () แต่ละวิธี "สมาชิก" ใช้ตัวชี้ที่พิมพ์เฉพาะเพื่อดำเนินการ

ฉันใช้รายการที่เชื่อมโยงบัฟเฟอร์แบบวนรอบและสิ่งอื่น ๆ จำนวนมากในลักษณะนี้

ฉันต้องสารภาพฉันไม่เคยคิดเลยว่าจะใช้มรดกด้วยวิธีนี้อย่างไร ฉันคิดว่าการผสมผสานบางอย่างของ Kieveli ที่นำเสนออาจเป็นเส้นทางที่ดี

dtb.c:

#include <limits.h>
#include <string.h>
#include <stdlib.h>

static void dtb_xlt(void *dst, const void *src, vint len, const byte *tbl);

DTABUF *dtb_crt(vint minsiz,vint incsiz,vint maxsiz) {
    DTABUF          *dbp;

    if(!minsiz) { return NULL; }
    if(!incsiz)                  { incsiz=minsiz;        }
    if(!maxsiz || maxsiz<minsiz) { maxsiz=minsiz;        }
    if(minsiz+incsiz>maxsiz)     { incsiz=maxsiz-minsiz; }
    if((dbp=(DTABUF*)malloc(sizeof(*dbp))) == NULL) { return NULL; }
    memset(dbp,0,sizeof(*dbp));
    dbp->min=minsiz;
    dbp->inc=incsiz;
    dbp->max=maxsiz;
    dbp->siz=minsiz;
    dbp->cur=0;
    if((dbp->dta=(byte*)malloc((vuns)minsiz)) == NULL) { free(dbp); return NULL; }
    return dbp;
    }

DTABUF *dtb_dlt(DTABUF *dbp) {
    if(dbp) {
        free(dbp->dta);
        free(dbp);
        }
    return NULL;
    }

vint dtb_adddta(DTABUF *dbp,const byte *xlt256,const void *dtaptr,vint dtalen) {
    if(!dbp) { errno=EINVAL; return -1; }
    if(dtalen==-1) { dtalen=(vint)strlen((byte*)dtaptr); }
    if((dbp->cur + dtalen) > dbp->siz) {
        void        *newdta;
        vint        newsiz;

        if((dbp->siz+dbp->inc)>=(dbp->cur+dtalen)) { newsiz=dbp->siz+dbp->inc; }
        else                                       { newsiz=dbp->cur+dtalen;   }
        if(newsiz>dbp->max) { errno=ETRUNC; return -1; }
        if((newdta=realloc(dbp->dta,(vuns)newsiz))==NULL) { return -1; }
        dbp->dta=newdta; dbp->siz=newsiz;
        }
    if(dtalen) {
        if(xlt256) { dtb_xlt(((byte*)dbp->dta+dbp->cur),dtaptr,dtalen,xlt256); }
        else       { memcpy(((byte*)dbp->dta+dbp->cur),dtaptr,(vuns)dtalen);   }
        dbp->cur+=dtalen;
        }
    return 0;
    }

static void dtb_xlt(void *dst,const void *src,vint len,const byte *tbl) {
    byte            *sp,*dp;

    for(sp=(byte*)src,dp=(byte*)dst; len; len--,sp++,dp++) { *dp=tbl[*sp]; }
    }

vint dtb_addtxt(DTABUF *dbp,const byte *xlt256,const byte *format,...) {
    byte            textÝ501¨;
    va_list         ap;
    vint            len;

    va_start(ap,format); len=sprintf_len(format,ap)-1; va_end(ap);
    if(len<0 || len>=sizeof(text)) { sprintf_safe(text,sizeof(text),"STRTOOLNG: %s",format); len=(int)strlen(text); }
    else                           { va_start(ap,format); vsprintf(text,format,ap); va_end(ap);                     }
    return dtb_adddta(dbp,xlt256,text,len);
    }

vint dtb_rmvdta(DTABUF *dbp,vint len) {
    if(!dbp) { errno=EINVAL; return -1; }
    if(len > dbp->cur) { len=dbp->cur; }
    dbp->cur-=len;
    return 0;
    }

vint dtb_reset(DTABUF *dbp) {
    if(!dbp) { errno=EINVAL; return -1; }
    dbp->cur=0;
    if(dbp->siz > dbp->min) {
        byte *newdta;
        if((newdta=(byte*)realloc(dbp->dta,(vuns)dbp->min))==NULL) {
            free(dbp->dta); dbp->dta=null; dbp->siz=0;
            return -1;
            }
        dbp->dta=newdta; dbp->siz=dbp->min;
        }
    return 0;
    }

void *dtb_elmptr(DTABUF *dbp,vint elmidx,vint elmlen) {
    if(!elmlen || (elmidx*elmlen)>=dbp->cur) { return NULL; }
    return ((byte*)dbp->dta+(elmidx*elmlen));
    }

dtb.h

typedef _Packed struct {
    vint            min;                /* initial size                       */
    vint            inc;                /* increment size                     */
    vint            max;                /* maximum size                       */
    vint            siz;                /* current size                       */
    vint            cur;                /* current data length                */
    void            *dta;               /* data pointer                       */
    } DTABUF;

#define dtb_dtaptr(mDBP)                (mDBP->dta)
#define dtb_dtalen(mDBP)                (mDBP->cur)

DTABUF              *dtb_crt(vint minsiz,vint incsiz,vint maxsiz);
DTABUF              *dtb_dlt(DTABUF *dbp);
vint                dtb_adddta(DTABUF *dbp,const byte *xlt256,const void *dtaptr,vint dtalen);
vint                dtb_addtxt(DTABUF *dbp,const byte *xlt256,const byte *format,...);
vint                dtb_rmvdta(DTABUF *dbp,vint len);
vint                dtb_reset(DTABUF *dbp);
void                *dtb_elmptr(DTABUF *dbp,vint elmidx,vint elmlen);

PS: vint เป็นเพียงประเภทของ int - ฉันใช้มันเพื่อเตือนฉันว่าความยาวนั้นแปรผันจากแพลตฟอร์มหนึ่งไปอีกแพลตฟอร์มหนึ่ง (สำหรับการย้ายพอร์ต)


7
moly ศักดิ์สิทธิ์นี่สามารถชนะการประกวด C ที่สับสน! ฉันชอบมัน! :)
horseyguy

@horseyguy ไม่มันทำไม่ได้ มันถูกเผยแพร่ นอกจากนี้ยังพิจารณาการรวมไฟล์ส่วนหัวกับเครื่องมือ iocccsize มันยังไม่สมบูรณ์โปรแกรม 2009 ไม่มีการแข่งขันดังนั้นจึงไม่สามารถเปรียบเทียบ iocccsize ได้ CPP ถูกทารุณกรรมหลายต่อหลายครั้งดังนั้นมันค่อนข้างเก่า ฯลฯ ขออภัย ฉันไม่ได้พยายามที่จะเป็นเชิงลบ แต่ฉันเป็นจริง ฉันเรียงลำดับความหมายของคุณและมันเป็นการอ่านที่ดีและฉันได้รับการโหวต (และใช่ฉันมีส่วนร่วมอยู่ในนั้นและใช่ฉันชนะเกินไป.)
Pryftan

6

ปิดหัวข้อเล็กน้อย แต่คอมไพเลอร์ C ++ ดั้งเดิม, Cfrontรวบรวม C ++ เป็น C จากนั้นไปที่แอสเซมเบลอร์

เก็บรักษาไว้ที่นี่


ฉันเคยเห็นมันมาก่อน ฉันเชื่อว่ามันเป็นงานที่ดี

@Anthony Cuozzo: Stan Lippman เขียนหนังสือเล่มหนึ่งชื่อว่า 'C ++ - Inside the model model' ซึ่งเขาเกี่ยวข้องกับประสบการณ์และการตัดสินใจออกแบบในการเขียนและการบำรุงรักษา c-front ก็ยังคงเป็นที่ดีอ่านและช่วยให้ผมอย่างมากเมื่อเปลี่ยนจาก C ถึง C ++ หลายปีที่ผ่านมา
zebrabox

5

ถ้าคุณคิดว่าวิธีการที่เรียกว่าวัตถุเป็นวิธีการคงที่ที่ผ่านการนัย 'this ' ไปยังฟังก์ชันจะทำให้ OO ใน C คิดได้ง่ายขึ้น

ตัวอย่างเช่น:

String s = "hi";
System.out.println(s.length());

กลายเป็น:

string s = "hi";
printf(length(s)); // pass in s, as an implicit this

หรืออะไรทำนองนั้น


6
@Artelius: แน่นอน แต่บางครั้งก็ไม่ชัดเจนจนกว่าจะมีการระบุไว้ +1 สำหรับสิ่งนี้
Lawrence Dol

1
จะดีกว่านี้อีกแล้วstring->length(s);
OozeMeister

4

ffmpeg (ชุดเครื่องมือสำหรับการประมวลผลวิดีโอ) เขียนในแบบตรง C (และภาษาแอสเซมบลี) แต่ใช้สไตล์เชิงวัตถุ มันเต็มไปด้วย structs กับพอยน์เตอร์ของฟังก์ชัน มีชุดของฟังก์ชั่นจากโรงงานที่เริ่มต้นโครงสร้างด้วยตัวชี้ "วิธีการ" ที่เหมาะสม


ฉันไม่เห็นฟังก์ชั่นของโรงงานใด ๆ ในนั้น (ffmpeg) แต่ดูเหมือนว่ามันจะไม่ใช้ polymorphism / การสืบทอด (วิธีที่แนะนำเล็กน้อยด้านบน)
FL4SOF

avcodec_open เป็นฟังก์ชั่นหนึ่งจากโรงงาน มันยัดตัวชี้ฟังก์ชันลงในโครงสร้าง AVCodecContext (เช่น draw_horiz_band) หากคุณดูการใช้งานแมโครของ FF_COMMON_FRAME ใน avcodec.h คุณจะเห็นบางสิ่งบางอย่างคล้ายกับการสืบทอดของสมาชิกข้อมูล IMHO, ffmpeg พิสูจน์ให้ผมเห็นว่า OOP จะทำดีที่สุดใน C ++ ไม่ได้ซี
นาย fooz

3

จริงๆถ้าคุณคิดว่า catefully มาตรฐานแม้ C ใช้ห้องสมุด OOP - พิจารณาFILE *เป็นตัวอย่าง: fopen()เริ่มต้นFILE *วัตถุและคุณใช้มันใช้วิธีการสมาชิกfscanf(), fprintf(), fread(), fwrite()และคนอื่น ๆ fclose()และในที่สุดก็จบมันด้วย

คุณสามารถไปกับ pseudo-Objective-C ด้วยวิธีที่ไม่ยากเช่นกัน:

typedef void *Class;

typedef struct __class_Foo
{
    Class isa;
    int ivar;
} Foo;

typedef struct __meta_Foo
{
    Foo *(*alloc)(void);
    Foo *(*init)(Foo *self);
    int (*ivar)(Foo *self);
    void (*setIvar)(Foo *self);
} meta_Foo;

meta_Foo *class_Foo;

void __meta_Foo_init(void) __attribute__((constructor));
void __meta_Foo_init(void)
{
    class_Foo = malloc(sizeof(meta_Foo));
    if (class_Foo)
    {
        class_Foo = {__imp_Foo_alloc, __imp_Foo_init, __imp_Foo_ivar, __imp_Foo_setIvar};
    }
}

Foo *__imp_Foo_alloc(void)
{
    Foo *foo = malloc(sizeof(Foo));
    if (foo)
    {
        memset(foo, 0, sizeof(Foo));
        foo->isa = class_Foo;
    }
    return foo;
}

Foo *__imp_Foo_init(Foo *self)
{
    if (self)
    {
        self->ivar = 42;
    }
    return self;
}
// ...

ใช้:

int main(void)
{
    Foo *foo = (class_Foo->init)((class_Foo->alloc)());
    printf("%d\n", (foo->isa->ivar)(foo)); // 42
    foo->isa->setIvar(foo, 60);
    printf("%d\n", (foo->isa->ivar)(foo)); // 60
    free(foo);
}

นี่คือสิ่งที่อาจเป็นผลมาจากรหัส Objective-C บางอย่างเช่นนี้หากใช้ตัวแปล Objective-C-to-C ที่ค่อนข้างเก่า:

@interface Foo : NSObject
{
    int ivar;
}
- (int)ivar;
- (void)setIvar:(int)ivar;
@end

@implementation Foo
- (id)init
{
    if (self = [super init])
    {
        ivar = 42;
    }
    return self;
}
@end

int main(void)
{
    Foo *foo = [[Foo alloc] init];
    printf("%d\n", [foo ivar]);
    [foo setIvar:60];
    printf("%d\n", [foo ivar]);
    [foo release];
}

มีอะไร__attribute__((constructor))ทำในvoid __meta_Foo_init(void) __attribute__((constructor))?
AE Drew

1
นี่เป็นส่วนขยาย GCC ที่จะทำให้แน่ใจว่าฟังก์ชั่นที่ทำเครื่องหมายถูกเรียกเมื่อไบนารีถูกโหลดเข้าสู่หน่วยความจำ @AEDrew
Maxthon Chan

popen(3)ส่งกลับค่า a FILE *สำหรับตัวอย่างอื่น
Pryftan

3

ฉันคิดว่าสิ่งที่ Adam Rosenfield โพสต์เป็นวิธีที่ถูกต้องในการทำ OOP ใน C. ฉันต้องการเพิ่มว่าสิ่งที่เขาแสดงคือการนำไปใช้ของวัตถุ กล่าวอีกนัยหนึ่งคือการนำการติดตั้งไปใช้จริงใน.cไฟล์ขณะที่อินเตอร์เฟสจะถูกวางไว้ในส่วนหัว.hไฟล์ตัวอย่างเช่นการใช้ตัวอย่างลิงด้านบน:

อินเทอร์เฟซจะมีลักษณะดังนี้:

//monkey.h

    struct _monkey;

    typedef struct _monkey monkey;

    //memory management
    monkey * monkey_new();
    int monkey_delete(monkey *thisobj);
    //methods
    void monkey_dance(monkey *thisobj);

คุณสามารถดูได้ในอินเทอร์เฟซ .hไฟล์คุณกำหนดเฉพาะต้นแบบ จากนั้นคุณสามารถรวบรวมส่วน "การนำไปใช้งาน.c" ลงในไลบรารีแบบคงที่หรือแบบไดนามิก สิ่งนี้สร้างการห่อหุ้มและคุณสามารถเปลี่ยนการใช้งานได้ตามต้องการ ผู้ใช้วัตถุของคุณจำเป็นต้องรู้อะไรเกี่ยวกับการใช้งานมัน นอกจากนี้ยังให้ความสำคัญกับการออกแบบโดยรวมของวัตถุ

มันเป็นความเชื่อส่วนตัวของฉันที่ว่า oop เป็นวิธีการกำหนดโครงสร้างโค้ดและการใช้ซ้ำของคุณและไม่มีอะไรเกี่ยวข้องกับสิ่งอื่น ๆ เหล่านั้นที่ถูกเพิ่มใน c ++ เช่นการโหลดมากเกินไปหรือเทมเพลต ใช่คุณสมบัติเหล่านี้มีประโยชน์มาก แต่ไม่ได้เป็นตัวแทนของการเขียนโปรแกรมเชิงวัตถุจริงๆ


คุณสามารถประกาศโครงสร้างด้วยtypedef struct Monkey {} Monkey; จุดประสงค์ในการพิมพ์ได้หลังจากที่สร้างขึ้นแล้ว?
MarcusJ

1
@ MarcusJ The struct _monkeyเป็นเพียงต้นแบบ นิยามชนิดที่แท้จริงถูกกำหนดไว้ในไฟล์การนำไปใช้งาน (ไฟล์. c) สิ่งนี้จะสร้างเอฟเฟกต์การห่อหุ้มและช่วยให้ผู้พัฒนา API สามารถกำหนดโครงสร้างลิงในอนาคตโดยไม่ต้องแก้ไข API ผู้ใช้ API จะต้องเกี่ยวข้องกับวิธีการจริงเท่านั้น ผู้ออกแบบ API ดูแลการใช้งานรวมถึงวิธีการจัดวางวัตถุ / โครงสร้าง ดังนั้นรายละเอียดของวัตถุ / struct จึงถูกซ่อนจากผู้ใช้ (ชนิดทึบแสง)

ฉันกำหนดโครงสร้างของฉันในส่วนหัวนี่ไม่ใช่มาตรฐานหรือไม่ ฉันทำอย่างนั้นเพราะบางครั้งฉันจำเป็นต้องเข้าถึงสมาชิกของโครงสร้างภายนอกห้องสมุด
MarcusJ

1
@MarcusJ คุณสามารถกำหนดโครงสร้างในส่วนหัวได้หากคุณต้องการ (ไม่มีมาตรฐาน) แต่ถ้าคุณต้องการที่จะเปลี่ยนมันเป็นโครงสร้างภายในถนนคุณอาจทำลายรหัสของคุณ Encapsulation เป็นเพียงรูปแบบของการเข้ารหัสที่ทำให้เปลี่ยนการใช้งานได้ง่ายขึ้นโดยไม่ทำให้โค้ดของคุณเสียหาย คุณสามารถเข้าถึงสมาชิกของคุณผ่านวิธีการเข้าถึงเช่นint getCount(ObjectType obj)ฯลฯ หากคุณเลือกที่จะกำหนดโครงสร้างในไฟล์การใช้งาน

2

คำแนะนำของฉัน: ทำให้มันง่าย หนึ่งในปัญหาที่ใหญ่ที่สุดที่ฉันมีคือการบำรุงรักษาซอฟต์แวร์รุ่นเก่า (บางครั้งอายุมากกว่า 10 ปี) หากรหัสไม่ง่ายอาจเป็นเรื่องยาก ใช่เราสามารถเขียน OOP ที่มีประโยชน์มากด้วย polymorphism ใน C แต่มันยากที่จะอ่าน

ฉันชอบวัตถุที่เรียบง่ายที่ห่อหุ้มฟังก์ชันการทำงานที่กำหนดไว้อย่างดี ตัวอย่างที่ดีของสิ่งนี้คือGLIB2ตัวอย่างเช่นตารางแฮช:

GHastTable* my_hash = g_hash_table_new(g_str_hash, g_str_equal);
int size = g_hash_table_size(my_hash);
...

g_hash_table_remove(my_hash, some_key);

กุญแจคือ:

  1. สถาปัตยกรรมที่เรียบง่ายและรูปแบบการออกแบบ
  2. บรรลุการห่อหุ้ม OOP ขั้นพื้นฐาน
  3. ใช้งานง่ายอ่านเข้าใจและบำรุงรักษา

1

ถ้าฉันจะเขียน OOP ใน CI อาจจะไปพร้อมกับหลอก - Pimplออกแบบแทนที่จะส่งพอยน์เตอร์ไปที่ struct สิ่งนี้ทำให้เนื้อหาทึบแสงและช่วยให้เกิดความแตกต่างและการสืบทอด

ปัญหาที่แท้จริงของ OOP ใน C คือสิ่งที่เกิดขึ้นเมื่อตัวแปรออกจากขอบเขต ไม่มี destructors ที่คอมไพเลอร์สร้างขึ้นและอาจทำให้เกิดปัญหาได้ มาโครอาจช่วยได้ แต่ก็ดูน่าเกลียดอยู่ตลอดเวลา


1
เมื่อการเขียนโปรแกรมใน C ฉันจัดการกับขอบเขตโดยใช้ifคำสั่งและปล่อยพวกเขาในตอนท้าย ตัวอย่างเช่นif ( (obj = new_myObject()) ) { /* code using myObject */ free_myObject(obj); }

1

อีกวิธีในการโปรแกรมในลักษณะเชิงวัตถุด้วย C คือการใช้ตัวสร้างโค้ดซึ่งแปลงภาษาเฉพาะโดเมนเป็น C ดังเช่นที่ทำกับ TypeScript และ JavaScript เพื่อให้ OOP เป็น js


0
#include "triangle.h"
#include "rectangle.h"
#include "polygon.h"

#include <stdio.h>

int main()
{
    Triangle tr1= CTriangle->new();
    Rectangle rc1= CRectangle->new();

    tr1->width= rc1->width= 3.2;
    tr1->height= rc1->height= 4.1;

    CPolygon->printArea((Polygon)tr1);

    printf("\n");

    CPolygon->printArea((Polygon)rc1);
}

เอาท์พุท:

6.56
13.12

นี่คือรายการของการเขียนโปรแกรม OO กับ C.

นี่คือ C จริงแท้ไม่มีมาโครตัวประมวลผลล่วงหน้า เรามีการสืบทอดความหลากหลายและการห่อหุ้มข้อมูล (รวมถึงข้อมูลส่วนตัวไปยังคลาสหรือวัตถุ) ไม่มีโอกาสที่จะได้รับการรับรองเทียบเท่ากับการป้องกันข้อมูลนั่นคือข้อมูลส่วนตัวลงในห่วงโซ่การรับรู้ด้วยเช่นกัน แต่นี่ไม่ใช่ความไม่สะดวกเพราะฉันไม่คิดว่าจำเป็น

CPolygon ไม่ได้เป็นอินสแตนซ์เพราะเราใช้เพื่อจัดการกับวัตถุของโซ่แห่งการสืบทอดที่มีลักษณะร่วมกัน แต่มีการใช้งานที่แตกต่างกัน (Polymorphism)


0

@Adam Rosenfield มีคำอธิบายที่ดีมากเกี่ยวกับวิธีการบรรลุ OOP ด้วย C

นอกจากนี้ฉันอยากจะแนะนำให้คุณอ่าน

1) pjsip

ห้องสมุด C ที่ดีมากสำหรับ VoIP คุณสามารถเรียนรู้วิธีการใช้ OOP ผ่านโครงสร้างและตารางตัวชี้ฟังก์ชัน

2) iOS Runtime

เรียนรู้ว่า iOS Runtime มอบอำนาจให้ C วัตถุประสงค์มันบรรลุ OOP ผ่านตัวชี้ ISA, คลาสเมตา


0

สำหรับฉันการวางแนววัตถุใน C ควรมีคุณสมบัติเหล่านี้:

  1. การห่อหุ้มและการซ่อนข้อมูล (สามารถทำได้โดยใช้ตัวชี้ structs / opaque)

  2. การสืบทอดและการสนับสนุนสำหรับความหลากหลาย (การสืบทอดเดี่ยวสามารถทำได้โดยใช้ structs - ตรวจสอบให้แน่ใจว่าฐานนามธรรมไม่สามารถใช้งานได้ทันที)

  3. ฟังก์ชั่นการสร้างและ destructor (ไม่ใช่เรื่องง่ายที่จะบรรลุ)

  4. การตรวจสอบประเภท (อย่างน้อยสำหรับประเภทที่ผู้ใช้กำหนดเนื่องจาก C ไม่มีการบังคับใช้)

  5. การนับการอ้างอิง (หรือบางอย่างเพื่อนำไปใช้RAII )

  6. การสนับสนุนที่ จำกัด สำหรับการจัดการข้อยกเว้น (setjmp และ longjmp)

ยิ่งไปกว่านั้นควรใช้ข้อมูลจำเพาะ ANSI / ISO และไม่ควรใช้ฟังก์ชั่นเฉพาะของคอมไพเลอร์


สำหรับหมายเลข (5) - คุณไม่สามารถใช้ RAII ในภาษาที่ไม่มี destructors (ซึ่งหมายความว่า RAII ไม่ใช่เทคนิคที่คอมไพเลอร์สนับสนุนใน C หรือ Java)
Tom

นวกรรมิกและ destructors สามารถเขียนสำหรับวัตถุที่ใช้ c - ฉันเดาว่า GObject ทำ และ ofcourse RAAI (มันไม่ได้ตรงไปข้างหน้าอาจจะน่าเกลียดและไม่จำเป็นต้องปฏิบัติในทุก ๆ ) - ทั้งหมดที่ฉันกำลังมองหาคือการระบุความหมายตาม C เพื่อให้บรรลุข้างต้น
FL4SOF

C ไม่รองรับ destructors คุณต้องพิมพ์บางอย่างเพื่อให้มันทำงานได้ นั่นหมายความว่าพวกเขาจะไม่ทำความสะอาดตัวเอง GObject ไม่ได้เปลี่ยนภาษา
Tom

0

ดูhttp://ldeniau.web.cern.ch/ldeniau/html/oopc/oopc.html หากไม่มีสิ่งอื่นใดที่อ่านผ่านเอกสารเป็นประสบการณ์การรู้แจ้ง


3
โปรดระบุบริบทสำหรับลิงค์ที่คุณแบ่งปัน แม้ว่าลิงก์ที่คุณแบ่งปันอาจมีประโยชน์มาก แต่ก็ขอแนะนำให้คุณจับประเด็นสำคัญ ๆ ของบทความที่แบ่งปันซึ่งตอบคำถาม ด้วยวิธีนี้แม้ว่าลิงก์จะถูกลบคำตอบของคุณจะยังคงเกี่ยวข้องและเป็นประโยชน์
ishmaelMakitla

0

ฉันมาช้าไปงานปาร์ตี้ที่นี่ แต่ฉันชอบที่จะหลีกเลี่ยงสุดขั้วทั้งสองอย่าง - รหัสมากเกินไปหรือมากเกินไปทำให้งงงวย แต่แมโครที่เห็นได้ชัดเจนสองคู่สามารถทำให้รหัส OOP ง่ายต่อการพัฒนาและอ่าน:

/*
 * OOP in C
 *
 * gcc -o oop oop.c
 */

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

struct obj2d {
    float x;                            // object center x
    float y;                            // object center y
    float (* area)(void *);
};

#define X(obj)          (obj)->b1.x
#define Y(obj)          (obj)->b1.y
#define AREA(obj)       (obj)->b1.area(obj)

void *
_new_obj2d(int size, void * areafn)
{
    struct obj2d * x = calloc(1, size);
    x->area = areafn;
    // obj2d constructor code ...
    return x;
}

// --------------------------------------------------------

struct rectangle {
    struct obj2d b1;        // base class
    float width;
    float height;
    float rotation;
};

#define WIDTH(obj)      (obj)->width
#define HEIGHT(obj)     (obj)->height

float rectangle_area(struct rectangle * self)
{
    return self->width * self->height;
}

#define NEW_rectangle()  _new_obj2d(sizeof(struct rectangle), rectangle_area)

// --------------------------------------------------------

struct triangle {
    struct obj2d b1;
    // deliberately unfinished to test error messages
};

#define NEW_triangle()  _new_obj2d(sizeof(struct triangle), triangle_area)

// --------------------------------------------------------

struct circle {
    struct obj2d b1;
    float radius;
};

#define RADIUS(obj)     (obj)->radius

float circle_area(struct circle * self)
{
    return M_PI * self->radius * self->radius;
}

#define NEW_circle()     _new_obj2d(sizeof(struct circle), circle_area)

// --------------------------------------------------------

#define NEW(objname)            (struct objname *) NEW_##objname()


int
main(int ac, char * av[])
{
    struct rectangle * obj1 = NEW(rectangle);
    struct circle    * obj2 = NEW(circle);

    X(obj1) = 1;
    Y(obj1) = 1;

    // your decision as to which of these is clearer, but note above that
    // macros also hide the fact that a member is in the base class

    WIDTH(obj1)  = 2;
    obj1->height = 3;

    printf("obj1 position (%f,%f) area %f\n", X(obj1), Y(obj1), AREA(obj1));

    X(obj2) = 10;
    Y(obj2) = 10;
    RADIUS(obj2) = 1.5;
    printf("obj2 position (%f,%f) area %f\n", X(obj2), Y(obj2), AREA(obj2));

    // WIDTH(obj2)  = 2;                                // error: struct circle has no member named width
    // struct triangle  * obj3 = NEW(triangle);         // error: triangle_area undefined
}

ฉันคิดว่าสิ่งนี้มีความสมดุลที่ดีและข้อผิดพลาดที่เกิดขึ้น (อย่างน้อยกับตัวเลือก gcc 6.3 ที่เป็นค่าเริ่มต้น) สำหรับข้อผิดพลาดที่เป็นไปได้บางส่วนนั้นมีประโยชน์แทนที่จะทำให้เกิดความสับสน ประเด็นทั้งหมดคือการปรับปรุงประสิทธิภาพการทำงานของโปรแกรมเมอร์ไม่ใช่เหรอ?


0

หากคุณต้องการเขียนรหัสเล็กน้อยลองทำสิ่งนี้: https://github.com/fulminati/class-framework

#include "class-framework.h"

CLASS (People) {
    int age;
};

int main()
{
    People *p = NEW (People);

    p->age = 10;

    printf("%d\n", p->age);
}

2
โปรดอย่าเพิ่งโพสต์เครื่องมือหรือห้องสมุดเพื่อเป็นคำตอบ อย่างน้อยก็แสดงให้เห็นว่ามันแก้ปัญหาในคำตอบของตัวเอง
Baum mit Augen

0

ฉันกำลังทำสิ่งนี้ด้วยวิธีแก้ปัญหามาโคร ดังนั้นสำหรับผู้ที่กล้าหาญเท่านั้นฉันเดาว่า ;-) แต่มันค่อนข้างดีอยู่แล้วและฉันก็กำลังทำโปรเจ็กต์สองสามชิ้นอยู่ด้านบน มันทำงานเพื่อให้คุณกำหนดไฟล์ส่วนหัวที่แยกต่างหากสำหรับแต่ละชั้นเรียน แบบนี้:

#define CLASS Point
#define BUILD_JSON

#define Point__define                            \
    METHOD(Point,public,int,move_up,(int steps)) \
    METHOD(Point,public,void,draw)               \
                                                 \
    VAR(read,int,x,JSON(json_int))               \
    VAR(read,int,y,JSON(json_int))               \

ในการใช้คลาสคุณสร้างไฟล์ส่วนหัวและไฟล์ C ที่คุณใช้เมธอด:

METHOD(Point,public,void,draw)
{
    printf("point at %d,%d\n", self->x, self->y);
}

ในส่วนหัวที่คุณสร้างขึ้นสำหรับคลาสคุณรวมส่วนหัวอื่น ๆ ที่คุณต้องการและกำหนดประเภท ฯลฯ ที่เกี่ยวข้องกับชั้นเรียน ทั้งในส่วนหัวของชั้นเรียนและในไฟล์ C คุณรวมไฟล์ข้อกำหนดคุณสมบัติของชั้นเรียน (ดูตัวอย่างรหัสแรก) และ X-แมโคร X-macros เหล่านี้ ( 1 , 2 , 3ฯลฯ ) จะขยายรหัสไปยังคลาสโครงสร้างที่แท้จริงและการประกาศอื่น ๆ

เพื่อสืบทอดคลาส#define SUPER supernameและเพิ่มsupername__define \เป็นบรรทัดแรกในนิยามคลาส ทั้งสองจะต้องมี นอกจากนี้ยังมีการสนับสนุน JSON, สัญญาณ, คลาสนามธรรมและอื่น ๆ

W_NEW(classname, .x=1, .y=2,...)การสร้างวัตถุใช้เพียง การเริ่มต้นขึ้นอยู่กับการกำหนดค่าเริ่มต้น struct ใน C11 มันทำงานได้ดีและทุกอย่างที่ไม่อยู่ในรายการถูกตั้งค่าเป็นศูนย์

W_CALL(o,method)(1,2,3)จะเรียกวิธีการใช้งาน ดูเหมือนว่าการเรียกใช้ฟังก์ชันคำสั่งซื้อที่สูงขึ้น แต่เป็นเพียงแมโคร มันขยายตัว((o)->klass->method(o,1,2,3))ซึ่งเป็นการแฮ็คที่ดีจริงๆ

โปรดดูเอกสารและรหัสตัวเอง

เนื่องจากเฟรมเวิร์กต้องการรหัสสำเร็จรูปบางแผ่นฉันจึงเขียนสคริปต์ Perl (wobject) ที่ทำงานได้ หากคุณใช้สิ่งนั้นคุณก็สามารถเขียนได้

class Point
    public int move_up(int steps)
    public void draw()
    read int x
    read int y

และมันจะสร้างไฟล์ข้อมูลจำเพาะคลาสส่วนหัวของชั้นเรียนและไฟล์ C ซึ่งรวมถึงPoint_impl.cตำแหน่งที่คุณใช้คลาส มันช่วยประหยัดงานได้ค่อนข้างมากถ้าคุณมีคลาสที่เรียบง่าย แต่ก็ยังมีทุกอย่างใน C. wobjectเป็นเครื่องสแกนแบบนิพจน์ทั่วไปที่ใช้ง่ายซึ่งปรับให้เข้ากับความต้องการเฉพาะหรือเขียนใหม่ตั้งแต่เริ่มต้น



0

คุณสามารถลองใช้COOPซึ่งเป็นเฟรมเวิร์กที่เป็นมิตรกับโปรแกรมเมอร์สำหรับ OOP ใน C, มีคุณสมบัติของคลาส, ข้อยกเว้น, ความหลากหลายและการจัดการหน่วยความจำ (สำคัญสำหรับโค้ดแบบฝัง) มันเป็นไวยากรณ์ที่ค่อนข้างเบาดูบทช่วยสอนใน Wiki ที่นั่น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.