C'de bir sınıfı nasıl uygularsınız? [kapalı]


139

C (C ++ veya nesne yönelimli derleyiciler yok) kullanmak zorunda olduğumu ve dinamik bellek ayırmaya sahip olmadığımı varsayarsak, bir sınıfı uygulamak için kullanabileceğim bazı teknikler veya bir sınıfın iyi bir yaklaşımı nedir? "Sınıf" ı ayrı bir dosyaya ayırmak her zaman iyi bir fikir midir? Sabit sayıda örneği varsayarak ve hatta derleme süresinden önce her nesneye olan referansı sabit olarak tanımlayarak belleği önceden tahsis edebildiğimizi varsayın. Hangi OOP konseptini uygulamak zorunda olacağım (değişiklik gösterecek) hakkında varsayımlar yapmaktan çekinmeyin ve her biri için en iyi yöntemi önerin.

Kısıtlamalar:

  • Gömülü bir sistem için kod yazıyorum ve derleyici ve önceden varolan kod tabanı C olduğunu çünkü C ve bir OOP kullanmak zorunda.
  • Dinamik bellek tahsisi yoktur, çünkü dinamik olarak tahsis etmeye başlarsak tükenmeyeceğimizi makul olarak varsaymak için yeterli belleğimiz yoktur.
  • Çalıştığımız derleyicilerin işlev işaretçileriyle bir sorunu yok

26
Zorunlu soru: Nesneye yönelik kod yazmak zorunda mısınız? Herhangi bir nedenle yaparsanız, bu iyi, ama oldukça yokuş yukarı bir savaşta mücadele edeceksiniz. C'ye nesne yönelimli kod yazmaya çalışmaktan kaçınmanız muhtemelen en iyisidir. Kesinlikle mümkündür - gevşemenin mükemmel cevabına bakın - ancak tam olarak "kolay" değildir ve sınırlı belleğe sahip gömülü bir sistem üzerinde çalışıyorsanız, mümkün olmayabilir. Yine de yanılıyor olabilirim - Sizi bunun dışında tartışmaya çalışmıyorum, sadece sunulmamış olabilecek bazı karşıt noktaları sunun.
Chris Lutz

1
Açıkçası, buna gerek yok. Ancak, sistemin karmaşıklığı, kodu sürdürülemez hale getirdi. Benim düşüncem, karmaşıklığı azaltmanın en iyi yolunun bazı OOP kavramlarını uygulamaktır. 3 dakika içinde yanıt veren herkese teşekkürler. Siz çılgın ve hızlısınız!
Ben Gartner

8
Bu benim alçakgönüllü fikrim, ancak OOP kodu anında sürdürülemez kılıyor. Yönetilmesini kolaylaştırabilir, ancak daha sürdürülebilir olması gerekmez. C'de "ad alanları" olabilir (Apache Portable Runtime tüm global sembolleri apr_ön ekler ve GLib bunlara ön g_ad ekler ) ve OOP olmadan diğer düzenleme faktörlerini kullanabilirsiniz. Uygulamayı yine de yeniden yapılandıracaksanız, daha sürdürülebilir bir prosedür yapısı bulmaya çalışırken biraz zaman geçirmeyi düşünürüm.
Chris Lutz

bu daha önce hiç tartışılmamıştı - önceki cevaplardan herhangi birine baktınız mı?
Larry Watanabe

Silinmiş bir cevabımdaki bu kaynak da yardımcı olabilir: planetpdf.com/codecuts/pdfs/ooc.pdf C'de OO yapmak için tam bir yaklaşımı açıklar.
Ruben Steins

Yanıtlar:


86

Bu, tam olarak istediğiniz "nesne yönelimli" özellik setine bağlıdır. Aşırı yükleme ve / veya sanal yöntemler gibi şeylere ihtiyacınız varsa, büyük olasılıkla yapılara işlev işaretçileri eklemeniz gerekir:

typedef struct {
  float (*computeArea)(const ShapeClass *shape);
} ShapeClass;

float shape_computeArea(const ShapeClass *shape)
{
  return shape->computeArea(shape);
}

Bu, bir sınıfı, temel sınıfı "devralarak" ve uygun bir işlevi uygulayarak uygulamanızı sağlar:

typedef struct {
  ShapeClass shape;
  float width, height;
} RectangleClass;

static float rectangle_computeArea(const ShapeClass *shape)
{
  const RectangleClass *rect = (const RectangleClass *) shape;
  return rect->width * rect->height;
}

Bu, elbette, işlev işaretçisinin düzgün bir şekilde ayarlandığından emin olan bir yapıcı da uygulamanızı gerektirir. Normalde örnek için dinamik olarak bellek ayırırsınız, ancak arayanın bunu yapmasına da izin verebilirsiniz:

void rectangle_new(RectangleClass *rect)
{
  rect->width = rect->height = 0.f;
  rect->shape.computeArea = rectangle_computeArea;
}

Birkaç farklı kurucu istiyorsanız, işlev adlarını "süslemeniz" gerekecektir, birden fazla rectangle_new()işleve sahip olamazsınız :

void rectangle_new_with_lengths(RectangleClass *rect, float width, float height)
{
  rectangle_new(rect);
  rect->width = width;
  rect->height = height;
}

Kullanımı gösteren temel bir örnek:

int main(void)
{
  RectangleClass r1;

  rectangle_new_with_lengths(&r1, 4.f, 5.f);
  printf("rectangle r1's area is %f units square\n", shape_computeArea(&r1));
  return 0;
}

Umarım bu size en azından bazı fikirler verir. C'de başarılı ve zengin bir nesne odaklı çerçeve için glib'in GObject kütüphanesine bakın.

Ayrıca, yukarıda modellenen hiçbir açık "sınıf" olmadığını, her nesnenin kendi yöntem işaretleyicilerine sahip olduğunu ve bu da genellikle C ++ 'da bulacağınızdan biraz daha esnek olduğunu unutmayın. Ayrıca, bellek maliyeti. Bir classyapıdaki yöntem işaretleyicilerini doldurarak bundan kurtulabilir ve her nesne örneğinin bir sınıfa başvurması için bir yol icat edebilirsiniz.


Nesne yönelimli C yazmaya çalışmak zorunda kalmadan, genellikle en iyi sonucu alan const ShapeClass *veya const void *bağımsız değişken olarak işlevler yapmak mı? İkincisi miras konusunda biraz daha hoş görünebilir, ancak her iki şekilde de argümanları görebiliyorum ...
Chris Lutz

1
@Chris: Evet, bu zor bir soru. : | GTK + (GObject kullanan) uygun sınıfı kullanır, yani RectangleClass *. Bu, genellikle döküm yapmak zorunda olduğunuz anlamına gelir, ancak kullanışlı makrolar bu konuda yardımcı olur, böylece BASECLASS * p'yi yalnızca SUBCLASS (p) kullanarak her zaman SUBCLASS * 'a yayınlayabilirsiniz.
açma

1
Derleyicim ikinci kod satırında başarısız oluyor: float (*computeArea)(const ShapeClass *shape);bunun ShapeClassbilinmeyen bir tür olduğunu söylüyor .
DanielSank

@DanielSank, 'typedef struct' tarafından istenen ileri bildirim eksikliğinden kaynaklanmaktadır (verilen örnekte gösterilmemiştir). structReferansların kendisi olduğundan , tanımlanmadan önce bir bildirim gerektirir . Bu, Lundin'in cevabında burada bir örnekle açıklanmıştır . Örneği ileri bildirimi içerecek şekilde değiştirmek sorununuzu çözmelidir; typedef struct ShapeClass ShapeClass; struct ShapeClass { float (*computeArea)(const ShapeClass *shape); };
S. Whittaker

Dikdörtgen'in tüm Şekiller'de olmayan bir işlevi olduğunda ne olur? Örneğin, get_corners (). Bir daire bunu uygulamaz, ancak bir dikdörtgen olabilir. Devralınan üst sınıfın bir parçası olmayan bir işleve nasıl erişirsiniz?
Otus

24

Ödev için de bir kez yapmak zorunda kaldım. Bu yaklaşımı izledim:

  1. Veri üyelerinizi bir yapıda tanımlayın.
  2. İlk argüman olarak yapınıza bir işaretçi alan işlev üyelerinizi tanımlayın.
  3. Bunları tek bir başlıkta ve bir c. Yapı tanımı ve işlev bildirimleri için başlık, uygulamalar için c.

Basit bir örnek şudur:

/// Queue.h
struct Queue
{
    /// members
}
typedef struct Queue Queue;

void push(Queue* q, int element);
void pop(Queue* q);
// etc.
/// 

Bu geçmişte yaptığım şey, ancak işlev prototiplerini gerektiği gibi .c veya .h dosyasına yerleştirerek (yanıtımda belirttiğim gibi) sahte kapsam ekleyerek.
Taylor Leese

Bunu beğendim, yapı bildirimi tüm belleği ayırır. Nedense bunun iyi olacağını unuttum.
Ben Gartner

Bence typedef struct Queue Queue;orada bir ihtiyacın var.
Craig McQueen

3
Ya da sadece typedef struct {/ * members * /} Kuyruğu;
Brooks Moses

#Craig: Hatırlatma için teşekkürler, güncellendi.
erelender

12

Yalnızca bir sınıf istiyorsanız struct, "nesne" verisi olarak s dizisini kullanın ve "üye" işlevlerine işaretçiler iletin. Uygulamayı istemci kodundan gizlemek için typedef struct _whatever Whateverbildirmeden önce kullanabilirsiniz struct _whatever. Böyle bir "nesne" ile C standart kütüphane FILEnesnesi arasında hiçbir fark yoktur .

Kalıtım ve sanal işlevlere sahip birden fazla sınıf istiyorsanız, yapının üyeleri olarak işlevlere veya sanal işlevler tablosuna paylaşılan bir işaretçiye sahip olmak yaygındır. GObject kütüphane hem bu typedef hile kullanır ve yaygın olarak kullanılmaktadır.

ANSI C ile bu online olarak kullanılabilir teknikler üzerine bir kitap da var .


1
Güzel! OOP ile ilgili kitaplar için başka öneriler var mı? Veya C'deki diğer modern tasarım teknikleri? (veya gömülü sistemler?)
Ben Gartner

7

GOBject'e bir göz atabilirsiniz. size bir nesne yapmanın ayrıntılı bir yolunu sunan bir OS kütüphanesi.

http://library.gnome.org/devel/gobject/stable/


1
Çok ilginç. Lisansı bilen var mı? İşimdeki amaçlar için, açık kaynak kodlu bir kütüphaneyi bir projeye bırakmak, muhtemelen yasal açıdan işe yaramayacaktır.
Ben Gartner

GTK + ve bu projenin bir parçası olan (GObject dahil) tüm kütüphaneler GNU LGPL altında lisanslanmıştır, bu da onlara özel yazılımdan bağlanabileceğiniz anlamına gelir. Yine de, bunun gömülü iş için uygun olup olmayacağını bilmiyorum.
Chris Lutz

7

C Arayüzler ve Uygulamalar: Yeniden Kullanılabilir Yazılım Oluşturma Teknikleri , David R. Hanson

http://www.informit.com/store/product.aspx?isbn=0201498413

Bu kitap, sorunuzun üstesinden gelmek için mükemmel bir iş çıkarıyor. Addison Wesley Professional Computing serisinde.

Temel paradigma şöyle bir şeydir:

/* for data structure foo */

FOO *myfoo;
myfoo = foo_create(...);
foo_something(myfoo, ...);
myfoo = foo_append(myfoo, ...);
foo_delete(myfoo);

5

OOP'nin C'de nasıl yapılması gerektiğine dair basit bir örnek vereceğim.

/// Object.h
typedef struct Object {
    uuid_t uuid;
} Object;

int Object_init(Object *self);
uuid_t Object_get_uuid(Object *self);
int Object_clean(Object *self);

/// Person.h
typedef struct Person {
    Object obj;
    char *name;
} Person;

int Person_init(Person *self, char *name);
int Person_greet(Person *self);
int Person_clean(Person *self);

/// Object.c
#include "object.h"

int Object_init(Object *self)
{
    self->uuid = uuid_new();

    return 0;
}
uuid_t Object_get_uuid(Object *self)
{ // Don't actually create getters in C...
    return self->uuid;
}
int Object_clean(Object *self)
{
    uuid_free(self->uuid);

    return 0;
}

/// Person.c
#include "person.h"

int Person_init(Person *self, char *name)
{
    Object_init(&self->obj); // Or just Object_init(&self);
    self->name = strdup(name);

    return 0;
}
int Person_greet(Person *self)
{
    printf("Hello, %s", self->name);

    return 0;
}
int Person_clean(Person *self)
{
    free(self->name);
    Object_clean(self);

    return 0;
}

/// main.c
int main(void)
{
    Person p;

    Person_init(&p, "John");
    Person_greet(&p);
    Object_get_uuid(&p); // Inherited function
    Person_clean(&p);

    return 0;
}

Temel kavram 'miras alınan sınıfı' yapının tepesine yerleştirmeyi içerir. Bu şekilde, yapıdaki ilk 4 bayta erişmek de 'kalıtsal sınıftaki' ilk 4 bayta erişir (Çılgın olmayan en iyi duruma getirme varsayımı). Şimdi, yapının işaretçisi 'miras alınan sınıfa' yayınlandığında, 'miras alınan sınıf', 'miras alınan değerlere' normal olarak üyelerine erişeceği şekilde erişebilir.

Bu ve inşaatçılar, yıkıcılar, tahsis ve deallocarion fonksiyonları için bazı adlandırma kuralları (tavsiye ederim init, temiz, yeni, ücretsiz) size uzun bir yol sağlayacaktır.

Sanal işlevlere gelince, yapıda işlev işaretçileri kullanın, muhtemelen Class_func (...); sarıcı da. (Basit) şablonlara gelince, boyutu belirlemek için bir size_t parametresi ekleyin, bir void * işaretçisi veya yalnızca önem verdiğiniz işlevsellik ile bir 'sınıf' türü gerektirir. (örneğin int GetUUID (Object * self); GetUUID (& p);)


Yasal Uyarı: Tüm kod akıllı telefon üzerinde yazılı. Gerektiğinde hata kontrolleri ekleyin. Hata olup olmadığını kontrol edin.
yyny

4

structBir sınıfın veri üyelerini simüle etmek için a kullanın . Yöntem kapsamı açısından, özel işlev prototiplerini .c dosyasına ve genel işlevleri .h dosyasına yerleştirerek özel yöntemleri simüle edebilirsiniz .


4
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <uchar.h>

/**
 * Define Shape class
 */
typedef struct Shape Shape;
struct Shape {
    /**
     * Variables header...
     */
    double width, height;

    /**
     * Functions header...
     */
    double (*area)(Shape *shape);
};

/**
 * Functions
 */
double calc(Shape *shape) {
        return shape->width * shape->height;
}

/**
 * Constructor
 */
Shape _Shape() {
    Shape s;

    s.width = 1;
    s.height = 1;

    s.area = calc;

    return s;
}

/********************************************/

int main() {
    Shape s1 = _Shape();
    s1.width = 5.35;
    s1.height = 12.5462;

    printf("Hello World\n\n");

    printf("User.width = %f\n", s1.width);
    printf("User.height = %f\n", s1.height);
    printf("User.area = %f\n\n", s1.area(&s1));

    printf("Made with \xe2\x99\xa5 \n");

    return 0;
};

3

Sizin durumunuzda, sınıfın iyi yaklaşımı bir ADT olabilir . Ama yine de aynı olmayacak.


1
Herkes soyut veri türü ve sınıf arasında kısa bir fark verebilir mi? Ben her zaman birbiriyle yakından bağlantılı iki kavram var.
Ben Gartner

Gerçekten yakından ilişkilidirler. Bir sınıf, bir ADT'nin uygulaması olarak görülebilir, çünkü (sözde) aynı arabirimi karşılayan başka bir uygulama ile değiştirilebilir. Kavramlar açıkça tanımlanmadığı için kesin bir fark vermek zor.
Jørgen Fogh

3

Stratejim:

  • Sınıf için tüm kodu ayrı bir dosyada tanımlayın
  • Sınıf için tüm arabirimleri ayrı bir başlık dosyasında tanımlayın
  • Tüm üye işlevleri, örnek adı (o.foo () yerine, "foo (oHandle) yerine kullanılan bir" ClassHandle "alır
  • Yapıcı, bellek ayırma stratejisine bağlı olarak void ClassInit (ClassHandle h, int x, int y, ...) veya ClassHandle ClassInit (int x, int y, ...) işleviyle değiştirilir.
  • Tüm üye değişkenler, sınıf dosyasında statik bir yapının üyesi olarak depolanır ve dosyayı içine alır, dış dosyaların ona erişmesini önler
  • Nesneler, önceden tanımlanmış tutamaçlarla (arabirimde görünür) veya somutlaştırılabilen sabit bir nesne sınırıyla yukarıdaki statik yapı dizisinde saklanır
  • Yararlıysa, sınıf, dizi boyunca döngü yapacak ve tüm somut nesnelerin işlevlerini çağıracak genel işlevler içerebilir (RunAll () her bir Çalışmayı (oHandle) çağırır
  • Deinit (ClassHandle h) işlevi, dinamik ayırma stratejisinde ayrılan belleği (dizi dizini) serbest bırakır

Herkes bu yaklaşımın herhangi bir varyasyon herhangi bir sorun, delik, potansiyel tuzaklar veya gizli yararları / dezavantajları görüyor mu? Bir tasarım yöntemini yeniden icat edersem (ve öyle olmam gerektiğini varsayarsam), bana ismini gösterebilir misin?


Tarz olarak, sorunuza ekleyeceğiniz bilgileriniz varsa, sorunuzu bu bilgileri içerecek şekilde düzenlemeniz gerekir.
Chris Lutz

Malloc'dan dinamik olarak büyük bir öbekten ClassInit () 'e dinamik olarak sabit bir boyut havuzundan seçim yapmak yerine, başka bir nesneyi sorduğunuzda ve bir tane sağlamak için kaynaklara sahip olmadığınızda ne olacağı hakkında bir şey yapmak yerine taşınmış gibi görünüyorsunuz. .
Pete Kirkham

Evet, bellek yönetimi yükü döndürülen tanıtıcı geçerli olup olmadığını denetlemek için ClassInit () çağıran koda kaydırılır. Esasen sınıf için kendi özel yığınımızı yarattık. Genel amaçlı bir yığın uygulamadığımız sürece herhangi bir dinamik ayırma yapmak istiyorsak bundan kaçınmanın bir yolunu gördüğümden emin değilim. Öbekteki miras riskini bir sınıfa ayırmayı tercih ederim.
Ben Gartner

3

Ayrıca bu cevaba ve bu cevaba bakınız

Bu mümkün. O zamanlar her zaman iyi bir fikir gibi gözüküyor ama daha sonra bir bakım kabusu oluyor. Kodunuz, her şeyi birbirine bağlayan kod parçalarıyla doludur. Yeni bir programcı, işlev işaretçileri kullanırsanız kodu okuma ve anlamada çok fazla sorun yaşayacaktır, çünkü hangi işlevlerin çağrıldığı açık değildir.

Get / set işlevleriyle veri gizleme, C'de uygulanması kolaydır, ancak burada durur. Gömülü ortamda bu konuda birçok girişim gördüm ve sonunda her zaman bir bakım sorunu.

Hepinizin hazır bakım sorunları olduğundan, net bir şekilde yönlendiririm.


2

Benim yaklaşımım structve öncelikle ilişkili tüm işlevleri ayrı bir kaynak dosya (lar) taşımak, böylece "taşınabilir" kullanılabilir.

Derleyici bağlı olarak, belki içine işlevleri dahil edebilmek struct, ama bu var çok derleyici özgü uzantısı ve rutin olarak kullanılan standart I son sürümü ile ilgisi yoktur :)


2
Fonksiyon göstergelerinin hepsi iyidir. Bunları büyük anahtar ifadelerini bir arama tablosuyla değiştirmek için kullanma eğilimindeyiz.
Ben Gartner

2

İlk c ++ derleyicisi aslında C ++ kodunu C'ye çeviren bir önişlemciydi.

Bu yüzden C'de sınıflara sahip olmak çok mümkündür. Eski bir C ++ ön işlemcisini kazıp deneyebilir ve ne tür çözümler yarattığını görebilirsiniz.


Bu olurdu cfront; C ++ 'da istisnalar eklendiğinde sorunlara yol açtı - istisnaların ele alınması önemsiz değil.
Jonathan Leffler

2

GTK tamamen C üzerine kurulmuştur ve birçok OOP kavramı kullanır. GTK'nın kaynak kodunu okudum ve oldukça etkileyici ve okunması kesinlikle daha kolay. Temel kavram, her "sınıfın" basitçe bir yapı ve ilişkili statik işlevler olmasıdır. Statik işlevlerin tümü "örnek" yapısını parametre olarak kabul eder, sonra ne gerekiyorsa yapar ve gerekirse sonuçları döndürür. Örneğin, "GetPosition (CircleStruct obj)" işlevine sahip olabilirsiniz. İşlev basitçe yapıdan geçecek, konum numaralarını çıkaracak, muhtemelen yeni bir PositionStruct nesnesi oluşturacak, x ve y'yi yeni PositionStruct'a yapıştıracak ve döndürecektir. GTK, yapıları yapıların içine yerleştirerek miras bu şekilde uygular. oldukça zeki.


1

Sanal yöntemler ister misiniz?

Değilse, sadece yapının kendisinde bir dizi işlev işaretçisi tanımlarsınız. Tüm işlev işaretleyicilerini standart C işlevlerine atarsanız, C ++ 'dan nasıl yapacağınıza çok benzer bir sözdizimindeki işlevleri çağırabilirsiniz.

Sanal yöntemlere sahip olmak istiyorsanız daha karmaşık hale gelir. Temel olarak, her yapıya kendi VTable'ınızı uygulamanız ve hangi işlevin çağrıldığına bağlı olarak VTable'a işlev işaretçileri atamanız gerekir. Daha sonra yapı içinde, VTable'da işlev işaretçisini çağıran bir işlev işaretçileri kümesine ihtiyacınız olacaktır. Aslında bu, C ++ 'ın yaptığı şeydir.

TBH olsa ... ikincisini istiyorsanız o zaman muhtemelen sadece bir C ++ derleyici bulmak ve proje yeniden derleme daha iyidir. C ++ ile olan takıntıyı gömülü olarak kullanamayacağımı hiç anlamadım. Birçok kez kullandım ve hızlı çalışıyor ve hafıza problemleri yok. Tabii ne yaptığınız konusunda biraz daha dikkatli olmalısınız ama bu o kadar da karmaşık değil.


Zaten söyledim ve tekrar söyleyeceğim, ama tekrar söyleyeceğim: Fonksiyon işaretleyicilerine veya C ++ tarzında OOP oluşturmak için C ++ stili yapılardan işlev çağırma yeteneğine ihtiyacınız yok, OOP çoğunlukla işlevsellik ve değişkenlerin mirası hakkında (içerik) her ikisi de C'de işlev işaretçileri veya çoğaltma kodu olmadan elde edilebilir.
yyny

0

C, doğru bir şekilde belirttiğiniz gibi bir OOP dili değildir, bu nedenle gerçek bir sınıf yazmanın yerleşik bir yolu yoktur. Yapmanız gereken en iyi şey, yapılara ve işlev işaretçilerine bakmaktır , bunlar bir sınıfın yaklaşıkını oluşturmanıza izin verecektir. Bununla birlikte, C prosedürel olduğundan daha fazla C benzeri kod yazmayı düşünebilirsiniz (yani sınıfları kullanmaya çalışmadan).

Ayrıca, C kullanabilirsiniz, muhtemelen C ++ kullanabilirsiniz ve sınıfları alabilirsiniz.


4
Ben aşağılamak olmaz, ama FYI, fonksiyon işaretçileri, ya da fonksiyonları yapılardan çağrı yeteneği (ki niyetiniz olduğunu düşünüyorum) OOP ile ilgisi yoktur. OOP çoğunlukla işlevsellik ve değişkenlerin mirasıyla ilgilidir, her ikisi de C'de fonksiyon işaretçileri veya kopyaları olmadan elde edilebilir.
yyny
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.