[0]zero-index clytie0x/zero-index

7.1 ประกาศและอ่านตาราง

ในบทที่ 5 เราเก็บคะแนนสอบของนักเรียนทั้งห้องไว้ในอาเรย์ตัวเดียว ทีนี้ถ้านักเรียน 4 คนสอบคนละ 3 วิชา ข้อมูลจะเป็น ตาราง มีแถวและคอลัมน์ จะประกาศอาเรย์สี่ตัวแยกกันก็ได้ แต่พอมี 1000 คนก็ไม่ไหว ภาษา C จึงให้ประกาศอาเรย์ที่มีสองมิติได้ เรียกว่า อาเรย์สองมิติ (2D array) ใช้เก็บอะไรก็ได้ที่เป็นตาราง เช่น ตารางคะแนน ผังที่นั่ง กระดานเกม และแผนที่

ประกาศอาเรย์สองมิติ

เขียนขนาดสองวงเล็บ วงเล็บแรกคือจำนวน แถว (row) วงเล็บที่สองคือจำนวน คอลัมน์ (column)

int score[4][3];

ได้ช่องทั้งหมด 4 × 3 = 12 ช่อง แต่ละช่องคือ int หนึ่งตัว เรียกช่องด้วยเลขสองตัว score[แถว][คอลัมน์] และเหมือนอาเรย์ในบทที่ 5 ทุกอย่าง คือ เลขเริ่มที่ 0 แถวมีเลข 0 ถึง 3 คอลัมน์มีเลข 0 ถึง 2

คอลัมน์ (column) 0 1 2 แถว (row) 0 78 65 90 1 82 91 70 2 55 88 73 3 95 60 84 score[2][1]

ให้คิดว่าแถวคือนักเรียน คอลัมน์คือวิชา score[2][1] คือคะแนนของนักเรียนคนที่มีเลข 2 ในวิชาที่มีเลข 1 ได้ 88 ลำดับสำคัญมาก แถวมาก่อน คอลัมน์มาทีหลังเสมอ

ใส่ค่าเริ่มต้นและเรียกใช้ช่อง

ใส่ค่าตอนประกาศได้ด้วยปีกกาซ้อนปีกกา ปีกกาด้านในหนึ่งชุดคือหนึ่งแถว

#include <stdio.h>

int main()
{
    int score[4][3] = { // (1)!
        {78, 65, 90},
        {82, 91, 70},
        {55, 88, 73},
        {95, 60, 84}
    };
    printf("score[2][1] = %d\n", score[2][1]);
    score[2][1] = 93; // (2)!
    printf("after fix = %d\n", score[2][1]);
    return 0;
}
  1. ปีกกาด้านนอกครอบทั้งตาราง ปีกกาด้านในแต่ละชุดคือหนึ่งแถว มี 3 ค่าตามจำนวนคอลัมน์ แถวสุดท้ายไม่ต้องมีจุลภาคต่อท้าย
  2. หนึ่งช่องใช้เหมือนตัวแปร int ธรรมดาทุกอย่าง กำหนดค่าใหม่ บวก ลบ หรือส่งให้ scanf ด้วย &score[r][c] ก็ได้
score[2][1] = 88
after fix = 93

ตารางมีสองมิติ จึงใช้ลูปซ้อนสองชั้นแบบที่เรียนในบทที่ 4 ลูปนอกเดินทีละแถว ลูปในเดินทีละคอลัมน์ของแถวนั้น ตั้งชื่อตัวนับว่า r (row) กับ c (column) จะไม่สับสนว่าตัวไหนคือแถว

#include <stdio.h>

int main()
{
    int score[4][3] = {
        {78, 65, 90},
        {82, 91, 70},
        {55, 88, 73},
        {95, 60, 84}
    };
    for (int r = 0; r < 4; r++) {
        for (int c = 0; c < 3; c++) {
            printf("%d", score[r][c]);
            if (c < 3 - 1) {
                printf(" ");
            }
        }
        printf("\n");
    }
    return 0;
}
78 65 90
82 91 70
55 88 73
95 60 84

บรรทัดที่ 14 ใส่ช่องว่างหลังทุกตัวยกเว้นตัวสุดท้ายของแถว แต่ละบรรทัดจึงไม่มีช่องว่างเกินท้าย ส่วน printf("\n") บรรทัดที่ 18 อยู่นอกลูปใน แต่ยังอยู่ในลูปนอก จึงขึ้นบรรทัดใหม่ครั้งเดียวต่อหนึ่งแถว ลองย้ายมันเข้าไปไว้ในลูปใน แล้วดูว่าผลเปลี่ยนเป็นอย่างไร

รับตารางจากข้อมูลเข้า

โจทย์ตารางเกือบทุกข้อให้ข้อมูลเข้ารูปแบบเดียวกัน บรรทัดแรกบอกจำนวนแถว R กับจำนวนคอลัมน์ C บรรทัดต่อมาอีก R บรรทัดเป็นตัวเลขบรรทัดละ C ตัว เราไม่รู้ R กับ C ล่วงหน้า จึง ประกาศอาเรย์ขนาดเท่าขอบเขตที่มากที่สุดในโจทย์ แล้วใช้แค่มุมบนซ้าย R × C ช่อง

ตัวอย่างนี้เป็นสวนมะม่วงที่แบ่งเป็นแปลง R แถว C คอลัมน์ แต่ละแปลงเก็บมะม่วงได้กี่ลัง โจทย์บอกว่า R และ C ไม่เกิน 100

#include <stdio.h>

int crates[100][100]; // (1)!

int main()
{
    int rows, cols;
    scanf("%d %d", &rows, &cols);
    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            scanf("%d", &crates[r][c]); // (2)!
        }
    }
    int total = 0;
    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            total += crates[r][c];
        }
    }
    printf("%d rows, %d columns\n", rows, cols);
    printf("total = %d crates\n", total);
    return 0;
}
  1. ประกาศไว้นอก main เหมือนอาเรย์ใหญ่ในบทที่ 5 ทุกช่องจึงเริ่มต้นเป็น 0 และไม่ทำให้โปรแกรมพังเมื่อขนาดใหญ่ เหตุผลดูได้ในส่วนลองทำให้พังท้ายหน้า
  2. อ่านทีละตัว ไล่ทีละแถวจากซ้ายไปขวา ตรงกับลำดับตัวเลขในข้อมูลเข้า scanf ข้ามช่องว่างและการขึ้นบรรทัดให้เอง จึงไม่ต้องสนใจว่าตัวเลขอยู่บรรทัดไหน
3 4
5 2 7 1
0 9 3 4
8 6 2 6
3 rows, 4 columns
total = 53 crates

หาค่ามากสุดและตำแหน่ง

หาค่ามากสุดทำแบบเดียวกับอาเรย์หนึ่งมิติ ต่างกันแค่ตำแหน่งต้องจำสองค่า คือแถวกับคอลัมน์ ให้เริ่มจากช่อง [0][0] แล้วเทียบทุกช่อง

#include <stdio.h>

int crates[100][100];

int main()
{
    int rows, cols;
    scanf("%d %d", &rows, &cols);
    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            scanf("%d", &crates[r][c]);
        }
    }
    int best_r = 0, best_c = 0;
    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            if (crates[r][c] > crates[best_r][best_c]) {
                best_r = r;
                best_c = c;
            }
        }
    }
    printf("best plot: row %d column %d\n", best_r + 1, best_c + 1);
    printf("crates = %d\n", crates[best_r][best_c]);
    return 0;
}
3 4
5 2 7 1
0 9 3 4
8 6 2 6
best plot: row 2 column 2
crates = 9
  • เก็บแค่ตำแหน่ง best_r กับ best_c ก็พอ ค่ามากสุดหาได้จาก crates[best_r][best_c]
  • ในโค้ดแถวแรกคือแถว 0 แต่โจทย์ส่วนใหญ่นับแถวแรกเป็นแถวที่ 1 จึงต้อง + 1 ตอนพิมพ์ อ่านโจทย์ให้ดีทุกครั้งว่านับจาก 0 หรือ 1
  • ใช้ > ไม่ใช่ >= ถ้ามีค่ามากสุดซ้ำกันหลายช่อง จะได้ช่องที่เจอก่อน คือแถวบนสุด และถ้าแถวเดียวกันก็ซ้ายสุด โจทย์มักกำหนดแบบนี้เมื่อคำตอบเสมอกัน

ในหน่วยความจำ ตารางคือแถวยาวแถวเดียว

หน่วยความจำของคอมพิวเตอร์ไม่มีแถวและคอลัมน์ มีแต่ช่องเรียงกันเป็นเส้นตรง ภาษา C จึงเก็บอาเรย์สองมิติโดยวางแถว 0 ทั้งแถวก่อน ต่อด้วยแถว 1 ทั้งแถว ไปเรื่อยๆ

78 [0][0] 65 [0][1] 90 [0][2] แถว 0 82 [1][0] 91 [1][1] 70 [1][2] แถว 1 55 [2][0] 88 [2][1] 73 [2][2] แถว 2 95 [3][0] 60 [3][1] 84 [3][2] แถว 3

เรื่องนี้มีผลสองอย่างที่ต้องรู้

  • ขนาดโตเร็วมาก int grid[1000][1000] คือหนึ่งล้านช่อง ช่องละ 4 byte รวม 4 ล้าน byte หรือราว 4 MB เกิน stack ราว 2 MB ที่วัดได้ในหน้า 5.5 ไปมาก ถ้าประกาศไว้ใน main จะพัง ตารางจึงประกาศไว้นอก main เสมอ
  • เลขคอลัมน์ที่เกินจะล้นไปแถวถัดไปเงียบๆ ช่องถัดจาก score[0][2] ในหน่วยความจำคือ score[1][0] ถ้าเขียน score[0][3] ซึ่งไม่มีอยู่จริง โปรแกรมจะไม่ฟ้อง แต่หยิบ score[1][0] มาให้แทน เรื่องนี้จะกลับมาเป็นบั๊กที่หายากในหน้า 7.4

ลองทำให้พัง

แบบที่ 1 ไม่บอกจำนวนคอลัมน์

อาเรย์หนึ่งมิติที่ใส่ค่าเริ่มต้นไม่ต้องบอกขนาดก็ได้ หลายคนจึงลองเขียนแบบเดียวกันกับตาราง ในโปรแกรม score เปลี่ยนบรรทัดที่ 5 เป็น int score[][] = {

score.c:5:9: error: array type has incomplete element type 'int[]'
    5 |     int score[][] = {
      |         ^~~~~
score.c:5:9: note: declaration of 'score' as multidimensional array must have bounds for all dimensions except the first
score.c:5:9: warning: unused variable 'score' [-Wunused-variable]

บรรทัด note บอกกฎไว้ตรงๆ อาเรย์หลายมิติละขนาดได้แค่มิติแรก เพราะ compiler ต้องรู้ว่าหนึ่งแถวยาวกี่ช่อง ถึงจะรู้ว่าแถวถัดไปเริ่มตรงไหนในหน่วยความจำ เขียน int score[][3] จึงใช้ได้ แต่เขียนขนาดครบทั้งสองมิติอ่านง่ายกว่า

แบบที่ 2 เรียกแถวที่ไม่มีอยู่

ตารางมี 4 แถว คือแถว 0 ถึง 3 ในโปรแกรม score แก้บรรทัดที่ 11 ให้พิมพ์ score[4][1]

printf("score[4][1] = %d\n", score[4][1]);

คอมไพล์ใน Code::Blocks ที่เปิดแค่ -Wall ไม่มีคำเตือนเลย บนเครื่องที่เขียนเอกสารนี้รันแล้วได้ score[4][1] = 95 ดูเหมือนคะแนนจริงเสียด้วย ทั้งที่เป็นค่าขยะจากหน่วยความจำที่อยู่ถัดจากตาราง ถ้าคอมไพล์แบบเปิด -O2 เพิ่ม gcc ถึงจะเตือน และรันแล้วได้ 254

score.c:11:39: warning: array subscript 4 is above array bounds of 'int[4][3]' [-Warray-bounds=]
   11 |     printf("score[4][1] = %d\n", score[4][1]);
      |                                  ~~~~~^~~
score.c:5:9: note: while referencing 'score'
    5 |     int score[4][3] = {
      |         ^~~~~

คำเตือนนี้ขึ้นได้เพราะเลข 4 เขียนตายตัวในโค้ด ถ้าเลขแถวมาจากตัวแปรในลูป ไม่มีใครเตือน ต้องคุมเองว่าลูปเดินถึง r < rows ไม่ใช่ r <= rows

แบบที่ 3 สลับแถวกับคอลัมน์ตอนอ่าน

ในโปรแกรม mango ลูปอ่านเขียนสลับกันเป็น r < cols และ c < rows ส่วนลูปอื่นยังถูก แล้วเพิ่มส่วนพิมพ์ตารางท้ายโปรแกรม

#include <stdio.h>

int crates[100][100];

int main()
{
    int rows, cols;
    scanf("%d %d", &rows, &cols);
    for (int r = 0; r < cols; r++) {
        for (int c = 0; c < rows; c++) {
            scanf("%d", &crates[r][c]);
        }
    }
    for (int r = 0; r < rows; r++) {
        for (int c = 0; c < cols; c++) {
            printf("%d", crates[r][c]);
            if (c < cols - 1) {
                printf(" ");
            }
        }
        printf("\n");
    }
    return 0;
}
3 4
5 2 7 1
0 9 3 4
8 6 2 6
5 2 7 0
1 0 9 0
3 4 8 0

ลูปอ่านเห็นตารางเป็น 4 แถว แถวละ 3 ตัว ตัวเลขจึงถูกเก็บผิดช่องทั้งหมด และคอลัมน์สุดท้ายเป็น 0 เพราะไม่เคยถูกอ่านใส่ ถ้าทดสอบแค่ตารางจัตุรัสอย่าง 3 × 3 จะไม่เห็นบั๊กนี้ เพราะ rows กับ cols เท่ากัน ทดสอบโปรแกรมตารางให้ใช้ R กับ C ที่ไม่เท่ากันเสมอ

แบบที่ 4 ประกาศตารางใหญ่ไว้ใน main

ย้าย crates เข้าไปไว้ใน main และขยายเป็น int crates[1000][1000]; เผื่อโจทย์ที่ R และ C ถึง 1000 คอมไพล์ผ่านโดยไม่มีคำเตือน แต่พอรันแล้วป้อนตารางเล็กๆ โปรแกรมปิดตัวทันทีโดยไม่พิมพ์อะไร Code::Blocks ขึ้นว่า

Process returned -1073741571 (0xC00000FD)

นี่คือ stack overflow แบบเดียวกับในหน้า 5.5 ตารางขนาดนี้ดูไม่ใหญ่เพราะเขียนแค่ 1000 แต่ 1000 × 1000 คือหนึ่งล้านช่อง ราว 4 MB เกิน stack ราว 2 MB ไปเท่าตัว ย้ายกลับไปไว้นอก main แล้วจะรันได้ปกติ

สรุป

  • int a[R][C]; ได้ตาราง R แถว C คอลัมน์ เรียกช่องด้วย a[แถว][คอลัมน์] เลขเริ่มที่ 0
  • เดินทั้งตารางด้วยลูปซ้อน ลูปนอกคือแถว ลูปในคือคอลัมน์ ตั้งชื่อตัวนับ r กับ c จะไม่สับสน
  • ประกาศขนาดเท่าขอบเขตมากสุดในโจทย์ ไว้นอก main แล้วใช้แค่ R × C ช่องมุมบนซ้าย
  • หาค่ามากสุดในตารางต้องจำตำแหน่งสองค่า และระวังว่าโจทย์นับแถวจาก 0 หรือ 1
  • ในหน่วยความจำตารางเรียงทีละแถว เลขเกินขอบไม่ฟ้อง แต่หยิบช่องอื่นมาแทน
  • ทดสอบด้วยตารางที่ R ไม่เท่ากับ C เสมอ