Nesneye yönelik C yazmak kötü mü? [kapalı]


14

Her zaman çoğunlukla nesne yönelimli C kodu yazmak gibi görünüyor, bu yüzden ben bir yapı dosyası veya bir yapı oluşturmak istiyorsunuz bir şey vardı sonra bu yapıya sahip işlevler (yöntemler) için işaretçiyi iletmek demek:

struct foo {
    int x;
};

struct foo* createFoo(); // mallocs foo

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

Bu kötü bir uygulama mı? C'yi "doğru yol" yazmayı nasıl öğrenirim?


10
Linux'un (çekirdek) çoğu bu şekilde yazılmıştır, aslında sanal yöntem dağıtımı gibi OO benzeri kavramları bile taklit eder. Bunu oldukça uygun buluyorum.
Kilian Foth

13
" [T] Gerçek Programcı'nın herhangi bir dilde FORTRAN programları yazabileceğini belirledi. " - Ed Post, 1983
Ross Patterson

4
C ++ 'a geçmek istememenizin bir nedeni var mı? Sevmediğin kısımlarını kullanmak zorunda değilsin.
svick

5
Bu gerçekten "Nesne yönelimli" nedir? " Bu cisme yönelimli demem. Bunun prosedürel olduğunu söyleyebilirim. (Kalıtımınız yok, polimorfizminiz yok, kapsülleme / devleti gizleme kabiliyetiniz yok ve muhtemelen OO'nun başımın tepesinden çıkmayan diğer ayırt edici özellikleri eksik.) İyi veya kötü uygulamanın bu semantiğe bağlı olmadığı. , rağmen.
jpmc26

3
@ jpmc26: Eğer dilbilimsel bir kuralcı iseniz, Alan Kay'ı dinlemelisiniz, terimi icat etti, ne anlama geldiğini söyler ve OOP'un tamamen Mesajlaşma ile ilgili olduğunu söyler . Dilbilimsel bir tanımlayıcıysanız, terimin yazılım geliştirme topluluğunda kullanımını araştırırsınız. Cook tam olarak bunu yaptı, OO olduğunu iddia eden veya OO olarak kabul edilen dillerin özelliklerini analiz etti ve ortak bir şeyleri olduğunu gördü: Mesajlaşma .
Jörg W Mittag

Yanıtlar:


24

Hayır, bu bile tek bile gibi kuralları kullanmak her ne kadar bunu yapmak için teşvik edilmektedir kötü uygulama değildir struct foo *foo_new();vevoid foo_free(struct foo *foo);

Tabii ki, bir yorumun dediği gibi, bunu sadece uygun olduğunda yapın. Bir yapıcı kullanmanın bir anlamı yoktur int.

Önek foo_, bir çok kütüphanenin izlediği bir kuraldır, çünkü diğer kütüphanelerin isimlendirilmesiyle çatışmaya karşı koruma sağlar. Diğer işlevler genellikle kullanım kuralına sahiptir foo_<function>(struct foo *foo, <parameters>);. Bu struct fooopak bir tip olmanıza izin verir .

Özellikle "alt ad alanları" ile, kural için libcurl belgelerine bir göz atın , böylece curl_multi_*ilk parametre döndürüldüğünde bir işlevi çağırmak ilk bakışta yanlış görünür curl_easy_init().

Daha da genel yaklaşımlar var, bkz . ANSI-C ile Nesneye Yönelik Programlama


11
Daima "uygun olan yerlerde" uyarı ile. OOP gümüş bir kurşun değildir.
Tekilleştirici

C, bu işlevleri bildirebileceğiniz ad alanlarına sahip değil mi? Benzer std::string, olamazdı foo::create? C kullanmýyorum. Belki bu sadece C ++?
Chris Cirefice

@ChrisCirefice C'de bir ad alanı yoktur, bu nedenle birçok kütüphane yazarı işlevleri için önek kullanır.
Residuum

2

Kötü değil, mükemmel. Nesne Tabanlı Programlama (eğer kaptırıyorum sürece, iyi bir şeydir olabilir çok fazla iyi bir şey var). C, OOP için en uygun dil değildir, ancak bu, bundan en iyi şekilde yararlanmanızı engellememelidir.


4
Kabul edemediğim için değil, ama senin fikrin gerçekten bazı ayrıntılarla desteklenmeli .
Tekilleştirici

1

Fena değil. Birçok hatayı önleyen RAII kullanımını onaylar (bellek sızıntıları, başlatılmamış değişkenler kullanma, serbest bırakıldıktan sonra kullanma vb. Güvenlik sorunlarına neden olabilir).

Bu nedenle, kodunuzu yalnızca GCC veya Clang ile (ve MS derleyicisi ile değil) derlemek istiyorsanız cleanup, nesnelerinizi düzgün bir şekilde tahrip edecek niteliği kullanabilirsiniz . Nesnenizi böyle beyan ederseniz:

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

Sonra my_str_destructor(ptr)ptr kapsam dışına çıktığında çalıştırılacaktır. Sadece işlev argümanlarıyla kullanılamayacağını unutmayın .

Ayrıca, ad alanlarına sahip olmadığı my_str_için yöntem adlarınızda kullanmayı unutmayın Cve başka bir işlev adıyla çarpışmak kolaydır.


2
Afaik, RAII, temizliği sağlamak için C ++ içindeki nesneler için yıkıcıların örtülü olarak çağrılmasıyla ilgilidir ve açık kaynak sürüm çağrıları ekleme ihtiyacını ortadan kaldırır. Yani, çok yanılmıyorsam, RAII ve C birbirini dışlar.
cmaster - eski haline monica

Eğer varsa @cmaster #definesizin typenames kullanmak __attribute__((cleanup(my_str_destructor)))o zaman bütün örtük olarak alacak #definekapsamı (hepsi senin değişken bildirimleri eklenecektir).
Marqin

A) gcc kullanırsanız çalışır, b) türü yalnızca işlev yerel değişkenlerinde kullanırsanız ve c) türü yalnızca çıplak bir sürümde kullanırsanız ( #define'd türüne veya dizilerine işaretçi olmaz ). Kısacası: Standart C değil ve kullanımda çok fazla esneklik yok.
cmaster - reinstate monica

Cevabımda belirtildiği gibi, bu clang'da da işe yarıyor.
Marqin

Ah, bunu fark etmedim. Bu, a) gereksinimini __attribute__((cleanup()))hemen hemen yarı standart hale getirdiğinden, bu gereksinimi a) çok daha az ciddi hale getirir . Ancak, b) ve c) hala duruyor ...
cmaster - eski haline monica

-2

Böyle bir kodun birçok avantajı olabilir, ancak maalesef C Standardı bunu kolaylaştırmak için yazılmamıştır. Derleyiciler tarihsel olarak Standardın gerektirdiği şeyin ötesinde, bu kodun Standart C'de mümkün olandan çok daha temiz bir şekilde yazılmasını mümkün kılan etkili davranışsal garantiler sundu, ancak derleyiciler son zamanlarda bu garantileri optimizasyon adına iptal etmeye başladı.

En dikkat çekici olarak, birçok C derleyicisi tarihsel olarak (belgeleme değilse tasarım gereği), iki yapı tipi aynı başlangıç ​​dizisini içeriyorsa, türler ilgisiz olsa bile, bu ortak dizinin üyelerine erişmek için her iki türden bir işaretçinin kullanılabileceğini garanti etmiştir. ve ayrıca, ortak bir başlangıç ​​sekansı oluşturmak amacıyla, yapılara yönelik tüm göstergeler eşdeğerdir. Böyle bir davranışı kullanan kod, kullanmayan koddan çok daha temiz ve daha güvenli olabilir, ancak ne yazık ki Standart, ortak bir başlangıç ​​sırasını paylaşan yapıların aynı şekilde düzenlenmesini gerektirse de, kodun gerçekten kullanılmasını yasaklar bir diğerinin başlangıç ​​sırasına erişmek için bir tür işaretçi.

Sonuç olarak, C'ye nesne yönelimli kod yazmak istiyorsanız, C'nin işaretçi türü kurallarına uymak için çok fazla çembere atlamaya karar vermeniz (ve bu kararı erkenden vermeniz gerekir) ve modern derleyiciler, eski derleyiciler istendiği gibi çalışan bir kod üretmiş olsa bile, kayıyorsa saçma sapan kod üretir veya kodun yalnızca eski stil işaretçi davranışını destekleyecek şekilde yapılandırılmış derleyicilerle (örn. Bazı insanlar "-fno-katı-aliasing") Bazı insanlar "-fno-katı-aliasing" i kötülük olarak görürler, ancak "-fno-katı-aliasing" C'yi bir dil olarak düşünmenin daha yararlı olacağını söyleyebilirim. bazı amaçlar için "standart" C'den daha büyük anlamsal güç sunar,ancak başka amaçlar için önemli olabilecek optimizasyonlar pahasına.

Örneğin, geleneksel derleyicilerde, tarihsel derleyiciler aşağıdaki kodu yorumlayacaktır:

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

aşağıdaki adımları sırayla gerçekleştirerek: ilk üyesini artırın, ilk üyenin *pen düşük bitini tamamlayın *t, ardından ilk üyesini azaltın ve ilk üyesinin *pen düşük bitini tamamlayın *t. Modern derleyiciler, işlem sırasını farklı nesneler için daha verimli olacak pve ttanımlayacak bir kodla yeniden düzenler , ancak yapmazlarsa davranışı değiştirir.

Bu örnek elbette kasıtlı olarak konfirme edilmiştir ve pratikte, başka bir türün ortak başlangıç ​​sırasının bir parçası olan üyelere erişmek için bir tür işaretçiyi kullanan kod genellikle işe yarayacaktır, ancak ne yazık ki böyle bir kodun ne zaman başarısız olabileceğini bilmenin bir yolu yoktur. tür temelli örtüşme analizini devre dışı bırakmak dışında hiç güvenli bir şekilde kullanmak mümkün değildir.

İki işaretçi rasgele tiplere takas gibi bir şey yapmak için bir işlev yazmak isterse daha az çelişkili bir örnek ortaya çıkabilir. "1990'ların C" derleyicilerinin büyük çoğunluğunda, bu şu şekilde gerçekleştirilebilir:

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

Bununla birlikte Standart C'de, aşağıdakileri kullanmak gerekir:

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

Eğer *p2tahsis depoda tutulur ve geçici işaretçi tahsis depoda muhafaza edilmez, etkin tip *p2girişimleri kullanmak geçici pointer tipi ve kod olacak *p2geçici-işaretçi eşleşmedi herhangi türü olarak type Tanımsız Davranışı çağırır. Bir derleyicinin böyle bir şeyi fark edeceğinden son derece düşüktür, ancak modern derleyici felsefesi programcıların Tanımlanmamış Davranışlardan kaçınmasını gerektirdiğinden, tahsis edilmiş depolama alanı kullanmadan yukarıdaki kodu yazmanın başka güvenli yollarını düşünemiyorum. .


Downvoters: Yorum yapmak ister misiniz? Nesne yönelimli programlamanın temel bir yönü, birden çok türün ortak yönleri paylaşması ve bu tür herhangi bir öğeye işaret etmenin, bu ortak yönlere erişmek için kullanılabilmesidir. OP örneği bunu yapmaz, ancak "nesne yönelimli" olma yüzeyini zorlukla çizer. Tarihsel C derleyicileri, polimorfik kodun bugünün standart C'sinde mümkün olandan çok daha temiz bir şekilde yazılmasına izin verecekti. İnsanlara hangi açıdan katılmıyorum?
supercat

Hm ... standartın verdiği garantilerin ortak ilk alt dizinin üyelerine temiz bir şekilde erişmenize nasıl izin vermediğini gösterdiğinizi düşünüyor musunuz? Çünkü bence, sözleşmeye bağlı davranış sınırları içinde optimizasyon yapmaya cesaret edemeyen kötülükler hakkındaki rant bu sefer mi? (Bunun benim iki downvoters sakıncalı bulduğumuz tahmin.)
Deduplicator

OOP mutlaka kalıtım gerektirmez, bu nedenle iki yapı arasındaki uyumluluk pratikte çok önemli değildir. İşlev işaretçileri bir yapıya yerleştirerek ve bu işlevleri belirli bir şekilde çağırarak gerçek OO elde edebilirim. Tabii ki, foo_function(foo*, ...)C'deki bu sözde OO sadece sınıflara benzeyen belirli bir API stilidir, ancak soyut veri türleriyle modüler programlama olarak adlandırılmalıdır.
amon

@Deduplicator: Belirtilen örneğe bakın. "İ1" alanı her iki yapının ortak başlangıç ​​dizisinin bir üyesidir, ancak kod bir "struct trio" nun ilk üyesine erişmek için bir "struct pair *" kullanmaya çalışırsa modern derleyiciler başarısız olur.
supercat

Hangi modern C derleyicisi bu örnekte başarısız oluyor? İlginç seçeneklere ihtiyacınız var mı?
Deduplicator

-3

Bir sonraki adım, yapı bildirimini gizlemektir. Bunu .h dosyasına koydunuz:

typedef struct foo_s foo_t;

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

Ve sonra .c dosyasında:

struct foo_s {
    ...
};

6
Amaca bağlı olarak bu bir sonraki adım olabilir. Olsa da olmasa da, bu soruya uzaktan cevap vermeye bile çalışmaz.
Tekilleştirici
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.