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
ให้คิดว่าแถวคือนักเรียน คอลัมน์คือวิชา 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;
}
- ปีกกาด้านนอกครอบทั้งตาราง ปีกกาด้านในแต่ละชุดคือหนึ่งแถว มี 3 ค่าตามจำนวนคอลัมน์ แถวสุดท้ายไม่ต้องมีจุลภาคต่อท้าย
- หนึ่งช่องใช้เหมือนตัวแปร
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;
}
- ประกาศไว้นอก
mainเหมือนอาเรย์ใหญ่ในบทที่ 5 ทุกช่องจึงเริ่มต้นเป็น 0 และไม่ทำให้โปรแกรมพังเมื่อขนาดใหญ่ เหตุผลดูได้ในส่วนลองทำให้พังท้ายหน้า - อ่านทีละตัว ไล่ทีละแถวจากซ้ายไปขวา ตรงกับลำดับตัวเลขในข้อมูลเข้า
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 ทั้งแถว ไปเรื่อยๆ
เรื่องนี้มีผลสองอย่างที่ต้องรู้
- ขนาดโตเร็วมาก
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 เสมอ