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".
Jawaban:
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 digunakanfoo_<function>(struct foo *foo, <parameters>);
. Ini memungkinkan Andastruct foo
menjadi tipe buram.Lihat dokumentasi libcurl untuk konvensi, terutama dengan "ruangnama", sehingga memanggil fungsi
curl_multi_*
terlihat salah pada pandangan pertama saat parameter pertama dikembalikan olehcurl_easy_init()
.Bahkan ada pendekatan yang lebih umum, lihat Pemrograman Berorientasi Objek Dengan ANSI-C
sumber
std::string
, tidak bisakah Anda milikifoo::create
? Saya tidak menggunakan C. Mungkin itu hanya di C ++?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.
sumber
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: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, karenaC
tidak memiliki ruang nama, dan mudah untuk berbenturan dengan beberapa nama fungsi lainnya.sumber
#define
untuk digunakan__attribute__((cleanup(my_str_destructor)))
maka Anda akan mendapatkannya secara implisit dalam seluruh#define
cakupan (itu akan ditambahkan ke semua deklarasi variabel Anda).#define
tipe d atau array itu). Singkatnya: Ini bukan standar C, dan Anda membayar dengan banyak fleksibilitas dalam penggunaan.__attribute__((cleanup()))
cukup banyak standar kuasi. Namun, b) dan c) masih berdiri ...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:
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 jikap
dant
mengidentifikasi 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:
Namun, dalam Standar C, seseorang harus menggunakan:
Jika
*p2
disimpan dalam penyimpanan yang dialokasikan, dan penunjuk sementara tidak disimpan dalam penyimpanan yang dialokasikan, jenis efektif*p2
penunjuk akan menjadi jenis penunjuk sementara, dan kode yang mencoba untuk digunakan*p2
sebagai 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 .sumber
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.Langkah selanjutnya adalah menyembunyikan deklarasi struct. Anda menaruh ini di file .h:
Dan kemudian dalam file .c:
sumber