ใน C เครื่องหมายวงเล็บทำหน้าที่เป็นกรอบสแต็กหรือไม่


153

หากฉันสร้างตัวแปรภายในวงเล็บปีกกาชุดใหม่ตัวแปรนั้นจะแตกออกจากสแต็กในวงเล็บปิดหรือไม่หรือจะออกไปเที่ยวจนจบฟังก์ชัน ตัวอย่างเช่น:

void foo() {
   int c[100];
   {
       int d[200];
   }
   //code that takes a while
   return;
}

จะdใช้หน่วยความจำระหว่างcode that takes a whileส่วนหรือไม่


8
คุณหมายถึง (1) ตามมาตรฐาน (2) การปฏิบัติสากลในการใช้งานหรือ (3) การปฏิบัติร่วมกันระหว่างการใช้งานหรือไม่?
David Thornley

คำตอบ:


83

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

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

นอกจากนี้โปรดทราบว่าตัวแปรในตัวเครื่องอาจไม่ใช้พื้นที่สแต็คใด ๆ เลย: สามารถเก็บไว้ใน CPU register หรือในที่เก็บข้อมูลเสริมอื่น ๆ หรือปรับให้เหมาะสมทั้งหมด

ดังนั้นdอาร์เรย์ในทางทฤษฎีสามารถใช้หน่วยความจำสำหรับฟังก์ชั่นทั้งหมด อย่างไรก็ตามคอมไพเลอร์อาจปรับให้เหมาะสมหรือแบ่งปันหน่วยความจำกับตัวแปรท้องถิ่นอื่น ๆ ซึ่งอายุการใช้งานจะไม่ทับซ้อนกัน


9
นั่นเป็นเรื่องเฉพาะของการนำไปใช้หรือไม่
avakar

54
ใน C ++ ตัวทำลายของวัตถุจะถูกเรียกเมื่อสิ้นสุดขอบเขต การเรียกคืนหน่วยความจำว่าเป็นปัญหาเฉพาะของการนำไปใช้หรือไม่
Kristopher Johnson

8
@ pm100: destructors จะถูกเรียก มันไม่ได้บอกอะไรเกี่ยวกับความทรงจำที่วัตถุนั้นครอบครอง
Donal Fellows

9
มาตรฐาน C ระบุว่าอายุการใช้งานของตัวแปรอัตโนมัติที่ประกาศในบล็อกจะขยายออกไปจนกว่าการดำเนินการของบล็อกจะสิ้นสุดลง ดังนั้นตัวแปรอัตโนมัติเหล่านั้นจะได้รับ "ทำลาย" ที่ส่วนท้ายของบล็อก
caf

3
@KristopherJohnson: ถ้าวิธีหนึ่งมีสองบล็อกแยกกันซึ่งแต่ละคนประกาศอาร์เรย์ 1Kbyte และบล็อกที่สามซึ่งเรียกว่าวิธีซ้อนกันคอมไพเลอร์จะมีอิสระในการใช้หน่วยความจำเดียวกันสำหรับทั้งสองอาร์เรย์และ / หรือการวางอาร์เรย์ ที่ส่วนที่ตื้นที่สุดของสแต็กและย้ายตัวชี้สแต็กด้านบนเรียกวิธีซ้อนกัน พฤติกรรมดังกล่าวสามารถลดความลึกของสแต็กได้ 2K ที่จำเป็นสำหรับการเรียกใช้ฟังก์ชัน
supercat

39

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

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

void foo() {
   int c[100];
   int *p;

   {
       int d[200];
       p = d;
   }

   /* Can I access p[0] here? */

   return;
}

(ในคำอื่น ๆ : คอมไพเลอร์ได้รับอนุญาตให้ deallocate dแม้ว่าในทางปฏิบัติส่วนใหญ่ไม่ได้?)

คำตอบก็คือคอมไพเลอร์จะได้รับอนุญาตให้ deallocate dและการเข้าถึงp[0]ที่ความคิดเห็นบ่งชี้ว่าเป็นพฤติกรรมที่ไม่ได้กำหนด (โปรแกรมจะไม่ได้รับอนุญาตให้เข้าถึงนอกวัตถุภายในขอบเขตด้านใน) ส่วนที่เกี่ยวข้องของมาตรฐาน C คือ 6.2.4p5:

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


ในขณะที่บางคนเรียนรู้ว่าขอบเขตและหน่วยความจำทำงานอย่างไรใน C และ C ++ หลังจากใช้ภาษาระดับสูงขึ้นไปหลายปีฉันพบว่าคำตอบนี้มีความแม่นยำและมีประโยชน์มากกว่าภาษาที่ยอมรับ
Chris

20

คำถามของคุณไม่ชัดเจนเพียงพอที่จะตอบอย่างไม่น่าสงสัย

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

ในทางกลับกันเมื่ออายุการใช้งานของวัตถุในท้องถิ่นสิ้นสุดลงหน่วยความจำที่ครอบครองโดยวัตถุนั้นสามารถนำมาใช้ใหม่สำหรับวัตถุในท้องถิ่นอื่นได้ในภายหลัง ตัวอย่างเช่นในรหัสนี้

void foo()
{
  {
    int d[100];
  }
  {
    double e[20];
  }
}

ทั้งสองอาร์เรย์มักจะใช้พื้นที่หน่วยความจำเดียวกันซึ่งหมายความว่าจำนวนทั้งหมดของหน่วยความจำภายในที่จำเป็นสำหรับการทำงานfooเป็นสิ่งที่จำเป็นสำหรับสองอาร์เรย์ที่ใหญ่ที่สุดไม่ใช่สำหรับทั้งสองในเวลาเดียวกัน

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


6

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


3

ไม่ d [] จะไม่อยู่ในสแต็กสำหรับส่วนที่เหลือของรูทีน แต่ alloca () นั้นแตกต่างกัน

แก้ไข: Kristopher จอห์นสัน (และไซมอนและแดเนียล) เป็นที่เหมาะสมและตอบสนองเริ่มต้นของผมก็คือผิด ด้วย gcc 4.3.4.on CYGWIN รหัส:

void foo(int[]);
void bar(void);
void foobar(int); 

void foobar(int flag) {
    if (flag) {
        int big[100000000];
        foo(big);
    }
    bar();
}

ให้:

_foobar:
    pushl   %ebp
    movl    %esp, %ebp
    movl    $400000008, %eax
    call    __alloca
    cmpl    $0, 8(%ebp)
    je      L2
    leal    -400000000(%ebp), %eax
    movl    %eax, (%esp)
    call    _foo
L2:
    call    _bar
    leave
    ret

ใช้ชีวิตและเรียนรู้! และการทดสอบอย่างรวดเร็วดูเหมือนว่าจะแสดงว่า AndreyT นั้นถูกต้องเกี่ยวกับการจัดสรรหลายรายการ

เพิ่มมากในภายหลัง : การทดสอบด้านบนแสดงเอกสาร gccไม่ถูกต้องนัก หลายปีที่ผ่านมาได้กล่าว (เน้นเพิ่ม):

"พื้นที่สำหรับอาร์เรย์ที่มีความยาวผันแปรได้รับการจัดสรรคืนทันทีที่ขอบเขต ของชื่ออาร์เรย์สิ้นสุด "


การคอมไพล์ด้วยการเพิ่มประสิทธิภาพถูกปิดใช้งานไม่จำเป็นต้องแสดงสิ่งที่คุณจะได้รับในรหัสที่ได้รับการเพิ่มประสิทธิภาพ ในกรณีนี้พฤติกรรมที่เดียวกัน (จัดสรรในช่วงเริ่มต้นของการทำงานและมีเพียงฟรีเมื่อออกจากฟังก์ชั่น): godbolt.org/g/M112AQ แต่ไม่ใช่ cygwin gcc ไม่เรียกใช้allocaฟังก์ชัน ฉันประหลาดใจจริงๆที่ cygwin gcc จะทำเช่นนั้น มันไม่ใช่แม้กระทั่งอาเรย์ที่มีความยาวผันแปรได้ดังนั้น IDK จึงเป็นสาเหตุที่ทำให้เกิด
Peter Cordes

2

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


1

dโดยทั่วไปแล้วตัวแปรของคุณจะไม่แตกออกจากสแต็ก วงเล็บปีกกาไม่แสดงถึงกรอบสแต็ก มิฉะนั้นคุณจะไม่สามารถทำสิ่งนี้:

char var = getch();
    {
        char next_var = var + 1;
        use_variable(next_char);
    }

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

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

อัปเดต: นี่คือสิ่งที่C specบอกไว้ เกี่ยวกับวัตถุที่มีระยะเวลาการจัดเก็บอัตโนมัติ (ส่วน 6.4.2):

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

ส่วนเดียวกันกำหนดคำว่า "อายุการใช้งาน" เป็น (เน้นที่เหมือง):

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

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

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

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


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

@ george- พฤติกรรมที่คุณอธิบายรวมถึงตัวอย่างของโจเซฟขึ้นอยู่กับคอมไพเลอร์และแพลตฟอร์มที่คุณใช้ ตัวอย่างเช่นการรวบรวมรหัสเดียวกันสำหรับเป้าหมาย MIPS ให้ผลลัพธ์ที่แตกต่างอย่างสิ้นเชิง ฉันพูดอย่างหมดจดจากมุมมองของข้อมูลจำเพาะ C (เนื่องจาก OP ไม่ได้ระบุคอมไพเลอร์หรือเป้าหมาย) ฉันจะแก้ไขคำตอบและเพิ่มรายละเอียดเพิ่มเติม
bta

0

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


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

0

มีการให้ข้อมูลจำนวนมากเกี่ยวกับมาตรฐานที่ระบุว่าเป็นการใช้งานจริงโดยเฉพาะ

ดังนั้นการทดสอบหนึ่งอาจเป็นที่สนใจ หากเราลองใช้รหัสต่อไปนี้:

#include <stdio.h>
int main() {
    int* x;
    int* y;
    {
        int a;
        x = &a;
        printf("%p\n", (void*) x);
    }
    {
        int b;
        y = &b;
        printf("%p\n", (void*) y);
    }
}

ใช้ gcc เราได้ที่นี่สองครั้งอยู่เดียวกัน: Coliro

แต่ถ้าเราลองใช้รหัสต่อไปนี้:

#include <stdio.h>
int main() {
    int* x;
    int* y;
    {
        int a;
        x = &a;
    }
    {
        int b;
        y = &b;
    }
    printf("%p\n", (void*) x);
    printf("%p\n", (void*) y);
}

ใช้ gcc เราได้ที่นี่สองที่อยู่ที่แตกต่างกัน: Coliro

ดังนั้นคุณไม่สามารถแน่ใจได้อย่างแท้จริงว่าเกิดอะไรขึ้น

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