Объектная ориентация в C - PullRequest
145 голосов
/ 06 января 2009

Каким будет набор изящных препроцессорных хаков (совместимых с ANSI C89 / ISO C90), которые обеспечивают некую некрасивую (но пригодную для использования) объектную ориентацию в C?

Я знаком с несколькими различными объектно-ориентированными языками, поэтому, пожалуйста, не отвечайте с ответами типа "Learn C ++!". Я прочитал « Объектно-ориентированное программирование с ANSI C » (будьте осторожны: Формат PDF ) и несколько других интересных решений, но в основном я заинтересован в вашем: -)!


См. Также Можете ли вы написать объектно-ориентированный код на C?

Ответы [ 21 ]

175 голосов
/ 06 января 2009

Я бы посоветовал не использовать препроцессор (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

Чтобы получить полиморфизм (то есть виртуальные функции), вы используете указатели на функции и, необязательно, таблицы указателей на функции, также известные как виртуальные таблицы или 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;
}

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

Еще одна (хитрая) вещь, которую вы можете сделать, это изменить динамический тип объекта во время выполнения! Вы просто переназначаете новый указатель vtable. Вы даже можете выборочно изменять некоторые виртуальные функции, сохраняя другие, создавая новые гибридные типы. Просто будьте осторожны, создавая новую виртуальную таблицу вместо изменения глобальной виртуальной таблицы, в противном случае вы случайно затронете все объекты данного типа.

31 голосов
/ 06 января 2009

Однажды я работал с библиотекой 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 точно определяли, что вы должны делать с каждым объектом.

29 голосов
/ 06 января 2009

C Object System (COS) звучит многообещающе (все еще в альфа-версии). Он пытается свести к минимуму доступные концепции ради простоты и гибкости: единообразное объектно-ориентированное программирование, включая открытые классы, метаклассы, метаклассы свойств, универсальные шаблоны, мультиметоды, делегирование, владение, исключения, контракты и замыкания. Существует черновик (PDF), который описывает его.

Исключение в C является реализацией C89 TRY-CATCH-FINALLY, найденной в других языках OO. Он поставляется с комплектом тестов и некоторыми примерами.

Оба от Лорана Денио, который много работает над ООП в C .

16 голосов
/ 06 января 2009

Рабочий стол GNOME для Linux написан на объектно-ориентированном C и имеет объектную модель под названием " GObject ", которая поддерживает свойства, наследование, полиморфизм, а также некоторые другие полезные свойства, такие как ссылки, события обработка (называемая «сигналами»), типизация времени выполнения, конфиденциальные данные и т. д.

Он включает в себя хаки препроцессора для выполнения таких вещей, как приведение типов в иерархии классов и т. Д. Вот пример класса, который я написал для GNOME (такие вещи, как gchar - это typedefs):

Источник класса

Заголовок класса

Внутри структуры GObject есть целое число GType, которое используется в качестве магического числа для системы динамической типизации GLib (вы можете привести всю структуру к «GType», чтобы найти ее тип).

6 голосов
/ 05 августа 2009

Немного не по теме, но оригинальный компилятор C ++, Cfront , скомпилировал C ++ в C и затем в ассемблер.

Сохранено здесь .

6 голосов
/ 06 января 2009

Если вы думаете о методах, вызываемых на объектах, как о статических методах, которые передают неявный 'this' в функцию, это может упростить процесс мышления в O.

Например:

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

становится:

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

Или что-то в этом роде.

5 голосов
/ 06 января 2009

Раньше я делал такие вещи в Си, прежде чем знал, что такое ООП.

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

Идея состоит в том, что объект создается с помощью xxx_crt () и удаляется с помощью xxx_dlt (). Каждый из методов "member" принимает специально типизированный указатель для работы.

Я реализовал связанный список, циклический буфер и ряд других вещей таким образом.

Должен признаться, я никогда не задумывался о том, как реализовать наследование с помощью этого подхода. Я полагаю, что какая-то смесь того, что предлагал Кивели, могла бы стать хорошим путем.

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 был просто typedef от int - я использовал его, чтобы напомнить мне, что его длина варьируется от платформы к платформе (для портирования).

4 голосов
/ 06 января 2009

ffmpeg (набор инструментов для обработки видео) написан на прямом C (и ассемблере), но с использованием объектно-ориентированного стиля. Он полон структур с указателями на функции. Существует набор фабричных функций, которые инициализируют структуры с помощью соответствующих указателей «метода».

3 голосов
/ 02 июня 2013

Если вы действительно любите мыслить, даже стандартная библиотека C использует OOP - рассмотрим FILE * в качестве примера: fopen() инициализирует объект FILE *, и вы используете его, используя методы-члены fscanf(), fprintf(), fread(), fwrite() и другие, и в конечном итоге завершить его с fclose().

Вы также можете пойти по пути псевдообъектива-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-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];
}
3 голосов
/ 08 января 2015

Я думаю, что то, что написал Адам Розенфилд, является правильным способом выполнения ООП в 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 file" в статическую или динамическую библиотеку. Это создает инкапсуляцию, а также вы можете изменить реализацию по своему желанию. Пользователь вашего объекта почти ничего не должен знать о его реализации. Это также делает акцент на общий дизайн объекта.

Я лично убежден, что oop - это способ концептуализации структуры вашего кода и возможности его повторного использования, который на самом деле не имеет ничего общего с теми другими вещами, которые добавляются в c ++, такими как перегрузка или шаблоны. Да, это очень хорошие полезные функции, но они не представляют, что такое объектно-ориентированное программирование.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...