「自定義函數之間如何調用?形參實參有什麼注意點?」這是C語言模組化程式設計的核心技能。本文從調用機制、參數傳遞語義、作用域規則、聲明與定義分離、遞歸與相互遞歸、工程化最佳實踐六個維度,系統講解函數調用全景。
調用者棧幀 被調用者棧幀 (callee)
┌─────────────────────┐ ┌─────────────────────┐
│ 返回地址 (RA) │ ←──── │ (由call指令壓入) │
├─────────────────────┤ ├─────────────────────┤
│ 實參3 (右到左壓棧) │ │ 形參3 (副本/引用) │
│ 實參2 │ │ 形參2 │
│ 實參1 │ │ 形參1 │
├─────────────────────┤ ├─────────────────────┤
│ 保存的寄存器 (callee-saved) │ 局部變量、臨時值 │
├─────────────────────┤ ├─────────────────────┤
│ ... │ │ ... │
└─────────────────────┘ └─────────────────────┘
棧增長方向 (高地址 → 低地址)
關鍵點:
| 場景 | 實參 | 形參 | 效果 | 注意事項 |
|---|---|---|---|---|
| 基本類型 | int x = 10; | void f(int p) | 值拷貝,函數內修改不影響外部 | 需修改外部值必須傳指針 f(&x) / void f(int *p) |
| 指針類型 | int *ptr = &x; | void f(int *p) | 指針值拷貝,指向同一地址,可修改指向對象 | 函數內修改指針本身 (如p = malloc(...)) 不影響外部指針 |
| 數組 | int arr[10]; | void f(int arr[]) 或 void f(int *arr) | 退化為指向首元素指針,長度信息丟失 | 必須額外傳遞長度參數,或約定結束標記 (如\0) |
| 結構體 | struct S s; | void f(struct S s) | 整體拷貝 (淺拷貝),開銷大,修改不影響外部 | 大結構體必須傳指針 void f(const struct S *s) |
| 函數指針 | int (*cmp)(int,int); | void f(int (*cmp)(int,int)) | 回調函數,實現多態/策略模式 | 簽名必須完全匹配,可用typedef簡化 |
// 文件: math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
// 函數聲明 (原型) - 告訴編譯器函數簽名
int max(int a, int b); // 最大公約數 (此處應為gcd)
int min(int a, int b); // 最小公倍數 (此處應為lcm)
// 更好的命名
int gcd(int a, int b); // Greatest Common Divisor
int lcm(int a, int b); // Least Common Multiple
#endif
// 文件: math_utils.c
#include "math_utils.h"
int gcd(int a, int b) { // 定義
int m = (a < b) ? a : b;
for (; a % m != 0 || b % m != 0; m--);
return m;
}
int lcm(int a, int b) {
// 注意:a*b可能溢出,工程上應用長整型或先除後乘
return a / gcd(a, b) * b; // 調用同文件/其他文件函數
}
// 文件: main.c
#include
#include "math_utils.h" // 包含聲明
int main() {
int x, y;
printf("輸入兩個正整數: ");
if (scanf("%d %d", &x, &y) != 2) return 1;
printf("最大公約數: %d\n", gcd(x, y)); // 調用
printf("最小公倍數: %d\n", lcm(x, y));
return 0;
}
// 間接遞歸:even/odd 相互調用
// 必須先聲明其中一個
int odd(int n); // 前向聲明
int even(int n) {
if (n == 0) return 1;
return odd(n - 1);
}
int odd(int n) { // 定義
if (n == 0) return 0;
return even(n - 1);
}
// 直接遞歸:階乘
int factorial(int n) {
if (n <= 1) return 1; // 基線條件
return n * factorial(n - 1); // 遞歸調用
}
/**
* @brief 計算兩正整數最大公約數 (歐幾里得算法)
* @param a > 0, b > 0 (違反導致未定義行為)
* @return gcd(a,b) > 0
* @note 無副作用,線程安全
*/
int gcd(int a, int b);
const修飾、按值傳遞小對象、按const *傳大對象const指針,文檔標註 `[out]`const指針,標註 `[in,out]`typedef enum { OK=0, ERR_NULL_PTR, ERR_INVALID_ARG, ERR_OVERFLOW } ErrCode;
ErrCode safe_lcm(int a, int b, int *result) {
if (!result) return ERR_NULL_PTR;
if (a <= 0 || b <= 0) return ERR_INVALID_ARG;
int g = gcd(a, b);
if (a / g > INT_MAX / b) return ERR_OVERFLOW; // 溢出檢查
*result = a / g * b;
return OK;
}
static inline用於頭文件小函數,避免調用開銷,防止多定義錯誤。| 錯誤代碼 | 問題 | 修正 |
|---|---|---|
void f(int a[]) { sizeof(a) } | 數組退化為指針,`sizeof`得指針大小 | 額外傳遞長度參數 `void f(int *a, size_t n)` |
int *p; f(p); // f分配內存 | 傳值調用,外部p不變 | int **p; f(p); // void f(int **p) { *p = malloc(...); } |
return &local_var; | 返回棧地址,函數返回後失效 | 返回動態分配內存、靜態變量、或輸出參數 |
| 遞歸無基線條件/不收斂 | 棧溢出崩潰 | 必須有基線條件,且每次遞歸向基線推進 |
| 頭文件放函數定義 | 多文件包含導致重複定義鏈接錯誤 | 頭文件只放聲明,定義放`.c`,或用`static inline` |
掌握「聲明分離、傳值本質、指針傳址、契約設計、錯誤碼返回、測試友好」這套函數工程化體系,是從「寫出能跑的代碼」邁向「構建可維護系統」的關鍵一步。