บทนำ: ฟันเฟืองลับที่ขับเคลื่อนสถาปัตยกรรมคอมพิวเตอร์

สวัสดีครับน้องๆ วิศวกรและเพื่อนนักพัฒนาชาว www.123microcontroller.com ทุกคน! วันนี้วิศวกรขอบตาดำๆ จะพาทุกคนไปทำความรู้จักกับโครงสร้างข้อมูล (Data Structures) ที่ใกล้ชิดกับสถาปัตยกรรมของฮาร์ดแวร์และการทำงานของภาษา C มากที่สุด นั่นก็คือ Stack (สแต็ก) ครับ

เวลาที่เราเริ่มศึกษาการเขียนโปรแกรม โครงสร้างข้อมูลประเภท Linear มักเป็นสิ่งแรกที่เราเจอ และ Stack ก็คือหนึ่งในนั้นครับ ในระดับ System Programming ไม่ว่าจะเป็นการเรียกใช้ฟังก์ชัน การทำ Recursion หรือการคำนวณสมการคณิตศาสตร์ ไมโครคอนโทรลเลอร์ล้วนต้องพึ่งพากลไกของ Stack ทั้งสิ้น วันนี้เราจะมาเจาะลึกกันว่า แหล่งข้อมูลระดับเซียนอธิบายถึง Stack ไว้ว่าอย่างไรในโลกของ Data Structures ครับ!

สแต็ก (Stack) คืออะไรในเชิงสถาปัตยกรรม?

ในบริบทของโครงสร้างข้อมูล Stack จัดเป็นโครงสร้างข้อมูลแบบเชิงเส้น (Linear data structure) ชนิดหนึ่ง จุดเด่นที่สำคัญที่สุดของมันคือการทำงานตามหลักการ LIFO (Last-In, First-Out) ซึ่งหมายความว่า ข้อมูลที่ถูกนำเข้าไปเป็นชิ้นสุดท้าย จะเป็นข้อมูลชิ้นแรกที่ถูกดึงออกมาใช้งาน บางครั้งก็อาจเรียกอีกชื่อหนึ่งว่า FILO (First-In, Last-Out) ก็ได้เช่นกัน

เพื่อให้เห็นภาพง่ายๆ ให้เรานึกถึง “กองจานข้าวในร้านอาหาร” หรือ “กระป๋องใส่ลูกเทนนิส” ครับ เวลาที่เราเก็บจาน เราต้องวางจานใบใหม่ซ้อนทับขึ้นไปด้านบนเสมอ (Push) และเวลาที่เราจะหยิบจานไปใช้ เราก็ต้องหยิบใบที่อยู่บนสุดออกมาก่อน (Pop) เราไม่สามารถดึงจานใบตรงกลางหรือใบข้างล่างสุดออกมาได้โดยไม่ยกใบข้างบนออกก่อน

3 ปฏิบัติการหลัก (Operations) ของ Stack:

  1. PUSH: การแทรกหรือเพิ่มข้อมูลใหม่เข้าไปที่ตำแหน่ง “บนสุด (Top)” ของ Stack
  2. POP: การดึงและลบข้อมูลที่อยู่ตำแหน่ง “บนสุด (Top)” ออกจาก Stack
  3. PEEP (หรือ PEEK): การอ่านหรือตรวจสอบค่าข้อมูลที่อยู่บนสุด โดยไม่ได้ลบข้อมูลนั้นออกจาก Stack

การนำ Stack ไปสร้างบนหน่วยความจำ (Implementation):

ในภาษา C เราสามารถสร้าง Stack ได้ 2 รูปแบบหลักๆ ครับ:

  • การใช้ Array (Static): เป็นวิธีที่ง่ายและแพร่หลายที่สุดในงาน Embedded โดยเราจะสร้าง Array ขึ้นมาเป็นตัวถังเก็บข้อมูล และใช้ตัวแปร Integer หนึ่งตัว (มักตั้งชื่อว่า top) เป็นตัวชี้ (Index) ไปยังตำแหน่งบนสุด ข้อเสียคือขนาดจะถูกกำหนดตายตัว หากใส่ข้อมูลเกินขีดจำกัดก็จะพังได้
  • การใช้ Linked List (Dynamic): เป็นการนำ List มาประยุกต์ โดยจำกัดเงื่อนไขให้เราเติมโหนด (Node) ใหม่และลบโหนดออกได้เฉพาะที่ “หัว (Head/Top)” ของ List เท่านั้น ข้อดีคือขนาดจะยืดหยุ่น หดหรือขยายได้ตามที่ใช้จริงโดยไม่เปลือง RAM ล่วงหน้า

บทบาทที่ยิ่งใหญ่ของ Stack ในระบบคอมพิวเตอร์:

Stack ไม่ได้เป็นแค่ทฤษฎีในห้องเรียน แต่เป็นเบื้องหลังกลไกสำคัญของระบบ เช่น:

  • System Call Stack (Stack Frames): นี่คือจุดที่ฮาร์ดแวร์ทำงานร่วมกับ C Compiler แบบแนบแน่น! ทุกครั้งที่มีการเรียกใช้ฟังก์ชัน (Function Call) ระบบจะสร้างก้อนข้อมูลที่เรียกว่า “Stack Frame” (หรือ Activation Record) แล้ว Push มันลงบน Call Stack ของโปรแกรม Stack Frame นี้จะใช้เก็บ “ตัวแปร Local”, “พารามิเตอร์”, และที่สำคัญที่สุดคือ “Return Address” เพื่อให้ CPU รู้ว่าเมื่อจบฟังก์ชันแล้ว ต้องกระโดดกลับไปทำงานบรรทัดไหน
  • การทำ Recursion: การเขียนฟังก์ชันเรียกตัวเองซ้ำๆ (Recursive function) ทำงานได้ก็เพราะการ Push Stack Frame ใหม่ซ้อนทับขึ้นไปเรื่อยๆ นี่แหละครับ
  • การแปลงและประมวลผลสมการ (Expression Evaluation): คอมไพเลอร์ใช้ Stack ในการแปลงสมการคณิตศาสตร์ที่มนุษย์อ่าน (Infix เช่น A+B) ให้กลายเป็นรูปแบบที่เครื่องเข้าใจง่ายอย่าง Postfix หรือ Prefix

Stack Data Structure Diagram

ตัวอย่างการสร้าง Array-based Stack สำหรับ Embedded

มาดูตัวอย่างการสร้าง Stack เบื้องต้นด้วย Array (Array-based Stack) ซึ่งเป็นท่ามาตรฐานที่แข็งแกร่งและคาดเดา Memory ได้ดีที่สุดสำหรับไมโครคอนโทรลเลอร์กันครับ:

#include <stdio.h>
#include <stdbool.h>

#define MAX_SIZE 5 // กำหนดขนาดสูงสุดของ Stack เพื่อสกัดกั้น Dynamic Allocation

/* 
 * โครงสร้าง Stack เก็บในตัวแปร Static/Global 
 * เหมาะกับงาน Embedded ที่มี Stack กลางเดียวในโมดูล
 */
static int stack_data[MAX_SIZE];
static int top = -1; // เริ่มต้นที่ -1 แสดงว่า Stack ว่างเปล่า (ยิง Address 0 ยังไม่ได้)

/* ฟังก์ชันตรวจสอบว่า Stack ว่างหรือไม่ */
bool is_empty(void) {
    return (top == -1);
}

/* ฟังก์ชันตรวจสอบว่า Stack เต็มหรือไม่ */
bool is_full(void) {
    return (top == MAX_SIZE - 1);
}

/* 1. PUSH Operation: ล็อกเป้าและยิงข้อมูลลง Stack บนสุด */
bool push(int new_value) {
    if (is_full()) {
        printf("ERROR: Stack Overflow! Cannot push %d\n", new_value);
        return false; // ป้องกัน Buffer Overflow ไปทับ Memory โซนอื่น
    }
    
    top = top + 1;               // ขยับตัวชี้ระดับของเหลว (Top Index) ขึ้น
    stack_data[top] = new_value; // วางกล่องค่าลงไป
    return true;                 // ยืนยันว่าเก็บสำเร็จ
}

/* 2. POP Operation: ฉกข้อมูลบนสุดออกจาก Stack */
int pop(void) {
    if (is_empty()) {
        printf("ERROR: Stack Underflow!\n");
        return -1; // ส่งค่า Error Code กลับไป
    }
    
    int popped_value = stack_data[top]; // แอบดูค่าบนสุดเพื่อดึงออกมา
    top = top - 1;                      // ลดระดับ Index ลง (ข้อมูลเก่ายังค้างใน RAM แต่ถูกลบทางตรรกะแล้ว)
    return popped_value;
}

int main(void) {
    push(10);
    push(20);
    push(30);
    
    printf("Popped: %d\n", pop()); // จะเด้ง 30 ออกมาก่อน (LIFO)
    printf("Popped: %d\n", pop()); // จะเด้ง 20 ลำดับถัดมา
    
    return 0;
}

สกัดกั้นภัยคุกคามจากการควบคุม Stack ผิดพลาด

ในการจัดการกับ Stack เพื่อให้ระบบทำงานได้อย่างปลอดภัย (Safe and Reliable) ตำราสาย System Programming ต่างๆ ได้เตือนถึงหลุมพรางสำคัญไว้ดังนี้ครับ:

  1. ระวังปีศาจ Stack Overflow: หากคุณใช้ Array สร้าง Stack และไม่ได้เขียนโค้ดตรวจสอบเงื่อนไข top == MAX_SIZE - 1 ก่อนทำ Push การเขียนข้อมูลลงไปจะทะลุขอบเขตของ Array ทำให้เกิด Buffer Overflow ไปทับข้อมูลอื่นทันที ในทางกลับกัน หากเป็นการเรียกใช้ฟังก์ชันซ้อนกันลึกเกินไป (Deep Recursion) จน System Call Stack ใน RAM ล้น ก็จะทำให้เกิด “Fatal Error” ระดับฮาร์ดแวร์ รีเซ็ตบอร์ดทันที หรือที่เราคุ้นในชื่อ “Stack Overflow” นั่นเองครับ
  2. ป้องกัน Stack Underflow: ก่อนทำปฏิบัติการ Pop หรือ Peek ต้องเช็คเสมอว่า Stack มีข้อมูลอยู่หรือไม่ (เช็ค top == -1) การพยายามดึงข้อมูลจาก Stack ที่ว่างเปล่า (Empty stack) จะเรียกว่า Stack Underflow ซึ่งจะทำให้เราดึง “ค่าขยะ (Garbage Address)” บน RAM ไปคำนวณ หรือเกิด Error ลึกลับตามมาได้
  3. การใช้ Dynamic Stack (Linked List): หากคุณเลือกอิมพลีเมนต์ Stack ด้วย Linked List ร่วมกับฟังก์ชัน malloc() ในภาษา C กฎเหล็กคือในฟังก์ชัน pop() นอกจากคุณจะต้องเลื่อน Pointer top ถอยลงมาแล้ว คุณต้องเรียก free(temp_node) เพื่อคืนพื้นที่หน่วยความจำของโหนดที่ถูกลบให้ระบบเสมอ หากทิ้งไว้เฉยๆ ระบบของคุณจะเผชิญกับปัญหา Memory Leak ทันทีครับ

สรุป (Conclusion)

Stacks เป็น Data Structure ที่มีความเรียบง่ายแต่กลับเปี่ยมไปด้วยมนต์ขลังครับ หลักการทำงานแบบ LIFO (Last-In, First-Out เข้าหลัง-ออกก่อน) ของมันเป็นฟันเฟืองลึกลับที่ทำให้ภาษาระดับระบบอย่าง C สามารถควบคุม Flow แตกบรานช์ของโปรแกรม สลับการทำงานไปมาระหว่างฟังก์ชัน (Call Frames) หรือจำลองการประมวลผลอัลกอริทึมคณิตศาสตร์ที่ซับซ้อนได้อย่างเป็นชั้นเป็นรูปแบบ การประยุกต์ใช้งานและสร้าง Stack ให้ปลอดภัยไร้บั๊กจึงเป็นสิ่งสกิลพื้นฐานที่โปรแกรมเมอร์สายฮาร์ดแวร์ต้องตีให้แตกครับ

ถ้าเพื่อนๆ ชอบบทความที่เจาะลึกโครงสร้างข้อมูลและหลักการทำงานของคอมพิวเตอร์ระดับ Machine Code แบบนี้ อย่าลืมแวะเข้ามาติดตาม อ่านบทความสอน C สำหรับสายสมองกลฝังตัว หรือมาร่วมตั้งกระทู้แลกเปลี่ยนความรู้เจาะลึกบั๊กโหดๆ กันต่อที่เว็บ www.123microcontroller.com ของเรานะครับ แล้วพบกันใหม่ในบทความประลองโครงสร้างข้อมูลหน้า Happy Coding ครับทุกคน!