[0]zero-index clytie0x/zero-index

8.3 ส่งอาเรย์และสตริงให้ฟังก์ชัน

โจทย์ส่วนใหญ่ตั้งแต่บทที่ 5 เป็นต้นมาเก็บข้อมูลไว้ในอาเรย์ งานที่ทำซ้ำบ่อยที่สุดจึงเป็นงานกับอาเรย์ เช่น อ่านเข้า หาผลรวม หาค่ามากสุด พิมพ์ออก หน้านี้จะย้ายงานพวกนี้ไปเป็นฟังก์ชันที่ใช้ซ้ำได้ทุกอาเรย์ และจะเห็นว่าอาเรย์มีกติกาต่างจากตัวแปรธรรมดาในหน้า 8.2 สองข้อ ข้อแรกฟังก์ชันไม่รู้ขนาดของอาเรย์ ข้อสองฟังก์ชันแก้อาเรย์ของคนเรียกได้

ส่งอาเรย์เข้าฟังก์ชัน

#include <stdio.h>

int sum_of_array(int values[], int n) // (1)!
{
    int sum = 0;
    for (int i = 0; i < n; i++) {
        sum += values[i];
    }
    return sum;
}

int main()
{
    int scores[1000];
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%d", &scores[i]);
    }
    printf("total = %d\n", sum_of_array(scores, n)); // (2)!
    printf("first three = %d\n", sum_of_array(scores, 3));
    return 0;
}
  1. พารามิเตอร์ที่เป็นอาเรย์เขียนวงเล็บเหลี่ยมว่างๆ ไม่ต้องบอกขนาด ส่วน n คือจำนวนช่องที่ใช้จริง
  2. ตอนเรียกใส่แค่ชื่ออาเรย์ ไม่มีวงเล็บเหลี่ยม
5
70 85 90 60 75
total = 380
first three = 245

ฟังก์ชันตัวเดียวใช้กับอาเรย์ขนาดไหนก็ได้ เพราะขนาดถูกส่งเข้ามาทาง n บรรทัดที่ 21 ส่ง 3 เข้าไปแทน n ฟังก์ชันก็รวมแค่สามช่องแรก

ต้องส่งความยาวไปด้วยเสมอ

ทำไมไม่ให้ฟังก์ชันนับขนาดเอง เพราะสิ่งที่ส่งเข้าไปจริงๆ ไม่ใช่ทั้งอาเรย์ แต่เป็นแค่ ตำแหน่งของช่องแรก ในหน่วยความจำ ฟังก์ชันรู้ว่าอาเรย์เริ่มตรงไหน แต่ไม่รู้ว่าจบตรงไหน เหมือนบอกเพื่อนว่าบ้านเราอยู่ต้นซอย แต่ไม่ได้บอกว่าซอยยาวกี่หลัง

ผลที่ตามมาคือ

  • ฟังก์ชันที่รับอาเรย์ต้องมีพารามิเตอร์บอกความยาวคู่กันเสมอ เกือบทุกตัวในหน้านี้จึงมี int n
  • sizeof ที่ใช้หาขนาดอาเรย์ได้ใน main ตามหน้า 6.3 ใช้ในฟังก์ชันไม่ได้ ดูแบบที่ 1 ในส่วนลองทำให้พัง
  • ถ้าส่ง n ผิด ฟังก์ชันจะเดินเลยขอบอาเรย์ไปโดยไม่มีใครห้าม ได้ผลแบบหน้า 5.5

รายละเอียดว่าตำแหน่งในหน่วยความจำคืออะไร และทำไมภาษา C ออกแบบมาแบบนี้ จะเรียนในบทที่ 10

ฟังก์ชันแก้อาเรย์ของคนเรียกได้

หน้า 8.2 เห็นแล้วว่าแก้พารามิเตอร์ int ไม่ทำให้ตัวแปรของ main เปลี่ยน อาเรย์ไม่เป็นแบบนั้น

#include <stdio.h>

void add_bonus_all(int scores[], int n, int bonus)
{
    for (int i = 0; i < n; i++) {
        scores[i] += bonus; // (1)!
    }
}

void print_array(int values[], int n)
{
    for (int i = 0; i < n; i++) {
        if (i > 0) {
            printf(" "); // (2)!
        }
        printf("%d", values[i]);
    }
    printf("\n");
}

int main()
{
    int scores[3] = {70, 85, 90};
    print_array(scores, 3);
    add_bonus_all(scores, 3, 5);
    print_array(scores, 3);
    return 0;
}
  1. แก้ช่องของอาเรย์ที่ส่งเข้ามา คือแก้ช่องของ scores ใน main ตัวจริง
  2. พิมพ์ช่องว่างก่อนทุกตัว ยกเว้นตัวแรก จะได้ไม่มีช่องว่างเกินท้ายบรรทัด grader บางระบบนับช่องว่างท้ายบรรทัดเป็นคำตอบผิด
70 85 90
75 90 95

คะแนนใน main เพิ่มขึ้นทุกช่อง เหตุผลมาจากหัวข้อที่แล้ว ฟังก์ชันได้ตำแหน่งของช่องแรกของอาเรย์ตัวจริง ไม่ได้ได้สำเนา scores[i] ในฟังก์ชันจึงเป็นกล่องเดียวกับ scores[i] ใน main

ภาษา C ไม่คัดลอกอาเรย์ เพราะอาเรย์อาจใหญ่เป็นล้านช่อง ถ้าต้องคัดลอกทุกครั้งที่เรียกฟังก์ชันจะทั้งช้าและเปลืองหน่วยความจำ แต่ต้องระวังด้านกลับด้วย ฟังก์ชันที่ตั้งใจแค่อ่านอาเรย์ ห้ามเผลอเขียนทับช่องไหน เพราะ main จะได้ข้อมูลที่ถูกเปลี่ยนไปแล้ว

ส่งอะไรฟังก์ชันได้อะไรแก้ในฟังก์ชันแล้วข้างนอกเปลี่ยนไหม
int double charสำเนาของค่าไม่เปลี่ยน
อาเรย์ตำแหน่งของช่องแรกของอาเรย์ตัวจริงเปลี่ยน
ช่องเดียวของอาเรย์ เช่น scores[2]สำเนาของค่าในช่องนั้นไม่เปลี่ยน

ชุดฟังก์ชันสำหรับอาเรย์

เพราะฟังก์ชันเขียนลงอาเรย์ได้ จึงแยกส่วนอ่านข้อมูลออกเป็นฟังก์ชันได้ด้วย ตัวอย่างนี้เปรียบเทียบคะแนนสองห้อง ใช้ฟังก์ชันชุดเดียวกับทั้งสองห้อง ห้องละครั้ง

#include <stdio.h>

int room_a[1000];
int room_b[1000];

void read_array(int values[], int n)
{
    for (int i = 0; i < n; i++) {
        scanf("%d", &values[i]);
    }
}

int max_of_array(int values[], int n)
{
    int best = values[0];
    for (int i = 1; i < n; i++) {
        if (values[i] > best) {
            best = values[i];
        }
    }
    return best;
}

int index_of_max(int values[], int n)
{
    int best_index = 0;
    for (int i = 1; i < n; i++) {
        if (values[i] > values[best_index]) {
            best_index = i;
        }
    }
    return best_index;
}

int main()
{
    int n, m;
    scanf("%d", &n);
    read_array(room_a, n); // (1)!
    scanf("%d", &m);
    read_array(room_b, m);
    printf("room A best %d at seat %d\n", max_of_array(room_a, n), index_of_max(room_a, n) + 1); // (2)!
    printf("room B best %d at seat %d\n", max_of_array(room_b, m), index_of_max(room_b, m) + 1);
    return 0;
}
  1. read_array เขียนค่าลงช่องของ room_a โดยตรง อ่านเสร็จแล้ว main ใช้ได้ทันที
  2. ฟังก์ชันคืนตำแหน่งแบบเริ่มที่ 0 ตามอาเรย์ แต่คนนับที่นั่งเริ่มที่ 1 จึงบวก 1 ตอนพิมพ์
5
62 88 75 91 70
4
80 95 95 60
room A best 91 at seat 4
room B best 95 at seat 2

สังเกตสามเรื่อง

  • max_of_array เริ่มจากช่องแรกแบบหน้า 5.2 ไม่ได้เริ่มจาก 0 จึงใช้กับคะแนนติดลบได้ด้วย ฟังก์ชันนี้จึงต้องถูกเรียกกับอาเรย์ที่มีอย่างน้อยหนึ่งช่องเสมอ
  • ห้อง B มี 95 สองคน index_of_max ใช้ > ไม่ใช่ >= จึงได้คนแรก ถ้าโจทย์อยากได้คนสุดท้ายให้เปลี่ยนเป็น >=
  • อาเรย์สองตัวประกาศไว้นอก main ตามนิสัยจากหน้า 5.5 ก็ยังส่งเข้าฟังก์ชันเป็นพารามิเตอร์ได้ตามปกติ การส่งเป็นพารามิเตอร์ทำให้ฟังก์ชันเดียวใช้ได้กับทั้ง room_a และ room_b

ฟังก์ชันอย่าง read_array max_of_array print_array เขียนครั้งเดียวแล้วใช้ได้แทบทุกโจทย์ หลายคนเก็บไว้เป็นชุดเครื่องมือของตัวเองแล้วคัดลอกมาใช้ตอนซ้อม

อาเรย์สองมิติต้องบอกจำนวนคอลัมน์

อาเรย์สองมิติส่งได้เหมือนกัน แต่วงเล็บเหลี่ยมคู่ที่สองต้องมีจำนวนคอลัมน์ และต้องเท่ากับตอนประกาศ

#include <stdio.h>

int field[100][100];

int row_sum(int grid[][100], int row, int cols) // (1)!
{
    int sum = 0;
    for (int c = 0; c < cols; c++) {
        sum += grid[row][c];
    }
    return sum;
}

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", &field[r][c]);
        }
    }
    int best_row = 0;
    for (int r = 1; r < rows; r++) {
        if (row_sum(field, r, cols) > row_sum(field, best_row, cols)) {
            best_row = r;
        }
    }
    printf("row %d harvest %d\n", best_row + 1, row_sum(field, best_row, cols));
    return 0;
}
  1. ช่องแรกว่างได้ แต่ช่องที่สองต้องเป็น 100 ให้ตรงกับ field[100][100] ข้างบน ส่วนจำนวนแถวและคอลัมน์ที่ใช้จริงส่งแยกเข้ามาเหมือนอาเรย์มิติเดียว
3 4
5 2 7 1
3 9 4 6
8 1 2 3
row 2 harvest 22

ต้องบอกจำนวนคอลัมน์เพราะในหน้า 7.1 เราเห็นแล้วว่าตารางในหน่วยความจำคือแถวยาวแถวเดียว ช่อง grid[row][c] อยู่ห่างจากช่องแรก row × 100 + c ช่อง ถ้าไม่รู้ว่าแถวหนึ่งยาว 100 ช่อง ก็คำนวณตำแหน่งนี้ไม่ได้ ส่วนจำนวนแถวไม่ต้องรู้ เหมือนอาเรย์มิติเดียวที่ไม่ต้องรู้ความยาว

ส่งสตริงไม่ต้องส่งความยาว

สตริงคืออาเรย์ของ char จึงส่งแบบเดียวกัน แต่มีข้อดีกว่าอาเรย์ตัวเลข คือมี '\0' บอกจุดจบอยู่แล้วตามหน้า 6.2 ฟังก์ชันจึงเดินไปจนเจอ '\0' ได้เอง ไม่ต้องส่งความยาว ตัวอย่างนี้ย้ายการตัด '\n' จากหน้า 6.3 มาเป็นฟังก์ชัน แล้วเพิ่มอีกสามตัว

#include <stdio.h>
#include <ctype.h>

char line[1005];
char reversed[1005];

void strip_newline(char text[])
{
    for (int i = 0; text[i] != '\0'; i++) {
        if (text[i] == '\n' || text[i] == '\r') {
            text[i] = '\0';
        }
    }
}

int count_vowels(char text[])
{
    int count = 0;
    for (int i = 0; text[i] != '\0'; i++) {
        char c = tolower(text[i]);
        if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
            count++;
        }
    }
    return count;
}

void make_upper(char text[])
{
    for (int i = 0; text[i] != '\0'; i++) {
        text[i] = toupper(text[i]);
    }
}

void reverse_into(char source[], char result[]) // (1)!
{
    int length = 0;
    while (source[length] != '\0') {
        length++;
    }
    for (int i = 0; i < length; i++) {
        result[i] = source[length - 1 - i];
    }
    result[length] = '\0'; // (2)!
}

int main()
{
    fgets(line, sizeof line, stdin);
    strip_newline(line);
    printf("vowels = %d\n", count_vowels(line));
    reverse_into(line, reversed);
    printf("reversed = %s\n", reversed);
    make_upper(line);
    printf("upper = %s\n", line);
    return 0;
}
  1. ฟังก์ชัน return อาเรย์ไม่ได้ ถ้าอยากได้สตริงใหม่ ให้คนเรียกเตรียมอาเรย์ว่างมาให้ แล้วฟังก์ชันเขียนผลลงไป แบบเดียวกับที่ strcpy ในหน้า 6.4 รับปลายทางเป็นพารามิเตอร์ตัวแรก
  2. สตริงที่สร้างเองต้องปิดด้วย '\0' เสมอ ไม่อย่างนั้น printf จะพิมพ์เลยไปจนกว่าจะบังเอิญเจอ '\0'
Hello Songkhla
vowels = 4
reversed = alhkgnoS olleH
upper = HELLO SONGKHLA

ต้องเรียก reverse_into ก่อน make_upper เพราะ make_upper แก้ line ตัวจริง ถ้าสลับลำดับ สตริงที่กลับหลังจะเป็นตัวพิมพ์ใหญ่ไปด้วย ฟังก์ชันที่แก้อาเรย์ได้ทำให้ลำดับการเรียกมีผลเสมอ

อีกเรื่องที่เปลี่ยนไปคือ sizeof line ใน fgets ใช้ได้เพราะ line ตรงนั้นคือตัวอาเรย์จริงที่ประกาศไว้ว่า char line[1005] ไม่ใช่พารามิเตอร์ sizeof จึงได้ 1005 ถ้าย้ายบรรทัด fgets ไปไว้ในฟังก์ชันที่รับ char text[] จะใช้ sizeof text ไม่ได้ด้วยเหตุผลเดียวกับหัวข้อความยาว ต้องส่งขนาดเข้าไปเป็นพารามิเตอร์

ลองทำให้พัง

แบบที่ 1 ใช้ sizeof หาความยาวในฟังก์ชัน

#include <stdio.h>

int sum_of_array(int scores[])
{
    int n = sizeof(scores) / sizeof(scores[0]);
    int sum = 0;
    for (int i = 0; i < n; i++) {
        sum += scores[i];
    }
    return sum;
}

int main()
{
    int scores[5] = {70, 85, 90, 60, 75};
    printf("in main: %d\n", (int)(sizeof(scores) / sizeof(scores[0])));
    printf("sum = %d\n", sum_of_array(scores));
    return 0;
}
size.c:5:19: warning: 'sizeof' on array function parameter 'scores' will return size of 'int *' [-Wsizeof-array-argument]
    5 |     int n = sizeof(scores) / sizeof(scores[0]);
      |                   ^
size.c:3:22: note: declared here
    3 | int sum_of_array(int scores[])
      |                  ~~~~^~~~~~~~
in main: 5
sum = 155

ใน main สูตรนี้ใช้ได้ sizeof(scores) คือขนาดของทั้งอาเรย์เป็น byte คือ 5 ช่อง × 4 byte = 20 หารด้วยขนาดของช่องเดียว 4 byte ได้ 5 ช่องถูกต้อง แต่ในฟังก์ชัน scores เป็นแค่ตำแหน่ง ไม่ใช่ทั้งอาเรย์ sizeof(scores) จึงเป็นขนาดของตำแหน่ง ซึ่งบนเครื่อง 64 bit คือ 8 byte หารด้วยขนาดของ int 4 byte ได้ 2 ผลรวมจึงมีแค่ 70 + 85 = 155 compiler เตือนไว้ตรงตัวว่า sizeof ของพารามิเตอร์ที่เป็นอาเรย์จะได้ขนาดของ int * ซึ่งเป็นชนิดของตำแหน่งที่จะเรียนในบทที่ 10 แก้ด้วยการส่ง n เข้าไป

แบบที่ 2 ส่งช่องเดียวแทนทั้งอาเรย์

ในโปรแกรม sum-array แก้บรรทัดที่ 20 ให้ส่ง scores[n] เพราะคิดว่าต้องบอกขนาดในวงเล็บเหลี่ยม

sum.c:20:47: error: passing argument 1 of 'sum_of_array' makes pointer from integer without a cast [-Wint-conversion]
   20 |     printf("total = %d\n", sum_of_array(scores[n], n));
      |                                         ~~~~~~^~~
      |                                               |
      |                                               int
sum.c:3:22: note: expected 'int *' but argument is of type 'int'
    3 | int sum_of_array(int values[], int n)
      |                  ~~~~^~~~~~~~

scores[n] คือค่าในช่องที่ n ช่องเดียว เป็น int ธรรมดา แต่ฟังก์ชันรอตำแหน่งของอาเรย์ (int *) ชนิดไม่ตรงกันจึงคอมไพล์ไม่ผ่าน ตอนเรียกให้ใส่ชื่ออาเรย์เปล่าๆ เท่านั้น

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

ในโปรแกรม field เปลี่ยนหัวฟังก์ชันเป็น int row_sum(int grid[][], int row, int cols)

field.c:5:17: error: array type has incomplete element type 'int[]'
    5 | int row_sum(int grid[][], int row, int cols)
      |                 ^~~~
field.c:5:17: note: declaration of 'grid' as multidimensional array must have bounds for all dimensions except the first
field.c:25:21: error: type of formal parameter 1 is incomplete
   25 |         if (row_sum(field, r, cols) > row_sum(field, best_row, cols)) {
      |                     ^~~~~
field.c:25:47: error: type of formal parameter 1 is incomplete
   25 |         if (row_sum(field, r, cols) > row_sum(field, best_row, cols)) {
      |                                               ^~~~~
field.c:29:57: error: type of formal parameter 1 is incomplete
   29 |     printf("row %d harvest %d\n", best_row + 1, row_sum(field, best_row, cols));
      |                                                         ^~~~~

บรรทัด note บอกกฎไว้ครบ อาเรย์หลายมิติต้องบอกขนาดทุกมิติ ยกเว้นมิติแรก error ที่ตามมาอีกสามจุดเกิดจากหัวฟังก์ชันพังตัวเดียว แก้ตัวแรกแล้วหายหมด

แบบที่ 4 จำนวนคอลัมน์ไม่ตรงกับตอนประกาศ

ในโปรแกรม field ประกาศตารางเป็น int field[100][50]; แต่หัวฟังก์ชันยังเป็น grid[][100]

field.c:25:21: error: passing argument 1 of 'row_sum' from incompatible pointer type [-Wincompatible-pointer-types]
   25 |         if (row_sum(field, r, cols) > row_sum(field, best_row, cols)) {
      |                     ^~~~~
      |                     |
      |                     int (*)[50]
field.c:5:17: note: expected 'int (*)[100]' but argument is of type 'int (*)[50]'
    5 | int row_sum(int grid[][100], int row, int cols)
      |             ~~~~^~~~~~~~~~~

error กับ note คู่นี้ขึ้นซ้ำอีกสองครั้งที่บรรทัด 25 และ 29 ตามจำนวนจุดที่เรียก row_sum ในที่นี้ตัดมาให้ดูคู่แรกคู่เดียว ถ้าภาษา C ยอมให้ผ่าน ฟังก์ชันจะคิดว่าแถวหนึ่งยาว 100 ช่อง แต่ตารางจริงยาว 50 ช่อง แถวที่ 1 ของฟังก์ชันจะไปตกที่แถวที่ 2 ของตารางจริง compiler จึงไม่ยอมตั้งแต่ต้น ชนิด int (*)[100] อ่านว่าตำแหน่งของแถวที่ยาว 100 ช่อง ไม่ต้องจำรูปแบบนี้ แค่ดูเลขในวงเล็บเหลี่ยมให้ตรงกัน วิธีที่ง่ายที่สุดคือประกาศตารางกับหัวฟังก์ชันด้วยขนาดเดียวกันตามขอบเขตโจทย์

สรุป

  • พารามิเตอร์อาเรย์เขียน int values[] ตอนเรียกใส่แค่ชื่ออาเรย์
  • ฟังก์ชันได้ตำแหน่งของช่องแรก ไม่รู้ความยาว ต้องส่ง n ไปด้วยทุกครั้ง และ sizeof ในฟังก์ชันใช้หาความยาวไม่ได้
  • อาเรย์ไม่ถูกคัดลอก ฟังก์ชันแก้ช่องไหน อาเรย์ของคนเรียกก็เปลี่ยนตรงนั้น
  • อาเรย์สองมิติต้องบอกจำนวนคอลัมน์ให้ตรงกับตอนประกาศ เช่น int grid[][100]
  • สตริงไม่ต้องส่งความยาว เดินจนเจอ '\0' ได้เอง สตริงที่สร้างใหม่ต้องปิดด้วย '\0'
  • return อาเรย์ไม่ได้ ให้คนเรียกส่งอาเรย์ปลายทางเข้ามาให้ฟังก์ชันเขียนผล