[0]zero-index clytie0x/zero-index

5.1 อาเรย์และช่องที่ 0

ในบทที่ 4 เราอ่านตัวเลขทีละตัวในลูปแล้วบวกสะสมได้ แต่ตัวเลขแต่ละตัวถูกทับทิ้งทันทีที่อ่านตัวถัดไป ถ้าโจทย์ถามว่า "มีกี่คนที่ได้คะแนนไม่ต่ำกว่าค่าเฉลี่ย" เราต้องรู้ค่าเฉลี่ยก่อน ซึ่งรู้ได้ก็ต่อเมื่ออ่านครบทุกคนแล้ว แล้วต้องย้อนกลับไปดูคะแนนของทุกคนอีกรอบ ตอนนั้นคะแนนหายไปหมดแล้ว หน้านี้รู้จัก อาเรย์ (array) ตัวแปรที่เก็บค่าได้หลายค่าในชื่อเดียว

ตัวแปรห้าตัวยังพอไหว สี่สิบตัวไม่ไหว

ถ้าห้องมีนักเรียน 5 คน ใช้ตัวแปร 5 ตัวก็ยังเขียนได้

#include <stdio.h>

int main()
{
    int s1, s2, s3, s4, s5;
    scanf("%d %d %d %d %d", &s1, &s2, &s3, &s4, &s5);
    int total = s1 + s2 + s3 + s4 + s5;
    printf("total = %d\n", total);
    return 0;
}
70 85 90 65 80
total = 390

แต่ห้องจริงมี 40 คน โจทย์แข่งขันให้มาเป็นแสนตัว เราเขียนตัวแปรแสนตัวไม่ได้ และเขียนลูปวนผ่าน s1 s2 s3 ก็ไม่ได้ เพราะชื่อตัวแปรเป็นแค่ตัวหนังสือในโค้ด โปรแกรมเปลี่ยนเลขท้ายชื่อตอนรันไม่ได้ อาเรย์แก้ปัญหานี้ด้วยการให้ชื่อเดียว แล้วแยกแต่ละค่าด้วย ตัวเลข ซึ่งเป็นค่าที่คำนวณและวนลูปได้

ประกาศอาเรย์

int score[5];

บรรทัดนี้ขอกล่อง int ห้ากล่อง วางเรียงติดกันในหน่วยความจำ ทั้งแถวใช้ชื่อ score ตัวเลขในวงเล็บเหลี่ยมตอนประกาศคือ จำนวนช่อง ทุกช่องเป็นชนิดเดียวกันหมด

เวลาใช้ ให้เขียนชื่อแล้วตามด้วยเลขช่องในวงเล็บเหลี่ยม เลขช่องเรียกว่า index (ดัชนี) และ ช่องแรกคือช่องที่ 0

score 70 85 90 65 80 0 1 2 3 4 index score[0] score[4] 5 cells: index 0 to 4
#include <stdio.h>

int main()
{
    int score[5];
    score[0] = 70;
    score[1] = 85;
    score[2] = 90;
    score[3] = 65;
    score[4] = 80;
    printf("first = %d\n", score[0]);
    printf("last = %d\n", score[4]);
    int i = 2;
    printf("score[%d] = %d\n", i, score[i]);
    printf("score[%d] = %d\n", i + 1, score[i + 1]);
    score[1] = score[1] + 5;
    printf("score[1] = %d\n", score[1]);
    return 0;
}
first = 70
last = 80
score[2] = 90
score[3] = 65
score[1] = 90
  • score[0] ถึง score[4] แต่ละตัวใช้ได้เหมือนตัวแปร int ธรรมดาทุกอย่าง ใส่ค่า อ่านค่า เอาไปคำนวณ
  • บรรทัดที่ 14 กับ 15 เลขช่องเป็นตัวแปรหรือนิพจน์ก็ได้ score[i] เมื่อ i เป็น 2 คือ score[2] ข้อนี้คือเหตุผลทั้งหมดที่อาเรย์มีประโยชน์ เพราะ i เปลี่ยนค่าในลูปได้
  • วงเล็บเหลี่ยมมีสองความหมาย ตอนประกาศ int score[5] คือมีห้าช่อง ตอนใช้ score[4] คือช่องที่ index เป็น 4

ทำไมเริ่มที่ 0

ให้คิดว่าเลขในวงเล็บเหลี่ยมไม่ได้แปลว่า "ช่องที่เท่าไร" แต่แปลว่า "เดินจากช่องแรกไปกี่ช่อง" ช่องแรกไม่ต้องเดินเลย จึงเป็น 0 ช่องถัดไปเดินหนึ่งช่อง จึงเป็น 1 เหมือนไม้บรรทัดที่เริ่มที่ 0 เลขบนไม้บรรทัดบอกว่าห่างจากจุดเริ่มกี่เซนติเมตร

ผลที่ตามมาคือกฎที่ต้องจำให้ขึ้นใจ

อาเรย์ N ช่อง

ช่องแรกคือ [0] ช่องสุดท้ายคือ [N - 1] ไม่มีช่อง [N]

ชื่อเว็บนี้ zero-index ก็มาจากเรื่องนี้ แปลว่า index เริ่มที่ศูนย์ บทแรกของเว็บจึงเป็นบทที่ 0 และแผนที่บทในหน้าแรกก็ติดเลขบทไว้แบบช่องอาเรย์ [0] [1] [2] ถ้าวันไหนลืมว่าอาเรย์เริ่มที่เลขอะไร ให้นึกถึงชื่อเว็บ

กำหนดค่าตอนประกาศ

ใส่ค่าทีละช่องแบบข้างบนยาวเกินไป ใส่ค่าทั้งหมดในปีกกาตอนประกาศได้เลย

#include <stdio.h>

int main()
{
    int score[5] = {70, 85, 90, 65, 80};
    int bonus[5] = {5, 3};
    int zeros[5] = {0};
    for (int i = 0; i < 5; i++) {
        printf("%d %d %d\n", score[i], bonus[i], zeros[i]);
    }
    return 0;
}
70 5 0
85 3 0
90 0 0
65 0 0
80 0 0
  • ค่าในปีกกาเติมลงช่อง 0 1 2 ไปตามลำดับ
  • ถ้าใส่ไม่ครบ ช่องที่เหลือเป็น 0 ทั้งหมด bonus ใส่สองค่า ช่อง 2 ถึง 4 จึงเป็น 0
  • = {0} จึงเป็นวิธีมาตรฐานที่ทำให้ทุกช่องเป็น 0 จะได้ใช้บ่อยมากในหน้า 5.3
  • ถ้าไม่ใส่ค่าเลย อย่าง int score[5]; ที่ประกาศใน main ทุกช่องเป็นค่าขยะ เหมือนตัวแปรธรรมดาที่ยังไม่ได้ใส่ค่าในหน้า 1.1

ลูป for (int i = 0; i < 5; i++) คือรูปแบบที่จะเห็นตลอดบทนี้ i เริ่มที่ 0 และหยุดก่อนถึง 5 จึงผ่านครบทุกช่องพอดี ตั้งแต่ 0 ถึง 4

ไม่บอกจำนวนช่องก็ได้

ถ้าใส่ค่าครบตั้งแต่ตอนประกาศ เว้นจำนวนช่องไว้ได้ compiler นับให้เอง ตัวอย่างนี้เก็บจำนวนวันในแต่ละเดือนของปีที่ไม่ใช่ปีอธิกสุรทิน รับเลขเดือน 1 ถึง 12 แล้วบอกว่าเดือนนั้นมีกี่วัน

#include <stdio.h>

int main()
{
    int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    int month;
    scanf("%d", &month);
    printf("%d\n", days[month - 1]);
    return 0;
}
2
28

คนนับเดือนเริ่มที่ 1 แต่อาเรย์เริ่มที่ 0 เดือน 2 จึงอยู่ช่อง 1 ต้องลบหนึ่งก่อนใช้เป็น index การแปลงระหว่าง "ลำดับที่คนนับ" กับ "index" แบบนี้จะเจอในโจทย์แทบทุกข้อ ให้ถามตัวเองทุกครั้งว่าเลขที่ถืออยู่เป็นแบบไหน

อ่าน N ตัวเข้าอาเรย์

โจทย์แบบ grader มักให้ N มาก่อน แล้วตามด้วยตัวเลข N ตัว และบอกขอบเขตไว้ เช่น 1 ≤ N ≤ 100 ให้ประกาศอาเรย์ตาม ค่ามากสุดที่โจทย์บอก แล้วใช้แค่ N ช่องแรก ภาษา C ยอมให้เขียน int height[n]; หลังอ่าน n ได้ก็จริง แต่อาเรย์แบบนั้นใส่ = {0} ไม่ได้ และถ้า n ใหญ่ เช่น ล้านตัว โปรแกรมพังตอนรันโดยไม่มีคำเตือนใดๆ ส่วนขนาดตายตัวตามขอบเขตรู้ตั้งแต่ตอนเขียนว่าพอทุกชุดทดสอบ

#include <stdio.h>

int main()
{
    int height[100]; // (1)!
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%d", &height[i]); // (2)!
    }
    for (int i = 0; i < n; i++) {
        if (i > 0) {
            printf(" ");
        }
        printf("%d", height[i]);
    }
    printf("\n");
    for (int i = n - 1; i >= 0; i--) { // (3)!
        if (i < n - 1) {
            printf(" ");
        }
        printf("%d", height[i]);
    }
    printf("\n");
    return 0;
}
  1. โจทย์บอกว่า N ไม่เกิน 100 จึงขอไว้ 100 ช่อง ถ้า N เป็น 4 ก็ใช้แค่ช่อง 0 ถึง 3 ช่องที่เหลือปล่อยว่างไว้ ไม่เป็นไร
  2. scanf ต้องการที่อยู่ของกล่องเหมือนเดิม กล่องคือ height[i] จึงใส่ & หน้ามันเป็น &height[i]
  3. พิมพ์กลับหลัง เริ่มจากช่องสุดท้าย n - 1 ลดลงทีละหนึ่งจนถึงช่อง 0 ตัวแรกที่พิมพ์คือช่อง n - 1 จึงพิมพ์ช่องว่างก่อนตัวเลขเมื่อ i < n - 1
4
152 160 147 171
152 160 147 171
171 147 160 152

ลูปอ่านกับลูปพิมพ์แยกกันได้ เพราะทุกค่ายังอยู่ในอาเรย์ นี่คือสิ่งที่ลูปในบทที่ 4 ทำไม่ได้ จะอ่านกี่รอบ ย้อนไปช่องไหน ก็ได้ค่าเดิม

ช่องว่างท้ายบรรทัด

ทั้งสองลูปพิมพ์ช่องว่าง ก่อน ตัวเลขทุกตัวยกเว้นตัวแรก แบบเดียวกับหน้า 4.1 จึงไม่มีช่องว่างเกินท้ายบรรทัด ถ้าเขียน printf("%d ", height[i]) จะมีช่องว่างหลังตัวสุดท้ายด้วย ตัวตรวจแบบฝึกหัดของเว็บนี้ไม่สนช่องว่างท้ายบรรทัด แต่ grader บางระบบเทียบทุกตัวอักษร

กลับลำดับในอาเรย์

บางโจทย์ต้องกลับลำดับแล้วเอาอาเรย์ไปใช้ต่อ ไม่ใช่แค่พิมพ์ วิธีคือสลับค่าช่องแรกกับช่องสุดท้าย ช่องที่สองกับช่องรองสุดท้าย เข้ามาเรื่อยๆ จนถึงตรงกลาง

การสลับค่าสองกล่องต้องมีกล่องพักหนึ่งใบ ถ้าเขียน a = b; แล้ว b = a; เลย ค่าเดิมของ a จะหายไปตั้งแต่บรรทัดแรก เหมือนสลับน้ำในแก้วสองใบ ต้องมีแก้วเปล่าใบที่สาม

#include <stdio.h>

int main()
{
    int height[100];
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%d", &height[i]);
    }
    for (int i = 0; i < n / 2; i++) {
        int temp = height[i];
        height[i] = height[n - 1 - i];
        height[n - 1 - i] = temp;
    }
    for (int i = 0; i < n; i++) {
        if (i > 0) {
            printf(" ");
        }
        printf("%d", height[i]);
    }
    printf("\n");
    return 0;
}
5
152 160 147 171 158
158 171 147 160 152
  • คู่ของช่อง i คือช่อง n - 1 - i ลองแทน i = 0 ได้ช่องสุดท้ายพอดี
  • วนแค่ n / 2 รอบ ถ้าวนครบ n รอบ ครึ่งหลังจะสลับกลับไปเป็นแบบเดิม ลองแก้เป็น i < n แล้วดูผลเอง
  • N เป็นเลขคี่ ช่องตรงกลางไม่ต้องสลับกับใคร 5 / 2 ได้ 2 จึงสลับแค่สองคู่ ช่อง 2 อยู่ที่เดิมพอดี

ลองทำให้พัง

int score[5] = {70, 85, 90, 65, 80};
printf("%d\n", score);
scores.c:6:14: warning: format '%d' expects argument of type 'int', but argument 2 has type 'int *' [-Wformat=]
    6 |     printf("%d\n", score);
      |             ~^     ~~~~~
      |              |     |
      |              int   int *
      |             %n

score เฉยๆ ไม่มีวงเล็บเหลี่ยม ไม่ใช่ค่าในอาเรย์ แต่เป็นที่อยู่ของอาเรย์ในหน่วยความจำ ชนิด int * จะได้เรียนในบทที่ 10 รอบนี้ compiler แนะนำ %n มาด้วย ซึ่งไม่ใช่สิ่งที่เราต้องการ ห้ามทำตาม บนเครื่องที่เขียนเอกสารนี้ รันสามครั้งได้ -2111833536 -1826621040 และ 1080031088 ไม่ซ้ำกันเลย printf พิมพ์ทั้งอาเรย์ในคำสั่งเดียวไม่ได้ ต้องวนลูปพิมพ์ทีละช่อง

แบบที่ 2 คัดลอกอาเรย์ด้วย =

int score[5] = {70, 85, 90, 65, 80};
int backup[5];
backup = score;
copy.c:7:12: error: assignment to expression with array type
    7 |     backup = score;
      |            ^

ใช้ = กับทั้งอาเรย์ไม่ได้ ข้อความแปลว่ากำหนดค่าให้สิ่งที่เป็นอาเรย์ทั้งก้อนไม่ได้ ต้องคัดลอกทีละช่อง

for (int i = 0; i < 5; i++) {
    backup[i] = score[i];
}

แบบที่ 3 ใส่ค่าเกินจำนวนช่อง

int score[5] = {70, 85, 90, 65, 80, 75};
days.c:5:41: warning: excess elements in array initializer
    5 |     int score[5] = {70, 85, 90, 65, 80, 75};
      |                                         ^~
days.c:5:41: note: (near initialization for 'score')

excess แปลว่าเกิน ห้าช่องใส่หกค่าไม่ได้ ค่าที่เกินถูกทิ้งไปเฉยๆ แค่เป็น warning โปรแกรมจึงยังรันได้ ถ้าเห็นข้อความนี้ แปลว่านับจำนวนช่องผิด

ความผิดพลาดที่เจอบ่อยที่สุดกับอาเรย์คือใช้ช่องที่ไม่มีอยู่จริง เช่น score[5] ในอาเรย์ห้าช่อง เรื่องนี้ compiler แทบไม่เตือน และผลที่ได้แปลกพอจะมีหัวข้อของตัวเองในหน้า 5.5

สรุป

  • int score[5]; ประกาศอาเรย์ห้าช่อง ทุกช่องเป็น int
  • ช่องแรกคือ [0] ช่องสุดท้ายคือ [N - 1] เลข index คือจำนวนช่องที่เดินจากช่องแรก
  • = {...} กำหนดค่าตอนประกาศ ใส่ไม่ครบช่องที่เหลือเป็น 0 = {0} ทำให้ทุกช่องเป็น 0
  • ประกาศอาเรย์ตามขอบเขตมากสุดในโจทย์ แล้ววนด้วย for (int i = 0; i < n; i++)
  • รับค่าด้วย scanf("%d", &a[i]) พิมพ์และคัดลอกทีละช่องเท่านั้น
  • สลับค่าสองช่องต้องมีตัวแปรพัก กลับลำดับทั้งอาเรย์วนแค่ n / 2 รอบ