ฉันรู้ว่าการอ้างอิงคือน้ำตาลประโยคดังนั้นรหัสจึงง่ายต่อการอ่านและเขียน
แต่อะไรคือความแตกต่าง
int &x = *(int*)0;
บน gcc การอ้างอิงสามารถชี้ไปที่ NULL ได้
ฉันรู้ว่าการอ้างอิงคือน้ำตาลประโยคดังนั้นรหัสจึงง่ายต่อการอ่านและเขียน
แต่อะไรคือความแตกต่าง
int &x = *(int*)0;
บน gcc การอ้างอิงสามารถชี้ไปที่ NULL ได้
คำตอบ:
ตัวชี้สามารถกำหนดใหม่ได้:
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;
ตัวชี้มีที่อยู่หน่วยความจำและขนาดของตัวเองบนสแต็ก (4 ไบต์บน x86) ในขณะที่การอ้างอิงใช้ที่อยู่หน่วยความจำเดียวกัน (พร้อมตัวแปรดั้งเดิม) แต่ยังใช้พื้นที่บนสแต็ก เนื่องจากการอ้างอิงมีที่อยู่เดียวกันกับตัวแปรดั้งเดิมจึงปลอดภัยที่จะคิดว่าการอ้างอิงเป็นชื่ออื่นสำหรับตัวแปรเดียวกัน หมายเหตุ: ตัวชี้ใดที่สามารถอยู่บนสแต็กหรือกอง อ้างอิงเหมือนกัน การอ้างสิทธิ์ของฉันในคำชี้แจงนี้ไม่ใช่ตัวชี้ต้องชี้ไปที่สแต็ก ตัวชี้เป็นเพียงตัวแปรที่เก็บที่อยู่หน่วยความจำ ตัวแปรนี้อยู่ในสแต็ก เนื่องจากการอ้างอิงมีพื้นที่ของตัวเองบนสแต็กและเนื่องจากที่อยู่นั้นเหมือนกับตัวแปรที่อ้างอิง เพิ่มเติมเกี่ยวกับstack vs heap. นี่หมายความว่ามีที่อยู่จริงของการอ้างอิงที่คอมไพเลอร์จะไม่บอกคุณ
int x = 0;
int &r = x;
int *p = &x;
int *p2 = &r;
assert(p == p2);
คุณสามารถมีพอยน์เตอร์ให้กับพอยน์เตอร์ถึงพอยน์เตอร์ที่เสนอระดับการอ้อมเพิ่มเติม ในขณะที่การอ้างอิงมีเพียงระดับทางอ้อมเท่านั้น
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);
สามารถกำหนดตัวชี้ได้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
พอยน์เตอร์สามารถทำซ้ำผ่านอาร์เรย์ คุณสามารถใช้++
เพื่อไปที่รายการถัดไปที่ตัวชี้ชี้ไปและ+ 4
ไปที่องค์ประกอบที่ 5 ไม่ว่าขนาดของวัตถุจะเป็นเท่าใดที่ตัวชี้ชี้ไป
ตัวชี้ต้องถูกยกเลิกการ*
ลงทะเบียนเพื่อเข้าถึงตำแหน่งหน่วยความจำที่ชี้ไปในขณะที่การอ้างอิงสามารถใช้โดยตรง ตัวชี้ไปยังการใช้งานระดับ / struct เข้าถึงมันเป็นสมาชิกในขณะที่ใช้อ้างอิง->
.
ไม่สามารถยัดข้อมูลอ้างอิงลงในอาร์เรย์ได้ในขณะที่พอยน์เตอร์สามารถ (กล่าวถึงโดย user @litb)
การอ้างอิง Const สามารถผูกกับขมับ พอยน์เตอร์ไม่สามารถ (ไม่ได้โดยไม่มีทางอ้อม):
const int &x = int(12); //legal C++
int *y = &int(12); //illegal to dereference a temporary.
สิ่งนี้ทำให้const&
ปลอดภัยยิ่งขึ้นสำหรับใช้ในรายการอาร์กิวเมนต์และอื่น ๆ
การอ้างอิงสามารถคิดได้ว่าเป็นตัวชี้คงที่ (เพื่อไม่ให้สับสนกับตัวชี้ไปยังค่าคงที่!) ด้วยการเปลี่ยนทิศทางโดยอัตโนมัติเช่นคอมไพเลอร์จะใช้*
โอเปอเรเตอร์สำหรับคุณ
การอ้างอิงทั้งหมดจะต้องเริ่มต้นด้วยค่าที่ไม่ใช่ศูนย์มิฉะนั้นการรวบรวมจะล้มเหลว ไม่สามารถรับที่อยู่ของการอ้างอิง - ผู้ดำเนินการที่อยู่จะส่งคืนที่อยู่ของค่าอ้างอิงแทน - หรือเป็นไปได้ที่จะทำการคำนวณทางคณิตศาสตร์เกี่ยวกับการอ้างอิง
โปรแกรมเมอร์ C อาจไม่ชอบการอ้างอิง C ++ เนื่องจากมันจะไม่ชัดเจนอีกต่อไปเมื่อเกิดการเปลี่ยนทิศทางหรือถ้าอาร์กิวเมนต์ได้รับการส่งผ่านโดยค่าหรือตัวชี้โดยไม่ดูที่ฟังก์ชันของลายเซ็น
โปรแกรมเมอร์ C ++ อาจไม่ชอบใช้พอยน์เตอร์เนื่องจากถือว่าไม่ปลอดภัย - แม้ว่าการอ้างอิงจะไม่ปลอดภัยกว่าพอยน์เตอร์ที่แน่นอนยกเว้นในกรณีที่น่ารำคาญที่สุด - ขาดความสะดวกในการอ้อมโดยอัตโนมัติและมีความหมายแฝงความหมายแตกต่างกัน
พิจารณาคำสั่งต่อไปนี้จากคำถามที่พบบ่อย C ++ :
แม้ว่าการอ้างอิงมักจะดำเนินการโดยใช้ที่อยู่ในภาษาประกอบพื้นฐานโปรดอย่าได้คิดว่าการอ้างอิงเป็นตัวชี้ตลกมองไปยังวัตถุ การอ้างอิงคือวัตถุ มันไม่ได้เป็นตัวชี้ไปยังวัตถุหรือสำเนาของวัตถุ มันเป็นวัตถุ
แต่ถ้าการอ้างอิงเป็นวัตถุจริงๆแล้วจะมีการอ้างอิงที่ห้อยอยู่ได้อย่างไร? ในภาษาที่ไม่มีการจัดการมันเป็นไปไม่ได้ที่การอ้างอิงจะเป็น 'ปลอดภัย' มากกว่าตัวชี้ - โดยทั่วไปแล้วมันไม่มีทางที่จะใช้นามแฝงค่าที่เชื่อถือได้ข้ามขอบเขต!
มาจากพื้นหลัง C A, C ++ อ้างอิงอาจมีลักษณะเช่นแนวคิดที่ค่อนข้างโง่ แต่หนึ่งยังคงควรใช้พวกเขาแทนของตัวชี้ที่เป็นไปได้: ร้ายอัตโนมัติคือความสะดวกสบายและการอ้างอิงเป็นประโยชน์โดยเฉพาะอย่างยิ่งเมื่อต้องรับมือกับRAII - แต่ไม่ได้เพราะความปลอดภัยของการรับรู้ใด ๆ ข้อได้เปรียบ แต่เพราะพวกเขาทำให้การเขียนรหัสสำนวนน้อยที่น่าอึดอัดใจ
RAII เป็นหนึ่งในแนวคิดหลักของ C ++ แต่มันไม่โต้ตอบกับการคัดลอกความหมาย การส่งวัตถุโดยการอ้างอิงหลีกเลี่ยงปัญหาเหล่านี้เนื่องจากไม่มีการคัดลอกที่เกี่ยวข้อง หากไม่มีการอ้างอิงในภาษาคุณจะต้องใช้พอยน์เตอร์แทนซึ่งยุ่งยากกว่าในการใช้ดังนั้นเป็นการละเมิดหลักการออกแบบภาษาว่าวิธีแก้ปัญหาที่ดีที่สุดควรง่ายกว่าตัวเลือกอื่น
หากคุณต้องการอวดความจริงมีสิ่งหนึ่งที่คุณสามารถทำได้ด้วยการอ้างอิงที่คุณไม่สามารถทำได้ด้วยตัวชี้: ขยายอายุการใช้งานของวัตถุชั่วคราว ใน 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 ไปยังวัตถุชั่วคราวและไม่สามารถใช้ที่อยู่ของวัตถุนั้นได้
const &
โยงและเฉพาะเมื่อการอ้างอิงออกไปนอกขอบเขต destructor ของชนิดที่อ้างอิงจริง (เมื่อเปรียบเทียบกับประเภทการอ้างอิงที่อาจเป็นฐาน) จะถูกเรียก เนื่องจากเป็นการอ้างอิงจึงไม่มีการแบ่งส่วนใด ๆ
Animal x = fast ? getHare() : getTortoise()
แล้วx
จะประสบปัญหาการแบ่งคลาสสิกในขณะที่Animal& x = ...
จะทำงานอย่างถูกต้อง
นอกเหนือจากน้ำตาล syntactic การอ้างอิงคือconst
ตัวชี้ ( ไม่ใช่ตัวชี้ไปที่const
) คุณต้องสร้างสิ่งที่มันอ้างถึงเมื่อคุณประกาศตัวแปรอ้างอิงและคุณไม่สามารถเปลี่ยนแปลงได้ในภายหลัง
อัปเดต: ตอนนี้ฉันคิดอีกแล้วมีความแตกต่างที่สำคัญ
เป้าหมายของตัวชี้ const สามารถถูกแทนที่โดยการใช้ที่อยู่ของมันและใช้นักแสดง const
เป้าหมายของการอ้างอิงไม่สามารถถูกแทนที่ด้วยวิธีการใด ๆ ที่สั้นของ UB
สิ่งนี้ควรอนุญาตให้คอมไพเลอร์ทำการเพิ่มประสิทธิภาพมากขึ้นในการอ้างอิง
T* const
น้ำตาลที่มีวากยสัมพันธ์ที่แตกต่างกัน (ซึ่งเกิดขึ้นเพื่อกำจัด * และ & จากรหัสของคุณ)
int i; int const *pci = &i; /* implicit conv to const int* */ int *pi = const_cast<int*>(pci);
ตกลง
ตรงกันข้ามกับความเห็นที่นิยมมันเป็นไปได้ที่จะมีการอ้างอิงที่เป็นโมฆะ
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
คุณลืมส่วนที่สำคัญที่สุด:
การเข้าถึงของสมาชิกกับพอยน์เตอร์ใช้->
การเข้าถึงของสมาชิกกับการอ้างอิงที่ใช้.
foo.bar
เป็นอย่างชัดเจนดีกว่าfoo->bar
ในทางเดียวกันว่าviเป็นอย่างชัดเจนดีกว่าEmacs :-)
->
ให้ไว้สำหรับการอ้างอิงไปยังตัวชี้เช่นเดียวกับตัวชี้
.
และ->
มีสิ่งที่จะทำอย่างไรกับ vi และ emacs :)
.
ดีกว่าการใช้->
แต่ก็เหมือนกับ vi vs emacs มันเป็นเรื่องส่วนตัวและคุณไม่สามารถพิสูจน์อะไรได้เลย
การอ้างอิงมีความคล้ายคลึงกับพอยน์เตอร์ แต่พวกมันถูกสร้างขึ้นมาโดยเฉพาะเพื่อเป็นประโยชน์ในการปรับแต่งคอมไพเลอร์
ตัวอย่างเช่น:
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)
ถูกทำลายในตอนท้ายของการแสดงออก อย่างไรก็ตามโดยการผูกวัตถุนั้นกับการอ้างอิงref
C + + จะขยายอายุการใช้งานของวัตถุชั่วคราวนั้นจนกว่าref
จะออกนอกขอบเขต
maybeModify
ไม่ใช้ที่อยู่ของสิ่งที่เกี่ยวข้องจะx
ง่ายกว่าการพิสูจน์ว่าการคำนวณเลขตัวชี้ไม่เกิดขึ้น
void maybeModify(int& x) { 1[&x]++; }
คิดซึ่งความคิดเห็นอื่น ๆ ข้างต้นกำลังพูดถึง
จริงๆแล้วการอ้างอิงนั้นไม่เหมือนตัวชี้
คอมไพเลอร์เก็บ "อ้างอิง" กับตัวแปร, เชื่อมโยงชื่อกับที่อยู่หน่วยความจำ; เป็นหน้าที่ของการแปลชื่อตัวแปรใด ๆ ไปยังที่อยู่หน่วยความจำเมื่อทำการรวบรวม
เมื่อคุณสร้างการอ้างอิงคุณจะบอกคอมไพเลอร์ว่าคุณกำหนดชื่ออื่นให้กับตัวแปรตัวชี้ นั่นเป็นสาเหตุที่การอ้างอิงไม่สามารถ "ชี้ไปที่โมฆะ" ได้เนื่องจากตัวแปรไม่สามารถเป็นได้และไม่ใช่
พอยน์เตอร์เป็นตัวแปร พวกเขามีที่อยู่ของตัวแปรอื่น ๆ หรืออาจเป็นโมฆะ สิ่งสำคัญคือตัวชี้มีค่าในขณะที่การอ้างอิงเท่านั้นมีตัวแปรที่อ้างอิง
ตอนนี้คำอธิบายของรหัสจริง:
int a = 0;
int& b = a;
ที่นี่คุณไม่ได้สร้างตัวแปรอื่นที่ชี้ไปที่a
; a
คุณจะเป็นเพียงการเพิ่มชื่ออีกครั้งเพื่อให้เนื้อหาของหน่วยความจำที่ถือค่าของ หน่วยความจำนี้มีสองชื่อa
และb
และสามารถแก้ไขได้โดยใช้ชื่อใดชื่อหนึ่ง
void increment(int& n)
{
n = n + 1;
}
int a;
increment(a);
เมื่อเรียกใช้ฟังก์ชันคอมไพเลอร์มักสร้างพื้นที่หน่วยความจำสำหรับอาร์กิวเมนต์ที่จะคัดลอกไป ฟังก์ชั่นลายเซ็นกำหนดช่องว่างที่ควรจะสร้างและให้ชื่อที่ควรใช้สำหรับช่องว่างเหล่านี้ การประกาศพารามิเตอร์เป็นการอ้างอิงเพียงบอกคอมไพเลอร์ให้ใช้พื้นที่หน่วยความจำตัวแปรอินพุตแทนการจัดสรรพื้นที่หน่วยความจำใหม่ในระหว่างการเรียกใช้เมธอด มันอาจฟังดูแปลกที่จะบอกว่าฟังก์ชั่นของคุณจะจัดการกับตัวแปรที่ประกาศในขอบเขตการโทรโดยตรง แต่จำไว้ว่าเมื่อรันโค้ดที่คอมไพล์แล้วจะไม่มีขอบเขตอีกต่อไป มีหน่วยความจำแบบเรียบธรรมดาและรหัสฟังก์ชั่นของคุณสามารถปรับเปลี่ยนตัวแปรใด ๆ ได้
ตอนนี้อาจมีบางกรณีที่คอมไพเลอร์ของคุณอาจไม่สามารถทราบข้อมูลอ้างอิงเมื่อรวบรวมเช่นเมื่อใช้ตัวแปร extern ดังนั้นการอ้างอิงอาจหรือไม่อาจใช้เป็นตัวชี้ในรหัสอ้างอิง แต่ในตัวอย่างที่ฉันให้คุณเป็นไปได้มากที่สุดที่จะไม่ใช้ตัวชี้
การอ้างอิงไม่สามารถเป็นNULL
ได้
void Foo::bar() { virtual_baz(); }
segfaults นั้น หากคุณไม่ทราบว่าการอ้างอิงอาจเป็นโมฆะคุณไม่สามารถติดตามค่า null กลับไปยังจุดเริ่มต้นได้
int &r=*p;
เป็นพฤติกรรมที่ไม่ได้กำหนด ณ จุดที่คุณไม่ได้มี "การอ้างอิงชี้โมฆะ" คุณมีโปรแกรมที่ไม่สามารถให้เหตุผลเกี่ยวกับการที่ทุกคน
ในขณะที่ทั้งการอ้างอิงและพอยน์เตอร์ถูกใช้เพื่อเข้าถึงค่าอื่นทางอ้อมมีความแตกต่างที่สำคัญสองประการระหว่างการอ้างอิงและพอยน์เตอร์ ประการแรกคือการอ้างอิงมักจะอ้างถึงวัตถุ: มันเป็นข้อผิดพลาดในการกำหนดการอ้างอิงโดยไม่ต้องเริ่มต้นมัน พฤติกรรมของการมอบหมายคือความแตกต่างที่สำคัญที่สอง: การกำหนดให้การอ้างอิงเปลี่ยนวัตถุที่การอ้างอิงถูกผูกไว้ มันไม่ได้เชื่อมโยงการอ้างอิงไปยังวัตถุอื่น เมื่อเริ่มต้นแล้วการอ้างอิงจะอ้างอิงถึงวัตถุต้นแบบเดียวกันเสมอ
พิจารณาชิ้นส่วนของโปรแกรมทั้งสองนี้ ในครั้งแรกเรากำหนดหนึ่งตัวชี้ไปยังอีก:
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 ไม่ใช่การอ้างอิง หลังจากการมอบหมายการอ้างอิงทั้งสองยังคงอ้างถึงวัตถุดั้งเดิมของพวกเขาและมูลค่าของวัตถุเหล่านั้นก็เหมือนกันเช่นกัน
มีความแตกต่างทางความหมายที่อาจดูลึกลับหากคุณไม่คุ้นเคยกับการเรียนภาษาคอมพิวเตอร์ในแบบนามธรรมหรือแม้แต่ในเชิงวิชาการ
ในระดับสูงสุดแนวคิดในการอ้างอิงคือพวกเขาเป็น "นามแฝง" แบบโปร่งใส คอมพิวเตอร์ของคุณอาจใช้ที่อยู่เพื่อให้ทำงานได้ แต่คุณไม่ควรกังวลเกี่ยวกับสิ่งนั้น: คุณควรจะคิดว่าพวกเขาเป็น "ชื่ออื่น" สำหรับวัตถุที่มีอยู่และไวยากรณ์แสดงถึงสิ่งนั้น พวกมันเข้มงวดกว่าพอยน์เตอร์เพื่อให้คอมไพเลอร์ของคุณสามารถเตือนคุณได้อย่างน่าเชื่อถือเมื่อคุณกำลังจะสร้างการอ้างอิงที่ห้อยต่องแต่งมากกว่าเมื่อคุณกำลังจะสร้างตัวชี้ห้อย
นอกจากนั้นยังมีความแตกต่างระหว่างพอยน์เตอร์และการอ้างอิง ไวยากรณ์ที่ใช้พวกเขาแตกต่างกันอย่างเห็นได้ชัดและคุณไม่สามารถอ้างอิง "re-seat" มีการอ้างอิงถึงความว่างเปล่าหรือมีตัวชี้ไปยังการอ้างอิง
การอ้างอิงเป็นนามแฝงสำหรับตัวแปรอื่นในขณะที่ตัวชี้ถือที่อยู่หน่วยความจำของตัวแปร การอ้างอิงโดยทั่วไปจะใช้เป็นพารามิเตอร์ฟังก์ชันเพื่อให้วัตถุที่ส่งผ่านไม่ใช่สำเนา แต่เป็นวัตถุเอง
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.
ไม่สำคัญว่าต้องใช้พื้นที่มากแค่ไหนเนื่องจากคุณไม่เห็นผลข้างเคียงใด ๆ (โดยไม่ต้องใช้รหัส) ในทุกพื้นที่ที่มันต้องใช้
ในทางตรงกันข้ามความแตกต่างที่สำคัญอย่างหนึ่งระหว่างการอ้างอิงและพอยน์เตอร์คือการกำหนดเวลาให้การอ้างอิง 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 ทำงาน
แห่งนี้ตั้งอยู่บนพื้นฐานของการกวดวิชา สิ่งที่เขียนทำให้ชัดเจนยิ่งขึ้น:
>>> 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
นอกจากนี้ยังตกลงที่จะลืมคำศัพท์ของคำสั่งดังกล่าวคือการสร้างการอ้างอิงของTom
Tom
nullptr
หรือไม่? คุณอ่านส่วนอื่น ๆ ของกระทู้นี้จริง ๆ หรือ ...
การอ้างอิงไม่ใช่ชื่ออื่นที่ให้กับหน่วยความจำบางส่วน เป็นตัวชี้ที่ไม่เปลี่ยนรูปซึ่งจะไม่อ้างอิงโดยอัตโนมัติในการใช้งาน โดยทั่วไปจะเดือดลงไปที่:
int& j = i;
มันกลายเป็นภายใน
int* const j = &i;
const
ตัวชี้ไปใช้ ความยืดหยุ่นนั้นไม่ได้พิสูจน์ว่ามีความแตกต่างระหว่างการอ้างอิงและตัวชี้
การอ้างอิงใน C ++ คืออะไร? อินสแตนซ์เฉพาะบางประเภทที่ไม่ใช่ประเภทวัตถุไม่ได้เป็นชนิดของวัตถุ
ตัวชี้ใน C ++ คืออะไร อินสแตนซ์เฉพาะบางประเภทที่เป็นประเภทวัตถุเป็นชนิดของวัตถุ
จากคำนิยาม ISO C ++ ของประเภทวัตถุ :
วัตถุประเภทคือ (อาจจะเป็นพันธุ์ -qualified) ประเภทที่ไม่ได้เป็นประเภทฟังก์ชั่นไม่ได้เป็นชนิดการอ้างอิงและไม่พันธุ์เป็นโมฆะ
มันอาจสำคัญที่ต้องทราบชนิดของวัตถุเป็นหมวดหมู่ระดับบนสุดของจักรวาลประเภทใน C ++ การอ้างอิงเป็นหมวดหมู่ระดับบนสุดแต่ตัวชี้ไม่ใช่
ตัวชี้และการอ้างอิงที่กล่าวถึงกันในบริบทของประเภทสารประกอบ นี่เป็นพื้นฐานเนื่องจากลักษณะของไวยากรณ์ของผู้ประกาศที่สืบทอดมาจาก (และขยาย) C ซึ่งไม่มีการอ้างอิง (นอกจากนี้ยังมีตัวประกาศมากกว่าหนึ่งชนิดตั้งแต่ C ++ 11 ในขณะที่พอยน์เตอร์ยังคงเป็น "unityped": &
+ &&
กับ*
.) ดังนั้นการร่างภาษาเฉพาะโดย "ส่วนขยาย" ที่มีลักษณะคล้ายกันของ C ในบริบทนี้ค่อนข้างสมเหตุสมผล . (ฉันจะยังคงยืนยันว่าวากยสัมพันธ์ของผู้ประกาศเสียการแสดงออกเชิงวากยสัมพันธ์มากทำให้ทั้งผู้ใช้มนุษย์และการใช้งานที่น่าผิดหวังดังนั้นพวกเขาทั้งหมดไม่ได้มีคุณสมบัติที่จะสร้างขึ้นในการออกแบบภาษาใหม่ นี่เป็นหัวข้อที่แตกต่างอย่างสิ้นเชิงเกี่ยวกับการออกแบบ PL)
มิฉะนั้นจะไม่มีนัยสำคัญที่ตัวชี้สามารถมีคุณสมบัติเป็นประเภทที่เฉพาะเจาะจงพร้อมการอ้างอิงร่วมกัน พวกเขาแบ่งปันคุณสมบัติทั่วไปน้อยเกินไปนอกเหนือจากความคล้ายคลึงกันทางไวยากรณ์ดังนั้นจึงไม่จำเป็นต้องรวมเข้าด้วยกันในกรณีส่วนใหญ่
โปรดสังเกตว่าข้อความข้างต้นระบุถึง "พอยน์เตอร์" และ "การอ้างอิง" เป็นประเภทเท่านั้น มีคำถามที่สนใจเกี่ยวกับอินสแตนซ์ของพวกเขา (เช่นตัวแปร) นอกจากนี้ยังมีความเข้าใจผิดมามากเกินไป
ความแตกต่างของหมวดหมู่ระดับบนสุดสามารถเปิดเผยความแตกต่างที่เป็นรูปธรรมจำนวนมากซึ่งไม่ได้เชื่อมโยงกับตัวชี้โดยตรง:
cv
บ่น การอ้างอิงไม่สามารถกฎพิเศษเพิ่มเติมอีกสองสามข้อเกี่ยวกับการอ้างอิง:
&&
พารามิเตอร์ (ในฐานะ "การอ้างอิงการส่งต่อ") ขึ้นอยู่กับการยุบอ้างอิงระหว่างการลดพารามิเตอร์เทมเพลตอนุญาตให้"การส่งต่อที่สมบูรณ์แบบ"ของพารามิเตอร์std::initializer_list
กฎการขยายอายุการอ้างอิงที่คล้ายกัน มันเป็นหนอนอีกกระป๋องหนึ่งฉันรู้ว่าการอ้างอิงคือน้ำตาลประโยคดังนั้นรหัสจึงง่ายต่อการอ่านและเขียน
ในทางเทคนิคแล้วมันผิดธรรมดา การอ้างอิงไม่ใช่น้ำตาลเชิงซ้อนของคุณสมบัติอื่นใดใน C ++ เนื่องจากไม่สามารถแทนที่ได้อย่างแน่นอนโดยคุณสมบัติอื่น ๆ โดยไม่มีความแตกต่างทางความหมาย
(ในทำนองเดียวกันแลมบ์ดา - นิพจน์ s ไม่ได้เป็นน้ำตาล syntactic ของคุณสมบัติอื่น ๆ ใน C ++ เพราะมันไม่สามารถจำลองได้อย่างแม่นยำด้วยคุณสมบัติ "ไม่ระบุ" เช่นคำสั่งประกาศของตัวแปรที่ถูกจับซึ่งอาจมีความสำคัญเพราะลำดับการเริ่มต้นของตัวแปรนั้น อย่างมีนัยสำคัญ.)
C ++ มีน้ำตาลเชิงประโยคเพียงไม่กี่ชนิดในแง่นี้ อินสแตนซ์หนึ่งคือ (สืบทอดมาจาก C) โอเปอเรเตอร์ในตัว (ไม่ใช่โอเวอร์โหลด) []
ซึ่งกำหนดไว้ว่ามีคุณสมบัติความหมายเดียวกันของรูปแบบเฉพาะของการรวมกันในตัวดำเนินการ unary *
และไบนารี+
และไบนารี
ดังนั้นตัวชี้และการอ้างอิงจึงใช้หน่วยความจำในจำนวนเท่ากัน
ข้อความข้างต้นผิดพลาด เพื่อหลีกเลี่ยงความเข้าใจผิดดังกล่าวให้ดูที่กฎ ISO C ++ แทน:
จาก[intro.object] / 1 :
... วัตถุหนึ่งครอบครองพื้นที่เก็บข้อมูลในช่วงเวลาของการก่อสร้างตลอดช่วงชีวิตของมันและในช่วงเวลาแห่งการทำลายล้าง ...
จาก[dcl.ref] / 4 :
มันไม่ได้ระบุว่าต้องการการอ้างอิงหรือไม่
หมายเหตุเหล่านี้เป็นคุณสมบัติความหมาย
แม้ว่าพอยน์เตอร์จะไม่ผ่านการรับรองเพียงพอที่จะใส่รวมกับการอ้างอิงในแง่ของการออกแบบภาษา แต่ก็ยังมีข้อโต้แย้งบางอย่างที่ทำให้เป็นที่ถกเถียงกันที่จะเลือกระหว่างพวกเขาในบริบทอื่น ๆ เช่นเมื่อทำการเลือกชนิดพารามิเตอร์
แต่นี่ไม่ใช่เรื่องราวทั้งหมด ฉันหมายถึงมีหลายสิ่งมากกว่าพอยน์เตอร์เทียบกับการอ้างอิงที่คุณต้องพิจารณา
หากคุณไม่ต้องติดกับตัวเลือกที่เฉพาะเจาะจงเช่นนั้นในกรณีส่วนใหญ่คำตอบนั้นสั้น: คุณไม่จำเป็นต้องใช้พอยน์เตอร์ดังนั้นคุณจึงไม่จำเป็น พอยน์เตอร์มักจะไม่ดีพอเพราะสิ่งเหล่านั้นบ่งบอกถึงสิ่งต่าง ๆ ที่คุณไม่คาดคิดมากเกินไปและพวกเขาจะต้องพึ่งพาสมมติฐานโดยปริยายจำนวนมากเกินไปซึ่งจะบ่อนทำลายความสามารถในการบำรุงรักษาและแม้แต่การพกพาของโค้ด การใช้พอยน์เตอร์โดยไม่จำเป็นเป็นรูปแบบที่ไม่ดีแน่นอนและควรหลีกเลี่ยงในแง่ของ C ++ ที่ทันสมัย ทบทวนจุดประสงค์ของคุณและในที่สุดคุณจะพบว่าตัวชี้นั้นเป็นคุณสมบัติของการเรียงลำดับครั้งสุดท้ายในกรณีส่วนใหญ่
&
reference ที่ระบุเป็นพารามิเตอร์ประเภทที่ 1 (และโดยปกติควรมีconst
คุณสมบัติ)&&
ประเภทการอ้างอิงเป็นประเภทพารามิเตอร์ที่ 1 (และโดยปกติจะไม่มีตัวระบุ)operator=
ในฐานะสมาชิกฟังก์ชันพิเศษต้องใช้ประเภทการอ้างอิงคล้ายกับพารามิเตอร์ที่ 1 ของตัวสร้างสำเนา / ย้าย++
int
unique_ptr
และshared_ptr
(หรือแม้แต่กับโฮมบรูว์ด้วยตัวเองหากคุณต้องการให้ทึบ ) แทนที่จะใช้พอยน์เตอร์ชี้std::optional
แทนที่จะเป็นพอยน์เตอร์พอยน์เตอร์observer_ptr
ใน Library Fundamental TSข้อยกเว้นเพียงอย่างเดียวไม่สามารถแก้ไขได้ในภาษาปัจจุบัน:
operator new
โดยเฉพาะการปฏิบัติภาษาทำงานร่วมกันจำเป็นต้องมีตัวชี้เช่น (อย่างไรก็ตามcv - void*
ยังคงแตกต่างกันและปลอดภัยกว่าเมื่อเทียบกับตัวชี้วัตถุธรรมดาเพราะมันจะออกกฎตัวคำนวณเลขคณิตที่ไม่คาดคิดนอกเสียจากว่าคุณจะพึ่งพาส่วนขยายที่ไม่สอดคล้องกับบางอย่างvoid*
ของ GNU)ดังนั้นในทางปฏิบัติคำตอบนั้นชัดเจนมาก: เมื่อสงสัยให้หลีกเลี่ยงพอยน์เตอร์ คุณต้องใช้พอยน์เตอร์ก็ต่อเมื่อมีเหตุผลที่ชัดเจนว่าไม่มีสิ่งใดเหมาะสมกว่า ยกเว้นกรณีพิเศษสองสามข้อที่กล่าวถึงข้างต้นตัวเลือกดังกล่าวมักไม่เฉพาะเจาะจง C ++ เท่านั้น (แต่น่าจะเป็นภาษาที่ใช้งานเฉพาะ) กรณีดังกล่าวสามารถ:
หากคุณพบคำถามผ่านผลการค้นหาของ Google บางตัว (ไม่ใช่เฉพาะ C ++)นี่น่าจะเป็นตำแหน่งที่ผิด
อ้างอิงใน C ++ ค่อนข้าง "แปลก" ราวกับมันเป็นหลักไม่ได้ชั้นแรก: พวกเขาจะได้รับการปฏิบัติเป็นวัตถุหรือฟังก์ชั่นที่ถูกเรียกเพื่อให้พวกเขามีโอกาสที่จะสนับสนุนการดำเนินการบางชั้นแรกเหมือนเป็นตัวถูกดำเนินการทางด้านซ้ายของไม่มีโอเปอเรเตอร์การเข้าถึงสมาชิกแยกประเภทของออบเจกต์ที่อ้างอิงได้อย่างอิสระ ภาษาอื่นอาจมีหรือไม่มีข้อ จำกัด ที่คล้ายคลึงกันในการอ้างอิงของพวกเขา
การอ้างอิงใน C ++ จะไม่รักษาความหมายในภาษาต่างๆ ตัวอย่างเช่นการอ้างอิงโดยทั่วไปไม่ได้หมายความถึงคุณสมบัติที่ไม่เป็นศูนย์ในค่าเช่นเดียวกับใน C ++ ดังนั้นสมมติฐานดังกล่าวอาจไม่ทำงานในภาษาอื่นบางภาษา (และคุณจะพบคู่ตัวอย่างค่อนข้างง่ายเช่น Java, C #, ... )
ยังคงมีคุณสมบัติทั่วไปบางอย่างในการอ้างอิงในภาษาการเขียนโปรแกรมที่แตกต่างกันโดยทั่วไป แต่ขอทิ้งไว้สำหรับคำถามอื่น ๆ ใน SO
(หมายเหตุด้าน: คำถามอาจมีความสำคัญเร็วกว่าภาษา "C-like" ที่เกี่ยวข้องเช่นALGOL 68 เทียบกับ PL / I )
การอ้างอิงถึงตัวชี้เป็นไปได้ใน 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;
}
ฉันใช้การอ้างอิงเว้นแต่ฉันต้องการอย่างใดอย่างหนึ่งต่อไปนี้:
พอยน์เตอร์ Null สามารถใช้เป็นค่าของ Sentinel ซึ่งมักเป็นวิธีที่ประหยัดในการหลีกเลี่ยงการทำงานหนักเกินไปหรือการใช้บูล
คุณสามารถทำเลขคณิตกับตัวชี้ได้ ตัวอย่างเช่น,p += offset;
&r + offset
ตำแหน่งที่r
ได้รับการประกาศเป็นข้อมูลอ้างอิง
มีความแตกต่างพื้นฐานอย่างหนึ่งระหว่างพอยน์เตอร์และการอ้างอิงที่ฉันไม่ได้เห็นใครพูดถึง: การอ้างอิงเปิดใช้งานซีแมนทิกส์แบบ pass-by-reference ในอาร์กิวเมนต์ของฟังก์ชัน ตัวชี้แม้ว่ามันจะไม่สามารถมองเห็นได้ในตอนแรกไม่ได้: พวกเขาเพียง แต่ให้ความหมายแบบ pass-by-value นี้ได้รับการอธิบายอย่างมากในบทความนี้
ขอแสดงความนับถือ & rzej
ความเสี่ยงในการเพิ่มความสับสนฉันต้องการที่จะโยนในการป้อนข้อมูลบางอย่างฉันแน่ใจว่าส่วนใหญ่ขึ้นอยู่กับวิธีที่คอมไพเลอร์ดำเนินการอ้างอิง แต่ในกรณีของ 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 เห็นได้ชัดว่ามันรวบรวมได้ดี แต่ทำให้เกิดข้อผิดพลาดในการแบ่งส่วนที่รันไทม์เพราะมันไม่ได้ชี้ไปที่ตัวแปรที่ถูกต้องอีกต่อไปเรามีการอ้างอิงที่ขาดซึ่งยังคงมีอยู่ (จนกระทั่งหลุดออกจากขอบเขต) แต่ไม่มีประโยชน์
กล่าวอีกนัยหนึ่งคือการอ้างอิงไม่มีอะไรนอกจากตัวชี้ที่มีกลไกการชี้ออกไปทำให้ปลอดภัยและใช้งานง่ายขึ้น (ไม่มีคณิตศาสตร์ตัวชี้โดยไม่ตั้งใจไม่ผสมกัน '.' และ '->' ฯลฯ ) สมมติว่าคุณ อย่าลองใช้เรื่องไร้สาระเหมือนตัวอย่างด้านบน;)
ตอนนี้ไม่คำนึงถึงวิธีการอ้างอิงคอมไพเลอร์ที่จับก็จะมักจะมีชนิดของตัวชี้ภายใต้ประทุนบางเพราะการอ้างอิงจะต้องอ้างถึงตัวแปรเฉพาะที่อยู่หน่วยความจำที่เฉพาะเจาะจงเพื่อให้การทำงานเป็นไปตามคาดไม่มีการรับรอบนี้ (เพราะฉะนั้น คำว่า 'อ้างอิง')
กฎสำคัญเพียงข้อเดียวที่สำคัญที่ต้องจดจำด้วยการอ้างอิงคือต้องกำหนดไว้ในเวลาที่ประกาศ (ยกเว้นการอ้างอิงในส่วนหัวในกรณีนั้นจะต้องกำหนดไว้ในตัวสร้างหลังจากวัตถุที่อยู่ในนั้นคือ สร้างมันสายเกินไปที่จะกำหนด)
โปรดจำไว้ว่าตัวอย่างของฉันด้านบนเป็นเพียงตัวอย่างที่แสดงให้เห็นว่าการอ้างอิงคืออะไรคุณจะไม่ต้องการใช้การอ้างอิงด้วยวิธีเหล่านั้น! สำหรับการใช้งานอ้างอิงอย่างถูกต้องมีคำตอบมากมายอยู่ที่นี่แล้วที่ตอกตะปูบนหัว
ความแตกต่างอีกประการหนึ่งคือคุณสามารถมีตัวชี้ให้เป็นประเภทโมฆะ (และมันหมายถึงตัวชี้ไปยังสิ่งใด ๆ ) แต่การอ้างอิงถึงโมฆะนั้นเป็นสิ่งต้องห้าม
int a;
void * p = &a; // ok
void & p = a; // forbidden
ฉันไม่สามารถพูดได้ว่าฉันมีความสุขมากกับความแตกต่างนี้ ฉันจะชอบมันมากจะได้รับอนุญาตให้มีการอ้างอิงความหมายกับสิ่งที่มีที่อยู่และมิฉะนั้นพฤติกรรมเดียวกันสำหรับการอ้างอิง มันจะช่วยให้การกำหนดเทียบเท่าของฟังก์ชั่นห้องสมุด C เช่น memcpy โดยใช้การอ้างอิง
นอกจากนี้การอ้างอิงที่เป็นพารามิเตอร์ของฟังก์ชันที่ถูก 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 ตามมูลค่ากว่าโดยอ้างอิงถ้าพวกเขาไม่ได้แก้ไขและส่งกลับโดยฟังก์ชั่น
การใช้การอ้างอิงที่น่าสนใจอีกประการหนึ่งคือการระบุอาร์กิวเมนต์เริ่มต้นของประเภทที่ผู้ใช้กำหนด:
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 ไปยังด้านชั่วคราว' ของการอ้างอิง
โปรแกรมนี้อาจช่วยในการทำความเข้าใจคำตอบของคำถาม นี่เป็นโปรแกรมอย่างง่ายของการอ้างอิง "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
ฉันรู้สึกว่ายังมีอีกประเด็นที่ยังไม่ได้กล่าวถึง
ซึ่งแตกต่างจากพอยน์เตอร์การอ้างอิงนั้นเทียบเท่ากับวัตถุที่พวกเขาอ้างถึงคือการดำเนินการใด ๆ ที่สามารถนำไปใช้กับวัตถุที่ใช้สำหรับการอ้างอิงและด้วยไวยากรณ์เดียวกันที่แน่นอน
แม้ว่าสิ่งนี้อาจดูตื้น ๆ แต่ฉันเชื่อว่าคุณสมบัตินี้มีความสำคัญอย่างยิ่งต่อคุณสมบัติ 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 ++ และไลบรารี่มาตรฐานดังนั้นนี่จึงเป็นคุณสมบัติหลักของการอ้างอิง
มีความแตกต่างที่ไม่ใช่ด้านเทคนิคที่สำคัญมากระหว่างพอยน์เตอร์และการอ้างอิง: อาร์กิวเมนต์ที่ส่งผ่านไปยังฟังก์ชันโดยตัวชี้จะมองเห็นได้ชัดเจนกว่าอาร์กิวเมนต์ที่ส่งผ่านไปยังฟังก์ชันโดยการอ้างอิงที่ไม่ใช่แบบ 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เป็นตัวอย่างของสิ่งนี้
บางทีอุปมาอุปมัยบางอย่างอาจช่วยได้ ในบริบทของหน้าจอเดสก์ทอปของคุณ -
ตัวชี้สามารถเริ่มต้นเป็น 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 "="
const int& i = 0
เรายังสามารถมี
ความแตกต่างคือตัวแปรตัวชี้แบบไม่คงที่ (เพื่อไม่ให้สับสนกับตัวชี้ไปยังค่าคงที่) อาจมีการเปลี่ยนแปลงบางครั้งในระหว่างการดำเนินการของโปรแกรมต้องการตัวชี้ความหมายที่จะใช้ (&, *) ผู้ประกอบการในขณะที่การอ้างอิงสามารถตั้งค่า เท่านั้น (นั่นคือเหตุผลที่คุณสามารถตั้งค่าไว้ในรายการ initializer คอนสตรัคเตอร์เท่านั้น แต่ไม่ใช่อย่างอื่น) และใช้ค่าการเข้าถึงซีแมนทิกส์สามัญ มีการแนะนำการอ้างอิงโดยพื้นฐานเพื่อให้รองรับผู้ให้บริการมากเกินไปเนื่องจากฉันได้อ่านหนังสือเก่า ๆ ตามที่บางคนระบุไว้ในหัวข้อนี้ - ตัวชี้สามารถตั้งค่าเป็น 0 หรือค่าอะไรก็ได้ที่คุณต้องการ 0 (NULL, nullptr) หมายความว่าตัวชี้ถูกเตรียมใช้งานโดยไม่มีอะไร มันเป็นข้อผิดพลาดของตัวชี้โมฆะ dereference แต่จริงๆแล้วตัวชี้อาจมีค่าที่ไม่ได้ชี้ไปยังตำแหน่งหน่วยความจำที่ถูกต้อง การอ้างอิงในตาของพวกเขาพยายามที่จะไม่อนุญาตให้ผู้ใช้เพื่อเริ่มต้นการอ้างอิงถึงบางสิ่งบางอย่างที่ไม่สามารถอ้างอิงได้เนื่องจากความจริงที่ว่าคุณให้ค่า rvalue ประเภทที่ถูกต้องเสมอ แม้ว่าจะมีหลายวิธีในการทำให้ตัวแปรอ้างอิงถูกกำหนดค่าเริ่มต้นไปยังตำแหน่งหน่วยความจำผิด - เป็นการดีกว่าสำหรับคุณที่จะไม่ขุดลึกลงไปในรายละเอียด ในระดับเครื่องทั้งตัวชี้และการอ้างอิงทำงานอย่างสม่ำเสมอ - ผ่านตัวชี้ สมมติว่าในการอ้างอิงที่สำคัญคือน้ำตาลประโยค การอ้างอิงค่า rvalue นั้นแตกต่างจากนี้ - เป็นวัตถุสแต็ก / กองฮีปตามธรรมชาติ แม้ว่าจะมีหลายวิธีในการทำให้ตัวแปรอ้างอิงถูกกำหนดค่าเริ่มต้นไปยังตำแหน่งหน่วยความจำผิด - เป็นการดีกว่าสำหรับคุณที่จะไม่ขุดลึกลงไปในรายละเอียด ในระดับเครื่องทั้งตัวชี้และการอ้างอิงทำงานอย่างสม่ำเสมอ - ผ่านตัวชี้ สมมติว่าในการอ้างอิงที่สำคัญคือน้ำตาลประโยค การอ้างอิงค่า rvalue นั้นแตกต่างจากนี้ - เป็นวัตถุสแต็ก / กองฮีปตามธรรมชาติ แม้ว่าจะมีหลายวิธีในการทำให้ตัวแปรอ้างอิงถูกกำหนดค่าเริ่มต้นไปยังตำแหน่งหน่วยความจำผิด - เป็นการดีกว่าสำหรับคุณที่จะไม่ขุดลึกลงไปในรายละเอียด ในระดับเครื่องทั้งตัวชี้และการอ้างอิงทำงานอย่างสม่ำเสมอ - ผ่านตัวชี้ สมมติว่าในการอ้างอิงที่สำคัญคือน้ำตาลประโยค การอ้างอิงค่า rvalue นั้นแตกต่างจากนี้ - เป็นวัตถุสแต็ก / กองฮีปตามธรรมชาติ
ในคำง่าย ๆ เราสามารถพูดได้ว่าการอ้างอิงเป็นชื่อทางเลือกสำหรับตัวแปรในขณะที่ตัวชี้เป็นตัวแปรที่ถือที่อยู่ของตัวแปรอื่น เช่น
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 */