首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C >> C 語言入門

C 語言核心篇:函數與遞歸完整指南

     編輯:C 語言入門

前言

函數是 C 語言模組化程式設計的基石,而遞歸則是解決複雜問題的利器。本文將透過七個經典案例,系統講解函數定義、聲明、參數傳遞、程式終止控制,以及遞歸與遞推的實戰應用。

一、函數的定義與返回值

核心概念

函數定義包含返回類型、函數名、參數列表與函數體。透過 return 將運算結果傳回呼叫端。

實作範例:基本類型返回

#include <stdio.h>

int getInt() {
    return 10;
}

double getDouble() {
    return 5.5;
}

int main() {
    int n = getInt();
    printf("%d\n", n);

    double d = getDouble();
    printf("%lf\n", d);
    return 0;
}

解析:getInt 回傳整數 10,getDouble 回傳浮點數 5.5。主函數透過變數接收返回值並列印。

二、函數聲明:隱式 vs 顯式

關鍵差異

  • 隱式聲明:呼叫前未聲明,編譯器假設返回 int,參數不檢查(C99 已廢棄)
  • 顯式聲明:預先宣告原型,參數可省略或用 void 明確表示無參數

程式碼示範

#include <stdio.h>

int main() {
    f1();                    // 隱式聲明
    double f2(/*void*//*int i*/); // 顯式聲明,參數註解示意
    f2(1, 2);
    return 0;
}

int f1() {
    printf("f1()\n");
    return 0;
}

double f2(int a, int b) {
    printf("f2()\n");
    return 3.0;
}

注意:f2 宣告時參數可寫 void(不接受參數)或留空(接受任意參數),定義時需一致。

三、參數傳遞:值傳遞 vs 陣列傳遞

核心原理

  • 值傳遞:形參是實參副本,函數內修改不影響外部
  • 陣列傳遞:實際傳遞首元素地址,函數內修改會影響原陣列

對比實驗

#include <stdio.h>

void fun1(int a) {           // 值傳遞
    printf("fun1 入參 a=%d\n", a);
    a += 10;
    printf("fun1 修改後 a=%d\n", a);
}

void fun2(int array[]) {     // 陣列傳遞(實為指標)
    printf("fun2 入參 array[0]=%d\n", array[0]);
    array[0] += 10;
    printf("fun2 修改後 array[0]=%d\n", array[0]);
}

int main() {
    int a = 5;
    int arr[10] = {1};

    printf("main 呼叫前 a=%d\n", a);
    fun1(a);
    printf("main 呼叫後 a=%d\n\n", a);  // 未變

    printf("main 呼叫前 arr[0]=%d\n", arr[0]);
    fun2(arr);
    printf("main 呼叫後 arr[0]=%d\n", arr[0]); // 已變
    return 0;
}

四、函數返回與程式終止

return vs exit()

  • return:僅結束當前函數,控制權回呼叫端
  • exit():立即終止整個程式,執行清理再退出

互動測試

#include <stdio.h>
#include <stdlib.h>

void fun(int e) {
    if (1 == e)
        exit(EXIT_SUCCESS);  // 直接結束程式
    else
        return;              // 僅返回主函數
}

int main() {
    int n;
    printf("輸入 1 呼叫 exit(),輸入 2 執行 return:");
    scanf("%d", &n);
    fun(n);
    printf("只有選擇 2 才會看到這行\n");
    return 0;
}

五、遞歸實戰:階乘計算

遞歸版本

int factorial(int n) {
    if (1 == n) return 1;        // 遞歸出口
    return n * factorial(n - 1); // 遞歸步進
}

遞推版本

int f = 1;
for (int i = 2; i <= n; i++)
    f *= i;

完整對比程式

#include <stdio.h>

int factorial(int n) {
    if (1 == n) return 1;
    return n * factorial(n - 1);
}

int main() {
    int n;
    printf("請輸入求階乘的數:");
    scanf("%d", &n);

    // 遞推
    int f = 1;
    for (int i = 2; i <= n; i++) f *= i;
    printf("遞推法:%d! = %d\n", n, f);

    // 遞歸
    f = factorial(n);
    printf("遞歸法:%d! = %d\n", n, f);
    return 0;
}

六、遞歸 vs 遞推效能對決:費氏數列

兩種實作

// 遞推 O(n)
long fib(int n) {
    long f1 = 1, f2 = 1, fn = 1;
    for (int i = 2; i < n; i++) {
        fn = f1 + f2;
        f1 = f2;
        f2 = fn;
    }
    return fn;
}

// 遞歸 O(2^n) - 指數級爆炸
long fib_f(int n) {
    if (n == 1 || n == 2) return 1;
    return fib_f(n - 1) + fib_f(n - 2);
}

計時測試

#include <time.h>

int main() {
    int n;
    printf("請輸入月份:");
    scanf("%d", &n);

    time_t begin = time(0);
    long sum = fib(n);
    time_t stop = time(0);
    printf("遞推:第 %d 月兔子數 %ld,耗時 %lf 秒\n", n, sum, difftime(stop, begin));

    begin = time(0);
    sum = fib_f(n);
    stop = time(0);
    printf("遞歸:第 %d 月兔子數 %ld,耗時 %lf 秒\n", n, sum, difftime(stop, begin));
    return 0;
}

結論:遞歸程式碼簡潔但效能差,遞推適合大規模計算。實務上可用備忘錄或動態規劃優化遞歸。

七、經典遞歸應用:漢諾塔

問題轉化

將 n 個盤子從 A 柱借助 B 柱移至 C 柱,規則:一次移一個、小盤在大盤上。

遞歸策略

  1. 將 n-1 個盤子從 A 借助 C 移至 B
  2. 將第 n 個盤子從 A 移至 C
  3. 將 n-1 個盤子從 B 借助 A 移至 C

程式碼實現

#include <stdio.h>

void move(int n, char a, char b, char c) {
    if (n == 1) {
        printf("\t%c->%c\n", a, c);
    } else {
        move(n - 1, a, c, b);  // n-1: A->B
        printf("\t%c->%c\n", a, c); // 第 n 個: A->C
        move(n - 1, b, a, c);  // n-1: B->C
    }
}

int main() {
    int n;
    printf("請輸入盤子數:");
    scanf("%d", &n);
    move(n, 'A', 'B', 'C');
    return 0;
}

標準函數庫精選

1. 格式化輸入輸出

#include <stdio.h>

int main() {
    int a, b;
    scanf("%*d%d", &a, &b);      // %*d 跳過第一個整數
    printf("a=%d, b=%d\n", a, b);

    printf("%10d,%-10d,%d\n", 100, 200, 300); // 寬度、靠左/右
    printf("%08d\n", 100);        // 前導補零
    printf("%0*d\n", 8, 100);     // 動態寬度補零
    printf("0%o,0x%x\n", 8, 16);  // 八/十六進制
    printf("%.2f\n", 12.34567);   // 保留兩位小數
    printf("%10.2f\n", 12.34567); // 寬度 10 含小數點
    return 0;
}

2. 動態記憶體管理

基本類型

#include <stdlib.h>

int *pi = malloc(sizeof(int));
*pi = 10;
printf("%d\n", *pi);
free(pi);  // 必須釋放

陣列與結構體

// 動態陣列
int *arr = malloc(sizeof(int) * 10);
for (int i = 0; i < 10; i++) arr[i] = i + 1;
free(arr);

// 結構體
struct Data { int a; double b; char c; };
struct Data *p = malloc(sizeof(struct Data));
p->a = 10; p->b = 12.34; p->c = 'a';
free(p);

3. 時間函數

#include <time.h>

int main() {
    time_t t = time(0);
    struct tm *local = localtime(&t);
    printf("%d年%d月%d日 %d:%d:%d\n",
           1900 + local->tm_year,
           local->tm_mon + 1,
           local->tm_mday,
           local->tm_hour, local->tm_min, local->tm_sec);
    return 0;
}

檔案 I/O 完整指南

1. 檔案開啟模式

模式說明
r唯讀,檔案必須存在
w唯寫,存在則清空,不存在則建立
a追加寫入,不存在則建立
w+讀寫,同 w

2. 格式化讀寫:fprintf/fscanf

FILE *fp = fopen("data.txt", "w");
fprintf(fp, "%c %d %f %lf", 'a', 10, 12.34f, 12345.6789);
fclose(fp);

fp = fopen("data.txt", "r");
char c; int i; float f; double d;
fscanf(fp, "%c%d%f%lf", &c, &i, &f, &d);
printf("%c\n%d\n%f\n%lf\n", c, i, f, d);
fclose(fp);

3. 二進制讀寫:fwrite/fread

// 寫入
fwrite(&a, sizeof(int), 1, fp);
fwrite(&d, sizeof(double), 1, fp);
int len = strlen(str) + 1;
fwrite(&len, sizeof(int), 1, fp);  // 先寫長度
fwrite(str, sizeof(char), len, fp); // 再寫內容

// 讀取
fread(&a1, sizeof(int), 1, fp);
fread(&d1, sizeof(double), 1, fp);
fread(&len1, sizeof(int), 1, fp);
fread(str1, sizeof(char), len1, fp);

4. 結構體直接讀寫

struct Student { int id; char name[20]; char addr[200]; };
struct Student stu = {10000, "zhangsan", "江蘇南京"};
fwrite(&stu, sizeof(struct Student), 1, fp); // 一次寫入整個結構

5. 隨機存取:fseek/ftell


// SEEK_SET: 檔案開頭, SEEK_CUR: 目前位置, SEEK_END: 檔案結尾
fseek(fp, 0, SEEK_SET);      // 移至開頭
fseek(fp, 5, SEEK_CUR);      // 向後 5 位元組
fseek(fp, -10, SEEK_END);    // 從結尾向前 10 位元組

// 取得檔案大小
fseek(fp, 0, SEEK_END);
long len = ftell(fp);
printf("檔案大小:%ld bytes\n", len);

6. 字元/字串拷貝檔案

FILE *src = fopen("file.c", "r");
FILE *dst = fopen("file副本.c", "w");
char ch;
while ((ch = getc(src)) != EOF)
    putc(ch, dst);
fclose(src); fclose(dst);

7. 字串格式化:sprintf/sscanf

char buf[] = "abcdefghijklmnopqrstuvwxyz";
fprintf(fp, "%lu%s", strlen(buf), buf); // 寫入長度+內容

// 讀取
int len; fscanf(fp, "%d", &len);
char *buf1 = malloc(len + 1);
fscanf(fp, "%s", buf1);
buf1[len] = '\0';

8. 可變參數函數

#include <stdarg.h>

int max(int cnt, ...) {
    va_list ap;
    va_start(ap, cnt);
    int maxv = va_arg(ap, int);
    for (int i = 1; i < cnt; i++) {
        int v = va_arg(ap, int);
        if (v > maxv) maxv = v;
    }
    va_end(ap);
    return maxv;
}

// 呼叫
max(10, 55, 34, 28, 16, 47, 62, 77, 86, 91, 9); // 返回 91

總結

  • 函數是模組化核心,掌握聲明、定義、參數傳遞三要素
  • return 與 exit() 語意不同,勿混用
  • 遞歸需明確出口與步進,注意效能陷阱
  • 標準庫提供豐富 I/O、記憶體、時間操作,善用之
  • 檔案操作務必配對 fopen/fclose,二進制讀寫注意位元組序

熟練這些基礎,即可應對絕大多數 C 語言工程實戰。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved