/ Parameter

[365 วันแห่งโปรแกรม #day19] Pass by Pointer

วันที่สิบเก้าของ ‪#‎365วันแห่งโปรแกรม‬ วันนี้เราจะมีคุยกันเรื่อง Pass by Pointer


เมื่อหลายวันก่อนเราเคยคุยกันเรื่อง Parameter ครั้งนั้นผมพูดถึงการ Pass by Value และ Pass by Reference ไว้ แล้วผมก็สรุปว่าภาษาสมัยใหม่นั้นเวลาส่งตัวแปรแบบ Reference-Type เข้าไปในฟังก์ชัน จะถือเป็นการ Pass by Value เพราะเป็นการ copy reference ไปให้ formal parameter (Pass Reference by Value) ไม่ใช่สร้าง alias ใน function เพื่อแทน actual parameter ซึ่งมันก็ถูกต้องตามนั้น แต่มันมีของที่ถูกต้องกว่านั้น คือ Pass by Pointer

Pass by Pointer คืออะไร?

Pass by Pointer คือการที่เราส่ง pointer ที่ชี้ไปยัง object (address ของ object) เข้าไปในฟังก์ชันที่รับพารามิเตอร์เป็น pointer

Pass by Pointer ต่างจาก Pass by Reference ตรงไหน?

Pass by Reference เป็นการสร้าง Alias ให้ actual parameter เพื่อเอาไปใช้งานในฟังก์ชัน การเปลี่ยนแปลงใดๆ ที่กระทำต่อตัวแปรในฟังก์ชันก็จะกระทบถึงตัวแปรของ caller ด้วย แต่ Pass by Pointer คือการคัดลอกค่าของ address ที่ pointer นั้นชี้อยู่ไปให้ตัวแปรใหม่ (formal parameter) ในฟังก์ชันที่ถูกเรียก ดังนั้นทั้งสองตัวแปรไม่ถือว่าเป็นตัวแปรเดียวกัน แต่แค่ชี้ไปที่ address เดียวกัน ทำให้การ assign ค่าใหม่ให้ตัวแปรในฟังก์ชัน (ย้ายไปชี้ที่ address อื่น) จะไม่ส่งผลกระทบใดๆ ต่อตัวแปรของ caller

Pass by Pointer ต่างจาก Pass Reference by Value ตรงไหน?

ไม่ต่าง ในเมื่อเป็นการ copy reference ไปให้ formal parameter เหมือนกัน จึงไม่แตกต่างกัน ตอนที่เขียนบทความเก่าผมอิงจากภาษาใหม่ๆ เป็นหลัก ซึ่งจะไม่มี explicit pointer ให้ใช้จึงไม่ได้พูดถึง ดังนั้นขอให้เข้าใจตรงกันว่า Pass by Value แบ่งเป็น Pass by Value จริงๆ และ Pass Reference by Value (หรือ Pass by Pointer)

ลองเช็คการทำงานของ Pass by Pointer ด้วยโค้ด

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

  • & หมายถึงให้ดึง address ของตัวแปร
  • * หมายถึงให้ dereference ตัวแปร pointer นั้น (ดึงค่าที่ชี้อยู่)
  • & หมายถึงให้ดึง address ที่ pointer ชี้อยู่ (&foo และ foo ได้ผลเช่นเดียวกับ เพราะตัวแปร pointer จะคืนค่า address ที่ชี้อยู่ทุกครั้ง)
class Foo{
public:
	int bar;
};

คลาส Foo เป็นคลาสที่มี Property เดียวคือ int bar

void PassByPointer(Foo * foo){
	foo->bar = 15;
	cout << "==PassByPointer==" << endl;
	cout << foo->bar << endl;
	cout << foo << endl;
	cout << &foo << endl;
}

ฟังก์ชัน PassByPointer รับพารามิเตอร์เป็น pointer ของ Foo หลังจากได้ไปแล้วจะทำการเปลี่ยนค่า bar เป็น 15 แล้วพิมพ์ bar ออกมาดู ต่อจะพิมพ์ address ของ object ของ Foo (ตำแหน่งของ object บน heap) แล้วก็พิมพ์ address ของ pointer (ตำแหน่งของตัวแปร pointer บน stack) ออกมา

void PassByReference(Foo & foo){
	foo.bar = 10;
	cout << "==PassByReference==" << endl;
	cout << foo.bar << endl;
	cout << &foo << endl;
}

ฟังก์ชัน PassByReference รับพารามิเตอร์เป็น address ของ Foo (เอาไปสร้าง alias) หลังจากได้ไปแล้วจะทำการเปลี่ยนค่า bar เป็น 10 แล้วพิมพ์ bar ต่อมาจะทำการพิมพ์ address ของ object ของ Foo (ตำแหน่งของ object บน heap) ออกมาก

*คำสั่งที่ใช้พิมพ์ address ของ object ในฟังก์ชันนี้หน้าตาเหมือน คำสั่งที่ใช้พิมพ์ address ของ pointer ในฟังก์ชันที่แล้ว เพราะโดยทั่วไป ตัวแปร pointer จะคืนค่า address ที่ชี้อยู่เสมอ แต่สำหรับ value-type แล้วจะต้องใช้คำสั่ง & ถึงจะได้ address ของ object เพราะตัวมันคือ object นั้นเองอยู่แล้ว (หากใช้ & กับ pointer จะได้ address ของตัวมันเอง ไม่ใช่ address ของ object)

*ในฟังก์ชันนี้ address ของตัวแปร และ address ของ object คือตัวเดียวกัน เพราะการ parse by reference จะทำให้ formal parameter ทำตัวเป็น value-type

int main(){
	Foo* foo = new Foo();

	cout << "==Initial==" << endl;
	cout << foo->bar << endl;
	cout << foo << endl;
	cout << &foo << endl;

	PassByReference(*foo);

	cout << "==After PassByReference==" << endl;
	cout << foo->bar << endl;
	cout << foo << endl;
	cout << &foo << endl;

	PassByPointer(foo);

	cout << "==After PassByPointer==" << endl;
	cout << foo->bar << endl;
	cout << foo << endl;
	cout << &foo << endl;
    
	return 0;
}

ส่วนแรกก่อน เข้า PassByReference จะเป็นการ initial แล้วให้พิมพ์ค่า bar และตำแหน่งของ object และ ตำแหน่งของ pointer ออกมา

ส่วนต่อมา หลังจากเข้า PassByReference ไปแล้ว ถึงก่อน PassByPointer เป็นการเช็คอีกครั้งว่าค่า ฝั่ง caller เปลี่ยนแปลงไปอย่างไร เมื่อมีการเปลี่ยนแปลงค่า bar ในฟังก์ชัน PassByReference

ส่วนต่อมา หลังจากเข้า PassByPointer เป็นการเช็คอีกครั้งว่าค่า ฝั่ง caller เปลี่ยนแปลงไปอย่างไร เมื่อมีการเปลี่ยนแปลงค่า bar ในฟังก์ชัน PassByPointer

==Initial==
0
00BBA6E0
0095F850
==PassByReference==
10
00BBA6E0
==After PassByReference==
10
00BBA6E0
0095F850
==PassByPointer==
15
00BBA6E0
0095F760
==After PassByPointer==
15
00BBA6E0
0095F850

ใน่ช่วงของ initial เราได้ค่า bar เป็น 0 (ไม่ได้ assign) และได้ address ของ object เป็น 00BBA6E0 และได้ address ของ pointer เป็น 0095F850

ต่อไปในฟังก์ชัน PassByReference เราได้ค่า bar เป็น 10 (มีการสั่งเปลี่ยนในังก์ชัน) และได้ address ของ object และ pointer เป็น 00BBA6E0 แสดงว่า ตัวแปรในฟังก์ชันเป็น value-type ของ object ที่เราส่งมาแน่ๆ (การเปลี่ยนแปลงใดๆ กับตัวแปรนี้ รวมถึงการ assign ค่าใหม่จะกระทบกับ caller แน่นอน)

หลังจากฟังก์น PassByReference จบ เราพบว่าค่า bar ด้านนอกเป็น 10 เช่นกัน ส่วน address ทั้งสองยังคงเท่าเดิม

ต่อไปในฟังก์ชัน PassByPointer เราได้ค่า bar เป็น 15 (มีการสั่งเปลี่ยนในังก์ชัน) address ของ object เป็น 00BBA6E0 เหมือนเดิม แต่ address ของ pointer เป็น 0095F760 ซึ่งไม่เหมือนเดิม เพราะการ pass แบบนี้จะเป็นการสร้าง pointer ใหม่ แต่ copy reference จาก pointer เดิมมาใส่ ดังนั้นถ้าเรา re-assign ค่าให้ pointer ใหม่นี้ pointer เดิมย่อมไม่ได้รับผลกระทบ แต่การกระทำใดๆ ที่มีผลต่อ obnject เดิมนั้น ก็ยังกระทบ object ที่ pointer ของฝั่ง caller ชี้อยู่เหมือนเดิม เพราะเป็น object เดียวกัน (ดูจาก address)

ท้ายที่สุดเรามาตรวจค่าในฝั่ง caller กันอีกครั้ง พบว่า bar ได้ 15 (ถูกเปลี่ยนใน PassByPointer) นอกนั้นยังเหมือนเดิม

สรุป

เรื่องราวของวันนี้แค่ต้องการทำให้เห็นภาพว่า Pass by Pointer คืออะไร จะได้คุยกับคนอื่นเขารู้เรื่องแค่นั้น (มีคนคุยเรื่องพวกนี้กันด้วยเหรอ?)

‪#‎day19 #365วันแห่งโปรแกรม ‪#‎โครงการ365วันแห่ง‬...