言從頭學(xué)PDF:290頁(yè)69節(jié)案頭手冊(cè),從指針到文件操作避坑指南)
簡(jiǎn)介《C語(yǔ)言從頭學(xué)》匯編是一份面向零基礎(chǔ)及初學(xué)者的C語(yǔ)言自學(xué)PDF資料由LaoWaiHang整理其CSDN專(zhuān)欄系列文章而成適合希望系統(tǒng)掌握C語(yǔ)言、邊看文字邊對(duì)照代碼理解知識(shí)點(diǎn)的編程入門(mén)者。資源包內(nèi)共1個(gè)PDF文件大小約17.03MB內(nèi)容按章節(jié)編排并配有目錄涵蓋從入門(mén)程序、基本語(yǔ)法、運(yùn)算符、流程控制、數(shù)據(jù)類(lèi)型、指針、函數(shù)到結(jié)構(gòu)體、聯(lián)合體、枚舉、位字段、數(shù)組、字符串、內(nèi)存管理、文件操作、預(yù)處理指令及標(biāo)準(zhǔn)庫(kù)頭文件等完整知識(shí)體系。全書(shū)采用“文字案例代碼”模式對(duì)原專(zhuān)欄中表述不清之處做了修正并充實(shí)完善了示例程序每章穿插可運(yùn)行代碼便于讀者在文字理解受阻時(shí)對(duì)照代碼快速突破。目前已有99人學(xué)習(xí)參考適合作為C語(yǔ)言入門(mén)到進(jìn)階的自學(xué)材料與查閱手冊(cè)。1. 從一份 290 頁(yè)的 PDF 說(shuō)起為什么我建議你把它當(dāng)案頭手冊(cè)而不是教程市面上 C 語(yǔ)言資料多如牛毛但真正能讓人從「Hello World」一路啃到「文件操作四連發(fā)」的其實(shí)沒(méi)幾個(gè)。這份《C語(yǔ)言從頭學(xué)》匯編 PDF290 頁(yè)、69 個(gè)小節(jié)從第 0 節(jié)前言到第 69 節(jié)time.h把 C 語(yǔ)言從基本語(yǔ)法到標(biāo)準(zhǔn)庫(kù)頭文件幾乎犁了一遍。它不是那種「21 天精通」的速成冊(cè)子更像是一本按知識(shí)點(diǎn)切碎、每節(jié)配案例代碼的案頭手冊(cè)。適合誰(shuí)適合已經(jīng)裝好 VS2022 或 GCC、能自己敲代碼、但一遇到指針和結(jié)構(gòu)體就卡殼的初學(xué)者也適合工作幾年后想回頭把 C 語(yǔ)言底層細(xì)節(jié)重新捋一遍的從業(yè)者。我拿到這份 PDF 的第一反應(yīng)是終于有人把「文字案例代碼」這個(gè)模式堅(jiān)持到底了而不是只丟一堆概念讓你自己悟。2. 拆解這份 PDF 的知識(shí)骨架69 節(jié)到底怎么排的2.1 從入門(mén)程序到運(yùn)算符優(yōu)先級(jí)前 11 節(jié)在鋪什么路前 11 節(jié)是整份 PDF 的地基。第 1 節(jié)入門(mén)程序直接甩出一個(gè)帶getchar()阻塞的 Hello World這個(gè)細(xì)節(jié)很關(guān)鍵——很多教程只寫(xiě)printf就結(jié)束屏幕一閃而過(guò)新手根本看不到輸出。作者在這里用char cgetchar();把程序卡住雖然變量c沒(méi)被處理但目的明確讓你看清運(yùn)行結(jié)果。第 2 節(jié)基本語(yǔ)法概念把語(yǔ)句、表達(dá)式、語(yǔ)句塊、空格、注釋五個(gè)概念一次講透其中關(guān)于「一個(gè)語(yǔ)句分成多行時(shí)不能將命令名、函數(shù)名強(qiáng)行斷成兩行」的提醒是實(shí)打?qū)嵉牟瓤咏?jīng)驗(yàn)。第 3 節(jié)和第 4 節(jié)集中講printf()和占位符把%i、%d、%f、%s以及寬度限定、正負(fù)號(hào)顯示、小數(shù)位數(shù)、部分字符串輸出全部覆蓋。第 5 節(jié)頭文件及標(biāo)準(zhǔn)庫(kù)解釋了#include stdio.h的本質(zhì)并區(qū)分了尖括號(hào)和雙引號(hào)的使用場(chǎng)景。第 6 節(jié)變量把作用域和for循環(huán)內(nèi)重復(fù)聲明變量的玄學(xué)講清楚了——for循環(huán)本身構(gòu)成語(yǔ)句塊大括號(hào)省略了但作用域還在。第 7 到第 11 節(jié)依次是算術(shù)運(yùn)算符、關(guān)系運(yùn)算符、邏輯運(yùn)算符、位運(yùn)算符、運(yùn)算符優(yōu)先級(jí)五節(jié)連起來(lái)就是一套完整的運(yùn)算符體系。2.2 流程控制、數(shù)據(jù)類(lèi)型、指針函數(shù)中段 20 節(jié)的遞進(jìn)邏輯第 12 到第 14 節(jié)是流程控制三連發(fā)從條件語(yǔ)句到循環(huán)語(yǔ)句再到跳轉(zhuǎn)語(yǔ)句節(jié)奏很穩(wěn)。第 15 到第 19 節(jié)進(jìn)入數(shù)據(jù)類(lèi)型深水區(qū)數(shù)據(jù)類(lèi)型一二講基本類(lèi)型和修飾符sizeof運(yùn)算符單獨(dú)成節(jié)類(lèi)型的自動(dòng)轉(zhuǎn)換和顯式轉(zhuǎn)換各占一節(jié)。這個(gè)安排很合理因?yàn)轭?lèi)型轉(zhuǎn)換是后面指針和函數(shù)傳參的基礎(chǔ)。第 20 節(jié)指針只有一節(jié)但內(nèi)容密度極高從指針聲明到解引用再到指針運(yùn)算基本把指針的核心用法壓縮進(jìn)去了。第 21 到第 27 節(jié)是函數(shù)專(zhuān)題函數(shù)定義、main()函數(shù)、參數(shù)的傳值與傳址引用、函數(shù)原型、exit()和atexit()、函數(shù)說(shuō)明符、可變參數(shù)。其中「參數(shù)的傳值與傳址引用」這一節(jié)是理解指針和函數(shù)關(guān)系的關(guān)鍵很多初學(xué)者在這里翻車(chē)PDF 用案例代碼把兩種傳參方式的差異擺出來(lái)對(duì)照看就能明白。第 28 到第 32 節(jié)是數(shù)組和字符串?dāng)?shù)組一二、字符串、與字符串變量相關(guān)的幾個(gè)函數(shù)、字符串?dāng)?shù)組。第 33 和第 34 節(jié)是內(nèi)存管理一二把malloc、free、內(nèi)存泄漏這些概念提前引入為后面的結(jié)構(gòu)體和文件操作打基礎(chǔ)。2.3 結(jié)構(gòu)體、文件操作、標(biāo)準(zhǔn)庫(kù)頭文件后 38 節(jié)的實(shí)戰(zhàn)指向第 35 到第 41 節(jié)是結(jié)構(gòu)體專(zhuān)題struct結(jié)構(gòu)、結(jié)構(gòu)體變量的賦值與比較、struct指針、struct結(jié)構(gòu)體的嵌套、結(jié)構(gòu)體位字段和彈性數(shù)組成員、typedef命令、union結(jié)構(gòu)和enum類(lèi)型。這七節(jié)把自定義類(lèi)型能踩的坑基本都覆蓋了尤其是位字段和彈性數(shù)組成員屬于進(jìn)階內(nèi)容但講得不晦澀。第 42 和第 43 節(jié)是預(yù)處理指令一二把#define、#include、條件編譯這些講清楚。第 44 和第 45 節(jié)是 I/O 函數(shù)一二第 46 到第 49 節(jié)是文件操作四連發(fā)從文件打開(kāi)關(guān)閉到讀寫(xiě)定位四節(jié)下來(lái)基本能獨(dú)立寫(xiě)文件處理程序。第 50 到第 53 節(jié)是變量說(shuō)明符、多文件項(xiàng)目、命令行參數(shù)、字符集屬于工程化必備知識(shí)。第 54 到第 69 節(jié)是標(biāo)準(zhǔn)庫(kù)頭文件逐個(gè)擊破assert.h、ctype.h、errno.h、float.h、limits.h、locale.h、math.h三節(jié)、signal.h、stdlib.h三節(jié)、stdio.h兩節(jié)、stddef.h和stdbool.h、string.h、time.h。這 16 節(jié)相當(dāng)于一份標(biāo)準(zhǔn)庫(kù)速查手冊(cè)用到哪個(gè)查哪個(gè)。3. 把 PDF 跑起來(lái)環(huán)境準(zhǔn)備與第一個(gè)可調(diào)試工程3.1 VS2022 和 GCC 兩條路選哪條PDF 里明確說(shuō)了作者用的是 VS2022但沒(méi)強(qiáng)制你跟著用。如果你在 Windows 上VS2022 社區(qū)版是最省事的安裝時(shí)勾選「使用 C 的桌面開(kāi)發(fā)」工作負(fù)載C 語(yǔ)言編譯器就一并裝好了。如果你在 Linux 或者 macOS 上GCC 或 Clang 是更自然的選擇。我一般會(huì)建議新手先用 VS2022 把前 10 節(jié)跑一遍因?yàn)?IDE 的報(bào)錯(cuò)提示對(duì)理解語(yǔ)法錯(cuò)誤幫助很大等到第 20 節(jié)指針和第 33 節(jié)內(nèi)存管理時(shí)再切到 GCC 用命令行編譯強(qiáng)迫自己看編譯器的原始報(bào)錯(cuò)。兩條路的具體操作如下VS2022 路線(xiàn)新建「空項(xiàng)目」→ 右鍵源文件 → 添加 → 新建項(xiàng) → 選「C 文件」但命名時(shí)用.c后綴 → 把 PDF 里的代碼粘進(jìn)去 → 按 F5 編譯運(yùn)行。注意VS2022 默認(rèn)可能把.c文件當(dāng) C 編譯需要在項(xiàng)目屬性 → C/C → 高級(jí) → 編譯為 → 選「編譯為 C 代碼 (/TC)」。GCC 路線(xiàn)把代碼保存為hello.c然后在終端執(zhí)行# 編譯 hello.c輸出可執(zhí)行文件 hello gcc -o hello hello.c -Wall # 運(yùn)行 ./hello-Wall是打開(kāi)所有警告新手階段強(qiáng)烈建議加上很多潛在問(wèn)題編譯器會(huì)直接告訴你。-o hello指定輸出文件名不指定的話(huà)默認(rèn)是a.out。3.2 用第 1 節(jié)代碼驗(yàn)證環(huán)境一個(gè)容易被忽略的阻塞技巧PDF 第 1 節(jié)的 Hello World 代碼里有一行char cgetchar();作者解釋說(shuō)是為了「阻塞程序的運(yùn)行防止屏幕一閃而過(guò)看不到運(yùn)行結(jié)果」。這個(gè)技巧在 VS2022 里其實(shí)不是必須的因?yàn)?VS 默認(rèn)會(huì)在程序結(jié)束后暫停控制臺(tái)。但如果你用 GCC 在終端直接運(yùn)行不加這行確實(shí)可能看不到輸出——尤其是雙擊可執(zhí)行文件的時(shí)候。我一般會(huì)把這行改成getchar();不聲明變量效果一樣但更干凈。驗(yàn)證環(huán)境是否配好就看這行代碼能不能讓程序停住等你按回車(chē)。#include stdio.h int main(void) { printf(Hello World!\n); getchar(); // 阻塞程序防止窗口一閃而過(guò) return 0; }編譯運(yùn)行后屏幕上應(yīng)該顯示Hello World!并停住按回車(chē)后程序退出。如果編譯報(bào)錯(cuò)「未定義標(biāo)識(shí)符 printf」檢查是否包含了#include stdio.h如果報(bào)錯(cuò)「main 必須返回 int」檢查main的返回類(lèi)型是不是寫(xiě)成了void。3.3 用第 4 節(jié)占位符代碼做一次輸出格式壓力測(cè)試PDF 第 4 節(jié)把printf的占位符和輸出格式講得很細(xì)我建議把這一節(jié)的代碼全部敲一遍然后故意改幾個(gè)參數(shù)看輸出變化。比如把%5d改成%-5d看空格補(bǔ)位方向的變化把%0.3f改成%0.6f看小數(shù)位數(shù)對(duì)四舍五入的影響。下面這段代碼把第 4 節(jié)的核心用法串在一起#include stdio.h int main(void) { // 寬度限定%5d 表示至少占 5 個(gè)字符寬度不足補(bǔ)空格 printf(整數(shù)寬度測(cè)試:[%5d]\n, 123); printf(左對(duì)齊:[%-5d]\n, 123); // 小數(shù)位數(shù)限定%0.3f 表示保留 3 位小數(shù)四舍五入 printf(小數(shù)位數(shù)測(cè)試:[%0.3f]\n, 12.34567); // 顯示正負(fù)號(hào)%d 強(qiáng)制顯示正數(shù)的加號(hào) printf(正負(fù)號(hào)測(cè)試:[%d] [%f]\n, 123, -456.78); // 部分字符串輸出%.5s 只輸出前 5 個(gè)字符 printf(部分字符串:[%.5s]\n, hello world); getchar(); return 0; }這段代碼的輸出結(jié)果應(yīng)該和 PDF 里描述的一致[ 123]、[123 ]、[12.346]、[123] [-456.780000]、[hello]。如果某個(gè)輸出不對(duì)大概率是占位符寫(xiě)錯(cuò)了比如把%0.3f寫(xiě)成了%.3f少了0在大多數(shù)編譯器上結(jié)果一樣但嚴(yán)格來(lái)說(shuō)%0.3f更規(guī)范。4. 避坑排查從指針到文件操作的五個(gè)血淚教訓(xùn)4.1 指針未初始化就解引用程序直接崩編譯器還不一定報(bào)錯(cuò)現(xiàn)象聲明了一個(gè)指針變量沒(méi)有賦初值就直接*p 10;程序運(yùn)行時(shí)崩潰報(bào)「段錯(cuò)誤」或「訪(fǎng)問(wèn)沖突」。原因指針變量聲明后里面存的是一個(gè)隨機(jī)地址這個(gè)地址可能指向系統(tǒng)保護(hù)區(qū)或無(wú)效內(nèi)存。解引用就是往這個(gè)隨機(jī)地址寫(xiě)數(shù)據(jù)操作系統(tǒng)直接終止程序。解決指針聲明時(shí)要么初始化為NULL要么立即指向一個(gè)有效的內(nèi)存地址。PDF 第 20 節(jié)雖然講了指針的基本用法但初學(xué)者很容易忽略初始化這一步。我一般會(huì)強(qiáng)制自己寫(xiě)成int *p NULL;用之前再檢查if (p ! NULL)。4.2 兩個(gè)整數(shù)相除結(jié)果丟小數(shù)占位符寫(xiě)對(duì)了也沒(méi)用現(xiàn)象printf(%f, 3/4);輸出0.000000而不是0.750000。原因C 語(yǔ)言里兩個(gè)整數(shù)相除結(jié)果還是整數(shù)小數(shù)部分直接被截?cái)唷?/4的結(jié)果是0然后0被轉(zhuǎn)成0.000000輸出。PDF 第 7 節(jié)專(zhuān)門(mén)提醒了這一點(diǎn)但新手看到%f占位符會(huì)誤以為會(huì)自動(dòng)轉(zhuǎn)成浮點(diǎn)。解決兩個(gè)操作數(shù)至少有一個(gè)寫(xiě)成浮點(diǎn)形式比如3.0/4或3/4.0或者用強(qiáng)制類(lèi)型轉(zhuǎn)換(float)3/4。如果兩個(gè)都是變量強(qiáng)制類(lèi)型轉(zhuǎn)換是唯一選擇。4.3 結(jié)構(gòu)體賦值和比較的陷阱能用不能用現(xiàn)象兩個(gè)同類(lèi)型的結(jié)構(gòu)體變量用比較是否相等編譯器報(bào)錯(cuò)「無(wú)效的操作數(shù)」。原因C 語(yǔ)言不允許直接用比較兩個(gè)結(jié)構(gòu)體因?yàn)榻Y(jié)構(gòu)體可能包含填充字節(jié)逐字節(jié)比較結(jié)果不可靠。PDF 第 36 節(jié)專(zhuān)門(mén)講了結(jié)構(gòu)體變量的賦值與比較賦值可以用逐成員復(fù)制但比較必須逐成員進(jìn)行。解決寫(xiě)一個(gè)比較函數(shù)逐個(gè)成員用比較全部相等才返回真。如果結(jié)構(gòu)體里有數(shù)組或指針還需要更復(fù)雜的比較邏輯。4.4 文件打開(kāi)后忘記關(guān)閉程序跑久了句柄耗盡現(xiàn)象程序循環(huán)打開(kāi)文件處理數(shù)據(jù)跑一段時(shí)間后fopen返回NULL報(bào)「打開(kāi)文件失敗」。原因每次fopen都會(huì)占用一個(gè)文件句柄操作系統(tǒng)對(duì)同時(shí)打開(kāi)的文件數(shù)量有限制。忘記fclose會(huì)導(dǎo)致句柄泄漏最終耗盡。解決fopen和fclose必須成對(duì)出現(xiàn)。PDF 第 46 到第 49 節(jié)的文件操作四連發(fā)里反復(fù)強(qiáng)調(diào)了這一點(diǎn)。我一般會(huì)在fopen之后立即寫(xiě)fclose然后再往中間填讀寫(xiě)代碼這樣不容易忘。4.5scanf讀字符串不檢查長(zhǎng)度緩沖區(qū)溢出防不勝防現(xiàn)象用scanf(%s, buf);讀用戶(hù)輸入輸入超長(zhǎng)字符串時(shí)程序崩潰或行為異常。原因scanf的%s不檢查目標(biāo)緩沖區(qū)大小輸入超過(guò)buf容量就會(huì)溢出覆蓋相鄰內(nèi)存。解決用scanf(%99s, buf);限定最大讀取長(zhǎng)度假設(shè)buf是 100 字節(jié)或者改用fgets(buf, sizeof(buf), stdin);。PDF 第 44 和 45 節(jié)的 I/O 函數(shù)里提到了scanf的用法但長(zhǎng)度限制這個(gè)坑需要自己留心。5. 進(jìn)階用法把 PDF 當(dāng)查詢(xún)手冊(cè)配合編譯器警告和調(diào)試器5.1 用-Wall -Wextra把編譯器變成你的代碼審查員PDF 第 2 節(jié)講基本語(yǔ)法概念時(shí)提到「現(xiàn)在計(jì)算機(jī)編輯器都有檢查功能」但沒(méi)展開(kāi)說(shuō)編譯器警告的用法。我自己的習(xí)慣是GCC 編譯時(shí)永遠(yuǎn)帶上-Wall -Wextra -Werror。-Wall打開(kāi)常用警告-Wextra打開(kāi)額外警告-Werror把警告當(dāng)錯(cuò)誤處理強(qiáng)制自己修掉每一個(gè)警告。比如下面這段代碼#include stdio.h int main(void) { int x; // 聲明了但未初始化 printf(%d\n, x); // 使用未初始化變量 return 0; }用gcc -Wall -Wextra -Werror -o test test.c編譯會(huì)直接報(bào)錯(cuò)「‘x’ is used uninitialized」不讓你通過(guò)。這個(gè)習(xí)慣能幫你提前發(fā)現(xiàn)很多 PDF 里提到的「未定義行為」。5.2 用 GDB 單步跟蹤指針和內(nèi)存變化PDF 第 20 節(jié)指針和第 33 節(jié)內(nèi)存管理是兩份最容易翻車(chē)的內(nèi)容。光看代碼很難理解指針指向哪里、內(nèi)存怎么分配。我一般會(huì)建議在 Linux 下用 GDB 單步跟蹤# 編譯時(shí)加 -g 生成調(diào)試信息 gcc -g -o debug_test debug_test.c # 啟動(dòng) GDB gdb ./debug_test # 在 main 函數(shù)處打斷點(diǎn) (gdb) break main # 運(yùn)行 (gdb) run # 單步執(zhí)行 (gdb) next # 打印變量值 (gdb) print x # 打印指針指向的值 (gdb) print *p # 查看內(nèi)存布局 (gdb) x/4xw px/4xw p表示從p指向的地址開(kāi)始以十六進(jìn)制字word為單位顯示 4 個(gè)值。這個(gè)命令在調(diào)試數(shù)組和動(dòng)態(tài)內(nèi)存時(shí)特別有用能直觀看到內(nèi)存里存了什么。5.3 把 69 節(jié)當(dāng)索引按需查閱而不是從頭讀到尾這份 PDF 最大的價(jià)值不是讓你從第 1 節(jié)讀到第 69 節(jié)而是當(dāng)你在實(shí)際編碼中遇到問(wèn)題時(shí)能快速定位到相關(guān)章節(jié)。比如你寫(xiě)文件操作時(shí)遇到fseek定位不對(duì)直接翻到第 48 節(jié)文件操作三比如你搞不清typedef和#define的區(qū)別翻到第 40 節(jié)和第 42 節(jié)對(duì)照看。我自己的用法是在 PDF 里用關(guān)鍵詞搜索找到對(duì)應(yīng)小節(jié)看文字解釋然后對(duì)照案例代碼改自己的程序。這種「查詢(xún)式學(xué)習(xí)」比從頭啃到尾效率高得多尤其適合已經(jīng)有一定基礎(chǔ)、需要補(bǔ)特定知識(shí)點(diǎn)的從業(yè)者。5.4 一個(gè)具體技巧用條件編譯隔離調(diào)試代碼PDF 第 42 和 43 節(jié)的預(yù)處理指令里講了條件編譯但沒(méi)給一個(gè)完整的調(diào)試場(chǎng)景。我一般會(huì)在代碼里這樣寫(xiě)#include stdio.h // 定義 DEBUG 宏時(shí)啟用調(diào)試輸出注釋掉則關(guān)閉 #define DEBUG int main(void) { int x 10; #ifdef DEBUG printf(調(diào)試信息x %d\n, x); #endif printf(正式輸出x %d\n, x); return 0; }編譯時(shí)如果不想看到調(diào)試信息把#define DEBUG注釋掉即可不用刪代碼。這個(gè)技巧在排查 PDF 第 46 到 49 節(jié)文件操作問(wèn)題時(shí)特別有用——你可以在每個(gè)fopen、fread、fwrite后面加調(diào)試輸出定位到底哪一步出了問(wèn)題。5.5 驗(yàn)證方法用sizeof和limits.h確認(rèn)類(lèi)型邊界PDF 第 17 節(jié)專(zhuān)門(mén)講了sizeof運(yùn)算符第 56 節(jié)講了limits.h。這兩個(gè)結(jié)合起來(lái)可以驗(yàn)證你對(duì)數(shù)據(jù)類(lèi)型的理解是否正確。比如下面這段代碼#include stdio.h #include limits.h int main(void) { printf(int 大小%zu 字節(jié)\n, sizeof(int)); printf(int 最大值%d\n, INT_MAX); printf(int 最小值%d\n, INT_MIN); printf(char 是否帶符號(hào)%d\n, CHAR_MIN 0); return 0; }在 32 位和 64 位系統(tǒng)上跑一遍對(duì)比輸出結(jié)果能直觀感受到「同一份代碼在不同平臺(tái)上行為可能不同」。這個(gè)驗(yàn)證習(xí)慣能幫你避開(kāi)很多跨平臺(tái)移植的坑。從那以后我每次拿到一份新的 C 語(yǔ)言資料都會(huì)先翻目錄看它覆蓋了哪些頭文件、哪些標(biāo)準(zhǔn)庫(kù)函數(shù)然后挑三個(gè)自己最不熟悉的點(diǎn)寫(xiě)代碼驗(yàn)證一遍。這份 PDF 的 69 節(jié)結(jié)構(gòu)正好適合這種用法——它不是讓你從頭讀到尾的教程而是一本可以隨時(shí)翻查、隨時(shí)驗(yàn)證的案頭手冊(cè)。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取