Bagaimana Anda mengimplementasikan kelas di C? [Tutup]

139

Dengan asumsi saya harus menggunakan C (tidak ada C ++ atau penyusun berorientasi objek) dan saya tidak memiliki alokasi memori dinamis, teknik apa sajakah yang dapat saya gunakan untuk mengimplementasikan suatu kelas, atau perkiraan yang baik dari suatu kelas? Apakah selalu merupakan ide yang baik untuk mengisolasi "kelas" ke file yang terpisah? Asumsikan bahwa kita dapat melakukan pra-alokasi memori dengan mengasumsikan jumlah instance tetap, atau bahkan mendefinisikan referensi ke setiap objek sebagai konstanta sebelum waktu kompilasi. Jangan ragu untuk membuat asumsi tentang konsep OOP mana yang perlu saya terapkan (akan bervariasi) dan menyarankan metode terbaik untuk masing-masing.

Pembatasan:

  • Saya harus menggunakan C dan bukan OOP karena saya menulis kode untuk sistem tertanam, dan kompiler dan basis kode yang sudah ada di C.
  • Tidak ada alokasi memori dinamis karena kami tidak memiliki cukup memori untuk menganggap wajar kami tidak akan kehabisan jika kami mulai mengalokasikannya secara dinamis.
  • Compiler yang bekerja dengan kami tidak memiliki masalah dengan pointer fungsi
Ben Gartner
sumber
26
Pertanyaan wajib: Apakah Anda harus menulis kode berorientasi objek? Jika Anda melakukannya untuk alasan apa pun, itu baik-baik saja, tetapi Anda akan berjuang di medan yang agak sulit. Ini mungkin yang terbaik jika Anda menghindari mencoba menulis kode berorientasi objek dalam C. Ini tentu mungkin - lihat jawaban yang sangat baik dari bersantai - tetapi itu tidak sepenuhnya "mudah," dan jika Anda bekerja pada sistem tertanam dengan memori terbatas, itu mungkin tidak layak. Namun, saya mungkin salah - saya tidak berusaha mendebat Anda, cukup tunjukkan beberapa tandingan yang mungkin tidak disajikan.
Chris Lutz
1
Sebenarnya, kita tidak harus melakukannya. Namun, kompleksitas sistem telah membuat kode tidak dapat dipelihara. Perasaan saya adalah bahwa cara terbaik untuk mengurangi kompleksitas adalah dengan mengimplementasikan beberapa konsep OOP. Terima kasih untuk semua orang yang merespons dalam 3 menit. Kalian gila dan cepat!
Ben Gartner
8
Ini hanya pendapat saya yang sederhana, tetapi OOP tidak membuat kode dapat dipelihara secara instan. Ini mungkin membuatnya lebih mudah untuk dikelola, tetapi tidak selalu lebih mudah dikelola. Anda dapat memiliki "namespaces" di C (Apache Portable Runtime memberi awalan semua simbol global dengan apr_dan GLib memberinya awalan g_untuk membuat namespace) dan faktor pengorganisasian lainnya tanpa OOP. Jika Anda akan merestrukturisasi aplikasi, saya akan mempertimbangkan meluangkan waktu untuk mencoba membuat struktur prosedural yang lebih mudah dikelola.
Chris Lutz
ini telah dibahas tanpa akhir sebelumnya - apakah Anda melihat jawaban sebelumnya?
Larry Watanabe
Sumber ini, yang ada dalam jawaban saya yang dihapus, mungkin juga dapat membantu: planetpdf.com/codecuts/pdfs/ooc.pdf Ini menjelaskan pendekatan lengkap untuk melakukan OO dalam C.
Ruben Steins

Jawaban:

86

Itu tergantung pada set fitur "berorientasi objek" yang tepat yang ingin Anda miliki. Jika Anda membutuhkan hal-hal seperti overloading dan / atau metode virtual, Anda mungkin perlu menyertakan pointer fungsi dalam struktur:

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

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

Ini akan memungkinkan Anda menerapkan kelas, dengan "mewarisi" kelas dasar, dan menerapkan fungsi yang sesuai:

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

Ini tentu saja mengharuskan Anda untuk juga mengimplementasikan konstruktor, yang memastikan fungsi pointer diatur dengan benar. Biasanya Anda akan mengalokasikan memori secara dinamis untuk instance, tetapi Anda dapat membiarkan penelepon juga melakukannya:

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

Jika Anda menginginkan beberapa konstruktor berbeda, Anda harus "mendekorasi" nama fungsi, Anda tidak dapat memiliki lebih dari satu rectangle_new()fungsi:

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

Berikut adalah contoh dasar yang menunjukkan penggunaan:

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

Saya harap ini memberi Anda beberapa ide, setidaknya. Untuk kerangka kerja berorientasi objek yang sukses dan kaya di C, lihat ke perpustakaan GObject glib .

Perhatikan juga bahwa tidak ada "kelas" eksplisit yang dimodelkan di atas, setiap objek memiliki pointer metode sendiri yang sedikit lebih fleksibel daripada yang biasanya Anda temukan di C ++. Juga, biaya memori. Anda bisa lolos dari itu dengan memasukkan pointer metode dalam classstruktur, dan menemukan cara untuk setiap instance objek untuk referensi kelas.

beristirahat
sumber
Tidak harus mencoba menulis berorientasi objek C, apakah biasanya membuat fungsi yang mengambil const ShapeClass *atau const void *sebagai argumen? Tampaknya bahwa yang terakhir mungkin sedikit lebih bagus pada warisan, tapi aku bisa melihat argumen kedua cara ...
Chris Lutz
1
@ Chris: Ya, itu pertanyaan yang sulit. : | GTK + (yang menggunakan GObject) menggunakan kelas yang tepat, yaitu RectangleClass *. Ini berarti Anda sering harus melakukan gips, tetapi mereka menyediakan makro yang berguna membantu dengan itu, sehingga Anda selalu dapat melemparkan BASECLASS * p ke SUBCLASS * hanya menggunakan SUBCLASS (p).
bersantai
1
Kompiler saya gagal pada baris kedua kode: float (*computeArea)(const ShapeClass *shape);mengatakan itu ShapeClassadalah tipe yang tidak dikenal.
DanielSank
@DanielSank karena kurangnya deklarasi maju yang disyaratkan oleh 'typedef struct` (tidak diperlihatkan dalam contoh yang diberikan). Karena structreferensi itu sendiri, itu memerlukan deklarasi sebelum didefinisikan. Ini dijelaskan dengan sebuah contoh di sini dalam jawaban Lundin . Mengubah contoh untuk menyertakan deklarasi maju harus menyelesaikan masalah Anda; typedef struct ShapeClass ShapeClass; struct ShapeClass { float (*computeArea)(const ShapeClass *shape); };
S. Whittaker
Apa yang terjadi ketika Rectangle memiliki fungsi yang tidak semua Bentuk lakukan. Misalnya, get_corners (). Lingkaran tidak akan mengimplementasikan ini tetapi mungkin persegi panjang. Bagaimana Anda mengakses fungsi yang bukan bagian dari kelas induk yang Anda warisi?
Otus
24

Saya harus melakukannya sekali juga untuk pekerjaan rumah. Saya mengikuti pendekatan ini:

  1. Tentukan anggota data Anda dalam sebuah struct.
  2. Tentukan anggota fungsi Anda yang mengambil pointer ke struct Anda sebagai argumen pertama.
  3. Lakukan ini dalam satu tajuk & satu c. Header untuk definisi definisi & deklarasi fungsi, c untuk implementasi.

Contoh sederhana adalah ini:

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

void push(Queue* q, int element);
void pop(Queue* q);
// etc.
/// 
Erelender
sumber
Ini adalah apa yang telah saya lakukan di masa lalu, tetapi dengan penambahan lingkup pemalsuan dengan menempatkan prototipe fungsi baik dalam file .c atau .h sesuai kebutuhan (seperti yang saya sebutkan dalam jawaban saya).
Taylor Leese
Saya suka ini, deklarasi struct mengalokasikan semua memori. Untuk beberapa alasan saya lupa ini akan bekerja dengan baik.
Ben Gartner
Saya pikir Anda perlu typedef struct Queue Queue;di sana.
Craig McQueen
3
Atau ketikkan saja struct {/ * members * /} Antrian;
Brooks Moses
#Craig: Terima kasih atas pengingatnya, diperbarui.
erelender
12

Jika Anda hanya menginginkan satu kelas, gunakan array structs sebagai data "objek" dan berikan pointer ke fungsi "anggota". Anda dapat menggunakan typedef struct _whatever Whateversebelum mendeklarasikan struct _whateveruntuk menyembunyikan implementasi dari kode klien. Tidak ada perbedaan antara "objek" dan FILEobjek pustaka standar C tersebut .

Jika Anda menginginkan lebih dari satu kelas dengan fungsi pewarisan dan virtual, maka itu umum untuk memiliki pointer ke fungsi sebagai anggota struct, atau pointer bersama ke tabel fungsi virtual. The GObject perpustakaan menggunakan kedua ini dan trik typedef, dan secara luas digunakan.

Ada juga buku tentang teknik untuk ini tersedia secara online - Object Oriented Programming dengan ANSI C .

Pete Kirkham
sumber
1
Keren! Adakah rekomendasi lain untuk buku tentang OOP di C? Atau teknik desain modern lainnya di C? (atau sistem yang disematkan?)
Ben Gartner
7

Anda dapat melihat di GOBject. ini adalah pustaka OS yang memberi Anda cara verbose untuk melakukan objek.

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

Alex
sumber
1
Sangat tertarik. Adakah yang tahu tentang lisensi? Untuk tujuan saya di tempat kerja, menjatuhkan perpustakaan open source ke dalam proyek mungkin tidak akan berhasil dari sudut pandang hukum.
Ben Gartner
GTK +, dan semua perpustakaan yang merupakan bagian dari proyek itu (termasuk GObject), dilisensikan di bawah GNU LGPL, yang berarti Anda dapat menautkannya dari perangkat lunak berpemilik. Saya tidak tahu apakah itu akan layak untuk pekerjaan tertanam.
Chris Lutz
7

C Antarmuka dan Implementasi: Teknik untuk Membuat Perangkat Lunak yang Dapat Digunakan Kembali , David R. Hanson

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

Buku ini sangat baik dalam meliput pertanyaan Anda. Ada dalam seri Addison Wesley Professional Computing.

Paradigma dasarnya adalah seperti ini:

/* for data structure foo */

FOO *myfoo;
myfoo = foo_create(...);
foo_something(myfoo, ...);
myfoo = foo_append(myfoo, ...);
foo_delete(myfoo);
Mark Harrison
sumber
5

Saya akan memberikan contoh sederhana tentang bagaimana OOP harus dilakukan dalam C. Saya menyadari ini adalah dari tahun 2009 tetapi ingin menambahkan ini pula.

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

Konsep dasar melibatkan penempatan 'kelas warisan' di bagian atas struct. Dengan cara ini, mengakses 4 byte pertama dalam struct juga mengakses 4 byte pertama dalam 'kelas yang diwariskan' (Dengan asumsi non-crazy optimalisations). Sekarang, ketika pointer dari struct dilemparkan ke 'kelas yang diwariskan', 'kelas yang diwarisi' dapat mengakses 'nilai-nilai yang diwarisi' dengan cara yang sama dengan mengakses anggota-anggotanya secara normal.

Ini dan beberapa konvensi penamaan untuk fungsi konstruktor, destruktor, alokasi, dan deallocarion (saya sarankan init, clean, baru, gratis) akan membantu Anda.

Adapun fungsi Virtual, gunakan pointer fungsi di struct, mungkin dengan Class_func (...); bungkus juga. Adapun templat (sederhana), tambahkan parameter size_t untuk menentukan ukuran, memerlukan pointer * kosong, atau memerlukan tipe 'kelas' hanya dengan fungsionalitas yang Anda pedulikan. (mis. int GetUUID (Objek * sendiri); GetUUID (& p);)

yyny
sumber
Penafian: Semua kode ditulis di smartphone. Tambahkan centang kesalahan jika perlu. Periksa bug.
yyny
4

Gunakan a structuntuk mensimulasikan data anggota kelas. Dalam hal cakupan metode, Anda dapat mensimulasikan metode pribadi dengan menempatkan prototipe fungsi pribadi dalam file .c dan fungsi publik dalam file .h.

Taylor Leese
sumber
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;
};
Pierozi
sumber
3

Dalam kasus Anda, perkiraan yang baik dari kelas bisa menjadi ADT . Tapi tetap saja tidak akan sama.

Artem Barger
sumber
1
Adakah yang bisa memberikan perbedaan singkat antara tipe data abstrak dan kelas? Saya selalu memiliki dua konsep yang terkait erat.
Ben Gartner
Mereka memang terkait erat. Kelas dapat dilihat sebagai implementasi dari ADT, karena (seharusnya) dapat digantikan oleh implementasi lain yang memenuhi antarmuka yang sama. Saya pikir sulit untuk memberikan perbedaan yang tepat, karena konsepnya tidak didefinisikan dengan jelas.
Jørgen Fogh
3

Strategi saya adalah:

  • Tentukan semua kode untuk kelas dalam file terpisah
  • Tentukan semua antarmuka untuk kelas dalam file header terpisah
  • Semua fungsi anggota mengambil "ClassHandle" yang merupakan singkatan dari nama instance (bukan o.foo (), panggil foo (oHandle)
  • Konstruktor diganti dengan fungsi void ClassInit (ClassHandle h, int x, int y, ...) ATAU ClassHandle ClassInit (int x, int y, ...) tergantung pada strategi alokasi memori
  • Semua variabel anggota disimpan sebagai anggota struct statis di file kelas, mengenkapsulasi dalam file, mencegah file luar mengaksesnya
  • Objek disimpan dalam array dari struct statis di atas, dengan pegangan yang telah ditentukan (terlihat di antarmuka) atau batas tetap objek yang dapat dipakai
  • Jika berguna, kelas dapat berisi fungsi publik yang akan diulang melalui array dan memanggil fungsi semua objek instantiated (RunAll () memanggil setiap Run (oHandle)
  • Fungsi Deinit (ClassHandle h) membebaskan memori yang dialokasikan (indeks array) dalam strategi alokasi dinamis

Adakah yang melihat masalah, lubang, potensi jebakan, atau manfaat / kelemahan tersembunyi dari salah satu variasi pendekatan ini? Jika saya menemukan kembali metode desain (dan saya kira saya harus), bisakah Anda mengarahkan saya ke nama metode itu?

Ben Gartner
sumber
Sebagai gaya, jika Anda memiliki informasi untuk ditambahkan ke pertanyaan Anda, Anda harus mengedit pertanyaan Anda untuk memasukkan informasi ini.
Chris Lutz
Anda tampaknya telah pindah dari malloc secara dinamis mengalokasikan dari tumpukan besar ke ClassInit () memilih secara dinamis dari kumpulan ukuran tetap, daripada benar-benar melakukan apa pun tentang apa yang akan terjadi ketika Anda meminta objek lain dan tidak memiliki sumber daya untuk menyediakannya .
Pete Kirkham
Ya, beban manajemen memori dialihkan ke kode yang memanggil ClassInit () untuk memeriksa apakah pegangan yang dikembalikan valid. Pada dasarnya kami telah membuat tumpukan khusus kami untuk kelas. Tidak yakin saya melihat cara untuk menghindari ini jika kami ingin melakukan alokasi dinamis, kecuali kami menerapkan tumpukan tujuan umum. Saya lebih suka mengisolasi risiko yang diturunkan dalam heap ke satu kelas.
Ben Gartner
3

Lihat juga jawaban ini dan yang ini

Itu mungkin. Itu selalu tampak seperti ide bagus pada saat itu tetapi setelah itu menjadi mimpi buruk pemeliharaan. Kode Anda menjadi penuh dengan potongan-potongan kode yang mengikat semuanya. Seorang programmer baru akan memiliki banyak masalah dalam membaca dan memahami kode jika Anda menggunakan pointer fungsi karena tidak akan jelas fungsi apa yang disebut.

Menyembunyikan data dengan fungsi get / set mudah diterapkan di C tetapi berhenti di situ. Saya telah melihat beberapa upaya dalam hal ini di lingkungan tertanam dan pada akhirnya selalu merupakan masalah pemeliharaan.

Karena kalian semua siap memiliki masalah pemeliharaan saya akan menghindari.

Gerhard
sumber
2

Pendekatan saya adalah memindahkan structdan semua fungsi yang terkait utamanya ke file sumber yang terpisah sehingga dapat digunakan "dengan mudah".

Bergantung pada kompiler Anda, Anda mungkin dapat memasukkan fungsi ke dalam struct, tetapi itu adalah ekstensi yang sangat spesifik compiler, dan tidak ada hubungannya dengan versi terakhir dari standar yang saya gunakan secara rutin :)

warren
sumber
2
Pointer fungsi semuanya baik. Kami cenderung menggunakannya untuk mengganti pernyataan switch besar dengan tabel pencarian.
Ben Gartner
2

Kompiler c ++ pertama sebenarnya adalah preprocessor yang menerjemahkan kode C ++ ke C.

Jadi sangat mungkin untuk memiliki kelas dalam C. Anda dapat mencoba dan menggali preprocessor C ++ lama dan melihat solusi apa yang dibuatnya.

Kodok
sumber
Itu akan menjadi cfront; itu mengalami masalah ketika pengecualian ditambahkan ke C ++ - penanganan pengecualian tidak sepele.
Jonathan Leffler
2

GTK dibangun sepenuhnya di atas C dan menggunakan banyak konsep OOP. Saya telah membaca kode sumber GTK dan itu sangat mengesankan, dan jelas lebih mudah dibaca. Konsep dasarnya adalah bahwa setiap "kelas" hanyalah sebuah struct, dan fungsi statis yang terkait. Fungsi statis semua menerima struct "instance" sebagai parameter, melakukan apa pun yang diperlukan, dan mengembalikan hasil jika perlu. Misalnya, Anda mungkin memiliki fungsi "GetPosition (CircleStruct obj)". Fungsi ini hanya akan menggali melalui struct, mengekstrak angka posisi, mungkin membangun objek PositionStruct baru, menempelkan x dan y di PositionStruct baru, dan mengembalikannya. GTK bahkan mengimplementasikan pewarisan seperti ini dengan menanamkan struct di dalam struct. cukup pintar.

rocketsarefast
sumber
1

Apakah Anda ingin metode virtual?

Jika tidak maka Anda hanya mendefinisikan satu set fungsi pointer di struct itu sendiri. Jika Anda menetapkan semua fungsi pointer ke fungsi C standar maka Anda akan dapat memanggil fungsi dari C dalam sintaksis yang sangat mirip dengan bagaimana Anda akan di bawah C ++.

Jika Anda ingin memiliki metode virtual itu menjadi lebih rumit. Pada dasarnya Anda harus mengimplementasikan VTable Anda sendiri untuk setiap struct dan menetapkan pointer fungsi ke VTable tergantung pada fungsi yang dipanggil. Anda kemudian akan memerlukan satu set pointer fungsi dalam struct itu sendiri yang pada gilirannya memanggil pointer fungsi di VTable. Inilah, pada dasarnya, apa yang dilakukan C ++.

TBH meskipun ... jika Anda menginginkan yang terakhir maka Anda mungkin lebih baik hanya menemukan kompiler C ++ yang dapat Anda gunakan dan kompilasi ulang proyek. Saya tidak pernah mengerti obsesi dengan C ++ tidak dapat digunakan dalam embedded. Saya sudah sering menggunakannya dan berfungsi cepat dan tidak memiliki masalah memori. Tentu Anda harus sedikit lebih berhati-hati tentang apa yang Anda lakukan tetapi sebenarnya tidak terlalu rumit.

Goz
sumber
Saya sudah mengatakannya dan akan mengatakannya lagi, tetapi akan mengatakannya lagi: Anda tidak perlu pointer fungsi atau kemampuan untuk memanggil fungsi dari struct C ++ style untuk membuat OOP di C, OOP kebanyakan tentang pewarisan fungsionalitas dan variabel (konten) yang keduanya dapat dicapai dalam C tanpa pointer fungsi atau kode duplikat.
yyny
0

C bukan bahasa OOP, seperti yang Anda tunjukkan dengan benar, jadi tidak ada cara bawaan untuk menulis kelas yang benar. Anda bertaruh terbaik adalah dengan melihat struct , dan pointer fungsi , ini akan membiarkan Anda membangun perkiraan sebuah kelas. Namun, karena C bersifat prosedural, Anda mungkin ingin mempertimbangkan untuk menulis lebih banyak kode mirip-C (yaitu tanpa mencoba menggunakan kelas).

Juga, jika Anda bisa menggunakan C, Anda bisa menggunakan C ++ dan mendapatkan kelas.

Benjamin
sumber
4
Saya tidak akan downvote, tetapi FYI, pointer fungsi, atau kemampuan untuk memanggil fungsi dari struct (yang saya kira adalah niat Anda) tidak ada hubungannya dengan OOP. OOP sebagian besar tentang pewarisan fungsionalitas dan variabel, yang keduanya dapat dicapai dalam C tanpa pointer fungsi atau duplikasi.
yyny