字游戲源碼詳解:隨機(jī)數(shù)與輸入校驗(yàn)實(shí)戰(zhàn))
簡介這份資源是C語言“猜數(shù)字”游戲的源程序壓縮包內(nèi)包含兩個不同版本的cpp文件適合剛接觸C語言、希望鞏固隨機(jī)數(shù)生成、條件判斷、循環(huán)控制與輸入輸出等核心知識的初學(xué)者參考。包內(nèi)共2個文件合計(jì)大小約3KB均為C/C源碼兩個版本對應(yīng)開發(fā)過程中的迭代改進(jìn)通過對比可學(xué)習(xí)代碼的逐步優(yōu)化與功能調(diào)整。資源雖然小巧但完整演示了使用srand()與rand()生成隨機(jī)目標(biāo)數(shù)字、以if...else判斷猜高猜低、用while循環(huán)持續(xù)重試直至命中的經(jīng)典流程同時(shí)涉及變量聲明、函數(shù)劃分、對非數(shù)字輸入的isdigit()檢查和清晰注釋結(jié)構(gòu)等實(shí)踐要點(diǎn)。目前已有411人學(xué)習(xí)下載適合配合GCC等編譯器動手驗(yàn)證也可在此基礎(chǔ)上擴(kuò)展猜測次數(shù)限制、計(jì)時(shí)器等新功能作為課堂練習(xí)或自學(xué)C語言的配套示例。1. 從一道課后題到可運(yùn)行的C語言猜數(shù)字很多教程把猜數(shù)字講成“三個數(shù)字比大小”但真正落地的時(shí)候十個新人里有八個翻車在隨機(jī)數(shù)和輸入殘留上。我這次拆的這個C語言猜數(shù)字源程序就是一套能直接編譯運(yùn)行、能二次開發(fā)的完整源碼核心邏輯只依賴標(biāo)準(zhǔn)庫不摻任何平臺私貨。它適合兩類人一類是剛學(xué)完printf和if、想找一個“看得懂又能跑通”的練手項(xiàng)目的新手另一類是準(zhǔn)備拿它做課設(shè)或面試熱身、想看看循環(huán)、隨機(jī)數(shù)、輸入校驗(yàn)怎么組織的老手。下文會講清楚這個項(xiàng)目到底怎么跑、代碼每一段在干什么、哪些地方最容易踩坑以及怎么把它改成帶難度選擇和猜數(shù)歷史記錄的版本。2. 項(xiàng)目定位與技術(shù)選型單文件、標(biāo)準(zhǔn)庫與三步式循環(huán)2.1 為什么是單文件C語言而不是拆成多模塊這個項(xiàng)目從頭到尾只有一個源文件結(jié)構(gòu)上完全可以用“單文件極簡工程”來概括。對于一個猜數(shù)字游戲來說拆成多個文件反而會增加閱讀負(fù)擔(dān)——你需要管理頭文件、函數(shù)聲明、構(gòu)建腳本而這些對初學(xué)階段沒有任何收益。常見做法是把所有邏輯放進(jìn)一個.c文件頂部放#include和宏定義中間按功能順序排列函數(shù)最后在main里把流程串起來。整個工程的依賴非常干凈stdio.h負(fù)責(zé)輸入輸出stdlib.h提供rand()、srand()和malloc等工具time.h提供time()用于生成隨機(jī)種子。這三個頭文件是C語言標(biāo)準(zhǔn)庫的一部分意味著這段代碼可以在任何裝有C編譯器的平臺上直接編譯不需要額外安裝第三方庫。從技術(shù)選型的角度看這恰好體現(xiàn)了C語言“小、快、貼近底層”的特點(diǎn)——一個一百多行的程序就能完整覆蓋“生成隨機(jī)數(shù)、循環(huán)判斷、輸入校驗(yàn)”這三個核心邏輯。2.2 游戲主循環(huán)從“猜一次”到“猜對為止”猜數(shù)字的核心是一個do-while循環(huán)。選擇do-while而不是while是因?yàn)橛螒蛞蟆跋炔乱淮卧倥袛唷睙o論輸入是否正確循環(huán)體至少要執(zhí)行一次。這個細(xì)節(jié)很關(guān)鍵很多新手用while (guess ! answer)開頭結(jié)果發(fā)現(xiàn)變量沒有初始化程序直接進(jìn)入死循環(huán)或者產(chǎn)生未定義行為。代碼如下int answer 0; int guess 0; int attempts 0; srand((unsigned int)time(NULL)); answer rand() % 100 1; do { printf(請輸入你猜的數(shù)字1-100): ); scanf(%d, guess); attempts; if (guess answer) { printf(太大了\n); } else if (guess answer) { printf(太小了\n); } else { printf(恭喜你猜對了總共用了 %d 次。\n, attempts); } } while (guess ! answer);這段代碼的邏輯說明srand用當(dāng)前時(shí)間作為隨機(jī)種子確保每次運(yùn)行程序生成的answer都不一樣printf在scanf之前給出提示讓用戶明確輸入范圍attempts放在判斷之前保證無論對錯都計(jì)入次數(shù)。參數(shù)方面%d表示讀取整數(shù)guess是把輸入值寫入變量地址% 100 1把隨機(jī)數(shù)映射到 1 到 100 的區(qū)間。如果不寫srandrand()會始終產(chǎn)生同一組“隨機(jī)”數(shù)字這是最容易踩的坑。2.3 函數(shù)劃分把判斷、計(jì)數(shù)和提示拆出來完整的猜數(shù)字源程序不會把全部邏輯塞進(jìn)main而是拆成幾個職責(zé)明確的函數(shù)。例如generate_answer()負(fù)責(zé)生成隨機(jī)數(shù)play_game()負(fù)責(zé)循環(huán)交互main()只負(fù)責(zé)調(diào)用入口。這樣做的好處是方便測試和擴(kuò)展——想改成猜 1 到 1000只需要改generate_answer里的取模數(shù)字想記錄最短猜測次數(shù)只需要在play_game里加一個變量。下面是一個函數(shù)拆分的示意圖也是我通常推薦初學(xué)者模仿的結(jié)構(gòu)#include stdio.h #include stdlib.h #include time.h int generate_answer(int lower, int upper) { return rand() % (upper - lower 1) lower; } void play_game(int lower, int upper) { int answer generate_answer(lower, upper); int guess 0; int attempts 0; do { printf(輸入 %d 到 %d 之間的數(shù)字: , lower, upper); scanf(%d, guess); attempts; if (guess answer) { printf(偏大\n); } else if (guess answer) { printf(偏小\n); } } while (guess ! answer); printf(正確嘗試次數(shù): %d\n, attempts); } int main(void) { srand((unsigned int)time(NULL)); play_game(1, 100); return 0; }參數(shù)說明generate_answer的lower和upper決定了隨機(jī)范圍play_game把邊界值透傳給生成函數(shù)這樣修改游戲難度時(shí)不需要動main。main里只保留srand和play_game調(diào)用讓程序的入口看起來非常清爽。這個拆法還有一個好處——如果你以后想寫測試用例可以直接調(diào)用generate_answer驗(yàn)證輸出是否在區(qū)間內(nèi)而不需要模擬用戶輸入。2.4 目錄結(jié)構(gòu)與文件清單這個項(xiàng)目的資源包通常包含以下內(nèi)容文件/目錄作用guess.c主源文件包含全部函數(shù)定義README.md使用說明編譯命令、運(yùn)行示例Makefile可選自動化編譯配置make即可生成可執(zhí)行文件如果你拿到的是最精簡版本可能只有一個.c文件和一段編譯命令。但無論哪種形式核心都是那個源文件。Makefile 的存在與否不影響代碼理解它只是把gcc guess.c -o guess這條命令封裝成可重復(fù)執(zhí)行的規(guī)則。我在實(shí)際開發(fā)中習(xí)慣加上-Wall -Wextra編譯選項(xiàng)它能暴露大多數(shù)隱性問題比如未使用的變量、類型不匹配等。3. 編譯運(yùn)行與踩坑實(shí)錄從 gcc 到輸入殘留的完整排查3.1 在 Windows 和 macOS/Linux 上跑起來Windows 下如果沒有配置過命令行工具可以安裝 TDM-GCC 或使用 Dev-C 自帶的編譯器。命令行編譯方式如下gcc guess.c -o guess.exe guess.exemacOS 和 Linux 上用系統(tǒng)自帶的clang或gccgcc guess.c -o guess -Wall -Wextra ./guess-o guess指定輸出的可執(zhí)行文件名-Wall和-Wextra是警告選項(xiàng)。我一般建議加上-stdc11把語言標(biāo)準(zhǔn)固定到 C11避免某些老編譯器把for (int i 0;;)這類聲明報(bào)錯。如果運(yùn)行后沒有任何響應(yīng)先檢查是不是編譯生成了.exe但你在 PowerShell 里沒加./或者 Linux 下沒有執(zhí)行權(quán)限需要先chmod x guess。3.2 踩坑一隨機(jī)數(shù)每次啟動都一樣現(xiàn)象無論運(yùn)行多少次程序生成的“秘密數(shù)字”永遠(yuǎn)是同一個值游戲完全沒有隨機(jī)性。原因rand()是一個偽隨機(jī)數(shù)生成器如果沒有提供隨機(jī)種子它就使用默認(rèn)種子 1導(dǎo)致每次調(diào)用生成的序列完全相同。這就是網(wǎng)上經(jīng)常說的“偽隨機(jī)玄學(xué)”——看起來在變實(shí)際上是有規(guī)律的。解決在main開頭調(diào)用srand((unsigned int)time(NULL));用當(dāng)前時(shí)間作為種子。time(NULL)返回從 1970 年 1 月 1 日至今的秒數(shù)每次都不同所以種子不同后續(xù)的rand()序列也就不同。注意srand只需要調(diào)用一次不要放在循環(huán)內(nèi)部否則頻繁重置種子反而可能得到重復(fù)結(jié)果。3.3 踩坑二輸入字母導(dǎo)致程序死循環(huán)現(xiàn)象用戶輸入的不是數(shù)字而是字母或符號比如輸入 “abc”程序就會跳過scanf循環(huán)一直執(zhí)行控制臺瘋狂打印提示信息看起來像死機(jī)。原因scanf(%d, guess)在遇到非數(shù)字字符時(shí)會讀取失敗guess保持原值輸入緩沖區(qū)里仍然殘留著 “abc”。下一次循環(huán)繼續(xù)讀取繼續(xù)失敗無限循環(huán)。解決強(qiáng)制檢查scanf的返回值讀取失敗時(shí)清空輸入緩沖區(qū)并重新提示。最常用的清空方式是循環(huán)讀取直到換行符if (scanf(%d, guess) ! 1) { while (getchar() ! \n); printf(輸入無效請重新輸入數(shù)字。\n); continue; }scanf返回成功匹配并賦值的輸入項(xiàng)數(shù)量成功讀取一個整數(shù)時(shí)返回 1失敗時(shí)返回 0。getchar()逐個吸收殘留字符直到遇到換行這樣緩沖區(qū)就干凈了。這段邏輯是猜數(shù)字項(xiàng)目中最容易被忽略的部分新手往往在寫完do-while后以為大功告成結(jié)果一輸入字母就翻車。3.4 踩坑三把判斷條件寫成guess ! answer guess ! 0現(xiàn)象用戶想通過輸入 0 提前退出游戲但程序沒有退出功能只能強(qiáng)行 CtrlC。原因原始代碼里沒有提供“退出游戲”的選項(xiàng)guess ! answer是唯一的循環(huán)條件。這不是 bug是需求缺失。解決在循環(huán)開頭增加一個選項(xiàng)判斷比如約定輸入 0 直接退出if (guess 0) { printf(游戲已退出答案是 %d\n, answer); return; }這樣即使用戶不想繼續(xù)猜也能體面地離開。很多二次改造版本會給用戶“繼續(xù)玩”和“退出”兩個選項(xiàng)本質(zhì)就是在這個位置加判斷。3.5 踩坑四邊界值沒想清楚猜 100 永遠(yuǎn)提示“太大”現(xiàn)象把答案范圍設(shè)在 1 到 100謎底是 100 時(shí)無論怎么猜程序都提示“太大”。原因猜謎范圍寫成rand() % 100生成的是 0 到 99猜 100 時(shí)永遠(yuǎn)大于答案。這是邊界值處理中很經(jīng)典的差一錯誤。解決正確寫法是rand() % 100 1模 100 得到 0 到 99加 1 后變成 1 到 100。如果想用變量控制上下限記住公式rand() % (upper - lower 1) lower。比如范圍是 50 到 100就寫rand() % 51 50這樣最小是 50最大是 100覆蓋完整區(qū)間。4. 核心代碼拆解從輸入校驗(yàn)到猜數(shù)反饋的完整鏈路4.1 完整源碼一份可以照著抄的猜數(shù)字實(shí)現(xiàn)下面是一份經(jīng)過整理的、可直接編譯運(yùn)行的完整源碼它把前面提到的坑全部修掉了并且加入了退出選項(xiàng)和輸入校驗(yàn)邏輯#include stdio.h #include stdlib.h #include time.h #include stdbool.h int generate_answer(int lower, int upper) { return rand() % (upper - lower 1) lower; } bool play_game(int lower, int upper) { int answer generate_answer(lower, upper); int guess 0; int attempts 0; printf(猜數(shù)范圍%d 到 %d輸入 0 退出。\n, lower, upper); while (true) { printf(請輸入數(shù)字: ); if (scanf(%d, guess) ! 1) { while (getchar() ! \n); printf(輸入無效請重新輸入。\n); continue; } if (guess 0) { printf(游戲退出正確答案是 %d\n, answer); return false; } attempts; if (guess answer) { printf(偏大\n); } else if (guess answer) { printf(偏小\n); } else { printf(猜對了共用 %d 次。\n, attempts); return true; } } } int main(void) { srand((unsigned int)time(NULL)); int lower 1; int upper 100; play_game(lower, upper); return 0; }整體邏輯鏈路是main設(shè)定猜數(shù)范圍 →play_game內(nèi)部調(diào)用generate_answer生成謎底 → 進(jìn)入無限循環(huán)讀取用戶輸入 → 校驗(yàn)失敗則清空緩沖區(qū)重來 → 猜 0 直接退出 → 否則比較并打印提示 → 猜對返回true。這個流程把“生成謎底、讀取輸入、反饋大小、判斷退出”四件事完全拆開了每一段都可以獨(dú)立理解和單獨(dú)測試。4.2 輸入校驗(yàn)為什么scanf返回值和getchar()是救命稻草scanf(%d, guess)的返回值是成功讀取的變量個數(shù)正常情況下是 1。如果用戶輸入了亂碼返回值變成 0此時(shí)guess的值還是上次殘留的內(nèi)容如果不清空緩沖區(qū)程序就會帶著臟數(shù)據(jù)陷入死循環(huán)。while (getchar() ! \n);這一行非常便宜它只是不停讀走緩沖區(qū)中的字符直到讀走換行符為止。代價(jià)是丟棄了當(dāng)前行所有輸入下次讓用戶重新輸入。這段處理雖然只有三行但它是整個項(xiàng)目“健壯性”的分水嶺。不做校驗(yàn)的版本只能容忍數(shù)字輸入做了校驗(yàn)的版本才能應(yīng)付真實(shí)使用場景——因?yàn)橛脩粲肋h(yuǎn)會輸入你沒想到的東西。我在給別人代碼評審時(shí)看到scanf直接裸用且不檢查返回值基本都會提示補(bǔ)上這一段。4.3 反饋輸出與計(jì)次邏輯為什么attempts要放在輸入成功之后attempts的位置是有講究的它必須放在“判斷輸入有效性”之后、“打印比較結(jié)果”之前。如果放在scanf之前那么輸入非法字符也會被計(jì)入次數(shù)導(dǎo)致統(tǒng)計(jì)失真如果放在printf之后雖然能準(zhǔn)確計(jì)次但猜對的那一次還沒來得及累加最終顯示次數(shù)會少一次。常見做法是放在if (guess 0)退出判斷之后這樣只有真正參與猜數(shù)的輸入才會計(jì)數(shù)退出操作不污染統(tǒng)計(jì)結(jié)果。從設(shè)計(jì)上看attempts是一個典型的“計(jì)數(shù)器狀態(tài)變量”它不需要被其他模塊訪問所以定義在play_game內(nèi)部即可。如果你將來想記錄多輪游戲的總次數(shù)才需要把它提升為外部變量或者通過返回值傳出。4.4 主函數(shù)與退出條件return true和return false的設(shè)計(jì)意圖play_game的返回類型是booltrue表示猜對false表示用戶退出。這個返回值在單輪游戲中看不出明顯意義但如果后續(xù)改成“多輪累計(jì)”或“排行榜記錄”就能派上用場——main可以根據(jù)返回值決定是否寫入歷史記錄。比如用戶退出時(shí)保存“未完成”狀態(tài)猜對時(shí)保存“完成并記錄次數(shù)”。main里的srand((unsigned int)time(NULL));必須在generate_answer之前執(zhí)行因?yàn)樯呻S機(jī)數(shù)依賴全局的種子狀態(tài)。把srand放在main開頭是最穩(wěn)妥的如果放在某個函數(shù)內(nèi)部容易因?yàn)檎{(diào)用順序問題而變成無效代碼這是運(yùn)行時(shí)行為上的一個隱藏依賴。4.5 從單文件到可擴(kuò)展generate_answer的參數(shù)化設(shè)計(jì)generate_answer(lower, upper)接收兩個邊界參數(shù)這就是參數(shù)化設(shè)計(jì)的雛形。修改范圍時(shí)不需要動任何判斷邏輯只需要在main里改lower和upper。比如改成猜 1 到 1000只需要設(shè)置int upper 1000;。如果更進(jìn)一步把lower和upper也交給用戶在啟動時(shí)輸入就能實(shí)現(xiàn)完全交互式的難度選擇。這一步改造在代碼組織上非常平滑——因?yàn)檫吔缰狄呀?jīng)參數(shù)化了剩下的只是“多讀兩個數(shù)字”的工作。但要注意用戶輸入邊界時(shí)必須校驗(yàn)合法性比如lower必須小于upper否則模運(yùn)算會得到負(fù)數(shù)或零游戲直接崩潰。這也是在改造時(shí)最容易出現(xiàn)的“默認(rèn)參數(shù)被破壞”的場景。5. 二次開發(fā)思路把“能跑”的猜數(shù)字改成“能玩”的猜數(shù)字拿到這份源碼后最重要的不是看懂它怎么跑而是想清楚要怎么改。如果只是交作業(yè)跑通就行但如果想真正掌握C語言的循環(huán)、分支和數(shù)據(jù)結(jié)構(gòu)我建議做三個方向的改造難度從低到高排列。第一步增加難度選擇。在main里讀取用戶輸入的數(shù)字根據(jù)輸入值決定lower和upper比如 1 表示簡單1-502 表示中等1-1003 表示困難1-500。改造時(shí)只需要加一個scanf和一組if-else但要記得校驗(yàn)用戶輸入必須是 1、2、3 之一否則回退到默認(rèn)難度。這一步能檢驗(yàn)?zāi)銓瘮?shù)參數(shù)化設(shè)計(jì)是否真的理解了。第二步把單輪游戲改成多輪計(jì)分。每輪結(jié)束后記錄attempts最后統(tǒng)計(jì)平均嘗試次數(shù)和最少嘗試次數(shù)。這一步需要引入數(shù)組或一組變量來保存每局的結(jié)果。簡單做法是用一個int records[10]數(shù)組play_game返回嘗試次數(shù)main負(fù)責(zé)存儲和求值。更高級的玩法是引入二分查找等最優(yōu)策略來“破解”自己的游戲——手動用二分法猜看看最好成績是多少。第三步把自己猜改成自動猜。寫一個computer_guess(int answer)函數(shù)用二分查找的方式自動猜數(shù)字每次取中間值然后根據(jù)返回值調(diào)整左右邊界統(tǒng)計(jì)需要多少次才能猜對。這個改造是理解“算法與數(shù)據(jù)結(jié)構(gòu)”在真實(shí)場景中如何落地的好練習(xí)因?yàn)榇a里會用到left、right、mid三個變量而mid (left right) / 2就是最經(jīng)典的二分模板。我建議每次改完一個功能都重新編譯并測試幾個邊界場景輸入 0 退出是否正常、輸入字母是否報(bào)錯、范圍設(shè)為 1 到 1 時(shí)游戲是否正常結(jié)束。這套源碼雖然只是一個猜數(shù)字但它的結(jié)構(gòu)具備“小而完整”的特征非常適合做各種實(shí)驗(yàn)。從那以后我每次拿到新代碼第一件事不是看業(yè)務(wù)邏輯而是先把輸入校驗(yàn)和循環(huán)邊界跑一遍因?yàn)榻^大多數(shù)坑都藏在別人想當(dāng)然的地方。希望你也能從這個小項(xiàng)目里找到一套屬于自己的C語言調(diào)試節(jié)奏。愿這份源碼能幫你少踩幾個坑多積累一些手感。本文還有配套的精品資源點(diǎn)擊獲取