/ 365วันแห่งโปรแกรม

[365 วันแห่งโปรแกรม #day28] Object ถูกเก็บบน memory อย่างไร?

บทความนี้แทนของเมื่อวานที่ไม่ได้เขียนครับ

วันที่ยี่สิบแปดของ ‪#‎365วันแห่งโปรแกรม‬ วันนี้เราจะมีคุยกันเรื่อง Object ถูกเก็บบน memory อย่างไร?


เกริ่นนำ

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

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

Object ใน OOP

Object ใน OOP ก็คือตัวแปรที่มีความซับซ้อนขึ้นกว่าตัวแปร Primitive ธรรมดา เพราะใน Object นั้นประกอบไปด้วยตัวแปร Primitive หรือ Object อื่นๆ ได้หลายตัว เราเลยบอกว่ามัน Complex นะ

Object อยู่ตรงไหนบน Memory

ในเมื่อมัน Complex มันก็ย่อมถูกจัดเก็บอย่าง Complex เช่นกัน เราเรียนกันมาว่า Primitive อยู่ใน Stack เพราะว่ามันใช้ Memory คงที่และขยายไม่ได้ ทำให้ Concept ของ Stack นั้นใช้งานได้ ส่วน Object ก็เก็บอยู่ใน Heap (บางกรณีอยู่ใน Stack ได้) เพราะสามารถจองพื้นที่ได้อิสระ อ้าว แต่ใน Object ก็มี Primitive Type นะ แล้วมันอยู่ตรงไหน แล้ว Object ใน Object อีกล่ะ ยังไงแน่ เรามาเริ่มกันเลยดีกว่าครับ

ตอนนี้ขอให้ทุกคนเข้าใจตรงกันนะครับว่าเราแบ่งผืน Memory เป็นช่องๆ เหมือน Array นั่นแหละ นั่นแปลว่าพื้นที่ใน Heap ก็หน้าตาไม่ต่างจากใน Stack เลย

เมื่อมีการ call function แล้ว ใน Stack จะเกิดอะไรขึ้น

ทุกครั้งที่มีการเรียกฟังก์ชัน จะมีการ push ค่าของ parameter ที่ใช้เรียกฟังก์ชันนั้นลงไปเป็นอันดับแรก ตามมาด้วย return address และ local variable ของฟังก์ชันนั้นๆ

int foo(int x){
	int y = x * 10;
    return y;
}

int main(string [] args)
{
	int result = foo(10);
	return 0;
}

main() call foo()

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

เมื่อ main() เรียกฟังก์ชัน foo() โดยส่งพารามิเตอร์เป็น 10 ไป เลข 10 นี้ก็จะถูกเก็บลง Stack ก่อน ตามด้วย Return Address และตัวแปรที่อยู่ใน foo() เช่นกัน

สิ่งที่เราสนใจคือตอนนี้ในส่วน local variable ของ main() และ foo() มีอะไรบ้าง พอลองอ่านจากโค้ดก็พบว่าเป็นแบบนี้

variable in main() and foo()

สรุปคือทั้ง main() และ foo() ยังมี local varaible ใน Stack กันแค่คนละตัวอยู่

ถ้าในฟังก์ชันเรามี Primitive หลายตัว แล้วบน Memory จะหน้าตาเป็นอย่างไร?

จากตัวอย่างเมื่อกี้นี้ เราลองปรับโค้ดใหม่สร้างตัวแปร primitive เพิ่ม กลายเป็น

int foo(int x){
	int y = x * 10;
    int z = y * y;
    return z;
}

int main(string [] args)
{
	int result = foo(10);
    char c = 'a';
	return 0;
}

variable in main() and foo() #2

สรุปแล้วก็แค่มีตัวแปรมาทับใน Stack เพิ่มแค่นั้นแหละ ก็เข้าใจง่ายดีครับสำหรับเรื่อง primitive data type แค่ซ้อนกันไปเรื่อยๆ

จริงๆ ตัวแปร c และ result มีขนาดต่างกัน แต่ไม่ได้เขียนไว้ในรูป เพราะต้องการสื่อแค่คอนเซ็ปต์เฉยๆ

ถ้าในฟังก์ชันมีตัวแปร Object ด้วยล่ะ?

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

class Bar()
{
	int x = 20;
}

int foo(int x){
	int y = x * 10;
    int z = y * y;
    Bar * bar = new Bar();
    z = z + bar->x;
    delete bar;
    return z;
}

int main(string [] args)
{
	int result = foo(10);
    char c = 'a';
	return 0;
}

ในโค้ดบนนี้มีการสร้าง class Bar ขึ้นมา ใน Bar มี member เป็น int ชื่อ x อีกที เรามาดูกันดีกว่าว่าเมื่อใน foo() มีการเรียกใช้งาน Bar แล้ว Stack จะเป็นอย่างไร

variable in main() and foo() #3

นั่นน ไหนบอกว่าเก็บ Object ใน Heap ไง แล้ว bar มาจากไหน? ไม่ต้องตกใจไปครับ bar ตัวนี้เป็น pointer ไม่ใช่ object ดังนั้นมันเก็บแค่ address ของ Object ที่อยู่ใน Heap อีกที

Pointer ใช้หน่วยความจำด้วยเหรอ?

ใช้สิครับตัวแปรทุกชนิดต้องการที่อยู่เสมอ ส่วนเรื่องขนาดก็ขึ้นกับชนิดของตัวแปรเลย แล้วอย่างนี้ Pointer ใช้พื้นที่เท่าไหร่ล่ะ? ตรงนี้ต้องตอบว่าขึ้นอยู่กับแพลตฟอร์มครับ แต่ถ้าจะเอาคร่าวๆ ก็ถ้า OS เป็น 32 bit ก็ใช้ 32 bit ครับ ถ้า OS เป็น 64 bit ก็ใช้ 64 bit ครับ เพราะจำนวน bit ที่ใช้หมายถึงความสามารถในการระบุตำแหน่งบนหน่วยความจำ ยิ่งมีจำนวน bit มากก็สามารถใช้ในระบบที่ มีหน่วยความจำใหญ่มากขึ้นได้

เอา Heap ของตัวอย่างเมื่อกี้มาดูหน่อย

heap #1

สมมติว่ากล่องสีเทาคือ Heap เมื่อมีการสร้าง Object ก็จะมีการจองพื้นที่ใน Heap (ตรงไหนก็ได้ที่ว่างพอ) ที่มีขนาดกว้างพอที่จะใส่ member ทั้งหมดได้ ในพื้นที่นั้นจะมีการเก็บค่าต่างๆ เรียงติดต่อกันไปเรื่อยๆ (คล้าย stack)

ในภาพนี้เราอาจจะยังไม่เห็นอะไรเท่าไหร่ครับ เรามาลองเพิ่ม member ให้ Bar ดีกว่าครับ

class Bar()
{
	int x = 20;
    int y = 20;
}

heap #2

จะเห็นว่ามีตัวแปร y ของ Bar เพิ่มมาละ อยู่ต่อจากตัวแปร x เลย

ถ้า Object มี member เป็น Object แล้วใน Memory จะมีหน้าตายังไง?

ถ้าเราใส่ Primitive ใน Object มันก็เก็บรวมไว้ในผืน Memory ที่ Object นั้นจองไว้ แล้วถ้าเราใส่ Object ซ้อนกันล่ะ จะเกิดอะไรขึ้น งั้นเรามาลองกันดีกว่าครับ

class Bar()
{
	int x = 20;
    int y = 20;
    string name = "test"
}

ตอนนี้เราเพิ่มตัวแปร string ลงไปใน Bar ครับ โดยตั้งชื่อตัวแปรว่า name และให้มีค่าเท่ากับ "test" ตัวแปรชนิด string นั้นเป็นตัวแปรแบบ complex ครับเพราะว่าไม่ได้มีขนาดที่แน่นอน เช่น ในตอนแรกเราบอกว่ามันมีค่าเป็น "test" ซึ่งใช้พื้นที่ 4 bytes แต่ต่อมาเราบอกให้มันเป็น "tester" ซึ่งใช้พื้นที่เพิ่มขึ้น 3 bytes รวมเป็น 7 bytes ดังนั้นจึงเอาไปใส่ที่เดิมไม่ได้แน่ สิ่งที่ทำได้คือ เก็บ pointer ไว้ร่วมกับ member อื่น แล้วไปจองพื้นที่ใหม่ใน Heap ทุกครั้งที่มีการเปลี่ยนแปลงของค่า แล้วก็เอา pointer ชี้ไปที่ใหม่ครับ

heap #3

จะเห็นว่ามีการจองพื้นที่ใหม่ใน Heap เพื่อเก็บ name ของ bar ตรงนี้ถ้าเรามีการกำหนดค่า bar ใหม่ ก็ต้องจองที่ใหม่อีกที

แล้วถ้าเราเรียกฟังก์ชันของ Object จะเกิดอะไรขึ้น?

class Bar()
{
	int x = 20;
    int y = 20;
    string name = "test"
    
    int mix(int z){
    	int result = x + y + z;
        return result
    }
}

int foo(int x){
	int y = x * 10;
    int z = y * y;
    Bar * bar = new Bar();
    z = z + bar->x;
    z = bar->mix(x);
    delete bar;
    return z;
}

int main(string [] args)
{
	int result = foo(10);
    char c = 'a';
	return 0;
}

เรามาดู Memory ในช่วงทีทำคำสั่ง z = bar->mix(x); กันดีกว่าครับ

heap #4

จะเห็นว่าไม่มีการ copy member ของ bar ไปใส่ Stack ด้วย ดังนั้นถ้าเรามีการเปลี่ยนค่าให้ x หรือ y ของ bar ตรงนี้ จะเป็นการเปลี่ยนค่าให้ member ของมันจริงๆ

สรุปคือ Stack จะเก็บเฉพาะตัวแปรที่ที่ถูกสร้างโดยฟังก์ชันที่กำลังทำงานอยู่เท่านั้น

สรุป

ขนาดพื้นที่ที่ใช้สำหรับ Object นั่นก็คือขนาดของ member ของมัน + Pointers ซึ่ง member สามารถเป็นได้ทั้ง Primitive และ Object ถ้าเป็น Primitive ก็จะเก็บค่าอยู่ใน memory ส่วนที่ Object จองไว้เลย แต่ถ้าเป็น Object ก็จะเก็บไว้แค่ Pointer แล้วไปจองที่ใหม่สำหรับ Object นั้น

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

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