ขนาดของตัวชี้ (Size of Pointers): พื้นที่หน่วยความจำของนักชี้เป้า ศาสตร์ที่สายฮาร์ดแวร์ต้องรู้ลึก!

สวัสดีครับน้องๆ วิศงกรและเพื่อนนักพัฒนาชาว www.123microcontroller.com ทุกคน! กลับมาพบกับวิศวกรขอบตาดำๆ กันอีกครั้งครับ

เมื่อเราเริ่มเขียนโปรแกรมภาษา C เรามักจะท่องจำกันได้ว่า ตัวแปร char กินพื้นที่หน่วยความจำ 1 ไบต์ ส่วน int อาจจะกิน 2 หรือ 4 ไบต์ ขึ้นอยู่กับสถาปัตยกรรมชิป แต่คำถามคือ แล้วถ้าเป็น “ตัวชี้ (Pointers)” ล่ะครับ มันกินพื้นที่ใน RAM เท่าไหร่?

น้องๆ หลายคนอาจจะคิดอย่างมีเหตุผลว่า “ถ้า Pointer ชี้ไปที่ char มันก็ต้องมีขนาด 1 ไบต์สิ” …นี่คือความเข้าใจผิดที่คลาสสิกมากครับ!

วันนี้เราจะมาเจาะลึกถึง “ขนาดของตัวชี้ (Size of Pointers)” ในบริบทของพื้นฐานและส่วนประกอบกันครับ ว่าตัวแปร Pointer มันกินพื้นที่เท่าไหร่กันแน่ ทำไมตอนรันบน PC กับบอร์ดไมโครคอนโทรลเลอร์ขนาดมันถึงไม่เท่ากัน และเราจะจัดการกับมันอย่างไรให้โค้ดของเราพอร์ต (Port) ไปรันได้ทุกแพลตฟอร์มโดยไม่พัง ไปลุยกันเลยครับ!

ไขปริศนาขนาดของ Pointer

ในระดับโครงสร้างแล้ว ตัวแปร Pointer ก็คือตัวแปรชนิดหนึ่งที่ใช้เก็บ “ตัวเลขที่อยู่ (Memory Address)” ของหน่วยความจำครับ แหล่งข้อมูลระดับเซียนได้อธิบายกลไกเรื่องขนาดของมันไว้ดังนี้:

1. ขนาดขึ้นอยู่กับสถาปัตยกรรม (Architecture-Dependent)

ขนาดของ Pointer ไม่ได้ขึ้นอยู่กับว่ามันชี้ไปที่ข้อมูลชนิดอะไร แต่ขึ้นอยู่กับ “สถาปัตยกรรมของระบบและคอมไพเลอร์ (Memory Models)” ล้วนๆ เลยครับ ยกตัวอย่างเช่น:

  • บนระบบปฏิบัติการแบบ 32-bit (เช่น ไมโครคอนโทรลเลอร์ ARM Cortex-M ทั่วไป) ขนาดของ Pointer ทุกชนิดจะเท่ากับ 4 ไบต์ เสมอ (32 บิต) เพื่อให้มันมีบิตเพียงพอที่จะครอบคลุมพื้นที่หน่วยความจำ 4GB ได้ทั้งหมด
  • บนระบบ 64-bit (เช่น PC สมัยใหม่) ขนาดของ Pointer มักจะกระโดดขึ้นไปเป็น 8 ไบต์ (64 บิต) เพื่อรองรับหน่วยความจำที่มหาศาลขึ้น

2. ชนิดข้อมูล (Data Type) ไม่มีผลกับขนาด

ไม่ว่าคุณจะประกาศ char *, int *, float * หรือแม้แต่พอยน์เตอร์ที่ชี้ไปยัง Structure ขนาดยักษ์ ตัวแปรพอยน์เตอร์เหล่านี้ก็จะมีขนาด 4 ไบต์ (บนระบบ 32-bit) เท่ากันทั้งหมด! เหตุผลก็เพราะว่า “บ้านเลขที่ (Address)” ย่อมมีรูปแบบและขนาดเท่ากันเสมอ ไม่ว่าบ้านหลังนั้นจะหลังเล็ก (char) หรือเป็นคฤหาสน์หลังใหญ่ (struct) ก็ตามครับ

3. ข้อยกเว้นของสาย Embedded (Harvard Architecture)

จุดนี้นักพัฒนาสายฮาร์ดแวร์ต้องระวังให้มากครับ! สำหรับไมโครคอนโทรลเลอร์ที่ใช้สถาปัตยกรรมแบบ Harvard (เช่น ชิปตระกูล 8051 หรือ AVR) ซึ่งมีการแยกหน่วยความจำโปรแกรม (Code/ROM) และหน่วยความจำข้อมูล (Data/RAM) ออกจากกันทางกายภาพ ส่งผลให้ “ขนาดของ Pointer อาจไม่เท่ากัน” ภายในชิปตัวเดียว! พอยน์เตอร์ที่ชี้ไปยังฟังก์ชัน (Function Pointer ใน ROM) อาจจะมีขนาดไม่เท่ากับพอยน์เตอร์ที่ชี้ไปยังตัวแปรข้อมูล (Data Pointer ใน RAM) โดยในชิป 8-bit บางตัว ขนาดของพอยน์เตอร์อาจมีตั้งแต่ 1 ไบต์ไปจนถึง 3 หรือ 4 ไบต์เลยทีเดียว

4. ชนิดข้อมูลมาตรฐานสำหรับจัดเก็บ Pointer

ในเมื่อขนาดของ Pointer เอาแน่เอานอนไม่ได้เวลาเปลี่ยนบอร์ด มาตรฐาน C99 จึงเตรียมชนิดข้อมูลพิเศษไว้ให้เราใช้ใน <stdint.h> ได้แก่ intptr_t และ uintptr_t ซึ่งคอมไพเลอร์การันตีว่ามันจะมีขนาด “ใหญ่พอ” ที่จะเก็บค่า Address ของ Pointer ในระบบนั้นๆ ได้อย่างปลอดภัยโดยที่ข้อมูลที่อยู่ไม่สูญหายแน่นอน

แผนภาพแสดงขนาดของ Pointer ที่เท่ากันเสมอไม่ว่าจะชี้ไปที่ Data Type ใด

ตัวอย่างโค้ด (Code Example)

มาดูโค้ดพิสูจน์ขนาดของ Pointer สไตล์ Clean Code กันครับ โค้ดนี้จะใช้โอเปอเรเตอร์ sizeof เพื่อตรวจสอบขนาด ซึ่งจะเห็นได้ชัดเจนว่าชนิดข้อมูลปลายทางไม่มีผลกับขนาดของ Pointer ใน RAM เลย

#include <stdio.h>
#include <stdint.h> /* สำหรับ intptr_t และ uintptr_t */

/* สร้าง Structure จำลองขนาดใหญ่ */
typedef struct {
    int id;
    double values[10]; /* ใช้อาร์เรย์เพื่อทำให้โครงสร้างมีขนาดใหญ่ */
    char name[20];
} SensorData;

int main(void) {
    /* ประกาศตัวแปร Pointer ชนิดต่างๆ */
    char *char_ptr = NULL;
    int *int_ptr = NULL;
    SensorData *struct_ptr = NULL;

    /*
     * 1. ใช้ sizeof เพื่อดูขนาดของ Pointer ตัวมันเอง
     * 🛡️ สังเกตการใช้ %zu ซึ่งเป็น Format Specifier ที่ถูกต้องสำหรับค่าประเภท size_t
     */
    printf("--- Size of Pointers ---\n");
    printf("Size of char pointer   : %zu bytes\n", sizeof(char_ptr));
    printf("Size of int pointer    : %zu bytes\n", sizeof(int_ptr));
    printf("Size of struct pointer : %zu bytes\n", sizeof(struct_ptr));
    printf("Size of void pointer   : %zu bytes\n", sizeof(void*));

    /*
     * 2. การเก็บค่า Address ไว้ในตัวแปรจำนวนเต็มอย่างปลอดภัย
     * ห้ามใช้ int ธรรมดา ให้ใช้ intptr_t หรือ uintptr_t เสมอ
     */
    int sensor_val = 100;
    int *pi = &sensor_val;

    uintptr_t safe_address = (uintptr_t)pi;
    printf("\n--- Storing Pointer safely ---\n");
    printf("Address stored in uintptr_t: 0x%zX\n", safe_address);

    return 0;
}

(หมายเหตุ: หากคุณรันโค้ดนี้บน PC 64-bit โค้ดจะพิมพ์เลข 8 ออกมาทั้งหมด แต่ถ้า Cross-compile ไปรันบนบอร์ด ARM Cortex-M 32-bit โค้ดจะพิมพ์เลข 4 ออกมาครับ)

ข้อควรระวัง / Best Practices

การจัดการกับขนาดของ Pointer มีหลุมพรางที่หนังสือ Secure Coding และผู้เชี่ยวชาญเตือนไว้บ่อยมากๆ ดังนี้ครับ:

  • ห้ามแคสต์ Pointer ไปเป็น int ธรรมดาเด็ดขาด: นี่คือบั๊กยอดฮิตเวลาวิศวกรย้ายโค้ดเก่าจากระบบ 32-bit ไปรันบน 64-bit ครับ! สมมติคุณใช้คำสั่ง int tmp = (int)pi; บนระบบ 64-bit ขนาดของ int อาจจะแคบเพียง 4 ไบต์ แต่ Pointer มีขนาดถึง 8 ไบต์ ผลคือ “ข้อมูล Address ของคุณจะถูกหั่นทิ้ง (Truncated) หายไปครึ่งหนึ่งทันที!” เมื่อคุณพยายามแคสต์กลับมาเป็น Pointer ระบบจะพัง (Crash) ทันที วิธีแก้: หากจำเป็นต้องเก็บค่า Address ให้ใช้ intptr_t หรือ uintptr_t เสมอครับ
  • ใช้ sizeof(pointer) ให้ถูกที่: เวลาเราจองหน่วยความจำแบบไดนามิกด้วย malloc เราต้องส่งขนาดของ “ข้อมูล” ที่มันชี้ไป ไม่ใช่ขนาดของตัว Pointer เอง เช่น ต้องเขียน malloc(10 * sizeof(int)) ไม่ใช่ malloc(10 * sizeof(int*)) (ยกเว้นว่าคุณกำลังตั้งใจสร้าง Array ของ Pointer จริงๆ)
  • ระวัง Array Decay: จุดที่โปรแกรมเมอร์มักพลาดคือการหาจำนวนสมาชิกใน Array เมื่อส่ง Array เข้าฟังก์ชันแล้ว (มันจะสลายร่างกลายเป็น Pointer ทันทีที่เข้าฟังก์ชัน) การเรียก sizeof(array) ภายในฟังก์ชัน จะได้ผลลัพธ์เป็นขนาดของ Pointer (4 หรือ 8 ไบต์) ไม่ใช่ขนาดของ Array ทั้งก้อนเหมือนตอนประกาศแรกสุดครับ
  • ความแตกต่างของ Function Pointers: บนบอร์ดสถาปัตยกรรมแบบ Harvard (เช่น ตระกูล 8051 หรือ AVR) โปรแกรมเมอร์อย่าทึกทักเอาเองว่าการแคสต์ Function Pointer ไปเป็น Data Pointer แล้วจะทำงานได้ปกติ เพราะขนาดมันอาจจะไม่เท่ากัน และการดึงข้อมูลจาก ROM กับ RAM ใช้คำสั่งระดับ Assembly คนละชุดกันเลยครับ

สรุปทิ้งท้าย

สรุปสั้นๆ คือ ขนาดของตัวชี้ (Size of Pointers) ไม่ได้ขึ้นอยู่กับชนิดของตัวแปรที่มันชี้ไป แต่ถูกกำหนดโดย สถาปัตยกรรมของฮาร์ดแวร์และหน่วยความจำ (Memory Models) เป็นหลักครับ การเข้าใจว่าบน 32-bit คือ 4 ไบต์ และบน 64-bit คือ 8 ไบต์ จะช่วยให้น้องๆ เข้าใจเวลาคอมไพเลอร์แจ้งเตือนเรื่อง Data Truncation และช่วยให้ออกแบบโครงสร้างข้อมูล (Data Structures) ได้อย่างมีประสิทธิภาพมากขึ้น ปราศจากบั๊กจอฟ้าครับ

น้องๆ คนไหนกำลังมีปัญหากับการส่ง Pointer ข้ามฟังก์ชัน หรือเพิ่งเจอบั๊กจอฟ้าตอนพอร์ตโค้ดจาก 32-bit ไป 64-bit อย่าลืมแวะเข้ามาตั้งกระทู้พูดคุยและแชร์โค้ดกันต่อได้ที่บอร์ด www.123microcontroller.com ของพวกเราได้เลยนะครับ! แล้วพบกันใหม่ในบทความหน้า Happy Coding ครับทุกคน!