C อาร์เรย์



อาร์เรย์คือ ตัวแปรหลายตัวชนิดเดียวกัน วางเรียงติดกันในหน่วยความจำ เข้าถึงด้วย index

ประกาศและใช้งาน

#include <stdio.h>

int main(void)
{
    int scores[5];              // อาร์เรย์ int 5 ตัว — ค่าเริ่มต้นเป็นขยะ

    scores[0] = 80;
    scores[1] = 75;
    scores[2] = 90;
    scores[3] = 65;
    scores[4] = 88;

    printf("%d\n", scores[0]);          // 80
    printf("%d\n", scores[4]);          // 88

    int sum = 0;
    for (int i = 0; i < 5; i++) {
        sum += scores[i];
    }
    printf("รวม %d เฉลี่ย %.1f\n", sum, sum / 5.0);
    return 0;
}
  • ขนาดในวงเล็บ [5] = จำนวนสมาชิก ต้องเป็นค่าคงที่ตอนคอมไพล์ (หรือ VLA — ดูท้ายบท)
  • index จาก 0 ถึง ขนาด - 1
  • ไม่มีการตรวจขอบเขตscores[5], scores[-1], scores[999] คอมไพล์ผ่านและรัน = undefined behavior

initialize ตอนประกาศ

int a[5] = {10, 20, 30, 40, 50};
int b[5] = {1, 2};              // ที่เหลือเป็น 0 → {1, 2, 0, 0, 0}
int c[5] = {0};                // ทุกตัวเป็น 0
int d[]  = {1, 2, 3, 4};       // ให้คอมไพเลอร์นับขนาดเอง → d มี 4 ตัว
int e[100] = {0};             // 100 ตัว เป็น 0 หมด

// designated initializer (C99)
int f[10] = {[0] = 1, [9] = 99};   // f[0]=1, f[9]=99, ที่เหลือ 0

int arr[100]; (ไม่ init) มีค่าขยะ ทั้ง 100 ตัว — ถ้าต้องการ 0 เขียน = {0}

หาความยาวด้วย sizeof

int nums[] = {5, 10, 15, 20, 25, 30};
size_t len = sizeof(nums) / sizeof(nums[0]);   // 24 / 4 = 6

for (size_t i = 0; i < len; i++) {
    printf("%d ", nums[i]);
}
printf("\n");

sizeof(nums) = ขนาดทั้งอาร์เรย์เป็นไบต์, sizeof(nums[0]) = ขนาดหนึ่งสมาชิก — หารกันได้จำนวนสมาชิก

เทคนิคนี้ใช้ได้เฉพาะกับตัวแปรอาร์เรย์จริง ในขอบเขตที่ประกาศ — ไม่ได้กับ pointer หรืออาร์เรย์ที่ส่งเข้าฟังก์ชันแล้ว (ดูหัวข้อถัดไป)

array decay — ส่งอาร์เรย์เข้าฟังก์ชัน

เมื่อส่งอาร์เรย์เป็น argument มันจะ “สลายตัว” (decay) เป็น pointer ไปสมาชิกตัวแรก ฟังก์ชันได้แค่ที่อยู่ ไม่รู้ขนาด:

#include <stdio.h>

void print_all(int arr[], int n)     // int arr[] จริง ๆ คือ int *arr
{
    printf("sizeof ในฟังก์ชัน: %zu\n", sizeof(arr));   // ขนาด pointer (มัก 8) ไม่ใช่ 20
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main(void)
{
    int data[5] = {1, 2, 3, 4, 5};
    printf("sizeof ใน main: %zu\n", sizeof(data));   // 5 * sizeof(int) (มัก 20)

    print_all(data, 5);              // ต้องส่งขนาดไปด้วยเสมอ
    return 0;
}

ผลที่ตามมา: ฟังก์ชันที่รับอาร์เรย์ต้องมีทางรู้ขนาด — วิธีที่พบบ่อยคือรับ “จำนวนสมาชิก” เป็นพารามิเตอร์แยก อีกวิธีคือใช้ตัวปิดท้าย (สตริงใช้ '\0', บางอาร์เรย์ใช้ sentinel เช่น NULL ตัวสุดท้าย) — C ไม่มีทางรู้ขนาดจาก pointer เปล่า ๆ

void f(int arr[]), void f(int arr[5]), void f(int *arr) — ทั้งสามเหมือนกันหมดสำหรับคอมไพเลอร์ ตัวเลขใน [] ถูกเมิน

ฟังก์ชันแก้เนื้ออาร์เรย์ได้

เพราะฟังก์ชันได้ pointer ไปอาร์เรย์จริง (ไม่ใช่สำเนา) มันแก้ค่าข้างในได้:

#include <stdio.h>

void double_all(int arr[], int n)
{
    for (int i = 0; i < n; i++) {
        arr[i] *= 2;                 // แก้อาร์เรย์ตัวจริงของผู้เรียก
    }
}

int main(void)
{
    int v[3] = {1, 2, 3};
    double_all(v, 3);
    printf("%d %d %d\n", v[0], v[1], v[2]);   // 2 4 6
    return 0;
}

ทุกอย่างในการเรียกฟังก์ชันยังเป็น pass by value — pointer ที่เกิดจากการ decay ก็ถูก *คัดลอก* เข้าพารามิเตอร์เหมือนกัน เพียงแต่มันชี้ไปที่อาร์เรย์เดิมของผู้เรียก ฟังก์ชันจึงแก้ *เนื้อ* อาร์เรย์ได้ (ต่างจากการส่งตัวแปร int ตรง ๆ ในบทที่ 8 ที่ได้แค่สำเนาของค่า)

อาร์เรย์ของ char = สตริง

char name[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
char name2[] = "Hello";        // เหมือนกัน — คอมไพเลอร์ใส่ '\0' ให้ (ขนาด 6)
printf("%s\n", name2);

บทหน้าว่าด้วยสตริงโดยเฉพาะ

VLA — variable length array (C99, ทางเลือก C11)

void process(int n)
{
    int buffer[n];               // ขนาดกำหนดตอนรัน — VLA
    // ...
}

VLA จองบน stack — ถ้า n ใหญ่มากจะ stack overflow และมาตรฐานบางส่วนไม่บังคับให้รองรับ หลายทีมห้ามใช้ VLA สำหรับข้อมูลขนาดไม่รู้แน่ใช้ malloc (บทที่ 14) ดีกว่า

เรื่องที่คนมักพลาด

  • เข้าถึงนอกขอบเขตarr[n] เมื่ออาร์เรย์มี n ตัว (index สุดท้ายคือ n-1), arr[-1], index จากตัวแปรที่ไม่ได้เช็ค — UB ทั้งหมด C ไม่เตือน
  • int arr[10]; แล้วอ่านก่อน assign — ค่าขยะ ใส่ = {0} ถ้าต้องการ 0
  • sizeof(arr) ในฟังก์ชันที่รับอาร์เรย์ — ได้ขนาด pointer (มัก 8) ไม่ใช่ขนาดอาร์เรย์ ต้องบอกขนาดด้วยวิธีอื่น (พารามิเตอร์แยก หรือตัวปิดท้าย/sentinel)
  • int n = 5; int arr[n]; แล้วคาดว่าเป็นอาร์เรย์ปกติ — เป็น VLA (C99) พฤติกรรมต่างกัน และ = {0} ใช้ไม่ได้
  • copy อาร์เรย์ด้วย b = a; — ไม่ได้ อาร์เรย์ assign ไม่ได้ ใช้ memcpy(b, a, sizeof a); หรือ loop
  • เปรียบเทียบอาร์เรย์ด้วย a == b — เทียบที่อยู่ ไม่ใช่เนื้อหา ต้องเทียบทีละสมาชิกตามชนิด memcmp เทียบ *ไบต์ดิบ* — ใช้ได้เฉพาะชนิดจำนวนเต็มที่ไม่มี padding (อาร์เรย์ของ double: 0.0 กับ -0.0 value เท่ากันแต่ memcmp บอกไม่เท่า)
  • คืนอาร์เรย์ local จากฟังก์ชันint* f() { int a[5]; return a; } — dangling

สรุป

  • อาร์เรย์ = บล็อกต่อเนื่อง index จาก 0, ไม่มี bounds check; ขนาดคงที่ (ประกาศปกติ กำหนดตอนคอมไพล์ / VLA กำหนดตอนรัน — ปรับขนาดหลังสร้างไม่ได้)
  • init: {v1, v2} (ที่เหลือ 0), {0} (ทุกตัว 0), [] ให้คอมไพเลอร์นับขนาด
  • ความยาว: sizeof(arr) / sizeof(arr[0]) — เฉพาะตัวแปรอาร์เรย์จริง
  • ส่งเข้าฟังก์ชัน → decay เป็น pointer (ที่ถูกส่งแบบ pass by value เช่นกัน) → ต้องบอกขนาด (พารามิเตอร์แยก หรือ sentinel), และฟังก์ชันแก้เนื้ออาร์เรย์ได้
  • อาร์เรย์ assign / เปรียบเทียบตรง ๆ ไม่ได้ — ใช้ memcpy / memcmp

บทถัดไปว่าด้วยสตริงใน C — ทำไมมันคืออาร์เรย์ของ char ที่ปิดด้วย '\0' และฟังก์ชันใน <string.h>