Apakah buruk untuk menulis berorientasi objek C? [Tutup]

14

Saya sepertinya selalu menulis kode dalam C yang sebagian besar berorientasi objek, jadi katakanlah saya punya file sumber atau sesuatu yang saya akan buat struct lalu meneruskan pointer ke struct ini ke fungsi (metode) yang dimiliki oleh struktur ini:

struct foo {
    int x;
};

struct foo* createFoo(); // mallocs foo

void destroyFoo(struct foo* foo); // frees foo and its things

Apakah ini praktik buruk? Bagaimana saya belajar menulis C "cara yang tepat".

mosmo
sumber
10
Banyak Linux (kernel) ditulis dengan cara ini, bahkan malah meniru lebih banyak konsep mirip-OO seperti pengiriman metode virtual. Saya menganggap itu cukup tepat.
Kilian Foth
13
" [Dia] bertekad Programmer Nyata dapat menulis program FORTRAN dalam bahasa apa pun. " - Ed Post, 1983
Ross Patterson
4
Apakah ada alasan mengapa Anda tidak ingin beralih ke C ++? Anda tidak harus menggunakan bagian yang tidak Anda sukai.
svick
5
Ini benar-benar menimbulkan pertanyaan, "Apa itu 'berorientasi objek'?" Saya tidak akan menyebut objek ini berorientasi. Saya akan mengatakan itu prosedural. (Anda tidak memiliki warisan, tidak ada polimorfisme, tidak ada enkapsulasi / kemampuan untuk menyembunyikan keadaan, dan mungkin kehilangan ciri-ciri OO lainnya yang tidak keluar dari kepala saya.) Apakah praktik yang baik atau buruk tidak bergantung pada semantik tersebut meskipun begitu.
jpmc26
3
@ jpmc26: Jika Anda adalah seorang ahli bahasa linguistik, Anda harus mendengarkan Alan Kay, ia menemukan istilah itu, ia dapat mengatakan apa artinya, dan ia mengatakan OOP adalah tentang Pesan . Jika Anda seorang ahli bahasa linguistik, Anda akan mensurvei penggunaan istilah dalam komunitas pengembangan perangkat lunak. Cook melakukan hal itu, dia menganalisis fitur bahasa yang mengklaim atau dianggap sebagai OO, dan dia menemukan bahwa mereka memiliki satu kesamaan: Pesan .
Jörg W Mittag

Jawaban:

24

Tidak, ini bukan praktik yang buruk, bahkan dianjurkan untuk melakukannya, walaupun orang bahkan dapat menggunakan konvensi seperti struct foo *foo_new();danvoid foo_free(struct foo *foo);

Tentu saja, seperti komentar mengatakan, hanya lakukan ini jika perlu. Tidak ada gunanya menggunakan konstruktor untuk int.

Awalan foo_adalah konvensi yang diikuti oleh banyak perpustakaan, karena melindungi terhadap bentrok dengan penamaan perpustakaan lain. Fungsi lain sering kali memiliki konvensi untuk digunakan foo_<function>(struct foo *foo, <parameters>);. Ini memungkinkan Anda struct foomenjadi tipe buram.

Lihat dokumentasi libcurl untuk konvensi, terutama dengan "ruangnama", sehingga memanggil fungsi curl_multi_*terlihat salah pada pandangan pertama saat parameter pertama dikembalikan oleh curl_easy_init().

Bahkan ada pendekatan yang lebih umum, lihat Pemrograman Berorientasi Objek Dengan ANSI-C

Residuum
sumber
11
Selalu dengan peringatan "jika perlu". OOP bukan peluru perak.
Deduplicator
Bukankah C memiliki ruang nama tempat Anda dapat mendeklarasikan fungsi-fungsi ini? Mirip dengan std::string, tidak bisakah Anda miliki foo::create? Saya tidak menggunakan C. Mungkin itu hanya di C ++?
Chris Cirefice
@ChrisCirefice Tidak ada ruang nama di C, itu sebabnya banyak penulis perpustakaan menggunakan awalan untuk fungsinya.
Residuum
2

Itu tidak buruk, ini luar biasa. Pemrograman Berorientasi Objek adalah hal yang baik (kecuali Anda terbawa, Anda bisa memiliki terlalu banyak hal yang baik). C bukan bahasa yang paling cocok untuk OOP, tetapi itu tidak akan menghentikan Anda mendapatkan yang terbaik dari itu.

gnasher729
sumber
4
Bukan berarti saya bisa tidak setuju, tapi pendapat Anda benar-benar harus didukung oleh beberapa elaborasi.
Deduplicator
1

Itu tidak buruk. Itu mendukung untuk menggunakan RAII yang mencegah banyak bug (kebocoran memori, menggunakan variabel yang tidak diinisialisasi, digunakan setelah bebas dll. Yang dapat menyebabkan masalah keamanan).

Jadi, jika Anda ingin mengkompilasi kode Anda hanya dengan GCC atau Dentang (dan bukan dengan kompiler MS), Anda dapat menggunakan cleanup atribut, yang akan merusak objek Anda dengan benar. Jika Anda mendeklarasikan objek Anda seperti itu:

my_str __attribute__((cleanup(my_str_destructor))) ptr;

Kemudian my_str_destructor(ptr) akan dijalankan ketika ptr keluar dari ruang lingkup. Perlu diingat, bahwa itu tidak dapat digunakan dengan argumen fungsi .

Juga, ingatlah untuk menggunakan my_str_nama metode Anda, karena Ctidak memiliki ruang nama, dan mudah untuk berbenturan dengan beberapa nama fungsi lainnya.

Marqin
sumber
2
Afaik, RAII adalah tentang menggunakan panggilan implisit dari destruktor untuk objek di C ++ untuk memastikan pembersihan, menghindari kebutuhan untuk menambahkan panggilan rilis sumber daya eksplisit. Jadi, jika saya tidak banyak salah, RAII dan C saling eksklusif.
cmaster - mengembalikan monica
@ cmaster Jika Anda mengetikkan nama Anda #defineuntuk digunakan __attribute__((cleanup(my_str_destructor)))maka Anda akan mendapatkannya secara implisit dalam seluruh #definecakupan (itu akan ditambahkan ke semua deklarasi variabel Anda).
Marqin
Itu bekerja jika a) Anda menggunakan gcc, b) jika Anda menggunakan tipe hanya dalam variabel fungsi lokal, dan c) jika Anda menggunakan tipe hanya dalam versi telanjang (tidak ada pointer ke #definetipe d atau array itu). Singkatnya: Ini bukan standar C, dan Anda membayar dengan banyak fleksibilitas dalam penggunaan.
cmaster - mengembalikan monica
Seperti yang disebutkan dalam jawaban saya, itu juga berfungsi dalam dentang.
Marqin
Ah, saya tidak menyadarinya. Itu memang membuat persyaratan a) jauh lebih tidak parah, karena itu membuat __attribute__((cleanup()))cukup banyak standar kuasi. Namun, b) dan c) masih berdiri ...
cmaster - mengembalikan monica
-2

Mungkin ada banyak keuntungan dari kode tersebut, tetapi sayangnya Standar C tidak ditulis untuk memfasilitasinya. Kompiler secara historis menawarkan jaminan perilaku yang efektif di luar apa yang disyaratkan Standar yang memungkinkan untuk menulis kode seperti itu jauh lebih bersih daripada yang mungkin dalam Standar C, tetapi kompiler baru-baru ini mulai mencabut jaminan tersebut atas nama optimasi.

Yang paling penting, banyak kompiler C secara historis dijamin (dengan desain jika bukan dokumentasi) bahwa jika dua tipe struktur mengandung urutan awal yang sama, sebuah pointer ke kedua tipe dapat digunakan untuk mengakses anggota dari urutan umum tersebut, bahkan jika jenisnya tidak terkait, dan lebih lanjut bahwa untuk tujuan membangun urutan awal yang sama semua petunjuk ke struktur adalah setara. Kode yang memanfaatkan perilaku seperti itu bisa jauh lebih bersih dan lebih aman dari pada kode yang tidak, tetapi sayangnya meskipun Standar mensyaratkan bahwa struktur yang berbagi urutan awal yang sama harus ditata dengan cara yang sama, ia melarang kode untuk benar-benar menggunakan pointer dari satu jenis untuk mengakses urutan awal yang lain.

Akibatnya, jika Anda ingin menulis kode berorientasi objek dalam C, Anda harus memutuskan (dan harus membuat keputusan ini sejak awal) untuk melompati banyak rintangan untuk mematuhi aturan tipe pointer C dan bersiaplah untuk memiliki kompiler modern menghasilkan kode nonsensikal jika seseorang tergelincir, bahkan jika kompiler lama akan menghasilkan kode yang berfungsi sebagaimana dimaksud, atau mendokumentasikan persyaratan bahwa kode hanya akan dapat digunakan dengan kompiler yang dikonfigurasi untuk mendukung perilaku pointer gaya lama (misalnya menggunakan sebuah "-fno-strict-aliasing") Beberapa orang menganggap "-fno-strict-aliasing" sebagai kejahatan, tetapi saya menyarankan agar lebih membantu jika menganggap "-fno-strict-aliasing" C sebagai bahasa yang menawarkan kekuatan semantik yang lebih besar untuk beberapa tujuan daripada "standar" C,tetapi dengan mengorbankan optimasi yang mungkin penting untuk beberapa tujuan lain.

Sebagai contoh, pada kompiler tradisional, kompiler historis akan menginterpretasikan kode berikut:

struct pair { int i1,i2; };
struct trio { int i1,i2,i3; };

void hey(struct pair *p, struct trio *t)
{
  p->i1++;
  t->i1^=1;
  p->i1--;
  t->i1^=1;
}

sebagai melakukan langkah-langkah berikut secara berurutan: meningkatkan anggota pertama *p, melengkapi bit terendah dari anggota pertama *t, lalu mengurangi anggota pertama *p, dan melengkapi bit terendah dari anggota pertama *t. Kompiler modern akan mengatur ulang urutan operasi dengan cara kode mana yang akan lebih efisien jika pdan tmengidentifikasi objek yang berbeda, tetapi akan mengubah perilaku jika tidak.

Contoh ini tentu saja sengaja dibuat, dan dalam kode praktek yang menggunakan pointer dari satu jenis untuk mengakses anggota yang merupakan bagian dari urutan awal umum dari jenis lain biasanya akan bekerja, tetapi sayangnya karena tidak ada cara untuk mengetahui kapan kode tersebut mungkin gagal sama sekali tidak mungkin menggunakannya dengan aman kecuali dengan menonaktifkan analisis aliasing berbasis tipe.

Contoh agak kurang dibuat-buat akan terjadi jika seseorang ingin menulis fungsi untuk melakukan sesuatu seperti bertukar dua pointer ke tipe sewenang-wenang. Dalam sebagian besar kompiler "1990-an C", itu dapat dicapai melalui:

void swap_pointers(void **p1, void **p2)
{
  void *temp = *p1;
  *p1 = *p2;
  *p2 = temp;
}

Namun, dalam Standar C, seseorang harus menggunakan:

#include "string.h"
#include "stdlib.h"
void swap_pointers2(void **p1, void **p2)
{
  void **temp = malloc(sizeof (void*));
  memcpy(temp, p1, sizeof (void*));
  memcpy(p1, p2, sizeof (void*));
  memcpy(p2, temp, sizeof (void*));
  free(temp);
}

Jika *p2disimpan dalam penyimpanan yang dialokasikan, dan penunjuk sementara tidak disimpan dalam penyimpanan yang dialokasikan, jenis efektif *p2penunjuk akan menjadi jenis penunjuk sementara, dan kode yang mencoba untuk digunakan *p2sebagai jenis apa pun yang tidak cocok dengan penunjuk-sementara tipe akan memanggil Perilaku Tidak Terdefinisi. Sudah pasti sangat tidak mungkin bahwa kompiler akan memperhatikan hal seperti itu, tetapi karena filosofi kompiler modern mengharuskan pemrogram menghindari Perilaku Tidak Terdefinisi dengan cara apa pun, saya tidak dapat memikirkan cara aman lainnya untuk menulis kode di atas tanpa menggunakan penyimpanan yang dialokasikan .

supercat
sumber
Downvoters: Ingin berkomentar? Aspek kunci dari pemrograman berorientasi objek adalah kemampuan untuk memiliki beberapa tipe berbagi aspek yang sama, dan memiliki penunjuk ke tipe tersebut dapat digunakan untuk mengakses aspek-aspek umum tersebut. Contoh OP tidak melakukan itu, tetapi hampir tidak menggores permukaan menjadi "berorientasi objek". Kompiler C historis akan memungkinkan kode polimorfik ditulis dengan cara yang jauh lebih bersih daripada yang mungkin dalam standar saat ini C. Merancang kode berorientasi objek dalam C dengan demikian mengharuskan seseorang menentukan bahasa apa yang ditargetkan. Dengan aspek apa orang tidak setuju?
supercat
Hm ... dapatkah Anda menunjukkan bagaimana jaminan yang diberikan standar tidak memungkinkan Anda mengakses anggota sub-urutan awal yang umum? Karena saya pikir itu yang kata-kata kasar Anda tentang kejahatan berani untuk mengoptimalkan dalam batas-batas perilaku kontrak bergantung pada saat ini? (Itu dugaan saya apa yang ditemukan kedua downvotitor tidak menyenangkan.)
Deduplicator
OOP tidak selalu membutuhkan pewarisan, jadi kompatibilitas antara dua struct tidak banyak masalah dalam praktiknya. Saya bisa mendapatkan OO nyata dengan meletakkan pointer fungsi ke dalam sebuah struct, dan memanggil fungsi-fungsi itu dengan cara tertentu. Tentu saja, foo_function(foo*, ...)pseudo-OO dalam C ini hanyalah gaya API tertentu yang terlihat seperti kelas, tetapi seharusnya lebih tepat disebut pemrograman modular dengan tipe data abstrak.
amon
@Dupuplikator: Lihat contoh yang ditunjukkan. Field "i1" adalah anggota dari urutan awal umum kedua struktur, tetapi kompiler modern akan gagal jika kode mencoba menggunakan "struct pair *" untuk mengakses anggota awal dari "struct trio".
supercat
Kompiler C modern mana yang gagal contoh itu? Adakah opsi menarik yang dibutuhkan?
Deduplicator
-3

Langkah selanjutnya adalah menyembunyikan deklarasi struct. Anda menaruh ini di file .h:

typedef struct foo_s foo_t;

foo_t * foo_new(...);
void foo_destroy(foo_t *foo);
some_type foo_whatever(foo_t *foo, ...);
...

Dan kemudian dalam file .c:

struct foo_s {
    ...
};
Solomon Lambat
sumber
6
Itu mungkin langkah selanjutnya, tergantung tujuannya. Apakah itu atau tidak, ini bahkan tidak mencoba menjawab pertanyaan dari jarak jauh.
Deduplicator