อะไรคือความแตกต่างระหว่างตัวแปรตัวชี้และตัวแปรอ้างอิงใน C ++?


3262

ฉันรู้ว่าการอ้างอิงคือน้ำตาลประโยคดังนั้นรหัสจึงง่ายต่อการอ่านและเขียน

แต่อะไรคือความแตกต่าง


100
ฉันคิดว่าจุดที่ 2 ควรเป็น "ตัวชี้ได้รับอนุญาตให้เป็น NULL แต่การอ้างอิงไม่ใช่รหัสที่มีรูปแบบไม่ถูกต้องเท่านั้นที่สามารถสร้างการอ้างอิง NULL และพฤติกรรมของมันไม่ได้กำหนดไว้"
Mark Ransom

19
พอยน์เตอร์เป็นเพียงวัตถุประเภทอื่นและเช่นเดียวกับวัตถุใด ๆ ใน C ++ สามารถเป็นตัวแปรได้ การอ้างอิงในทางตรงกันข้ามจะไม่วัตถุวัตถุเพียงตัวแปร
Kerrek SB

19
คอมไพล์โดยไม่มีคำเตือน: int &x = *(int*)0;บน gcc การอ้างอิงสามารถชี้ไปที่ NULL ได้
Calmarius

20
การอ้างอิงเป็นนามแฝงตัวแปร
Khaled.K

20
ฉันชอบที่ประโยคแรกคือการเข้าใจผิดทั้งหมด การอ้างอิงมีความหมายของตัวเอง
การแข่งขัน Lightness ใน Orbit

คำตอบ:


1707
  1. ตัวชี้สามารถกำหนดใหม่ได้:

    int x = 5;
    int y = 6;
    int *p;
    p = &x;
    p = &y;
    *p = 10;
    assert(x == 5);
    assert(y == 10);

    การอ้างอิงไม่สามารถและต้องได้รับมอบหมายเมื่อเริ่มต้น:

    int x = 5;
    int y = 6;
    int &r = x;
  2. ตัวชี้มีที่อยู่หน่วยความจำและขนาดของตัวเองบนสแต็ก (4 ไบต์บน x86) ในขณะที่การอ้างอิงใช้ที่อยู่หน่วยความจำเดียวกัน (พร้อมตัวแปรดั้งเดิม) แต่ยังใช้พื้นที่บนสแต็ก เนื่องจากการอ้างอิงมีที่อยู่เดียวกันกับตัวแปรดั้งเดิมจึงปลอดภัยที่จะคิดว่าการอ้างอิงเป็นชื่ออื่นสำหรับตัวแปรเดียวกัน หมายเหตุ: ตัวชี้ใดที่สามารถอยู่บนสแต็กหรือกอง อ้างอิงเหมือนกัน การอ้างสิทธิ์ของฉันในคำชี้แจงนี้ไม่ใช่ตัวชี้ต้องชี้ไปที่สแต็ก ตัวชี้เป็นเพียงตัวแปรที่เก็บที่อยู่หน่วยความจำ ตัวแปรนี้อยู่ในสแต็ก เนื่องจากการอ้างอิงมีพื้นที่ของตัวเองบนสแต็กและเนื่องจากที่อยู่นั้นเหมือนกับตัวแปรที่อ้างอิง เพิ่มเติมเกี่ยวกับstack vs heap. นี่หมายความว่ามีที่อยู่จริงของการอ้างอิงที่คอมไพเลอร์จะไม่บอกคุณ

    int x = 0;
    int &r = x;
    int *p = &x;
    int *p2 = &r;
    assert(p == p2);
  3. คุณสามารถมีพอยน์เตอร์ให้กับพอยน์เตอร์ถึงพอยน์เตอร์ที่เสนอระดับการอ้อมเพิ่มเติม ในขณะที่การอ้างอิงมีเพียงระดับทางอ้อมเท่านั้น

    int x = 0;
    int y = 0;
    int *p = &x;
    int *q = &y;
    int **pp = &p;
    pp = &q;//*pp = q
    **pp = 4;
    assert(y == 4);
    assert(x == 0);
  4. สามารถกำหนดตัวชี้ได้nullptrโดยตรงในขณะที่การอ้างอิงไม่สามารถทำได้ หากคุณพยายามอย่างหนักพอและรู้วิธีคุณสามารถสร้างที่อยู่ของการอ้างอิงnullptrได้ nullptrในทำนองเดียวกันถ้าคุณพยายามอย่างหนักพอที่คุณสามารถมีการอ้างอิงถึงตัวชี้แล้วว่าสามารถมีการอ้างอิง

    int *p = nullptr;
    int &r = nullptr; <--- compiling error
    int &r = *p;  <--- likely no compiling error, especially if the nullptr is hidden behind a function call, yet it refers to a non-existent int at address 0
  5. พอยน์เตอร์สามารถทำซ้ำผ่านอาร์เรย์ คุณสามารถใช้++เพื่อไปที่รายการถัดไปที่ตัวชี้ชี้ไปและ+ 4ไปที่องค์ประกอบที่ 5 ไม่ว่าขนาดของวัตถุจะเป็นเท่าใดที่ตัวชี้ชี้ไป

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

  7. ไม่สามารถยัดข้อมูลอ้างอิงลงในอาร์เรย์ได้ในขณะที่พอยน์เตอร์สามารถ (กล่าวถึงโดย user @litb)

  8. การอ้างอิง Const สามารถผูกกับขมับ พอยน์เตอร์ไม่สามารถ (ไม่ได้โดยไม่มีทางอ้อม):

    const int &x = int(12); //legal C++
    int *y = &int(12); //illegal to dereference a temporary.

    สิ่งนี้ทำให้const&ปลอดภัยยิ่งขึ้นสำหรับใช้ในรายการอาร์กิวเมนต์และอื่น ๆ


23
... แต่การยกเลิกการอ้างอิง NULL ไม่ได้ถูกกำหนดไว้ ตัวอย่างเช่นคุณไม่สามารถทดสอบว่าการอ้างอิงเป็น NULL หรือไม่ & อ้างอิง == NULL
Pat Notz

69
หมายเลข 2 ไม่เป็นความจริง การอ้างอิงไม่ใช่เพียง "ชื่ออื่นสำหรับตัวแปรเดียวกัน" การอ้างอิงอาจถูกส่งผ่านไปยังฟังก์ชั่นที่เก็บไว้ในชั้นเรียน ฯลฯ ในลักษณะคล้ายกับพอยน์เตอร์ มีอยู่อย่างอิสระจากตัวแปรที่ชี้ไป
Derek Park

31
ไบรอันสแต็กไม่เกี่ยวข้องกัน การอ้างอิงและพอยน์เตอร์ไม่จำเป็นต้องใช้พื้นที่บนสแต็ก พวกเขาทั้งสองสามารถได้รับการจัดสรรในกอง
Derek Park

22
ไบรอันความจริงที่ว่าตัวแปร (ในกรณีนี้ตัวชี้หรือการอ้างอิง) ต้องการพื้นที่ไม่ได้หมายความว่ามันต้องใช้พื้นที่บนสแต็ก ตัวชี้และการอ้างอิงอาจไม่เพียงชี้ไปที่ฮีปเท่านั้น แต่อาจถูกจัดสรรบนฮีป
Derek Park

38
ความแตกต่างที่สำคัญอื่น: การอ้างอิงไม่สามารถถูกยัดเข้าไปในอาร์เรย์ได้
โยฮันเนสชอม - litb

382

การอ้างอิง C ++ คืออะไร ( สำหรับโปรแกรมเมอร์ C )

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

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

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

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

พิจารณาคำสั่งต่อไปนี้จากคำถามที่พบบ่อย C ++ :

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

แต่ถ้าการอ้างอิงเป็นวัตถุจริงๆแล้วจะมีการอ้างอิงที่ห้อยอยู่ได้อย่างไร? ในภาษาที่ไม่มีการจัดการมันเป็นไปไม่ได้ที่การอ้างอิงจะเป็น 'ปลอดภัย' มากกว่าตัวชี้ - โดยทั่วไปแล้วมันไม่มีทางที่จะใช้นามแฝงค่าที่เชื่อถือได้ข้ามขอบเขต!

เหตุใดฉันจึงพิจารณาการอ้างอิง C ++ มีประโยชน์

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

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


17
@kriss: ไม่คุณยังสามารถรับการอ้างอิงที่เป็นอันตรายได้โดยส่งกลับตัวแปรอัตโนมัติโดยอ้างอิง
Ben Voigt

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

4
@kriss: ไม่การอ้างอิงไปยังวัตถุที่มีระยะเวลาการจัดเก็บอัตโนมัติแตกต่างจากวัตถุชั่วคราวอย่างมาก อย่างไรก็ตามฉันเพิ่งให้ตัวอย่างเคาน์เตอร์กับคำสั่งของคุณว่าคุณจะได้รับการอ้างอิงที่ไม่ถูกต้องโดยการอ้างอิงตัวชี้ที่ไม่ถูกต้องอีกครั้ง Christoph นั้นถูกต้อง - การอ้างอิงนั้นไม่ปลอดภัยกว่าตัวชี้โปรแกรมที่ใช้การอ้างอิงโดยเฉพาะนั้นยังสามารถทำลายความปลอดภัยของประเภท
Ben Voigt

7
การอ้างอิงไม่ใช่ตัวชี้ มันเป็นชื่อใหม่สำหรับวัตถุที่มีอยู่
catphive

18
@ catphive: จริงถ้าคุณไปด้วยความหมายของภาษาไม่เป็นความจริงถ้าคุณดูการใช้งานจริง C ++ เป็นภาษา 'เวทมนต์' ที่ C และถ้าคุณลบเวทมนตร์ออกจากการอ้างอิงคุณจะพบกับตัวชี้
Christoph

191

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

std::string s1 = "123";
std::string s2 = "456";

std::string s3_copy = s1 + s2;
const std::string& s3_reference = s1 + s2;

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

หากคุณลองวิธีนี้โดยที่ไม่constควรรวบรวม คุณไม่สามารถผูกการอ้างอิงที่ไม่ใช่ const ไปยังวัตถุชั่วคราวและไม่สามารถใช้ที่อยู่ของวัตถุนั้นได้


5
แต่กรณีการใช้งานสำหรับสิ่งนี้คืออะไร?
Ahmad Mushtaq

20
ดี s3_copy จะสร้างชั่วคราวจากนั้นคัดลอกสร้างลงใน s3_copy ในขณะที่ s3_reference ใช้โดยตรงชั่วคราว จากนั้นจะพูดจาหยาบคายจริงๆคุณต้องดูที่ Return Value Optimization โดยคอมไพเลอร์ได้รับอนุญาตให้ตัดการสร้างสำเนาในกรณีแรก
ราคาแมตต์

6
@digitalSurgeon: เวทมนตร์มีพลังมาก อายุการใช้งานของวัตถุถูกขยายโดยข้อเท็จจริงของการconst &โยงและเฉพาะเมื่อการอ้างอิงออกไปนอกขอบเขต destructor ของชนิดที่อ้างอิงจริง (เมื่อเปรียบเทียบกับประเภทการอ้างอิงที่อาจเป็นฐาน) จะถูกเรียก เนื่องจากเป็นการอ้างอิงจึงไม่มีการแบ่งส่วนใด ๆ
David Rodríguez - dribeas

9
การอัปเดตสำหรับ C ++ 11: ประโยคสุดท้ายควรอ่าน "คุณไม่สามารถผูกการอ้างอิง lvalue ที่ไม่ใช่ค่า const กับชั่วคราว" ได้เนื่องจากคุณสามารถผูกการอ้างอิงrvalue ที่ไม่ใช่ const กับชั่วคราวและมีพฤติกรรมยืดอายุการใช้งานเหมือนกัน
Oktalist

4
การใช้งานที่สำคัญของเรื่องนี้คือ: @AhmadMushtaq ชั้นเรียนมา หากไม่มีการเกี่ยวข้องกับมรดกคุณอาจใช้ซีแมนทิกส์คุณค่าซึ่งจะถูกหรือฟรีเนื่องจากการก่อสร้าง RVO / ย้าย แต่ถ้าคุณมีAnimal x = fast ? getHare() : getTortoise()แล้วxจะประสบปัญหาการแบ่งคลาสสิกในขณะที่Animal& x = ...จะทำงานอย่างถูกต้อง
Arthur Tacca

128

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

อัปเดต: ตอนนี้ฉันคิดอีกแล้วมีความแตกต่างที่สำคัญ

เป้าหมายของตัวชี้ const สามารถถูกแทนที่โดยการใช้ที่อยู่ของมันและใช้นักแสดง const

เป้าหมายของการอ้างอิงไม่สามารถถูกแทนที่ด้วยวิธีการใด ๆ ที่สั้นของ UB

สิ่งนี้ควรอนุญาตให้คอมไพเลอร์ทำการเพิ่มประสิทธิภาพมากขึ้นในการอ้างอิง


8
ฉันคิดว่านี่เป็นคำตอบที่ดีที่สุด คนอื่นพูดถึงการอ้างอิงและพอยน์เตอร์เหมือนพวกสัตว์ต่าง ๆ แล้วกำหนดว่าพฤติกรรมของพวกเขาแตกต่างกันอย่างไร มันไม่ได้ทำให้เรื่องง่ายขึ้นเลย ฉันเข้าใจการอ้างอิงเสมอว่าเป็นT* constน้ำตาลที่มีวากยสัมพันธ์ที่แตกต่างกัน (ซึ่งเกิดขึ้นเพื่อกำจัด * และ & จากรหัสของคุณ)
Carlo Wood

2
"เป้าหมายของตัวชี้ const สามารถถูกแทนที่ได้ด้วยการระบุที่อยู่และใช้การ cast const" การทำเช่นนั้นเป็นพฤติกรรมที่ไม่ได้กำหนด ดูstackoverflow.com/questions/25209838/…สำหรับรายละเอียด
dgnuff

1
พยายามที่จะเปลี่ยนทั้งการอ้างอิงของการอ้างอิงหรือค่าของตัวชี้ const (หรือสเกลาร์ const ใด ๆ ) เป็นความผิดกฎหมายที่เท่าเทียมกัน สิ่งที่คุณสามารถทำได้: ลบการรับรอง const ที่เพิ่มโดยการแปลงโดยนัย: int i; int const *pci = &i; /* implicit conv to const int* */ int *pi = const_cast<int*>(pci);ตกลง
curiousguy

1
ความแตกต่างที่นี่คือ UB เทียบกับเป็นไปไม่ได้อย่างแท้จริง ไม่มีไวยากรณ์ใน C ++ ที่จะช่วยให้คุณเปลี่ยนแปลงจุดอ้างอิงที่

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

126

ตรงกันข้ามกับความเห็นที่นิยมมันเป็นไปได้ที่จะมีการอ้างอิงที่เป็นโมฆะ

int * p = NULL;
int & r = *p;
r = 1;  // crash! (if you're lucky)

จริงอยู่ที่มันยากมากที่จะมีการอ้างอิง - แต่ถ้าคุณจัดการมันคุณจะฉีกผมของคุณพยายามที่จะหามัน การอ้างอิงไม่ปลอดภัยอย่างแท้จริงใน C ++!

ในทางเทคนิคนี่เป็นการอ้างอิงที่ไม่ถูกต้องไม่ใช่การอ้างอิงที่เป็นโมฆะ C ++ ไม่รองรับการอ้างอิงที่ไม่มีค่าเป็นแนวคิดเนื่องจากคุณอาจพบในภาษาอื่น มีการอ้างอิงที่ไม่ถูกต้องประเภทอื่นเช่นกัน การอ้างอิงที่ไม่ถูกต้องใด ๆจะทำให้ปีศาจมีพฤติกรรมที่ไม่ได้กำหนดเช่นเดียวกับการใช้ตัวชี้ที่ไม่ถูกต้อง

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

ตัวอย่างด้านบนของฉันสั้นและถูกประดิษฐ์ นี่คือตัวอย่างจริงมากขึ้น

class MyClass
{
    ...
    virtual void DoSomething(int,int,int,int,int);
};

void Foo(const MyClass & bar)
{
    ...
    bar.DoSomething(i1,i2,i3,i4,i5);  // crash occurs here due to memory access violation - obvious why?
}

MyClass * GetInstance()
{
    if (somecondition)
        return NULL;
    ...
}

MyClass * p = GetInstance();
Foo(*p);

ฉันต้องการย้ำว่าวิธีเดียวที่จะได้รับการอ้างอิงว่างเปล่าคือผ่านรหัสที่ผิดรูปแบบและเมื่อคุณมีมันคุณจะได้รับพฤติกรรมที่ไม่ได้กำหนด มันไม่มีเหตุผลที่จะตรวจสอบการอ้างอิงที่เป็นโมฆะ; ตัวอย่างเช่นคุณสามารถลองได้if(&bar==NULL)...แต่คอมไพเลอร์อาจปรับคำแถลงออกจากการมีอยู่! การอ้างอิงที่ถูกต้องไม่สามารถเป็นค่า NULL ได้ดังนั้นจากมุมมองของคอมไพเลอร์การเปรียบเทียบเป็นเท็จเสมอและมีอิสระที่จะกำจัดifอนุประโยคเป็นโค้ดที่ตายแล้ว - นี่คือสาระสำคัญของพฤติกรรมที่ไม่ได้กำหนด

วิธีที่เหมาะสมในการหลีกเลี่ยงปัญหาคือการหลีกเลี่ยงการยกเลิกตัวชี้ NULL เพื่อสร้างการอ้างอิง นี่เป็นวิธีอัตโนมัติในการทำสิ่งนี้ให้สำเร็จ

template<typename T>
T& deref(T* p)
{
    if (p == NULL)
        throw std::invalid_argument(std::string("NULL reference"));
    return *p;
}

MyClass * p = GetInstance();
Foo(deref(p));

สำหรับผู้สูงอายุที่ดูปัญหานี้จากคนที่มีทักษะการเขียนที่ดีขึ้นให้ดูNull Referencesจาก Jim Hyslop และ Herb Sutter

สำหรับตัวอย่างอื่นของอันตรายของการยกเลิกการลงทะเบียนตัวชี้โมฆะให้ดูการเปิดเผยพฤติกรรมที่ไม่ได้กำหนดเมื่อพยายามพอร์ตโค้ดไปยังแพลตฟอร์มอื่นโดย Raymond Chen


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

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

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

7
@ David Schwartz ถ้าฉันพูดถึงวิธีการทำงานต่าง ๆ ตามมาตรฐานคุณจะถูกต้อง แต่นั่นไม่ใช่สิ่งที่ฉันกำลังพูดถึง - ฉันกำลังพูดถึงพฤติกรรมที่สังเกตได้จริงกับคอมไพเลอร์ยอดนิยมและการคาดการณ์จากความรู้คอมไพเลอร์ทั่วไปและสถาปัตยกรรมซีพียูของฉันในสิ่งที่อาจเกิดขึ้น หากคุณเชื่อว่าการอ้างอิงนั้นเหนือกว่าพอยน์เตอร์เพราะปลอดภัยกว่าและไม่คิดว่าการอ้างอิงนั้นไม่ดีคุณจะต้องเจอกับปัญหาง่ายๆสักวันหนึ่งเหมือนกับที่ฉันเคยเป็น
Mark Ransom

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

115

คุณลืมส่วนที่สำคัญที่สุด:

การเข้าถึงของสมาชิกกับพอยน์เตอร์ใช้->
การเข้าถึงของสมาชิกกับการอ้างอิงที่ใช้.

foo.barเป็นอย่างชัดเจนดีกว่าfoo->barในทางเดียวกันว่าviเป็นอย่างชัดเจนดีกว่าEmacs :-)


4
@Orion Edwards> การเข้าถึงสมาชิกด้วยการใช้พอยน์เตอร์ ->> การเข้าถึงสมาชิกพร้อมการอ้างอิง สิ่งนี้ไม่เป็นความจริง 100% คุณสามารถมีการอ้างอิงไปยังตัวชี้ ในกรณีนี้คุณจะเข้าถึงสมาชิกของตัวชี้การยกเลิกการอ้างอิงโดยใช้ -> struct Node {Node * ถัดไป }; โหนด * แรก; // p เป็นการอ้างอิงถึงตัวชี้ void foo (Node * & p) {p-> next = first; } Node * bar = new Node; foo (บาร์); - OP: คุณคุ้นเคยกับแนวคิดของ rvalues ​​และ lvalues ​​หรือไม่?

3
ตัวชี้สมาร์ทมีทั้งคู่ (เมธอดบนคลาสตัวชี้อัจฉริยะ) และ -> (เมธอดเกี่ยวกับชนิดของต้นแบบ)
JBRWilkinson

1
คำสั่ง@ user6105 Orion Edwardsเป็นจริง 100% "เข้าถึงสมาชิกของ [ตัว] de-referenced pointer"ตัวชี้ไม่มีสมาชิกใด ๆ วัตถุที่ตัวชี้อ้างถึงมีสมาชิกและการเข้าถึงสิ่งเหล่านั้นเป็นสิ่งที่->ให้ไว้สำหรับการอ้างอิงไปยังตัวชี้เช่นเดียวกับตัวชี้
Max Truxa

1
ทำไมที่.และ->มีสิ่งที่จะทำอย่างไรกับ vi และ emacs :)
artm

10
@artM - มันเป็นเรื่องตลกและอาจไม่สมเหตุสมผลกับผู้พูดภาษาอังกฤษที่ไม่ใช่เจ้าของภาษา ขอโทษด้วย. เพื่ออธิบายว่า vi ดีกว่า emacs หรือเปล่า บางคนคิดว่า vi นั้นเหนือกว่าและอื่น ๆ ก็คิดตรงกันข้าม ในทำนองเดียวกันฉันคิดว่าการใช้.ดีกว่าการใช้->แต่ก็เหมือนกับ vi vs emacs มันเป็นเรื่องส่วนตัวและคุณไม่สามารถพิสูจน์อะไรได้เลย
Orion Edwards

74

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

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

ตัวอย่างเช่น:

void maybeModify(int& x); // may modify x in some way

void hurtTheCompilersOptimizer(short size, int array[])
{
    // This function is designed to do something particularly troublesome
    // for optimizers. It will constantly call maybeModify on array[0] while
    // adding array[1] to array[2]..array[size-1]. There's no real reason to
    // do this, other than to demonstrate the power of references.
    for (int i = 2; i < (int)size; i++) {
        maybeModify(array[0]);
        array[i] += array[1];
    }
}

คอมไพเลอร์การปรับให้เหมาะสมอาจตระหนักว่าเรากำลังเข้าถึง [0] และ [1] ค่อนข้างจะเป็นพวง มันชอบที่จะเพิ่มประสิทธิภาพอัลกอริทึมเพื่อ:

void hurtTheCompilersOptimizer(short size, int array[])
{
    // Do the same thing as above, but instead of accessing array[1]
    // all the time, access it once and store the result in a register,
    // which is much faster to do arithmetic with.
    register int a0 = a[0];
    register int a1 = a[1]; // access a[1] once
    for (int i = 2; i < (int)size; i++) {
        maybeModify(a0); // Give maybeModify a reference to a register
        array[i] += a1;  // Use the saved register value over and over
    }
    a[0] = a0; // Store the modified a[0] back into the array
}

ในการเพิ่มประสิทธิภาพดังกล่าวจำเป็นต้องพิสูจน์ว่าไม่มีสิ่งใดสามารถเปลี่ยนอาร์เรย์ [1] ในระหว่างการโทร มันค่อนข้างง่ายที่จะทำ ฉันไม่น้อยกว่า 2 ดังนั้นอาร์เรย์ [i] จึงไม่สามารถอ้างถึงอาร์เรย์ [1] อาจจะแก้ไข () ได้รับ a0 เป็นข้อมูลอ้างอิง (aliasing array [0]) เนื่องจากไม่มีเลขคณิต "การอ้างอิง" คอมไพเลอร์จึงต้องพิสูจน์ว่าบางทีโมดิฟายไม่เคยได้รับที่อยู่ของ x และได้พิสูจน์ว่าไม่มีอะไรเปลี่ยนแปลงอาร์เรย์ [1]

นอกจากนี้ยังต้องพิสูจน์ว่าไม่มีวิธีที่การโทรในอนาคตสามารถอ่าน / เขียน [0] ในขณะที่เรามีสำเนาการลงทะเบียนชั่วคราวใน a0 สิ่งนี้มักจะพิสูจน์ได้เล็กน้อยเพราะในหลายกรณีมันชัดเจนว่าการอ้างอิงนั้นไม่เคยถูกเก็บไว้ในโครงสร้างถาวรเช่นคลาสของอินสแตนซ์

ตอนนี้ทำสิ่งเดียวกันกับพอยน์เตอร์

void maybeModify(int* x); // May modify x in some way

void hurtTheCompilersOptimizer(short size, int array[])
{
    // Same operation, only now with pointers, making the
    // optimization trickier.
    for (int i = 2; i < (int)size; i++) {
        maybeModify(&(array[0]));
        array[i] += array[1];
    }
}

พฤติกรรมเหมือนกัน; ตอนนี้มันยากกว่ามากที่จะพิสูจน์ว่าบางทีโมดิฟายไม่เคยแก้ไขอาเรย์ [1] เพราะเราได้ให้พอยน์เตอร์แล้ว แมวออกจากกระเป๋า ตอนนี้มันต้องทำการพิสูจน์ที่ยากกว่านี้มาก: การวิเคราะห์แบบคงที่ของบางทีแก้ไขเพื่อพิสูจน์ว่าไม่เคยเขียนถึง & x + 1 นอกจากนี้ยังต้องพิสูจน์ว่าไม่เคยบันทึกตัวชี้ที่สามารถอ้างถึงอาร์เรย์ [0] ซึ่งเป็นเพียง เป็นเล่ห์เหลี่ยม

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

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

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

F createF(int argument);

void extending()
{
    const F& ref = createF(5);
    std::cout << ref.getArgument() << std::endl;
};

โดยปกติวัตถุชั่วคราวเช่นวัตถุที่สร้างขึ้นโดยการเรียกเพื่อcreateF(5)ถูกทำลายในตอนท้ายของการแสดงออก อย่างไรก็ตามโดยการผูกวัตถุนั้นกับการอ้างอิงrefC + + จะขยายอายุการใช้งานของวัตถุชั่วคราวนั้นจนกว่าrefจะออกนอกขอบเขต


จริงร่างกายจะต้องมองเห็นได้ อย่างไรก็ตามการพิจารณาว่าmaybeModifyไม่ใช้ที่อยู่ของสิ่งที่เกี่ยวข้องจะxง่ายกว่าการพิสูจน์ว่าการคำนวณเลขตัวชี้ไม่เกิดขึ้น
Cort Ammon

ฉันเชื่อว่าเครื่องมือเพิ่มประสิทธิภาพได้ทำเช่นนั้นแล้ว "การหากลุ่มของเลขคณิตตัวชี้ไม่เกิดขึ้น" ตรวจสอบสาเหตุอื่น ๆ
Ben Voigt

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

ฉันมีความรู้สึกว่ามีคนตั้งค่าสถานะผิด ๆ ว่าล้าสมัยความคิดเห็นตามแนวความvoid maybeModify(int& x) { 1[&x]++; }คิดซึ่งความคิดเห็นอื่น ๆ ข้างต้นกำลังพูดถึง
Ben Voigt

68

จริงๆแล้วการอ้างอิงนั้นไม่เหมือนตัวชี้

คอมไพเลอร์เก็บ "อ้างอิง" กับตัวแปร, เชื่อมโยงชื่อกับที่อยู่หน่วยความจำ; เป็นหน้าที่ของการแปลชื่อตัวแปรใด ๆ ไปยังที่อยู่หน่วยความจำเมื่อทำการรวบรวม

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

พอยน์เตอร์เป็นตัวแปร พวกเขามีที่อยู่ของตัวแปรอื่น ๆ หรืออาจเป็นโมฆะ สิ่งสำคัญคือตัวชี้มีค่าในขณะที่การอ้างอิงเท่านั้นมีตัวแปรที่อ้างอิง

ตอนนี้คำอธิบายของรหัสจริง:

int a = 0;
int& b = a;

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

void increment(int& n)
{
    n = n + 1;
}

int a;
increment(a);

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

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


2
การอ้างอิงเป็นการอ้างอิงถึงค่า l ไม่จำเป็นต้องเป็นตัวแปร ด้วยเหตุนี้จึงใกล้ตัวชี้มากกว่าชื่อแทนจริง (คอมไพล์เวลาคอมไพล์) ตัวอย่างของนิพจน์ที่สามารถอ้างอิงได้คือ * p หรือแม้แต่ * p ++

5
ใช่ฉันแค่ชี้ให้เห็นถึงความจริงที่ว่าการอ้างอิงอาจไม่ดันตัวแปรใหม่บนสแต็กเสมอไปตามที่ตัวชี้ใหม่จะทำ
Vincent Robert

1
@VincentRobert: มันจะทำหน้าที่เหมือนกับตัวชี้ ... หากฟังก์ชั่นอินไลน์ทั้งการอ้างอิงและตัวชี้จะถูกปรับให้เหมาะสม หากมีการเรียกใช้ฟังก์ชันที่อยู่ของวัตถุจะต้องถูกส่งผ่านไปยังฟังก์ชัน
Ben Voigt

1
int * p = NULL; int & r = * p; การอ้างอิงที่ชี้ไปที่ NULL; ถ้า (r) {} -> boOm;)
sree

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

45

การอ้างอิงไม่สามารถเป็นNULLได้


10
ดูคำตอบของ Mark Ransom สำหรับตัวอย่างเคาน์เตอร์ นี่คือตำนานที่ถูกกล่าวหาบ่อยที่สุดเกี่ยวกับการอ้างอิง แต่มันก็เป็นตำนาน การรับประกันเพียงอย่างเดียวที่คุณมีตามมาตรฐานคือคุณมี UB ทันทีเมื่อคุณมีการอ้างอิง NULL แต่นั่นก็คล้ายกับการพูดว่า "รถคันนี้ปลอดภัยไม่สามารถออกไปจากถนนได้ (เราจะไม่รับผิดชอบใด ๆ สำหรับสิ่งที่อาจเกิดขึ้นหากคุณนำมันออกจากถนนอย่างไรก็ตามมันอาจระเบิดได้)"
cmaster - คืนสถานะโมนิก้า

17
@cmaster: ในโปรแกรมที่ถูกต้องการอ้างอิงต้องไม่เป็นโมฆะ แต่ตัวชี้สามารถ นี่ไม่ใช่ตำนานนี่เป็นความจริง
user541686

8
@ Mehrdad ใช่โปรแกรมที่ใช้ได้อยู่บนท้องถนน แต่ไม่มีอุปสรรคในการรับส่งข้อมูลที่จะบังคับให้โปรแกรมของคุณใช้งานได้จริง ส่วนใหญ่ของถนนเป็นเครื่องหมายที่ขาดหายไป ดังนั้นจึงเป็นเรื่องง่ายมากที่จะออกจากถนนในเวลากลางคืน และมันเป็นสิ่งสำคัญสำหรับการแก้ไขข้อบกพร่องที่คุณรู้ว่าสิ่งนี้สามารถเกิดขึ้นได้: การอ้างอิงโมฆะสามารถเผยแพร่ก่อนที่มันจะขัดข้องโปรแกรมของคุณเช่นเดียวกับตัวชี้โมฆะสามารถ และเมื่อไหร่ที่คุณมีรหัสเช่นvoid Foo::bar() { virtual_baz(); }segfaults นั้น หากคุณไม่ทราบว่าการอ้างอิงอาจเป็นโมฆะคุณไม่สามารถติดตามค่า null กลับไปยังจุดเริ่มต้นได้
cmaster - คืนสถานะโมนิก้า

4
int * p = NULL; int & r = * p; การอ้างอิงที่ชี้ไปที่ NULL; ถ้า (r) {} -> boOm;) -
sree

10
@sree int &r=*p;เป็นพฤติกรรมที่ไม่ได้กำหนด ณ จุดที่คุณไม่ได้มี "การอ้างอิงชี้โมฆะ" คุณมีโปรแกรมที่ไม่สามารถให้เหตุผลเกี่ยวกับการที่ทุกคน
cdhowie

35

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

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

int ival = 1024, ival2 = 2048;
int *pi = &ival, *pi2 = &ival2;
pi = pi2;    // pi now points to ival2

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

int &ri = ival, &ri2 = ival2;
ri = ri2;    // assigns ival2 to ival

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


"การอ้างอิงหมายถึงวัตถุเสมอ" เป็นเท็จโดยสมบูรณ์
Ben Voigt

32

มีความแตกต่างทางความหมายที่อาจดูลึกลับหากคุณไม่คุ้นเคยกับการเรียนภาษาคอมพิวเตอร์ในแบบนามธรรมหรือแม้แต่ในเชิงวิชาการ

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

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


27

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

    void fun(int &a, int &b); // A common usage of references.
    int a = 0;
    int &b = a; // b is an alias for a. Not so common to use. 

20

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

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

ตัวอย่างเช่น:

class scope_test
{
public:
    ~scope_test() { printf("scope_test done!\n"); }
};

...

{
    const scope_test &test= scope_test();
    printf("in scope\n");
}

จะพิมพ์:

in scope
scope_test done!

นี่คือกลไกภาษาที่อนุญาตให้ ScopeGuard ทำงาน


1
คุณไม่สามารถใช้ที่อยู่ของการอ้างอิง แต่นั่นไม่ได้หมายความว่าพวกเขาไม่ได้ใช้พื้นที่ นอกเหนือจากการเพิ่มประสิทธิภาพพวกเขาสามารถทำได้อย่างแน่นอนที่สุด
Lightness Races ในวงโคจร

2
ผลกระทบแม้จะมี "การอ้างอิงในสแต็กไม่ใช้พื้นที่ใด ๆ เลย" เป็นเท็จอย่างชัดแจ้ง
Lightness Races ในวงโคจร

1
@ Tomalak ก็ดีขึ้นอยู่กับคอมไพเลอร์ด้วย แต่ใช่การพูดแบบนี้ค่อนข้างสับสน ฉันคิดว่ามันจะสับสนน้อยกว่าเพียงแค่ลบออก
MSN

1
ไม่ว่าในกรณีใดก็ตามมันอาจหรือไม่ก็ได้ ดังนั้น "มันไม่ได้" ตามที่ยืนยันแน่ชัดเป็นสิ่งที่ผิด นั่นคือสิ่งที่ฉันพูด :) [ฉันจำไม่ได้ว่ามาตรฐานพูดอะไรเกี่ยวกับปัญหา กฎของสมาชิกอ้างอิงอาจบอกกฎทั่วไปของ "การอ้างอิงอาจใช้พื้นที่" แต่ฉันไม่มีสำเนามาตรฐานกับฉันที่นี่บนชายหาด: D]
Lightness Races ใน Orbit

20

แห่งนี้ตั้งอยู่บนพื้นฐานของการกวดวิชา สิ่งที่เขียนทำให้ชัดเจนยิ่งขึ้น:

>>> The address that locates a variable within memory is
    what we call a reference to that variable. (5th paragraph at page 63)

>>> The variable that stores the reference to another
    variable is what we call a pointer. (3rd paragraph at page 64)

เพียงจำไว้ว่า

>>> reference stands for memory location
>>> pointer is a reference container (Maybe because we will use it for
several times, it is better to remember that reference.)

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

ดูข้อความต่อไปนี้

int Tom(0);
int & alias_Tom = Tom;

alias_Tom สามารถเข้าใจได้ว่าเป็น alias of a variable (แตกต่างกันด้วยtypedefซึ่งเป็น)alias of a type นอกจากนี้ยังตกลงที่จะลืมคำศัพท์ของคำสั่งดังกล่าวคือการสร้างการอ้างอิงของTomTom


1
และถ้าคลาสมีตัวแปรอ้างอิงมันควรจะเริ่มต้นได้ด้วย nullptr หรือวัตถุที่ถูกต้องในรายการเริ่มต้น
Misgevolution

1
การใช้ถ้อยคำในคำตอบนี้ทำให้เกิดความสับสนเกินกว่าจะใช้งานได้จริง นอกจากนี้ @Misgevolution คุณแนะนำให้ผู้อ่านเริ่มต้นการอ้างอิงด้วยnullptrหรือไม่? คุณอ่านส่วนอื่น ๆ ของกระทู้นี้จริง ๆ หรือ ...
underscore_d

1
ไม่ดีขอโทษสำหรับสิ่งที่โง่ที่ฉันพูด ฉันต้องนอนไม่หลับในเวลานั้น 'เริ่มต้นด้วย nullptr' ผิดทั้งหมด
Misgevolution

19

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

int& j = i;

มันกลายเป็นภายใน

int* const j = &i;

13
นี่ไม่ใช่สิ่งที่มาตรฐาน C ++ บอกไว้และไม่จำเป็นสำหรับคอมไพเลอร์ที่จะใช้การอ้างอิงตามวิธีที่อธิบายโดยคำตอบของคุณ
jogojapan

@jogojapan: วิธีใดก็ตามที่ถูกต้องสำหรับคอมไพเลอร์ C ++ ที่จะใช้การอ้างอิงก็เป็นวิธีที่ถูกต้องในการนำconstตัวชี้ไปใช้ ความยืดหยุ่นนั้นไม่ได้พิสูจน์ว่ามีความแตกต่างระหว่างการอ้างอิงและตัวชี้
Ben Voigt

2
@BenVoigt มันอาจเป็นความจริงที่ว่าการใช้งานที่ถูกต้องของหนึ่งคือการใช้งานที่ถูกต้องของอื่น ๆ แต่ไม่ปฏิบัติตามในวิธีที่ชัดเจนจากคำจำกัดความของแนวคิดทั้งสองนี้ คำตอบที่ดีน่าจะเริ่มต้นจากคำจำกัดความและแสดงให้เห็นว่าทำไมการอ้างสิทธิ์ทั้งสองนั้นในที่สุดจึงเป็นเรื่องจริง คำตอบนี้ดูเหมือนจะเป็นความคิดเห็นในคำตอบอื่น ๆ
jogojapan

การอ้างอิงคือชื่ออื่นที่กำหนดให้กับวัตถุ คอมไพเลอร์ได้รับอนุญาตให้มีการนำไปใช้ในรูปแบบใด ๆ ตราบใดที่คุณไม่สามารถบอกความแตกต่างได้สิ่งนี้เรียกว่ากฎ "as-if" ส่วนสำคัญที่นี่คือคุณไม่สามารถบอกความแตกต่าง หากคุณสามารถค้นพบว่าตัวชี้ไม่มีที่เก็บข้อมูลคอมไพเลอร์มีข้อผิดพลาด หากคุณสามารถค้นพบว่าการอ้างอิงไม่มีที่เก็บข้อมูลคอมไพเลอร์ยังคงเป็นไปตามมาตรฐาน
sp2danny

18

คำตอบโดยตรง

การอ้างอิงใน C ++ คืออะไร? อินสแตนซ์เฉพาะบางประเภทที่ไม่ใช่ประเภทวัตถุไม่ได้เป็นชนิดของวัตถุ

ตัวชี้ใน C ++ คืออะไร อินสแตนซ์เฉพาะบางประเภทที่เป็นประเภทวัตถุเป็นชนิดของวัตถุ

จากคำนิยาม ISO C ++ ของประเภทวัตถุ :

วัตถุประเภทคือ (อาจจะเป็นพันธุ์ -qualified) ประเภทที่ไม่ได้เป็นประเภทฟังก์ชั่นไม่ได้เป็นชนิดการอ้างอิงและไม่พันธุ์เป็นโมฆะ

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

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

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

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

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

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

กฎพิเศษเพิ่มเติมอีกสองสามข้อเกี่ยวกับการอ้างอิง:

  • ผู้ประกาศแบบผสมมีข้อ จำกัด ในการอ้างอิงมากขึ้น
  • อ้างอิงสามารถยุบ
    • กฎพิเศษเกี่ยวกับ&&พารามิเตอร์ (ในฐานะ "การอ้างอิงการส่งต่อ") ขึ้นอยู่กับการยุบอ้างอิงระหว่างการลดพารามิเตอร์เทมเพลตอนุญาตให้"การส่งต่อที่สมบูรณ์แบบ"ของพารามิเตอร์
  • การอ้างอิงมีกฎพิเศษในการเริ่มต้น อายุการใช้งานของตัวแปรที่ประกาศเป็นประเภทการอ้างอิงอาจแตกต่างจากวัตถุทั่วไปผ่านทางส่วนขยาย
    • BTW, บริบทอื่น ๆ สองสามอย่างเช่นการเริ่มต้นที่เกี่ยวข้องกับstd::initializer_listกฎการขยายอายุการอ้างอิงที่คล้ายกัน มันเป็นหนอนอีกกระป๋องหนึ่ง
  • ...

ความเข้าใจผิด

น้ำตาลทราย

ฉันรู้ว่าการอ้างอิงคือน้ำตาลประโยคดังนั้นรหัสจึงง่ายต่อการอ่านและเขียน

ในทางเทคนิคแล้วมันผิดธรรมดา การอ้างอิงไม่ใช่น้ำตาลเชิงซ้อนของคุณสมบัติอื่นใดใน C ++ เนื่องจากไม่สามารถแทนที่ได้อย่างแน่นอนโดยคุณสมบัติอื่น ๆ โดยไม่มีความแตกต่างทางความหมาย

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

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

การเก็บรักษา

ดังนั้นตัวชี้และการอ้างอิงจึงใช้หน่วยความจำในจำนวนเท่ากัน

ข้อความข้างต้นผิดพลาด เพื่อหลีกเลี่ยงความเข้าใจผิดดังกล่าวให้ดูที่กฎ ISO C ++ แทน:

จาก[intro.object] / 1 :

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

จาก[dcl.ref] / 4 :

มันไม่ได้ระบุว่าต้องการการอ้างอิงหรือไม่

หมายเหตุเหล่านี้เป็นคุณสมบัติความหมาย

เน้น

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

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

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

  • บางครั้งกฎภาษาต้องมีการใช้ประเภทเฉพาะอย่างชัดเจน หากคุณต้องการใช้คุณสมบัติเหล่านี้ให้ปฏิบัติตามกฎ
    • ตัวสร้างสำเนาต้องการประเภทcv - &reference ที่ระบุเป็นพารามิเตอร์ประเภทที่ 1 (และโดยปกติควรมีconstคุณสมบัติ)
    • ตัวสร้างการเคลื่อนย้ายจำเป็นต้องมีประเภทเฉพาะของcv - &&ประเภทการอ้างอิงเป็นประเภทพารามิเตอร์ที่ 1 (และโดยปกติจะไม่มีตัวระบุ)
    • โอเวอร์โหลดที่เฉพาะเจาะจงของผู้ประกอบการต้องมีการอ้างอิงหรือไม่อ้างอิงประเภท ตัวอย่างเช่น:
      • โอเวอร์โหลดoperator=ในฐานะสมาชิกฟังก์ชันพิเศษต้องใช้ประเภทการอ้างอิงคล้ายกับพารามิเตอร์ที่ 1 ของตัวสร้างสำเนา / ย้าย
      • Postfix ต้องใช้หุ่น++int
      • ...
  • ถ้าคุณรู้ว่า pass-by-value (เช่นการใช้ประเภท non-reference) เพียงพอให้ใช้โดยตรงโดยเฉพาะอย่างยิ่งเมื่อใช้งานการสนับสนุนการคัดลอกคัดลอก C ++ 17 ที่ได้รับคำสั่ง ( คำเตือน : อย่างไรก็ตามเพื่อเหตุผลอย่างละเอียดเกี่ยวกับความจำเป็นอาจมีความซับซ้อนมาก )
  • หากคุณต้องการใช้งานมือจับบางส่วนด้วยความเป็นเจ้าของให้ใช้ตัวชี้สมาร์ทเช่นunique_ptrและshared_ptr(หรือแม้แต่กับโฮมบรูว์ด้วยตัวเองหากคุณต้องการให้ทึบ ) แทนที่จะใช้พอยน์เตอร์ชี้
  • หากคุณกำลังทำซ้ำบางช่วงให้ใช้ตัววนซ้ำ (หรือบางช่วงที่ยังไม่ได้จัดทำโดยไลบรารีมาตรฐาน) แทนที่จะเป็นพอยน์เตอร์แบบเว้นเสียแต่ว่าคุณจะมั่นใจพอยน์เตอร์พอยต์แบบเดิมจะทำได้ดีกว่า กรณี
  • ถ้าคุณรู้ว่า pass-by-value เพียงพอแล้วและคุณต้องการซีแมนทิกส์ nullable ที่ชัดเจนให้ใช้ wrapper like std::optionalแทนที่จะเป็นพอยน์เตอร์พอยน์เตอร์
  • หากคุณทราบว่ารหัสผ่านไม่เหมาะอย่างยิ่งสำหรับเหตุผลข้างต้นและคุณไม่ต้องการซีแมนทิกส์ที่เป็นโมฆะได้ให้ใช้การกำหนดค่า {lvalue, rvalue, forwarding}
  • แม้ว่าคุณต้องการซีแมนทิกส์เหมือนตัวชี้แบบดั้งเดิม แต่ก็มีบางสิ่งที่เหมาะสมกว่าเช่นobserver_ptrใน Library Fundamental TS

ข้อยกเว้นเพียงอย่างเดียวไม่สามารถแก้ไขได้ในภาษาปัจจุบัน:

  • เมื่อคุณใช้งานตัวชี้สมาร์ทข้างต้นคุณอาจต้องจัดการกับตัวชี้แบบดิบ
  • operator newโดยเฉพาะการปฏิบัติภาษาทำงานร่วมกันจำเป็นต้องมีตัวชี้เช่น (อย่างไรก็ตามcv - void*ยังคงแตกต่างกันและปลอดภัยกว่าเมื่อเทียบกับตัวชี้วัตถุธรรมดาเพราะมันจะออกกฎตัวคำนวณเลขคณิตที่ไม่คาดคิดนอกเสียจากว่าคุณจะพึ่งพาส่วนขยายที่ไม่สอดคล้องกับบางอย่างvoid*ของ GNU)
  • พอยน์เตอร์ของฟังก์ชันสามารถแปลงจากนิพจน์แลมบ์ดาโดยไม่มีการดักจับในขณะที่การอ้างอิงฟังก์ชันไม่สามารถทำได้ คุณต้องใช้ตัวชี้ฟังก์ชั่นในรหัสที่ไม่ใช่แบบทั่วไปสำหรับกรณีดังกล่าวแม้ว่าคุณจะไม่ต้องการค่าที่เป็นโมฆะก็ตาม

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

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

คำเตือนภาษาเป็นกลาง

หากคุณพบคำถามผ่านผลการค้นหาของ Google บางตัว (ไม่ใช่เฉพาะ C ++)นี่น่าจะเป็นตำแหน่งที่ผิด

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

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

ยังคงมีคุณสมบัติทั่วไปบางอย่างในการอ้างอิงในภาษาการเขียนโปรแกรมที่แตกต่างกันโดยทั่วไป แต่ขอทิ้งไว้สำหรับคำถามอื่น ๆ ใน SO

(หมายเหตุด้าน: คำถามอาจมีความสำคัญเร็วกว่าภาษา "C-like" ที่เกี่ยวข้องเช่นALGOL 68 เทียบกับ PL / I )


17

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

#include<iostream>
using namespace std;

void swap(char * &str1, char * &str2)
{
  char *temp = str1;
  str1 = str2;
  str2 = temp;
}

int main()
{
  char *str1 = "Hi";
  char *str2 = "Hello";
  swap(str1, str2);
  cout<<"str1 is "<<str1<<endl;
  cout<<"str2 is "<<str2<<endl;
  return 0;
}

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

#include<stdio.h>
/* Swaps strings by swapping pointers */
void swap1(char **str1_ptr, char **str2_ptr)
{
  char *temp = *str1_ptr;
  *str1_ptr = *str2_ptr;
  *str2_ptr = temp;
}

int main()
{
  char *str1 = "Hi";
  char *str2 = "Hello";
  swap1(&str1, &str2);
  printf("str1 is %s, str2 is %s", str1, str2);
  return 0;
}

เยี่ยมชมข้อมูลต่อไปนี้เกี่ยวกับการอ้างอิงถึงตัวชี้:

อย่างที่ฉันบอกไปแล้วว่าตัวชี้ไปยังการอ้างอิงเป็นไปไม่ได้ ลองใช้โปรแกรมต่อไปนี้:

#include <iostream>
using namespace std;

int main()
{
   int x = 10;
   int *ptr = &x;
   int &*ptr1 = ptr;
}

16

ฉันใช้การอ้างอิงเว้นแต่ฉันต้องการอย่างใดอย่างหนึ่งต่อไปนี้:

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

  • คุณสามารถทำเลขคณิตกับตัวชี้ได้ ตัวอย่างเช่น,p += offset;


5
คุณสามารถเขียน&r + offsetตำแหน่งที่rได้รับการประกาศเป็นข้อมูลอ้างอิง
MM

15

มีความแตกต่างพื้นฐานอย่างหนึ่งระหว่างพอยน์เตอร์และการอ้างอิงที่ฉันไม่ได้เห็นใครพูดถึง: การอ้างอิงเปิดใช้งานซีแมนทิกส์แบบ pass-by-reference ในอาร์กิวเมนต์ของฟังก์ชัน ตัวชี้แม้ว่ามันจะไม่สามารถมองเห็นได้ในตอนแรกไม่ได้: พวกเขาเพียง แต่ให้ความหมายแบบ pass-by-value นี้ได้รับการอธิบายอย่างมากในบทความนี้

ขอแสดงความนับถือ & rzej


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

ฉันเคยคิดเช่นนี้ แต่ดูบทความเชื่อมโยงอธิบายว่าทำไมมันไม่เป็นเช่นนั้น
Andrzej

2
@Andrzj: นั่นเป็นเพียงประโยคยาวมากในความคิดเห็นของฉัน: จับถูกคัดลอก
Ben Voigt

ฉันต้องการคำอธิบายเพิ่มเติมเกี่ยวกับ "การคัดลอกที่จับ" นี้ ฉันเข้าใจแนวคิดพื้นฐานบางอย่าง แต่ฉันคิดว่าการอ้างอิงและตัวชี้ทางกายภาพทั้งสองชี้ตำแหน่งของหน่วยความจำของตัวแปร มันเป็นนามแฝงหรือไม่ที่เก็บค่าตัวแปรและอัปเดตเมื่อค่าของตัวแปรเปลี่ยนแปลงหรืออย่างอื่น? ฉันเป็นสามเณรและโปรดอย่าตั้งค่าสถานะเป็นคำถามโง่ ๆ
Asim

1
@Andrzej False ในทั้งสองกรณีจะเกิดการส่งผ่านค่า การอ้างอิงถูกส่งผ่านตามค่าและตัวชี้ถูกส่งผ่านตามค่า การพูดเป็นอย่างอื่นทำให้มือใหม่สับสน
เส้นทาง Miles

14

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

#include <iostream>
int main(int argc, char** argv) {
    // Create a string on the heap
    std::string *str_ptr = new std::string("THIS IS A STRING");
    // Dereference the string on the heap, and assign it to the reference
    std::string &str_ref = *str_ptr;
    // Not even a compiler warning! At least with gcc
    // Now lets try to print it's value!
    std::cout << str_ref << std::endl;
    // It works! Now lets print and compare actual memory addresses
    std::cout << str_ptr << " : " << &str_ref << std::endl;
    // Exactly the same, now remember to free the memory on the heap
    delete str_ptr;
}

ซึ่งส่งออกสิ่งนี้:

THIS IS A STRING
0xbb2070 : 0xbb2070

หากคุณสังเกตเห็นว่าที่อยู่หน่วยความจำยังเหมือนเดิมหมายความว่าการอ้างอิงนั้นชี้ไปยังตัวแปรบนฮีปสำเร็จแล้ว! ตอนนี้ถ้าคุณต้องการรับนอกลู่นอกทางจริง ๆ นี่ยังใช้งานได้:

int main(int argc, char** argv) {
    // In the actual new declaration let immediately de-reference and assign it to the reference
    std::string &str_ref = *(new std::string("THIS IS A STRING"));
    // Once again, it works! (at least in gcc)
    std::cout << str_ref;
    // Once again it prints fine, however we have no pointer to the heap allocation, right? So how do we free the space we just ignorantly created?
    delete &str_ref;
    /*And, it works, because we are taking the memory address that the reference is
    storing, and deleting it, which is all a pointer is doing, just we have to specify
    the address with '&' whereas a pointer does that implicitly, this is sort of like
    calling delete &(*str_ptr); (which also compiles and runs fine).*/
}

ซึ่งส่งออกสิ่งนี้:

THIS IS A STRING

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

กล่าวอีกนัยหนึ่งคือการอ้างอิงไม่มีอะไรนอกจากตัวชี้ที่มีกลไกการชี้ออกไปทำให้ปลอดภัยและใช้งานง่ายขึ้น (ไม่มีคณิตศาสตร์ตัวชี้โดยไม่ตั้งใจไม่ผสมกัน '.' และ '->' ฯลฯ ) สมมติว่าคุณ อย่าลองใช้เรื่องไร้สาระเหมือนตัวอย่างด้านบน;)

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

กฎสำคัญเพียงข้อเดียวที่สำคัญที่ต้องจดจำด้วยการอ้างอิงคือต้องกำหนดไว้ในเวลาที่ประกาศ (ยกเว้นการอ้างอิงในส่วนหัวในกรณีนั้นจะต้องกำหนดไว้ในตัวสร้างหลังจากวัตถุที่อยู่ในนั้นคือ สร้างมันสายเกินไปที่จะกำหนด)

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


14

ความแตกต่างอีกประการหนึ่งคือคุณสามารถมีตัวชี้ให้เป็นประเภทโมฆะ (และมันหมายถึงตัวชี้ไปยังสิ่งใด ๆ ) แต่การอ้างอิงถึงโมฆะนั้นเป็นสิ่งต้องห้าม

int a;
void * p = &a; // ok
void & p = a;  //  forbidden

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


13

นอกจากนี้การอ้างอิงที่เป็นพารามิเตอร์ของฟังก์ชันที่ถูก inline อาจถูกจัดการแตกต่างจากตัวชี้

void increment(int *ptrint) { (*ptrint)++; }
void increment(int &refint) { refint++; }
void incptrtest()
{
    int testptr=0;
    increment(&testptr);
}
void increftest()
{
    int testref=0;
    increment(testref);
}

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

แน่นอนว่าสำหรับฟังก์ชั่นที่ไม่ได้ inline ตัวชี้และการอ้างอิงสร้างรหัสเดียวกันและมันจะดีกว่าเสมอที่จะผ่าน intrinsics ตามมูลค่ากว่าโดยอ้างอิงถ้าพวกเขาไม่ได้แก้ไขและส่งกลับโดยฟังก์ชั่น


11

การใช้การอ้างอิงที่น่าสนใจอีกประการหนึ่งคือการระบุอาร์กิวเมนต์เริ่มต้นของประเภทที่ผู้ใช้กำหนด:

class UDT
{
public:
   UDT() : val_d(33) {};
   UDT(int val) : val_d(val) {};
   virtual ~UDT() {};
private:
   int val_d;
};

class UDT_Derived : public UDT
{
public:
   UDT_Derived() : UDT() {};
   virtual ~UDT_Derived() {};
};

class Behavior
{
public:
   Behavior(
      const UDT &udt = UDT()
   )  {};
};

int main()
{
   Behavior b; // take default

   UDT u(88);
   Behavior c(u);

   UDT_Derived ud;
   Behavior d(ud);

   return 1;
}

รสชาติเริ่มต้นใช้การอ้างอิงแบบ 'ผูก const ไปยังด้านชั่วคราว' ของการอ้างอิง


11

โปรแกรมนี้อาจช่วยในการทำความเข้าใจคำตอบของคำถาม นี่เป็นโปรแกรมอย่างง่ายของการอ้างอิง "j" และตัวชี้ "ptr" ที่ชี้ไปยังตัวแปร "x"

#include<iostream>

using namespace std;

int main()
{
int *ptr=0, x=9; // pointer and variable declaration
ptr=&x; // pointer to variable "x"
int & j=x; // reference declaration; reference to variable "x"

cout << "x=" << x << endl;

cout << "&x=" << &x << endl;

cout << "j=" << j << endl;

cout << "&j=" << &j << endl;

cout << "*ptr=" << *ptr << endl;

cout << "ptr=" << ptr << endl;

cout << "&ptr=" << &ptr << endl;
    getch();
}

เรียกใช้โปรแกรมและดูผลลัพธ์และคุณจะเข้าใจ

นอกจากนี้ใช้เวลา 10 นาทีและดูวิดีโอนี้: https://www.youtube.com/watch?v=rlJrrGV0iOg


11

ฉันรู้สึกว่ายังมีอีกประเด็นที่ยังไม่ได้กล่าวถึง

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

แม้ว่าสิ่งนี้อาจดูตื้น ๆ แต่ฉันเชื่อว่าคุณสมบัตินี้มีความสำคัญอย่างยิ่งต่อคุณสมบัติ C ++ หลายประการเช่น:

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

  • lvalues พิจารณาคำสั่งที่str[0] = 'X';ไม่มีการอ้างอิงมันจะใช้ได้กับ c-strings ( char* str) เท่านั้น การส่งคืนอักขระโดยการอ้างอิงอนุญาตให้คลาสที่ผู้ใช้กำหนดมีสัญกรณ์เดียวกัน

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

  • overloads ประกอบการ ด้วยการอ้างอิงมันเป็นไปได้ที่จะแนะนำทางอ้อมให้ผู้ประกอบการเรียก - พูดoperator+(const T& a, const T& b)ในขณะที่ยังคงสัญกรณ์มัดเดียวกัน นอกจากนี้ยังใช้งานได้กับฟังก์ชั่นการโอเวอร์โหลดปกติ

จุดเหล่านี้ช่วยเพิ่มส่วนสำคัญของ C ++ และไลบรารี่มาตรฐานดังนั้นนี่จึงเป็นคุณสมบัติหลักของการอ้างอิง


"การส่งโดยนัย " การร่ายเป็นโครงสร้างไวยากรณ์ที่มีอยู่ในไวยากรณ์; หล่ออยู่เสมออย่างชัดเจน
curiousguy

9

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

void fn1(std::string s);
void fn2(const std::string& s);
void fn3(std::string& s);
void fn4(std::string* s);

void bar() {
    std::string x;
    fn1(x);  // Cannot modify x
    fn2(x);  // Cannot modify x (without const_cast)
    fn3(x);  // CAN modify x!
    fn4(&x); // Can modify x (but is obvious about it)
}

ย้อนกลับไปใน C, การเรียกร้องที่มีลักษณะเช่นfn(x)เท่านั้นที่สามารถผ่านค่าจึงแน่นอนไม่สามารถแก้ไขx; fn(&x)การปรับเปลี่ยนการโต้แย้งคุณจะต้องผ่านตัวชี้ ดังนั้นหากมีการโต้แย้งไม่ได้นำโดย&คุณรู้ว่ามันจะไม่ถูกแก้ไข (การสนทนา&หมายถึงการดัดแปลงนั้นไม่เป็นความจริงเพราะบางครั้งคุณต้องผ่านโครงสร้างแบบอ่านอย่างเดียวขนาดใหญ่ด้วยconstตัวชี้)

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


8

บางทีอุปมาอุปมัยบางอย่างอาจช่วยได้ ในบริบทของหน้าจอเดสก์ทอปของคุณ -

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

6

ความแตกต่างระหว่างตัวชี้และการอ้างอิง

ตัวชี้สามารถเริ่มต้นเป็น 0 และอ้างอิงไม่ได้ ในความเป็นจริงการอ้างอิงจะต้องอ้างถึงวัตถุ แต่ตัวชี้สามารถเป็นตัวชี้โมฆะ:

int* p = 0;

แต่เราไม่สามารถมีและยังint& p = 0;int& p=5 ;

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

Int x = 0;
Int y = 5;
Int& p = x;
Int& p1 = y;

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

ข้อแตกต่างก็คือตัวชี้สามารถชี้ไปที่วัตถุอื่นได้อย่างไรก็ตามการอ้างอิงนั้นอ้างอิงถึงวัตถุเดียวกันเสมอมายกตัวอย่างนี้:

Int a = 6, b = 5;
Int& rf = a;

Cout << rf << endl; // The result we will get is 6, because rf is referencing to the value of a.

rf = b;
cout << a << endl; // The result will be 5 because the value of b now will be stored into the address of a so the former value of a will be erased

อีกจุดหนึ่ง: เมื่อเรามีเทมเพลตเหมือนกับเทมเพลต STL ชนิดของเทมเพลตคลาสนั้นจะส่งคืนการอ้างอิงไม่ใช่ตัวชี้เพื่อให้อ่านง่ายหรือกำหนดค่าใหม่โดยใช้โอเปอเรเตอร์ []:

Std ::vector<int>v(10); // Initialize a vector with 10 elements
V[5] = 5; // Writing the value 5 into the 6 element of our vector, so if the returned type of operator [] was a pointer and not a reference we should write this *v[5]=5, by making a reference we overwrite the element by using the assignment "="

1
const int& i = 0เรายังสามารถมี
Revolver_Ocelot

1
ในกรณีนี้การอ้างอิงจะถูกใช้ในการอ่านเท่านั้นเราไม่สามารถแก้ไขการอ้างอิง const นี้ได้แม้จะใช้ "const_cast" เพราะ "const_cast" ยอมรับเฉพาะตัวชี้ที่ไม่ได้อ้างอิง
dhokar.w

1
const_cast ทำงานได้ดีกับการอ้างอิง: coliru.stacked-crooked.com/a/eebb454ab2cfd570
Revolver_Ocelot

1
คุณกำลังสร้างการอ้างอิงไม่ใช้การอ้างอิงลองสิ่งนี้ const int & i =; const_cast <int> (i); ฉันพยายามทิ้งค่าคงที่ของการอ้างอิงเพื่อให้สามารถเขียนและกำหนดค่าใหม่ให้กับการอ้างอิงได้ แต่ไม่สามารถทำได้ กรุณาโฟกัส !!
dhokar.w

5

ความแตกต่างคือตัวแปรตัวชี้แบบไม่คงที่ (เพื่อไม่ให้สับสนกับตัวชี้ไปยังค่าคงที่) อาจมีการเปลี่ยนแปลงบางครั้งในระหว่างการดำเนินการของโปรแกรมต้องการตัวชี้ความหมายที่จะใช้ (&, *) ผู้ประกอบการในขณะที่การอ้างอิงสามารถตั้งค่า เท่านั้น (นั่นคือเหตุผลที่คุณสามารถตั้งค่าไว้ในรายการ initializer คอนสตรัคเตอร์เท่านั้น แต่ไม่ใช่อย่างอื่น) และใช้ค่าการเข้าถึงซีแมนทิกส์สามัญ มีการแนะนำการอ้างอิงโดยพื้นฐานเพื่อให้รองรับผู้ให้บริการมากเกินไปเนื่องจากฉันได้อ่านหนังสือเก่า ๆ ตามที่บางคนระบุไว้ในหัวข้อนี้ - ตัวชี้สามารถตั้งค่าเป็น 0 หรือค่าอะไรก็ได้ที่คุณต้องการ 0 (NULL, nullptr) หมายความว่าตัวชี้ถูกเตรียมใช้งานโดยไม่มีอะไร มันเป็นข้อผิดพลาดของตัวชี้โมฆะ dereference แต่จริงๆแล้วตัวชี้อาจมีค่าที่ไม่ได้ชี้ไปยังตำแหน่งหน่วยความจำที่ถูกต้อง การอ้างอิงในตาของพวกเขาพยายามที่จะไม่อนุญาตให้ผู้ใช้เพื่อเริ่มต้นการอ้างอิงถึงบางสิ่งบางอย่างที่ไม่สามารถอ้างอิงได้เนื่องจากความจริงที่ว่าคุณให้ค่า rvalue ประเภทที่ถูกต้องเสมอ แม้ว่าจะมีหลายวิธีในการทำให้ตัวแปรอ้างอิงถูกกำหนดค่าเริ่มต้นไปยังตำแหน่งหน่วยความจำผิด - เป็นการดีกว่าสำหรับคุณที่จะไม่ขุดลึกลงไปในรายละเอียด ในระดับเครื่องทั้งตัวชี้และการอ้างอิงทำงานอย่างสม่ำเสมอ - ผ่านตัวชี้ สมมติว่าในการอ้างอิงที่สำคัญคือน้ำตาลประโยค การอ้างอิงค่า rvalue นั้นแตกต่างจากนี้ - เป็นวัตถุสแต็ก / กองฮีปตามธรรมชาติ แม้ว่าจะมีหลายวิธีในการทำให้ตัวแปรอ้างอิงถูกกำหนดค่าเริ่มต้นไปยังตำแหน่งหน่วยความจำผิด - เป็นการดีกว่าสำหรับคุณที่จะไม่ขุดลึกลงไปในรายละเอียด ในระดับเครื่องทั้งตัวชี้และการอ้างอิงทำงานอย่างสม่ำเสมอ - ผ่านตัวชี้ สมมติว่าในการอ้างอิงที่สำคัญคือน้ำตาลประโยค การอ้างอิงค่า rvalue นั้นแตกต่างจากนี้ - เป็นวัตถุสแต็ก / กองฮีปตามธรรมชาติ แม้ว่าจะมีหลายวิธีในการทำให้ตัวแปรอ้างอิงถูกกำหนดค่าเริ่มต้นไปยังตำแหน่งหน่วยความจำผิด - เป็นการดีกว่าสำหรับคุณที่จะไม่ขุดลึกลงไปในรายละเอียด ในระดับเครื่องทั้งตัวชี้และการอ้างอิงทำงานอย่างสม่ำเสมอ - ผ่านตัวชี้ สมมติว่าในการอ้างอิงที่สำคัญคือน้ำตาลประโยค การอ้างอิงค่า rvalue นั้นแตกต่างจากนี้ - เป็นวัตถุสแต็ก / กองฮีปตามธรรมชาติ


4

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

int a = 20;
int &r = a;
r = 40;  /* now the value of a is changed to 40 */

int b =20;
int *ptr;
ptr = &b;  /*assigns address of b to ptr not the value */
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.