入門教程(一):基礎(chǔ)知識(shí)大雜燴)
前言這是一篇面向初學(xué)者的 C 入門梳理。標(biāo)題叫大雜燴確實(shí)是因?yàn)槿腴T階段需要同時(shí)建立好幾條線索程序怎么從源碼變成可執(zhí)行文件、變量和類型是怎么回事、控制流和函數(shù)怎么寫(xiě)、類為什么存在、標(biāo)準(zhǔn)庫(kù)能幫我們省掉哪些重復(fù)勞動(dòng)。這些內(nèi)容單獨(dú)看都不難混在一起就容易只見(jiàn)樹(shù)木不見(jiàn)森林。入門階段最常見(jiàn)的誤解有三個(gè)。第一把 C 當(dāng)成帶類的 C以為了解printf和for循環(huán)就算入門結(jié)果一遇到std::string、引用、構(gòu)造函數(shù)就卡住。第二過(guò)早糾結(jié)語(yǔ)法細(xì)節(jié)比如指針的指針、模板偏特化卻不知道標(biāo)準(zhǔn)庫(kù)的std::vector和std::string已經(jīng)能覆蓋絕大多數(shù)日常需求。第三照抄網(wǎng)上代碼但不理解生命周期寫(xiě)出返回局部變量引用的函數(shù)程序在某個(gè)時(shí)刻莫名崩潰。本文按編譯模型 → 類型與變量 → 控制流與函數(shù) → 類 → 標(biāo)準(zhǔn)庫(kù)的順序走一遍每個(gè)部分只講最核心的概念并給出一個(gè)綜合示例。代碼以 C17 為基準(zhǔn)。文中不會(huì)聲稱任何代碼已經(jīng)編譯運(yùn)行所有結(jié)論都按標(biāo)準(zhǔn)語(yǔ)義描述讀者應(yīng)當(dāng)在本地工具鏈上自行驗(yàn)證。一、從源碼到可執(zhí)行文件C 是編譯型語(yǔ)言。一個(gè)源文件大致要經(jīng)過(guò)四個(gè)階段階段工具主要工作預(yù)處理預(yù)處理器展開(kāi)#include、替換宏、處理?xiàng)l件編譯編譯編譯器把翻譯單元轉(zhuǎn)成匯編或目標(biāo)代碼做類型檢查匯編匯編器把匯編轉(zhuǎn)成機(jī)器碼形式的目標(biāo)文件鏈接鏈接器合并目標(biāo)文件與庫(kù)解析符號(hào)生成可執(zhí)行文件初學(xué)者最容易忽略的是聲明與定義的區(qū)別。聲明告訴編譯器有這么個(gè)名字類型是這樣定義則真正分配存儲(chǔ)或給出函數(shù)體。一個(gè)函數(shù)可以聲明多次但只能定義一次。頭文件里通常放聲明源文件里放定義原因就在這里。最小的 C 程序長(zhǎng)這樣// 適用于 C17#include iostreamint main() {std::cout Hello, C\n;return 0;}main的返回類型必須是intvoid main()從來(lái)不是標(biāo)準(zhǔn)寫(xiě)法某些編譯器容忍它只是擴(kuò)展。main函數(shù)比較特殊不寫(xiě)return語(yǔ)句時(shí)標(biāo)準(zhǔn)規(guī)定等價(jià)于返回 0。std::cout是標(biāo)準(zhǔn)輸出流\n是換行符比std::endl少一次強(qiáng)制刷新通常更合適。源文件的擴(kuò)展名.cpp、.cc、.cxx由編譯器約定標(biāo)準(zhǔn)并不規(guī)定。編譯命令因工具鏈而異本文不展開(kāi)具體命令行參數(shù)。二、類型、變量與初始化C 是靜態(tài)類型語(yǔ)言每個(gè)變量在編譯期就有確定的類型。常用基礎(chǔ)類型包括int、long long、double、char、bool。標(biāo)準(zhǔn)只規(guī)定了最小范圍例如int至少 16 位實(shí)際寬度由實(shí)現(xiàn)決定sizeof(long)在 Windows 的 LLP64 模型下通常是 4 字節(jié)在 Linux 的 LP64 模型下通常是 8 字節(jié)。寫(xiě)跨平臺(tái)代碼時(shí)不要假設(shè)具體寬度需要固定寬度就用cstdint里的std::int32_t、std::int64_t。初始化方式有好幾種推薦使用花括號(hào)初始化// 適用于 C17int a 0; // 復(fù)制初始化int b(0); // 直接初始化int c{0}; // 列表初始化推薦double d{1.5};bool flag{false};花括號(hào)初始化的好處是會(huì)拒絕窄化轉(zhuǎn)換。例如int x{3.14};在標(biāo)準(zhǔn) C 中是編譯錯(cuò)誤而int x 3.14;只會(huì)悄悄截?cái)?。這類把錯(cuò)誤提前到編譯期的特性入門階段就應(yīng)該養(yǎng)成習(xí)慣。auto讓編譯器從初始化表達(dá)式推導(dǎo)類型從 C11 起可用。它減少了冗長(zhǎng)的類型名但不要濫用當(dāng)讀者需要從代碼直接看出類型時(shí)寫(xiě)明確類型更清晰。const表示初始化后不可修改應(yīng)當(dāng)優(yōu)先使用它能幫助編譯器優(yōu)化也能防止誤改。constexpr表示可以在編譯期求值從 C11 起可用C14 起對(duì)函數(shù)體的限制放寬。// 適用于 C17const int kMaxCount 100;constexpr int kSquare(int x) { return x * x; }auto name std::string{C};命名習(xí)慣上常量常用k前綴或全大寫(xiě)變量用小駝峰或下劃線分隔團(tuán)隊(duì)統(tǒng)一即可標(biāo)準(zhǔn)不做要求。三、控制流與函數(shù)控制流語(yǔ)句與 C 類似if/else、switch、for、while、do/while、break、continue。C11 增加了范圍for遍歷容器時(shí)非常方便// 適用于 C17std::vectorint values{1, 2, 3, 4, 5};int sum 0;for (int v : values) {sum v;}for (const auto v : values) {// 只讀遍歷避免拷貝}函數(shù)由返回類型、名字、參數(shù)列表和函數(shù)體組成。C 支持重載同名函數(shù)只要參數(shù)列表不同即可共存返回類型不參與重載決議。默認(rèn)參數(shù)只能從右向左連續(xù)給出。函數(shù)聲明放在頭文件定義放在源文件或者對(duì)短小的函數(shù)使用inline。參數(shù)傳遞有三種常見(jiàn)方式選擇哪種直接影響語(yǔ)義和開(kāi)銷方式寫(xiě)法適用場(chǎng)景傳值void f(int x)小型平凡類型或需要在函數(shù)內(nèi)修改副本傳引用void f(std::string s)需要修改調(diào)用方對(duì)象傳常量引用void f(const std::string s)只讀且對(duì)象較大避免拷貝傳右值引用void f(std::string s)移動(dòng)語(yǔ)義通常配合std::move返回局部變量的引用或指針是典型的懸垂引用屬于未定義行為。返回局部變量本身按值是安全的編譯器會(huì)做返回值優(yōu)化或移動(dòng)。四、類與對(duì)象入門類把數(shù)據(jù)和對(duì)數(shù)據(jù)的操作放在一起。一個(gè)最基本的類包含構(gòu)造函數(shù)、成員函數(shù)和訪問(wèn)控制// 適用于 C17#include string#include utilityclass Rectangle {public:Rectangle(double width, double height): width_(width), height_(height) {}double area() const { return width_ * height_; }void scale(double factor) {width_ * factor;height_ * factor;}private:double width_;double height_;};這里有幾個(gè)入門必須掌握的點(diǎn)。構(gòu)造函數(shù)初始化列表按成員聲明順序執(zhí)行不是按書(shū)寫(xiě)順序所以成員初始化最好與聲明順序一致。area() const末尾的const表示該函數(shù)不修改對(duì)象狀態(tài)const 對(duì)象只能調(diào)用 const 成員函數(shù)。數(shù)據(jù)成員設(shè)為private外部通過(guò)公開(kāi)接口訪問(wèn)這就是封裝。如果類需要被繼承并且會(huì)通過(guò)基類指針刪除派生類對(duì)象基類析構(gòu)函數(shù)必須是virtual否則是未定義行為。類如果管理了資源比如裸指針要么手寫(xiě)析構(gòu)、拷貝構(gòu)造、拷貝賦值規(guī)則三要么用 RAII 成員讓編譯器生成零規(guī)則?,F(xiàn)代 C 強(qiáng)烈推薦零規(guī)則用std::string、std::vector、std::unique_ptr作為成員不手寫(xiě)資源管理代碼。對(duì)象切片也值得提前知道把派生類對(duì)象按值傳給基類參數(shù)只會(huì)保留基類部分派生類的數(shù)據(jù)和行為丟失。需要多態(tài)時(shí)應(yīng)傳引用或指針。五、標(biāo)準(zhǔn)庫(kù)先用起來(lái)再談原理初學(xué)者不必急著實(shí)現(xiàn)容器。標(biāo)準(zhǔn)庫(kù)已經(jīng)提供了高質(zhì)量的基礎(chǔ)設(shè)施std::string動(dòng)態(tài)長(zhǎng)度的字符串替代char[]和strcpy。std::vectorT動(dòng)態(tài)數(shù)組連續(xù)存儲(chǔ)隨機(jī)訪問(wèn) O(1)尾部追加均攤 O(1)。std::arrayT, N固定長(zhǎng)度數(shù)組不涉及堆分配。std::map/std::unordered_map有序/無(wú)序鍵值容器。std::sort排序算法平均復(fù)雜度 O(n log n)要求比較器滿足嚴(yán)格弱序。std::iostream輸入輸出流。使用容器要包含對(duì)應(yīng)頭文件并注意std::限定。不要在頭文件里寫(xiě)using namespace std;那會(huì)把整個(gè)命名空間塞給所有包含者。源文件里為了簡(jiǎn)短可以用但更推薦顯式寫(xiě)std::。std::vector的operator[]不做邊界檢查越界是未定義行為需要檢查時(shí)用at()越界會(huì)拋std::out_of_range。size()返回的是無(wú)符號(hào)的std::size_t與有符號(hào)整數(shù)比較時(shí)要注意轉(zhuǎn)換規(guī)則編譯器通常會(huì)給出警告。六、綜合示例統(tǒng)計(jì)學(xué)生成績(jī)下面這個(gè)程序把結(jié)構(gòu)體、容器、范圍 for、lambda、排序和函數(shù)組合在一起適合作為第一階段的練手目標(biāo)。// 適用于 C17#include algorithm#include iostream#include string#include vectorstruct Student {std::string name;int score;};double average(const std::vectorStudent students) {if (students.empty()) {return 0.0;}int sum 0;for (const auto s : students) {sum s.score;}return static_castdouble(sum) / static_castdouble(students.size());}int main() {std::vectorStudent students{{Alice, 90},{Bob, 75},{Carol, 88}};std::sort(students.begin(), students.end(),[](const Student a, const Student b) {return a.score b.score;});for (const auto s : students) {std::cout s.name : s.score \n;}std::cout average: average(students) \n;return 0;}逐點(diǎn)說(shuō)明。Student是聚合類型可以用花括號(hào)按成員順序初始化。std::vector的初始化列表構(gòu)造會(huì)依次復(fù)制這些元素。std::sort的比較器返回a.score b.score表示降序比較器必須滿足嚴(yán)格弱序用是常見(jiàn)錯(cuò)誤會(huì)導(dǎo)致未定義行為。average接收 const 引用避免復(fù)制整個(gè)容器先判斷空容器避免除零把sum轉(zhuǎn)成double再除避免整數(shù)除法截?cái)?。按?biāo)準(zhǔn)語(yǔ)義輸出順序應(yīng)為 Carol、Alice、Bob平均分為 84.333...具體小數(shù)位數(shù)由std::cout的默認(rèn)精度決定。常見(jiàn)坑點(diǎn)? 寫(xiě)void main()。 ? 標(biāo)準(zhǔn)要求int main()末尾可省略return 0;。? 用int x 3.14;做初始化窄化轉(zhuǎn)換被靜默接受。 ? 用int x{3.14};讓編譯器在窄化時(shí)報(bào)錯(cuò)。? 返回函數(shù)內(nèi)局部變量的引用或指針。 ? 按值返回或返回生命周期覆蓋調(diào)用方的對(duì)象。? 在頭文件里寫(xiě)using namespace std;。 ? 頭文件用完整限定名源文件中也優(yōu)先顯式寫(xiě)std::。? 用std::vector::operator[]訪問(wèn)不確定是否越界的位置。 ? 需要檢查時(shí)用at()或先判斷下標(biāo)小于size()。?std::sort的比較器寫(xiě)成a.score b.score。 ? 使用嚴(yán)格弱序a.score b.score相等時(shí)返回false。? 認(rèn)為sizeof(long)在所有平臺(tái)都是 8 字節(jié)。 ? 它是實(shí)現(xiàn)定義行為需要固定寬度用cstdint的類型。? 手寫(xiě)new[]/delete[]管理數(shù)組。 ? 用std::vector或std::array讓 RAII 負(fù)責(zé)釋放??偨Y(jié)主題入門要點(diǎn)編譯模型預(yù)處理、編譯、匯編、鏈接聲明可多次定義只有一次類型與初始化靜態(tài)類型優(yōu)先花括號(hào)初始化const、auto、constexpr常用控制流與函數(shù)范圍 for 遍歷容器按值/引用/const 引用按需選擇類構(gòu)造函數(shù)初始化列表、const 成員函數(shù)、封裝、虛析構(gòu)標(biāo)準(zhǔn)庫(kù)先用std::string、std::vector、std::sort不要重復(fù)造輪子安全習(xí)慣不返回局部引用、不越界、比較器滿足嚴(yán)格弱序入門階段最重要的不是記住所有語(yǔ)法而是建立類型—生命周期—所有權(quán)這條主線。把這條主線抓住后面學(xué)移動(dòng)語(yǔ)義、智能指針、模板都會(huì)順很多。