Orientasi objek dalam C


157

Apa yang akan menjadi set hacks preprocessor yang bagus (kompatibel ANSI C89 / ISO C90) yang memungkinkan semacam orientasi objek yang jelek (tetapi dapat digunakan) di C?

Saya akrab dengan beberapa bahasa berorientasi objek yang berbeda, jadi jangan merespons dengan jawaban seperti "Pelajari C ++!". Saya telah membaca " Pemrograman Berorientasi Objek Dengan ANSI C " (berhati-hatilah: format PDF ) dan beberapa solusi menarik lainnya, tetapi saya lebih tertarik dengan Anda :-)!


Lihat juga Bisakah Anda menulis kode berorientasi objek dalam C?


1
Dapatkah saya menanggapi belajar D dan menggunakan abi yang kompatibel untuk di mana Anda benar-benar membutuhkan C. digitalmars.com/d
Tim Matthews

2
@Dinah: Terima kasih untuk "Lihat juga". Posting itu menarik.

1
Pertanyaan yang menarik tampaknya mengapa Anda ingin hack pra-prosesor OOP pada C.
#

3
@ Calyth: Saya menemukan bahwa OOP berguna dan "Saya bekerja dengan beberapa sistem embedded yang hanya benar-benar memiliki kompiler C tersedia" (dari atas). Selain itu, tidakkah Anda menemukan hack preprocessor yang bagus menarik untuk dilihat?

Jawaban:


31

C Object System (COS) terdengar menjanjikan (masih dalam versi alpha). Ini mencoba untuk meminimalkan konsep yang tersedia demi kesederhanaan dan fleksibilitas: pemrograman berorientasi objek yang seragam termasuk kelas terbuka, metaclasses, metaclasses properti, generik, multimethod, delegasi, kepemilikan, pengecualian, kontrak dan penutupan. Ada draft paper (PDF) yang menjelaskannya.

Pengecualian dalam C adalah implementasi C89 dari TRY-CATCH-FINALLY yang ditemukan dalam bahasa OO lainnya. Itu datang dengan testuite dan beberapa contoh.

Baik oleh Laurent Deniau, yang bekerja banyak pada OOP di C .


@vonbrand COS bermigrasi ke github di mana komit terakhir adalah musim panas lalu. Kedewasaan dapat menjelaskan kurangnya komitmen.
filant

185

Saya akan menyarankan terhadap preprocessor (ab) gunakan untuk mencoba dan membuat sintaks C lebih seperti itu dari bahasa yang lebih berorientasi objek. Pada level paling dasar, Anda hanya menggunakan struct sederhana sebagai objek dan membagikannya dengan pointer:

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

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

Untuk mendapatkan hal-hal seperti warisan dan polimorfisme, Anda harus bekerja lebih keras. Anda dapat melakukan pewarisan manual dengan membuat anggota pertama struktur menjadi instance dari superclass, dan kemudian Anda dapat melemparkan pointer ke kelas basis dan turunan secara bebas:

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

Untuk mendapatkan polimorfisme (yaitu fungsi virtual), Anda menggunakan pointer fungsi, dan tabel penunjuk fungsi opsional, juga dikenal sebagai tabel virtual atau tabel:

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

Dan itulah bagaimana Anda melakukan polimorfisme dalam C. Ini tidak cantik, tetapi itu berhasil. Ada beberapa masalah lengket yang melibatkan lemparan pointer antara kelas basis dan turunan, yang aman selama kelas basis adalah anggota pertama dari kelas turunan. Multiple inheritance jauh lebih sulit - dalam hal ini, untuk membedakan antara kelas dasar selain yang pertama, Anda perlu menyesuaikan pointer secara manual berdasarkan offset yang tepat, yang benar-benar rumit dan rawan kesalahan.

Hal lain (rumit) yang dapat Anda lakukan adalah mengubah tipe dinamis suatu objek saat runtime! Anda baru saja menetapkan ulang pointer vtable baru. Anda bahkan dapat secara selektif mengubah beberapa fungsi virtual sambil menjaga yang lain, menciptakan tipe hybrid baru. Berhati-hatilah untuk membuat vtable baru alih-alih memodifikasi global vtable, jika tidak, Anda akan secara tidak sengaja memengaruhi semua objek dari jenis tertentu.


6
Adam, bersenang-senang mengubah vtable global tipe adalah untuk mensimulasikan bebek-mengetik di C. :)
jmucchiello

Sekarang saya kasihan C ++ ... Yah tentu saja sintaks C ++ lebih jelas, tapi karena itu bukan sintaks sepele, saya dikurangi. Saya ingin tahu apakah sesuatu hibrida antara C ++ dan C dapat dicapai, jadi void * masih akan menjadi tipe castable yang valid. Bagian dengan struct derived {struct base super;};jelas untuk menebak cara kerjanya, karena urutan byte itu benar.
jokoon

2
+1 untuk kode elegan, ditulis dengan baik. Ini persis apa yang saya cari!
Homunculus Reticulli

3
Sudah selesai dilakukan dengan baik. Ini persis seperti yang saya lakukan dan itu juga cara yang benar. Alih-alih membutuhkan pointer ke struct / objek dalam pikiran Anda hanya harus meneruskan pointer ke integer (alamat). Ini akan memungkinkan Anda untuk meneruskan segala jenis objek untuk panggilan metode polimorfik tanpa batas. Juga, satu-satunya hal yang hilang adalah fungsi untuk menginisialisasi struct Anda (objek / kelas). Ini akan mencakup fungsi malloc dan mengembalikan pointer. Mungkin saya akan menambahkan bagian bagaimana melakukan passing pesan (objektif-c) dalam C.

1
Ini adalah kesalahan saya dari C ++, dan menggunakan C lebih (sebelum saya hanya menggunakan C ++ untuk warisan) Terima kasih
Anne Quinn

31

Saya pernah bekerja dengan perpustakaan C yang diimplementasikan dengan cara yang menurut saya cukup elegan. Mereka telah menulis, dalam C, cara untuk mendefinisikan objek, kemudian mewarisi dari mereka sehingga mereka dapat diperluas sebagai objek C ++. Ide dasarnya adalah ini:

  • Setiap objek memiliki file sendiri
  • Fungsi dan variabel publik didefinisikan dalam file .h untuk suatu objek
  • Variabel dan fungsi pribadi hanya terletak di file .c
  • Untuk "mewarisi" sebuah struct baru dibuat dengan anggota pertama dari struct yang menjadi objek untuk diwarisi

Warisan sulit untuk dijelaskan, tetapi pada dasarnya begini:

struct vehicle {
   int power;
   int weight;
}

Kemudian di file lain:

struct van {
   struct vehicle base;
   int cubic_size;
}

Maka Anda bisa membuat van dibuat dalam memori, dan digunakan oleh kode yang hanya tahu tentang kendaraan:

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

Ini bekerja dengan indah, dan file .h mendefinisikan dengan tepat apa yang harus Anda lakukan dengan setiap objek.


Saya sangat suka solusi ini, kecuali bahwa semua "objek" internal adalah publik.
Lawrence Dol

6
@Software Monkey: C tidak memiliki kontrol akses. Satu-satunya cara untuk menyembunyikan detail implementasi adalah berinteraksi melalui pointer buram, yang bisa sangat menyakitkan, karena semua bidang perlu diakses melalui metode accessor yang mungkin tidak dapat digariskan.
Adam Rosenfield

1
@ Adam: Kompiler yang mendukung optimasi waktu tautan akan menyatukan mereka ...
Christoph

9
Jika Anda melakukan ini, Anda juga harus memastikan bahwa semua fungsi dalam file .c yang tidak didefinisikan sebagai publik didefinisikan sebagai statis sehingga mereka tidak berakhir sebagai fungsi yang disebut dalam file objek Anda. Itu memastikan tidak ada yang bisa menemukan nama mereka di fase tautan.
jmucchiello

2
@ Marscel: C digunakan karena kode itu digunakan pada papan tingkat rendah menjalankan berbagai prosesor untuk sistem otonom. Mereka semua mendukung kompilasi dari C ke binari asli masing-masing. Pendekatan tersebut membuat kode itu sangat mudah dibaca setelah Anda menyadari apa yang mereka coba lakukan.
Kieveli

18

Desktop GNOME untuk Linux ditulis dalam berorientasi objek C, dan memiliki model objek yang disebut " GObject " yang mendukung properti, warisan, polimorfisme, serta beberapa barang lain seperti referensi, penanganan acara (disebut "sinyal"), runtime mengetik, data pribadi, dll.

Ini termasuk preprocessor hacks untuk melakukan hal-hal seperti mengetik di hierarki kelas, dll. Berikut adalah contoh kelas yang saya tulis untuk GNOME (hal-hal seperti gchar adalah typedefs):

Sumber Kelas

Header kelas

Di dalam struktur GObject ada bilangan bulat GType yang digunakan sebagai angka ajaib untuk sistem pengetikan dinamis GLib (Anda dapat melemparkan seluruh struct ke "GType" untuk menemukan jenisnya).


Sayangnya, file read me / tutorial (tautan wiki) tidak berfungsi dan hanya ada referensi manual untuk itu (saya berbicara tentang GObject dan bukan GTK). tolong berikan beberapa file tutorial untuk hal yang sama ...
FL4SOF

Tautan telah diperbaiki.
James Cape

4
Tautan rusak lagi.
SeanRamey

6

Saya dulu melakukan hal semacam ini di C, sebelum saya tahu apa itu OOP.

Berikut ini adalah contoh, yang mengimplementasikan buffer data yang tumbuh sesuai permintaan, mengingat ukuran minimum, peningkatan dan ukuran maksimum. Implementasi khusus ini adalah "elemen" berbasis, yang mengatakan itu dirancang untuk memungkinkan koleksi seperti daftar dari semua jenis C, bukan hanya byte-buffer panjang variabel.

Idenya adalah bahwa objek instantiated menggunakan xxx_crt () dan dihapus menggunakan xxx_dlt (). Setiap metode "anggota" membutuhkan pointer yang diketik khusus untuk beroperasi.

Saya menerapkan daftar tertaut, buffer siklik, dan sejumlah hal lain dengan cara ini.

Saya harus akui, saya tidak pernah memikirkan bagaimana menerapkan warisan dengan pendekatan ini. Saya membayangkan bahwa beberapa perpaduan yang ditawarkan oleh Kieveli mungkin merupakan jalan yang baik.

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 hanyalah tip int - saya menggunakannya untuk mengingatkan saya bahwa panjangnya adalah variabel dari platform ke platform (untuk porting).


7
Holy Moly, ini bisa memenangkan kontes C yang membingungkan! saya suka itu! :)
horseyguy

@horseyguy Tidak, tidak bisa. Sudah diterbitkan. Mereka juga mempertimbangkan penyertaan penyalahgunaan file header terhadap alat iocccsize. Ini juga bukan program yang lengkap. 2009 tidak punya kontes jadi tidak bisa membandingkan iocccsize. CPP telah disalahgunakan berkali-kali juga sehingga sudah cukup tua. Dll. Maaf. Saya tidak mencoba menjadi negatif, namun saya realistis. Saya agak mengerti maksud Anda dan ini adalah bacaan yang bagus dan saya telah memilihnya. (Dan ya saya berpartisipasi di dalamnya dan ya saya menang juga.)
Pryftan

6

Sedikit di luar topik, tetapi kompiler C ++ asli, Cfront , mengkompilasi C ++ ke C dan kemudian ke assembler.

Diawetkan di sini .


Saya sebenarnya sudah melihatnya sebelumnya. Saya percaya itu adalah pekerjaan yang bagus.

@Anthony Cuozzo: Stan Lippman menulis sebuah buku bagus berjudul 'C ++ - Inside the object model' di mana ia menceritakan banyak pengalaman dan keputusan desainnya dalam menulis dan memelihara c-front. Ini masih merupakan bacaan yang baik dan sangat membantu saya ketika beralih dari C ke C ++ bertahun-tahun yang lalu
zebrabox

5

Jika Anda berpikir tentang metode yang memanggil objek sebagai metode statis yang melewatkan implisit thiske fungsi itu dapat membuat berpikir OO dalam C lebih mudah.

Sebagai contoh:

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

menjadi:

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

Atau semacam itu.


6
@ Artelius: Tentu, tapi terkadang yang jelas tidak, sampai dinyatakan. +1 untuk ini.
Lawrence Dol

1
lebih baik lagistring->length(s);
OozeMeister

4

ffmpeg (toolkit untuk pemrosesan video) ditulis dalam C lurus (dan bahasa rakitan), tetapi menggunakan gaya berorientasi objek. Ini penuh dengan struct dengan pointer fungsi. Ada satu set fungsi pabrik yang menginisialisasi struct dengan petunjuk "metode" yang sesuai.


saya tidak melihat fungsi pabrik di dalamnya (ffmpeg), tetapi sepertinya tidak menggunakan polimorfisme / pewarisan (cara sepele yang disarankan di atas).
FL4SOF

avcodec_open adalah satu fungsi pabrik. Ini memasukkan pointer fungsi ke dalam struktur AVCodecContext (seperti draw_horiz_band). Jika Anda melihat FF_COMMON_FRAME penggunaan makro di avcodec.h, Anda akan melihat sesuatu yang mirip dengan warisan anggota data. IMHO, ffmpeg membuktikan kepada saya bahwa OOP paling baik dilakukan dalam C ++, bukan C.
276 Mr

3

Jika Anda benar-benar berpikir secara hati-hati, bahkan standar penggunaan C perpustakaan OOP - menganggap FILE *sebagai contoh: fopen()menginisialisasi sebuah FILE *objek, dan Anda menggunakannya menggunakan metode anggota fscanf(), fprintf(), fread(), fwrite()dan lain-lain, dan akhirnya menyelesaikan dengan fclose().

Anda juga bisa menggunakan cara pseudo-Objective-C yang tidak sulit juga:

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

Menggunakan:

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

Inilah yang mungkin dihasilkan dari beberapa kode Objective-C seperti ini, jika penerjemah Objective-C-to-C yang cukup lama digunakan:

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

Apa yang __attribute__((constructor))harus dilakukan void __meta_Foo_init(void) __attribute__((constructor))?
AE Drew

1
Ini adalah ekstensi GCC yang akan memastikan bahwa fungsi yang ditandai dipanggil ketika biner dimasukkan ke dalam memori. @AEDrew
Maxthon Chan

popen(3)juga mengembalikan FILE *untuk contoh lain.
Pryftan

3

Saya pikir apa yang diposting Adam Rosenfield adalah cara yang benar untuk melakukan OOP di C. Saya ingin menambahkan bahwa apa yang dia tunjukkan adalah implementasi objek. Dengan kata lain implementasi yang sebenarnya akan dimasukkan ke dalam .cfile, sedangkan antarmuka akan dimasukkan ke dalam .hfile header . Misalnya, menggunakan contoh monyet di atas:

Antarmuka akan terlihat seperti:

//monkey.h

    struct _monkey;

    typedef struct _monkey monkey;

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

Anda dapat melihat di .hfile antarmuka Anda hanya mendefinisikan prototipe. Anda kemudian dapat mengkompilasi bagian " .cfile" implementasi ke perpustakaan statis atau dinamis. Ini menciptakan enkapsulasi dan Anda juga dapat mengubah implementasinya sesuka hati. Pengguna objek Anda perlu tahu hampir tidak ada tentang penerapannya. Ini juga menempatkan fokus pada desain keseluruhan objek.

Ini keyakinan pribadi saya bahwa oop adalah cara untuk mengkonseptualisasikan struktur kode Anda dan penggunaan kembali dan benar-benar tidak ada hubungannya dengan hal-hal lain yang ditambahkan ke c ++ seperti overloading atau template. Ya itu adalah fitur berguna yang sangat bagus tetapi mereka tidak mewakili pemrograman berorientasi objek apa sebenarnya.


Anda dapat mendeklarasikan struct dengan typedef struct Monkey {} Monkey; Apa gunanya mengetik setelah itu dibuat?
MarcusJ

1
@ MarscJ Ini struct _monkeyhanyalah sebuah prototipe. Definisi tipe aktual didefinisikan dalam file implementasi (file .c). Ini menciptakan efek enkapsulasi dan memungkinkan pengembang API untuk mendefinisikan kembali struktur monyet di masa depan tanpa memodifikasi API. Pengguna API hanya perlu peduli dengan metode yang sebenarnya. Perancang API menangani implementasi termasuk bagaimana objek / struct diletakkan. Jadi detail objek / struct disembunyikan dari pengguna (tipe buram).

Saya mendefinisikan struct saya di header, bukankah ini standar? Yah, saya melakukannya dengan cara itu karena saya kadang-kadang perlu mengakses anggota struct di luar perpustakaan itu.
MarcusJ

1
@MarcusJ Anda dapat menentukan Anda struct di header jika Anda mau (tidak ada standar). Tetapi jika Anda ingin mengubah struktur internalnya, Anda dapat memecahkan kode Anda. Enkapsulasi hanyalah gaya pengkodean yang membuatnya lebih mudah untuk mengubah implementasi tanpa melanggar kode Anda. Anda selalu dapat mengakses anggota Anda melalui metode accessor seperti int getCount(ObjectType obj)dll jika Anda memilih untuk mendefinisikan struct dalam file implementasi.

2

Rekomendasi saya: tetap sederhana. Salah satu masalah terbesar yang saya miliki adalah memelihara perangkat lunak yang lebih lama (terkadang lebih dari 10 tahun). Jika kodenya tidak sederhana, itu bisa sulit. Ya, orang dapat menulis OOP yang sangat berguna dengan polimorfisme dalam C, tetapi bisa sulit dibaca.

Saya lebih suka objek sederhana yang merangkum beberapa fungsi yang terdefinisi dengan baik. Contoh yang bagus untuk hal ini adalah GLIB2 , misalnya tabel hash:

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

Kuncinya adalah:

  1. Arsitektur sederhana dan pola desain
  2. Mencapai enkapsulasi OOP dasar.
  3. Mudah diimplementasikan, dibaca, dimengerti, dan dipelihara

1

Jika saya akan menulis OOP di CI mungkin akan pergi dengan desain pseudo- Pimpl . Alih-alih meneruskan pointer ke struct, Anda akhirnya melewati pointer ke pointer ke struct. Ini membuat konten menjadi buram dan memfasilitasi polimorfisme dan pewarisan.

Masalah sebenarnya dengan OOP di C adalah apa yang terjadi ketika variabel keluar dari ruang lingkup. Tidak ada destruktor yang dihasilkan kompiler dan yang dapat menyebabkan masalah. Makro mungkin bisa membantu, tetapi akan selalu jelek untuk dilihat.


1
Ketika memprogram dalam C, saya berurusan dengan ruang lingkup dengan menggunakan ifpernyataan dan melepaskannya di akhir. Sebagai contohif ( (obj = new_myObject()) ) { /* code using myObject */ free_myObject(obj); }

1

Cara lain untuk memprogram dalam gaya berorientasi objek dengan C adalah dengan menggunakan generator kode yang mengubah bahasa domain spesifik menjadi C. Seperti yang dilakukan dengan TypeScript dan JavaScript untuk membawa OOP ke 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);
}

Keluaran:

6.56
13.12

Berikut ini adalah pertunjukan tentang apa itu pemrograman OO dengan C.

Ini nyata, C murni, tidak ada makro preprosesor. Kami memiliki pewarisan, polimorfisme, dan enkapsulasi data (termasuk data pribadi ke kelas atau objek). Tidak ada peluang untuk setara kualifikasi yang dilindungi, yaitu, data pribadi juga bersifat pribadi di rantai pewarisan juga. Tapi ini bukan ketidaknyamanan karena saya pikir itu tidak perlu.

CPolygon tidak dipakai karena kami hanya menggunakannya untuk memanipulasi objek dari rantai bawaan yang memiliki aspek umum tetapi implementasi yang berbeda dari mereka (Polimorfisme).


0

@Adam Rosenfield memiliki penjelasan yang sangat bagus tentang bagaimana mencapai OOP dengan C

Selain itu, saya akan merekomendasikan Anda untuk membaca

1) pjsip

Pustaka C yang sangat bagus untuk VoIP. Anda dapat mempelajari cara mencapai OOP melalui struct dan tabel pointer fungsi

2) iOS Runtime

Pelajari bagaimana iOS Runtime menggerakkan Objective C. Ini mencapai OOP melalui isa pointer, kelas meta


0

Bagi saya orientasi objek dalam C harus memiliki fitur-fitur ini:

  1. Enkapsulasi dan penyembunyian data (dapat dicapai dengan menggunakan struct / pointer buram)

  2. Warisan dan dukungan untuk polimorfisme (pewarisan tunggal dapat dicapai dengan menggunakan struct - pastikan basis abstrak tidak instantiable)

  3. Fungsi konstruktor dan destruktor (tidak mudah dicapai)

  4. Pengecekan tipe (setidaknya untuk tipe yang ditentukan pengguna karena C tidak menerapkan apa pun)

  5. Penghitungan referensi (atau sesuatu untuk mengimplementasikan RAII )

  6. Dukungan terbatas untuk penanganan pengecualian (setjmp dan longjmp)

Di atas semua itu harus bergantung pada spesifikasi ANSI / ISO dan tidak boleh bergantung pada fungsi spesifik kompiler.


Untuk angka (5) - Anda tidak dapat mengimplementasikan RAII dalam bahasa tanpa destruktor (yang berarti RAII bukan teknik yang didukung kompiler di C atau Java).
Tom

konstruktor dan destruktor dapat ditulis untuk objek berbasis c - saya kira GObject melakukannya. dan ofcourse RAAI (tidak lurus ke depan, mungkin jelek dan tidak perlu pragmatis sama sekali) - yang saya cari hanyalah mengidentifikasi semantik berbasis C untuk mencapai hal di atas.
FL4SOF

C tidak mendukung destruktor. Anda harus mengetikkan sesuatu untuk membuatnya bekerja. Itu berarti mereka tidak membersihkan diri mereka sendiri. GObject tidak mengubah bahasa.
Tom

0

Lihatlah http://ldeniau.web.cern.ch/ldeniau/html/oopc/oopc.html . Jika tidak ada yang lain membaca dokumentasi adalah pengalaman yang mencerahkan.


3
Harap berikan konteks untuk tautan yang Anda bagikan. Meskipun tautan yang Anda bagikan memang sangat membantu, disarankan untuk lebih baik menangkap aspek-aspek kunci dari artikel yang dibagikan yang menanggapi pertanyaan. Dengan cara ini, bahkan jika tautan dihapus, jawaban Anda tetap relevan dan bermanfaat.
ishmaelMakitla

0

Saya agak terlambat ke pesta di sini, tetapi saya ingin menghindari kedua makro ekstrem - terlalu banyak atau terlalu banyak kode mengaburkan, tetapi beberapa makro yang jelas dapat membuat kode OOP lebih mudah untuk dikembangkan dan dibaca:

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

Saya pikir ini memiliki keseimbangan yang baik, dan kesalahan yang dihasilkannya (setidaknya dengan opsi default gcc 6.3) untuk beberapa kesalahan yang lebih mungkin membantu daripada membingungkan. Intinya adalah untuk meningkatkan produktivitas programmer, bukan?



0

Saya juga sedang mengerjakan ini berdasarkan pada solusi makro. Jadi itu hanya untuk yang paling berani, saya kira ;-) Tapi itu sudah cukup bagus, dan saya sudah mengerjakan beberapa proyek di atasnya. Ini berfungsi agar Anda pertama kali menentukan file header terpisah untuk setiap kelas. Seperti ini:

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

Untuk mengimplementasikan kelas, Anda membuat file header untuk itu dan file C tempat Anda menerapkan metode:

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

Di header yang Anda buat untuk kelas, Anda menyertakan header lain yang Anda butuhkan dan menentukan jenis dll yang terkait dengan kelas. Baik di header kelas dan di file C Anda menyertakan file spesifikasi kelas (lihat contoh kode pertama) dan X-makro. Makro X ini ( 1 , 2 , 3 dll.) Akan memperluas kode ke struct kelas aktual dan deklarasi lainnya.

Untuk mewarisi kelas, #define SUPER supernamedan tambahkan supername__define \sebagai baris pertama dalam definisi kelas. Keduanya pasti ada di sana. Ada juga dukungan JSON, sinyal, kelas abstrak, dll.

Untuk membuat objek, cukup gunakan W_NEW(classname, .x=1, .y=2,...) . Inisialisasi didasarkan pada inisialisasi struct yang diperkenalkan pada C11. Ini bekerja dengan baik dan semua yang tidak terdaftar diatur ke nol.

Untuk memanggil metode, gunakan W_CALL(o,method)(1,2,3) . Sepertinya panggilan fungsi tingkat tinggi tetapi ini hanya makro. Perluasan ((o)->klass->method(o,1,2,3))yang merupakan hack yang sangat bagus.

Lihat Dokumentasi dan kode itu sendiri .

Karena kerangka kerja membutuhkan beberapa kode boilerplate, saya menulis skrip Perl (wobject) yang berfungsi. Jika Anda menggunakannya, Anda bisa menulis

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

dan itu akan membuat file spesifikasi kelas, header kelas, dan file C, yang termasuk di Point_impl.cmana Anda mengimplementasikan kelas. Menghemat banyak pekerjaan, jika Anda memiliki banyak kelas sederhana tetapi masih semuanya dalam C. wobject adalah pemindai berbasis ekspresi reguler yang sangat sederhana yang mudah untuk beradaptasi dengan kebutuhan spesifik, atau ditulis ulang dari awal.



0

Anda dapat mencoba COOP , kerangka kerja yang ramah programmer untuk OOP di C, fitur Kelas, Pengecualian, Polimorfisme, dan Manajemen Memori (penting untuk kode Embedded). Ini sintaks yang relatif ringan, lihat tutorial di Wiki di sana.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.