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
  • void function จะ 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 declaration warning, พฤติกรรมเสี่ยง
  • 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 ไปยังตัวแปร localchar *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