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
#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;
}
- โจทย์บอกว่า N ไม่เกิน 100 จึงขอไว้ 100 ช่อง ถ้า N เป็น 4 ก็ใช้แค่ช่อง 0 ถึง 3 ช่องที่เหลือปล่อยว่างไว้ ไม่เป็นไร
scanfต้องการที่อยู่ของกล่องเหมือนเดิม กล่องคือheight[i]จึงใส่&หน้ามันเป็น&height[i]- พิมพ์กลับหลัง เริ่มจากช่องสุดท้าย
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 อยู่ที่เดิมพอดี
ลองทำให้พัง
แบบที่ 1 พิมพ์ทั้งอาเรย์ด้วย %d ตัวเดียว
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รอบ