首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

劣質代碼評析:《C語言入門經典》附錄B 21點遊戲(三)—— 初始化與輸入處理重構

     編輯:關於C語言

聚焦:initCardsScreen() 與 dispTitle() 的問題與改良

原書代碼中 initCardsScreen() 負責初始化牌堆、玩家點數、總分陣列,並提示輸入玩家姓名,職責過重且耦合全域變數。本文展示如何拆解並現代化這兩個函數。

問題 1:initCardsScreen() 參數列表過長且語意模糊

// 原始宣告 (簡化)
void initCardsScreen(int cards[], int playerPoints[], int dealerPoints[], int total[], int *numCards);

缺陷:

  • 5 個參數全為輸出參數,呼叫端極易傳錯順序
  • 混合「牌堆初始化」與「玩家狀態重置」兩個不同領域操作
  • 姓名輸入屬於 UI 層,不應在資料初始化函數中

重構:職責分離

1. 牌堆初始化 init_poker()


void init_poker(POKER *p) {
    p->num_cards = sizeof(p->cards) / sizeof(p->cards[0]); // 52
    for (int i = 0; i < p->num_cards; i++) {
        p->cards[i] = i % 13 + 1;  // 1-13 循環 4 次 (4 套牌)
    }
}

只做一件事:建立有序牌堆。洗牌由 dealcard() 隨機抽取隱含完成。

2. 玩家狀態重置 reset_gamer()


void reset_gamer(GAMER *g) {
    g->low = g->high = g->score = 0;
}

在發第一張牌前呼叫,或直接用結構體初始化 GAMER player = {0,0,0};。

3. 玩家姓名輸入獨立化


void get_player_name(char *buf, int size) {
    printf("請輸入您的姓名: ");
    if (fgets(buf, size, stdin)) {
        buf[strcspn(buf, "\n")] = '\0';  // 移除換行
    }
}
  • 用 fgets() 替代 scanf(),安全讀取含空格姓名
  • strcspn() 移除尾端換行,無殘留緩衝區問題
  • 呼叫端:char name[32]; get_player_name(name, sizeof(name));

問題 2:dispTitle() 硬編碼標題與格式


// 原始
void dispTitle() {
    int i;
    for (i = 0; i < 40; i++) printf("*");
    printf("\n   Blackjack Game\n");
    for (i = 0; i < 40; i++) printf("*");
}

重構:參數化與區域設定友善


void print_title(const char *title, int width, char border) {
    for (int i = 0; i < width; i++) putchar(border);
    putchar('\n');
    int pad = (width - strlen(title)) / 2;
    for (int i = 0; i < pad; i++) putchar(' ');
    printf("%s\n", title);
    for (int i = 0; i < width; i++) putchar(border);
    putchar('\n');
}

// 呼叫
print_title("21點遊戲", 40, '=');

優點:可複用於不同標題、寬度、邊框字元;支援多語言標題 (UTF-8 環境下)。

問題 3:getAns() 輸入處理脆弱


// 原始片段
char getAns(char *prompt) {
    char ans;
    printf("%s", prompt);
    scanf("%c", &ans);  // 會讀到殘留換行符!
    return ans;
}

重構:健壯的單字元輸入


char get_yes_no(const char *prompt) {
    int c;
    while (1) {
        printf("%s", prompt);
        c = getchar();
        while (getchar() != '\n');  // 清空緩衝區至換行
        if (c == 'y' || c == 'Y') return 'Y';
        if (c == 'n' || c == 'N') return 'N';
        printf("請輸入 Y 或 N\n");
    }
}

// 通用單字元輸入 (可指定合法字元集)
char get_char(const char *prompt, const char *valid) {
    int c;
    while (1) {
        printf("%s", prompt);
        c = getchar();
        while (getchar() != '\n');
        if (strchr(valid, c)) return c;
        printf("無效輸入,合法選項: %s\n", valid);
    }
}

關鍵點:

  1. getchar() 讀單字元,while(getchar()!='\n') 清理整行
  2. 回傳標準化大寫,呼叫端只需比較 'Y'/'N'
  3. 通用版 get_char() 可用於 H/S (Hit/Stand) 等選單

整合後的 main() 片段


int main() {
    POKER poker;
    GAMER player = {0}, dealer = {0};
    char name[32];

    init_poker(&poker);
    get_player_name(name, sizeof(name));
    print_title("21點遊戲", 40, '=');
    printf("歡迎, %s!\n\n", name);

    srand((unsigned)time(NULL));

    do {
        reset_gamer(&player);
        reset_gamer(&dealer);
        // ... 遊戲主流程 ...
    } while (get_yes_no("\n再玩一局 (Y/N)? ") == 'Y');

    return 0;
}

重構效益總結

函數原始問題重構後
initCardsScreen5 參數、多職責、全域耦合拆為 init_poker, reset_gamer, get_player_name,各司其職
dispTitle硬編碼、不可複用print_title 參數化、支援國際化
getAnsscanf 殘留緩衝區、無驗證get_yes_no/get_char 循環驗證、清理緩衝區

核心原則

  1. 初始化即建構:資料結構宣告時初始化 (= {0}),避免單獨初始化函數
  2. UI 與資料分離:姓名輸入、標題顯示屬表現層,不混入領域邏輯
  3. 輸入即驗證:get_*() 內部循環直到合法輸入,主流程無需防禦性檢查
  4. 緩衝區衛生:getchar() + 清理迴圈,或 fgets() + 解析,杜絕 scanf() 留坑

這些看似瑣碎的改進,正是將「能跑的代碼」轉化為「可維護的軟體」的關鍵細節。下一篇將完整呈現最終可編譯執行的 21 點遊戲代碼。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved