1 變數範疇:全域變數 vs 區域變數
變數範疇(scope)決定一個變數能在程式的哪個範圍被看見、使用。可以想成學校的公告方式:寫在所有函式之外的全域變數(global variable),就像貼在校門口公佈欄的公告,整個程式檔(每個函式)都能看到、都能存取;寫在函式內部的區域變數(local variable),就像貼在某間教室黑板上的通知,只有那個函式內部看得到,出了那個函式就看不到了。
int count = 0; // 全域變數:整個檔案都看得到
void demo(int n) {
int local_x = n * 2; // 區域變數:只在demo()內部有效
printf("%d\n", local_x);
} // 離開demo(),local_x就不存在了
int main(void) {
count = 5; // main()也能存取全域的count
demo(count);
return 0;
}
| 類型 | 宣告位置 | 可見範圍 | 存在時間 |
|---|---|---|---|
| 全域變數 | 所有函式之外 | 整個原始檔的每個函式都能存取 | 程式開始到結束 |
| 區域變數 | 函式(或區塊)內部 | 只在宣告它的函式(區塊)內 | 進入該函式(區塊)才產生,離開就消失 |
2 變數遮蔽(shadowing)深化:同名變數在不同層級的行為
若區域變數與全域變數同名,區域變數會遮蔽(shadow)全域變數——在這個區域變數有效的範圍內,對這個名字的存取一律指向區域變數,全域的那個完全被「擋住看不見」,但並沒有被刪除,只是暫時存取不到。
int count = 0; // 全域變數
void demo(int n) {
int count = 100; // 區域變數,遮蔽了全域的count
printf("%d\n", count); // 印的是100,不是全域的0
}
如果兩個不同的函式(例如 inc() 和 main())內都各自宣告了一個叫 sum 的區域變數,這兩個 sum 是兩塊完全不同的記憶體,互不相干——即使名字一樣,也不會互相覆蓋或影響。追蹤這類程式時,務必在每個 sum 旁邊標註「這是哪個函式的區域變數」,才不會把不同函式裡的值搞混。
3 函式原型宣告(prototype):為什麼呼叫必須先「認識」函式
C 語言的編譯器是由上往下讀原始碼的。如果一個函式在被呼叫的當下,編譯器還沒看過它的定義、也沒看過任何宣告,編譯器就完全不知道這個函式需要幾個參數、參數型態是什麼、回傳型態是什麼——這會導致編譯失敗(或至少產生嚴重警告)。函式原型宣告(prototype)就是先把函式的「簽名」(名稱+參數型態+回傳型態)預告給編譯器,讓它提早知道怎麼呼叫這個函式。
#include <stdio.h>
int square(int); // 函式原型宣告:先預告 square() 的簽名
int main(void) {
printf("%d", square(5)); // 此時編譯器已經知道 square() 長什麼樣子
return 0;
}
int square(int n) { // 完整定義寫在後面也沒關係
return n * n;
}
1. 把函式的完整定義整個寫在呼叫它的程式碼之前。
2. 或者只先寫一行原型宣告(函式簽名加分號,不含函式本體)放在前面,完整定義可以留到後面才寫——這是實務上更常見的做法,尤其當多個函式互相呼叫時特別好用。
統測很愛出「程式碼裡明明已經寫好了原型宣告,但前面被 // 註解掉」這種題型——正確答案往往不是重新設計程式邏輯,而是把註解符號拿掉、讓已經寫好的原型宣告生效就好,是最簡單也最容易被忽略的解法。
4 傳值呼叫在函式參數中的效果(複習與深化)
延續第7章的觀念:函式參數若是一般型態(不是指標),呼叫時傳進去的只是原始變數值的一份複本。函式內對這個參數做任何修改,都只發生在這份複本上,絕對不會影響到呼叫端的原始變數,即使參數變數跟呼叫端的變數剛好同名也一樣。
int inc(int xin){
int sum = 2;
sum = sum + xin; // 只修改inc()自己的區域sum
xin++; // 只修改inc()自己收到的複本,不影響呼叫端
return (sum);
}
int main(){
int x = 10;
int result = inc(x); // x以傳值方式傳入,xin收到10的複本
printf("%d, %d", result, x); // result=12, x依然是10(沒被xin++影響)
return 0;
}
即使程式碼裡呼叫端變數(如 x)跟函式參數(如 xin)名字不同很好分辨,考題常常會讓兩者名字很像甚至刻意讓函式內外都有同名的變數(見第2節),這時候務必先確認「現在改的是複本還是原始變數」,複本的任何變化都不會傳回呼叫端——除非改用第7章教的傳址呼叫(指標參數)。
5 遞迴(recursion):終止條件與呼叫層追蹤
遞迴是函式呼叫自己。寫遞迴函式一定要有能讓遞迴停下來的終止條件(base case),否則函式會一直呼叫自己、永遠停不下來,最終導致程式當機(stack overflow)。
int fact(int n) { // 遞迴範例:階乘
if (n <= 1) return 1; // 終止條件
return n * fact(n - 1); // 呼叫自己,範圍縮小
}
int fib(int n) {
if (n == 0) return 0; // 終止條件一
if (n == 1) return 1; // 終止條件二
return fib(n - 1) + fib(n - 2);
}
遇到遞迴追蹤題,先寫出每一層呼叫傳入的參數是什麼、什麼時候會觸發終止條件(哪一層是最後一層),再從最深、最先返回的那一層開始,把結果一層一層往外帶回去計算,不要試圖從最外層直接心算到底。
if(n==1) return;(等於1才停止)跟 if(n!=1){...遞迴...}(不等於1才繼續遞迴,等於1就自然結束)雖然邏輯效果類似,但要特別注意:後者的寫法是「終止條件成立時,函式內部完全不執行任何動作(包含不會讓計數器繼續累加),直接返回」,很容易被誤判成「還會多執行一次」。
6 static 區域變數:跨呼叫保留狀態
一般的區域變數,每次呼叫函式都會重新產生、重新初始化,函式結束後就消失。加上 static 關鍵字後,這個區域變數只會在第一次呼叫時初始化一次,之後每次呼叫都會延續上一次結束時的值,不會重新歸零。
int nextId(void) {
static int id = 0; // 只在第一次呼叫時初始化,之後保留上次的值
id++;
return id;
}
int main(void) {
printf("%d %d %d\n", nextId(), nextId(), nextId()); // 1 2 3
return 0;
}
| 宣告方式 | 每次呼叫函式時 | 值會不會保留到下次呼叫 |
|---|---|---|
| 一般區域變數 | 重新產生、重新初始化 | ❌ 不會,每次都是全新的 |
static 區域變數 | 只在第一次呼叫時初始化 | ✅ 會,延續上次結束時的值 |
7 函式回傳型態與精度選擇
設計函式時,回傳型態要能剛好裝得下你要回傳的結果。回傳整數用 int;回傳需要小數精確度的結果,必須用 float 或 double;完全不需要回傳值,才用 void。
double Func(int w, int h) {
double A;
A = w * h / 2.0; // w*h先算出整數15,再除以浮點數2.0
return A; // 整個除法會以浮點數方式運算,得7.5
}
題目常會同時要求「接收兩個整數」和「回傳值要有小數精確度」。作答時要逐一檢查每個選項:括號裡的參數型態對不對(int, int)、回傳型態對不對(float 或 double,不能是 int 或 void)——只要有任何一項不符合,就不是正確答案,兩項都要同時滿足。
15 / 2(兩個都是整數)會得到整數除法的結果 7(直接捨去小數);但只要除法算式中有任何一邊是浮點數(如 15 / 2.0),整個運算就會以浮點數方式進行,得到 7.5。判斷除法是整數除法還是浮點數除法,只需要看算式中有沒有浮點數字面常數或浮點數變數參與。
8 111~115 歷屆統測真題全選項深度剖析(共 7 題)
以下收錄統測專業科目(二)近年全部 7 題函式、變數範疇與遞迴真題。每題均提供高解析度原始考題掃描圖、互動選項按鈕與所有選項逐項深入解析:
int a=5, b=3;
double result=0;
result=Func(a, b);
double Func(int w, int h)
{
double A;
A=w*h/2.0;
return A;
}
🔍 四個選項逐項深度剖析
result=Func(a,b); 這行確實會呼叫函式並把回傳值指定給 result。w*h/2.0 是先做整數除法「15/2」得到整數7,再把7轉型成浮點數7.0,忽略了除數 2.0 本身就是浮點數常數,整個除法運算會以浮點數方式進行,不會先做整數除法捨去小數、之後才轉型。A=w*h/2.0:先計算 w*h=15(兩個int相乘,仍是整數),再除以 2.0(浮點數字面常數),因為除數是浮點數,整個除法會以浮點數方式運算,得 15/2.0=7.5,選 (C)。/2.0 這個除法運算完全沒有執行或被忽略了,直接把 w*h 的結果15當成最終答案(只是轉成浮點數形式15.0),忽略了程式碼明確寫了要再除以2.0這個步驟。🔍 四個選項逐項深度剖析
(int, int) 的選項相比,不夠嚴謹地符合題意。void,代表這個函式完全沒有回傳值,但題目明確要求函式要能「回傳」帶有小數點後至少兩位精確度的計算結果,void 完全無法回傳任何數值,不符合需求。int(整數),完全無法表示小數點後的精確度,題目明確要求「回傳值必須有小數點後至少兩位數精確度」,int 型態不可能達成。float BMI(int, int) 同時滿足了兩項需求:括號內明確列出兩個 int 參數,符合「接收兩個整數」;回傳型態是 float,具備小數精確度,能滿足「小數點後至少兩位精確度」的要求,選 (D)。#include <stdio.h>
//void sub(int i, char *s);
int main(int argc, char *argv[]) {
sub(argc, argv[2]);
return 0;
}
void sub(int i, char *s){
printf("total %d arguments, and the 2nd one is %s\n", i, s);
}
🔍 四個選項逐項深度剖析
// 關閉了,只要去掉 // 讓這行原型宣告生效,編譯器就能提前知道 sub() 的簽名,順利通過編譯,選 (B)。#include <stdlib.h> 是用來引入標準函式庫(如記憶體配置、型態轉換等)的標頭檔,跟本題「自訂函式 sub() 呼叫先於宣告」這個問題完全無關,加入這行並不能提供 sub() 的原型宣告,無法解決編譯錯誤。int sum=1, x=10;
int inc(int xin){
int sum=2;
sum = sum + xin;
xin++;
return (sum);
}
int main(){
int sum = 3;
sum=inc(x);
printf("%d, %d", sum, x);
return 0;
}
🔍 四個選項逐項深度剖析
sum=sum+xin 算錯;同時誤以為 xin++ 這個對函式內複本的遞增,會影響到 main() 呼叫時傳入的 x,讓 x 也跟著改變。但傳值呼叫的 xin 只是 x 的複本,函式內對 xin 的任何修改都不會影響到 main() 的 x。sum=sum+xin=3+10=13。但 inc() 內宣告的是自己獨立的區域 int sum=2,跟 main() 的區域 sum 是兩個完全不同的變數,不應該互相干擾。xin++ 這行對函式內部參數複本的遞增,會反映到 main() 呼叫時傳入的原始變數 x 身上,讓 x 從10變成11。但本題 inc() 的參數 xin 是一般 int 型態,屬於傳值呼叫,函式內對 xin 做任何修改都只影響複本,不會影響 main() 的 x,x 應維持原值10。inc(x),x 以傳值方式傳入,xin 接收複本10。inc() 內宣告了自己的區域 sum=2;sum=sum+xin=2+10=12;xin++ 只影響函式內的複本,不影響 main() 的 x。inc() 回傳12給 main() 的 sum。x 本身從未被真正修改過,仍是10。輸出 "12, 10",選 (D)。int sum=1, x=10; // 行號2:函式外部,全域變數
int inc(int xin){
int sum=2; // 行號4:inc()內部,區域變數
sum = sum + xin;
xin++;
return (sum);
}
int main(){
int sum = 3; // 行號10:main()內部,區域變數
sum=inc(x);
printf("%d, %d", sum, x);
return 0;
}
🔍 四個選項逐項深度剖析
int sum=1, x=10;),是全域變數;行號4寫在 inc() 函式內部(int sum=2;),是 inc() 專屬的區域變數;行號10寫在 main() 函式內部(int sum=3;),是 main() 專屬的區域變數。故行號2的 sum 是全域變數、行號4的 sum 是區域變數,敘述正確,選 (A)。int sum=3;,屬於區域變數,並不是全域變數;行號2是全域、行號10才是區域,這個選項誤以為兩者都是區域變數,忽略了行號2寫在函式外部這個關鍵位置。main() 函式的大括號內部宣告,屬於 main() 專屬的區域變數,只在 main() 裡有效,並不是全域變數;只有行號2(寫在所有函式之外)才是真正的全域變數,這個選項把兩者都誤判成全域變數。#include <stdio.h>
float f(float x){
return(a*x*x+b*x+c);
}
int main(){
float x, a=1, b=0, c=-1;
for(x=-10; x<=10; x=x+0.1)
printf("f(%.1f)=%.1f\n", x, f(x));
}
🔍 四個選項逐項深度剖析
int count = 0;
void foo(int n){
if(n != 1){
++count;
foo(n-1);
}
}
void main(){
int n = 3;
foo(n);
printf("%d", ++count);
}
🔍 四個選項逐項深度剖析
3!=1為真,count變1,遞迴呼叫foo(2)。foo(2):2!=1為真,count變2,遞迴呼叫foo(1)。foo(1):1!=1為假,終止條件成立,直接返回,不再遞迴、也不會再讓count增加。回到main(),count此時是2,++count是前置遞增,先把count加1變成3,再印出這個新值,選 (A)。n!=1 這個條件判斷式仍然成立(誤判1不等於1),因而又多執行了一次 ++count 與遞迴呼叫,讓 count 在 main() 印出前就已經是3,再經過 ++count 後變成4。但 n=1 時 n!=1 確實是假,foo(1) 不會再執行內部的 count 遞增。n!=1(也就是 n 遞減到1時就停止),並非遞減到0才停止,遞迴呼叫的層數被高估了。9 高職段考與模擬精選實戰題(5 題全選項解析)
透過以下 5 題精選模擬試題,全面檢驗你對全域/區域變數、函式原型、傳值呼叫、遞迴與 static 的掌握度:
int val = 100;
void show() {
int val = 5;
printf("%d ", val);
}
int main(void) {
show();
printf("%d", val);
return 0;
}
🔍 四個選項逐項深度剖析
int val=5; 這行宣告沒有真的生效,或誤以為 show() 用的其實是全域 val(而非重新宣告的區域 val),忽略了函式內確實重新宣告了一個同名的區域變數,並把它遮蔽了全域版本。int val = 5;(宣告一個新的區域變數並賦值為5),不是對全域 val 做加法運算,兩者是完全不同的兩塊記憶體,不會有任何加總的效果。#include <stdio.h>
int main(void) {
printf("%d", square(5));
return 0;
}
int square(int n) {
return n * n;
}
🔍 四個選項逐項深度剖析
square(5) 時,square() 的完整定義寫在 main() 之後,且沒有事先的函式原型宣告,編譯器在編譯到這個呼叫時還不知道 square() 的參數個數、型態與回傳型態,在較嚴謹的編譯設定下會產生編譯錯誤或警告。最保險的解法是在 main() 之前先加上一行函式原型宣告 int square(int);,讓編譯器提前知道 square() 的簽名,選 (A)。%d 是 C 語言中用來格式化輸出整數的正確寫法,square() 回傳的也確實是 int 型態,這裡的用法完全正確;%s 是用來輸出字串的格式,用在這裡反而是錯的。int total = 0;
int addOne(int n) {
total = total + 1;
return n + 1;
}
int main(void) {
int a = 5;
int b = addOne(a);
printf("%d %d %d", a, b, total);
return 0;
}
🔍 四個選項逐項深度剖析
addOne(5) 回傳 n+1=6,指定給 b,b=6。total 是全域變數,addOne() 內直接透過 total=total+1 修改的是全域版本(不是透過參數傳遞),所以 total 確實從0變成1。輸出 "5 6 1",選 (A)。n+1 的計算結果6,忽略了函式明確寫著 return n+1;。int sum(int n) {
if (n == 0) return 0;
return n + sum(n - 1);
}
int main(void) {
printf("%d", sum(4));
return 0;
}
🔍 四個選項逐項深度剖析
n==0,直接回傳0,不再繼續遞迴。逐層往回加總:sum(1)=1+0=1;sum(2)=2+1=3;sum(3)=3+3=6;sum(4)=4+6=10。最終印出10,選 (A)。return n + sum(n-1); 這行還會把 n 跟「n-1的遞迴結果」加總起來,並不是只回傳 n 這一個數字。n + sum(n-1))看成了「累乘」的遞迴(n × fact(n-1)),兩種遞迴的寫法很相似但運算完全不同,程式碼裡明確寫的是加號,不是乘號。n==0,且每次呼叫都用 n-1 讓 n 確實在遞減、最終一定會遞減到0,不會無窮遞迴;只有當終止條件寫錯(例如漏寫,或 n 永遠不會等於終止值)時才會發生無窮遞迴。int counter(void) {
static int c = 10;
c = c + 5;
return c;
}
int main(void) {
printf("%d %d %d", counter(), counter(), counter());
return 0;
}
🔍 四個選項逐項深度剖析
static int c = 10; 只會在第一次呼叫 counter() 時初始化一次,之後每次呼叫都會延續上一次結束時的值。第1次呼叫:c=10+5=15;第2次呼叫:c延續上次的15,c=15+5=20;第3次呼叫:c延續上次的20,c=20+5=25。依 printf 引數的呼叫順序輸出 "15 20 25",選 (A)。static 關鍵字的作用正是讓 c 只在第一次初始化、之後保留上次呼叫結束時的值,不會每次呼叫都重設回10。c=c+5; 這行運算完全沒有生效,或誤以為函式回傳的是 c「修改前」的舊值,忽略了函式明確地先把 c 加5、才把加完之後的新值回傳出去。10 考前衝刺 10 秒速記口訣 & 核心知識檢核清單
寫在函式外=全域,整個檔案都看得到;寫在函式內=區域,只有那個函式看得到。
區域變數跟全域變數同名時,該範圍內一律指向區域版本,全域版本被暫時擋住看不見。
函式定義寫在呼叫之後時,要先加一行原型宣告,讓編譯器提早知道函式簽名。
一般型態參數只是複本,函式內修改參數絕不會影響呼叫端的原始變數。
追蹤時先找出終止條件在哪一層觸發,再從最深層開始往回逐層帶回計算結果。
static 區域變數的值會保留到下次呼叫,不會像一般區域變數每次都重設。
同時檢查參數型態與回傳型態兩項需求,任一項不符合就不是正確的函式原型。