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;
}
- หัวของฟังก์ชัน
voidแปลว่าฟังก์ชันนี้ทำงานอย่างเดียว ไม่ส่งค่าอะไรกลับprint_lineคือชื่อ ตั้งตามกฎเดียวกับชื่อตัวแปร วงเล็บว่างแปลว่าฟังก์ชันนี้ไม่ได้ใช้ค่าอะไรจากข้างนอก แต่ในภาษา C วงเล็บว่างไม่ได้ห้ามส่งค่าเข้าไป เรียกprint_line(5)ก็ยังคอมไพล์ผ่านเงียบๆ ถ้าอยากบอกให้ชัดว่าไม่รับอะไรเลย ให้เขียนvoid print_line(void)แล้วการเรียกแบบนั้นจะได้ errortoo many arguments to function 'print_line'ทันที ส่วนในปีกกาข้างล่างคือคำสั่งที่จะทำทุกครั้งที่ถูกเรียก - เรียก (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;
}
- พารามิเตอร์สองตัว คั่นด้วยจุลภาค แต่ละตัวต้องมีชนิดของตัวเอง เขียน
char symbol, countไม่ได้ - อาร์กิวเมนต์ใส่ตามลำดับ ตัวแรกเข้า
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;
}
- ชนิดหน้าชื่อเปลี่ยนจาก
voidเป็นintแปลว่าฟังก์ชันนี้ส่งจำนวนเต็มกลับมาหนึ่งค่า returnคำนวณค่าทางขวา ส่งกลับไปให้คนเรียก แล้วจบฟังก์ชันทันที- ค่าที่ส่งกลับมาแทนที่ตรงที่เรียก เหมือน
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;
}
- มาถึงบรรทัดนี้ได้แปลว่าทุกเงื่อนไขข้างบนเป็นเท็จหมด คะแนนต่ำกว่า 50 แน่นอน ถ้าลืมบรรทัดนี้ ดูแบบที่ 4 ในส่วนลองทำให้พัง
- ออกจาก
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");
}
- prototype คือหัวฟังก์ชันตัวเดียวกับข้างล่างทุกตัวอักษร แค่ปิดด้วย
;แทนปีกกา - ตัวเต็มต้องมีชนิดที่ส่งกลับ ชื่อ และชนิดของพารามิเตอร์ตรงกับ 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อ่านเป็นลำดับขั้นตอน