[0]zero-index clytie0x/zero-index

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
scores scores + 2 70 85 90 60 75 [0] [1] [2] [3] [4] …FE20 …FE24 …FE28 …FE2C …FE30

นี่คือเหตุผลของสิ่งที่ค้างไว้หลายบท 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;
}
  1. เริ่มที่ช่องแรก วนตราบที่ยังไม่ถึง scores + n ซึ่งคือที่อยู่ถัดจากช่องสุดท้าย เทียบ pointer ด้วย < ได้ เพราะช่องที่อยู่หลังมีที่อยู่มากกว่า
  2. จำว่าค่าที่ดีที่สุดอยู่ที่ไหน ไม่ได้จำค่า
  3. 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;
}
  1. ใช้วงเล็บเหลี่ยมกับ pointer ได้ตามหัวข้อที่แล้ว values[i] คือ *(values + i)
  2. ส่งที่อยู่ของ rain[2] ในฟังก์ชัน values[0] จึงเป็น rain[2] และรวมสามช่องคือวันที่ 3 ถึง 5
  3. &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;
}
  1. รับที่อยู่ของช่องแรกและช่องสุดท้ายของช่วง ทั้งสองช่องรวมอยู่ในช่วงที่จะกลับ
  2. หยุดเมื่อสองตัวมาเจอกันพอดี (ความยาวคี่ ช่องกลางไม่ต้องย้าย) หรือสวนกันไปแล้ว (ความยาวคู่)
  3. ช่องสุดท้ายของอาเรย์ 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

#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] และชื่ออาเรย์ใช้ = หรือ ++ ไม่ได้