การใช้งาน stack และ heap ที่เหมาะสมใน C ++?


122

ฉันเขียนโปรแกรมมาระยะหนึ่งแล้ว แต่ส่วนใหญ่เป็น Java และ C # ฉันไม่เคยต้องจัดการหน่วยความจำด้วยตัวเองเลย ฉันเพิ่งเริ่มเขียนโปรแกรมใน C ++ และฉันรู้สึกสับสนเล็กน้อยว่าเมื่อใดที่ฉันควรจัดเก็บสิ่งต่างๆบนสแต็กและเมื่อใดที่จะเก็บไว้ในฮีป

ความเข้าใจของฉันคือตัวแปรที่ถูกเข้าถึงบ่อยมากควรถูกเก็บไว้ในสแต็กและอ็อบเจกต์ตัวแปรที่ไม่ค่อยได้ใช้และโครงสร้างข้อมูลขนาดใหญ่ทั้งหมดควรถูกเก็บไว้ในฮีป นี่ถูกต้องหรือฉันไม่ถูกต้อง?


คำตอบ:


242

ไม่ความแตกต่างระหว่างสแต็กและฮีปไม่ใช่ประสิทธิภาพ อายุการใช้งาน: ตัวแปรท้องถิ่นใด ๆ ภายในฟังก์ชัน (สิ่งที่คุณไม่ได้ malloc () หรือใหม่) อาศัยอยู่บนสแต็ก มันจะหายไปเมื่อคุณกลับจากฟังก์ชัน หากคุณต้องการให้บางสิ่งอยู่ได้นานกว่าฟังก์ชันที่ประกาศไว้คุณต้องจัดสรรสิ่งนั้นบนฮีป

class Thingy;

Thingy* foo( ) 
{
  int a; // this int lives on the stack
  Thingy B; // this thingy lives on the stack and will be deleted when we return from foo
  Thingy *pointerToB = &B; // this points to an address on the stack
  Thingy *pointerToC = new Thingy(); // this makes a Thingy on the heap.
                                     // pointerToC contains its address.

  // this is safe: C lives on the heap and outlives foo().
  // Whoever you pass this to must remember to delete it!
  return pointerToC;

  // this is NOT SAFE: B lives on the stack and will be deleted when foo() returns. 
  // whoever uses this returned pointer will probably cause a crash!
  return pointerToB;
}

เพื่อความเข้าใจที่ชัดเจนยิ่งขึ้นว่าสแต็กคืออะไรให้มาจากอีกด้านหนึ่งแทนที่จะพยายามทำความเข้าใจว่าสแต็กทำอะไรในแง่ของภาษาระดับสูงให้ค้นหา "call stack" และ "Calling convention" และดูว่า เครื่องทำงานจริงๆเมื่อคุณเรียกใช้ฟังก์ชัน หน่วยความจำคอมพิวเตอร์เป็นเพียงชุดของที่อยู่ "heap" และ "stack" เป็นสิ่งประดิษฐ์ของคอมไพเลอร์


7
การเพิ่มข้อมูลที่มีขนาดแตกต่างกันโดยทั่วไปจะอยู่ในฮีปจะปลอดภัย ข้อยกเว้นเดียวที่ฉันทราบคือ VLA ใน C99 (ซึ่งได้รับการสนับสนุนอย่าง จำกัด ) และฟังก์ชันการจัดสรร () ซึ่งมักจะเข้าใจผิดแม้กระทั่งโดยโปรแกรมเมอร์ C
Dan Olson

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

18
แน่นอนและ new / malloc () นั้นเป็นการทำงานที่ช้าและสแต็กมีแนวโน้มที่จะอยู่ใน dcache มากกว่าสายฮีปโดยพลการ สิ่งเหล่านี้เป็นข้อควรพิจารณาที่แท้จริง แต่มักจะรองจากคำถามเรื่องอายุการใช้งาน
Crashworks

1
"หน่วยความจำคอมพิวเตอร์เป็นเพียงชุดของที่อยู่" heap "และ" stack "เป็นสิ่งประดิษฐ์ของ compile" ?? เคยอ่านมาหลายที่แล้วว่า stack เป็นพื้นที่พิเศษของหน่วยความจำคอมพิวเตอร์ของเรา
Vineeth Chitteti

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

42

ฉันจะบอกว่า:

จัดเก็บไว้ในกองถ้าคุณทำได้

เก็บไว้ในกองถ้าคุณต้องการ

ดังนั้นจึงชอบกองซ้อนกับฮีป สาเหตุที่เป็นไปได้บางประการที่คุณไม่สามารถจัดเก็บบางสิ่งในสแต็ก ได้แก่ :

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

เป็นไปได้ด้วยคอมไพเลอร์ที่สมเหตุสมผลในการจัดสรรอ็อบเจ็กต์ขนาดที่ไม่คงที่บนฮีป (โดยปกติอาร์เรย์ที่ไม่ทราบขนาดในเวลาคอมไพล์)


1
สิ่งที่มากกว่าสอง KB มักจะดีที่สุดในฮีป ฉันไม่รู้ข้อมูลจำเพาะ แต่จำไม่ได้ว่าเคยทำงานกับสแต็กที่เป็น "megs สองสามตัว"
Dan Olson

2
นั่นคือสิ่งที่ฉันจะไม่เกี่ยวข้องกับผู้ใช้ในตอนเริ่มต้น สำหรับผู้ใช้เวกเตอร์และรายการดูเหมือนจะถูกจัดสรรบนสแต็กแม้ว่า STL จะเก็บเนื้อหาไว้ในฮีปก็ตาม คำถามดูเหมือนมากขึ้นในการตัดสินใจว่าเมื่อใดควรโทรใหม่ / ลบอย่างชัดเจน
David Rodríguez - dribeas

1
Dan: ฉันใส่ 2 กิ๊ก (ใช่ G เหมือนใน GIGS) ลงในสแต็กภายใต้ลินุกซ์ 32 บิต ขีด จำกัด ของสแตกขึ้นอยู่กับระบบปฏิบัติการ
นายรี

6
mrree: กอง Nintendo DS มีขนาด 16 กิโลไบต์ ขีด จำกัด สแต็กบางอย่างขึ้นอยู่กับฮาร์ดแวร์
มด

Ant: สแต็กทั้งหมดขึ้นอยู่กับฮาร์ดแวร์ขึ้นอยู่กับระบบปฏิบัติการและยังขึ้นอยู่กับคอมไพเลอร์
Viliami

24

มันละเอียดอ่อนกว่าคำตอบอื่น ๆ ที่แนะนำ ไม่มีการแบ่งสัมบูรณ์ระหว่างข้อมูลบนสแตกและข้อมูลบนฮีปตามวิธีที่คุณประกาศ ตัวอย่างเช่น:

std::vector<int> v(10);

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

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

ไม่เป็นเช่นนั้น สมมติว่าฟังก์ชันคือ:

void GetSomeNumbers(std::vector<int> &result)
{
    std::vector<int> v(10);

    // fill v with numbers

    result.swap(v);
}

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

ดังนั้นแนวทาง C ++ สมัยใหม่จึงไม่ควรทำเก็บที่อยู่ของข้อมูลฮีปในตัวแปรตัวชี้ภายในแบบเปล่า การจัดสรรฮีปทั้งหมดต้องซ่อนอยู่ภายในคลาส

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

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

a = b;

เปลี่ยนเป็นแบบนี้:

a.swap(b);

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

ข้อเสียคือวิธีนี้บังคับให้คุณส่งคืนค่าจากฟังก์ชันผ่านพารามิเตอร์เอาต์พุตแทนที่จะเป็นค่าส่งคืนจริง แต่พวกเขากำลังแก้ไขว่าใน C ++ 0x กับการอ้างอิง rvalue

ในสถานการณ์ที่ซับซ้อนที่สุดคุณจะต้องใช้ความคิดนี้ไปสู่จุดสูงสุดทั่วไปและใช้คลาสตัวชี้อัจฉริยะเช่นshared_ptrที่มีอยู่แล้วใน tr1 (แม้ว่าฉันจะเถียงว่าถ้าคุณต้องการมันคุณอาจย้ายไปอยู่นอกจุดที่น่าสนใจของ Standard C ++)


6

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


5

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

การจัดสรรในฮีปจำเป็นต้องค้นหาบล็อกการติดตามหน่วยความจำซึ่งไม่ใช่การดำเนินการตลอดเวลา (และต้องใช้รอบและค่าใช้จ่ายบางส่วน) สิ่งนี้อาจช้าลงเมื่อหน่วยความจำแยกส่วนและ / หรือใกล้จะใช้พื้นที่ที่อยู่เต็ม 100% ในทางกลับกันการจัดสรรสแต็กเป็นการดำเนินการแบบ "ฟรี" ในเวลาคงที่โดยทั่วไป

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


ทั้งฮีปและสแต็กเป็นหน่วยความจำเสมือนแบบเพจ เวลาในการค้นหาฮีปนั้นเร็วมากเมื่อเทียบกับเวลาที่ใช้ในการแมปในหน่วยความจำใหม่ ภายใต้ลินุกซ์ 32 บิตฉันสามารถใส่> 2gig ลงในสแต็กของฉันได้ ภายใต้ Macs ฉันคิดว่าสแต็ก จำกัด ไว้ที่ 65Meg
นายรี

3

Stack มีประสิทธิภาพมากกว่าและจัดการข้อมูลที่กำหนดขอบเขตได้ง่ายขึ้น

แต่ควรใช้ฮีปสำหรับสิ่งที่มีขนาดใหญ่กว่าไม่กี่ KB (ทำได้ง่ายใน C ++ เพียงสร้างไฟล์boost::scoped_ptrบนสแต็กเพื่อยึดตัวชี้ไปยังหน่วยความจำที่จัดสรร)

พิจารณาอัลกอริธึมแบบวนซ้ำที่เรียกเข้าสู่ตัวเอง มันยากมากที่จะ จำกัด และหรือคาดเดาการใช้งานสแต็คทั้งหมด! ในขณะที่ฮีปตัวจัดสรร ( malloc()หรือnew) สามารถระบุหน่วยความจำไม่เพียงพอโดยการส่งคืนNULLหรือthrowเพียงพอ

ที่มา : Linux Kernel ซึ่งมีสแต็กไม่เกิน 8KB!


สำหรับการอ้างอิงของผู้อ่านคนอื่น ๆ : (A) "ควร" ต่อไปนี้เป็นความคิดเห็นส่วนตัวของผู้ใช้เท่านั้นโดยมาจาก 1 การอ้างอิงที่ดีที่สุดและ 1 สถานการณ์ที่ผู้ใช้จำนวนมากไม่น่าจะพบเจอ (การเรียกซ้ำ) นอกจากนี้ (B) ไลบรารีมาตรฐานจัดเตรียมstd::unique_ptrไว้ซึ่งควรเป็นที่ต้องการสำหรับไลบรารีภายนอกเช่น Boost (แม้ว่าจะฟีดสิ่งต่าง ๆ ตามมาตรฐานเมื่อเวลาผ่านไป)
underscore_d


1

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


4
ฉันสงสัยว่าเขาถามว่าเมื่อไหร่จะวางของบนกองไม่ใช่อย่างไร
Steve Rowe

0

ในความคิดของฉันมีสองปัจจัยในการตัดสินใจ

1) Scope of variable
2) Performance.

ฉันต้องการใช้ stack เป็นส่วนใหญ่ แต่ถ้าคุณต้องการเข้าถึงตัวแปรนอกขอบเขตคุณสามารถใช้ heap ได้

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


0

อาจจะได้รับคำตอบที่ดีทีเดียว ฉันต้องการชี้ให้คุณดูชุดบทความด้านล่างเพื่อให้มีความเข้าใจอย่างลึกซึ้งในรายละเอียดระดับต่ำ Alex Darby มีบทความหลายชุดซึ่งเขาจะแนะนำคุณเกี่ยวกับดีบักเกอร์ นี่คือส่วนที่ 3 เกี่ยวกับ Stack http://www.altdevblogaday.com/2011/12/14/cc-low-level-curriculum-part-3-the-stack/


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