C ฟังก์ชัน
นิยามฟังก์ชัน
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int main(void)
{
int sum = add(3, 4);
printf("%d\n", sum); // 7
return 0;
}intข้างหน้า = ชนิดของค่าที่คืนadd= ชื่อ(int a, int b)= พารามิเตอร์พร้อมชนิด- ฟังก์ชันที่ไม่คืนค่าใช้
void:
void greet(const char *name)
{
printf("สวัสดี %s\n", name);
// ไม่มี return หรือ return; เปล่า ๆ
}ฟังก์ชันที่ไม่รับพารามิเตอร์ใช้ void ในวงเล็บ (ไม่ใช่วงเล็บว่าง):
int get_answer(void) { return 42; } // ชัดเจน: รับพารามิเตอร์ศูนย์ตัว
int get_answer(); // "declaration แบบไม่มี prototype" — ไม่บอกจำนวน/ชนิดพารามิเตอร์ใน C17
int f();เป็น *declaration ที่ไม่ให้ prototype* — เรียกด้วย argument กี่ตัวก็ไม่ถูกตรวจ (เสี่ยง) ส่วนint f() { ... }ที่เป็น *definition* กำหนดว่ามีพารามิเตอร์ศูนย์ตัว C23 เปลี่ยนให้()มีความหมายเท่ากับ(void)— เขียน(void)ให้ชัดไว้เสมอ ใช้ได้ทุกมาตรฐาน
ต้องประกาศก่อนใช้ — prototype
คอมไพเลอร์อ่านไฟล์จากบนลงล่าง ถ้า main เรียก add ที่นิยามอยู่ ข้างล่าง จะไม่รู้จัก — ต้องมี prototype (การประกาศลายเซ็น) ไว้ข้างบนก่อน:
#include <stdio.h>
int add(int a, int b); // prototype — บอกว่ามีฟังก์ชันนี้ รับ/คืนอะไร (ลงท้ายด้วย ;)
int main(void)
{
printf("%d\n", add(3, 4)); // ใช้ได้ เพราะรู้จัก prototype แล้ว
return 0;
}
int add(int a, int b) // definition — โค้ดจริง (ไม่มี ; หลังวงเล็บ)
{
return a + b;
}ถ้าไม่มี prototype แล้วเรียกก่อนนิยาม:
warning: implicit declaration of function 'add'C89 จะสมมติว่าคืน int และรับ argument ตามที่ให้ — เสี่ยงมาก C99+ ยังเตือน ทางที่ถูกคือใส่ prototype (หรือย้าย definition มาไว้ก่อน main)
ในโปรเจกต์จริง prototype อยู่ในไฟล์ .h (บทที่ 26)
pass by value — ส่งสำเนาเสมอ
C คัดลอก ค่าของ argument เข้าไปในพารามิเตอร์ ฟังก์ชันแก้พารามิเตอร์ไม่กระทบตัวแปรเดิม:
#include <stdio.h>
void try_to_double(int x)
{
x = x * 2; // แก้แค่สำเนา
printf("ในฟังก์ชัน: %d\n", x);
}
int main(void)
{
int n = 5;
try_to_double(n);
printf("ใน main : %d\n", n); // ยังเป็น 5!
return 0;
}ในฟังก์ชัน: 10
ใน main : 5ถ้าอยากให้ฟังก์ชันแก้ตัวแปรของผู้เรียกจริง ๆ ต้องส่ง pointer ไปที่ตัวแปรนั้น (บทที่ 13) — นั่นคือเหตุผลที่ scanf("%d", &n) มี &
อาร์เรย์ไม่ใช่ข้อยกเว้นจริง ๆ — ที่ส่งเข้าฟังก์ชันคือ pointer ไปสมาชิกตัวแรก (อาร์เรย์ “decay” เป็น pointer) และ pointer นั้นก็ถูก *คัดลอก* แบบ pass by value เหมือนกัน เพียงแต่มันชี้ไปที่อาร์เรย์ของผู้เรียก ฟังก์ชันจึงแก้ *เนื้อ* อาร์เรย์ได้ (แต่แก้ให้ pointer ของผู้เรียกชี้ที่อื่นไม่ได้) — บทที่ 9
return
int max(int a, int b)
{
if (a > b) {
return a; // ออกจากฟังก์ชันทันที
}
return b;
}
int classify(int n)
{
if (n < 0) return -1;
if (n == 0) return 0;
return 1;
}- ฟังก์ชันที่ประกาศคืน
intต้องreturnค่าทุกเส้นทาง — ถ้าตกถึงท้ายฟังก์ชันโดยไม่returnแล้วผู้เรียก *เอาค่าไปใช้* เป็น undefined behavior (ไม่ใช่แค่ “ได้ค่าขยะ”)-Wallเตือนcontrol reaches end of non-void function voidfunction จะreturn;เปล่า ๆ เพื่อออกก่อนได้ แต่ห้ามreturn value;- คืนได้ค่าเดียว — อยากคืนหลายค่าใช้ pointer parameter (out-parameter) หรือ struct (บทที่ 17)
recursion — ฟังก์ชันเรียกตัวเอง
#include <stdio.h>
// domain: n >= 0 และผลลัพธ์ต้องไม่เกิน LONG_MAX (long 64 บิต → n ถึง 20)
// n ติดลบจะเข้า base case ทันที (คืน 1) ซึ่งไม่ใช่นิยามของแฟกทอเรียล — ผู้เรียกต้องกันเอง
long factorial(int n)
{
if (n <= 1) {
return 1; // base case — หยุด (ครอบ n = 0 และ n = 1)
}
return n * factorial(n - 1); // recursive case
}
int main(void)
{
printf("%ld\n", factorial(5)); // 120
return 0;
}ทุก recursion ต้องมี base case ที่หยุด ไม่งั้น stack overflow (เรียกตัวเองไม่จบจน stack เต็ม → crash)
การเรียกฟังก์ชันแต่ละครั้งกินพื้นที่ stack — recursion ลึกมาก ๆ (เป็นแสน) จะ overflow บางกรณีเขียนเป็น loop ดีกว่า
ตัวอย่าง: ฟังก์ชันหลายตัวทำงานร่วมกัน
#include <stdio.h>
int is_prime(int n)
{
if (n < 2) return 0;
for (int i = 2; i <= n / i; i++) { // i <= n/i แทน i*i <= n — i*i overflow ที่ i≈46341
if (n % i == 0) return 0;
}
return 1;
}
void print_primes_up_to(int limit)
{
for (int n = 2; n <= limit; n++) {
if (is_prime(n)) {
printf("%d ", n);
}
if (n == limit) break; // กัน n++ ล้นช่วง int เมื่อ limit == INT_MAX
}
printf("\n");
}
int main(void)
{
print_primes_up_to(30); // 2 3 5 7 11 13 17 19 23 29
return 0;
}เรื่องที่คนมักพลาด
- ไม่มี prototype แล้วเรียกฟังก์ชันที่นิยามอยู่ข้างล่าง —
implicit declarationwarning, พฤติกรรมเสี่ยง int foo();แทนint foo(void);— ใน C17()ในการ *ประกาศ* = ไม่ให้ prototype (ไม่ตรวจ argument) เขียน(void)ชัดเสมอ- ลืม
returnบางเส้นทางในฟังก์ชันที่คืนค่า — ผู้เรียกเอาค่าไปใช้ = UB (-Wallเตือน) - **
i * i <= n/n * nในฟังก์ชันคณิต** — overflow แม้ input เป็นintที่ถูกต้อง ใช้i <= n / iหรือชนิดกว้างกว่า และระบุ domain ให้ชัด (factorial:0 <= n <= 20,is_prime: ทุกintบวก) - คาดว่าฟังก์ชันแก้ตัวแปรของผู้เรียก — ไม่ได้ ทุกอย่าง pass by value ต้องส่ง pointer (บทที่ 13)
- recursion ไม่มี base case หรือ base case ไม่มีทางถึง — stack overflow
- คืน pointer ไปยังตัวแปร local —
char *f() { char buf[10]; return buf; }—bufหายเมื่อฟังก์ชันจบ ผู้เรียกได้ dangling pointer (บทที่ 14, 15) - ประกาศ prototype ไม่ตรงกับ definition — argument/return ต่างกัน = UB
สรุป
- นิยาม:
type name(params) { ... }; prototype:type name(params);(มี;) — ต้องประกาศก่อนใช้ void= ไม่คืนค่า / ไม่รับพารามิเตอร์ (ใช้(void)ในวงเล็บให้ชัด)- pass by value เสมอ — พารามิเตอร์เป็นสำเนา แก้ไม่กระทบตัวเดิม; อาร์เรย์ก็ไม่ใช่ข้อยกเว้น (ส่ง pointer ที่ decay มา ซึ่งถูกคัดลอกเหมือนกัน — แต่มันชี้ที่อาร์เรย์เดิม เลยแก้เนื้อได้)
- ฟังก์ชันคืนค่าต้อง
returnทุกเส้นทาง, คืนได้ค่าเดียว - recursion ต้องมี base case; ระวัง stack overflow และการคืน pointer ไป local variable
จบเฟส 1 บทถัดไปเข้าเฟส 2 ที่เป็นหัวใจของ C เริ่มที่อาร์เรย์ — พื้นฐานก่อนเข้าเรื่อง pointer