การปรับขนาดของตัวแปร“ จริง” มีประโยชน์อย่างไร?


9

สิ่งหนึ่งที่ทำให้ฉันรู้สึกว่าคุณสมบัติเชิงบวกของ C (จริง ๆ แล้วการใช้งานเช่น gcc, clang, ... ) คือความจริงที่ว่ามันไม่ได้เก็บข้อมูลที่ซ่อนอยู่ถัดจากตัวแปรของคุณที่รันไทม์ โดยสิ่งนี้ฉันหมายความว่าหากคุณต้องการตัวแปร "x" ของประเภท "uint16_t" คุณสามารถมั่นใจได้ว่า "x" จะใช้พื้นที่ 2 ไบต์เท่านั้น (และจะไม่นำข้อมูลที่ซ่อนอยู่เช่นประเภท ฯลฯ มาใช้) .) ในทำนองเดียวกันถ้าคุณต้องการอาเรย์ของจำนวนเต็ม 100 จำนวนเต็มคุณอาจมั่นใจได้ว่ามันมีขนาดใหญ่เท่ากับจำนวนเต็ม 100 ตัว

แต่ยิ่งฉันพยายามที่จะเกิดขึ้นกับกรณีการใช้งานที่เป็นรูปธรรมสำหรับคุณลักษณะนี้มากขึ้นผมกำลังสงสัยว่าถ้าเป็นจริงมีใด ๆข้อได้เปรียบในทางปฏิบัติที่ทุกคน สิ่งเดียวที่ฉันสามารถขึ้นมาเพื่อให้ห่างไกลก็คือว่ามันเห็นได้ชัดว่าต้องการ RAM น้อย สำหรับสภาพแวดล้อมที่ จำกัด เช่นชิป AVR เป็นต้นนี่เป็นข้อดีอย่างมาก แต่สำหรับกรณีการใช้งานเดสก์ท็อป / เซิร์ฟเวอร์ทุกวันดูเหมือนว่าจะไม่เกี่ยวข้องเลย ความเป็นไปได้อีกอย่างหนึ่งที่ฉันคิดคืออาจเป็นประโยชน์ / สำคัญสำหรับการเข้าถึงฮาร์ดแวร์หรืออาจทำแผนที่ภูมิภาคของหน่วยความจำ (เช่นสำหรับเอาต์พุต VGA และอื่น ๆ ) ... ?

คำถามของฉัน: มีโดเมนที่เป็นรูปธรรมใดที่ไม่สามารถใช้งานได้หรือยุ่งยากเพียงอย่างเดียวหากไม่มีคุณสมบัตินี้

ป.ล. โปรดบอกฉันถ้าคุณมีชื่อที่ดีกว่าสำหรับมัน! ;)



@gnat ฉันคิดว่าฉันเข้าใจว่าปัญหาของคุณคืออะไร เป็นเพราะอาจมีหลายคำตอบใช่มั้ย ดีฉันได้รับว่าคำถามนี้อาจไม่เหมาะกับวิธีการทำงานของ stackexchange แต่ผมไม่ทราบว่าจะถาม elsewise ...
โทมัส Oltmann

1
@lxrec RTTI ถูกเก็บไว้ใน vtable และวัตถุจะเก็บตัวชี้ไปยัง vtable เท่านั้น นอกจากนี้ชนิดจะมี RTTI เท่านั้นหากมี vtable อยู่แล้วเนื่องจากมีvirtualฟังก์ชันสมาชิก ดังนั้น RTTI จึงไม่เพิ่มขนาดของวัตถุใด ๆ มันทำให้ไบนารีมีขนาดใหญ่ขึ้นโดยค่าคงที่เท่านั้น

3
@ThomasOltmann ทุกวัตถุที่มีวิธีเสมือนต้องใช้ตัวชี้ vtable คุณไม่สามารถใช้วิธีการเสมือนได้หากไม่มี นอกจากนี้คุณเลือกที่จะมีวิธีการเสมือน (และเป็น vtable) อย่างชัดเจน

1
@ThomasOltmann คุณดูสับสนมาก มันไม่ใช่ตัวชี้ไปยังวัตถุที่มีตัวชี้ vtable แต่เป็นตัววัตถุ คือT *อยู่เสมอขนาดเดียวกันและTอาจมีข้อมูลที่ซ่อนอยู่ที่จุดที่จะต้อง vtable และไม่มีคอมไพเลอร์ C ++ ที่เคยใส่ vtables ลงในวัตถุที่ไม่ต้องการ

คำตอบ:


5

มีประโยชน์หลายประการที่ชัดเจนคือในเวลารวบรวมเพื่อให้แน่ใจว่าสิ่งต่าง ๆ เช่นพารามิเตอร์ฟังก์ชั่นตรงกับค่าที่ถูกส่งผ่าน

แต่ฉันคิดว่าคุณกำลังถามว่าเกิดอะไรขึ้นที่รันไทม์

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

สิ่งที่รันไทม์นั้นแตกต่างจากที่คุณคิด

ตัวอย่างเช่นอย่าสมมติว่าใช้สองไบต์เท่านั้นเมื่อคุณประกาศ uint16_t ขึ้นอยู่กับหน่วยประมวลผลกลางและการจัดตำแหน่งคำที่สามารถครอบครอง 16, 32 หรือ 64 บิตบนสแต็ก คุณอาจพบว่ากางเกงขาสั้นของคุณใช้หน่วยความจำมากกว่าที่คุณคาดไว้มาก

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

C ช่วยให้คุณระบุ structs ด้วยความละเอียดระดับบิต:

struct myMessage {
  uint8_t   first_bit: 1;
  uint8_t   second_bit: 1;
  uint8_t   padding:6;
  uint16_t  somethingUseful;
}

โครงสร้างนี้มีความยาวสามไบต์พร้อมกำหนดสั้น ๆ เพื่อเริ่มต้นที่ออฟเซ็ตคี่ มันจะต้องได้รับการบรรจุเพื่อให้ตรงตามที่คุณกำหนดไว้ มิฉะนั้นคอมไพเลอร์จะจัดเรียงคำสมาชิก

คอมไพเลอร์จะสร้างโค้ดเบื้องหลังเพื่อแยกข้อมูลนี้และคัดลอกลงทะเบียนเพื่อให้คุณสามารถทำสิ่งที่มีประโยชน์ได้

ตอนนี้คุณสามารถเห็นได้ว่าทุกครั้งที่โปรแกรมของฉันเข้าถึงสมาชิกของโครงสร้าง myMessage มันจะรู้ได้อย่างชัดเจนถึงวิธีแยกและจัดการมัน

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

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


2
"คุณอาจพบว่ากางเกงขาสั้นของคุณใช้หน่วยความจำมากกว่าที่คุณคาดไว้" นี่เป็นสิ่งที่ผิดใน C: อาร์เรย์รับประกันว่าจะมีองค์ประกอบของพวกเขาในแบบที่ไม่มีช่องว่าง ใช่อาร์เรย์จำเป็นต้องจัดตำแหน่งอย่างถูกต้องเช่นเดียวshortกับ แต่นี่เป็นข้อกำหนดเพียงครั้งเดียวสำหรับการเริ่มต้นของอาเรย์ส่วนที่เหลือจะได้รับการจัดตำแหน่งอย่างถูกต้องโดยอัตโนมัติโดยอาศัยการติดต่อกัน
cmaster - คืนสถานะโมนิก้า

นอกจากนี้ไวยากรณ์สำหรับการแพ็ดนั้นไม่ถูกต้องมันควรจะuint8_t padding: 6;เหมือนกับสองบิตแรก //6 bits of padding inserted by the compilerหรือชัดเจนมากขึ้นเพียงแค่แสดงความคิดเห็น โครงสร้างตามที่คุณเขียนมีขนาดอย่างน้อยเก้าไบต์ไม่ใช่สาม
cmaster - คืนสถานะโมนิ

9

คุณมีเหตุผลเพียงหนึ่งเดียวที่มีประโยชน์: การทำแผนที่โครงสร้างข้อมูลภายนอก สิ่งเหล่านี้รวมถึงบัฟเฟอร์วิดีโอที่แมปหน่วยความจำการลงทะเบียนฮาร์ดแวร์ฯลฯ นอกจากนี้ยังรวมถึงข้อมูลที่ส่งไม่เป็นอันตรายภายนอกโปรแกรมเช่นใบรับรอง SSL, IP แพ็คเก็ต, ภาพ JPEG และโครงสร้างข้อมูลอื่น ๆ ที่มีอายุการใช้งานยาวนาน


5

C เป็นภาษาระดับต่ำเกือบจะเป็นชุดประกอบแบบพกพาดังนั้นโครงสร้างข้อมูลและโครงสร้างภาษาจึงใกล้เคียงกับโลหะ (โครงสร้างข้อมูลไม่มีค่าใช้จ่ายแอบแฝงยกเว้นการเว้นระยะห่างการจัดตำแหน่งและขนาดที่กำหนดโดยฮาร์ดแวร์และABI ) ดังนั้น C แน่นอนไม่มีการพิมพ์แบบไดนามิก แต่ถ้าคุณต้องการคุณสามารถยอมรับข้อตกลงที่ว่าค่าทั้งหมดของคุณเป็นผลรวมเริ่มต้นด้วยข้อมูลประเภท (เช่นบางenum... ); ใช้union-s และ (สำหรับอาร์เรย์เหมือนสิ่ง) สมาชิกของอาร์เรย์ที่มีความยืดหยุ่นในการstructที่มียังมีขนาดของอาร์เรย์

(เมื่อการเขียนโปรแกรมใน C มันเป็นความรับผิดชอบของคุณเพื่อกำหนดเอกสารและตามธรรมเนียมปฏิบัติที่มีประโยชน์ - เงื่อนไขสะดุดตาก่อนและหลังและค่าคงที่; ยังจัดสรร C หน่วยความจำแบบไดนามิกต้อง expliciting การประชุมเกี่ยวกับผู้ที่ควรfreeheap- บางmallocโซนหน่วยความจำ ated)

ดังนั้นเพื่อเป็นตัวแทนของค่าที่เป็นจำนวนเต็มชนิดบรรจุกล่องหรือสตริงหรือสัญลักษณ์บางอย่างของScheme - like หรือเวกเตอร์ของค่าคุณจะใช้แนวคิดที่ติดแท็กสหภาพ (ดำเนินการเป็นสหภาพของพอยน์เตอร์) - ทางเริ่มต้นโดยชนิด -, เช่น:

enum value_kind_en {V_NONE, V_INT, V_STRING, V_SYMBOL, V_VECTOR};
union value_en { // this union takes a word in memory
   const void* vptr; // generic pointer, e.g. to free it
   enum value_kind_en* vkind; // the value of *vkind decides which member to use
   struct intvalue_st* vint;
   struct strvalue_st* vstr;
   struct symbvalue_st* vsymb;
   struct vectvalue_st* vvect;
};
typedef union value_en value_t;
#define NULL_VALUE  ((value_t){NULL})
struct intvalue_st {
  enum value_kind_en kind; // always V_INT for intvalue_st
  int num;
};
struct strvalue_st {
  enum value_kind_en kind; // always V_STRING for strvalue_st
  const char*str;
};
struct symbvalue_st {
  enum value_kind_en kind; // V_SYMBOL
  struct strvalue_st* symbname;
  value_t symbvalue;
};
struct vectvalue_st {
  enum value_kind_en kind; // V_VECTOR;
  unsigned veclength;
  value_t veccomp[]; // flexible array of veclength components.
};

เพื่อให้ได้ชนิดไดนามิกของค่าบางอย่าง

enum value_kind_en value_type(value_t v) {
  if (v.vptr != NULL) return *(v.vkind);
  else return V_NONE;
}

นี่คือ "การส่งแบบไดนามิก" ไปยังเวกเตอร์:

struct vectvalue_st* dyncast_vector (value_t v) {
   if (value_type(v) == V_VECTOR) return v->vvect;
   else return NULL;
}

และ "safe accessor" ภายในเวกเตอร์:

value_t vector_nth(value_t v, unsigned rk) {
   struct vectvalue_st* vecp = dyncast_vector(v);
   if (vecp && rk < vecp->veclength) return vecp->veccomp[rk];
   else return NULL_VALUE;
}

โดยทั่วไปคุณจะกำหนดฟังก์ชั่นสั้น ๆ ส่วนใหญ่ด้านบนเช่นเดียวกับstatic inlineในไฟล์ส่วนหัว

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

value_t make_vector(unsigned size, ... /*value_t arguments*/) {
   struct vectvalue_st* vec = GC_MALLOC(sizeof(*vec)+size*sizeof(value));
   vec->kind = V_VECTOR;
   va_args args;
   va_start (args, size);
   for (unsigned ix=0; ix<size; ix++) 
     vec->veccomp[ix] = va_arg(args,value_t);
   va_end (args);
   return (value_t){vec};
}

และถ้าคุณมีสามตัวแปร

value_t v1 = somevalue(), v2 = otherval(), v3 = NULL_VALUE;

คุณสามารถสร้างเวกเตอร์จากพวกเขาโดยใช้ make_vector(3,v1,v2,v3)

หากคุณไม่ต้องการใช้ถังเก็บขยะของ Boehm (หรือออกแบบเอง) คุณควรระวังอย่างมากเกี่ยวกับการกำหนด destructors และจัดทำเอกสารใครอย่างไรและเมื่อไรควรจะมีหน่วยความจำfree-d; ดูนี้ตัวอย่างเช่น ดังนั้นคุณสามารถใช้malloc(แต่ทดสอบกับความล้มเหลว) แทนGC_MALLOCข้างบน แต่คุณต้องกำหนดและใช้ฟังก์ชัน destructor อย่างระมัดระวังvoid destroy_value(value_t)

จุดแข็งของ C คืออยู่ในระดับต่ำพอที่จะทำให้โค้ดดังกล่าวเป็นไปได้และกำหนดอนุสัญญาของคุณเอง (โดยเฉพาะกับซอฟต์แวร์ของคุณ)


ฉันคิดว่าคุณเข้าใจคำถามของฉันผิด ฉันไม่ต้องการให้พิมพ์แบบไดนามิกใน C. ฉันอยากรู้ว่าคุณสมบัติเฉพาะของ C นี้ใช้งานได้จริงหรือไม่
โทมัส Oltmann

แต่คุณหมายถึงคุณสมบัติใดของ C โครงสร้างข้อมูล C อยู่ใกล้กับโลหะดังนั้นจึงไม่มีค่าใช้จ่ายแอบแฝง (ยกเว้นการจัดตำแหน่งและข้อ จำกัด ด้านขนาด)
Basile Starynkevitch

ตรงนั้น: /
Thomas Oltmann

C ถูกประดิษฐ์ขึ้นเป็นภาษาระดับต่ำ แต่เมื่อการปรับให้เหมาะสมเปิดใช้คอมไพเลอร์เช่นกระบวนการ gcc ภาษาที่ใช้ไวยากรณ์ระดับต่ำ แต่ไม่ได้ให้การเข้าถึงระดับต่ำในการรับประกันพฤติกรรมที่ให้แพลตฟอร์มอย่างเชื่อถือได้ หนึ่งต้องการขนาดของการใช้ malloc และ memcpy แต่การใช้สำหรับการคำนวณที่อยู่ของนักเล่นอาจไม่ได้รับการสนับสนุนใน "modern" C.
supercat
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.