[0]zero-index clytie0x/zero-index

8.1 เขียนและเรียกฟังก์ชัน

น้องใช้ฟังก์ชันมาตั้งแต่บทที่ 0 แล้ว printf scanf strlen toupper ล้วนเป็นฟังก์ชัน และ main ที่เขียนทุกโปรแกรมก็เป็นฟังก์ชันตัวหนึ่ง ฟังก์ชัน (function) คือชุดคำสั่งที่ตั้งชื่อไว้ แล้วเรียกใช้ด้วยชื่อนั้นกี่ครั้งก็ได้ หน้านี้จะเขียนฟังก์ชันของตัวเอง ทั้งแบบที่สั่งให้ทำงานเฉยๆ แบบที่รับค่าเข้าไป และแบบที่คำนวณแล้วส่งคำตอบกลับมา

ทำไมต้องแบ่งโค้ดเป็นฟังก์ชัน

โปรแกรมพิมพ์ป้ายหน้าห้องสอบ มีเส้นคั่น = 20 ตัวสามเส้น เขียนด้วยสิ่งที่เรียนมาถึงบทที่ 7 ได้แบบนี้

#include <stdio.h>

int main()
{
    for (int i = 0; i < 20; i++) {
        printf("=");
    }
    printf("\n");
    printf("POSN CAMP 2026\n");
    for (int i = 0; i < 20; i++) {
        printf("=");
    }
    printf("\n");
    printf("Room 4\n");
    for (int i = 0; i < 20; i++) {
        printf("=");
    }
    printf("\n");
    return 0;
}
====================
POSN CAMP 2026
====================
Room 4
====================

ลูปพิมพ์เส้นเหมือนกันทุกตัวอักษรสามชุด ถ้าวันหนึ่งอยากเปลี่ยนเส้นให้ยาว 30 ตัว ต้องตามแก้สามที่ ลืมไปที่เดียวป้ายก็เบี้ยว ทีนี้ลองย้ายลูปไปไว้ในฟังก์ชันชื่อ print_line

#include <stdio.h>

void print_line() // (1)!
{
    for (int i = 0; i < 20; i++) {
        printf("=");
    }
    printf("\n");
}

int main()
{
    print_line(); // (2)!
    printf("POSN CAMP 2026\n");
    print_line();
    printf("Room 4\n");
    print_line();
    return 0;
}
  1. หัวของฟังก์ชัน void แปลว่าฟังก์ชันนี้ทำงานอย่างเดียว ไม่ส่งค่าอะไรกลับ print_line คือชื่อ ตั้งตามกฎเดียวกับชื่อตัวแปร วงเล็บว่างแปลว่าฟังก์ชันนี้ไม่ได้ใช้ค่าอะไรจากข้างนอก แต่ในภาษา C วงเล็บว่างไม่ได้ห้ามส่งค่าเข้าไป เรียก print_line(5) ก็ยังคอมไพล์ผ่านเงียบๆ ถ้าอยากบอกให้ชัดว่าไม่รับอะไรเลย ให้เขียน void print_line(void) แล้วการเรียกแบบนั้นจะได้ error too many arguments to function 'print_line' ทันที ส่วนในปีกกาข้างล่างคือคำสั่งที่จะทำทุกครั้งที่ถูกเรียก
  2. เรียก (call) ฟังก์ชันด้วยการเขียนชื่อตามด้วยวงเล็บ ต้องมีวงเล็บเสมอแม้ไม่มีอะไรข้างใน และปิดด้วย ; เหมือนคำสั่งอื่น
====================
POSN CAMP 2026
====================
Room 4
====================

ผลเหมือนเดิมทุกตัวอักษร แต่ได้สามอย่างเพิ่มมา

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

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

เรียกแล้วโปรแกรมไปไหน

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

#include <stdio.h>

void greet()
{
    printf("2 inside greet\n");
}

int main()
{
    printf("1 start main\n");
    greet();
    printf("3 back in main\n");
    greet();
    printf("4 end main\n");
    return 0;
}
1 start main
2 inside greet
3 back in main
2 inside greet
4 end main

บรรทัดที่ 11 เรียก greet โปรแกรมกระโดดไปทำคำสั่งในฟังก์ชันจนถึงปีกกาปิด แล้ว กลับมาที่จุดที่เรียก ทำบรรทัดถัดไปต่อ เรียกครั้งที่สองที่บรรทัดที่ 13 ก็กระโดดไปทำใหม่ทั้งหมดอีกรอบ เหมือนเปิดหนังสือไปอ่านภาคผนวกแล้วกลับมาหน้าเดิม ฟังก์ชันที่ไม่มีใครเรียกจะไม่ทำงานเลย ถึงจะเขียนไว้ในไฟล์ก็ตาม

ส่งค่าเข้าไปด้วยพารามิเตอร์

print_line พิมพ์ได้แต่ = 20 ตัว ถ้าอยากให้ฟังก์ชันเดียวพิมพ์ตัวอักษรอะไรก็ได้ กี่ตัวก็ได้ ให้ประกาศตัวแปรไว้ในวงเล็บของหัวฟังก์ชัน ตัวแปรพวกนี้เรียกว่า พารามิเตอร์ (parameter) ส่วนค่าที่ใส่ในวงเล็บตอนเรียก เรียกว่า อาร์กิวเมนต์ (argument)

ตัวอย่างนี้วาดพีระมิดแบบหน้า 4.4 แต่ย้ายลูปข้างในทั้งสองตัวไปไว้ในฟังก์ชัน

#include <stdio.h>

void print_row(char symbol, int count) // (1)!
{
    for (int i = 0; i < count; i++) {
        printf("%c", symbol);
    }
}

int main()
{
    int height;
    scanf("%d", &height);
    for (int row = 1; row <= height; row++) {
        print_row(' ', height - row); // (2)!
        print_row('*', 2 * row - 1);
        printf("\n");
    }
    return 0;
}
  1. พารามิเตอร์สองตัว คั่นด้วยจุลภาค แต่ละตัวต้องมีชนิดของตัวเอง เขียน char symbol, count ไม่ได้
  2. อาร์กิวเมนต์ใส่ตามลำดับ ตัวแรกเข้า symbol ตัวที่สองเข้า count อาร์กิวเมนต์เป็นนิพจน์อย่าง height - row ก็ได้ คำนวณเสร็จก่อนแล้วค่อยส่งค่าเข้าไป
4
   *
  ***
 *****
*******

ตอนเรียก print_row(' ', 3) โปรแกรมสร้างกล่อง symbol ใส่ ' ' และกล่อง count ใส่ 3 แล้วค่อยเริ่มทำคำสั่งในฟังก์ชัน พารามิเตอร์จึงใช้เหมือนตัวแปรธรรมดาที่มีค่าอยู่แล้ว เทียบกับลูปซ้อนในบทที่ 4 ตัว main ตอนนี้อ่านออกเสียงได้เลยว่า "แต่ละแถว พิมพ์ช่องว่าง พิมพ์ดาว ขึ้นบรรทัดใหม่" ไม่ต้องนั่งแกะว่าลูปข้างในตัวไหนทำอะไร

สังเกตว่าในฟังก์ชันไม่มี printf("\n") เพราะในแถวเดียวต้องเรียกสองครั้ง ถ้าขึ้นบรรทัดใหม่ในฟังก์ชัน ช่องว่างกับดาวจะไปอยู่คนละบรรทัด ก่อนเขียนฟังก์ชันให้คิดก่อนว่ามันรับผิดชอบงานแค่ไหน

คืนค่าด้วย return

ฟังก์ชันที่ผ่านมาทำงานแล้วจบ ไม่ได้ให้อะไรกลับมา แต่ฟังก์ชันที่ใช้บ่อยที่สุดคือแบบที่คำนวณแล้ว ส่งค่ากลับ (return) เหมือน strlen ที่ส่งความยาวกลับมาให้เก็บใส่ตัวแปร

#include <stdio.h>

int rectangle_area(int width, int height) // (1)!
{
    return width * height; // (2)!
}

int main()
{
    int w1, h1, w2, h2;
    scanf("%d %d %d %d", &w1, &h1, &w2, &h2);
    int first = rectangle_area(w1, h1); // (3)!
    printf("room 1 = %d\n", first);
    printf("room 2 = %d\n", rectangle_area(w2, h2));
    printf("total = %d\n", first + rectangle_area(w2, h2));
    return 0;
}
  1. ชนิดหน้าชื่อเปลี่ยนจาก void เป็น int แปลว่าฟังก์ชันนี้ส่งจำนวนเต็มกลับมาหนึ่งค่า
  2. return คำนวณค่าทางขวา ส่งกลับไปให้คนเรียก แล้วจบฟังก์ชันทันที
  3. ค่าที่ส่งกลับมาแทนที่ตรงที่เรียก เหมือน rectangle_area(w1, h1) กลายเป็นตัวเลขตัวหนึ่ง จะเก็บใส่ตัวแปร ส่งให้ printf หรือเอาไปบวกต่อก็ได้
8 6 5 4
room 1 = 48
room 2 = 20
total = 68

ฟังก์ชันเรียกฟังก์ชันอื่นได้ด้วย ตัวอย่างนี้เขียน max_of_two ก่อน แล้วสร้าง max_of_three จากมัน โดยหาตัวที่มากกว่าของสองตัวแรกก่อน แล้วเทียบกับตัวที่สาม

#include <stdio.h>

int max_of_two(int a, int b)
{
    if (a > b) {
        return a;
    }
    return b;
}

int max_of_three(int a, int b, int c)
{
    return max_of_two(max_of_two(a, b), c);
}

int main()
{
    int math, science, english;
    scanf("%d %d %d", &math, &science, &english);
    printf("best = %d\n", max_of_three(math, science, english));
    return 0;
}
72 91 85
best = 91

บรรทัดที่ 13 ทำจากข้างในออกมาข้างนอก max_of_two(72, 91) ได้ 91 ก่อน แล้วค่อย max_of_two(91, 85) ได้ 91 เทียบกับการหาค่ามากสุดของสามตัวในหน้า 3.2 ที่ต้องเขียน if ซ้อนกันหลายชั้น แบบนี้สั้นกว่าและผิดยากกว่า

ใน max_of_two ไม่ต้องมี else เพราะถ้า a > b จริง return a; จะจบฟังก์ชันไปแล้ว บรรทัด return b; ทำเฉพาะตอนที่เงื่อนไขเป็นเท็จเท่านั้น

return หลายจุด และ return ใน void

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

#include <stdio.h>

char grade_of(int score)
{
    if (score >= 80) {
        return 'A';
    }
    if (score >= 70) {
        return 'B';
    }
    if (score >= 60) {
        return 'C';
    }
    if (score >= 50) {
        return 'D';
    }
    return 'F'; // (1)!
}

void print_report(int score)
{
    if (score < 0 || score > 100) {
        printf("%d invalid\n", score);
        return; // (2)!
    }
    printf("%d %c\n", score, grade_of(score));
}

int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        int score;
        scanf("%d", &score);
        print_report(score);
    }
    return 0;
}
  1. มาถึงบรรทัดนี้ได้แปลว่าทุกเงื่อนไขข้างบนเป็นเท็จหมด คะแนนต่ำกว่า 50 แน่นอน ถ้าลืมบรรทัดนี้ ดูแบบที่ 4 ในส่วนลองทำให้พัง
  2. ออกจาก print_report ทันที บรรทัด printf ข้างล่างจึงไม่ทำ คะแนนที่ผิดจะไม่ถูกส่งไปตัดเกรด
5
85 73 49 120 60
85 A
73 B
49 F
120 invalid
60 C

ฟังก์ชัน grade_of ส่ง char กลับ ชนิดที่ส่งกลับได้ก็คือชนิดที่ประกาศตัวแปรได้ เช่น int long long double char ยกเว้นอาเรย์ ซึ่งจะเห็นวิธีอ้อมในหน้า 8.3

หัวฟังก์ชันรับอะไรส่งอะไรกลับเรียกแบบไหน
void print_line()ไม่รับไม่ส่งprint_line();
void print_row(char symbol, int count)ตัวอักษรกับจำนวนไม่ส่งprint_row('*', 5);
int rectangle_area(int width, int height)จำนวนเต็มสองตัวจำนวนเต็มint a = rectangle_area(8, 6);
char grade_of(int score)จำนวนเต็มตัวอักษรprintf("%c", grade_of(73));

แล้ว int main() ล่ะ ก็คือฟังก์ชันที่ส่ง int กลับเหมือนกัน คนที่เรียกมันคือระบบปฏิบัติการ return 0; คือบอกว่าทำงานจบเรียบร้อย ส่วนเลขแปลกๆ อย่าง -1073741676 ที่เห็นตอนหารด้วยศูนย์ในหน้า 1.2 คือค่าที่ระบบใส่ให้เองตอนโปรแกรมพังกลางทาง

ลำดับในไฟล์ และ prototype

compiler อ่านไฟล์จากบนลงล่างรอบเดียว ตอนเจอบรรทัดที่เรียกฟังก์ชัน มันต้องรู้จักฟังก์ชันนั้นแล้ว ว่ารับอะไรกี่ตัว ส่งอะไรกลับ ไม่อย่างนั้นจะตรวจไม่ได้ว่าเรียกถูกไหม ทุกตัวอย่างที่ผ่านมาจึงเขียนฟังก์ชันไว้ ก่อน main

อีกวิธีคือเขียนแค่หัวฟังก์ชันปิดด้วย ; ไว้ข้างบน เรียกว่า prototype (ต้นแบบ) เป็นการบอก compiler ล่วงหน้าว่ามีฟังก์ชันนี้ แล้วค่อยเขียนตัวเต็มไว้หลัง main

#include <stdio.h>

int rectangle_area(int width, int height); // (1)!
void print_line(int length);

int main()
{
    print_line(12);
    printf("area = %d\n", rectangle_area(8, 6));
    print_line(12);
    return 0;
}

int rectangle_area(int width, int height) // (2)!
{
    return width * height;
}

void print_line(int length)
{
    for (int i = 0; i < length; i++) {
        printf("-");
    }
    printf("\n");
}
  1. prototype คือหัวฟังก์ชันตัวเดียวกับข้างล่างทุกตัวอักษร แค่ปิดด้วย ; แทนปีกกา
  2. ตัวเต็มต้องมีชนิดที่ส่งกลับ ชื่อ และชนิดของพารามิเตอร์ตรงกับ prototype ไม่อย่างนั้นได้ error
------------
area = 48
------------

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

ที่จริงน้องใช้ prototype มาตั้งแต่บทที่ 0 แล้ว #include <stdio.h> คือการเอาไฟล์ที่มี prototype ของ printf scanf และเพื่อนๆ มาแปะไว้บนสุดของโปรแกรม compiler จึงรู้จักฟังก์ชันพวกนี้ก่อนเจอใน main

ลองทำให้พัง

แบบที่ 1 เรียกก่อนที่ compiler จะรู้จัก

ในโปรแกรม sign ย้ายฟังก์ชัน print_line ทั้งก้อนไปไว้หลัง main โดยไม่เขียน prototype

sign.c:5:5: error: implicit declaration of function 'print_line'; did you mean '_inline'? [-Wimplicit-function-declaration]
    5 |     print_line();
      |     ^~~~~~~~~~
      |     _inline
sign.c:13:6: warning: conflicting types for 'print_line'; have 'void()'
   13 | void print_line()
      |      ^~~~~~~~~~
sign.c:5:5: note: previous implicit declaration of 'print_line' with type 'void()'
    5 |     print_line();
      |     ^~~~~~~~~~

implicit declaration แปลว่า compiler เจอชื่อ print_line ทั้งที่ยังไม่เคยมีใครประกาศ error เกิดที่บรรทัดแรกที่เรียก คำแนะนำ did you mean '_inline' เป็นการเดาชื่อที่สะกดคล้ายกันจากคำในระบบ ซึ่งคราวนี้เดาผิด ไม่ต้องทำตาม ส่วน warning ข้างล่างเกิดตอนมันเจอตัวจริงทีหลัง แล้วพบว่าเป็นชื่อที่เคยเดาไว้ก่อนหน้า แก้ได้สองทาง ย้ายฟังก์ชันกลับขึ้นไปไว้ก่อน main หรือเติม prototype ไว้ข้างบน

แบบที่ 2 ส่งอาร์กิวเมนต์ไม่ครบ

ในโปรแกรม rooms แก้บรรทัดที่ 12 เป็น int first = rectangle_area(w1);

rooms.c:12:17: error: too few arguments to function 'rectangle_area'
   12 |     int first = rectangle_area(w1);
      |                 ^~~~~~~~~~~~~~
rooms.c:3:5: note: declared here
    3 | int rectangle_area(int width, int height)
      |     ^~~~~~~~~~~~~~

ฟังก์ชันรับสองค่า แต่ส่งไปค่าเดียว compiler บอกว่า too few arguments และบรรทัด note ชี้กลับไปที่หัวฟังก์ชันให้ดูว่าต้องส่งอะไรบ้าง ส่งเกินก็ได้ error คล้ายกันคือ too many arguments

แบบที่ 3 เอาค่าจากฟังก์ชัน void

ฟังก์ชัน print_line(int length) ในโปรแกรม proto ไม่ส่งอะไรกลับ แต่เผลอเขียนเหมือนมันส่งค่ากลับมา

int width = print_line(10);
line.c:13:17: error: void value not ignored as it ought to be
   13 |     int width = print_line(10);
      |                 ^~~~~~~~~~

อ่านตรงตัวว่า "ค่าของ void ไม่ได้ถูกทิ้งไปอย่างที่ควรเป็น" คือฟังก์ชัน void ไม่มีค่าให้เก็บ ถ้าอยากได้ค่ากลับมา ต้องเปลี่ยนชนิดหน้าชื่อฟังก์ชันและเขียน return ส่งค่ากลับ

แบบที่ 4 ลืม return บางทาง

ในโปรแกรม report ลบบรรทัด return 'F'; ออก เหลือแค่สี่ if

report.c:17:1: warning: control reaches end of non-void function [-Wreturn-type]
   17 | }
      | ^

control reaches end of non-void function แปลว่ามีทางที่โปรแกรมเดินมาถึงปีกกาปิดของฟังก์ชันที่ต้องส่งค่ากลับ โดยไม่เจอ return เลย ในที่นี้คือตอนคะแนนต่ำกว่า 50 ซึ่งไม่ตรงกับ if ไหน เป็นแค่ warning จึงรันได้ แต่ค่าที่ได้กลับมาเป็นค่าขยะ บนเครื่องที่เขียนเอกสารนี้ ป้อน 1 กับ 30 แล้วได้ 30 ตามด้วยอักขระรหัส 30 ซึ่งเป็นอักขระควบคุมที่มองไม่เห็น แทนที่จะเป็น F ค่าขยะที่ได้บังเอิญเท่ากับคะแนนที่ส่งเข้าไป เครื่องอื่นหรือ compiler อื่นอาจได้ค่าอื่น

เวลาเขียนฟังก์ชันที่มีหลาย return ให้ไล่ดูทุกทางว่าจบด้วย return ครบไหม วิธีที่ปลอดภัยคือให้บรรทัดสุดท้ายก่อนปีกกาปิดเป็น return เสมอ

แบบที่ 5 มี prototype แต่ไม่มีตัวจริง

ในโปรแกรม proto ลบตัวเต็มของ rectangle_area ข้างล่างออก เหลือแค่ prototype ข้างบน

ld.exe: proto.o:proto.c:(.text+0x22): undefined reference to `rectangle_area'
collect2.exe: error: ld returned 1 exit status

หน้าตาไม่เหมือน error ที่ผ่านมา เพราะไม่ได้มาจาก compiler แต่มาจาก linker (ld) โปรแกรมที่ประกอบชิ้นส่วนเป็นไฟล์ .exe เป็นขั้นสุดท้าย compiler ผ่านแล้วเพราะ prototype สัญญาไว้ว่ามีฟังก์ชันนี้ แต่ตอนประกอบหาตัวจริงไม่เจอ จึงขึ้น undefined reference ถ้าเจอข้อความนี้ ให้เช็กว่าเขียนตัวเต็มแล้วหรือยัง และสะกดชื่อตรงกับ prototype ไหม

สรุป

  • ฟังก์ชันคือชุดคำสั่งที่ตั้งชื่อไว้ เรียกด้วย ชื่อ(อาร์กิวเมนต์) ทำเสร็จแล้วกลับมาทำต่อที่จุดที่เรียก
  • หัวฟังก์ชันบอกชนิดที่ส่งกลับ ชื่อ และพารามิเตอร์ void แปลว่าไม่ส่งอะไรกลับ
  • อาร์กิวเมนต์เข้าพารามิเตอร์ตามลำดับ จำนวนต้องตรงกัน
  • return ค่า; ส่งค่ากลับและจบฟังก์ชันทันที ฟังก์ชันที่ไม่ใช่ void ต้องจบด้วย return ทุกทาง
  • ต้องประกาศก่อนเรียก เขียนฟังก์ชันไว้ก่อน main หรือใส่ prototype ไว้ข้างบน
  • ฟังก์ชันที่ดีทำงานเดียว ตั้งชื่อบอกว่าทำอะไร และทำให้ main อ่านเป็นลำดับขั้นตอน