112 年 國立中正大學資訊工程學系碩士班乙組《計算機概論(含程式設計)》
第 1 題6 分
(6 points) How many function calls does a(15) produce if we use this recursive function written in C? Please choose the correct answer and explain why.
int a(int n) {
if (n == 0)
return 1;
else
return n * a(n-1);
}
A. 14
B. 15
C. 16
D. 17
登入後即可作答並保存紀錄。
這題考驗對遞迴函數呼叫次數的理解。
遞迴函數 a(n) 的定義如下:
- 當
n等於 0 時,回傳 1。
-否則,回傳n乘以a(n-1)的結果。
我們要計算 a(15) 會產生多少次函數呼叫。
當我們呼叫 a(15) 時:
a(15)呼叫a(14)a(14)呼叫a(13)- ...
a(1)呼叫a(0)
第 2 題5 分
(5 points) What is the correct printed message of the following program fragment written in C? Please choose the correct answer and explain why.
int score = 2, all_num_test = 5;
for(int num_test = 0, final_score = 0; num_test < all_num_test; num_test++, final_score += score) {
printf("%d ", final_score);
}
A. 0 1 2 3 4
B. 0 1 2 3 4 5
C. 1 2 3 4 5
D. 1 2 3 4 5 6
登入後即可作答並保存紀錄。
核心觀念
本題考查 C 語言 for 迴圈的執行順序:
程式中的變數初值為:
score = 2all_num_test = 5num_test = 0final_score = 0
每次迴圈執行完 printf 後,才會執行:
num_test++;
final_score += score;
因此,第一次列印時的 final_score 仍是初始值 。
解題方法
逐次追蹤 num_test 與 final_score:
| 迴圈次數 | num_test 進入條件時的值 | 列印的 final_score | 更新後的 final_score |
|---|---|---|---|
| 1 | 0 | 0 | 2 |
| 2 | 1 | 2 | 4 |
| 3 | 2 | 4 | 6 |
| 4 | 3 | 6 | 8 |
| 5 | 4 | 8 | 10 |
第五次更新後,num_test 變成 ,條件
num_test < all_num_test
變成 ,結果為假,迴圈結束。
所以實際輸出為:
0 2 4 6 8
選項分析
第 3 題6 分
(6 points) There is a large enough array A with indices p and q, which are initialized by 0. Array A and indices p and q can only be accessed in the functions f(x) and g(). If functions f(x) and g() are defined as follows. Which data structure is operated by those functions? Please choose the correct answer and explain why.
f(x)
•
•
g()
•
•
• return
A. Hash
B. Heap
C. Stack
D. Queue
登入後即可作答並保存紀錄。
這題考驗對資料結構操作方式的理解,特別是函數 f(x) 和 g() 如何存取陣列 A 及指標 p,來模擬某種資料結構的操作。
首先,我們來分析函數 f(x) 和 g() 的行為:
函數 f(x):
p \leftarrow p + 1: 指標p先遞增。這意味著下一個元素將被寫入陣列的「下一個」可用位置。A[p] \leftarrow x: 將輸入值x存入陣列A中,索引是遞增後的p。
這個函數的作用是將元素x放入陣列A中,並且p指向這個剛放入元素的下一個位置,準備好接收下一個元素。這是一個「加入」元素的動作。
函數 g():
x \leftarrow A[p]: 從陣列A中取出一個元素,索引是當前的p,並將其值存入變數x。p \leftarrow p - 1: 指標p遞減。這意味著下一次存取將會是「前一個」元素。return x: 回傳取出的元素值。
這個函數的作用是從陣列A中取出一個元素,並且p指向這個剛取出元素的「前一個」位置。這是一個「移除」或「讀取」元素的動作。
現在我們來觀察 p 的行為:
f(x)總是讓p遞增,並在新的p位置寫入。g()總是從當前p位置讀取,然後讓p遞減。
假設 p 和 q 最初都初始化為 0。
當我們連續呼叫 f(x) 幾次,例如 f(10), f(20), f(30):
- 第一次
f(10):p變為 1,A[1] = 10。
第 4 題8 分
(8 points) Please read the following program fragment of the C programming language. If a user inputs the sequence of numbers (-1, -2, -3, 1, 2, 3), what are the values stored in variables s, g, z, and i at the end of the execution of the loop?
int z = 0, g = 0, s = 0, i = 0, t;
while (i < 6) {
scanf("%d", &t);
s = s + t;
if (t >= 0)
++g;
else
z++;
i += 1;
}
登入後即可作答並保存紀錄。
這題考驗對 C 語言迴圈、條件判斷、輸入處理以及變數更新的理解。我們要模擬程式執行,並記錄變數 s, g, z, i 在迴圈結束時的值。
程式碼首先初始化了五個整數變數:z = 0, g = 0, s = 0, i = 0。變數 t 沒有被初始化,但會在 scanf 中被賦值。
迴圈的條件是 i < 6,這表示迴圈會執行 6 次,因為 i 從 0 開始,每次加 1。
使用者輸入的數字序列是 -1, -2, -3, 1, 2, 3,總共有 6 個數字,正好對應迴圈執行的次數。
我們來逐步追蹤變數的變化:
迴圈前:
z = 0, g = 0, s = 0, i = 0
第一次迭代:
- 迴圈條件:
i < 6(0 < 6) 為真。 scanf("%d", &t);: 讀取第一個輸入-1,所以t = -1。s = s + t;:s變為0 + (-1) = -1。if (t >= 0):t是-1,條件-1 >= 0為假。else z++;:z變為0 + 1 = 1。i += 1;:i變為0 + 1 = 1。- 結束時:
z = 1,g = 0,s = -1,i = 1。
第二次迭代:
- 迴圈條件:
i < 6(1 < 6) 為真。 scanf("%d", &t);: 讀取第二個輸入-2,所以t = -2。s = s + t;:s變為-1 + (-2) = -3。if (t >= 0):t是-2,條件-2 >= 0為假。else z++;:z變為1 + 1 = 2。i += 1;:i變為1 + 1 = 2。- 結束時:
z = 2,g = 0,s = -3,i = 2。
第三次迭代:
- 迴圈條件:
i < 6(2 < 6) 為真。 scanf("%d", &t);: 讀取第三個輸入-3,所以t = -3。s = s + t;:s變為-3 + (-3) = -6。if (t >= 0):t是-3,條件-3 >= 0為假。else z++;:z變為2 + 1 = 3。
第 5 題6 分
Let f be the following function:
int f(char *s, char *t) {
char *p1, *p2;
for (p1 = s; *p1; p1++) {
for (p2 = t; *p2; p2++) {
if (*p1 == *p2) break;
}
if (*p2 == '\0') break;
}
return p1 - s;
}
What is the return value of f("abcd", "babc")?
登入後即可作答並保存紀錄。
這題考驗對 C 語言字串指標操作、迴圈嵌套以及字串比較的理解。函數 f 的目的是尋找字串 s 中第一個在字串 t 中也出現的字元,並回傳該字元在 s 中的索引。
函數 f 接受兩個字串指標 s 和 t 作為參數。
它使用兩個指標 p1 和 p2 來遍歷這兩個字串。
p1 從 s 的開頭開始,p2 從 t 的開頭開始。
外層迴圈: for (p1 = s; *p1; p1++)
p1初始化為指向字串s的第一個字元。- 迴圈繼續的條件是
*p1(即p1所指向的字元) 不是字串結束符\0。 p1++讓p1指向字串s中的下一個字元。
此迴圈遍歷字串s中的每一個字元。
內層迴圈: for (p2 = t; *p2; p2++)
p2初始化為指向字串t的第一個字元。- 迴圈繼續的條件是
*p2(即p2所指向的字元) 不是字串結束符\0。 p2++讓p2指向字串t中的下一個字元。
此迴圈遍歷字串t中的每一個字元。
判斷條件: if (*p1 == *p2) break;
- 在內層迴圈中,如果當前
s中的字元 (*p1) 與t中的某個字元 (*p2) 相符,則立即跳出內層迴圈 (break;)。
外層迴圈的判斷: if (*p2 == '\0') break;
- 在內層迴圈結束後,檢查
p2的值。 - 如果內層迴圈是因為
break;而結束(表示找到了匹配的字元),那麼p2指向的是匹配的字元,它不是\0。 - 如果內層迴圈是自然結束(遍歷完
t中的所有字元都沒找到匹配),那麼p2會指向t的結束符\0。 - 如果
*p2 == '\0',表示對於當前的*p1,在整個字串t中都沒有找到匹配的字元。此時,外層迴圈也應該跳出 (break;),因為我們已經找到了s中第一個不在t中的字元(或者說,是第一個在t中找不到匹配的字元)。
回傳值: return p1 - s;
- 函數回傳
p1和s的差值。由於p1和s都是指標,它們的差值代表了p1相對於s的偏移量,也就是p1指向的字元在字串s中的索引。 - 如果迴圈是因為
*p1 == *p2匹配成功而中斷,p1指向的就是第一個匹配的字元。 - 如果迴圈是因為
*p2 == '\0'而中斷,p1指向的就是第一個在t中找不到匹配的字元。
現在我們來模擬 f("abcd", "babc") 的執行過程:
第 6 題6 分
Suppose that s is the following structure:
struct {
double a;
union {
char b[4];
double c;
int d;
} e;
char f[4];
} s;
If char values occupy one byte, int values occupy four bytes, and double values occupy eight bytes, how much space will a C compiler allocate for s (assume that the compiler leaves no holes between members)?
登入後即可作答並保存紀錄。
這題考驗對 C 語言結構 (struct) 和聯合 (union) 的記憶體配置規則的理解,以及編譯器如何處理成員的對齊 (alignment) 和填充 (padding)。題目特別說明「假設編譯器在成員之間不留空隙」(assume that the compiler leaves no holes between members),這簡化了問題,讓我們專注於結構和聯合的大小計算。
首先,我們需要了解 struct 和 union 的記憶體分配規則:
struct: 總大小是其所有成員大小的總和,並且可能因為對齊 (alignment) 而在成員之間插入填充 (padding)。每個成員的大小是其宣告的類型大小。union: 總大小是其最大成員的大小。所有成員共享同一塊記憶體空間。
題目給定的資訊:
char佔 1 位元組 (byte)。int佔 4 位元組。double佔 8 位元組。
現在我們來計算結構 s 的大小:
結構 s 包含三個成員:
-
double a;double的大小是 8 位元組。
-
union { char b[4]; double c; int d; } e;- 這是個聯合 (union) 成員,名為
e。 - 我們需要計算這個聯合的大小。聯合的大小是其最大成員的大小。
- 聯合
e的成員有:char b[4];: 4 個char,每個 1 位元組,總共4 * 1 = 4位元組。double c;:double的大小是 8 位元組。int d;:int的大小是 4 位元組。
- 比較這三個成員的大小:4, 8, 4。其中最大的是 8 位元組 (來自
double c)。 - 因此,聯合
e的大小是 8 位元組。
- 這是個聯合 (union) 成員,名為
-
char f[4];- 4 個
char,每個 1 位元組,總共4 * 1 = 4位元組。
- 4 個
現在我們將結構 s 的成員大小加總,同時考慮到「不留空隙」的假設:
- 成員
a(double): 8 位元組 - 成員
e(union): 8 位元組 (其最大成員 double c 的大小) - 成員
f(char[4]): 4 位元組
總大小 = sizeof(a) + sizeof(e) + sizeof(f)
總大小 = 8 位元組 + 8 位元組 + 4 位元組 = 20 位元組。
第 7 題5 分
The following program fragments illustrate the expressions using different operators. What is the output produced by each, assuming that i, j, and k are int variables?
i = 5; j = 0; k = -5;
printf("%d", i && j || k);
登入後即可作答並保存紀錄。
這題考驗對 C 語言邏輯運算子 (&& 邏輯 AND, || 邏輯 OR) 的求值順序和短路 (short-circuiting) 特性的理解。
C 語言中的邏輯運算子 && 和 || 是從左到右進行求值的,並且具有短路特性:
- 對於
A && B:如果A的求值結果為假 (false),則B不會被求值,整個表達式結果為假。 - 對於
A || B:如果A的求值結果為真 (true),則B不會被求值,整個表達式結果為真。
在 C 語言中,數值 0 被視為假 (false),任何非零值被視為真 (true)。
給定的表達式是 i && j || k。
變數的值為:i = 5, j = 0, k = -5。
讓我們逐步求值:
- 求值
i && j:i的值是5,在 C 語言中被視為真 (true)。j的值是0,在 C 語言中被視為假 (false)。- 由於是
&&(AND) 運算,需要兩個操作數都為真才為真。 true && false的結果是false。
第 8 題8 分
(8 points) Finish the conversion listed below.
a. Convert the octal value 123 to the decimal value
b. Convert the binary value 111010101001 to the octal value
c. Convert the hexadecimal value A4D to the octal value
d. Convert the decimal value 35 to the hexadecimal value
登入後即可作答並保存紀錄。
核心觀念
進位制以基底 表示數值:每一位數字乘上其位權後相加。從右至左,位權依序為 。常用基底包括二進位 、八進位 、十進位 與十六進位 ;十六進位中的 至 分別代表十進位的 至 。
二進位與八進位可直接互換:每 個二進位位元對應 個八進位數字,因為 。二進位與十六進位也可直接互換:每 個二進位位元對應 個十六進位數字,因為 。
解題方法
a. 八進位 轉十進位
依照八進位各位數的位權展開:
因此,。
b. 二進位 轉八進位
從右側開始,每 位分成一組:
各組分別換成八進位數字:
依序寫回,得到:
第 9 題6 分
(6 points) Write regular expressions for the following languages. The alphabet is {0, 1}.
a. (3 points) All strings of 0's and 1's that contain the string 010.
b. (3 points) All strings of 0's and 1's that begins with string 111 or 000.
登入後即可作答並保存紀錄。
這題考驗對正規表示式 (Regular Expression) 的撰寫能力。語言的字母表是 {0, 1}。
a. 包含字串 "010" 的所有字串
一個字串要包含 "010",表示 "010" 這三個字元必須連續出現在字串的某個位置。
在 "010" 之前,可以有任意數量的 0 或 1。
在 "010" 之後,也可以有任意數量的 0 或 1。
- 「任意數量的 0 或 1」可以用
(0|1)*或[01]*來表示。 - 「包含字串 010」表示
010必須出現。
所以,我們可以將它表示為:
(0|1)*010(0|1)*
這表示:
(0|1)*: 任意數量的 0 或 1 (包括 0 個)。010: 字串 "010" 必須出現。(0|1)*: 任意數量的 0 或 1 (包括 0 個)。
第 10 題12 分
(12 points) Explain the following terms:
a. (3 points) Binary translation
b. (3 points) Pseudo instruction in assembly language
c. (3 points) Memory leak
d. (3 points) Calling convention
登入後即可作答並保存紀錄。
這題要求解釋四個計算機科學的術語。
a. Binary translation (二進位翻譯)
二進位翻譯是指將一種二進位代碼(例如,為特定處理器架構編寫的機器碼)轉換為另一種二進位代碼的過程。這通常發生在以下情況:
- 模擬或仿真: 在一種架構的處理器上運行為另一種架構編寫的程式。翻譯器將原始架構的指令翻譯成目標架構的指令。例如,在 x86 處理器上運行 ARM 程式。
- 二進位轉譯器 (Binary Translator): 這種軟體工具讀取應用程式的機器碼,並將其轉換為另一組機器碼,而無需重新編譯原始程式碼。
- 向上或向下相容性: 有時用於使舊的軟體能在新的硬體上運行,或反之。
與直譯 (interpretation) 不同,二進位翻譯通常會生成新的可執行二進位檔,或者在執行時動態地進行翻譯(如動態二進位翻譯,Dynamic Binary Translation, DBT),以便在目標架構上高效執行。
b. Pseudo instruction in assembly language (組合語言中的偽指令)
偽指令 (Pseudo-instruction) 是組合語言中的一種指令,它本身並不是處理器真正執行的機器指令,而是由組合器 (assembler) 處理的指令。它們提供了一種方便的方式來執行某些常見的任務,或者讓程式更容易編寫和閱讀。
偽指令的功能通常包括:
- 定義資料: 例如
.data,.byte,.word,.string等,用於在記憶體中分配空間並初始化數據。 - 控制組合過程: 例如
.equ(定義符號),.macro(定義巨集),.if/.else/.endif(條件組合)。 - 生成機器指令: 有些偽指令會被組合器展開成一條或多條實際的機器指令。例如,某些處理器可能沒有直接支援的「複製」指令,但可以用偽指令
MOV AX, BX(如果 AX 和 BX 是暫存器),組合器會將其翻譯成底層的機器指令。 - 提供抽象: 允許程式設計師使用更高級的語法,而不用擔心底層的硬體細節。
c. Memory leak (記憶體洩漏)
記憶體洩漏 (Memory leak) 是指程式在運行過程中,動態分配了記憶體,但在不再需要使用該記憶體時,未能將其釋放回作業系統或記憶體管理器。
第 11 題12 分
Consider the following two C programs.
// Program A
#include <stdio.h>
#define func (var1, var2) \
var1 = var1 + var2 >> 2; \
var2 = (var1 + var2) % 10;
int main(void) {
int a, b, i;
a = 2;
b = 4;
for (i=0; i<4; i++) {
func(a, b);
printf("%d %d\n", a, b);
}
return 0;
}
// Program B
#include <stdio.h>
void func(int *ptr1, int *ptr2) {
*ptr1 = *ptr1 + *ptr2 >> 2;
*ptr2 = (*ptr1 + *ptr2) % 10;
}
int main(void) {
int a, b, i;
a = 2;
b = 4;
for (i=0; i<4; i++) {
func(&a, &b);
printf("%d %d\n", a, b);
}
return 0;
}
a. (3 points) Do these two programs have the same output? Why?
b. (4 points) Without compiler optimizations, which program has less execution time generally? Why?
c. (5 points) What are advantages and disadvantages of implementing functions as macros in C programs?
登入後即可作答並保存紀錄。
核心觀念
本題考查:
- C 語言「函式型巨集」與「物件型巨集」的語法差異。
- 巨集展開與函式呼叫的執行方式。
- 運算子優先序:
+高於>>,因此
等價於
- 巨集與函式在執行時間、型別檢查、參數求值等方面的差異。
a. 兩個程式是否有相同輸出?
先判斷 Program A 的巨集語法
題目中的宣告是:
#define func (var1, var2) \
var1 = var1 + var2 >> 2; \
var2 = (var1 + var2) % 10;
在 C 語言中,函式型巨集的巨集名稱與左括號之間不能有空白:
#define func(var1, var2)
題目寫成:
#define func (var1, var2)
因此 func 會被視為「物件型巨集」,而不是函式型巨集。
呼叫:
func(a, b);
會展開成類似:
(var1, var2)
var1 = var1 + var2 >> 2;
var2 = (var1 + var2) % 10;
(a, b);
這不是合法的 C 語法,因此 Program A 無法通過編譯,也就沒有輸出。
按照題目原文的嚴格答案
Program A 無法編譯,Program B 可以編譯,因此兩者沒有相同輸出。
若題目原意是函式型巨集
若將宣告修正為:
#define func(var1, var2) \
var1 = var1 + var2 >> 2; \
var2 = (var1 + var2) % 10;
則巨集展開後,兩個程式的運算順序相同。
Program B 的函式內容為:
*ptr1 = (*ptr1 + *ptr2) >> 2;
*ptr2 = (*ptr1 + *ptr2) % 10;
其中第二行使用的是第一行更新後的 *ptr1。
計算如下:
| 迴圈 | 原本的 | 更新後的 | 更新後的 | 輸出 |
|---|---|---|---|---|
| 第 1 次 | 1 5 | |||
| 第 2 次 | 1 6 | |||
| 第 3 次 | 1 7 | |||
| 第 4 次 | 2 9 |
因此,若修正巨集宣告,兩者輸出皆為:
1 5
1 6
1 7
2 9
b. 哪個程式一般執行時間較短?
在「沒有編譯器最佳化」的前提下,若 Program A 修正為合法的函式型巨集,通常是 Program A 執行時間較短。
原因
函式型巨集是在前處理階段直接展開。例如:
func(a, b);
會被替換成:
a = a + b >> 2;
b = (a + b) % 10;
因此不需要:
- 建立函式呼叫環境;
- 傳遞參數;
- 保存返回位址;
- 跳躍至函式;
- 從函式返回。
Program B 則需要真正呼叫:
func(&a, &b);
除了函式呼叫與返回成本外,還需要透過指標存取資料:
*ptr1
*ptr2
所以一般而言:
補充
巨集展開會增加程式碼大小。若巨集在大量位置被使用,可能造成指令快取使用效率下降,因此實際效能不一定永遠較佳。但在本題「無編譯器最佳化」且只比較函式呼叫額外成本的情況下,應答 Program A 較快。
兩個版本每次呼叫都只執行固定數量的運算,因此時間複雜度與空間複雜度皆為:
c. 使用 C 巨集實作函式的優缺點
優點
1. 減少函式呼叫成本
巨集在編譯前直接展開,不需執行函式呼叫與返回,因此適合非常短小、且被大量呼叫的程式片段。
2. 可處理多種資料型別
第 12 題20 分
(20 points) Consider the following C program, which is properly compiled and run at a 64-bit OS. We say that the pointer variable p1 points to v1 after executing the statement: p1=&v1 (line 10).
1 #include <stdio.h>
2 #include <stdlib.h>
3 void func(int* ptr) {
4
5 }
6 printf("%d\n", sizeof(ptr));
7 void main(void) {
8 int *p1, p2, *p3, **r1;
9 int v1, v2[100], v3;
10 int cond = rand();
11 p1 = &v1;
12 p3 = &v3;
13 r1 = &p1;
14 func(v2);
15
16 if (cond%2 == 0) {
17 *r1 = v2;
18 *p3 = 0;
19 v1 = 1;
20 } else {
21 p3 = NULL;
22 }
23
24 printf("%d\n", sizeof(v2));
25 }
a. (4 points) In line 14, which variables might the pointer variables p1, p3, r1, and ptr point to, respectively?
b. (4 points) In line 18, which variables might the pointer variables p1, p3, and r1 point to, respectively?
c. (8 points) In line 22, which variables might the pointer variables p1, p3, and r1 point to, respectively?
d. (4 points) What is the output of the program?
登入後即可作答並保存紀錄。
核心觀念
本題考 pointer 的指向關係、陣列傳入函式時的退化,以及 sizeof 的計算方式。
int *p1表示p1是指向int的指標;int p2則是一般整數,不是指標。int **r1表示r1是指向int *的指標。因此r1 = &p1後,r1指向的是指標變數p1。- 陣列名稱用在函式呼叫的引數位置時,會轉成指向首元素的指標。因此
func(v2)中,函式參數ptr指向v2[0]。 *r1 = v2會改寫r1所指的指標變數,也就是把p1改成指向v2[0]。sizeof(v2)計算整個陣列的大小;sizeof(ptr)計算指標本身的大小。
解題方法
先以變數或陣列元素為節點,追蹤每次指定後的指向關係:
p1 = &v1;
p3 = &v3;
r1 = &p1;
func(v2);
執行完第 13 行後,p1 指向 v1、p3 指向 v3、r1 指向 p1。呼叫 func(v2) 時,陣列 v2 退化成首元素位址,所以 ptr 指向 v2[0]。
a. 第 14 行
此時各指標的指向如下:
| 指標 | 指向 |
|---|---|
p1 | v1 |
p3 | v3 |
r1 | 指標變數 p1 |
ptr | 陣列元素 v2[0] |
b. 第 18 行
第 18 行位於 cond % 2 == 0 的分支中。執行第 17 行:
*r1 = v2;
因為 r1 指向 p1,這行等同於:
p1 = v2;
而 v2 會退化為 &v2[0]。接著第 18 行只會改寫 p3 所指的整數值,不會改變 p3 的指向。因此:
| 指標 | 指向 |
|---|---|
p1 | v2[0] |
p3 | v3 |
r1 | 指標變數 p1 |