เกริ่น
สวัสดีครับ วันนี้เราจะมาพูดถึงเรื่องของ Array ในระดับ Memory กันครับ หลายคนอาจจะเคยได้ยินคำว่า Array มาบ้างแล้ว แต่ยังไม่แน่ใจว่ามันคืออะไร และทำงานยังไงในระดับ Memory
วันนี้เราจะมาเจาะลึกกันแบบเห็นภาพครับ แต่ก่อนจะไปถึง Array เราขอปูพื้นเรื่อง Pointer กันก่อนนิดนึง เพราะสองอย่างนี้มันผูกกันแน่นมาก
Pointer คืออะไร?
Pointer คือ ตัวแปรที่เก็บที่อยู่ของตัวแปรอื่นๆ ในหน่วยความจำ (Memory) ซึ่งในภาษา C นั้น Pointer เป็นสิ่งที่สำคัญมาก เพราะมันช่วยให้เราสามารถจัดการกับข้อมูลในหน่วยความจำได้อย่างมีประสิทธิภาพ ตัวอย่าง
int a = 10;int *p = &a; // p คือ pointer ที่เก็บที่อยู่ของ aprintf("Value of a: %d\n", *p); // ใช้ *p เพื่อเข้าถึงค่าของตัวแปรที่ pointer ชี้ไปoutput จะได้
Value of a: 10แต่ถ้าเราลองเปลี่ยนโค้ดนิดหน่อย
int a = 10;int *p = &a; // p คือ pointer ที่เก็บที่อยู่ของ aprintf("Value of a: %d\n", *p); // ใช้ *p เพื่อเข้าถึงค่าของตัวแปรที่ pointer ชี้ไปprintf("Pointer of a: %p\n", (void*)p);output จะได้
Pointer of a: 0x7ffee3bff5acจะเห็นได้ว่า pointer เก็บ “ที่อยู่ของตัวแปร” ไม่ใช่ค่าของ a โดยตรง
Pointer ใช้ทำอะไรได้บ้าง?
Pointer มีประโยชน์มากมายในการเขียนโปรแกรม โดยเฉพาะเวลาที่เราทำงานใกล้กับระดับ memory โดยหลัก ๆ จะมีประโยชน์เด่น ๆ 2 อย่างคือ
- ประหยัดหน่วยความจำเวลาส่งข้อมูล
- สามารถจัดการแก้ไขข้อมูลได้โดยตรงในหน่วยความจำ
1. ประหยัดหน่วยความจำ
ในกรณีนี้มักจะเกิดขึ้นกับ struct ก่อนใหญ่ๆ เช่น
#include <stdio.h>
typedef struct { char name[100]; char description[1000]; int price;} Product;
void printProductByValue(Product p) { printf("%s - %d\n", p.name, p.price);}
void printProductByPointer(Product *p) { printf("%s - %d\n", p->name, p->price);}
int main() { Product p = {"Laptop", "Very powerful laptop...", 50000}; printProductByValue(p); // copy ทั้งก้อน แล้วส่งไปยังฟังก์ชัน printProductByPointer(&p); // ส่ง address ของ p แทนการ copy ทั้งก้อน}ในตัวอย่างนี้ ถ้าเราใช้ printProduct(Product p) เราจะต้อง copy ข้อมูลทั้งหมดของ struct Product ไปยังฟังก์ชัน ซึ่งจะใช้หน่วยความจำมาก แต่ถ้าเราใช้ printProduct(Product *p) เราจะส่งแค่ address ของ struct Product ไปยังฟังก์ชัน ซึ่งจะประหยัดหน่วยความจำมากขึ้น
2. แก้ไขข้อมูลได้โดยตรง
Pointer ยังช่วยให้เราสามารถแก้ไขข้อมูลได้โดยตรงในหน่วยความจำ เช่น
#include <stdio.h>void increment(int *p) { // รับ pointer เป็น parameter (*p)++; // แก้ไขค่าที่ pointer ชี้ไปโดยตรง}int main() { int a = 5; increment(&a); // ส่ง address ของ a ไปยังฟังก์ชัน printf("Value of a: %d\n", a); // Output จะเป็น 6 เพราะเราแก้ไขค่าของ a โดยตรงผ่าน pointer}Array คืออะไรในระดับ Memory
กลับมาที่เรื่องของ Array กันต่อ Array คือโครงสร้างข้อมูลที่ใช้เก็บข้อมูลหลาย ๆ ตัวในหน่วยความจำที่ต่อเนื่องกัน โดยที่แต่ละตัวจะมีขนาดเท่ากัน และสามารถเข้าถึงได้ด้วย index
ในภาษา C นั้น Array จะถูกเก็บแบบเรียงติดกันในหน่วยความจำ และในกรณีของ string (char array) ตัวสุดท้ายจะมี null character \0 เพื่อบอกว่าข้อมูลจบแล้ว เช่น
char name[6] = "Hello"; // Array of char ที่เก็บคำว่า "Hello"เรามาลองเข้าไปดูในโลกของ Memory กันครับ ว่า Array ในระดับลึกมันเป็นยังไง
สมมติว่าเรามี “Hello” และมันถูกเก็บอยู่ที่ตำแหน่ง 0x1000 ในหน่วยความจำ
0x1000: 'H'0x1001: 'e'0x1002: 'l'0x1003: 'l'0x1004: 'o'0x1005: '\0' // null character ที่บอกว่าข้อมูลจบแล้วWARNINGข้อมูลใน Array จะถูกเก็บต่อเนื่องกันในหน่วยความจำเสมอ
แต่โครงสร้างข้อมูลอย่าง Linked List จะไม่ได้เก็บข้อมูลต่อเนื่องกันสำหรับ Array แบบ dynamic (เช่น Array List หรือ vector) ข้อมูลภายในยังคงเรียงต่อกัน
แต่ตำแหน่งในหน่วยความจำอาจถูกย้ายเมื่อมีการขยายขนาด
Array ใน Function: ทำไมมันกลายเป็น Pointer
มาถึงแก่นแท้ที่ผมจะสื่อ Array เวลาเอาไปใช้กับ function มัน “ไม่ได้ถูก copy ทั้งก้อน” แต่มันจะถูกแปลง (decay) เป็น pointer ไปยัง element ตัวแรกโดยอัตโนมัติ
void print(int arr[]) { printf("%d\n", arr[0]);}กับ
void print(int *arr) { printf("%d\n", arr[0]);}ทั้งสองฟังก์ชันนี้จะทำงานเหมือนกัน เพราะว่า arr ในฟังก์ชันแรกจะถูกแปลงเป็น pointer ไปยัง element ตัวแรกของ array อยู่ดี แล้วตอนเรียกล่ะ?
int arr[3] = {1, 2, 3};print(arr);output จะได้
0x7ffee3bff5a0ซึ่งก็ไม่ได้ต่างจาก
print(&arr[0]);สรุป
ถ้าคุณเข้าใจแนวคิดพวกนี้แล้ว ไม่ว่าจะเป็น array, pointer และการทำงานของ memory คุณจะสามารถนำพื้นฐานนี้ไปต่อยอดในภาษาอื่น ๆ ได้อีกมาก เช่น Go หรือ Rust
ถึงแม้แต่ละภาษาจะมีวิธีจัดการ pointer หรือ memory ที่แตกต่างกันออกไป แต่แนวคิดพื้นฐานอย่าง “การอ้างอิงตำแหน่งในหน่วยความจำ” และ “การเข้าถึงข้อมูลผ่าน address” ยังคงเหมือนเดิม
พูดง่าย ๆ คือ syntax อาจเปลี่ยน แต่ mindset เดิมยังใช้ได้เสมอ
NOTERust → ไม่มี pointer แบบ C ตรง ๆ แต่ใช้ reference + ownership ถ้าคุณเคยทำ malloc แล้ว free ram ไม่หมดคุณจะเข้าใจแนวคิดนี้ได้ดีขึ้น