การเขียน object oriented C เป็นสิ่งที่ดีหรือไม่? [ปิด]


14

ฉันมักจะเขียนโค้ดใน C ที่ส่วนใหญ่เป็นเชิงวัตถุดังนั้นพูดว่าฉันมีไฟล์ต้นฉบับหรือสิ่งที่ฉันจะสร้าง struct แล้วผ่านตัวชี้ไปที่ struct นี้เพื่อฟังก์ชั่น (วิธีการ) เป็นเจ้าของโครงสร้างนี้:

struct foo {
    int x;
};

struct foo* createFoo(); // mallocs foo

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

นี่คือการปฏิบัติที่ไม่ดีหรือไม่? ฉันจะเรียนรู้วิธีการเขียน C ใน "วิธีการที่เหมาะสม" ได้อย่างไร


10
Linux (เคอร์เนล) ส่วนใหญ่เขียนด้วยวิธีนี้ในความเป็นจริงมันยังลอกเลียนแบบแนวคิด OO ที่มากขึ้นเช่นการแจกจ่ายเมธอดเสมือน ฉันคิดว่าเหมาะสมแล้ว
Kilian Foth

13
" [T] เขากำหนด Real Programmer สามารถเขียนโปรแกรม FORTRAN ในภาษาใดก็ได้ " - Ed Post, 1983
Ross Patterson

4
มีเหตุผลใดที่คุณไม่ต้องการเปลี่ยนเป็น C ++? คุณไม่จำเป็นต้องใช้ส่วนต่าง ๆ ของมันที่คุณไม่ชอบ
svick

5
สิ่งนี้ทำให้เกิดคำถามว่า "อะไรคือ" เชิงวัตถุ "? ฉันจะไม่เรียกวัตถุนี้ว่าอะไร ฉันจะบอกว่ามันเป็นขั้นตอน (คุณไม่มีการสืบทอดไม่มีความแตกต่างไม่มีการห่อหุ้ม / ความสามารถในการซ่อนสถานะและอาจขาดเครื่องหมายรับประกันคุณภาพอื่น ๆ ของ OO ที่ไม่หลุดพ้นจากศีรษะของฉัน) ไม่ว่าการปฏิบัติที่ดีหรือไม่ดีนั้นไม่ได้ขึ้นอยู่กับความหมายเหล่านั้น แม้ว่า
jpmc26

3
@ jpmc26: ถ้าคุณเป็น prescriptivist ภาษาที่คุณควรจะฟังอลันเคย์เขาคิดค้นคำว่าเขาได้รับที่จะบอกว่ามันหมายถึงอะไรและเขากล่าวว่าOOP คือทั้งหมดที่เกี่ยวกับการส่งข้อความ หากคุณเป็นนักอธิบายภาษาคุณจะสำรวจการใช้คำในชุมชนการพัฒนาซอฟต์แวร์ คุกทำอย่างนั้นเขาวิเคราะห์คุณสมบัติของภาษาที่อ้างว่าเป็น OO และเขาพบว่าพวกเขามีสิ่งหนึ่งที่เหมือนกันนั่นคือการส่งข้อความ
Jörg W Mittag

คำตอบ:


24

ไม่มีนี้ไม่ได้ปฏิบัติไม่ดีก็จะได้รับการสนับสนุนแม้จะทำเช่นนั้นแม้คนหนึ่งยังสามารถใช้การประชุมเหมือนstruct foo *foo_new();และvoid foo_free(struct foo *foo);

แน่นอนว่าในฐานะที่เป็นความคิดเห็นพูดเพียงทำสิ่งนี้ตามความเหมาะสม intมีความรู้สึกในการใช้คอนสตรัคสำหรับการไม่เป็น

คำนำหน้าfoo_เป็นธรรมเนียมปฏิบัติตามมาด้วยห้องสมุดจำนวนมากเพราะป้องกันการปะทะกันด้วยการตั้งชื่อห้องสมุดอื่น ๆ ฟังก์ชั่นอื่น ๆ foo_<function>(struct foo *foo, <parameters>);มักจะมีการประชุมที่จะใช้ สิ่งนี้ทำให้คุณstruct fooเป็นประเภททึบแสง

มีลักษณะที่เป็นเอกสาร libcurlสำหรับการประชุมโดยเฉพาะอย่างยิ่งกับ "subnamespaces" เพื่อให้การเรียกฟังก์ชั่นที่มีลักษณะผิดปกติตั้งแต่แรกเห็นเมื่อพารามิเตอร์แรกที่ถูกส่งกลับโดยcurl_multi_*curl_easy_init()

มีวิธีการทั่วไปเพิ่มเติมดูการเขียนโปรแกรมเชิงวัตถุด้วย ANSI-C


11
เสมอกับข้อแม้ "ตามความเหมาะสม" OOP ไม่ใช่กระสุนเงิน
Deduplicator

C ไม่มีเนมสเปซที่คุณสามารถประกาศฟังก์ชั่นเหล่านี้ได้หรือไม่? คล้ายกับที่std::stringคุณไม่มีfoo::createหรือ ฉันไม่ใช้ C. บางทีนั่นอาจเป็นเฉพาะใน C ++ ใช่ไหม
Chris Cirefice

@ChrisCirefice ไม่มีเนมสเปซใน C นั่นคือเหตุผลที่ผู้เขียนไลบรารีจำนวนมากใช้คำนำหน้าสำหรับฟังก์ชั่นของพวกเขา
Residuum

2

มันไม่เลวมันยอดเยี่ยม การเขียนโปรแกรมเชิงวัตถุเป็นสิ่งที่ดี (เว้นแต่คุณจะถูกพาออกไปคุณสามารถมีสิ่งที่ดีมากเกินไป) C ไม่ใช่ภาษาที่เหมาะสมที่สุดสำหรับ OOP แต่นั่นไม่ควรหยุดให้คุณได้รับสิ่งที่ดีที่สุด


4
ไม่ใช่ว่าฉันสามารถไม่เห็นด้วย แต่ความเห็นของคุณควรได้รับการสนับสนุนจากรายละเอียดบางอย่าง
Deduplicator

1

ไม่เลว. มันรับรองให้ใช้RAIIซึ่งป้องกันข้อบกพร่องมากมาย (การรั่วไหลของหน่วยความจำการใช้ตัวแปรที่ไม่ได้กำหนดค่าเริ่มต้นใช้งานหลังจากฟรีเป็นต้นซึ่งอาจทำให้เกิดปัญหาด้านความปลอดภัย)

ดังนั้นหากคุณต้องการคอมไพล์โค้ดของคุณด้วย GCC หรือ Clang (ไม่ใช่คอมไพเลอร์ MS) คุณสามารถใช้แอcleanupททริบิวซึ่งจะทำลายวัตถุของคุณอย่างถูกต้อง หากคุณประกาศวัตถุของคุณเช่นนั้น:

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

จากนั้นmy_str_destructor(ptr)จะถูกเรียกใช้เมื่อ ptr ออกนอกขอบเขต เพียงจำไว้ว่ามันไม่สามารถใช้กับอาร์กิวเมนต์ของฟังก์ชันได้

นอกจากนี้อย่าลืมใช้ my_str_ในชื่อเมธอดของคุณเพราะCไม่มีเนมสเปซและง่ายต่อการชนกับชื่อฟังก์ชั่นอื่น ๆ


2
Afaik, RAII กำลังใช้การโทรโดยอ้อมของ destructors สำหรับวัตถุใน C ++ เพื่อให้แน่ใจว่าล้างข้อมูลได้โดยไม่ต้องเพิ่มการเรียกใช้ทรัพยากรที่ชัดเจน ดังนั้นถ้าฉันไม่เข้าใจผิดมากนัก RAII และ C ต่างก็เป็นคนพิเศษ
cmaster - คืนสถานะโมนิก้า

@cmaster หากคุณพิมพ์ชื่อที่#defineจะใช้__attribute__((cleanup(my_str_destructor)))คุณจะได้รับมันโดยปริยายใน#defineขอบเขตทั้งหมด(มันจะถูกเพิ่มลงในการประกาศตัวแปรทั้งหมดของคุณ)
Marqin

ใช้งานได้หาก a) คุณใช้ gcc, b) หากคุณใช้ชนิดเฉพาะในฟังก์ชันตัวแปรท้องถิ่นและ c) หากคุณใช้ประเภทนี้เฉพาะในรุ่นที่ไม่มีการปิดบัง (ไม่มีตัวชี้ไปยัง#defineประเภท 'd' หรืออาร์เรย์ของมัน) ในระยะสั้น: มันไม่ได้มาตรฐาน C และคุณจ่ายด้วยความยืดหยุ่นในการใช้งานจำนวนมาก
cmaster - คืนสถานะโมนิก้า

ตามที่ระบุไว้ในคำตอบของฉันที่ยังใช้ในเสียงดังกราว
Marqin

อาฉันไม่ได้สังเกตว่า นั่นทำให้ข้อกำหนด a) มีความรุนแรงน้อยลงอย่างมากเนื่องจากทำให้__attribute__((cleanup()))มาตรฐานเสมือน อย่างไรก็ตาม b) และ c) ยังคงอยู่ ...
cmaster - คืนสถานะโมนิก้า

-2

มีข้อดีหลายประการสำหรับรหัสดังกล่าว แต่น่าเสียดายที่มาตรฐาน C ไม่ได้ถูกเขียนขึ้นเพื่ออำนวยความสะดวก คอมไพเลอร์ได้เสนอการรับประกันพฤติกรรมที่มีประสิทธิภาพเกินกว่ามาตรฐานที่กำหนดซึ่งทำให้สามารถเขียนโค้ดดังกล่าวได้อย่างเรียบร้อยกว่าในมาตรฐาน C แต่คอมไพเลอร์ได้เริ่มเพิกถอนการค้ำประกันดังกล่าวในนามของการปรับให้เหมาะสมที่สุด

ที่สะดุดตาที่สุดคอมไพเลอร์ C หลายคนมีการรับประกันในอดีต (โดยการออกแบบหากไม่มีเอกสารประกอบ) ว่าหากทั้งสองประเภทโครงสร้างมีลำดับเริ่มต้นเดียวกันตัวชี้ไปยังประเภทใดประเภทหนึ่งอาจถูกใช้เพื่อเข้าถึงสมาชิกของลำดับทั่วไปแม้ว่าประเภทนั้นจะไม่เกี่ยวข้อง และยิ่งไปกว่านั้นเพื่อจุดประสงค์ในการสร้างลำดับเริ่มต้นทั่วไปพอยน์เตอร์ทั้งหมดไปยังโครงสร้างนั้นเทียบเท่ากัน รหัสที่ใช้พฤติกรรมดังกล่าวอาจสะอาดกว่าและปลอดภัยกว่ารหัสที่ไม่ได้ แต่น่าเสียดายที่แม้ว่ามาตรฐานต้องการให้โครงสร้างที่ใช้ลำดับการใช้ร่วมกันเริ่มต้นร่วมกันต้องวางในลักษณะเดียวกันมันห้ามใช้รหัสจากการใช้งานจริง ตัวชี้ชนิดหนึ่งเพื่อเข้าถึงลำดับเริ่มต้นของอีกประเภทหนึ่ง

ดังนั้นหากคุณต้องการเขียนโค้ดเชิงวัตถุใน C คุณจะต้องตัดสินใจ (และควรทำการตัดสินใจก่อน) เพื่อข้ามห่วงจำนวนมากเพื่อปฏิบัติตามกฎตัวชี้ประเภทของ C และเตรียมพร้อมที่จะมี คอมไพเลอร์สมัยใหม่จะสร้างโค้ดแบบไร้สาระหากมีการเล็ดลอดขึ้นแม้ว่าคอมไพเลอร์รุ่นเก่าจะสร้างโค้ดที่ทำงานตามที่ตั้งใจไว้หรือมีการบันทึกเอกสารข้อกำหนดอื่น ๆ ที่รหัสจะสามารถใช้ได้กับคอมไพเลอร์ที่กำหนดค่าไว้ "-fno-เคร่งครัด - aliasing") บางคนคิดว่า "-fno-เคร่งครัด - aliasing" เป็นความชั่วร้าย แต่ฉันขอแนะนำว่ามันจะมีประโยชน์มากกว่าที่จะคิดว่า "-fno-เข้มงวด-aliasing" C เป็นภาษาที่ เสนอพลังความหมายมากขึ้นสำหรับวัตถุประสงค์บางอย่างมากกว่า "มาตรฐาน" Cแต่ด้วยค่าใช้จ่ายของการปรับให้เหมาะสมซึ่งอาจมีความสำคัญต่อวัตถุประสงค์อื่น

จากตัวอย่างในคอมไพเลอร์แบบดั้งเดิมคอมไพเลอร์เชิงประวัติจะตีความโค้ดต่อไปนี้:

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

เป็นขั้นตอนต่อไปในการสั่งซื้อ: เพิ่มสมาชิกคนแรกของ*pเสริมบิตต่ำสุดของสมาชิกคนแรกของ*tแล้วพร่องสมาชิกคนแรกของและเติมเต็มบิตต่ำสุดของสมาชิกคนแรกของ*p *tคอมไพเลอร์สมัยใหม่จะจัดลำดับของการดำเนินการใหม่ในแบบที่รหัสจะมีประสิทธิภาพมากขึ้นหากpและtระบุวัตถุที่แตกต่าง แต่จะเปลี่ยนพฤติกรรมหากพวกเขาไม่

ตัวอย่างนี้มีการวางแผนโดยเจตนาและในทางปฏิบัติรหัสซึ่งใช้ตัวชี้ประเภทหนึ่งเพื่อเข้าถึงสมาชิกที่เป็นส่วนหนึ่งของลำดับเริ่มต้นทั่วไปของประเภทอื่นมักจะงานได้ แต่น่าเสียดายที่ไม่มีวิธีรู้เมื่อรหัสดังกล่าวอาจล้มเหลว ไม่สามารถใช้งานได้อย่างปลอดภัยเลยยกเว้นการปิดใช้งานการวิเคราะห์นามแฝงแบบอิงประเภท

ตัวอย่างที่มีการประดิษฐ์น้อยกว่านั้นจะเกิดขึ้นหากต้องการเขียนฟังก์ชั่นเพื่อทำอะไรบางอย่างเช่นสลับสองพอยน์เตอร์เป็นประเภทใดก็ได้ ในคอมไพเลอร์ "1990s C" ส่วนใหญ่ที่สามารถทำได้ผ่าน:

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

อย่างไรก็ตามใน C มาตรฐานหนึ่งจะต้องใช้:

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

หาก*p2ถูกเก็บไว้ในที่เก็บข้อมูลที่จัดสรรและตัวชี้ชั่วคราวไม่ได้ถูกเก็บไว้ในที่จัดเก็บข้อมูลประเภทที่มีประสิทธิภาพ*p2จะกลายเป็นประเภทของตัวชี้ชั่วคราวและรหัสที่พยายามใช้*p2เป็นประเภทใด ๆ ที่ไม่ตรงกับตัวชี้ชั่วคราว ประเภทจะเรียกใช้พฤติกรรมที่ไม่ได้กำหนด เพื่อให้แน่ใจว่าไม่น่าเป็นไปได้อย่างยิ่งที่ผู้รวบรวมจะสังเกตเห็นสิ่งนี้ แต่เนื่องจากปรัชญาของคอมไพเลอร์สมัยใหม่ต้องการให้โปรแกรมเมอร์หลีกเลี่ยงพฤติกรรมที่ไม่ได้กำหนดเลยฉันไม่สามารถนึกถึงวิธีที่ปลอดภัยอื่น ๆ ในการเขียนรหัสข้างต้น .


Downvoters: สนใจที่จะแสดงความคิดเห็น? ลักษณะสำคัญของการเขียนโปรแกรมเชิงวัตถุคือความสามารถในการมีหลายชนิดที่ใช้ร่วมกันด้านทั่วไปและมีตัวชี้ไปยังประเภทใด ๆ ที่สามารถใช้งานได้เพื่อเข้าถึงลักษณะทั่วไปเหล่านั้น ตัวอย่างของ OP ไม่ได้ทำอย่างนั้น แต่มันแทบจะไม่ทำให้พื้นผิวของการเป็น "เชิงวัตถุ" คอมไพเลอร์ C ในอดีตจะอนุญาตให้เขียนรหัส polymorphic ในแบบที่สะอาดกว่าที่เป็นไปได้ในมาตรฐานของวันนี้ C การออกแบบโค้ดเชิงวัตถุใน C จึงต้องการให้กำหนดว่าภาษาใดที่ตรงเป้าหมาย ผู้คนไม่เห็นด้วยกับแง่มุมใด
supercat

หืมมม ... คุณแสดงให้เห็นว่าการรับรองมาตรฐานนั้นช่วยให้คุณเข้าถึงสมาชิกของลำดับย่อยเริ่มต้นทั่วไปได้อย่างไร เพราะฉันคิดว่านั่นเป็นสิ่งที่คุณโวยวายเกี่ยวกับความชั่วร้ายของความกล้าที่จะเพิ่มประสิทธิภาพภายในขอบเขตของพฤติกรรมตามสัญญาในเวลานี้? (นั่นคือสิ่งที่ฉันเดาว่าทั้งสอง downvoters พบว่าไม่เหมาะสม)
Deduplicator

OOP ไม่จำเป็นต้องมีการสืบทอดดังนั้นความเข้ากันได้ระหว่างสอง structs จึงไม่เป็นปัญหาในทางปฏิบัติ ฉันสามารถรับ OO จริงได้โดยใส่ตัวชี้ฟังก์ชันไว้ในโครงสร้างและเรียกใช้ฟังก์ชันเหล่านั้นในลักษณะเฉพาะ แน่นอนfoo_function(foo*, ...)หลอก -OO ใน C นี้เป็นเพียงรูปแบบ API เฉพาะที่เกิดขึ้นกับคลาส แต่มันควรจะเรียกว่าการเขียนโปรแกรมแบบแยกส่วนกับประเภทข้อมูลนามธรรม
amon

@Dupuplicator: ดูตัวอย่างที่ระบุ Field "i1" เป็นสมาชิกของลำดับเริ่มต้นทั่วไปของโครงสร้างทั้งสอง แต่คอมไพเลอร์สมัยใหม่จะล้มเหลวหากรหัสพยายามใช้ "struct pair *" เพื่อเข้าถึงสมาชิกเริ่มต้นของ "struct trio"
supercat

คอมไพเลอร์ C รุ่นใดที่ล้มเหลวตัวอย่างนั้น ต้องการตัวเลือกใดที่น่าสนใจ?
Deduplicator

-3

ขั้นต่อไปคือซ่อนการประกาศโครงสร้าง คุณใส่สิ่งนี้ลงในไฟล์. h:

typedef struct foo_s foo_t;

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

จากนั้นในไฟล์. c:

struct foo_s {
    ...
};

6
นั่นอาจเป็นขั้นตอนต่อไปขึ้นอยู่กับเป้าหมาย ไม่ว่าจะเป็นหรือไม่ก็ตามแม้จะพยายามตอบคำถามจากระยะไกลก็ตาม
Deduplicator
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.