เหตุใดจึงมี * การประกาศ * ของข้อมูลและฟังก์ชั่นที่จำเป็นในภาษา C เมื่อคำจำกัดความถูกเขียนในตอนท้ายของซอร์สโค้ด?


15

พิจารณารหัส "C" ต่อไปนี้:

#include<stdio.h>
main()
{   
  printf("func:%d",Func_i());   
}

Func_i()
{
  int i=3;
  return i;
}

Func_i()ถูกกำหนดในตอนท้ายของซอร์สโค้ดและไม่มีการประกาศให้ก่อนที่จะใช้main()มัน ในเวลามากเมื่อคอมไพเลอร์เห็นFunc_i()ในmain()มันออกมาของและพบว่าออกmain() Func_i()คอมไพเลอร์อย่างใดพบว่าค่าส่งกลับโดยและให้มันไปFunc_i() printf()ผมยังไม่ทราบว่าคอมไพเลอร์ไม่สามารถหาประเภทผลตอบแทนFunc_i()ของ มันโดยปกติจะใช้เวลา (คาดเดา?) ประเภทผลตอบแทนของการที่จะเป็นFunc_i() intนั่นคือถ้ารหัสfloat Func_i()นั้นคอมไพเลอร์จะให้ข้อผิดพลาด: ประเภทที่ขัดแย้งFunc_i()กัน

จากการสนทนาข้างต้นเราจะเห็นว่า:

  1. Func_i()คอมไพเลอร์สามารถหาค่าที่ส่งกลับโดย

    • หากคอมไพเลอร์สามารถค้นหาค่าที่ส่งคืนโดยFunc_i()ออกมาmain()และค้นหาซอร์สโค้ดเหตุใดจึงไม่พบประเภทของ Func_i () ซึ่งกล่าวถึง อย่างชัดเจน
  2. คอมไพเลอร์ต้องรู้ว่าFunc_i()เป็นประเภทลอย - นั่นเป็นสาเหตุที่ทำให้เกิดข้อผิดพลาดของประเภทที่ขัดแย้งกัน

  • หากคอมไพเลอร์รู้ว่าFunc_iเป็นประเภทลอยตัวแล้วทำไมมันยังถือว่าFunc_i()เป็นประเภท int และให้ข้อผิดพลาดของประเภทที่ขัดแย้งกัน? ทำไมมันไม่ได้ทำให้มันกลายFunc_i()เป็นประเภทลอย

ผมมีข้อสงสัยเช่นเดียวกันกับการประกาศตัวแปร พิจารณารหัส "C" ต่อไปนี้:

#include<stdio.h>
main()
{
  /* [extern int Data_i;]--omitted the declaration */
  printf("func:%d and Var:%d",Func_i(),Data_i);
}

 Func_i()
{
  int i=3;
  return i;
}
int Data_i=4;

คอมไพเลอร์ให้ข้อผิดพลาด: 'Data_i' ไม่ได้ประกาศ (ใช้งานครั้งแรกในฟังก์ชั่นนี้)

  • เมื่อคอมไพเลอร์เห็นFunc_i()มันจะลงไปที่ซอร์สโค้ดเพื่อค้นหาค่าที่ส่งคืนโดย Func_ () ทำไมคอมไพเลอร์ไม่สามารถทำเช่นเดียวกันสำหรับตัวแปร Data_i

แก้ไข:

ฉันไม่ทราบรายละเอียดของการทำงานภายในของคอมไพเลอร์แอสเซมเบลอร์ตัวประมวลผล ฯลฯ แนวคิดพื้นฐานของคำถามของฉันคือถ้าฉันบอก (เขียน) การคืนค่าของฟังก์ชั่นในซอร์สโค้ดในที่สุดหลังจากการใช้ จากฟังก์ชั่นนั้นภาษา "C" จะอนุญาตให้คอมพิวเตอร์ค้นหาค่านั้นโดยไม่ให้ข้อผิดพลาดใด ๆ ตอนนี้ทำไมคอมพิวเตอร์ไม่สามารถค้นหาประเภทเดียวกันได้ เหตุใดจึงไม่พบประเภทของ Data_i เนื่องจากพบค่าตอบแทนของ Func_i () แม้ว่าฉันจะใช้extern data-type identifier;คำสั่งฉันไม่ได้บอกค่าที่จะถูกส่งกลับโดยตัวระบุนั้น (ฟังก์ชั่น / ตัวแปร) หากคอมพิวเตอร์สามารถหาค่านั้นได้ทำไมถึงไม่สามารถหาประเภทนั้นได้ ทำไมเราต้องมีการประกาศล่วงหน้าเลย?

ขอขอบคุณ.


7
คอมไพเลอร์ไม่ได้ "ค้นหา" ค่าที่ส่งคืนโดย Func_i นี้จะกระทำในเวลาดำเนินการ
James McLeod

26
ฉันไม่ได้ลงคะแนน แต่คำถามตั้งอยู่บนพื้นฐานของความเข้าใจผิดที่ร้ายแรงบางประการเกี่ยวกับวิธีการทำงานของคอมไพเลอร์และคำตอบของคุณในความคิดเห็นแนะนำให้คุณยังคงมีอุปสรรค์แนวความคิดที่จะเอาชนะ
James McLeod

4
โปรดทราบว่ารหัสตัวอย่างแรกไม่ถูกต้องรหัสที่สอดคล้องกับมาตรฐานสำหรับสิบห้าปีที่ผ่านมา C99 ทำให้ไม่มีประเภทการส่งคืนในนิยามฟังก์ชันและการประกาศโดยนัยว่าFunc_iไม่ถูกต้อง ไม่เคยมีกฎในการประกาศตัวแปรที่ไม่ได้กำหนดโดยปริยายดังนั้นแฟรกเมนต์ที่สองจึงมีรูปแบบไม่ถูกต้องเสมอ (ใช่คอมไพเลอร์ยอมรับตัวอย่างแรกที่ยังนิ่งอยู่เพราะมันถูกต้องหากเลอะเทอะภายใต้ C89 / C90)
Jonathan Leffler

19
@ user31782: บรรทัดล่างของคำถาม: ทำไมภาษา X ถึงต้องใช้ Y เพราะนั่นคือตัวเลือกที่นักออกแบบสร้างขึ้น คุณดูเหมือนจะเถียงว่านักออกแบบของหนึ่งในภาษาที่ประสบความสำเร็จมากที่สุดที่เคยทำควรมีตัวเลือกที่แตกต่างกันหลายสิบปีที่แล้วแทนที่จะพยายามเข้าใจตัวเลือกเหล่านั้นในบริบทที่พวกเขาทำ คำตอบสำหรับคำถามของคุณ: ทำไมเราต้องประกาศล่วงหน้า? ได้รับแล้ว: เนื่องจาก C ใช้คอมไพเลอร์ one-pass คำตอบที่ง่ายที่สุดสำหรับคำถามติดตามส่วนใหญ่ของคุณคือเพราะมันจะไม่เป็นคอมไพเลอร์แบบ One-Pass
Mr.Mindor

4
@ user31782 คุณต้องการอ่านหนังสือมังกรเพื่อทำความเข้าใจว่าคอมไพเลอร์และตัวประมวลผลทำงานอย่างไร - เป็นไปไม่ได้ที่จะกลั่นความรู้ที่จำเป็นทั้งหมดให้เป็นคำตอบ SO เดียว (หรือแม้แต่ 100 ข้อ) หนังสือยอดเยี่ยมสำหรับทุกคนที่มีความสนใจในคอมไพเลอร์
Voo

คำตอบ:


26

เพราะ C คือผ่านเดียว , แบบคงที่พิมพ์ , อ่อนพิมพ์ , เรียบเรียงภาษา

  1. Single-passหมายถึงคอมไพเลอร์ไม่ได้มองไปข้างหน้าเพื่อดูความหมายของฟังก์ชั่นหรือตัวแปร เนื่องจากคอมไพเลอร์ไม่ได้มองไปข้างหน้าการประกาศฟังก์ชั่นจะต้องมาก่อนการใช้ฟังก์ชั่นมิฉะนั้นคอมไพเลอร์ไม่ทราบว่าลายเซ็นประเภทของมันคืออะไร อย่างไรก็ตามความหมายของฟังก์ชั่นสามารถเปิดได้ในภายหลังในไฟล์เดียวกันหรือแม้กระทั่งในไฟล์ที่แตกต่างกันโดยสิ้นเชิง ดูจุดที่ 4

    ข้อยกเว้นเพียงอย่างเดียวคือสิ่งประดิษฐ์เชิงประวัติที่สันนิษฐานว่าเป็นฟังก์ชันและตัวแปรที่ไม่ได้ประกาศเป็นประเภท "int" แนวปฏิบัติที่ทันสมัยคือการหลีกเลี่ยงการพิมพ์โดยนัยโดยการประกาศฟังก์ชั่นและตัวแปรอย่างชัดเจนเสมอ

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

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

  4. การคอมไพล์หมายความว่ากระบวนการวิเคราะห์ซอร์สโค้ดที่มนุษย์สามารถอ่านได้และแปลงเป็นคำสั่งที่เครื่องอ่านได้นั้นจะดำเนินการอย่างสมบูรณ์ก่อนที่โปรแกรมจะทำงาน เมื่อคอมไพเลอร์รวบรวมฟังก์ชั่นมันไม่มีความรู้ในสิ่งที่มันจะพบต่อไปในไฟล์ต้นฉบับที่กำหนด อย่างไรก็ตามเมื่อการคอมไพล์ (และแอสเซมบลีการเชื่อมโยง ฯลฯ ) เสร็จสมบูรณ์แล้วแต่ละฟังก์ชันในการปฏิบัติการที่เสร็จสิ้นแล้วจะมีพอยน์เตอร์ตัวเลขไปยังฟังก์ชันที่จะเรียกใช้เมื่อมีการเรียกใช้ นั่นคือเหตุผลที่ main () สามารถเรียกใช้ฟังก์ชันเพิ่มเติมในไฟล์ต้นฉบับได้ โดยเวลาหลัก () ทำงานจริงมันจะมีตัวชี้ไปยังที่อยู่ของ Func_i ()

    รหัสเครื่องมีความเฉพาะเจาะจงมาก รหัสสำหรับการเพิ่มจำนวนเต็มสองจำนวน (3 + 2) จะแตกต่างจากรหัสสำหรับการเพิ่มจำนวนสองลอย (3.0 + 2.0) สิ่งเหล่านี้ต่างจากการเพิ่ม int ลงในทุ่น (3 + 2.0) และอื่น ๆ คอมไพเลอร์กำหนดสำหรับทุกจุดในฟังก์ชันที่การดำเนินการที่แน่นอนต้องดำเนินการ ณ จุดนั้นและสร้างรหัสที่ดำเนินการตามที่ต้องการ เมื่อดำเนินการเสร็จแล้วจะไม่สามารถเปลี่ยนแปลงได้หากไม่ได้รวบรวมฟังก์ชันใหม่

การนำแนวคิดเหล่านี้ทั้งหมดมารวมกันด้วยเหตุผลที่ main () ไม่สามารถ "เห็น" เพิ่มเติมเพื่อกำหนดประเภทของ Func_i () คือการวิเคราะห์ประเภทนั้นเกิดขึ้นที่จุดเริ่มต้นของกระบวนการรวบรวม ณ จุดนั้นมีเพียงบางส่วนของไฟล์ต้นฉบับจนถึงคำจำกัดความของ main () เท่านั้นที่ถูกอ่านและวิเคราะห์และคำจำกัดความของ Func_i () ยังไม่เป็นที่รู้จักสำหรับคอมไพเลอร์

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

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

นอกจากนี้โปรดจำไว้ว่าสิ่งที่ฉันเขียนด้านบนใช้กับ C โดยเฉพาะ

ในภาษาอื่น ๆ คอมไพเลอร์อาจทำการส่งผ่านหลายครั้งผ่านซอร์สโค้ดและคอมไพเลอร์สามารถรับนิยามของ Func_i () โดยไม่ต้องมีการประกาศล่วงหน้า

ในภาษาอื่น ๆ ฟังก์ชั่นและ / หรือตัวแปรอาจถูกพิมพ์แบบไดนามิกดังนั้นตัวแปรเดียวสามารถถือหรือฟังก์ชั่นเดียวสามารถผ่านหรือส่งกลับจำนวนเต็มลอย, สตริง, อาร์เรย์หรือวัตถุในเวลาที่แตกต่างกัน

ในภาษาอื่น ๆ การพิมพ์อาจจะแข็งกว่าซึ่งต้องการการแปลงจากเลขทศนิยมเป็นจำนวนเต็มเพื่อระบุอย่างชัดเจน ในภาษาอื่นการพิมพ์อาจอ่อนกว่าทำให้การแปลงจากสตริง "3.0" เป็น float 3.0 เป็นจำนวนเต็ม 3 ที่จะดำเนินการโดยอัตโนมัติ

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


1
ขอบคุณสำหรับคำตอบแบบครบวงจร คำตอบของคุณและนิกกี้คือสิ่งที่ฉันอยากรู้ เช่นFunc_()+1ที่นี่ในเวลารวบรวมเรียบเรียงมีทราบชนิดของFunc_i()เพื่อสร้างรหัสเครื่องที่เหมาะสม อาจเป็นไปไม่ได้ที่แอสเซมบลีจะจัดการFunc_()+1โดยการเรียกชนิดในขณะใช้งานหรือเป็นไปได้ แต่การทำเช่นนั้นจะทำให้โปรแกรมช้าลงในเวลาทำงาน ฉันคิดว่ามันเพียงพอสำหรับฉันในตอนนี้
user106313

1
รายละเอียดที่สำคัญของฟังก์ชั่นที่ประกาศโดยปริยายของ C: พวกมันถูกสันนิษฐานว่าเป็นประเภทint func(...)... นั่นคือพวกเขารับรายการอาร์กิวเมนต์แบบแปรผัน ซึ่งหมายความว่าถ้าคุณกำหนดฟังก์ชั่นเป็นint putc(char)แต่ลืมที่จะประกาศมันจะถูกเรียกว่าเป็นint putc(int)(เพราะถ่านที่ส่งผ่านรายการอาร์กิวเมนต์ Variadic ได้รับการเลื่อนตำแหน่งเป็นint) ดังนั้นในขณะที่ตัวอย่างของ OP เกิดขึ้นเนื่องจากลายเซ็นของมันตรงกับคำประกาศโดยปริยาย แต่ก็เข้าใจได้ว่าทำไมพฤติกรรมนี้จึงหมดกำลังใจ (และเพิ่มคำเตือนที่เหมาะสม)
uliwitness

37

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

int Func_i();

ที่ด้านบนหรือในไฟล์ส่วนหัวเพื่อช่วยคอมไพเลอร์

ในตัวอย่างของคุณคุณใช้คุณสมบัติที่น่าสงสัยสองอย่างของภาษา C ที่ควรหลีกเลี่ยง:

  1. หากมีการใช้ฟังก์ชั่นก่อนที่จะมีการประกาศอย่างถูกต้องสิ่งนี้จะถูกใช้เป็น คอมไพเลอร์ใช้บริบททันทีเพื่อหาลายเซ็นของฟังก์ชัน คอมไพเลอร์จะไม่สแกนรหัสที่เหลือเพื่อหาว่าการประกาศจริงคืออะไร

  2. intหากสิ่งที่ถูกประกาศโดยไม่ต้องพิมพ์พิมพ์จะถูกนำไปเป็น นี่คือตัวอย่างของตัวแปรคงที่หรือฟังก์ชันส่งคืนชนิด

ดังนั้นในการที่เรามีการประกาศโดยปริยายprintf("func:%d",Func_i()) int Func_i()เมื่อคอมไพเลอร์มาถึงข้อกำหนดของฟังก์ชั่นFunc_i() { ... }สิ่งนี้จะเข้ากันได้กับประเภท แต่ถ้าคุณเขียนfloat Func_i() { ... }ที่จุดนี้คุณได้ implicity ประกาศและประกาศอย่างชัดเจนint Func_i() float Func_i()เนื่องจากการประกาศสองรายการไม่ตรงกันคอมไพเลอร์จึงให้ข้อผิดพลาดแก่คุณ

การล้างความเข้าใจผิดบางอย่าง

  • Func_iคอมไพเลอร์ไม่พบค่าส่งกลับโดย การไม่มีประเภทที่ชัดเจนหมายความว่าประเภทการคืนสินค้าเป็นintค่าเริ่มต้น แม้ว่าคุณจะทำสิ่งนี้:

    Func_i() {
        float f = 42.3;
        return f;
    }

    ชนิดจะเป็นint Func_i()และค่าส่งคืนจะถูกตัดอย่างเงียบ ๆ !

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


3
@ user31782: ลำดับของรหัสมีความสำคัญในเวลารวบรวม แต่ไม่ใช่เวลารันไทม์ คอมไพเลอร์อยู่นอกรูปภาพเมื่อโปรแกรมทำงาน ตามเวลาทำงานฟังก์ชั่นจะได้รับการประกอบและเชื่อมโยงที่อยู่ของมันจะได้รับการแก้ไขและติดอยู่ในตัวยึดที่อยู่ของการโทร (มันซับซ้อนกว่านั้นนิดหน่อย แต่นั่นเป็นแนวคิดพื้นฐาน) โปรเซสเซอร์สามารถแยกไปข้างหน้าหรือข้างหลังได้
Blrfl

20
@ user31782: คอมไพเลอร์ไม่พิมพ์ค่า คอมไพเลอร์ของคุณไม่ได้รันโปรแกรม !!
การแข่งขัน Lightness กับ Monica

1
@LightnessRacesinOrbit ฉันรู้ว่า ฉันเข้าใจผิดเขียนคอมไพเลอร์ในความคิดเห็นของฉันข้างต้นเพราะผมลืมชื่อตัวประมวลผล
user106313

3
@Carcigenicate C ได้รับอิทธิพลอย่างมากจากภาษา B ซึ่งมีเพียงประเภทเดียว: ประเภทตัวเลขที่มีความกว้างของคำที่สามารถใช้สำหรับพอยน์เตอร์ได้ C คัดลอกพฤติกรรมนี้ในตอนแรก แต่ตอนนี้มันผิดกฎหมายอย่างสมบูรณ์ตั้งแต่มาตรฐาน C99 Unitทำให้ประเภทเริ่มต้นที่ดีจากมุมมองทฤษฎีประเภท แต่ล้มเหลวในการปฏิบัติจริงของใกล้กับการเขียนโปรแกรมระบบโลหะที่ B แล้ว C ถูกออกแบบมาสำหรับ
amon

2
@ user31782: คอมไพเลอร์ต้องทราบชนิดของตัวแปรเพื่อสร้างแอสเซมบลีที่ถูกต้องสำหรับโปรเซสเซอร์ เมื่อคอมไพเลอร์พบโดยปริยายFunc_i()มันจะสร้างและบันทึกรหัสทันทีสำหรับโปรเซสเซอร์เพื่อข้ามไปยังตำแหน่งอื่นจากนั้นรับจำนวนเต็มบางส่วนจากนั้นดำเนินการต่อ เมื่อคอมไพเลอร์พบFunc_iคำจำกัดความในภายหลังมันทำให้แน่ใจว่าลายเซ็นตรงกันและถ้าพวกเขาทำมันจะวางแอสเซมบลีสำหรับFunc_i()ที่ที่อยู่นั้นและบอกให้คืนค่าจำนวนเต็ม 3เมื่อคุณเรียกใช้โปรแกรมประมวลผลแล้วตามคำแนะนำที่มีค่า
Mooing Duck

10

ขั้นแรกโปรแกรมของคุณจะถูกต้องสำหรับมาตรฐาน C90 แต่ไม่ใช่สำหรับโปรแกรมต่อไปนี้ นัย int (อนุญาตให้ประกาศฟังก์ชั่นโดยไม่ให้ผลตอบแทนประเภท) และประกาศฟังก์ชั่นโดยปริยาย (อนุญาตให้ใช้ฟังก์ชั่นโดยไม่ต้องประกาศมัน) จะไม่ถูกต้องมากขึ้น

ข้อสองนั่นไม่ได้ผลอย่างที่คุณคิด

  1. ประเภทผลลัพธ์เป็นตัวเลือกใน C90 โดยไม่ให้intผลลัพธ์หนึ่งรายการ มันเป็นความจริงสำหรับการประกาศตัวแปร (แต่คุณต้องให้คลาสหน่วยเก็บข้อมูลstaticหรือextern)

  2. สิ่งที่คอมไพเลอร์ทำเมื่อเห็นการFunc_iเรียกใช้โดยไม่มีการประกาศก่อนหน้านี้สมมติว่ามีการประกาศ

    extern int Func_i();

    มันไม่ได้ดูเพิ่มเติมในรหัสเพื่อดูวิธีการFunc_iประกาศอย่างมีประสิทธิภาพ หากไม่ได้รับการประกาศหรือกำหนดคอมไพเลอร์จะไม่เปลี่ยนพฤติกรรมของมันเมื่อรวบรวมFunc_i mainการประกาศโดยนัยมีไว้สำหรับฟังก์ชั่นเท่านั้นไม่มีสำหรับตัวแปร

    โปรดทราบว่ารายการพารามิเตอร์ที่ว่างเปล่าในการประกาศไม่ได้หมายความว่าฟังก์ชั่นไม่ได้ใช้พารามิเตอร์ (คุณจำเป็นต้องระบุ(void)สำหรับนั้น) มันหมายความว่าคอมไพเลอร์ไม่ต้องตรวจสอบประเภทของพารามิเตอร์และจะเหมือนกัน การแปลงโดยนัยที่ใช้กับข้อโต้แย้งที่ส่งผ่านไปยังฟังก์ชัน Variadic


หากคอมไพเลอร์สามารถค้นหาค่าที่ส่งคืนโดย Func_i () โดยออกมาจาก main () และค้นหาซอร์สโค้ดเหตุใดจึงไม่สามารถหาประเภทของ Func_i () ซึ่งกล่าวถึงอย่างชัดเจน
user106313

1
@ user31782 หากไม่มีการประกาศก่อนหน้าของ Func_i เมื่อเห็นว่า Func_i extern int Func_i()ถูกนำมาใช้ในการแสดงออกของสายพฤติกรรมเช่นถ้ามีผู้หญิงคนหนึ่ง มันไม่ได้ดูทุกที่
AProgrammer

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

10
@ user31782 คุณเข้าใจผิดอย่างร้ายแรงเกี่ยวกับการทำงานของโปรแกรม ร้ายแรงมากที่ฉันไม่คิดว่า p.se เป็นสถานที่ที่ดีในการแก้ไข (อาจเป็นการแชท แต่ฉันจะไม่ลองทำ)
AProgrammer

1
@ user31782: การเขียนตัวอย่างขนาดเล็กและรวบรวมด้วย-S(ถ้าคุณใช้gcc) จะช่วยให้คุณดูรหัสประกอบที่สร้างโดยคอมไพเลอร์ จากนั้นคุณสามารถมีความคิดเกี่ยวกับวิธีการจัดการค่าส่งคืนในเวลาทำงาน (โดยปกติจะใช้ตัวประมวลผลการลงทะเบียน
Giorgio

7

คุณเขียนความคิดเห็น:

การดำเนินการเสร็จแล้วทีละบรรทัด วิธีเดียวที่จะหาค่าที่ส่งคืนโดย Func_i () คือการกระโดดออกจากหลัก

นั่นเป็นความเข้าใจที่คลาดเคลื่อน: การดำเนินการไม่ได้เกิดขึ้นทีละบรรทัด การคอมไพล์เสร็จแล้วทีละบรรทัดและการจำแนกชื่อเสร็จสิ้นในระหว่างการรวบรวมและแก้ไขเฉพาะชื่อไม่ใช่คืนค่า

รูปแบบแนวคิดที่มีประโยชน์คือ: เมื่อคอมไพเลอร์อ่านบรรทัด:

  printf("func:%d",Func_i());

มันปล่อยรหัสเทียบเท่ากับ:

  1. call "function #2" and put the return value on the stack
  2. put the constant string "func:%d" on the stack
  3. call "function #1"

คอมไพเลอร์ยังสร้างบันทึกย่อในตารางภายในบางตัวที่function #2ยังไม่ได้ประกาศชื่อฟังก์ชั่นFunc_iซึ่งรับจำนวนอาร์กิวเมนต์ที่ไม่ระบุและคืนค่า int (ค่าเริ่มต้น)

ต่อมาเมื่อแยกวิเคราะห์สิ่งนี้:

 int Func_i() { ...

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

ดังนั้นคอมไพเลอร์ไม่ได้ "ค้นหา" Func_iเมื่อวิเคราะห์คำอ้างอิงแรก มันแค่จดบันทึกในตารางบางอันการแยกวิเคราะห์บรรทัดถัดไป และในตอนท้ายของไฟล์มีไฟล์ออบเจกต์และรายการที่อยู่ข้าม

หลังจากนั้นตัวเชื่อมโยงจะใช้สิ่งนี้ทั้งหมดและแทนที่ตัวชี้ทั้งหมดเป็น "function # 2" ด้วย jump address จริงดังนั้นมันจึงปล่อยสิ่งที่ต้องการ:

  call 0x0001215 and put the result on the stack
  put constant ... on the stack
  call ...
...
[at offset 0x0001215 in the file, compiled result of Func_i]:
  put 3 on the stack
  return top of the stack

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

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


ขอบคุณสำหรับคำตอบ. คอมไพเลอร์ไม่สามารถปล่อยรหัส:1. call "function #2", put the return-type onto the stack and put the return value on the stack?
user106313

1
(ต่อ.) นอกจากนี้: เกิดอะไรขึ้นถ้าคุณเขียนprintf(..., Func_i()+1);- คอมไพเลอร์มีทราบชนิดของFunc_iเพื่อที่จะสามารถตัดสินใจได้ว่ามันควรจะปล่อยadd integerหรือadd floatการเรียนการสอน คุณอาจพบบางกรณีพิเศษที่เรียบเรียงได้ต่อไปโดยไม่มีข้อมูลประเภท แต่คอมไพเลอร์ที่มีการทำงานสำหรับทุกกรณี
nikie

4
@ user31782: คำสั่งเครื่องตามกฎง่ายมาก : เพิ่มการลงทะเบียนจำนวนเต็ม 32 บิตสองรายการ โหลดที่อยู่หน่วยความจำไปยังการลงทะเบียนจำนวนเต็ม 16 บิต ข้ามไปยังที่อยู่ นอกจากนี้ยังไม่มีประเภท : คุณสามารถโหลดตำแหน่งหน่วยความจำที่แสดงหมายเลขทศนิยม 32 บิตลงในการลงทะเบียนจำนวนเต็ม 32 บิตและทำเลขคณิตด้วย (มันไม่ค่อยสมเหตุสมผล) ดังนั้นไม่คุณไม่สามารถปล่อยรหัสเครื่องแบบนั้นได้โดยตรง คุณสามารถเขียนคอมไพเลอร์ที่ทำสิ่งเหล่านั้นทั้งหมดด้วยการตรวจสอบรันไทม์และข้อมูลประเภทพิเศษในกองซ้อน แต่มันจะไม่เป็นคอมไพเลอร์ C
nikie

1
@ user31782: ขึ้นอยู่กับ IIRC floatค่าสามารถอยู่ในการลงทะเบียน FPU - จากนั้นจะไม่มีคำสั่งเลย คอมไพเลอร์เพียงแค่ติดตามว่าค่าใดถูกเก็บไว้ซึ่งลงทะเบียนระหว่างการรวบรวมและส่งข้อมูลเช่น "เพิ่มค่าคงที่ 1 ถึง FP register X" หรืออาจอยู่บนสแต็คหากไม่มีรีจิสเตอร์ฟรี จากนั้นจะมีคำสั่ง "เพิ่มตัวชี้สแต็ก 4" และค่าจะเป็น "อ้างอิง" เป็นบางอย่างเช่น "ตัวชี้สแต็ก - 4" แต่ทุกสิ่งเหล่านี้จะทำงานเฉพาะในกรณีที่ขนาดของตัวแปรทั้งหมด (ก่อนและหลัง) บนสแต็กเป็นที่รู้จักกันในเวลารวบรวม
nikie

1
จากการอภิปรายทั้งหมดที่ฉันได้มาถึงความเข้าใจนี้: เพื่อให้คอมไพเลอร์สร้างรหัสแอสเซมบลีที่เป็นไปได้สำหรับคำสั่งใด ๆ รวมถึงFunc_i()หรือ / และData_iมันต้องกำหนดประเภทของพวกเขา ไม่สามารถใช้ภาษาแอสเซมบลีในการโทรไปยังชนิดข้อมูล ฉันต้องเรียนรู้สิ่งต่าง ๆ ในรายละเอียดด้วยตัวเองเพื่อให้มั่นใจ
user106313

5

C และภาษาอื่น ๆ ที่ต้องมีการประกาศได้รับการออกแบบในยุคที่เวลาของโปรเซสเซอร์และหน่วยความจำมีราคาแพง การพัฒนาของ C และ Unix ไปจับมือสำหรับค่อนข้างบางเวลาและหลังไม่ได้มีหน่วยความจำเสมือนจนกว่า 3BSD ปรากฏตัวขึ้นในปี 1979 โดยไม่ต้องห้องพิเศษในการทำงานของคอมไพเลอร์มีแนวโน้มที่จะผ่านเดียวกิจการเพราะพวกเขาไม่ได้ ต้องการความสามารถในการรักษาการแสดงไฟล์ทั้งหมดในหน่วยความจำทั้งหมดในครั้งเดียว

คอมไพเลอร์แบบ Single Pass เป็นเหมือนเราที่แบกรับความไม่สามารถมองเห็นอนาคตได้ นี่หมายถึงสิ่งเดียวที่พวกเขาสามารถรู้ได้อย่างแน่นอนคือสิ่งที่พวกเขาได้รับการบอกกล่าวอย่างชัดเจนก่อนที่จะรวบรวมบรรทัดของรหัส เป็นเรื่องธรรมดาสำหรับเราคนใดคนหนึ่งที่Func_i()ประกาศในภายหลังในไฟล์ต้นฉบับ แต่คอมไพเลอร์ซึ่งทำงานกับโค้ดขนาดเล็กทีละไฟล์ไม่มีเงื่อนงำที่จะมา

ในช่วงต้น C (AT & T, K & R, C89) การใช้ฟังก์ชั่นfoo()ก่อนที่จะประกาศผลในพฤตินัยint foo()หรือประกาศโดยนัยของ ตัวอย่างของคุณใช้งานได้เมื่อFunc_i()มีการประกาศintเนื่องจากตรงกับสิ่งที่คอมไพเลอร์ประกาศในนามของคุณ การเปลี่ยนเป็นประเภทอื่นจะทำให้เกิดข้อขัดแย้งเนื่องจากไม่ตรงกับสิ่งที่คอมไพเลอร์เลือกในกรณีที่ไม่มีการประกาศอย่างชัดเจน พฤติกรรมนี้ถูกลบใน C99 ซึ่งการใช้ฟังก์ชั่นที่ไม่ได้ประกาศเป็นข้อผิดพลาด

แล้วประเภทผลตอบแทนล่ะ

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

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

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

พิจารณารหัสเหล่านี้:

double foo();         double foo();
double x;             int x;
x = foo();            x = foo();

รหัสทางด้านซ้ายจะคอมไพล์ลงไปที่การโทรเพื่อfoo()ตามด้วยการคัดลอกผลลัพธ์ที่ได้รับผ่านการประชุมสาย / ส่งคืนลงในที่ใดก็ตามที่xเก็บไว้ นั่นเป็นกรณีที่ง่าย

รหัสทางด้านขวาแสดงการแปลงประเภทและสาเหตุที่คอมไพเลอร์จำเป็นต้องทราบประเภทการคืนสินค้าของฟังก์ชัน ไม่สามารถทิ้งเลขทศนิยมไปยังหน่วยความจำโดยที่รหัสอื่นคาดว่าจะเห็นintเนื่องจากไม่มีการแปลงเวทย์มนตร์ หากผลลัพธ์สุดท้ายต้องเป็นจำนวนเต็มจะต้องมีคำแนะนำที่แนะนำโปรเซสเซอร์เพื่อทำการแปลงก่อนการจัดเก็บ โดยไม่ทราบชนิดการส่งคืนของfoo()ก่อนเวลาคอมไพเลอร์จะมีความคิดที่ว่ารหัสการเปลี่ยนแปลงเป็นสิ่งที่จำเป็น

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


ขอบคุณสำหรับคำตอบ (+1) ฉันไม่ทราบรายละเอียดของการทำงานภายในของคอมไพเลอร์แอสเซมเบลอร์ตัวประมวลผล ฯลฯ แนวคิดพื้นฐานของคำถามของฉันคือถ้าฉันบอก (เขียน) การคืนค่าของฟังก์ชั่นในซอร์สโค้ดในที่สุดหลังจากการใช้ จากฟังก์ชั่นนั้นภาษาจะอนุญาตให้คอมพิวเตอร์ค้นหาค่านั้นโดยไม่ให้ข้อผิดพลาดใด ๆ ตอนนี้ทำไมคอมพิวเตอร์ไม่สามารถค้นหาประเภทเดียวกันได้ เหตุใดจึงไม่พบประเภทของ Data_i เนื่องจากพบFunc_i()ค่าส่งคืน
user106313

ฉันยังไม่พอใจ double foo(); int x; x = foo();เพียงแค่ให้ข้อผิดพลาด ฉันรู้ว่าเราไม่สามารถทำได้ คำถามของฉันคือว่าในการเรียกใช้ฟังก์ชั่นหน่วยประมวลผลพบค่าตอบแทนเท่านั้น; เหตุใดจึงไม่สามารถค้นหาประเภทผลตอบแทนด้วย
user106313

1
@ user31782: ไม่ควร มีต้นแบบสำหรับfoo()คอมไพเลอร์รู้ว่าจะทำอย่างไรกับมัน
Blrfl

2
@ user31782: หน่วยประมวลผลไม่มีแนวคิดเกี่ยวกับประเภทส่งคืน
Blrfl

1
@ user31782 สำหรับคำถามเวลารวบรวม: มันเป็นไปได้ที่จะเขียนภาษาที่การวิเคราะห์ประเภทนี้สามารถทำได้ในเวลารวบรวม C ไม่ใช่ภาษาดังกล่าว คอมไพเลอร์ C ไม่สามารถทำได้เพราะไม่ได้ออกแบบมาให้ทำ มันได้รับการออกแบบที่แตกต่างกัน? แน่นอนว่ามันต้องใช้พลังการประมวลผลและหน่วยความจำมากขึ้น บรรทัดล่างคือมันไม่ได้ มันถูกออกแบบในลักษณะที่คอมพิวเตอร์ประจำวันสามารถจัดการได้ดีที่สุด
Mr.Mindor
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.