พอยน์เตอร์เป็นแนวคิดที่หลายคนอาจสับสนในตอนแรกโดยเฉพาะอย่างยิ่งเมื่อมันมาถึงการคัดลอกค่าตัวชี้ไปรอบ ๆ และยังอ้างอิงถึงบล็อกหน่วยความจำเดียวกัน
ฉันพบว่าอุปมาที่ดีที่สุดคือพิจารณาตัวชี้เป็นแผ่นกระดาษที่มีที่อยู่บ้านอยู่และหน่วยความจำบล็อกมันอ้างอิงว่าเป็นบ้านจริง สามารถอธิบายการทำงานทุกประเภทได้อย่างง่ายดาย
ฉันได้เพิ่มรหัส Delphi ลงด้านล่างและความคิดเห็นที่เหมาะสม ฉันเลือก Delphi เนื่องจากภาษาการเขียนโปรแกรมหลักอื่น ๆ ของฉัน C # ไม่แสดงสิ่งต่าง ๆ เช่นการรั่วไหลของหน่วยความจำในลักษณะเดียวกัน
หากคุณต้องการเรียนรู้แนวความคิดระดับสูงของพอยน์เตอร์คุณควรละเว้นส่วนที่มีชื่อว่า "เลย์เอาต์ของหน่วยความจำ" ในคำอธิบายด้านล่าง พวกเขามีวัตถุประสงค์เพื่อให้ตัวอย่างของหน่วยความจำที่อาจมีลักษณะหลังจากการดำเนินการ แต่พวกเขามีระดับต่ำกว่าในธรรมชาติ อย่างไรก็ตามเพื่อที่จะอธิบายได้อย่างถูกต้องถึงวิธีการทำงานของบัฟเฟอร์ที่โอเวอร์รันได้อย่างแม่นยำเป็นสิ่งสำคัญที่ฉันต้องเพิ่มไดอะแกรมเหล่านี้
คำเตือน: สำหรับเจตนาและวัตถุประสงค์ทั้งหมดคำอธิบายนี้และตัวอย่างโครงร่างหน่วยความจำนั้นง่ายกว่าเดิมมาก มีค่าใช้จ่ายมากขึ้นและรายละเอียดเพิ่มเติมที่คุณจะต้องรู้หากคุณต้องจัดการกับหน่วยความจำในระดับต่ำ อย่างไรก็ตามสำหรับจุดประสงค์ในการอธิบายความจำและพอยน์เตอร์มันมีความแม่นยำเพียงพอ
สมมติว่าคลาส Thouse ที่ใช้ด้านล่างมีลักษณะดังนี้:
type
THouse = class
private
FName : array[0..9] of Char;
public
constructor Create(name: PChar);
end;
เมื่อคุณเริ่มต้นวัตถุเฮ้าส์ชื่อที่กำหนดให้กับตัวสร้างจะถูกคัดลอกลงในฟิลด์ส่วนตัว FName มีเหตุผลที่ถูกกำหนดเป็นอาร์เรย์ขนาดคงที่
ในความทรงจำจะมีค่าโสหุ้ยที่เกี่ยวข้องกับการจัดสรรบ้านฉันจะแสดงตัวอย่างด้านล่างนี้:
--- [ttttNNNNNNNNNN] ---
^ ^
| |
| + - อาร์เรย์ FName
|
+ - ค่าใช้จ่าย
พื้นที่ "tttt" เป็นค่าใช้จ่ายโดยทั่วไปจะมีมากกว่านี้สำหรับประเภท runtimes และภาษาประเภทต่างๆเช่น 8 หรือ 12 ไบต์ มีความจำเป็นที่ค่าใด ๆ ที่ถูกเก็บไว้ในพื้นที่นี้จะไม่ถูกเปลี่ยนแปลงโดยสิ่งอื่นใดนอกจากตัวจัดสรรหน่วยความจำหรือรูทีนระบบหลักหรือคุณเสี่ยงต่อการขัดข้องของโปรแกรม
จัดสรรหน่วยความจำ
รับผู้ประกอบการเพื่อสร้างบ้านของคุณและให้ที่อยู่บ้าน ตรงกันข้ามกับโลกแห่งความเป็นจริงการจัดสรรหน่วยความจำไม่สามารถบอกได้ว่าจะจัดสรรที่ไหน แต่จะหาจุดที่เหมาะสมที่มีพื้นที่เพียงพอและรายงานที่อยู่กลับไปยังหน่วยความจำที่จัดสรร
ในคำอื่น ๆ ผู้ประกอบการจะเลือกจุด
THouse.Create('My house');
รูปแบบหน่วยความจำ:
--- [ttttNNNNNNNNNN] ---
1234 บ้านของฉัน
เก็บตัวแปรด้วยที่อยู่
เขียนที่อยู่ไปที่บ้านใหม่ของคุณลงบนกระดาษ กระดาษนี้จะใช้เป็นข้อมูลอ้างอิงของคุณไปที่บ้านของคุณ หากไม่มีกระดาษแผ่นนี้คุณก็หายไปและไม่สามารถหาบ้านได้เว้นแต่คุณจะเข้าไปอยู่
var
h: THouse;
begin
h := THouse.Create('My house');
...
รูปแบบหน่วยความจำ:
ชั่วโมง
โวลต์
--- [ttttNNNNNNNNNN] ---
1234 บ้านของฉัน
คัดลอกค่าตัวชี้
เพียงเขียนที่อยู่บนกระดาษแผ่นใหม่ ตอนนี้คุณมีกระดาษสองแผ่นที่จะพาคุณไปที่บ้านหลังเดียวกันไม่ใช่บ้านสองหลัง ความพยายามใด ๆ ที่จะติดตามที่อยู่จากกระดาษแผ่นหนึ่งและจัดเรียงเฟอร์นิเจอร์ที่บ้านหลังนั้นจะทำให้ดูเหมือนว่าบ้านหลังอื่นได้รับการแก้ไขในลักษณะเดียวกันเว้นแต่คุณจะสามารถตรวจสอบได้อย่างชัดเจนว่ามันเป็นเพียงบ้านหลังหนึ่ง
หมายเหตุนี่มักจะเป็นแนวคิดที่ฉันมีปัญหามากที่สุดในการอธิบายกับผู้คนตัวชี้สองตัวไม่ได้หมายถึงวัตถุสองหรือบล็อกหน่วยความจำ
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := h1; // copies the address, not the house
...
h1
โวลต์
--- [ttttNNNNNNNNNN] ---
1234 บ้านของฉัน
^
H2
พ้นความจำ
รื้อถอนบ้าน จากนั้นคุณสามารถนำกระดาษกลับมาใช้ใหม่เพื่อที่อยู่ใหม่หากคุณต้องการหรือลบออกเพื่อลืมที่อยู่ไปยังบ้านที่ไม่มีอยู่อีกต่อไป
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
h := nil;
ที่นี่ฉันแรกสร้างบ้านและได้รับที่อยู่ของมัน จากนั้นฉันก็ทำอะไรบางอย่างกับบ้าน (ใช้รหัส ... เหลือไว้สำหรับการออกกำลังกายสำหรับผู้อ่าน) แล้วฉันก็ปล่อยให้เป็นอิสระ สุดท้ายฉันจะล้างที่อยู่จากตัวแปรของฉัน
รูปแบบหน่วยความจำ:
h <- +
v + - ก่อนที่จะฟรี
--- [ttttNNNNNNNNNN] --- |
1234 บ้านของฉัน <- +
h (ตอนนี้ไม่มีจุดไหน) <- +
+ - หลังฟรี
---------------------- | (หมายเหตุหน่วยความจำอาจยังคง
xx34 บ้านของฉัน <- + มีข้อมูลบางส่วน)
พอยน์เตอร์ที่ห้อยอยู่
คุณบอกผู้ประกอบการของคุณให้ทำลายบ้าน แต่คุณลืมที่จะลบที่อยู่ออกจากกระดาษของคุณ เมื่อคุณดูแผ่นกระดาษในภายหลังคุณลืมว่าบ้านไม่อยู่ที่นั่นแล้วและไปเยี่ยมชมด้วยผลลัพธ์ที่ล้มเหลว (ดูส่วนที่เกี่ยวกับการอ้างอิงที่ไม่ถูกต้องด้านล่าง)
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
... // forgot to clear h here
h.OpenFrontDoor; // will most likely fail
การใช้h
หลังจากการเรียกไปยัง.Free
อาจใช้งานได้ แต่นั่นเป็นเพียงโชคดีเท่านั้น เป็นไปได้มากว่ามันจะล้มเหลว ณ สถานที่ของลูกค้าในระหว่างการดำเนินการที่สำคัญ
h <- +
v + - ก่อนที่จะฟรี
--- [ttttNNNNNNNNNN] --- |
1234 บ้านของฉัน <- +
h <- +
v + - หลังจากฟรี
---------------------- |
xx34 บ้านของฉัน <- +
อย่างที่คุณเห็น h ยังคงชี้ไปที่ส่วนที่เหลือของข้อมูลในหน่วยความจำ แต่เนื่องจากอาจไม่สมบูรณ์ให้ใช้เหมือนเมื่อก่อนอาจล้มเหลว
หน่วยความจำรั่ว
คุณทำกระดาษหายและไม่สามารถหาบ้านได้ แม้ว่าบ้านจะยังคงยืนอยู่ที่ไหนสักแห่งและเมื่อคุณต้องการสร้างบ้านใหม่ในภายหลังคุณไม่สามารถนำจุดนั้นกลับมาใช้ใหม่ได้
var
h: THouse;
begin
h := THouse.Create('My house');
h := THouse.Create('My house'); // uh-oh, what happened to our first house?
...
h.Free;
h := nil;
ที่นี่เราเขียนทับเนื้อหาของh
ตัวแปรด้วยที่อยู่ของบ้านใหม่ แต่บ้านเก่ายังคงยืนอยู่ที่ไหนสักแห่ง หลังจากรหัสนี้ไม่มีทางที่จะไปถึงบ้านหลังนั้นและมันจะถูกทิ้งให้ยืน กล่าวอีกนัยหนึ่งหน่วยความจำที่จัดสรรไว้จะยังคงถูกจัดสรรอยู่จนกว่าแอปพลิเคชันจะปิดลงซึ่งจะทำให้ระบบปฏิบัติการฉีกขาด
เลย์เอาต์หน่วยความจำหลังจากการจัดสรรครั้งแรก:
ชั่วโมง
โวลต์
--- [ttttNNNNNNNNNN] ---
1234 บ้านของฉัน
เลย์เอาต์หน่วยความจำหลังจากการจัดสรรครั้งที่สอง:
ชั่วโมง
โวลต์
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
1234 บ้านของฉัน 5678 บ้านของฉัน
วิธีทั่วไปในการรับวิธีนี้คือการลืมที่จะปล่อยบางสิ่งบางอย่างแทนที่จะเขียนทับวิธีดังกล่าวข้างต้น ในเงื่อนไข Delphi สิ่งนี้จะเกิดขึ้นกับวิธีการต่อไปนี้:
procedure OpenTheFrontDoorOfANewHouse;
var
h: THouse;
begin
h := THouse.Create('My house');
h.OpenFrontDoor;
// uh-oh, no .Free here, where does the address go?
end;
หลังจากวิธีนี้ได้ดำเนินการแล้วไม่มีตัวแปรใดในที่อยู่ของบ้านของเราอยู่ แต่บ้านยังอยู่ข้างนอก
รูปแบบหน่วยความจำ:
h <- +
v + - ก่อนที่จะสูญเสียตัวชี้
--- [ttttNNNNNNNNNN] --- |
1234 บ้านของฉัน <- +
h (ตอนนี้ไม่มีจุดไหน) <- +
+ - หลังจากแพ้พอยน์เตอร์
--- [ttttNNNNNNNNNN] --- |
1234 บ้านของฉัน <- +
อย่างที่คุณเห็นข้อมูลเก่าถูกทิ้งไว้ในหน่วยความจำและจะไม่ถูกใช้ซ้ำโดยตัวจัดสรรหน่วยความจำ ตัวจัดสรรจะติดตามว่ามีการใช้พื้นที่หน่วยความจำใดและจะไม่นำมาใช้ซ้ำเว้นแต่คุณจะปล่อยให้เป็นอิสระ
การเพิ่มหน่วยความจำ แต่ทำให้มีการอ้างอิง (ตอนนี้ไม่ถูกต้อง)
รื้อถอนบ้านลบกระดาษชิ้นหนึ่ง แต่คุณยังมีกระดาษอีกแผ่นหนึ่งซึ่งมีที่อยู่เดิมอยู่เมื่อคุณไปที่ที่อยู่คุณจะไม่พบบ้าน แต่คุณอาจพบบางสิ่งที่คล้ายคลึงกับซากปรักหักพัง ของหนึ่ง
บางทีคุณอาจจะพบบ้าน แต่มันไม่ใช่บ้านที่คุณได้รับที่อยู่เดิมและความพยายามใด ๆ ที่จะใช้ราวกับว่ามันเป็นของคุณอาจล้มเหลวอย่างน่ากลัว
บางครั้งคุณอาจพบว่าที่อยู่ใกล้เคียงมีบ้านหลังใหญ่ตั้งอยู่บนที่อยู่สามแห่ง (ถนนสายหลัก 1-3) และที่อยู่ของคุณจะอยู่ตรงกลางของบ้าน ความพยายามใด ๆ ที่จะปฏิบัติต่อบ้านหลังใหญ่ 3 หลังที่เป็นบ้านหลังเล็ก ๆ นั้นอาจล้มเหลวอย่างน่ากลัว
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := h1; // copies the address, not the house
...
h1.Free;
h1 := nil;
h2.OpenFrontDoor; // uh-oh, what happened to our house?
ที่นี่บ้านถูกทำลายลงโดยการอ้างอิงในh1
และในขณะที่h1
ถูกล้างเช่นกันh2
ยังคงมีที่อยู่เก่าล้าสมัยที่อยู่ การเข้าสู่บ้านที่ไม่อาจยืนหรืออาจไม่ทำงานอีกต่อไป
นี่คือรูปแบบของตัวชี้ห้อยด้านบน ดูรูปแบบหน่วยความจำ
บัฟเฟอร์ล้น
คุณย้ายสิ่งต่าง ๆ เข้ามาในบ้านมากกว่าที่คุณสามารถใส่เข้าไปในบ้านเพื่อนบ้านหรือสนามหญ้า เมื่อเจ้าของบ้านใกล้เคียงนั้นกลับมาบ้านเขาจะพบทุกสิ่งที่เขาจะพิจารณาด้วยตัวเอง
นี่คือเหตุผลที่ฉันเลือกอาร์เรย์ที่มีขนาดคงที่ ในการตั้งเวทีให้สันนิษฐานได้ว่าบ้านหลังที่สองที่เราจัดสรรไว้จะด้วยเหตุผลบางประการก่อนที่จะมีการสร้างบ้านหลังแรกในหน่วยความจำ กล่าวอีกนัยหนึ่งบ้านหลังที่สองจะมีที่อยู่ต่ำกว่าบ้านหลังแรก นอกจากนี้ยังมีการจัดสรรติดกัน
ดังนั้นรหัสนี้:
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := THouse.Create('My other house somewhere');
^-----------------------^
longer than 10 characters
0123456789 <-- 10 characters
เลย์เอาต์หน่วยความจำหลังจากการจัดสรรครั้งแรก:
h1
โวลต์
----------------------- [ttttNNNNNNNNNN]
5678 บ้านของฉัน
เลย์เอาต์หน่วยความจำหลังจากการจัดสรรครั้งที่สอง:
h2 h1
VV
--- [ttttNNNNNNNNNN] ---- [ttttNNNNNNNNNN]
1234 บ้านของฉันอยู่ที่อื่น
^ --- + - ^
|
+ - เขียนทับ
ส่วนที่มักจะทำให้เกิดความผิดพลาดคือเมื่อคุณเขียนทับส่วนสำคัญของข้อมูลที่คุณเก็บไว้ซึ่งไม่ควรเปลี่ยนแบบสุ่ม ตัวอย่างเช่นมันอาจจะไม่เป็นปัญหาที่บางส่วนของชื่อของ h1-house ถูกเปลี่ยนในแง่ของการหยุดทำงานของโปรแกรม แต่การเขียนทับค่าใช้จ่ายของวัตถุจะมีความผิดพลาดมากที่สุดเมื่อคุณพยายามใช้วัตถุที่แตกหักเช่น ลิงค์เขียนทับที่เก็บไว้ในวัตถุอื่นในวัตถุ
รายการที่เชื่อมโยง
เมื่อคุณทำตามที่อยู่บนกระดาษคุณจะไปที่บ้านและที่บ้านนั้นมีกระดาษอีกแผ่นหนึ่งที่มีที่อยู่ใหม่สำหรับบ้านหลังถัดไปในโซ่และอื่น ๆ
var
h1, h2: THouse;
begin
h1 := THouse.Create('Home');
h2 := THouse.Create('Cabin');
h1.NextHouse := h2;
ที่นี่เราสร้างลิงค์จากบ้านของเราไปที่กระท่อมของเรา เราสามารถติดตามโซ่ได้จนกว่าบ้านจะไม่มีNextHouse
การอ้างอิงซึ่งหมายความว่ามันเป็นบ้านหลังสุดท้าย หากต้องการเยี่ยมชมบ้านของเราทั้งหมดเราสามารถใช้รหัสต่อไปนี้:
var
h1, h2: THouse;
h: THouse;
begin
h1 := THouse.Create('Home');
h2 := THouse.Create('Cabin');
h1.NextHouse := h2;
...
h := h1;
while h <> nil do
begin
h.LockAllDoors;
h.CloseAllWindows;
h := h.NextHouse;
end;
เลย์เอาต์ของหน่วยความจำ (เพิ่ม NextHouse เป็นลิงก์ในวัตถุซึ่งระบุด้วย LLLL สี่ตัวในแผนภาพด้านล่าง):
h1 h2
VV
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
1234Home + 5678 ห้องโดยสาร +
| ^ |
+ -------- + * (ไม่มีลิงก์)
ในแง่พื้นฐานที่อยู่หน่วยความจำคืออะไร?
ที่อยู่หน่วยความจำอยู่ในเงื่อนไขพื้นฐานเพียงตัวเลข หากคุณคิดว่าหน่วยความจำเป็นอาร์เรย์ขนาดใหญ่ไบต์แรกสุดจะมีที่อยู่ 0 เป็นที่อยู่ถัดไปคือที่อยู่ 1 เป็นต้นไป สิ่งนี้ง่าย แต่ดีพอ
ดังนั้นรูปแบบหน่วยความจำนี้:
h1 h2
VV
--- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN]
1234 บ้านของฉัน 5678 บ้านของฉัน
อาจมีที่อยู่ทั้งสองนี้ (ซ้ายสุด - ที่อยู่ 0):
ซึ่งหมายความว่ารายการที่ลิงก์ของเราด้านบนอาจมีลักษณะเช่นนี้:
h1 (= 4) h2 (= 28)
VV
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
1234Home 0028 5678 ห้องโดยสาร 0000
| ^ |
+ -------- + * (ไม่มีลิงก์)
เป็นเรื่องปกติในการจัดเก็บที่อยู่ที่ "ชี้ที่ไหน" เป็นศูนย์ที่อยู่
ในแง่พื้นฐานตัวชี้คืออะไร?
ตัวชี้เป็นเพียงตัวแปรที่เก็บที่อยู่หน่วยความจำ โดยทั่วไปคุณสามารถขอให้ภาษาการเขียนโปรแกรมให้หมายเลขของคุณ แต่ภาษาการเขียนโปรแกรมและ runtimes ส่วนใหญ่พยายามซ่อนความจริงที่ว่ามีตัวเลขอยู่ด้านล่างเพียงเพราะตัวเลขนั้นไม่ได้มีความหมายใด ๆ กับคุณ เป็นการดีที่สุดที่จะคิดว่าตัวชี้เป็นกล่องดำกล่าวคือ คุณไม่ทราบหรือสนใจว่าจะใช้งานจริงได้อย่างไรตราบเท่าที่ใช้งานได้จริง