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