การจัดสรรหน่วยความจำแบบไดนามิกค. โครงสร้างการควบคุมของภาษา C การแสดงโปรแกรมเป็นฟังก์ชัน ทำงานกับหน่วยความจำ โครงสร้าง. ฟังก์ชันการจัดสรรหน่วยความจำแบบไดนามิกมาตรฐาน

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

สมมติว่าการกำหนดตัวแปร ival

Intival = 1,024;
บังคับให้คอมไพเลอร์จัดสรรพื้นที่ในหน่วยความจำที่มีขนาดใหญ่พอที่จะจัดเก็บตัวแปรประเภท int เชื่อมโยงชื่อ ival กับพื้นที่นี้ และวางค่า 1024 ไว้ที่นั่น ทั้งหมดนี้ทำในขั้นตอนการคอมไพล์ก่อนที่โปรแกรมจะถูกดำเนินการ

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

เราสามารถอ้างถึงปริมาณใดปริมาณหนึ่งในสองปริมาณนี้ เมื่อเราเขียน:
อินท์ ival2 = อิล + 1;

จากนั้นเราเข้าถึงค่าที่มีอยู่ในตัวแปร ival: เราเพิ่ม 1 ลงไปและเริ่มต้นตัวแปร ival2 ด้วยค่าใหม่นี้ 1025 เราจะเข้าถึงที่อยู่ซึ่งเป็นที่ตั้งของตัวแปรได้อย่างไร

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

Int *ไพนต์; pint = // pint รับค่าของที่อยู่ ival

เราสามารถเข้าถึงวัตถุที่มีที่อยู่ประกอบด้วยไพน์ (ival ในกรณีของเรา) โดยใช้การดำเนินการ การตัดสิทธิเรียกอีกอย่างว่า ที่อยู่ทางอ้อม- การดำเนินการนี้ระบุด้วยสัญลักษณ์ * ต่อไปนี้เป็นวิธีเพิ่มรายการหนึ่งไปยัง ival ทางอ้อมโดยใช้ที่อยู่:

*ไพน์ = *ไพน์ + 1; // เพิ่ม ival โดยปริยาย

นิพจน์นี้ทำหน้าที่เหมือนกับทุกประการ

อิวาล = อิวาล + 1; // เพิ่ม ival อย่างชัดเจน

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

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

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

ตัวดำเนินการใหม่มีสองรูปแบบ รูปแบบแรกจัดสรรหน่วยความจำสำหรับวัตถุเดี่ยวบางประเภท:

Int *ไพน์ = int ใหม่(1024);
ที่นี่ ตัวดำเนินการใหม่จะจัดสรรหน่วยความจำสำหรับวัตถุที่ไม่มีชื่อประเภท int เริ่มต้นด้วยค่า 1024 และส่งคืนที่อยู่ของวัตถุที่สร้างขึ้น ที่อยู่นี้ใช้เพื่อเริ่มต้นไพนต์พอยน์เตอร์ การกระทำทั้งหมดบนวัตถุที่ไม่มีชื่อดังกล่าวจะดำเนินการโดยการยกเลิกการอ้างอิงตัวชี้นี้ เนื่องจาก เป็นไปไม่ได้ที่จะจัดการวัตถุไดนามิกอย่างชัดเจน

รูปแบบที่สองของตัวดำเนินการใหม่จะจัดสรรหน่วยความจำสำหรับอาร์เรย์ที่มีขนาดที่กำหนด ซึ่งประกอบด้วยองค์ประกอบบางประเภท:

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

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

// ปล่อยวัตถุเดี่ยวให้ลบไพนต์; // ปล่อยอาร์เรย์ให้ลบ pia; จะเกิดอะไรขึ้นถ้าเราลืมเพิ่มหน่วยความจำที่จัดสรรไว้? หน่วยความจำจะสูญเปล่า จะไม่ได้ใช้ แต่ไม่สามารถคืนเข้าสู่ระบบได้เนื่องจากเราไม่มีพอยน์เตอร์ให้ ปรากฏการณ์นี้ได้รับชื่อพิเศษหน่วยความจำรั่ว
- ในที่สุดโปรแกรมก็จะพังเนื่องจากหน่วยความจำไม่เพียงพอ (แน่นอนว่าถ้าทำงานนานพอ) การรั่วไหลเล็กน้อยอาจตรวจพบได้ยาก แต่มีระบบสาธารณูปโภคที่สามารถช่วยคุณในเรื่องนี้ได้

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

แบบฝึกหัดที่ 2.3

อธิบายความแตกต่างระหว่างวัตถุทั้งสี่:

(a) int ival = 1,024; (b) int *pi = (c) int *pi2 = int ใหม่(1024); (d) int *pi3 = int ใหม่;

แบบฝึกหัดที่ 2.4

ข้อมูลโค้ดต่อไปนี้ใช้ทำอะไร? การเข้าใจผิดเชิงตรรกะคืออะไร? (โปรดทราบว่าการดำเนินการของดัชนี () ถูกนำไปใช้กับตัวชี้ pia อย่างถูกต้อง คำอธิบายข้อเท็จจริงนี้มีอยู่ในส่วนที่ 3.9.2)
Int *pi = int ใหม่(10); int *pia = int ใหม่;< 10) {
ในขณะที่ (*pi
เปีย[*ปี่] = *ปี่;

*พาย = *พาย + 1;

) ลบพาย; ลบเปีย;

ดังนั้น. ประเภทที่สามสิ่งที่น่าสนใจที่สุดในหัวข้อนี้สำหรับเราคือหน่วยความจำประเภทไดนามิก < stdio.h> เราเคยทำงานกับอาร์เรย์มาก่อนอย่างไร? int a ตอนนี้เราทำงานกันยังไงบ้าง? เราจัดสรรให้มากเท่าที่จำเป็น: < stdlib.h> #รวม #รวม int main() (ขนาด size_t;// สร้างตัวชี้ไปที่ int // – โดยพื้นฐานแล้วเป็นอาร์เรย์ว่าง int *รายการ; สแกนฟ( // และ "อาร์เรย์ว่าง" ของเราตอนนี้อ้างถึงหน่วยความจำนี้รายการ = (int *)malloc(ขนาด * ขนาดของ(int));< size; ++i) { scanf (สำหรับ (int i = 0 ; i < size; ++i) { printf (สำหรับ (int i = 0 ; i" %d " , *(รายการ + i));- // *

// อย่าลืมทำความสะอาดตัวเองด้วยนะ!

ฟรี (รายการ); -

เป็นโมฆะ * malloc (ขนาด size_t);

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

ดังนั้น. ประเภทที่สามสิ่งที่น่าสนใจที่สุดในหัวข้อนี้สำหรับเราคือหน่วยความจำประเภทไดนามิก < stdio.h> เราเคยทำงานกับอาร์เรย์มาก่อนอย่างไร? int a ตอนนี้เราทำงานกันยังไงบ้าง? เราจัดสรรให้มากเท่าที่จำเป็น: < stdlib.h> หากการจัดสรรสำเร็จ ตัวชี้ไปยังไบต์แรกของหน่วยความจำที่จัดสรรจะถูกส่งกลับ // – โดยพื้นฐานแล้วเป็นอาร์เรย์ว่างหากไม่สำเร็จ - NULL นอกจากนี้ errno จะเท่ากับ ENOMEM (เราจะดูตัวแปรที่ยอดเยี่ยมนี้ในภายหลัง) นั่นคือการเขียนจะถูกกว่า:< size; ++i) { scanf (สำหรับ (int i = 0 ; i int main () ( size_t size; int *list; scanf (< size; ++i) { printf (สำหรับ (int i = 0 ; i, &ขนาด); รายการ = (int *)malloc(ขนาด * ขนาดของ(int)); // *

ถ้า (รายการ == NULL ) ( ข้ามข้อผิดพลาด; ) สำหรับ (int i = 0 ; i

ดังนั้น. ประเภทที่สามสิ่งที่น่าสนใจที่สุดในหัวข้อนี้สำหรับเราคือหน่วยความจำประเภทไดนามิก < stdlib.h> , รายการ + ฉัน);

) สำหรับ (int i = 0 ; i

, *(รายการ + i));

    ) ฟรี (รายการ);

    กลับ 0 ; ข้อผิดพลาด: ส่งคืน 1 ; -

    ไม่จำเป็นต้องล้างตัวชี้ NULL

    int main() (ฟรี (NULL);)

    – ด้วยเสียงกราวเดียวกันทุกอย่างจะเป็นไปด้วยดี (ไม่มีอะไรจะทำ) แต่ในกรณีที่แปลกใหม่กว่านั้นอาจทำให้โปรแกรมเสียหายได้

    ถัดจาก malloc และมานาฟรี คุณยังสามารถเห็น:

    เป็นโมฆะ * calloc (จำนวน size_t, ขนาด size_t);

    เช่นเดียวกับที่ malloc จะจัดสรรหน่วยความจำสำหรับการนับอ็อบเจ็กต์ขนาดไบต์ หน่วยความจำที่จัดสรรจะเริ่มต้นด้วยศูนย์

    เป็นโมฆะ * realloc (โมฆะ *ptr ขนาด size_t);

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

ถ้า ptr เป็น NULL การ realloc จะเหมือนกับการเรียก malloc

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

· หน่วยความจำสำหรับตัวแปร global และ static (ประกาศด้วยตัวระบุแบบคงที่) แต่ละตัวจะได้รับการจัดสรรก่อนที่โปรแกรมจะเริ่มดำเนินการตามคำอธิบายประเภท ตั้งแต่ต้นจนจบการทำงานของโปรแกรม ตัวแปรเหล่านี้จะเชื่อมโยงกับพื้นที่หน่วยความจำที่จัดสรรไว้ ดังนั้นพวกมันจึงมีอายุการใช้งานทั่วโลก แต่ขอบเขตของมันแตกต่างออกไป

· สำหรับตัวแปรโลคอลที่ประกาศภายในบล็อกและไม่มีตัวระบุ คงที่หน่วยความจำถูกจัดสรรด้วยวิธีอื่น ก่อนที่โปรแกรมจะเริ่มทำงาน (เมื่อโหลด) จะมีการจัดสรรพื้นที่หน่วยความจำขนาดใหญ่พอสมควรเรียกว่า สแต็ค(บางครั้งมีการใช้คำ สแต็คโปรแกรมหรือ โทรสแต็คเพื่อสร้างความแตกต่างระหว่างสแต็กที่เป็นประเภทข้อมูลนามธรรม) ขนาดของสแต็กขึ้นอยู่กับสภาพแวดล้อมการพัฒนา เช่น ใน MS Visual C++ โดยค่าเริ่มต้นจะมีการจัดสรร 1 เมกะไบต์สำหรับสแต็ก (ค่านี้สามารถปรับแต่งได้) ในระหว่างการทำงานของโปรแกรม เมื่อเข้าสู่บล็อกใดบล็อกหนึ่ง หน่วยความจำจะถูกจัดสรรบนสแต็กสำหรับตัวแปรที่แปลในบล็อก (ตามคำอธิบายประเภท) เมื่อออกจากบล็อก หน่วยความจำนี้จะถูกปลดปล่อย กระบวนการเหล่านี้ดำเนินการโดยอัตโนมัติ ซึ่งเป็นสาเหตุว่าทำไมจึงมักเรียกตัวแปรท้องถิ่นในภาษา C++ อัตโนมัติ.

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

การใช้คำว่า "สแต็ก" นั้นอธิบายได้ง่าย - ด้วยแนวทางที่เป็นที่ยอมรับในการจัดสรรหน่วยความจำและการจัดสรรคืน ตัวแปรที่ถูกวางไว้สุดท้ายบนสแต็ก (นี่คือตัวแปรที่แปลเป็นภาษาท้องถิ่นในบล็อกที่ซ้อนกันที่ลึกที่สุด) จะถูกลบออกจากสแต็กก่อน นั่นคือการจัดสรรและปล่อยหน่วยความจำเกิดขึ้นตามหลักการ LIFO (LAST IN – FIRST OUT, Last in – first out) นี่คือหลักการทำงานของสแต็ก เราจะดูที่สแต็กเป็นโครงสร้างข้อมูลแบบไดนามิกและการใช้งานที่เป็นไปได้ในส่วนถัดไป



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

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

รูปที่ 2.1 – แผนภาพการกระจายหน่วยความจำ

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



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

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

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

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

1. การจัดสรรหน่วยความจำแบบไดนามิกและแบบคงที่ (คงที่) ความแตกต่างหลัก

ในการทำงานกับอาร์เรย์ข้อมูล โปรแกรมต้องจัดสรรหน่วยความจำสำหรับอาร์เรย์เหล่านี้ ในการจัดสรรหน่วยความจำสำหรับอาร์เรย์ของตัวแปร จะใช้ตัวดำเนินการ ฟังก์ชัน ฯลฯ ที่เหมาะสม ในภาษาการเขียนโปรแกรม C++ วิธีการจัดสรรหน่วยความจำต่อไปนี้จะมีความโดดเด่น:

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

อินท์เอ็ม; // หน่วยความจำสำหรับอาร์เรย์ได้รับการจัดสรรครั้งเดียว ขนาดหน่วยความจำได้รับการแก้ไขแล้ว

2. พลวัต การจัดสรรหน่วยความจำ ในกรณีนี้ จะใช้ตัวดำเนินการใหม่และตัวดำเนินการลบรวมกัน ตัวดำเนินการใหม่จัดสรรหน่วยความจำสำหรับตัวแปร (อาร์เรย์) ในพื้นที่หน่วยความจำพิเศษที่เรียกว่าฮีป ตัวดำเนินการลบจะทำให้หน่วยความจำที่จัดสรรว่าง โอเปอเรเตอร์ใหม่แต่ละรายจะต้องมีโอเปอเรเตอร์ลบของตัวเอง

2. ข้อดีและข้อเสียของการใช้วิธีการจัดสรรหน่วยความจำแบบไดนามิกและแบบคงที่

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

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

ข้อดีของวิธีการจัดสรรหน่วยความจำแบบคงที่:

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

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

3. จะจัดสรรหน่วยความจำโดยใช้ตัวดำเนินการใหม่สำหรับตัวแปรเดี่ยวได้อย่างไร? แบบฟอร์มทั่วไป

รูปแบบทั่วไปของการจัดสรรหน่วยความจำสำหรับตัวแปรเดี่ยวโดยใช้ตัวดำเนินการใหม่มีดังนี้:

ptrName= ชนิดใหม่;
  • ptrName– ชื่อของตัวแปร (ตัวชี้) ที่จะชี้ไปยังหน่วยความจำที่จัดสรร
  • พิมพ์– ประเภทของตัวแปร ขนาดหน่วยความจำได้รับการจัดสรรเพียงพอที่จะวางค่าของตัวแปรประเภทนี้ลงไป พิมพ์ .
4. จะเพิ่มหน่วยความจำที่จัดสรรให้กับตัวแปรตัวเดียวโดยใช้ตัวดำเนินการลบได้อย่างไร แบบฟอร์มทั่วไป

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

ในภาษาการเขียนโปรแกรม Java และ C# ไม่จำเป็นต้องเพิ่มหน่วยความจำหลังจากการจัดสรร ซึ่งดำเนินการโดย “คนเก็บขยะ”

รูปแบบทั่วไปของตัวดำเนินการลบสำหรับตัวแปรตัวเดียวคือ:

ลบ ptrName;

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

5. ตัวอย่างการจัดสรร (ใหม่) และการเพิ่มหน่วยความจำ (ลบ) สำหรับพอยน์เตอร์ประเภทพื้นฐาน

ตัวอย่างแสดงให้เห็นถึงการใช้ตัวดำเนินการใหม่และลบ ตัวอย่างจะง่ายขึ้น

ตัวอย่างที่ 1ตัวชี้ให้พิมพ์ int ตัวอย่างที่ง่ายที่สุด

// การจัดสรรหน่วยความจำโดยใช้โอเปอเรเตอร์ใหม่อินท์ * พี; // ชี้ไปที่ int p = int ใหม่ ; // จัดสรรหน่วยความจำสำหรับตัวชี้*พี = 25; //เขียนค่าลงหน่วยความจำ // การใช้หน่วยความจำที่จัดสรรให้กับตัวชี้อินท์ดี; ง = *พี; // ง = 25 // เพิ่มหน่วยความจำที่จัดสรรให้กับตัวชี้ - บังคับลบพี;

ตัวอย่างที่ 2ตัวชี้ให้พิมพ์สองครั้ง

// จัดสรรหน่วยความจำเพื่อให้พอยน์เตอร์เพิ่มเป็นสองเท่าสองเท่า * pd = NULL ; pd = คู่ใหม่ ; //จัดสรรหน่วยความจำถ้า (pd!=NULL ) ( *pd = 10.89; // เขียนค่าดับเบิล d = *pd; // d = 10.89 - ใช้ในโปรแกรม // หน่วยความจำว่างลบ pd; -
6. “หน่วยความจำรั่ว” คืออะไร?

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

“หน่วยความจำรั่ว” เป็นข้อผิดพลาดทั่วไปของโปรแกรมเมอร์ หาก "หน่วยความจำรั่ว" เกิดขึ้นซ้ำหลายครั้ง อาจเป็นไปได้ว่าหน่วยความจำที่มีอยู่ในคอมพิวเตอร์จะถูก "ครอบครอง" สิ่งนี้จะนำไปสู่ผลลัพธ์ที่คาดเดาไม่ได้ของระบบปฏิบัติการ

7. จะจัดสรรหน่วยความจำโดยใช้โอเปอเรเตอร์ใหม่ได้อย่างไร เพื่อสกัดกั้นสถานการณ์วิกฤติที่อาจไม่สามารถจัดสรรหน่วยความจำได้ ข้อยกเว้น Bad_alloc ตัวอย่าง

เมื่อใช้โอเปอเรเตอร์ใหม่ อาจเป็นไปได้ว่าหน่วยความจำจะไม่ได้รับการจัดสรร หน่วยความจำอาจไม่ได้รับการจัดสรรในสถานการณ์ต่อไปนี้:

  • หากไม่มีหน่วยความจำว่าง
  • ขนาดของหน่วยความจำว่างน้อยกว่าที่ระบุไว้ในโอเปอเรเตอร์ใหม่

ในกรณีนี้ ข้อยกเว้น bad_alloc จะถูกส่งออกไป โปรแกรมสามารถสกัดกั้นสถานการณ์นี้และจัดการตามนั้น

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

int หลัก() ( // ประกาศอาร์เรย์ของพอยน์เตอร์ให้ลอยลอย * ptrArray; พยายาม (// พยายามจัดสรรหน่วยความจำสำหรับองค์ประกอบโฟลต 10 รายการ<< << endl; cout << ba.what() << endl; return -1; ptrArray = โฟลตใหม่ ; } ) catch (bad_alloc ba) ( cout// ออกจากฟังก์ชัน< 10; i++) ptrArray[i] = i * i + 3; int d = ptrArray; cout << d << endl; delete ptrArray; // ถ้าทุกอย่างเรียบร้อยดี ให้ใช้อาร์เรย์สำหรับ (int i = 0; i
// หน่วยความจำว่างที่จัดสรรให้กับอาร์เรย์

ตัวดำเนินการจัดสรรหน่วยความจำใหม่สำหรับตัวแปรเดียวช่วยให้สามารถเริ่มต้นพร้อมกับค่าของตัวแปรนั้นได้พร้อมกัน

โดยทั่วไป การจัดสรรหน่วยความจำสำหรับตัวแปรที่มีการกำหนดค่าเริ่มต้นพร้อมกันจะมีลักษณะดังนี้

ptrName= ชนิดใหม่( ค่า)
  • ptrName– ชื่อของตัวแปรพอยน์เตอร์ที่จัดสรรหน่วยความจำ
  • พิมพ์– ประเภทที่ตัวชี้ชี้ไป ptrName ;
  • ค่า– ค่าที่ตั้งไว้สำหรับพื้นที่หน่วยความจำที่จัดสรร (ค่าตัวชี้)

ตัวอย่าง.การจัดสรรหน่วยความจำสำหรับตัวแปรด้วยการเริ่มต้นพร้อมกัน ด้านล่างนี้คือฟังก์ชัน main() สำหรับแอปพลิเคชันคอนโซล สาธิตการจัดสรรหน่วยความจำด้วยการเริ่มต้นพร้อมกัน นอกจากนี้ยังคำนึงถึงสถานการณ์เมื่อความพยายามจัดสรรหน่วยความจำล้มเหลว (สถานการณ์วิกฤติ bad_alloc)

#รวม "stdafx.h" #รวม ใช้เนมสเปซมาตรฐาน; int หลัก() ( // การจัดสรรหน่วยความจำพร้อมการเริ่มต้นพร้อมกันลอย * pF; int * pI;ถ่าน * PC;<< พยายาม ( << endl; cout << ba.what() << endl; return -1; ptrArray = โฟลตใหม่ ; } // พยายามจัดสรรหน่วยความจำให้กับตัวแปรด้วยการเริ่มต้นพร้อมกัน pF = โฟลตใหม่ (3.88); // *pF = 3.88 pI = int ใหม่ (250); // *pI = 250 pC = ถ่านใหม่ ("M" ); // *pC = "M" ) catch (bad_alloc ba) ( cout "ข้อยกเว้น: หน่วยความจำไม่ได้จัดสรร"// หากจัดสรรหน่วยความจำให้ใช้พอยน์เตอร์ pF, pI, pC<< "*pF = " << f<< endl; cout << "*pI = " << i << endl; cout << "*pC = " << c << endl; ลอย f = *pF; // f = 3.88 int i = *pI; // ฉัน = 250;ถ่านค;

ค = *พีซี; // ค = "ม"

// พิมพ์ค่าเริ่มต้น

ศาล

// หน่วยความจำว่างที่จัดสรรไว้ก่อนหน้านี้สำหรับพอยน์เตอร์

ลบ pF;

ลบพีไอ;

ลบพีซี;

กลับ 0; -

การจัดสรรหน่วยความจำแบบไดนามิก

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

เพื่อแก้ไขปัญหาบางอย่าง มีวิธีการป้องกันและการประกันภัยดังนี้:

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

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

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


Yandex.Direct


#รวม int *ptrVar = malloc(ขนาดของ(int));

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



อย่างที่คุณเห็น มีจุดแข็งจุดหนึ่งในสัญลักษณ์นี้ เราไม่ควรเรียกใช้ฟังก์ชัน malloc() โดยใช้ sizeof(float) แต่เราส่งพอยน์เตอร์ไปยังประเภท float ไปที่ malloc() ซึ่งในกรณีนี้ขนาดของหน่วยความจำที่จัดสรรจะกำหนดตัวเองโดยอัตโนมัติ!

สิ่งนี้มีประโยชน์อย่างยิ่งหากคุณต้องการจัดสรรหน่วยความจำให้ห่างจากคำจำกัดความของตัวชี้:


ลอย *ptrVar;

- - - โค้ดหนึ่งร้อยบรรทัด */ .