อะไรคืออุปสรรคในการทำความเข้าใจพอยน์เตอร์และสิ่งที่สามารถทำได้เพื่อเอาชนะพวกเขา? [ปิด]


449

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

มีวิธีปฏิบัติที่ดีอะไรบ้างที่สามารถทำได้เพื่อนำคนไปสู่ระดับ "Ah-hah ฉันเข้าใจแล้ว" โดยไม่ทำให้พวกเขาจมอยู่กับแนวคิดโดยรวม โดยทั่วไปการฝึกซ้อมอย่างสถานการณ์


20
วิทยานิพนธ์ของคำถามนี้คือพอยน์เตอร์ยากที่จะเข้าใจ คำถามไม่มีหลักฐานว่าพอยน์เตอร์เข้าใจยากกว่าสิ่งอื่นใด
bmargulies

14
บางทีฉันอาจจะขาดอะไรบางอย่าง (เพราะฉันรหัสในภาษา GCC) แต่ฉันคิดเสมอว่าพอยน์เตอร์ในหน่วยความจำเป็นโครงสร้าง Key-> Value เนื่องจากมีราคาแพงในการส่งข้อมูลจำนวนมากในโปรแกรมคุณจึงสร้างโครงสร้าง (ค่า) และส่งไปรอบ ๆ ตัวชี้ / การอ้างอิง (คีย์) เนื่องจากคีย์เป็นตัวแทนที่มีขนาดเล็กกว่าของโครงสร้างที่ใหญ่กว่า ส่วนที่ยากคือเมื่อคุณต้องการเปรียบเทียบสองพอยน์เตอร์ / การอ้างอิง (คุณกำลังเปรียบเทียบคีย์หรือค่า) ซึ่งต้องการงานเพิ่มเติมเพื่อเจาะเข้าไปในข้อมูลที่มีอยู่ภายในโครงสร้าง (ค่า)
Evan Plaice

2
@ Wolfpack'08 "ดูเหมือนว่าฉันที่อยู่ในความทรงจำจะเป็น int เสมอ" - ดูเหมือนว่าคุณจะไม่มีอะไรมีประเภทเพราะมันเป็นเพียงส่วนหนึ่งของหน่วยความจำ "ที่จริงแล้วประเภทของตัวชี้เป็นประเภทของ var ที่ตัวชี้ชี้ไปที่" - ไม่ประเภทของตัวชี้คือตัวชี้ไปยังประเภทของ var ที่ตัวชี้ชี้ไปที่ - ซึ่งเป็นธรรมชาติและควรจะชัดเจน
Jim Balter

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

8
โดยสรุปแล้วนักเรียนอาจไม่เข้าใจเพราะพวกเขาไม่เข้าใจอย่างถูกต้องหรือทั้งหมดวิธีการทำงานของหน่วยความจำของคอมพิวเตอร์โดยทั่วไปและโดยเฉพาะอย่างยิ่งC "โมเดลหน่วยความจำ"ทำงาน หนังสือเล่มนี้การเขียนโปรแกรมจาก Ground Upให้บทเรียนที่ดีมากในหัวข้อเหล่านี้
Abbafei

คำตอบ:


745

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

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

ฉันได้เพิ่มรหัส 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 = 23

ซึ่งหมายความว่ารายการที่ลิงก์ของเราด้านบนอาจมีลักษณะเช่นนี้:

    h1 (= 4) h2 (= 28)
    VV
--- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL]
    1234Home 0028 5678 ห้องโดยสาร 0000
                   | ^ |
                   + -------- + * (ไม่มีลิงก์)

เป็นเรื่องปกติในการจัดเก็บที่อยู่ที่ "ชี้ที่ไหน" เป็นศูนย์ที่อยู่


ในแง่พื้นฐานตัวชี้คืออะไร?

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


59
บัฟเฟอร์ที่ overrun นั้นเฮฮา "เพื่อนบ้านกลับมาถึงกะโหลกศีรษะของเขาแตกเปิดทิ้งไว้ที่ขยะของคุณและฟ้องร้องคุณให้หลงลืม"
gtd

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

10
แต่ขอให้ถามอะไรที่ทำให้คุณสับสนเกี่ยวกับพอยน์เตอร์?
Lasse V. Karlsen

11
ฉันกลับมาที่โพสต์นี้หลายครั้งตั้งแต่คุณเขียนคำตอบ คำชี้แจงของคุณเกี่ยวกับรหัสนั้นยอดเยี่ยมมากและฉันขอขอบคุณที่คุณกลับมาอีกครั้งเพื่อเพิ่ม / ปรับปรุงความคิด ไชโย Lasse!
David McGraw

3
ไม่มีวิธีหน้าเดียวของข้อความ (ไม่ว่าจะนานแค่ไหน) สามารถสรุปความแตกต่างของการจัดการหน่วยความจำการอ้างอิงพอยน์เตอร์และอื่น ๆ ตั้งแต่ 465 คนโหวตสิ่งนี้ฉันก็บอกว่ามันทำหน้าที่ได้ดีพอ หน้าเริ่มต้นในข้อมูล มีอะไรให้เรียนอีกไหม? แน่นอนว่าเมื่อไหร่?
Lasse V. Karlsen

153

ในชั้นเรียน Comp Sci แรกของฉันเราทำแบบฝึกหัดต่อไปนี้ จริงอยู่ที่นี่เป็นห้องบรรยายที่มีนักเรียนประมาณ 200 คนในนั้น ...

ศาสตราจารย์เขียนบนกระดาน: int john;

จอห์นยืนขึ้น

ศาสตราจารย์เขียน: int *sally = &john;

แซลลี่ยืนขึ้นคะแนนที่จอห์น

ศาสตราจารย์: int *bill = sally;

บิลยืนขึ้นคะแนนที่จอห์น

ศาสตราจารย์: int sam;

แซมยืนขึ้น

ศาสตราจารย์: bill = &sam;

ตอนนี้บิลชี้ไปที่แซม

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


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

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

59
เหมือนแซมใช้ที่นั่งของจอห์นและจอห์นลอยไปรอบ ๆ ห้องจนกว่าเขาจะชนกับอะไรบางอย่างที่สำคัญ
just_wes

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

ปัญหาของตัวอย่างประเภทนี้คือการชี้ไปที่ X และ X ไม่เหมือนกัน และนี่ไม่ได้แสดงให้เห็นถึงผู้คน
Isaac Nequittepas

124

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


15
ฉันชอบสิ่งนี้มาก ไม่ยากที่จะเห็นว่าการเขียนไฮเปอร์ลิงก์ออกมาสองครั้งไม่ได้ทำให้เว็บไซต์ทั้งสองปรากฏขึ้น (เช่นเดียวกับที่int *a = bไม่ได้ทำสำเนาสองชุด*b)
Det

4
นี่เป็นสิ่งที่เข้าใจง่ายมากและทุกสิ่งที่ทุกคนควรเกี่ยวข้อง แม้ว่าจะมีหลายสถานการณ์ที่การเปรียบเทียบนี้แยกออกจากกัน ยอดเยี่ยมสำหรับการแนะนำอย่างรวดเร็วว่า +1
Brian Wigginton

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

@ThoAppelsin ไม่จำเป็นต้องเป็นจริงหากคุณเข้าถึงเว็บเพจ HTML แบบคงที่เช่นคุณกำลังเข้าถึงไฟล์เดียวบนเซิร์ฟเวอร์
dramzy

5
คุณคิดมาก ไฮเปอร์ลิงก์ชี้ไปที่ไฟล์บนเซิร์ฟเวอร์นั่นคือขอบเขตของการเปรียบเทียบ
dramzy

48

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

การฝึกซ้อมคือการขอให้พวกเขาใช้เครื่องเสมือนแบบพื้นฐานของ bytecode (ในภาษาใด ๆ ที่พวกเขาเลือกงูใหญ่ใช้งานได้ดีในเรื่องนี้) พร้อมชุดคำสั่งที่เน้นการปฏิบัติงานของตัวชี้ (โหลดจัดเก็บการกำหนดที่อยู่ จากนั้นให้นักเรียนเขียนโปรแกรมง่าย ๆ สำหรับชุดคำสั่งนั้น

สิ่งที่ต้องการมากกว่าการเพิ่มง่ายๆเล็กน้อยจะเกี่ยวข้องกับพอยน์เตอร์และพวกเขามั่นใจว่าจะได้รับ


2
น่าสนใจ ไม่มีความคิดวิธีการเริ่มต้นเกี่ยวกับเรื่องนี้แม้ว่า ทรัพยากรใดที่จะแบ่งปัน?
Karolis

1
ฉันเห็นด้วย. ตัวอย่างเช่นฉันเรียนรู้ที่จะเขียนโปรแกรมในที่ประชุมก่อนคและรู้วิธีลงทะเบียนทำงานการเรียนรู้พอยน์เตอร์เป็นเรื่องง่าย ในความเป็นจริงมีการเรียนรู้ไม่มากมันทั้งหมดเป็นธรรมชาติมาก
Milan Babuškov

ใช้ซีพียูพื้นฐานพูดสิ่งที่เรียกใช้ lawnmowers หรือเครื่องล้างจาน หรือเซตย่อยพื้นฐานของ ARM หรือ MIPS ทั้งสองมี ISA ง่าย ๆ
Daniel Goldberg

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

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

27

เหตุใดพอยน์เตอร์จึงเป็นปัจจัยนำของความสับสนสำหรับนักเรียนระดับใหม่และระดับเก่าในภาษา C / C ++?

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

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

ที่อยู่กล่อง ฉันจำได้ว่าตอนที่ฉันเรียนรู้ที่จะเขียนโปรแกรม BASIC ลงในไมโครคอมพิวเตอร์มีหนังสือสวย ๆ ที่มีเกมอยู่ในนั้นและบางครั้งคุณก็ต้องเจาะค่าลงในที่อยู่เฉพาะ พวกเขามีรูปของกล่องที่มีป้ายกำกับเพิ่มขึ้นด้วย 0, 1, 2 ... และมีการอธิบายว่ามีเพียงสิ่งเล็ก ๆ (ไบต์) เท่านั้นที่สามารถใส่ในกล่องเหล่านี้และมีจำนวนมาก - คอมพิวเตอร์บางเครื่อง มีมากถึง 65535! พวกเขาอยู่ติดกันและพวกเขาทั้งหมดมีที่อยู่

มีวิธีปฏิบัติที่ดีอะไรบ้างที่สามารถทำได้เพื่อนำคนไปสู่ระดับ "Ah-hah ฉันเข้าใจแล้ว" โดยไม่ทำให้พวกเขาจมอยู่กับแนวคิดโดยรวม โดยทั่วไปการฝึกซ้อมอย่างสถานการณ์

สำหรับการฝึกซ้อม? สร้าง struct:

struct {
char a;
char b;
char c;
char d;
} mystruct;
mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;
cout << 'Start: my_pointer = ' << *my_pointer << endl;
my_pointer++;
cout << 'After: my_pointer = ' << *my_pointer << endl;
my_pointer = &mystruct.a;
cout << 'Then: my_pointer = ' << *my_pointer << endl;
my_pointer = my_pointer + 3;
cout << 'End: my_pointer = ' << *my_pointer << endl;

ตัวอย่างเช่นข้างต้นยกเว้นใน C:

// Same example as above, except in C:
struct {
    char a;
    char b;
    char c;
    char d;
} mystruct;

mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';

char* my_pointer;
my_pointer = &mystruct.b;

printf("Start: my_pointer = %c\n", *my_pointer);
my_pointer++;
printf("After: my_pointer = %c\n", *my_pointer);
my_pointer = &mystruct.a;
printf("Then: my_pointer = %c\n", *my_pointer);
my_pointer = my_pointer + 3;
printf("End: my_pointer = %c\n", *my_pointer);

เอาท์พุท:

Start: my_pointer = s
After: my_pointer = t
Then: my_pointer = r
End: my_pointer = u

บางทีนั่นอาจอธิบายพื้นฐานบางอย่างผ่านตัวอย่าง?


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

4
ฉันรู้ว่านี่เป็นการโพสต์เก่า แต่มันจะดีถ้าเอาท์พุทของรหัสที่ให้ไว้ถูกเพิ่มเข้าไปในการโพสต์
Josh

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

24

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

มีคนอื่นเชื่อมโยงกับบทช่วยสอนนี้แล้ว แต่ฉันสามารถเน้นช่วงเวลาที่ฉันเริ่มเข้าใจตัวชี้:

บทช่วยสอนเกี่ยวกับพอยน์เตอร์และอาร์เรย์ใน C: บทที่ 3 - พอยน์เตอร์และสตริง

int puts(const char *s);

ในตอนนี้ให้ข้ามconst.พารามิเตอร์ที่ส่งผ่านไปยังputs()ตัวชี้นั่นคือค่าของตัวชี้ (เนื่องจากพารามิเตอร์ทั้งหมดใน C ถูกส่งผ่านตามค่า) และค่าของตัวชี้เป็นที่อยู่ที่จะชี้หรือเพียงแค่ , ที่อยู่. ดังนั้นเมื่อเราเขียนputs(strA);ตามที่เราเห็นเรากำลังผ่านที่อยู่ของ strA [0]

ขณะที่ฉันอ่านคำเหล่านี้เมฆก็แยกจากกันและลำแสงที่ห้อมล้อมฉันด้วยความเข้าใจของตัวชี้

แม้ว่าคุณจะเป็นนักพัฒนา VB .NET หรือ C # (เหมือนฉัน) และไม่เคยใช้รหัสที่ไม่ปลอดภัย แต่ก็ยังคุ้มค่าที่จะเข้าใจว่าพอยน์เตอร์ทำงานอย่างไรหรือคุณจะไม่เข้าใจว่าการอ้างอิงวัตถุทำงานอย่างไร จากนั้นคุณจะมีความคิดทั่วไป แต่เข้าใจผิดว่าผ่านการอ้างอิงวัตถุไปยังวิธีการคัดลอกวัตถุ


ทำให้ฉันสงสัยว่าจุดที่มีตัวชี้คืออะไร ในบล็อกโค้ดส่วนใหญ่ที่ฉันพบตัวชี้จะเห็นได้ในการประกาศ
Wolfpack'08

@ Wolfpack'08 ... อะไรนะ? คุณกำลังอ่านโค้ดอะไรอยู่ ??
Kyle Strand

@ KyleStrand ให้ฉันได้ดู
Wolfpack'08

19

ฉันพบ "การสอนเกี่ยวกับพอยน์เตอร์และอาร์เรย์ใน C" ของ Ted Jensen ซึ่งเป็นแหล่งข้อมูลที่ยอดเยี่ยมสำหรับการเรียนรู้เกี่ยวกับพอยน์เตอร์ มันแบ่งออกเป็น 10 บทเรียนเริ่มต้นด้วยคำอธิบายว่าพอยน์เตอร์คืออะไร (และใช้เพื่ออะไร) และจบด้วยพอยน์เตอร์ของฟังก์ชัน http://home.netcom.com/~tjensen/ptr/cpoint.htm

จากที่นั่น Beej's Guide to Network Programming สอน Unix sockets API ซึ่งคุณสามารถเริ่มทำสิ่งสนุก ๆ ได้ http://beej.us/guide/bgnet/


1
ฉันกวดวิชาที่สองของเท็ดเซ่น มันแบ่งพอยน์เตอร์ให้อยู่ในระดับของรายละเอียดนั่นไม่ละเอียดเกินไปไม่มีหนังสือที่ฉันอ่านทำ มีประโยชน์อย่างยิ่ง! :)
Dave Gallagher

12

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

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

widget->wazzle.fizzle = fazzle.foozle->wazzle;

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

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

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


10

ฉันคิดว่าฉันจะเพิ่มการเปรียบเทียบลงในรายการนี้ซึ่งฉันพบว่ามีประโยชน์มากเมื่ออธิบายตัวชี้ (ย้อนกลับไปในวันนั้น) เป็นติวเตอร์วิทยาศาสตร์คอมพิวเตอร์ ก่อนอื่นมา:


ตั้งเวที :

พิจารณาที่จอดรถที่มี 3 ช่องว่างช่องว่างเหล่านี้มีหมายเลข

-------------------
|     |     |     |
|  1  |  2  |  3  |
|     |     |     |

ในทางนี้เป็นเหมือนที่ตั้งหน่วยความจำพวกเขาเป็นลำดับและต่อเนื่อง .. เรียงลำดับเหมือนอาร์เรย์ ตอนนี้ไม่มีรถยนต์อยู่เลยดังนั้นมันจึงเป็นอาร์เรย์ที่ว่างเปล่า ( parking_lot[3] = {0})


เพิ่มข้อมูล

ที่จอดรถไม่เคยว่างเปล่าเป็นเวลานาน ... ถ้าเป็นเช่นนั้นจะไม่มีจุดหมายและไม่มีใครสร้างเลย ดังนั้นสมมติว่าเมื่อวันที่ย้ายไปที่ล็อตเติมด้วย 3 คัน, รถสีฟ้า, รถสีแดงและรถสีเขียว:

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |R| | |G| |
| o-o | o-o | o-o |

รถยนต์เหล่านี้ล้วนเป็นชนิดเดียวกัน (รถยนต์) ดังนั้นวิธีหนึ่งที่จะคิดว่านี้คือการที่รถของเรามีการจัดเรียงของข้อมูลบางส่วน (พูดint) แต่พวกเขามีค่าที่แตกต่างกัน ( blue, red, greenนั่นอาจจะเป็นสีenum)


ป้อนตัวชี้

ทีนี้ถ้าฉันพาคุณไปที่ลานจอดรถนี้และขอให้คุณหารถสีฟ้าให้ฉันขยายนิ้วหนึ่งนิ้วและใช้มันชี้ไปที่รถสีฟ้าในจุดที่ 1 นี่เหมือนกับการชี้และกำหนดไปยังที่อยู่หน่วยความจำ ( int *finger = parking_lot)

นิ้วของคุณ (ตัวชี้) ไม่ใช่คำตอบสำหรับคำถามของฉัน การมองที่นิ้วของคุณไม่ได้บอกอะไรเลย แต่ถ้าฉันมองว่านิ้วของคุณชี้ไปที่ใด (ลงทะเบียนตัวชี้) ฉันสามารถหารถ (ข้อมูล) ที่ฉันกำลังค้นหา


การกำหนดตัวชี้อีกครั้ง

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

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


พอยน์เตอร์คู่ (หรือตัวชี้ไปยังตัวชี้)

ใช้ได้กับตัวชี้มากกว่าหนึ่งตัวเช่นกัน ฉันสามารถถามได้ว่าตัวชี้อยู่ที่ไหนซึ่งชี้ไปที่รถสีแดงและคุณสามารถใช้มืออื่น ๆ และใช้นิ้วชี้ไปที่นิ้วแรก (เป็นเช่นนี้int **finger_two = &finger)

ทีนี้ถ้าฉันอยากรู้ว่ารถสีฟ้าคันไหนที่ฉันสามารถทำตามทิศทางของนิ้วแรกไปยังนิ้วที่สองไปที่รถ (ข้อมูล)


ตัวชี้ห้อย

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

   1     2     3
-------------------
| o=o |     | o=o |
| |B| |     | |G| |
| o-o |     | o-o |

ตัวชี้ของคุณยังคงชี้ไปที่รถสีแดงอยู่แต่ไม่อยู่อีกต่อไป สมมติว่ามีรถใหม่มาที่นั่น ... รถยนต์สีส้ม ตอนนี้ถ้าฉันถามคุณอีกครั้งว่า "รถสีแดงอยู่ที่ไหน" คุณยังคงชี้ไปที่นั่น แต่ตอนนี้คุณผิด นั่นไม่ใช่รถสีแดงนั่นคือสีส้ม


เลขคณิตของตัวชี้

ตกลงดังนั้นคุณยังคงชี้ไปที่จุดจอดรถคันที่สอง (ตอนนี้อยู่ในรถสีส้ม)

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |O| | |G| |
| o-o | o-o | o-o |

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


อย่าใช้คำว่า "double pointer" พอยน์เตอร์สามารถชี้ไปที่อะไรก็ได้ดังนั้นแน่นอนว่าคุณสามารถมีพอยน์เตอร์ที่ชี้ไปที่พอยน์เตอร์อื่น พวกเขาไม่ได้เป็นตัวชี้สองเท่า
gnasher729

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

1
@Emmet - ฉันไม่เห็นด้วยที่มีอีกมากที่สามารถเข้าสู่ตัวชี้ WRT ได้ แต่ฉันอ่านคำถาม: "without getting them bogged down in the overall concept"เป็นความเข้าใจระดับสูง และถึงจุดของคุณ: "I'm not sure that people have any difficulty understanding pointers at the high level of abstraction"- คุณจะประหลาดใจมากกี่คนที่ไม่เข้าใจตัวชี้ถึงระดับนี้
Mike

มีบุญในการขยายการเปรียบเทียบนิ้วรถไปยังบุคคล (ด้วยนิ้วเดียวหรือมากกว่า - และความผิดปกติทางพันธุกรรมที่สามารถอนุญาตให้แต่ละคนชี้ไปในทิศทางใดก็ได้!) นั่งในรถคันหนึ่งที่ชี้ไปที่รถคันอื่น งอเหนือการชี้ไปที่ความสูญเปล่าข้างๆล็อตในฐานะ "ตัวชี้เริ่มต้น" หรือมือทั้งสองพุ่งออกมาชี้ไปที่แถวของช่องว่างในฐานะ "ขนาดคงที่ [5] อาเรย์ของพอยน์เตอร์" หรือขดลงในฝ่ามือ "ตัวชี้โมฆะ" ที่ชี้ไปที่บางแห่งที่เป็นที่รู้จักกันว่าไม่มีรถ) ... 8-)
SlySven

คำอธิบายของคุณนั้นประเมินได้และเป็นคำตอบที่ดีสำหรับผู้เริ่มต้น
Yatendra Rathore

9

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

อย่างน้อยสิ่งที่ฉันเคยประสบในอดีตและเห็นคนอื่นจัดการกับปัญหาก็คือการจัดการตัวชี้ใน C / C ++ สามารถทำให้สับสนได้โดยไม่จำเป็น


9

ตัวอย่างของบทช่วยสอนที่มีชุดไดอะแกรมที่ดีจะช่วยให้เข้าใจพอยน์เตอร์ได้อย่างมาก

Joel Spolsky ให้คะแนนที่ดีเกี่ยวกับการทำความเข้าใจพอยน์เตอร์ในการรบแบบกองโจร Guide to Interviewing article:

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


8

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

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

เมื่อ api พูดว่า:

int doIt(char *buffer )
//*buffer is a pointer to the buffer

มันต้องการอะไร

มันอาจต้องการ:

ตัวเลขที่แสดงแอดเดรสไปยังบัฟเฟอร์

(เพื่อให้มันฉันจะพูดdoIt(mybuffer)หรือdoIt(*myBuffer)?)

ตัวเลขแสดงถึงที่อยู่ไปยังที่อยู่ในบัฟเฟอร์

(นั่นคือdoIt(&mybuffer)หรือdoIt(mybuffer)หรือdoIt(*mybuffer)?)

ตัวเลขแสดงที่อยู่ไปยังที่อยู่ไปยังที่อยู่ไปยังบัฟเฟอร์

(อาจจะเป็นdoIt(&mybuffer)หรือว่าเป็นdoIt(&&mybuffer)หรือไม่หรือแม้แต่doIt(&&&mybuffer))

และอื่น ๆ และภาษาที่เกี่ยวข้องไม่ได้ทำให้มันชัดเจนเพราะมันเกี่ยวข้องกับคำว่า "ตัวชี้" และ "การอ้างอิง" ที่ไม่ถือความหมายและความชัดเจนให้ฉันมากเท่ากับ "x ถือที่อยู่เป็น y" และ " ฟังก์ชันนี้ต้องใช้ที่อยู่เป็น y " คำตอบนั้นขึ้นอยู่กับสิ่งที่ heck "mybuffer" จะเริ่มต้นด้วยและสิ่งที่ doIt ตั้งใจจะทำกับมัน ภาษาไม่รองรับระดับการซ้อนที่พบในทางปฏิบัติ เช่นเมื่อฉันต้องส่ง "ตัวชี้" ไปยังฟังก์ชันที่สร้างบัฟเฟอร์ใหม่และจะปรับเปลี่ยนตัวชี้ให้ชี้ไปที่ตำแหน่งใหม่ของบัฟเฟอร์ มันต้องการตัวชี้หรือตัวชี้ไปยังตัวชี้จริง ๆ หรือไม่ดังนั้นจึงรู้ว่าจะไปแก้ไขเนื้อหาของตัวชี้ได้ที่ไหน เวลาส่วนใหญ่ฉันเพียงแค่ต้องเดาสิ่งที่มีความหมายโดย "

"Pointer" โอเวอร์โหลดมากเกินไป ตัวชี้เป็นที่อยู่ของค่าหรือไม่ หรือมันเป็นตัวแปรที่เก็บที่อยู่กับค่า เมื่อฟังก์ชั่นต้องการตัวชี้มันต้องการที่อยู่ที่ตัวแปรตัวชี้ถืออยู่หรือไม่หรือมันต้องการที่อยู่ไปยังตัวแปรตัวชี้? ฉันสับสน


ผมเคยเห็นมันอธิบายเช่นนี้ถ้าคุณเห็นการประกาศตัวชี้เช่นdouble *(*(*fn)(int))(char)นั้นผลจากการประเมินจะเป็น*(*(*fn)(42))('x') doubleคุณสามารถถอดเลเยอร์ของการประเมินเพื่อทำความเข้าใจว่าต้องใช้สื่อประเภทใด
Bernd Jendrissek

@BerndJendrissek ไม่แน่ใจว่าฉันติดตาม ผลลัพธ์ของการประเมิน (*(*fn)(42))('x') คืออะไร
Breton

คุณได้อะไร (ลองเรียกมันว่าx) ตรงไหนถ้าคุณประเมิน*xคุณจะได้สองเท่า
Bernd Jendrissek

@BerndJendrissek นี่ควรอธิบายบางอย่างเกี่ยวกับพอยน์เตอร์หรือไม่? ฉันไม่เข้าใจ ประเด็นของคุณคืออะไร? ฉันลอกเลเยอร์ออกแล้วและไม่ได้รับข้อมูลใหม่เกี่ยวกับประเภทสื่อกลางใด ๆ มันอธิบายอะไรเกี่ยวกับฟังก์ชั่นเฉพาะที่จะยอมรับได้? มันเกี่ยวข้องกับอะไร?
Breton

บางทีข้อความในคำอธิบายนี้ (และมันไม่ได้เป็นฉันฉันหวังว่าฉันจะหาสถานที่ที่ผมเห็นมันครั้งแรก) คือการคิดว่ามันน้อยกว่าในแง่ของสิ่งที่fn เป็นและอื่น ๆ ในแง่ของสิ่งที่คุณสามารถทำได้ด้วยfn
แบร์น Jendrissek

8

ฉันคิดว่าอุปสรรคสำคัญในการเข้าใจพอยน์เตอร์คือครูที่ไม่ดี

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

และแน่นอนว่าพวกเขายากที่จะเข้าใจอันตรายและกึ่งวิเศษ

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

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

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


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

ไม่จำเป็น. คุณต้องสามารถสร้างแบบจำลองคอมพิวเตอร์ในหัวของคุณเพื่อให้ได้พอยน์เตอร์ที่เหมาะสม (และเพื่อดีบักโปรแกรมอื่น ๆ ด้วย) ทุกคนไม่สามารถทำได้
Thorbjørn Ravn Andersen

5

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

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

ฉันไม่เห็นด้วยกับความคิดที่ว่า "คุณจะได้รับพวกเขาหรือคุณไม่ได้"

พวกเขาจะเข้าใจง่ายขึ้นเมื่อคุณเริ่มค้นหาการใช้งานจริงสำหรับพวกเขา (เช่นไม่ส่งโครงสร้างขนาดใหญ่ไปยังฟังก์ชัน)


5

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

   int *mypointer;

คุณจะได้เรียนรู้ก่อนว่าส่วนด้านซ้ายสุดของการสร้างตัวแปรกำหนดประเภทของตัวแปร การประกาศตัวชี้ไม่ทำงานเช่นนี้ใน C และ C ++ แต่พวกเขาบอกว่าตัวแปรชี้ไปที่ประเภททางซ้าย ในกรณีนี้: *mypointer ชี้ไปที่ int

ฉันไม่เข้าใจตัวชี้อย่างเต็มที่จนกว่าฉันจะลองใช้ใน C # (ไม่ปลอดภัย) พวกเขาทำงานในลักษณะเดียวกัน แต่ด้วยตรรกะและไวยากรณ์ที่สอดคล้องกัน ตัวชี้เป็นชนิดของตัวเอง นี่mypointer เป็นตัวชี้ไปยัง int

  int* mypointer;

อย่าให้ฉันเริ่มต้นด้วยตัวชี้ฟังก์ชั่น ...


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

1
ส่วนที่สองนั้นใช้งานไม่ได้ดีกับการประกาศที่ซับซ้อนมากขึ้น และไวยากรณ์ไม่เป็น "ไม่สอดคล้อง" เมื่อคุณตระหนักว่าส่วนทางด้านขวาของการประกาศตัวชี้แสดงให้คุณเห็นสิ่งที่คุณต้องทำกับตัวชี้เพื่อให้ได้สิ่งที่มีประเภทเป็นตัวระบุประเภทอะตอมทางด้านซ้าย
Bernd Jendrissek

2
int *p;มีความหมายง่าย ๆ : *pเป็นจำนวนเต็ม int *p, **ppหมายถึง: *pและ**ppเป็นจำนวนเต็ม
Miles Rout

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

@LightnessRacesinOrbit การจัดตำแหน่งที่ถูกต้องอยู่ไกลจากการเข้าใจผิด มันเป็นวิธีการสอนที่ถูกต้องเท่านั้น ไม่สอนมันทำให้เข้าใจผิด
Miles Rout

5

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


4

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

ตัวอย่างเช่นตามรายการที่เชื่อมโยง: 1) เริ่มต้นด้วยกระดาษของคุณพร้อมที่อยู่ 2) ไปที่ที่อยู่บนกระดาษ 3) เปิดกล่องจดหมายเพื่อค้นหากระดาษแผ่นใหม่ด้วยที่อยู่ถัดไป

ในรายการเชื่อมโยงเชิงเส้นกล่องจดหมายล่าสุดไม่มีอะไรอยู่ในนั้น (ท้ายสุดของรายการ) ในรายการที่เชื่อมโยงแบบวงกลมกล่องจดหมายล่าสุดมีที่อยู่ของกล่องจดหมายแรกในกล่องนั้น

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


ภาวะแทรกซ้อนที่น่ารังเกียจกับการเปรียบเทียบหมายเลขกล่องจดหมายคือในขณะที่ภาษาที่คิดค้นโดย Dennis Ritchie กำหนดพฤติกรรมในแง่ที่อยู่ของไบต์และค่าที่เก็บไว้ในไบต์เหล่านั้นภาษาที่กำหนดโดยมาตรฐาน C เชิญการใช้งานแบบ "เพิ่มประสิทธิภาพ" แบบจำลองที่ซับซ้อนกว่า แต่กำหนดแง่มุมต่าง ๆ ของแบบจำลองในลักษณะที่คลุมเครือขัดแย้งและไม่สมบูรณ์
supercat

3

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

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


2

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


2

ฉันคิดว่ามันอาจเป็นปัญหาเกี่ยวกับไวยากรณ์ ไวยากรณ์ C / C ++ สำหรับพอยน์เตอร์ดูเหมือนจะไม่สอดคล้องกันและซับซ้อนกว่าที่ควรจะเป็น

สิ่งที่จริง ๆ แล้วช่วยให้ฉันเข้าใจพอยน์เตอร์กำลังเผชิญหน้ากับแนวคิดของตัววนซ้ำใน c ++ Standard Template Libraryแม่แบบของไลบรารีมาตรฐานมันเป็นเรื่องที่น่าขันเพราะฉันทำได้เพียงทึกทักเอาเองว่าตัววนซ้ำนั้นเป็นลักษณะทั่วไปของตัวชี้

บางครั้งคุณก็ไม่สามารถมองเห็นป่าได้จนกว่าคุณจะเรียนรู้ที่จะไม่สนใจต้นไม้


ปัญหาส่วนใหญ่อยู่ในไวยากรณ์การประกาศ C แต่การใช้ตัวชี้จะแน่ใจได้ง่ายกว่าถ้า(*p)เป็น(p->)เช่นนั้นและด้วยเหตุนี้เราจึงมีp->->xความคลุมเครือแทน*p->x
MSalters

@Malters โอ้พระเจ้าของฉันคุณล้อเล่นใช่มั้ย ไม่มีความขัดแย้งที่นั่น เพียงแค่วิธีการa->b (*a).b
เส้นทาง Miles Rout

@Miles: แท้จริงและตรรกะที่* p->xหมายถึง* ((*a).b)ในขณะที่วิธีการ*p -> x (*(*p)) -> xการผสมคำนำหน้าและตัวดำเนินการ postfix ทำให้การแยกวิเคราะห์ไม่ชัดเจน
MSalters

@MSalters ไม่เพราะช่องว่างไม่เกี่ยวข้อง เหมือนบอกว่า1+2 * 3ควรเป็น 9
Miles Rout

2

ความสับสนนั้นมาจากเลเยอร์นามธรรมหลายชั้นที่รวมกันในแนวคิด "ตัวชี้" โปรแกรมเมอร์ไม่สับสนกับการอ้างอิงทั่วไปใน Java / Python แต่พอยน์เตอร์จะแตกต่างกันเนื่องจากมันแสดงลักษณะของสถาปัตยกรรมหน่วยความจำพื้นฐาน

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


1
สิ่งที่น่าสนใจคือตัวชี้ C จริง ๆ แล้วจะไม่เปิดเผยลักษณะสถาปัตยกรรมหน่วยความจำพื้นฐาน ความแตกต่างเพียงอย่างเดียวระหว่างการอ้างอิง Java และพอยน์เตอร์ C คือคุณสามารถมีชนิดที่ซับซ้อนที่เกี่ยวข้องกับพอยน์เตอร์ (เช่น. int *** หรือ char * ( ) (void * )), มีเลขคณิตพอยน์เตอร์สำหรับอาร์เรย์และพอยน์เตอร์ ของ void *, และ array / pointer duality นอกจากนั้นพวกเขาก็ทำงานเหมือนเดิม
jpalecek

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

@jpalecek: มันค่อนข้างง่ายที่จะเข้าใจว่าพอยน์เตอร์ทำงานอย่างไรกับการใช้งานที่บันทึกพฤติกรรมของพวกเขาในแง่ของสถาปัตยกรรมพื้นฐาน การพูดfoo[i]หมายถึงการไปยังจุดที่แน่นอนเคลื่อนไปข้างหน้าระยะทางที่กำหนดและดูว่ามีอะไรอยู่ที่นั่น สิ่งที่ซับซ้อนคือชั้น abstraction พิเศษที่ซับซ้อนยิ่งขึ้นซึ่งถูกเพิ่มโดย Standard ล้วนๆเพื่อประโยชน์ของคอมไพเลอร์ แต่โมเดลสิ่งต่าง ๆ ในแบบที่ไม่เหมาะสมสำหรับความต้องการโปรแกรมเมอร์และคอมไพเลอร์ต้องการเหมือนกัน
supercat

2

วิธีที่ฉันชอบอธิบายก็คือในแง่ของอาร์เรย์และดัชนี - คนอาจไม่คุ้นเคยกับพอยน์เตอร์ แต่พวกเขารู้ว่าดัชนีคืออะไร

ดังนั้นฉันคิดว่า RAM เป็นอาร์เรย์ (และคุณมี RAM เพียง 10 ไบต์):

unsigned char RAM[10] = { 10, 14, 4, 3, 2, 1, 20, 19, 50, 9 };

จากนั้นตัวชี้ไปยังตัวแปรนั้นเป็นเพียงดัชนีของ (ไบต์แรกของ) ตัวแปรนั้นใน RAM

ดังนั้นหากคุณมีตัวชี้ / ดัชนีunsigned char index = 2ดังนั้นค่าจะเป็นองค์ประกอบที่สามอย่างชัดเจนหรือตัวเลข 4 ตัวชี้ไปยังตัวชี้คือตำแหน่งที่คุณใช้หมายเลขนั้นและใช้เป็นดัชนีตัวเองเช่นRAM[RAM[index]]ตัวชี้ไปยังตัวชี้เป็นที่ที่คุณจะใช้ตัวเลขที่และใช้เป็นดัชนีตัวเองเช่น

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


1

หมายเลขตู้ไปรษณีย์

เป็นข้อมูลที่ช่วยให้คุณสามารถเข้าถึงสิ่งอื่นได้

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


1

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

ex-C ++ devs จำนวนมาก (ที่ไม่เข้าใจว่าตัววนซ้ำเป็นตัวชี้ที่ทันสมัยก่อนที่จะทิ้งภาษา) ข้ามไปที่ C # และยังเชื่อว่าพวกมันมีตัววนซ้ำที่เหมาะสม

อืมปัญหาคือว่าตัววนซ้ำทั้งหมดอยู่ในอัตราที่สมบูรณ์ซึ่งสิ่งที่รันไทม์ของแพลตฟอร์ม (Java / CLR) กำลังพยายามที่จะบรรลุ: การใช้งานใหม่ที่เรียบง่ายและทุกคน -a-dev ซึ่งอาจดี แต่พวกเขาพูดครั้งเดียวในหนังสือสีม่วงและพวกเขาบอกว่าแม้ก่อนและก่อนหน้า C:

ความร้าย

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

IEnumerable + LINQ + Massive Framework = การลงโทษรันไทม์ทางอ้อม 300MB จากหมัด, ลากแอพผ่าน heap ของอินสแตนซ์ของประเภทอ้างอิง ..

"Le Pointer ราคาถูก"


4
สิ่งนี้เกี่ยวข้องกับอะไร?
Neil Williams

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

Luaan คุณไม่อาจรู้ได้ว่ามีใครเรียนรู้ได้บ้างโดยถอดแยกชิ้นส่วน JIT ออกในปี 2000 มันจบลงที่ตารางกระโดดจากตารางตัวชี้ตามที่แสดงในปี 2000 ทางออนไลน์ใน ASM ดังนั้นการไม่เข้าใจความหมายที่แตกต่างกันอาจมีความหมายอื่น: การอ่านอย่างระมัดระวังเป็นทักษะที่สำคัญลองอีกครั้ง
rama-jka toti

1

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

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


0

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

int* my_int_pointer;

กล่าวว่า my_int_pointer มีที่อยู่ไปยังสถานที่ที่มี int

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


0

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

http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5พูดถึงมันมากกว่ากันหน่อย :-)


-1: หมายเลขอ้างอิงไม่ใช่ตัวชี้ไปยังตัวชี้; พวกเขาไม่ได้เป็นตัวชี้ในทุกแง่มุม อย่าสับสนพวกเขา
Ian Goldby

"พวกเขาไม่ได้เป็นพอยน์เตอร์ในแง่ใด ๆ " - อืมฉันขอแตกต่างกันไป
SarekOfVulcan

ตัวชี้คือตำแหน่งหน่วยความจำ หมายเลขอ้างอิงเป็นตัวบ่งชี้เฉพาะใด ๆ มันอาจเป็นตัวชี้ แต่ก็อาจเป็นดัชนีในอาร์เรย์หรือสิ่งอื่นใดสำหรับเรื่องนั้น ลิงก์ที่คุณให้เป็นกรณีพิเศษที่หมายเลขอ้างอิงเป็นตัวชี้ แต่ไม่จำเป็นต้องเป็น ดูเพิ่มเติมที่parashift.com/c++-faq-lite/references.html#faq-8.8
เอียนโกลด์บาย

ลิงก์นั้นไม่สนับสนุนการยืนยันของคุณว่าพวกเขาไม่ได้เป็นพอยน์เตอร์ไม่ว่าในกรณีใด - "ตัวอย่างเช่นด้ามจับอาจเป็นเฟร็ด ** ซึ่งตัวชี้เฟรด * ชี้ไปที่ ... " ฉันไม่คิดว่า -1 มีความยุติธรรม
SarekOfVulcan

0

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

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

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