115 身心障礙特考四等 資訊處理 程式設計概要 考古題詳解
第 一 題25 分
請根據下方 C 語言的程式碼片段,回答下列問題:
// 方法(A):巨集定義
#define CALC(x, y) ( x * y + x)
// 方法(B):函式定義
int calc(int x, int y) {
return x * y + x;
}
int main() { int a = 2, b = 3;
// 執行方法(A)
int resultA = CALC(++a, b + 2);
a = 2; // 重置 a 的值進行方法(B)
int resultB = calc(++a, b + 2);
return 0;
}
(一)請寫出 巨集展開後的實際運算式,並據此計算 的值。(5 分)
(二)請問方法(A)以及方法(B)在參數處理方式及實際執行結果上的差異。(10 分)
(三)若將方法(A)修正為:
#define CALC(x, y) ((x) * (y) + (x))
以便解決運算優先順序之問題,請說明是否可以得到與方法(B)相同的結果?(10 分)
登入後即可作答並保存紀錄。
核心觀念
本題考查 C 語言的前置處理器巨集展開與函式呼叫的差異:
- 巨集在編譯前進行文字替換;引數代入後,運算式依 C 語言的運算子優先順序及結合方向求值。
- 函式呼叫會先求出各引數的值,再將值傳入函式。此例中,函式內的
x、y是各自的區域參數。 - 加上括號可以避免巨集引數代入後發生優先順序錯誤,但不會讓巨集只求值一次,也不會消除重複使用引數的副作用。
解題方法
(一)巨集展開與 resultA
巨集定義為:
#define CALC(x, y) ( x * y + x)
將 ++a 代入 x,將 b + 2 代入 y,得到:
( ++a * b + 2 + ++a)
注意,巨集本體中的 y 前面沒有括號,因此 b + 2 代入後,乘法只會先乘上 b;整個式子等同於:
((++a * b) + 2 + ++a)
在 resultA 這行執行前,、。但這個運算式在同一個完整運算式中對 a 做了兩次未排序的修改:一次是第一個 ++a,另一次是最後一個 ++a。C 語言不保證這兩次修改的先後順序;依 C 語言標準,此處行為未定義(undefined behavior)。
因此,不能由標準 C 語言規則得出唯一的 resultA 數值。不能把兩次遞增擅自排成先後次序,再據此給出確定數值。
(二)方法(A)與方法(B)的差異
方法(A)是前處理器巨集,呼叫時直接以文字替換引數。由於 x 在巨集本體中出現兩次,++a 也會出現兩次,造成同一完整運算式內重複修改 a,因此行為未定義;
第 二 題25 分
請根據下方 Java 程式碼,回答下列問題:
class Main { public static void main(String[] args) { int[] myArr = {10, 20}; modify(myArr);
for (int i : myArr) {
System.out.print(i + " ");
}
}
public static void modify(int[] arr) {
arr[0] = 100; // 操作 A
arr = new int[]{1, 2, 3}; // 操作 B
arr[0] = 999; // 操作 C
}
}
(一)請比較一般程式語言中函式參數傳遞機制,傳值(Pass by value)與傳參考(Pass by reference)的差異。(5 分)
(二)請寫出上方程式碼執行後輸出的結果。(5 分)
(三)請依據上方程式碼的輸出結果,說明 Java 在傳遞陣列參數之機制,其本質是採用傳值還是傳參考?請結合操作 B 以及其後續的指令,解釋為何輸出結果符合該機制。(15 分)
登入後即可作答並保存紀錄。
核心觀念
本題考查函式參數的傳遞方式,以及 Java 傳遞陣列參數時的行為。
- 傳值(Pass by value):函式取得引數值的副本。函式內重新指定參數,只會改變該副本,不會改變呼叫端的變數。
- 傳參考(Pass by reference):函式直接取得呼叫端變數的參考;函式內重新指定參數,會使呼叫端變數也指向新的對象。
Java 一律採用傳值。傳入基本型別時,複製的是基本型別的值;傳入物件或陣列時,複製的是物件參考的值。因此,呼叫端與函式中的參數可以透過參考值指向同一個陣列,但函式重新指定參數,不會改變呼叫端的陣列參考。
解題方法
依序追蹤呼叫端陣列,以及 modify 方法內參數 arr 所指向的陣列:
- 初始狀態:
myArr指向內容為[10, 20]的陣列;呼叫modify(myArr)時,arr取得該參考值的副本,因此兩者暫時指向同一陣列。 - 操作 A:
arr[0] = 100:透過arr修改共同指向的陣列,陣列內容變成[100, 20]。因此myArr所指的內容也已改變。 - 操作 B:
arr = new int[]{1, 2, 3}:建立新陣列,並將方法內的區域參數arr改為指向新陣列。
第 三 題25 分
請根據下方的 Java 程式碼,回答下列問題:
class Animal { void eat() { System.out.print("Generic.."); } }
class Bird extends Animal { void eat() { super.eat(); System.out.print("Using beak(喙)"); }
void fly() {
System.out.println("I can fly");
}
}
(一)請說明過載(Overloading)以及覆寫(Overriding)在 Java 程式語言上的差別。(3 分)
(二)上方程式碼展現了那種特性(Overloading 或是 Overriding)?(2 分)
(三)請寫出執行 的輸出結果。(5 分)
(四)根據上方程式碼,Bird 類別具備飛行 的方法,當要新增一個 Ostrich(鴕鳥,屬於鳥類但卻不會飛行)類別,若採用直接繼承 Bird 類別會遇到什麼問題?(5 分)
(五)根據(四)所面臨的問題,請提出兩種解決該問題的方法。(10 分)
登入後即可作答並保存紀錄。
核心觀念
本題考查 Java 的方法過載(Overloading)、方法覆寫(Overriding),以及物件導向設計中的繼承與「不適當繼承」問題。
- 過載:同一個類別中有多個同名方法,但參數列不同,例如參數個數、型別或順序不同。呼叫哪個方法,主要依編譯時期的參數型態決定。
- 覆寫:子類別重新提供父類別既有方法的實作。方法名稱與參數列須相同,回傳型別須相同或符合 Java 的協變回傳型別規則,且存取權限不可縮小。呼叫實例方法時,會依執行時期物件的實際類別決定執行哪個實作。
- 繼承設計問題:子類別若繼承了不符合自身能力或行為的方法,會造成類別模型不合理,並違反里氏替換原則(Liskov Substitution Principle, LSP)的精神。
解題方法
(一)過載與覆寫的差別
| 比較項目 | 過載 Overloading | 覆寫 Overriding |
|---|---|---|
| 發生位置 | 通常在同一類別中 | 父類別與子類別之間 |
| 方法名稱 | 相同 | 相同 |
| 參數列 | 必須不同 | 必須相同 |
| 判定時機 | 編譯時期依參數列選擇 | 執行時期依實際物件型別分派 |
| 與繼承關係 | 不一定需要繼承 | 需要有繼承或介面實作關係 |
**注意:**只改變回傳型別而不改變參數列,不能構成方法過載;子類別若寫出相同名稱但不同參數的方法,屬於過載,不是覆寫。
(二)程式碼展現的特性
Animal 宣告了 eat,而 Bird extends Animal 又宣告了相同名稱、相同參數列的 eat,因此 Bird.eat 覆寫了 Animal.eat。
super.eat 是在子類別覆寫方法中明確呼叫父類別的 eat 實作,不會因此變成過載。
(三)new Bird.eat 的輸出
依序追蹤方法執行:
- 呼叫
Bird物件的eat。 super.eat執行Animal.eat,輸出Generic..。- 接著執行
Bird.eat中的第二個System.out.print,輸出Using beak(喙)。 - 兩處都使用
print,不會自動換行。
因此輸出為:
Generic..Using beak(喙)
(四)直接讓 Ostrich 繼承 Bird 的問題
Bird 提供 fly 方法。若 Ostrich extends Bird,鴕鳥便會繼承 fly,即使鴕鳥不會飛,程式介面仍允許呼叫 new Ostrich.fly。
若鴕鳥不支援飛行,繼承來的方法就不符合鴕鳥的實際行為;若將 fly 覆寫成空方法或丟出例外,則又使呼叫端無法一致地依賴 Bird 的飛行能力。這是父類別把「鳥」與「會飛」綁在一起所造成的設計缺陷,也違反 LSP 的設計精神。
(五)兩種解決方法
方法一:將共同行為抽到較一般的父類別,另以飛行介面表示飛行能力
讓 Bird 僅代表鳥類的共通特徵,將 fly 移至介面,例如:
第 四 題25 分
下方 Java 程式碼中, 方法根據支付方式之字串會執行不同邏輯。此設計的問題是:每新增一種支付方式,都必須修改 方法。為了解決這個問題,可以透過繼承與多型的方式提高程式維護性。請使用 Java 的繼承與多型重構此程式,並完成下列要求:
public class PaymentService { public void process(String type, double amount) { if (type.equals("CreditCard")) { System.out.println("Processing Credit Card: " + amount); } else if (type.equals("PayPal")) { System.out.println("Processing PayPal: " + amount); }
// 未來若增加 TaiwanPay,必須在此繼續修改代碼
}
}
(一)定義一個抽象類別 Payment。(5 分)
(二)將不同支付方式(包含新增之 TaiwanPay)的處理邏輯移至對應子類別。(15 分)
(三)寫出重構後呼叫端使用方式。(5 分)
登入後即可作答並保存紀錄。
核心觀念
本題考查 Java 的抽象類別、繼承與多型,以及如何將依支付方式判斷的條件分支改為由各支付類別分別處理。
原程式使用 if-else 判斷支付方式;新增 TaiwanPay 時,必須修改 PaymentService.process。重構後,PaymentService 只接收抽象型別 Payment,並呼叫其 process 方法;實際執行哪種支付邏輯,由傳入物件的執行期型別決定。
解題方法
先定義抽象類別 Payment,宣告共同操作 process(double amount)。再讓信用卡、PayPal 與 TaiwanPay 類別繼承 Payment,各自覆寫處理方法。最後,讓 PaymentService 接收 Payment 參數並呼叫方法,不再判斷支付方式字串。
abstract class Payment {
public abstract void process(double amount);
}
抽象類別可作為共同型別,並要求子類別實作抽象方法。由於 Payment 無法直接建立物件,呼叫端須傳入具體子類別的物件。