หากฉันกำหนดตัวแปรประเภทหนึ่ง (ซึ่งเท่าที่ฉันรู้เพียงแค่จัดสรรข้อมูลสำหรับเนื้อหาของตัวแปร) มันจะติดตามชนิดของตัวแปรชนิดใดได้อย่างไร
หากฉันกำหนดตัวแปรประเภทหนึ่ง (ซึ่งเท่าที่ฉันรู้เพียงแค่จัดสรรข้อมูลสำหรับเนื้อหาของตัวแปร) มันจะติดตามชนิดของตัวแปรชนิดใดได้อย่างไร
คำตอบ:
ตัวแปร (หรือมากกว่านั้นโดยทั่วไป:“ ออบเจ็กต์” ในแง่ของ C) จะไม่เก็บชนิดของมันไว้ที่รันไทม์ เท่าที่เกี่ยวข้องกับรหัสเครื่องมีหน่วยความจำไม่ได้พิมพ์เพียงอย่างเดียว แต่การดำเนินการกับข้อมูลนี้ตีความข้อมูลว่าเป็นประเภทเฉพาะ (เช่นลอยหรือเป็นตัวชี้) ประเภทจะใช้โดยคอมไพเลอร์เท่านั้น
ตัวอย่างเช่นเราอาจจะมี struct หรือระดับและตัวแปรstruct Foo { int x; float y; };
Foo f {}
การเข้าถึงฟิลด์auto result = f.y;
จะรวบรวมได้อย่างไร? คอมไพเลอร์รู้ว่าf
เป็นวัตถุประเภทFoo
และรู้โครงร่างของFoo
-objects ขึ้นอยู่กับรายละเอียดเฉพาะแพลตฟอร์มซึ่งอาจรวบรวมเป็น“ นำตัวชี้ไปที่จุดเริ่มต้นf
เพิ่ม 4 ไบต์จากนั้นโหลด 4 ไบต์และตีความข้อมูลนี้เป็นแบบลอย” ในชุดคำสั่งรหัสเครื่องจำนวนมาก (รวม x86-64 ) มีคำแนะนำโปรเซสเซอร์แตกต่างกันสำหรับการโหลดลอยหรือ ints
ตัวอย่างหนึ่งที่ระบบการพิมพ์ C ++ union Bar { int as_int; float as_float; }
ไม่สามารถติดตามชนิดสำหรับเราเป็นสหภาพเช่น การรวมกันมีวัตถุได้หลายประเภท ถ้าเราเก็บวัตถุในสหภาพนี่คือประเภทที่ใช้งานของสหภาพ เราต้องพยายามดึงมันออกมาจากสหภาพ แต่อย่างอื่นจะมีพฤติกรรมที่ไม่ได้กำหนด ไม่ว่าเราจะ "รู้" ในขณะที่การเขียนโปรแกรมประเภทที่ใช้งานอยู่หรือเราสามารถสร้างสหภาพที่ติดแท็กที่เราเก็บแท็กประเภท (มักจะ enum) แยกต่างหาก นี่เป็นเทคนิคทั่วไปใน C แต่เนื่องจากเราต้องเก็บค่ายูเนี่ยนและแท็กประเภทให้ตรงกันจึงมีข้อผิดพลาดค่อนข้างง่าย void*
ชี้คล้ายกับสหภาพ แต่สามารถเก็บวัตถุชี้ยกเว้นคำแนะนำการทำงาน
C ++ มีกลไกที่ดีกว่าสองข้อในการจัดการกับวัตถุประเภทที่ไม่รู้จัก: เราสามารถใช้เทคนิคเชิงวัตถุเพื่อทำการลบประเภท (โต้ตอบเฉพาะกับวัตถุด้วยวิธีเสมือนเพื่อที่เราไม่จำเป็นต้องรู้ประเภทจริง) หรือเราสามารถ ใช้std::variant
ชนิดของการรวมที่ปลอดภัยชนิด
มีกรณีหนึ่งที่ C ++ จัดเก็บชนิดของวัตถุคือ: หากคลาสของวัตถุนั้นมีวิธีการเสมือนใด ๆ (“ ชนิด polymorphic”, aka. interface) เป้าหมายของการเรียกใช้เมธอดเสมือนไม่เป็นที่รู้จักในเวลารวบรวมและได้รับการแก้ไขในเวลาทำงานตามประเภทไดนามิกของวัตถุ (“ การกระจายแบบไดนามิก”) คอมไพเลอร์ส่วนใหญ่จะใช้สิ่งนี้โดยการจัดเก็บตารางฟังก์ชันเสมือน (“ vtable”) ที่จุดเริ่มต้นของวัตถุ vtable ยังสามารถใช้เพื่อรับชนิดของวัตถุที่รันไทม์ จากนั้นเราสามารถวาดความแตกต่างระหว่างชนิดสแตติกที่รู้จักกันในเวลาคอมไพล์ของนิพจน์และชนิดของวัตถุแบบไดนามิกที่รันไทม์
C ++ ช่วยให้เราสามารถตรวจสอบประเภทของวัตถุแบบไดนามิกกับtypeid()
ผู้ประกอบการซึ่งให้เราstd::type_info
วัตถุ คอมไพเลอร์รู้ชนิดของวัตถุ ณ เวลารวบรวมหรือคอมไพเลอร์เก็บข้อมูลประเภทที่จำเป็นภายในวัตถุและสามารถเรียกคืนได้ในขณะทำงาน
void*
)
typeid(e)
introspects e
ประเภทคงที่ของการแสดงออก หากประเภทสแตติกเป็นชนิด polymorphic นิพจน์จะได้รับการประเมินและจะดึงข้อมูลประเภทไดนามิกของวัตถุนั้น คุณไม่สามารถชี้ typeid ที่หน่วยความจำชนิดที่ไม่รู้จักและรับข้อมูลที่เป็นประโยชน์ เช่นประเภทของสหภาพอธิบายสหภาพไม่ใช่วัตถุในสหภาพ typeid ของ a void*
เป็นเพียงตัวชี้โมฆะ และมันเป็นไปไม่ได้ที่จะเอ่ยvoid*
ถึงเนื้อหาของมัน ใน C ++ ไม่มีการชกมวยเว้นแต่จะตั้งโปรแกรมไว้อย่างชัดเจน
คำตอบอื่น ๆ อธิบายด้านเทคนิคได้ดี แต่ฉันต้องการเพิ่ม "วิธีคิดเกี่ยวกับรหัสเครื่อง" โดยทั่วไป
รหัสเครื่องหลังจากการรวบรวมนั้นค่อนข้างโง่และมันก็แค่คิดว่าทุกอย่างทำงานตามที่ตั้งใจ สมมติว่าคุณมีฟังก์ชั่นที่เรียบง่ายเช่น
bool isEven(int i) { return i % 2 == 0; }
มันต้องใช้ int และคายบูลออกมา
หลังจากที่คุณรวบรวมมันคุณสามารถคิดว่ามันเป็นอะไรที่คล้ายกับเครื่องคั้นน้ำผลไม้สีส้มอัตโนมัตินี้:
มันใช้เวลาในส้มและส่งกลับน้ำผลไม้ มันรู้จักชนิดของวัตถุที่มันเข้าไปหรือไม่ ไม่พวกมันควรจะเป็นส้ม จะเกิดอะไรขึ้นถ้ามันได้รับแอปเปิ้ลแทนที่จะเป็นสีส้ม บางทีมันอาจจะพัง ไม่สำคัญเนื่องจากเจ้าของที่รับผิดชอบจะไม่พยายามใช้วิธีนี้
ฟังก์ชั่นด้านบนนั้นคล้ายกัน: มันถูกออกแบบมาเพื่อใช้ ints และมันอาจทำลายหรือทำสิ่งที่ไม่เกี่ยวข้องเมื่อให้อาหารอย่างอื่น มัน (ปกติ) ไม่สำคัญเพราะคอมไพเลอร์ (โดยทั่วไป) ตรวจสอบว่ามันไม่เคยเกิดขึ้น - และมันไม่เคยเกิดขึ้นในรหัสที่มีรูปแบบที่ดี หากคอมไพเลอร์ตรวจพบความเป็นไปได้ที่ฟังก์ชั่นจะได้รับค่าที่พิมพ์ผิดมันจะปฏิเสธที่จะรวบรวมรหัสและส่งกลับข้อผิดพลาดประเภทแทน
ข้อแม้คือว่ามีบางกรณีของรหัสที่ไม่ดีที่คอมไพเลอร์จะผ่าน ตัวอย่างคือ:
void*
ไปorange*
เมื่อมีแอปเปิ้ลในส่วนอื่น ๆ ของตัวชี้ดังที่ได้กล่าวมาแล้วรหัสที่คอมไพล์ก็เหมือนกับเครื่องคั้นน้ำผลไม้ - มันไม่รู้ว่ามันประมวลผลอะไร และถ้าคำแนะนำนั้นผิดมันก็จะแตก นั่นคือสาเหตุที่ปัญหาข้างต้นใน C ++ ส่งผลให้เกิดการล่มล้มเหลว
void*
coerces ของ C คือfoo*
การส่งเสริมการคำนวณทางคณิตศาสตร์ตามปกติunion
พิมพ์ punning NULL
และnullptr
แม้กระทั่งการมีตัวชี้ที่ไม่ดีคือ UB เป็นต้น แต่ฉันไม่คิดว่าการจดรายชื่อของสิ่งเหล่านั้นทั้งหมดจะช่วยปรับปรุงคำตอบของคุณอย่างมีนัยสำคัญ มันเป็นมันเป็น
void*
จะไม่แปลงเป็น implicitly foo*
และunion
ไม่รองรับประเภท punning (มี UB)
ตัวแปรมีคุณสมบัติพื้นฐานจำนวนหนึ่งในภาษาเช่น C:
ในซอร์สโค้ดของคุณตำแหน่ง (5) เป็นแนวคิดและตำแหน่งนี้ถูกอ้างอิงโดยชื่อ (1) ดังนั้นการประกาศตัวแปรที่ใช้ในการสร้างที่ตั้งและพื้นที่สำหรับค่า (6) และในบรรทัดอื่น ๆ ของแหล่งที่มาเราอ้างถึงสถานที่นั้นและค่าที่มันถือโดยการตั้งชื่อตัวแปรในการแสดงออกบางอย่าง
ลดความซับซ้อนเพียงเล็กน้อยเมื่อโปรแกรมของคุณถูกแปลเป็นรหัสเครื่องโดยคอมไพเลอร์, ตำแหน่ง (5), เป็นหน่วยความจำบางส่วนหรือตำแหน่งลงทะเบียน CPU และนิพจน์รหัสแหล่งใด ๆ ที่อ้างอิงตัวแปรจะถูกแปลเป็นลำดับรหัสเครื่อง หรือตำแหน่งลงทะเบียน CPU
ดังนั้นเมื่อการแปลเสร็จสิ้นและโปรแกรมทำงานบนโปรเซสเซอร์ชื่อของตัวแปรจะถูกลืมอย่างมีประสิทธิภาพภายในรหัสเครื่องและคำแนะนำที่สร้างโดยคอมไพเลอร์อ้างถึงตำแหน่งที่ได้รับมอบหมายของตัวแปรเท่านั้น ชื่อ) หากคุณกำลังดีบั๊กและขอให้ดีบั๊กตำแหน่งของตัวแปรที่เกี่ยวข้องกับชื่อจะถูกเพิ่มไปยังข้อมูลเมตาของโปรแกรมแม้ว่าตัวประมวลผลจะยังคงเห็นคำสั่งรหัสเครื่องโดยใช้ตำแหน่ง (ไม่ใช่ข้อมูลเมตานั้น) (นี่คือการทำให้เข้าใจง่ายเกินไปเนื่องจากบางชื่ออยู่ในข้อมูลเมตาของโปรแกรมเพื่อวัตถุประสงค์ในการเชื่อมโยงการโหลดและการค้นหาแบบไดนามิก - ตัวประมวลผลยังคงดำเนินการตามคำสั่งของรหัสเครื่องที่มีการแจ้งให้โปรแกรมทราบเท่านั้น ถูกแปลงเป็นสถานที่)
เช่นเดียวกันกับประเภทขอบเขตและอายุการใช้งาน คอมไพเลอร์ที่สร้างขึ้นคำแนะนำรหัสเครื่องรู้รุ่นของเครื่องของสถานที่ซึ่งเก็บค่า คุณสมบัติอื่น ๆ เช่นชนิดถูกรวบรวมไว้ในซอร์สโค้ดแปลเป็นคำแนะนำเฉพาะที่เข้าถึงตำแหน่งของตัวแปร ตัวอย่างเช่นหากตัวแปรในคำถามคือไบต์ 8 บิตที่ถูกเซ็นชื่อกับไบต์ 8 บิตที่ไม่ได้ลงนามนิพจน์ในซอร์สโค้ดที่อ้างอิงตัวแปรจะถูกแปลเป็นพูดโหลดไบต์ที่เซ็นชื่อเทียบกับโหลดไบต์ที่ไม่ได้ลงนาม ตามที่จำเป็นเพื่อให้เป็นไปตามกฎของภาษา (C) ชนิดของตัวแปรจึงถูกเข้ารหัสเป็นการแปลรหัสต้นฉบับลงในคำสั่งเครื่องซึ่งสั่งให้ CPU รู้วิธีตีความหน่วยความจำหรือตำแหน่งลงทะเบียน CPU ทุกครั้งที่ใช้ตำแหน่งของตัวแปร
สิ่งสำคัญคือเราต้องบอก CPU ว่าต้องทำอย่างไรผ่านคำแนะนำ (และคำแนะนำเพิ่มเติม) ในชุดคำสั่งรหัสเครื่องของโปรเซสเซอร์ หน่วยประมวลผลจำได้น้อยมากเกี่ยวกับสิ่งที่เพิ่งทำหรือบอก - มันดำเนินการตามคำสั่งที่ให้ไว้เท่านั้นและมันเป็นงานของโปรแกรมเมอร์ภาษาคอมไพเลอร์หรือแอสเซมบลีเพื่อให้ชุดลำดับคำสั่งที่สมบูรณ์เพื่อจัดการตัวแปร
โปรเซสเซอร์สนับสนุนข้อมูลพื้นฐานบางประเภทโดยตรงเช่นไบต์ / คำ / int / ยาวลงนาม / ไม่ได้ลงนาม, ลอย, สองครั้ง ฯลฯ โดยทั่วไปโปรเซสเซอร์จะไม่บ่นหรือคัดค้านหากคุณสลับตำแหน่งหน่วยความจำเดียวกันเป็นลงชื่อหรือไม่ได้ลงนามสำหรับ ตัวอย่างแม้ว่าปกติแล้วจะเป็นข้อผิดพลาดเชิงตรรกะในโปรแกรม มันเป็นหน้าที่ของการเขียนโปรแกรมเพื่อสั่งการตัวประมวลผลในทุก ๆ การโต้ตอบกับตัวแปร
นอกเหนือจากประเภทพื้นฐานดั้งเดิมเราต้องเข้ารหัสสิ่งต่าง ๆ ในโครงสร้างข้อมูลและใช้อัลกอริทึมเพื่อจัดการพวกเขาในแง่ของดั้งเดิม
ใน C ++ วัตถุที่เกี่ยวข้องในลำดับชั้นของคลาสสำหรับ polymorphism มีตัวชี้โดยปกติจะอยู่ที่จุดเริ่มต้นของวัตถุซึ่งหมายถึงโครงสร้างข้อมูลเฉพาะคลาสซึ่งช่วยในการส่งแบบเสมือนการส่งแบบหล่อ ฯลฯ
โดยสรุปตัวประมวลผลไม่เช่นนั้นไม่รู้หรือจำการใช้ที่ตั้งของหน่วยเก็บข้อมูลอย่างตั้งใจ - จะประมวลผลคำสั่งรหัสเครื่องของโปรแกรมที่บอกวิธีจัดการกับหน่วยความจำในรีจิสเตอร์ CPU และหน่วยความจำหลัก ดังนั้นการเขียนโปรแกรมจึงเป็นหน้าที่ของซอฟต์แวร์ (และโปรแกรมเมอร์) ในการใช้ที่เก็บข้อมูลอย่างมีความหมายและนำเสนอชุดคำสั่งรหัสเครื่องที่สอดคล้องกับโปรเซสเซอร์ซึ่งดำเนินการโปรแกรมโดยรวมอย่างซื่อสัตย์
useT1(&unionArray[i].member1); useT2(&unionArray[j].member2); useT1(&unionArray[i].member1);
, เสียงดังกราวและ GCC มีแนวโน้มที่จะคิดว่าตัวชี้ไปยังunionArray[j].member2
ไม่สามารถเข้าถึงแม้ว่าทั้งสองจะได้มาจากที่เดียวกันunionArray[i].member1
unionArray[]
ถ้าฉันกำหนดตัวแปรของชนิดที่แน่นอนมันติดตามชนิดของตัวแปรได้อย่างไร
มีสองขั้นตอนที่เกี่ยวข้องที่นี่:
คอมไพเลอร์ C รวบรวมรหัส C กับภาษาเครื่อง คอมไพเลอร์มีข้อมูลทั้งหมดที่สามารถรับได้จากซอร์สไฟล์ของคุณ (และไลบรารีและสิ่งอื่น ๆ ที่จำเป็นต้องใช้ในการทำงาน) คอมไพเลอร์ C ติดตามสิ่งที่หมายถึงอะไร คอมไพเลอร์ C รู้ว่าถ้าคุณประกาศตัวแปรให้เป็นchar
มันก็เป็นถ่าน
มันทำได้โดยใช้ "สัญลักษณ์ตาราง" ซึ่งแสดงชื่อของตัวแปรประเภทของพวกเขาและข้อมูลอื่น ๆ มันเป็นโครงสร้างข้อมูลที่ค่อนข้างซับซ้อน แต่คุณสามารถนึกได้ว่ามันเป็นเพียงการติดตามความหมายของชื่อที่มนุษย์อ่านได้ ในเอาต์พุตไบนารีจากคอมไพเลอร์ไม่มีชื่อตัวแปรเช่นนี้ปรากฏขึ้นอีกต่อไป (ถ้าเราเพิกเฉยข้อมูลการดีบักเพิ่มเติมซึ่งโปรแกรมเมอร์อาจร้องขอ)
เอาต์พุตของคอมไพเลอร์ - คอมไพล์เอ็กซีคิ้วท์ - คือภาษาเครื่อง, ซึ่งโหลดเข้าสู่ RAM โดยระบบปฏิบัติการของคุณ, และดำเนินการโดยตรงโดยซีพียูของคุณ ในภาษาเครื่องไม่มีแนวคิด "พิมพ์" เลย - มีเพียงคำสั่งที่ทำงานในบางตำแหน่งใน RAM คำสั่งไม่แน่นอนมีชนิดถาวรที่พวกเขาทำงานด้วย (เช่นอาจจะมีคำสั่งภาษาเครื่อง "เพิ่มทั้งสองจำนวนเต็ม 16 บิตที่เก็บไว้ในสถานที่ที่ RAM 0x100 และ 0x521") แต่ไม่มีข้อมูลที่ใดก็ได้ในระบบนั้น ไบต์ที่ตำแหน่งเหล่านั้นจริง ๆ แล้วจะแทนจำนวนเต็ม มีการป้องกันจากข้อผิดพลาดประเภทไม่เป็นที่ทุกคนที่นี่
char *ptr = 0x123
ใน C) ฉันเชื่อว่าการใช้คำว่า "ตัวชี้" ของฉันควรจะชัดเจนในบริบทนี้ ถ้าไม่โปรดอย่าลังเลที่จะให้ฉันและฉันจะเพิ่มประโยคในคำตอบ
มีกรณีพิเศษที่สำคัญสองสามข้อที่ C ++ จัดเก็บชนิดที่รันไทม์
โซลูชันแบบคลาสสิกเป็นสหภาพแบบแยกส่วน:โครงสร้างข้อมูลที่มีหนึ่งในหลายประเภทของวัตถุรวมถึงเขตข้อมูลที่ระบุว่ามีประเภทใดในปัจจุบัน รุ่นเทมเพลตอยู่ในห้องสมุดมาตรฐาน c ++ std::variant
เป็น โดยปกติแท็กจะเป็นenum
แต่ถ้าคุณไม่ต้องการพื้นที่เก็บข้อมูลทั้งหมดสำหรับข้อมูลของคุณมันอาจเป็นบิตฟิลด์
กรณีทั่วไปอื่น ๆ ของเรื่องนี้คือการพิมพ์แบบไดนามิก เมื่อคุณclass
มีvirtual
ฟังก์ชั่นโปรแกรมจะเก็บตัวชี้ไปยังฟังก์ชั่นนั้นในตารางฟังก์ชั่นเสมือนซึ่งมันจะเริ่มต้นสำหรับแต่ละอินสแตนซ์ของclass
เมื่อมันถูกสร้างขึ้น โดยปกตินั่นจะหมายถึงหนึ่งตารางฟังก์ชันเสมือนสำหรับอินสแตนซ์ของคลาสทั้งหมดและแต่ละอินสแตนซ์จะถือตัวชี้ไปยังตารางที่เหมาะสม (ซึ่งช่วยประหยัดเวลาและหน่วยความจำเนื่องจากตารางจะมีขนาดใหญ่กว่าตัวชี้เดียว) เมื่อคุณเรียกvirtual
ใช้ฟังก์ชันนั้นผ่านตัวชี้หรือการอ้างอิงโปรแกรมจะค้นหาตัวชี้ฟังก์ชันในตารางเสมือน (ถ้ารู้ชนิดที่แน่นอน ณ เวลารวบรวมมันสามารถข้ามขั้นตอนนี้) ซึ่งอนุญาตให้โค้ดเรียกการประยุกต์ใช้ชนิดที่ได้รับมาแทนคลาสพื้นฐาน
สิ่งหนึ่งที่ทำให้นี้มีความเกี่ยวข้องที่นี่คือแต่ละofstream
ประกอบด้วยตัวชี้ไปยังofstream
ตารางเสมือนแต่ละifstream
กับifstream
ตารางเสมือนและอื่น ๆ สำหรับลำดับชั้นของคลาสตัวชี้ตารางเสมือนสามารถทำหน้าที่เป็นแท็กที่บอกโปรแกรมว่ามีคลาสวัตถุชนิดใด!
แม้ว่ามาตรฐานภาษาจะไม่บอกคนที่ออกแบบคอมไพเลอร์ว่าพวกเขาจะต้องใช้งาน runtime อย่างไรภายใต้ประทุนนี่คือวิธีที่คุณคาดหวังdynamic_cast
และtypeof
ทำงาน