10.3 pointer กับอาเรย์
ตั้งแต่บทที่ 5 มีหลายจุดที่บอกว่า "ชื่ออาเรย์คือที่อยู่" แต่ยังไม่ได้อธิบาย เช่น %s ไม่ต้องมี & ฟังก์ชันแก้อาเรย์ของคนเรียกได้ และ sizeof ในฟังก์ชันได้ 8 หน้านี้เฉลยทั้งหมดด้วยกฎข้อเดียว แล้วต่อด้วยการบวกลบ pointer ซึ่งทำให้เดินบนอาเรย์ ส่งแค่บางช่วงของอาเรย์ให้ฟังก์ชัน และใช้ pointer สองตัวเดินเข้าหากันได้ เทคนิคสุดท้ายนี้ใช้บ่อยในโจทย์กลับลำดับและโจทย์ที่ต้องดูข้อมูลจากสองฝั่ง
ชื่ออาเรย์คือที่อยู่ของช่องแรก
ช่องของอาเรย์วางติดกันในหน่วยความจำเสมอ ลองพิมพ์ที่อยู่ของทุกช่องดู
#include <stdio.h>
int main()
{
int scores[5] = {70, 85, 90, 60, 75};
printf("scores = %p\n", scores);
for (int i = 0; i < 5; i++) {
printf("scores + %d = %p, &scores[%d] = %p\n", i, scores + i, i, &scores[i]);
}
return 0;
}
scores = 00000026271FFE20
scores + 0 = 00000026271FFE20, &scores[0] = 00000026271FFE20
scores + 1 = 00000026271FFE24, &scores[1] = 00000026271FFE24
scores + 2 = 00000026271FFE28, &scores[2] = 00000026271FFE28
scores + 3 = 00000026271FFE2C, &scores[3] = 00000026271FFE2C
scores + 4 = 00000026271FFE30, &scores[4] = 00000026271FFE30
ที่อยู่เปลี่ยนทุกครั้งที่รันเหมือนในหน้า 10.1 แต่สังเกตสามเรื่องที่เหมือนเดิมเสมอ
- ใช้ชื่อ
scoresเฉยๆ ในที่ที่ต้องการค่า จะได้ที่อยู่ของscores[0]ภาษา C แปลงชื่ออาเรย์เป็น&scores[0]ให้เอง ชนิดจึงเป็นint *(ยกเว้นตอนอยู่หลังsizeofซึ่งยังได้ขนาดของทั้งอาเรย์ และตอนอยู่หลัง&ซึ่งจะเห็นในหน้า 10.4) ตรงกับคำเตือนในหน้า 5.1 ที่ลองพิมพ์ทั้งอาเรย์ด้วย%d - แต่ละช่องห่างกัน 4 เท่ากับขนาดของ
int(ฐานสิบหก 20 24 28 2C 30 คือห่างกันทีละ 4) scores + iได้ที่อยู่เดียวกับ&scores[i]ทุกช่อง ทั้งที่บวกไปแค่iไม่ใช่4 × i
นี่คือเหตุผลของสิ่งที่ค้างไว้หลายบท scanf("%s", name) ไม่ต้องมี & เพราะ name กลายเป็นที่อยู่ของช่องแรกอยู่แล้ว และฟังก์ชันที่รับอาเรย์ในหน้า 8.3 แก้อาเรย์ของคนเรียกได้ เพราะสิ่งที่ถูกคัดลอกเข้าไปคือที่อยู่ ไม่ใช่ตัวอาเรย์ เหมือน swap ในหน้า 10.2 ทุกอย่าง
บวกเลขกับ pointer
pointer บวกเลขจำนวนเต็มได้ แต่หน่วยไม่ใช่ byte p + 1 คือกล่องถัดไป ซึ่งอยู่ห่างไปเท่ากับขนาดของชนิดที่ p ชี้ compiler รู้ขนาดนี้จากชนิดของ pointer นี่เป็นอีกเหตุผลที่ pointer ต้องมีชนิดตามหน้า 10.1
#include <stdio.h>
int main()
{
int counts[3] = {0};
double heights[3] = {0};
char letters[3] = {0};
printf("int %p %p\n", counts, counts + 1);
printf("double %p %p\n", heights, heights + 1);
printf("char %p %p\n", letters, letters + 1);
return 0;
}
int 0000001B62FFFC14 0000001B62FFFC18
double 0000001B62FFFBF0 0000001B62FFFBF8
char 0000001B62FFFBED 0000001B62FFFBEE
บวก 1 เหมือนกันทั้งสามบรรทัด แต่ที่อยู่ขยับไป 4 8 และ 1 ตามขนาดของ int double และ char เราจึงไม่ต้องคูณขนาดเองเลย
ภาษา C นิยาม a[i] ไว้ว่าคือ *(a + i) แปลว่า "เลื่อนจากช่องแรกไป i กล่อง แล้วตามไปที่กล่องนั้น" สองแบบนี้จึงเป็นสิ่งเดียวกันทุกประการ
#include <stdio.h>
int main()
{
int scores[5] = {70, 85, 90, 60, 75};
for (int i = 0; i < 5; i++) {
printf("%d %d\n", scores[i], *(scores + i));
}
return 0;
}
70 70
85 85
90 90
60 60
75 75
อาเรย์จึงเริ่มที่ช่อง 0 เพราะเลขในวงเล็บเหลี่ยมคือระยะห่างจากช่องแรก ช่องแรกห่างจากตัวเอง 0 กล่อง และวงเล็บเหลี่ยมใช้กับ pointer ได้เหมือนกับอาเรย์ทุกอย่าง ถ้า p ชี้ช่องไหน p[1] ก็คือกล่องถัดจากช่องนั้น ในโค้ดจริงเขียน scores[i] อ่านง่ายกว่า แบบ *(scores + i) มีไว้ให้เข้าใจว่าข้างในทำอะไร
เดินบนอาเรย์ด้วย pointer
เพราะ p + 1 คือช่องถัดไป p++ จึงเป็นการเลื่อนไปช่องถัดไป และ pointer ลบกันได้ q - p คือจำนวนช่องระหว่างสองที่อยู่ ไม่ใช่จำนวน byte ตัวอย่างนี้หาผลรวมและคะแนนสูงสุดโดยไม่มี index เลย แล้วค่อยแปลงตำแหน่งกลับเป็น index ตอนท้าย
#include <stdio.h>
int main()
{
int scores[6] = {70, 85, 90, 60, 95, 75};
int n = 6;
int sum = 0;
int *best = scores;
for (int *p = scores; p < scores + n; p++) { // (1)!
sum += *p;
if (*p > *best) {
best = p; // (2)!
}
}
printf("sum = %d\n", sum);
printf("best = %d at index %d\n", *best, (int)(best - scores)); // (3)!
return 0;
}
- เริ่มที่ช่องแรก วนตราบที่ยังไม่ถึง
scores + nซึ่งคือที่อยู่ถัดจากช่องสุดท้าย เทียบ pointer ด้วย<ได้ เพราะช่องที่อยู่หลังมีที่อยู่มากกว่า - จำว่าค่าที่ดีที่สุดอยู่ที่ไหน ไม่ได้จำค่า
best - scoresคือ best อยู่ห่างจากช่องแรกกี่ช่อง ก็คือ index นั่นเอง ผลลบของ pointer ไม่ใช่intจึงแปลงเป็นintก่อนพิมพ์ แบบเดียวกับsizeofในหน้า 10.1
sum = 475
best = 95 at index 4
scores + n ชี้เลยช่องสุดท้ายไปหนึ่งช่อง ภาษา C อนุญาตให้สร้างที่อยู่นี้ไว้เทียบได้ แต่ห้ามใช้ * กับมัน เพราะไม่มีกล่องของเราอยู่ตรงนั้น ดูแบบที่ 1 ในส่วนลองทำให้พัง การลบ pointer แบบนี้ใช้อีกครั้งกับ strstr ในบทที่ 13 ซึ่งคืนที่อยู่ของคำที่หาเจอ แล้วลบกับชื่ออาเรย์เพื่อได้ตำแหน่ง
อาเรย์ที่ส่งเข้าฟังก์ชันคือ pointer
ในหัวฟังก์ชัน int values[] กับ int *values เป็นชนิดเดียวกันทุกประการ คือ pointer ไปยัง int ภาษา C ยอมให้เขียนแบบวงเล็บเหลี่ยมเพื่อบอกคนอ่านว่าตั้งใจส่งอาเรย์มา เมื่อพารามิเตอร์เป็นแค่ที่อยู่ จะส่งที่อยู่ของช่องไหนเข้าไปก็ได้ ฟังก์ชันก็จะเห็นอาเรย์ที่เริ่มจากช่องนั้น
#include <stdio.h>
int sum_of(int *values, int n)
{
int sum = 0;
for (int i = 0; i < n; i++) {
sum += values[i]; // (1)!
}
return sum;
}
int main()
{
int rain[7] = {12, 0, 5, 30, 8, 0, 21};
printf("week = %d\n", sum_of(rain, 7));
printf("day 3 to 5 = %d\n", sum_of(rain + 2, 3)); // (2)!
printf("weekend = %d\n", sum_of(&rain[5], 2)); // (3)!
return 0;
}
- ใช้วงเล็บเหลี่ยมกับ pointer ได้ตามหัวข้อที่แล้ว
values[i]คือ*(values + i) - ส่งที่อยู่ของ
rain[2]ในฟังก์ชันvalues[0]จึงเป็นrain[2]และรวมสามช่องคือวันที่ 3 ถึง 5 &rain[5]กับrain + 5คือที่อยู่เดียวกัน เลือกเขียนแบบที่อ่านง่ายกว่าในจุดนั้น
week = 76
day 3 to 5 = 43
weekend = 21
ปริมาณฝนวันที่ 3 ถึง 5 คือ 5 + 30 + 8 = 43 และวันเสาร์อาทิตย์คือ 0 + 21 = 21 ฟังก์ชันตัวเดียวทำงานกับทั้งอาเรย์หรือช่วงไหนก็ได้ โดยไม่ต้องรับตำแหน่งเริ่มเพิ่ม
เรื่องนี้อธิบายคำเตือนในหน้า 8.3 ที่บอกว่า 'sizeof' on array function parameter 'scores' will return size of 'int *' ด้วย ในฟังก์ชัน scores เป็น pointer ตัวหนึ่ง sizeof จึงได้ขนาดของ pointer คือ 8 byte ไม่ใช่ขนาดของอาเรย์ ฟังก์ชันไม่มีทางรู้ว่าอาเรย์ยาวเท่าไร จึงต้องส่งความยาวเข้าไปเองเสมอ
pointer สองตัวเดินเข้าหากัน
การกลับลำดับอาเรย์คือสลับช่องแรกกับช่องสุดท้าย แล้วขยับเข้ามาข้างในทีละช่องจากทั้งสองฝั่ง จนสองฝั่งมาเจอกัน เขียนด้วย pointer สองตัวได้ตรงกับความคิดนี้ที่สุด
#include <stdio.h>
void reverse(int *first, int *last) // (1)!
{
while (first < last) { // (2)!
int temp = *first;
*first = *last;
*last = temp;
first++;
last--;
}
}
void print_all(int *values, int n)
{
for (int i = 0; i < n; i++) {
if (i > 0) {
printf(" ");
}
printf("%d", values[i]);
}
printf("\n");
}
int main()
{
int line[7] = {11, 12, 13, 14, 15, 16, 17};
reverse(line, line + 6); // (3)!
print_all(line, 7);
reverse(&line[1], &line[3]);
print_all(line, 7);
return 0;
}
- รับที่อยู่ของช่องแรกและช่องสุดท้ายของช่วง ทั้งสองช่องรวมอยู่ในช่วงที่จะกลับ
- หยุดเมื่อสองตัวมาเจอกันพอดี (ความยาวคี่ ช่องกลางไม่ต้องย้าย) หรือสวนกันไปแล้ว (ความยาวคู่)
- ช่องสุดท้ายของอาเรย์ 7 ช่องคือ
line + 6ไม่ใช่line + 7
17 16 15 14 13 12 11
17 14 15 16 13 12 11
ครั้งที่สองกลับเฉพาะช่อง 1 ถึง 3 ซึ่งตอนนั้นคือ 16 15 14 กลายเป็น 14 15 16 ส่วนช่องอื่นอยู่ที่เดิม เพราะฟังก์ชันเห็นแค่ช่วงที่ได้รับ ช่วงยาว n ช่องสลับแค่ n / 2 ครั้ง
เขียนด้วย index ก็ได้ แต่ต้องรับอาเรย์ ตำแหน่งเริ่ม และตำแหน่งจบ สามตัว แบบ pointer ใช้แค่สองตัวและไม่ต้องคิดว่าช่วงนี้อยู่ตรงไหนของอาเรย์ใหญ่ เทคนิค pointer สองตัวจากสองฝั่งยังใช้ได้กับอีกหลายงาน เช่น ตรวจว่าลำดับอ่านจากหน้าไปหลังกับหลังไปหน้าเหมือนกันไหม
*p++ กับ (*p)++
เวลาอ่านโค้ดคนอื่นจะเจอ ++ ปนกับ * บ่อย สองแบบนี้ต่างกันมาก
| เขียน | ความหมาย |
|---|---|
(*p)++ | เพิ่มค่าในกล่องที่ p ชี้ p ไม่ขยับ |
p++ | ขยับ p ไปกล่องถัดไป ค่าในกล่องไม่เปลี่ยน |
*p++ = x | เขียน x ลงกล่องที่ p ชี้อยู่ตอนนี้ แล้วขยับ p ไปกล่องถัดไป |
#include <stdio.h>
int main()
{
int counts[3] = {5, 10, 15};
int *p = counts;
(*p)++;
p++;
*p++ = 20;
printf("%d %d %d\n", counts[0], counts[1], counts[2]);
printf("%d\n", *p);
return 0;
}
6 20 15
15
บรรทัดที่ 7 เพิ่ม counts[0] เป็น 6 บรรทัดที่ 8 ขยับไป counts[1] บรรทัดที่ 9 เขียน 20 ลง counts[1] แล้วขยับไป counts[2] p จึงชี้ 15 ตอนจบ *p++ ทำงานแบบนี้เพราะ ++ ที่อยู่หลังชื่อจับกับ p ก่อน * ถ้าไม่แน่ใจให้แยกเป็นสองคำสั่งแบบบรรทัดที่ 7 และ 8 อ่านง่ายกว่าเสมอ
ลองทำให้พัง
แบบที่ 1 เดินเลยช่องสุดท้าย
ในลูปเดินด้วย pointer เผลอใช้ <= แทน <
#include <stdio.h>
int main()
{
int scores[5] = {70, 85, 90, 60, 75};
int n = 5;
int sum = 0;
for (int *p = scores; p <= scores + n; p++) {
sum += *p;
}
printf("sum = %d\n", sum);
return 0;
}
คอมไพล์ด้วย -Wall ไม่มีคำเตือน ผลรวมที่ถูกคือ 380 แต่บนเครื่องที่เขียนเอกสารนี้ รันสามครั้งได้ sum = 764 sum = 717 และ sum = 963 ไม่ซ้ำกันเลย รอบสุดท้ายของลูป p เท่ากับ scores + 5 ซึ่งชี้เลยอาเรย์ไปแล้ว *p จึงหยิบของที่บังเอิญวางอยู่ถัดจากอาเรย์มาบวก เป็นความผิดเดียวกับใช้ scores[5] ในหน้า 5.5 เพราะ scores[5] ก็คือ *(scores + 5) pointer ไม่ได้ช่วยกันขอบอาเรย์ให้เลย
แบบที่ 2 คิดว่าชื่ออาเรย์เป็นตัวแปร pointer
ชื่ออาเรย์ให้ที่อยู่ได้ก็จริง แต่ไม่ใช่กล่อง pointer ที่เปลี่ยนค่าได้ ลองคัดลอกอาเรย์ด้วย = และเลื่อนชื่ออาเรย์ด้วย ++
#include <stdio.h>
int main()
{
int scores[5] = {70, 85, 90, 60, 75};
int backup[5];
backup = scores;
scores++;
printf("%d\n", backup[0]);
return 0;
}
copy.c:7:12: error: assignment to expression with array type
7 | backup = scores;
| ^
copy.c:8:11: error: lvalue required as increment operand
8 | scores++;
| ^~
ข้อความแรกบอกว่าเอาค่าใส่สิ่งที่เป็นอาเรย์ไม่ได้ ข้อความที่สองบอกว่า ++ ต้องใช้กับกล่องที่เปลี่ยนค่าได้ (lvalue คือสิ่งที่อยู่ทางซ้ายของ = ได้) อาเรย์จองที่ไว้ตายตัวตั้งแต่ประกาศ ย้ายไม่ได้ คัดลอกอาเรย์ต้องวนลูปทีละช่องแบบบทที่ 5 ถ้าอยากได้ตัวที่เลื่อนได้ ให้ประกาศ int *p = scores; แล้วเลื่อน p แทน
แบบที่ 3 บวก pointer สองตัวเพื่อหาตรงกลาง
หาช่องตรงกลางระหว่าง first กับ last แบบที่ทำกับ index
#include <stdio.h>
int main()
{
int scores[5] = {60, 70, 75, 85, 90};
int *first = scores;
int *last = scores + 4;
int *mid = (first + last) / 2;
printf("%d\n", *mid);
return 0;
}
middle.c:8:23: error: invalid operands to binary + (have 'int *' and 'int *')
8 | int *mid = (first + last) / 2;
| ^
ที่อยู่บวกที่อยู่ไม่มีความหมาย เหมือนเอาบ้านเลขที่สองหลังมาบวกกันแล้วไม่ได้บ้านหลังไหน สิ่งที่ทำได้มีแค่ pointer บวกหรือลบจำนวนเต็ม และ pointer ลบ pointer ช่องกลางจึงเขียนเป็น first + (last - first) / 2 คือเริ่มที่ first แล้วเดินไปครึ่งหนึ่งของระยะห่าง ได้ช่องที่เก็บ 75
แบบที่ 4 พิมพ์ผลลบของ pointer ด้วย %d
#include <stdio.h>
int main()
{
int scores[5] = {70, 85, 90, 60, 75};
int *p = &scores[3];
printf("index %d\n", p - scores);
return 0;
}
diff.c:7:20: warning: format '%d' expects argument of type 'int', but argument 2 has type 'long long int' [-Wformat=]
7 | printf("index %d\n", p - scores);
| ~^ ~~~~~~~~~~
| | |
| int long long int
| %lld
ผลลบของ pointer บนเครื่อง 64 bit มีขนาด 8 byte เท่า long long เพราะต้องรองรับระยะห่างของอาเรย์ที่ใหญ่มาก บนเครื่องที่เขียนเอกสารนี้ยังพิมพ์ index 3 ได้ถูก แต่เป็นความผิดแบบเดียวกับพิมพ์ long long ด้วย %d ในหน้า 1.3 แก้แบบเดียวกับ sizeof คือ (int)(p - scores) ระยะห่างในอาเรย์ของโจทย์ไม่เกิน int อยู่แล้ว
สรุป
- ชื่ออาเรย์ที่ใช้เป็นค่า คือที่อยู่ของช่องแรก
scoresเท่ากับ&scores[0]ชนิดint * p + iคือกล่องที่ห่างจากpไปiกล่อง และa[i]คือ*(a + i)q - pคือจำนวนช่องระหว่างสองที่อยู่ พิมพ์ด้วย(int)(q - p)- พารามิเตอร์
int values[]คือint *valuesส่งa + kเพื่อให้ฟังก์ชันทำงานกับช่วงที่เริ่มจากช่องkได้ - pointer สองตัวจากหัวและท้ายเดินเข้าหากันด้วย
while (first < last)ใช้กลับลำดับช่วงไหนก็ได้ - pointer ไม่กันขอบอาเรย์
*(a + n)ผิดเท่ากับa[n]และชื่ออาเรย์ใช้=หรือ++ไม่ได้