Hướng đối tượng trong C


157

Điều gì sẽ là một tập hợp các hack tiền xử lý tiện lợi (tương thích ANSI C89 / ISO C90) cho phép một số loại hướng đối tượng xấu (nhưng có thể sử dụng) trong C?

Tôi quen thuộc với một vài ngôn ngữ hướng đối tượng khác nhau, vì vậy vui lòng không trả lời bằng các câu trả lời như "Tìm hiểu C ++!". Tôi đã đọc " Lập trình hướng đối tượng với ANSI C " (hãy cẩn thận: định dạng PDF ) và một số giải pháp thú vị khác, nhưng tôi chủ yếu quan tâm đến bạn :-)!


Xem thêm Bạn có thể viết mã hướng đối tượng trong C không?


1
Tôi có thể trả lời để học D và sử dụng abi tương thích c cho nơi bạn thực sự cần C. digitalmars.com/d
Tim Matthews

2
@Dinah: Cảm ơn bạn đã "Xem thêm". Bài đăng đó thật thú vị.

1
Câu hỏi thú vị dường như là tại sao bạn muốn hack OOP tiền xử lý trên C.
Calyth

3
@Calyth: Tôi thấy rằng OOP rất hữu ích và "Tôi làm việc với một số hệ thống nhúng chỉ thực sự có trình biên dịch C" (từ phía trên). Hơn nữa, bạn không thấy các bản hack tiền xử lý tiện lợi để xem xét phải không?

Câu trả lời:


31

Hệ thống C Object (COS) nghe có vẻ đầy hứa hẹn (nó vẫn ở phiên bản alpha). Nó cố gắng giữ tối thiểu các khái niệm có sẵn vì mục đích đơn giản và linh hoạt: lập trình hướng đối tượng thống nhất bao gồm các lớp mở, siêu dữ liệu, siêu dữ liệu thuộc tính, tổng quát, đa phương thức, ủy quyền, ngoại lệ, hợp đồng và đóng. Có một bản nháp (PDF) mô tả nó.

Ngoại lệ trong C là một triển khai C89 của TRY-CATCH-FINALLY được tìm thấy trong các ngôn ngữ OO khác. Nó đi kèm với một testsuite và một số ví dụ.

Cả hai bởi Laurent Deniau, mà là làm việc rất nhiều vào OOP trong C .


@vonbrand COS đã di chuyển đến github nơi cam kết cuối cùng là vào mùa hè năm ngoái. Trưởng thành có thể giải thích thiếu cam kết.
philant

185

Tôi sẽ khuyên bạn không nên sử dụng tiền xử lý (ab) để thử và tạo cú pháp C giống với ngôn ngữ hướng đối tượng khác. Ở cấp độ cơ bản nhất, bạn chỉ cần sử dụng các cấu trúc đơn giản làm đối tượng và chuyển chúng xung quanh bằng con trỏ:

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

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

Để có được những thứ như kế thừa và đa hình, bạn phải làm việc chăm chỉ hơn một chút. Bạn có thể thực hiện kế thừa thủ công bằng cách có thành viên đầu tiên của cấu trúc là một thể hiện của siêu lớp, và sau đó bạn có thể chuyển xung quanh các con trỏ tới các lớp cơ sở và dẫn xuất:

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

Để có được tính đa hình (tức là các hàm ảo), bạn sử dụng các con trỏ hàm và các bảng con trỏ hàm tùy chọn, còn được gọi là các bảng ảo hoặc 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;
}

Và đó là cách bạn thực hiện đa hình trong C. Nó không đẹp, nhưng nó thực hiện công việc. Có một số vấn đề dính liên quan đến phôi con trỏ giữa các lớp cơ sở và lớp dẫn xuất, an toàn miễn là lớp cơ sở là thành viên đầu tiên của lớp dẫn xuất. Đa kế thừa khó hơn nhiều - trong trường hợp đó, để phân biệt giữa các lớp cơ sở khác với lớp đầu tiên, bạn cần điều chỉnh thủ công các con trỏ của mình dựa trên các độ lệch phù hợp, thực sự khó và dễ bị lỗi.

Một điều (khó khăn) khác mà bạn có thể làm là thay đổi kiểu động của một đối tượng khi chạy! Bạn chỉ cần gán lại cho nó một con trỏ vtable mới. Bạn thậm chí có thể thay đổi có chọn lọc một số chức năng ảo trong khi vẫn giữ các chức năng ảo khác, tạo các kiểu lai mới. Chỉ cần cẩn thận để tạo một vtable mới thay vì sửa đổi vtable toàn cầu, nếu không bạn sẽ vô tình ảnh hưởng đến tất cả các đối tượng của một loại nhất định.


6
Adam, niềm vui của việc thay đổi vtable toàn cầu của một loại là mô phỏng kiểu gõ vịt trong C. :)
jmucchiello

Bây giờ tôi thương hại C ++ ... Tất nhiên cú pháp C ++ rõ ràng hơn, nhưng vì nó không phải là một cú pháp tầm thường, tôi đã giảm nhẹ. Tôi tự hỏi nếu một cái gì đó lai giữa C ++ và C có thể đạt được, vì vậy void * vẫn sẽ là loại có thể đúc hợp lệ. Phần struct derived {struct base super;};rõ ràng để đoán cách thức hoạt động của nó, bởi vì thứ tự byte là chính xác.
jokoon

2
+1 cho mã thanh lịch, được viết tốt. Điều này thật đúng với gì mà tôi đã tìm kiếm!
Homunculus Reticulli

3
Làm tốt. Đây chính xác là cách tôi đã làm và nó cũng là cách chính xác. Thay vì yêu cầu một con trỏ tới struct / object, bạn chỉ nên chuyển một con trỏ tới một số nguyên (địa chỉ). Điều này sẽ cho phép bạn vượt qua trong bất kỳ loại đối tượng nào cho các cuộc gọi phương thức đa hình không giới hạn. Ngoài ra, thứ duy nhất còn thiếu là một hàm để khởi tạo cấu trúc (đối tượng / lớp) của bạn. Điều này sẽ bao gồm một hàm malloc và trả về một con trỏ. Có lẽ tôi sẽ thêm một phần về cách thực hiện chuyển tin nhắn (

1
Đây là rơm đã phá vỡ tôi của C ++ và để sử dụng C nhiều hơn (trước khi tôi chỉ sử dụng C ++ để thừa kế) Cảm ơn bạn
Anne Quinn

31

Tôi đã từng làm việc với một thư viện C được thực hiện theo cách khiến tôi khá thanh lịch. Họ đã viết, trong C, một cách để xác định các đối tượng, sau đó kế thừa từ chúng để chúng có thể mở rộng như một đối tượng C ++. Ý tưởng cơ bản là thế này:

  • Mỗi đối tượng có tập tin riêng của mình
  • Các hàm và biến công khai được định nghĩa trong tệp .h cho một đối tượng
  • Các biến và hàm riêng chỉ được đặt trong tệp .c
  • Để "kế thừa" một cấu trúc mới được tạo ra với thành viên đầu tiên của cấu trúc là đối tượng để kế thừa từ

Kế thừa rất khó để mô tả, nhưng về cơ bản nó là thế này:

struct vehicle {
   int power;
   int weight;
}

Sau đó, trong một tập tin khác:

struct van {
   struct vehicle base;
   int cubic_size;
}

Sau đó, bạn có thể có một chiếc xe được tạo trong bộ nhớ và được sử dụng bởi mã chỉ biết về xe cộ:

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

Nó hoạt động rất đẹp và các tệp .h xác định chính xác những gì bạn sẽ có thể làm với từng đối tượng.


Tôi thực sự thích giải pháp này, ngoại trừ tất cả các phần bên trong của "đối tượng" đều công khai.
Lawrence Dol

6
@Software Monkey: C không có quyền kiểm soát truy cập. Cách duy nhất để ẩn chi tiết triển khai là tương tác thông qua các con trỏ mờ, điều này có thể gây đau đớn, vì tất cả các trường sẽ cần được truy cập thông qua các phương thức truy cập có thể không được nội tuyến.
Adam Rosenfield

1
@Adam: Trình biên dịch hỗ trợ tối ưu hóa thời gian liên kết sẽ giúp chúng hoạt động tốt ...
Christoph

9
Nếu bạn làm điều này, bạn cũng nên đảm bảo rằng tất cả các hàm trong tệp .c không được xác định là công khai được xác định là tĩnh để chúng không kết thúc như các hàm được đặt tên trong tệp đối tượng của bạn. Điều đó đảm bảo không ai có thể tìm thấy tên của họ trong giai đoạn liên kết.
jmucchiello

2
@Marcel: C đã được sử dụng vì mã được triển khai trên các bảng cấp thấp chạy nhiều bộ xử lý cho các hệ thống tự trị. Tất cả họ đều hỗ trợ biên dịch từ C sang nhị phân riêng tương ứng của họ. Cách tiếp cận làm cho mã rất dễ đọc khi bạn nhận ra những gì họ đang cố gắng làm.
Kieveli

18

Máy tính để bàn Gnome cho Linux được viết bằng C hướng đối tượng và nó có mô hình đối tượng gọi là " GObject " hỗ trợ các thuộc tính, kế thừa, đa hình, cũng như một số tính năng khác như tham chiếu, xử lý sự kiện (gọi là "tín hiệu"), thời gian chạy đánh máy, dữ liệu riêng tư, v.v.

Nó bao gồm các bản hack tiền xử lý để thực hiện những việc như đánh máy xung quanh trong hệ thống phân cấp lớp, v.v ... Đây là một lớp ví dụ tôi đã viết cho Gnome (những thứ như gchar là typedefs):

Nguồn lớp

Lớp trưởng

Bên trong cấu trúc GObject có một số nguyên GType được sử dụng làm số ma thuật cho hệ thống gõ động của GLib (bạn có thể chuyển toàn bộ cấu trúc thành "GType" để tìm loại này).


Thật không may, tệp đọc tôi / hướng dẫn (liên kết wiki) không hoạt động và chỉ có hướng dẫn tham khảo cho điều đó (tôi đang nói về GObject chứ không phải GTK). vui lòng cung cấp một số tệp hướng dẫn cho cùng ...
FL4SOF

Liên kết đã được cố định.
James Cape

4
Liên kết bị phá vỡ một lần nữa.
SeanRamey

6

Tôi đã từng làm điều này trong C, trước khi tôi biết OOP là gì.

Dưới đây là một ví dụ, trong đó thực hiện bộ đệm dữ liệu phát triển theo yêu cầu, với kích thước tối thiểu, mức tăng và kích thước tối đa. Việc triển khai cụ thể này dựa trên "phần tử", nghĩa là nó được thiết kế để cho phép một bộ sưu tập giống như danh sách của bất kỳ loại C nào, không chỉ là bộ đệm byte có độ dài thay đổi.

Ý tưởng là đối tượng được khởi tạo bằng cách sử dụng xxx_crt () và bị xóa bằng xxx_dlt (). Mỗi phương thức "thành viên" cần một con trỏ được gõ cụ thể để hoạt động.

Tôi đã thực hiện một danh sách liên kết, bộ đệm tuần hoàn và một số thứ khác theo cách này.

Tôi phải thú nhận, tôi chưa bao giờ đưa ra bất kỳ suy nghĩ nào về cách thực hiện kế thừa với phương pháp này. Tôi tưởng tượng rằng một số pha trộn của Kieveli được cung cấp có thể là một con đường tốt.

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 chỉ đơn giản là một typedef của int - Tôi đã sử dụng nó để nhắc nhở tôi rằng độ dài của nó là biến đổi từ nền tảng sang nền tảng (để chuyển).


7
thánh moly, điều này có thể giành chiến thắng trong một cuộc thi C khó hiểu! tôi thích nó! :)
Horseyguy

@horseyguy Không, không thể. Nó đã được xuất bản. Ngoài ra, họ xem xét việc bao gồm lạm dụng các tệp tiêu đề đối với công cụ iocccsize. Đây cũng không phải là một chương trình hoàn chỉnh. Năm 2009 không có cuộc thi nên không thể so sánh iocccsize. CPP đã bị lạm dụng nhiều lần vì vậy nó khá cũ. V.v ... Xin lỗi. Tôi không cố gắng tiêu cực Tôi thực tế. Tôi sắp xếp ý nghĩa của bạn và đó là một bài đọc tốt và tôi đã bình chọn nó. (Và vâng, tôi tham gia vào nó và vâng tôi cũng thắng.)
Pryftan

6

Hơi lạc đề, nhưng trình biên dịch C ++ gốc, Cfront , đã biên dịch C ++ thành C và sau đó là trình biên dịch .

Bảo quản ở đây .


Tôi thực sự đã nhìn thấy nó trước đây. Tôi tin rằng đó là một phần tốt đẹp của công việc.

@Anthony Cuozzo: Stan Lippman đã viết một cuốn sách tuyệt vời có tên 'C ++ - Bên trong mô hình đối tượng' nơi ông liên quan rất nhiều kinh nghiệm và quyết định thiết kế của mình bằng văn bản và duy trì mặt trước. Nó vẫn là một cuốn sách hay và giúp tôi rất nhiều khi chuyển từ C sang C ++ nhiều năm trước
zebrabox

5

Nếu bạn nghĩ về các phương thức được gọi trên các đối tượng là các phương thức tĩnh truyền một hàm ' this' vào hàm, nó có thể làm cho việc suy nghĩ OO trong C dễ dàng hơn.

Ví dụ:

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

trở thành:

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

Hoặc điều tương tự.


6
@Artelius: Chắc chắn, nhưng đôi khi điều hiển nhiên là không, cho đến khi nó được nêu. +1 cho điều này.
Lawrence Dol

1
tốt hơn nữa sẽ làstring->length(s);
OozeMeister

4

ffmpeg (bộ công cụ xử lý video) được viết bằng chữ C thẳng (và ngôn ngữ lắp ráp), nhưng sử dụng kiểu hướng đối tượng. Nó có đầy đủ các cấu trúc với con trỏ hàm. Có một tập hợp các hàm xuất xưởng khởi tạo các cấu trúc với các con trỏ "phương thức" thích hợp.


Tôi không thấy bất kỳ chức năng nào của nhà máy trong đó (ffmpeg), thay vào đó dường như nó không sử dụng đa hình / kế thừa (cách tầm thường được đề xuất ở trên).
FL4SOF

avcodec_open là một chức năng của nhà máy. Nó nhồi con trỏ hàm vào cấu trúc AVCodecContext (như draw_horiz_band). Nếu bạn xem việc sử dụng macro FF_COMMON_FRAME trong avcodec.h, bạn sẽ thấy một cái gì đó giống với sự kế thừa của các thành viên dữ liệu. IMHO, ffmpeg chứng minh với tôi rằng OOP được thực hiện tốt nhất trong C ++ chứ không phải C.
Mr Fooz

3

Nếu bạn thực sự nghĩ catefully, ngay cả tiêu chuẩn sử dụng thư viện C OOP - xem xét FILE *như một ví dụ: fopen()khởi tạo một FILE *đối tượng, và bạn sử dụng nó sử dụng phương pháp thành viên fscanf(), fprintf(), fread(), fwrite()và những người khác, và cuối cùng là hoàn thiện nó với fclose().

Bạn cũng có thể đi theo cách giả-Objective-C cũng không khó:

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;
}
// ...

Để sử dụng:

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);
}

Đây là những gì có thể xảy ra từ một số mã Objective-C như thế này, nếu một trình dịch Objective-C-to-C khá cũ được sử dụng:

@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];
}

Làm gì __attribute__((constructor))trong void __meta_Foo_init(void) __attribute__((constructor))?
AE đã vẽ

1
Đây là một phần mở rộng GCC sẽ đảm bảo rằng hàm được đánh dấu sẽ được gọi khi nhị phân được tải vào bộ nhớ. @AEDrew
Maxthon Chan

popen(3)cũng trả về một FILE *ví dụ khác.
Pryftan

3

Tôi nghĩ những gì Adam Rosenfield đã đăng là cách thực hiện OOP chính xác trong C. Tôi muốn nói thêm rằng những gì anh ta thể hiện là việc thực hiện đối tượng. Nói cách khác, việc triển khai thực tế sẽ được đưa vào .ctệp, trong khi giao diện sẽ được đặt trong .htệp tiêu đề . Ví dụ: sử dụng ví dụ khỉ ở trên:

Giao diện sẽ trông như sau:

//monkey.h

    struct _monkey;

    typedef struct _monkey monkey;

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

Bạn có thể thấy trong .htệp giao diện bạn chỉ xác định các nguyên mẫu. Sau đó, bạn có thể biên dịch " .ctệp " phần thực hiện thành một thư viện tĩnh hoặc động. Điều này tạo ra đóng gói và bạn cũng có thể thay đổi việc thực hiện theo ý muốn. Người dùng đối tượng của bạn cần biết hầu như không có gì về việc thực hiện nó. Điều này cũng tập trung vào thiết kế tổng thể của đối tượng.

Đó là niềm tin cá nhân của tôi rằng oop là một cách khái niệm cấu trúc mã và khả năng sử dụng lại của bạn và thực sự không liên quan gì đến những thứ khác được thêm vào c ++ như quá tải hoặc mẫu. Vâng, đó là những tính năng hữu ích rất tốt nhưng chúng không đại diện cho việc lập trình hướng đối tượng thực sự là gì.


Bạn có thể khai báo một cấu trúc với typedef struct Monkey {} Monkey; điểm quan trọng của việc gõ nó sau khi nó được tạo ra?
MarcusJ

1
@MarcusJ Đơn struct _monkeygiản chỉ là một nguyên mẫu. Định nghĩa loại thực tế được định nghĩa trong tệp thực hiện (tệp .c). Điều này tạo ra hiệu ứng đóng gói và cho phép nhà phát triển API xác định lại cấu trúc khỉ trong tương lai mà không cần sửa đổi API. Người dùng API chỉ cần quan tâm đến các phương thức thực tế. Trình thiết kế API đảm nhiệm việc triển khai bao gồm cách trình bày đối tượng / struct. Vì vậy, các chi tiết của đối tượng / struct được ẩn khỏi người dùng (một loại mờ).

Tôi xác định cấu trúc của mình trong các tiêu đề, đây có phải là tiêu chuẩn không? Chà, tôi làm theo cách đó vì thỉnh thoảng tôi cần truy cập vào các thành viên của struct bên ngoài thư viện đó.
MarcusJ

1
@MarcusJ Bạn có thể xác định bạn cấu trúc trong các tiêu đề nếu bạn muốn (không có tiêu chuẩn). Nhưng nếu bạn muốn thay đổi cấu trúc bên trong, bạn có thể phá mã của mình. Encapsulation chỉ đơn thuần là một phong cách mã hóa giúp dễ dàng thay đổi việc triển khai mà không phá vỡ mã của bạn. Bạn luôn có thể truy cập các thành viên của mình thông qua các phương thức truy cập như int getCount(ObjectType obj)vv nếu bạn chọn xác định cấu trúc trong tệp thực hiện.

2

Đề nghị của tôi: giữ cho nó đơn giản. Một trong những vấn đề lớn nhất tôi gặp phải là duy trì phần mềm cũ hơn (đôi khi trên 10 tuổi). Nếu mã không đơn giản, nó có thể khó khăn. Vâng, người ta có thể viết OOP rất hữu ích với tính đa hình trong C, nhưng nó có thể khó đọc.

Tôi thích các đối tượng đơn giản đóng gói một số chức năng được xác định rõ. Một ví dụ tuyệt vời về điều này là GLIB2 , ví dụ như bảng băm:

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);

Các phím là:

  1. Kiến trúc đơn giản và mẫu thiết kế
  2. Đạt được đóng gói OOP cơ bản.
  3. Dễ dàng thực hiện, đọc, hiểu và duy trì

1

Nếu tôi định viết OOP bằng CI thì có lẽ sẽ đi với thiết kế giả Pimpl . Thay vì chuyển con trỏ đến các cấu trúc, cuối cùng bạn chuyển các con trỏ tới các con trỏ tới các cấu trúc. Điều này làm cho nội dung mờ đục và tạo điều kiện cho đa hình và kế thừa.

Vấn đề thực sự với OOP trong C là những gì xảy ra khi biến phạm vi thoát. Không có hàm hủy do trình biên dịch tạo và có thể gây ra sự cố. Macro có thể có thể giúp đỡ, nhưng nó sẽ luôn xấu xí khi nhìn vào.


1
Khi lập trình bằng C, tôi xử lý phạm vi bằng cách sử dụng các ifcâu lệnh và phát hành chúng ở cuối. Ví dụif ( (obj = new_myObject()) ) { /* code using myObject */ free_myObject(obj); }

1

Một cách khác để lập trình theo kiểu hướng đối tượng với C là sử dụng trình tạo mã để chuyển đổi ngôn ngữ cụ thể của miền thành C. Như đã thực hiện với TypeScript và JavaScript để đưa OOP lên 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);
}

Đầu ra:

6.56
13.12

Dưới đây là một chương trình về lập trình OO với C.

Đây là C thực, thuần túy, không có macro tiền xử lý. Chúng tôi có sự kế thừa, đa hình và đóng gói dữ liệu (bao gồm dữ liệu riêng tư cho các lớp hoặc đối tượng). Không có cơ hội cho vòng loại được bảo vệ tương đương, nghĩa là, dữ liệu riêng tư cũng là chuỗi riêng tư. Nhưng đây không phải là sự bất tiện vì tôi không nghĩ nó cần thiết.

CPolygon không được khởi tạo bởi vì chúng tôi chỉ sử dụng nó để thao túng các đối tượng của chuỗi không liên kết có các khía cạnh chung nhưng thực hiện chúng khác nhau (Đa hình).


0

@Adam Rosenfield có một lời giải thích rất hay về cách đạt được OOP với C

Bên cạnh đó, tôi khuyên bạn nên đọc

1) pjsip

Một thư viện C rất tốt cho VoIP. Bạn có thể tìm hiểu làm thế nào nó đạt được OOP mặc dù cấu trúc và bảng con trỏ hàm

2) Thời gian chạy iOS

Tìm hiểu cách iOS Runtime hỗ trợ Objective C. Nó đạt được OOP thông qua con trỏ isa, lớp meta


0

Đối với tôi hướng đối tượng trong C nên có các tính năng sau:

  1. Đóng gói và ẩn dữ liệu (có thể đạt được bằng cách sử dụng structs / con trỏ mờ)

  2. Kế thừa và hỗ trợ cho đa hình (có thể đạt được sự kế thừa đơn bằng cách sử dụng các cấu trúc - đảm bảo cơ sở trừu tượng không thể thực hiện được)

  3. Hàm xây dựng và hàm hủy (không dễ đạt được)

  4. Kiểm tra loại (ít nhất là đối với các loại do người dùng xác định vì C không thực thi bất kỳ)

  5. Đếm tham chiếu (hoặc một cái gì đó để thực hiện RAII )

  6. Hỗ trợ hạn chế cho xử lý ngoại lệ (setjmp và longjmp)

Trên hết, nó nên dựa vào thông số kỹ thuật ANSI / ISO và không nên dựa vào chức năng dành riêng cho trình biên dịch.


Đối với số (5) - Bạn không thể triển khai RAII bằng ngôn ngữ mà không có hàm hủy (có nghĩa là RAII không phải là kỹ thuật hỗ trợ trình biên dịch trong C hoặc Java).
Tom

hàm tạo và hàm hủy có thể được viết cho đối tượng dựa trên c - tôi đoán GObject thực hiện nó. và tất nhiên RAAI (nó không thẳng thắn, có thể xấu và không cần phải thực dụng chút nào) - tất cả những gì tôi đang tìm kiếm là xác định ngữ nghĩa dựa trên C để đạt được những điều trên.
FL4SOF

C không hỗ trợ các hàm hủy. Bạn phải gõ một cái gì đó để làm cho chúng hoạt động. Điều đó có nghĩa là họ không tự dọn dẹp. GObject không thay đổi ngôn ngữ.
Tom

0

Hãy xem http://ldeniau.web.cern.ch/ldeniau/html/oopc/oopc.html . Nếu không có gì khác đọc qua tài liệu là một kinh nghiệm khai sáng.


3
Vui lòng cung cấp ngữ cảnh cho liên kết bạn đang chia sẻ. Mặc dù liên kết bạn chia sẻ thực sự có thể rất hữu ích, nhưng tốt hơn hết là bạn nên nắm bắt các khía cạnh chính của bài viết được chia sẻ để trả lời câu hỏi. Bằng cách này, ngay cả khi liên kết bị xóa, câu trả lời của bạn vẫn sẽ có liên quan và hữu ích.
ishmaelMakitla

0

Tôi đến bữa tiệc muộn một chút nhưng tôi muốn tránh cả hai thái cực vĩ ​​mô - quá nhiều hoặc quá nhiều mã làm xáo trộn, nhưng một vài macro rõ ràng có thể làm cho mã OOP dễ dàng phát triển và đọc hơn:

/*
 * 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
}

Tôi nghĩ rằng điều này có sự cân bằng tốt và các lỗi mà nó tạo ra (ít nhất là với các tùy chọn gcc 6.3 mặc định) đối với một số lỗi có khả năng hơn là hữu ích thay vì gây nhầm lẫn. Toàn bộ vấn đề là nâng cao năng suất lập trình viên?



0

Tôi cũng đang làm việc trên cơ sở này dựa trên một giải pháp vĩ mô. Vì vậy, nó chỉ dành cho những người dũng cảm nhất ,-) Nhưng nó đã khá tốt rồi, và tôi đã làm việc với một vài dự án trên nó. Nó hoạt động để trước tiên bạn xác định một tệp tiêu đề riêng cho mỗi lớp. Như thế này:

#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))               \

Để thực hiện lớp, bạn tạo một tệp tiêu đề cho nó và một tệp C nơi bạn thực hiện các phương thức:

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

Trong tiêu đề bạn đã tạo cho lớp, bạn bao gồm các tiêu đề khác bạn cần và xác định các loại, v.v. liên quan đến lớp. Trong cả tiêu đề lớp và trong tệp C, bạn bao gồm tệp đặc tả lớp (xem ví dụ mã đầu tiên) và macro X. Các macro X ( 1 , 2 , 3 , v.v.) sẽ mở rộng mã thành các cấu trúc lớp thực tế và các khai báo khác.

Để kế thừa một lớp #define SUPER supernamevà thêmsupername__define \ làm dòng đầu tiên trong định nghĩa lớp. Cả hai phải ở đó. Ngoài ra còn có hỗ trợ JSON, tín hiệu, các lớp trừu tượng, v.v.

Để tạo một đối tượng, chỉ cần sử dụng W_NEW(classname, .x=1, .y=2,...). Việc khởi tạo dựa trên khởi tạo cấu trúc được giới thiệu trong C11. Nó hoạt động độc đáo và mọi thứ không được liệt kê được đặt thành không.

Để gọi một phương thức, sử dụng W_CALL(o,method)(1,2,3). Nó trông giống như một lệnh gọi hàm cao hơn nhưng nó chỉ là một macro. Nó mở rộng đến((o)->klass->method(o,1,2,3)) đó là một hack thực sự tốt đẹp.

Xem Tài liệumã chính nó .

Vì khung cần một số mã soạn sẵn, tôi đã viết một tập lệnh Perl (wobject) thực hiện công việc. Nếu bạn sử dụng nó, bạn chỉ có thể viết

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

và nó sẽ tạo tệp đặc tả lớp, tiêu đề lớp và tệp C, bao gồm Point_impl.cnơi bạn triển khai lớp. Nó tiết kiệm khá nhiều công việc, nếu bạn có nhiều lớp đơn giản nhưng mọi thứ vẫn ở C. wobject là một trình quét dựa trên biểu thức chính quy rất đơn giản, dễ thích ứng với các nhu cầu cụ thể hoặc được viết lại từ đầu.



0

Bạn có thể dùng thử COOP , khung thân thiện với lập trình viên cho OOP trong C, có các Lớp, Ngoại lệ, Đa hình và Quản lý bộ nhớ (quan trọng đối với mã nhúng). Đây là một cú pháp tương đối nhẹ, xem hướng dẫn trong Wiki ở đó.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.