目錄
- 一、引言
- 二、計算器需求分析與功能設計
- 2.1 功能定義
- 2.2 輸入處理設計
- 2.3 運算邏輯設計
- 三、核心算法實現
- 3.1 中綴轉後綴算法
- 3.2 後綴表達式計算算法
- 3.3 算法函數編寫
- 四、計算器實戰與測試
- 4.1 編寫主函數
- 4.2 測試常見表達式
- 4.3 錯誤處理
- 五、總結與展望
一、引言
在 C 語言實戰專欄中,我們不斷探索 C 語言在各種場景下的應用與實踐。今天,我們將深入到一個基礎且實用的項目 —— 實現一個支持多運算與括號的簡易計算器。這個計算器不僅是 C 語言基礎語法與算法知識的綜合應用,也是解決實際計算問題的有力工具。通過完成這個項目,我們能更深入地理解 C 語言的語法結構、運算符優先級處理、數據結構的運用以及程序設計的邏輯思維,無論是對於初學者夯實基礎,還是進階開發者提升編程能力,都具有重要的意義。
二、計算器需求分析與功能設計
2.1 功能定義
我們設計的這個簡易計算器,要能夠支持基本的四則運算,也就是加法(+)、減法(-)、乘法(*)、除法(/) ,還有取餘運算(%)。這些基本運算在日常數學計算裏是最常用的,像計算購物找零、分配物品數量、計算面積體積等場景都離不開它們。
同時,為了滿足更復雜的數學計算需求,計算器必須支持使用括號來改變運算優先級。比如在計算(3 + 5) * 2時,我們需要先計算括號內的加法,再進行乘法運算。有了括號,就可以靈活地表達各種複雜的數學邏輯,解決更具挑戰性的計算問題 ,像在工程計算、科學研究中的複雜公式計算,都需要依靠括號來確保運算順序的正確性。
2.2 輸入處理設計
計算器需要接收用户輸入的表達式字符串,就像"3*(4+2)-8/2"這種形式。在接收輸入時,我們可以使用scanf函數從標準輸入讀取用户輸入的字符串,或者使用gets函數讀取一整行輸入(不過gets函數存在緩衝區溢出風險,使用時需要謹慎)。
當獲取到輸入字符串後,首先要進行初步的合法性檢查。比如,檢查字符串中是否包含非法字符,除了數字、運算符(+、-、*、/、% 、(、) )之外的字符都是非法的;還要檢查括號是否匹配,左括號和右括號的數量必須相等,並且右括號不能出現在沒有匹配左括號的前面。如果發現非法字符或者括號不匹配,要及時提示用户輸入有誤。
預處理階段,我們可以去除輸入字符串中的空格,這樣可以簡化後續的處理流程。比如將"3 + ( 4 - 2 )“處理成"3+(4-2)”,方便後續對錶達式的解析和計算。
2.3 運算邏輯設計
在數學表達式中,我們日常習慣使用的是中綴表達式,也就是運算符在兩個操作數中間,像3 + 4這種形式。但是中綴表達式在計算時,需要考慮運算符的優先級和括號的影響,處理起來比較複雜。所以,我們要把中綴表達式轉換為後綴表達式,也叫逆波蘭表達式。
逆波蘭表達式最大的特點就是不需要括號來明確運算順序,運算符緊跟在操作數後面。比如中綴表達式3 + 4 * 2的後綴表達式是3 4 2 * +。這種表示方式的優勢在於計算時非常方便,可以直接使用棧來實現。在計算後綴表達式時,遇到操作數就壓入棧中,遇到運算符就從棧中彈出相應數量的操作數進行計算,然後把結果再壓回棧中,最終棧頂的元素就是表達式的計算結果。通過這種方式,大大簡化了表達式的計算過程,提高了計算效率。
三、核心算法實現
3.1 中綴轉後綴算法
中綴轉後綴算法主要使用棧來處理運算符優先級與括號。具體步驟如下:
- 初始化:創建一個空棧用於存儲運算符,一個空字符串用於存儲後綴表達式。
- 遍歷中綴表達式:從左到右依次讀取中綴表達式的每個字符。
- 如果是數字:直接將數字字符拼接到後綴表達式字符串中,當遇到非數字字符時,在數字字符後添加一個空格作為分隔符。例如,對於中綴表達式3+5,讀取到數字3時,將3添加到後綴表達式中,遇到+後,在3後添加空格,此時後綴表達式為3。
- 如果是左括號(:將左括號壓入運算符棧,因為左括號表示一個子表達式的開始,在它之後的運算符需要等待右括號出現後再處理。
- 如果是右括號):從運算符棧中彈出運算符,直到遇到左括號,將彈出的運算符依次添加到後綴表達式字符串中,然後丟棄左括號。例如,對於中綴表達式(3+5),當讀取到右括號時,從棧中彈出+,添加到後綴表達式中,此時後綴表達式為3 5 +。
- 如果是運算符:比較當前運算符與棧頂運算符的優先級。
- 如果棧為空或棧頂為左括號,或者當前運算符優先級高於棧頂運算符:將當前運算符壓入棧中。例如,對於中綴表達式35+2,讀取到時,因為棧為空,所以將壓入棧;接着讀取到+時,因為的優先級高於+,所以*仍留在棧中,將+壓入棧。
- 如果當前運算符優先級低於或等於棧頂運算符:從棧中彈出運算符並添加到後綴表達式字符串中,直到當前運算符優先級高於棧頂運算符或棧為空或棧頂為左括號,然後將當前運算符壓入棧。例如,對於中綴表達式3+52,讀取到+時,棧頂運算符是,的優先級高於+,所以先彈出添加到後綴表達式中,此時後綴表達式為3 5 2 *,然後再將+壓入棧。
- 遍歷結束:遍歷完中綴表達式後,將運算符棧中剩餘的運算符依次彈出並添加到後綴表達式字符串中。
以中綴表達式3*(4+2)-8/2為例,轉換過程如下:
|
步驟
|
讀取字符
|
後綴表達式
|
運算符棧
|
説明
|
|
1
|
3
|
3
|
空
|
數字 3 直接添加到後綴表達式
|
|
2
|
*
|
3
|
*
|
*壓入棧
|
|
3
|
(
|
3
|
*(
|
左括號壓入棧
|
|
4
|
4
|
3 4
|
*(
|
數字 4 直接添加到後綴表達式
|
|
5
|
+
|
3 4
|
*+(
|
+壓入棧,因為棧頂是左括號
|
|
6
|
2
|
3 4 2
|
*+(
|
數字 2 直接添加到後綴表達式
|
|
7
|
)
|
3 4 2 +
|
*
|
彈出+添加到後綴表達式,丟棄左括號
|
|
8
|
-
|
3 4 2 + *
|
-
|
*彈出添加到後綴表達式,-壓入棧
|
|
9
|
8
|
3 4 2 + * 8
|
-
|
數字 8 直接添加到後綴表達式
|
|
10
|
/
|
3 4 2 + * 8
|
-/
|
/壓入棧
|
|
11
|
2
|
3 4 2 + * 8 2
|
-/
|
數字 2 直接添加到後綴表達式
|
|
12
|
結束
|
3 4 2 + * 8 2 / -
|
空
|
彈出/和-添加到後綴表達式
|
最終得到的後綴表達式為3 4 2 + * 8 2 / -。
3.2 後綴表達式計算算法
後綴表達式計算使用棧來存儲操作數,遇到運算符時進行計算。具體原理和步驟如下:
- 初始化:創建一個空棧用於存儲操作數。
- 遍歷後綴表達式:從左到右依次讀取後綴表達式的每個字符。
- 如果是數字:將數字字符轉換為對應的數值,壓入操作數棧中。例如,對於後綴表達式3 4 +,讀取到3時,將 3 壓入棧;讀取到4時,將 4 壓入棧,此時棧中元素為[3, 4](棧頂在右邊)。
- 如果是運算符:從操作數棧中彈出兩個操作數,注意先彈出的是右操作數,後彈出的是左操作數。根據運算符進行相應的計算,將計算結果再壓回操作數棧中。例如,對於後綴表達式3 4 +,讀取到+時,彈出 4 和 3,計算3 + 4 = 7,然後將 7 壓入棧,此時棧中元素為[7]。
- 遍歷結束:遍歷完後綴表達式後,操作數棧中只剩下一個元素,這個元素就是表達式的計算結果。
對於後綴表達式3 4 2 + * 8 2 / -,計算過程如下:
|
步驟
|
讀取字符
|
操作數棧
|
説明
|
|
1
|
3
|
3
|
數字 3 壓入棧
|
|
2
|
4
|
3, 4
|
數字 4 壓入棧
|
|
3
|
2
|
3, 4, 2
|
數字 2 壓入棧
|
|
4
|
+
|
3, 6
|
彈出 2 和 4,計算4 + 2 = 6,結果 6 壓入棧
|
|
5
|
*
|
18
|
彈出 6 和 3,計算3 * 6 = 18,結果 18 壓入棧
|
|
6
|
8
|
18, 8
|
數字 8 壓入棧
|
|
7
|
2
|
18, 8, 2
|
數字 2 壓入棧
|
|
8
|
/
|
18, 4
|
彈出 2 和 8,計算8 / 2 = 4,結果 4 壓入棧
|
|
9
|
-
|
14
|
彈出 4 和 18,計算18 - 4 = 14,結果 14 壓入棧
|
最終計算結果為 14。
3.3 算法函數編寫
- 表達式解析函數:主要功能是識別輸入字符串中的數字和運算符,並將其按照中綴轉後綴算法進行處理,生成後綴表達式。在 C 語言中,可以使用isdigit函數(包含在<ctype.h>頭文件中)來判斷字符是否為數字。例如:
#include <ctype.h>
#include <string.h>
#include <stdio.h>
#define MAX_SIZE 100
// 定義棧結構
typedef struct {
char data[MAX_SIZE];
int top;
} Stack;
// 初始化棧
void initStack(Stack *s) {
s->top = -1;
}
// 入棧操作
int push(Stack *s, char c) {
if (s->top == MAX_SIZE - 1) {
return 0; // 棧滿
}
s->data[++(s->top)] = c;
return 1;
}
// 出棧操作
int pop(Stack *s, char *c) {
if (s->top == -1) {
return 0; // 棧空
}
*c = s->data[(s->top)--];
return 1;
}
// 獲取棧頂元素
int peek(Stack *s, char *c) {
if (s->top == -1) {
return 0; // 棧空
}
*c = s->data[s->top];
return 1;
}
// 判斷是否為運算符
int isOperator(char c) {
return c == '+' || c == '-' || c == '*' || c == '/' || c == '%';
}
// 判斷運算符優先級
int precedence(char op) {
switch (op) {
case '+':
case '-':
return 1;
case '*':
case '/':
case '%':
return 2;
default:
return -1;
}
}
// 中綴轉後綴
void infixToPostfix(char *infix, char *postfix) {
Stack s;
initStack(&s);
int i, j = 0;
char ch, temp;
for (i = 0; infix[i] != '\0'; i++) {
ch = infix[i];
if (isdigit(ch)) {
while (isdigit(ch) || ch == '.') { // 支持小數
postfix[j++] = ch;
ch = infix[++i];
}
postfix[j++] = ' ';
i--;
} else if (ch == '(') {
push(&s, ch);
} else if (ch == ')') {
while (pop(&s, &temp) && temp != '(') {
postfix[j++] = temp;
postfix[j++] = ' ';
}
} else if (isOperator(ch)) {
while (peek(&s, &temp) && precedence(temp) >= precedence(ch)) {
pop(&s, &temp);
postfix[j++] = temp;
postfix[j++] = ' ';
}
push(&s, ch);
}
}
while (pop(&s, &temp)) {
postfix[j++] = temp;
postfix[j++] = ' ';
}
postfix[j] = '\0';
}
- 計算函數:該函數接收後綴表達式字符串,按照後綴表達式計算算法,使用棧進行計算,返回最終結果。例如:
// 計算後綴表達式
int evaluatePostfix(char *postfix) {
Stack s;
initStack(&s);
int i, op1, op2, result;
char ch;
for (i = 0; postfix[i] != '\0'; i++) {
ch = postfix[i];
if (isdigit(ch)) {
int num = 0;
while (isdigit(ch)) {
num = num * 10 + (ch - '0');
ch = postfix[++i];
}
push(&s, num);
i--;
} else if (isOperator(ch)) {
pop(&s, &op2);
pop(&s, &op1);
switch (ch) {
case '+':
result = op1 + op2;
break;
case '-':
result = op1 - op2;
break;
case '*':
result = op1 * op2;
break;
case '/':
if (op2 != 0) {
result = op1 / op2;
} else {
// 處理除零錯誤
printf("Error: Division by zero\n");
return -1; // 這裏簡單返回-1表示錯誤
}
break;
case '%':
if (op2 != 0) {
result = op1 % op2;
} else {
// 處理取餘時除數為零錯誤
printf("Error: Modulo by zero\n");
return -1;
}
break;
}
push(&s, result);
}
}
pop(&s, &result);
return result;
}
通過這兩個函數,我們實現了將中綴表達式轉換為後綴表達式,並對後綴表達式進行計算的核心功能。
四、計算器實戰與測試
4.1 編寫主函數
主函數是整個程序的入口,它的主要職責是接收用户輸入的表達式,然後調用前面編寫的中綴轉後綴函數infixToPostfix將中綴表達式轉換為後綴表達式,再調用後綴表達式計算函數evaluatePostfix計算最終結果,並將結果輸出給用户。以下是主函數的代碼示例:
int main() {
char infix[MAX_SIZE];
char postfix[MAX_SIZE * 2];// 後綴表達式長度可能是中綴表達式的兩倍
int result;
printf("請輸入一個數學表達式(支持加減乘除、取餘和括號):\n");
fgets(infix, MAX_SIZE, stdin);
// 去除fgets讀取的換行符
infix[strcspn(infix, "\n")] = '\0';
infixToPostfix(infix, postfix);
printf("後綴表達式為:%s\n", postfix);
result = evaluatePostfix(postfix);
if (result != -1) {
printf("計算結果為:%d\n", result);
}
return 0;
}
在這段代碼中,首先定義了用於存儲中綴表達式和後綴表達式的字符數組infix和postfix,以及用於存儲計算結果的變量result。然後通過printf函數提示用户輸入數學表達式,使用fgets函數從標準輸入讀取用户輸入的表達式,存儲在infix數組中。接着調用infixToPostfix函數將中綴表達式轉換為後綴表達式,存儲在postfix數組中,並輸出後綴表達式。最後調用evaluatePostfix函數計算後綴表達式的結果,如果計算成功(結果不為 -1 ,表示沒有發生除零或取餘時除數為零的錯誤),則輸出計算結果。
4.2 測試常見表達式
為了驗證計算器功能的正確性,我們需要對多個常見表達式進行測試。以下是一些測試用例及結果:
|
測試表達式
|
後綴表達式
|
預期結果
|
實際結果
|
|
1+2*3
|
1 2 3 * +
|
7
|
7
|
|
(1+2)*3
|
1 2 + 3 *
|
9
|
9
|
|
10%3
|
10 3 %
|
1
|
1
|
|
3+5*(2-8)/2
|
3 5 2 8 - * 2 / +
|
-12
|
-12
|
|
2*(3+4)/2
|
2 3 4 + * 2 /
|
7
|
7
|
通過這些測試用例可以看出,計算器能夠正確地處理各種運算和括號,得到的實際結果與預期結果一致,説明我們實現的計算器功能是正確的。在實際測試過程中,可以使用循環批量測試更多的表達式,並且可以將測試結果記錄到文件中,方便後續查看和分析。
4.3 錯誤處理
在實際使用中,用户輸入的表達式可能包含非法字符或者存在語法錯誤,比如括號不匹配等情況。為了提高程序的健壯性,我們需要對這些錯誤進行處理。
- 檢測非法字符:在輸入處理階段,我們可以在讀取用户輸入後,遍歷輸入字符串,使用isalnum函數(包含在<ctype.h>頭文件中)判斷字符是否為字母或數字,使用自定義的isOperator函數判斷是否為合法運算符,如果既不是字母數字也不是合法運算符,則為非法字符。例如:
int i;
for (i = 0; infix[i] != '\0'; i++) {
if (!isalnum(infix[i]) &&!isOperator(infix[i]) && infix[i] != '(' && infix[i] != ')') {
printf("錯誤:輸入包含非法字符 '%c'\n", infix[i]);
return 1; // 表示程序異常退出
}
}
- 檢測括號不匹配:可以在遍歷輸入字符串時,使用一個計數器來記錄左括號和右括號的數量。遇到左括號時計數器加 1,遇到右括號時計數器減 1。如果在遍歷結束後,計數器不為 0,則説明括號不匹配。例如:
int count = 0;
for (i = 0; infix[i] != '\0'; i++) {
if (infix[i] == '(') {
count++;
} else if (infix[i] == ')') {
count--;
if (count < 0) {
printf("錯誤:右括號多餘左括號\n");
return 1;
}
}
}
if (count > 0) {
printf("錯誤:左括號多餘右括號\n");
return 1;
}
通過以上錯誤處理機制,當用户輸入的表達式存在非法字符或括號不匹配等語法錯誤時,程序能夠及時檢測並提示用户,避免程序因錯誤輸入而崩潰,提高了程序的穩定性和可靠性。
五、總結與展望
在本次 C 語言實戰中,我們成功實現了一個支持多運算與括號的簡易計算器。通過對計算器的需求分析,明確了支持四則運算、取餘運算以及括號改變運算優先級的功能定義。在輸入處理上,能夠接收用户輸入的表達式字符串,並進行初步的合法性檢查和預處理。
核心算法的實現是本次項目的關鍵,通過中綴轉後綴算法將複雜的中綴表達式轉換為易於計算的後綴表達式,再利用後綴表達式計算算法,藉助棧結構高效地完成表達式的計算。在函數編寫上,實現了表達式解析函數和計算函數,將算法邏輯轉化為可執行的代碼。
在實戰與測試環節,通過編寫主函數實現了完整的計算器交互流程,並且對常見表達式進行了測試,驗證了計算器功能的正確性,同時也加入了錯誤處理機制,提高了程序的健壯性。
展望未來,我們可以對計算器功能進行進一步擴展和優化。比如支持更多的數學函數,如三角函數(sin、cos、tan)、對數函數(log、ln)等,以滿足更復雜的數學計算需求;可以優化代碼的性能,提高計算效率,例如在處理大數字運算時,採用更高效的數據結構和算法;還可以改進用户界面,使其更加友好和便捷,比如實現圖形化界面,讓用户操作更加直觀。通過不斷地完善和優化,這個簡易計算器將變得更加實用和強大。