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;
}
- พารามิเตอร์ที่เป็นอาเรย์เขียนวงเล็บเหลี่ยมว่างๆ ไม่ต้องบอกขนาด ส่วน
nคือจำนวนช่องที่ใช้จริง - ตอนเรียกใส่แค่ชื่ออาเรย์ ไม่มีวงเล็บเหลี่ยม
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;
}
- แก้ช่องของอาเรย์ที่ส่งเข้ามา คือแก้ช่องของ
scoresในmainตัวจริง - พิมพ์ช่องว่างก่อนทุกตัว ยกเว้นตัวแรก จะได้ไม่มีช่องว่างเกินท้ายบรรทัด 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;
}
read_arrayเขียนค่าลงช่องของroom_aโดยตรง อ่านเสร็จแล้วmainใช้ได้ทันที- ฟังก์ชันคืนตำแหน่งแบบเริ่มที่ 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;
}
- ช่องแรกว่างได้ แต่ช่องที่สองต้องเป็น 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;
}
- ฟังก์ชัน
returnอาเรย์ไม่ได้ ถ้าอยากได้สตริงใหม่ ให้คนเรียกเตรียมอาเรย์ว่างมาให้ แล้วฟังก์ชันเขียนผลลงไป แบบเดียวกับที่strcpyในหน้า 6.4 รับปลายทางเป็นพารามิเตอร์ตัวแรก - สตริงที่สร้างเองต้องปิดด้วย
'\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อาเรย์ไม่ได้ ให้คนเรียกส่งอาเรย์ปลายทางเข้ามาให้ฟังก์ชันเขียนผล