單文件寫法在項目小時尚可接受,但隨著代碼增長,萬行級 main.cpp 會讓人「頭暈目眩」。將功能相關的聲明/定義拆分到獨立文件,帶來三大收益:
poker.c,改玩家邏輯只改 player.c。poker.c/.h 即可複用「洗牌、發牌、排序」完整功能。| 文件 | 職責 | 核心內容 |
|---|---|---|
poker.c | 撲克核心邏輯 | Card 結構、花色枚舉、CARD_COUNT、構造/洗牌/排序/比較/獲取名字函數、原始牌組數組 |
player.c | 玩家邏輯 | Player 結構、發牌函數 dispatchCards |
main.c | 程序入口與流程控制 | main()、初始化、調用洗牌/發牌/排序/打印、資源釋放 |
.c 文件直接 #include?初學者常嘗試在 main.c 中 #include "poker.c",但直接編譯會報「重複定義」錯誤。原因在於:
#include 本質是文本展開,將 poker.c 內容複製到 main.c。poker.c 同時參與編譯(生成 poker.o),鏈接時會發現同一函數/全局變量有兩個定義。解決方案:在構建系統(如 Qt Creator、CMake、VS)中將 poker.c、player.c 從編譯目標中移除(取消勾選),僅保留 main.c 參與編譯。此時 .c 文件內容經 #include 合併進 main.c,形成單一編譯單元,無重複定義。
⚠️ 注意:只從專案移除編譯引用,不要刪除物理文件,否則
#include找不到源文件。
#include
#include
#include
#define CARD_COUNT 54
enum Suit { heart, spade, diamond, club, joker1, joker2 };
typedef struct Card {
int value; // 1-15
enum Suit suit;
} Card;
typedef int (*COMPARE)(Card*, Card*);
Card pokers[CARD_COUNT]; // 原始牌組
void initOnePack() { /* ... 構造 54 張牌 ... */ }
Card** shuffle(const Card* src) { /* ... 洗牌演算法 ... */ }
void sort(Card** cards, int n, COMPARE cmp) { /* ... 冒泡/快排 ... */ }
int compare1(Card* a, Card* b) { /* 升序 */ }
int compare2(Card* a, Card* b) { /* 降序 */ }
char* getCardName(const Card* card) { /* 動態分配並返回 "紅桃A" 等字符串,調用者需 free */ }
typedef struct Player {
char name[64];
Card** cards; // 指向原始牌組元素的指針數組,節省內存
int cardsCount;
} Player;
void dispatchCards(Player** players, int playerCount, const Card** shuffledCards) {
int perPlayer = CARD_COUNT / playerCount + 1; // +1 保證容量
for (int i = 0; i < playerCount; ++i)
players[i]->cards = malloc(perPlayer * sizeof(Card*));
for (int i = 0; i < CARD_COUNT; ++i) {
Player* cur = players[i % playerCount];
cur->cards[cur->cardsCount++] = shuffledCards[i];
}
}
#include
#include
#include
#include "poker.c"
#include "player.c"
int main() {
initOnePack();
Card** shuffled = shuffle(pokers);
Player p1 = {"隔壁老王", NULL, 0};
Player p2 = {"小明", NULL, 0};
Player p3 = {"田中龜孫", NULL, 0};
Player* players[] = {&p1, &p2, &p3};
dispatchCards(players, 3, (const Card**)shuffled);
free(shuffled); // 洗牌臨時數組用完即釋放
for (int i = 0; i < 3; ++i) {
printf("%s: ", players[i]->name);
sort(players[i]->cards, players[i]->cardsCount, compare1);
for (int j = 0; j < players[i]->cardsCount; ++j) {
char* name = getCardName(players[i]->cards[j]);
printf("%s ", name);
free(name); // 歸還 getCardName 分配的內存
}
putchar('\n');
free(players[i]->cards); // 釋放指針數組
}
return 0;
}
.c 到 .h + .cpp上述做法屬於「單編譯單元」模式,適合極小項目或教學演示。工業級項目應採用標準模塊化:
poker.h:僅含聲明(結構體、函數原型、extern 全局變量聲明)。poker.cpp:包含 poker.h,實現所有函數,定義全局變量。poker.h player.h,調用接口。poker.cpp player.cpp main.cpp 生成目標文件,再鏈接。這樣做到真正的「接口與實現分離」,支持增量編譯、隱藏實現細節、控制編譯依賴。
文件拆分是軟件工程化的第一步。從「全在一個文件」到「按功能模塊拆分」,再到「頭文件/源文件分離 + 構建系統管理」,每一步都在解決規模增長帶來的複雜度問題。養成模塊化思維,代紙才能從「能跑」走向「好維護、好復用、好擴展」。