:基础知识大杂烩)
前言这是一篇面向初学者的 C 入门梳理。标题叫大杂烩确实是因为入门阶段需要同时建立好几条线索程序怎么从源码变成可执行文件、变量和类型是怎么回事、控制流和函数怎么写、类为什么存在、标准库能帮我们省掉哪些重复劳动。这些内容单独看都不难混在一起就容易只见树木不见森林。入门阶段最常见的误解有三个。第一把 C 当成带类的 C以为了解printf和for循环就算入门结果一遇到std::string、引用、构造函数就卡住。第二过早纠结语法细节比如指针的指针、模板偏特化却不知道标准库的std::vector和std::string已经能覆盖绝大多数日常需求。第三照抄网上代码但不理解生命周期写出返回局部变量引用的函数程序在某个时刻莫名崩溃。本文按编译模型 → 类型与变量 → 控制流与函数 → 类 → 标准库的顺序走一遍每个部分只讲最核心的概念并给出一个综合示例。代码以 C17 为基准。文中不会声称任何代码已经编译运行所有结论都按标准语义描述读者应当在本地工具链上自行验证。一、从源码到可执行文件C 是编译型语言。一个源文件大致要经过四个阶段阶段工具主要工作预处理预处理器展开#include、替换宏、处理条件编译编译编译器把翻译单元转成汇编或目标代码做类型检查汇编汇编器把汇编转成机器码形式的目标文件链接链接器合并目标文件与库解析符号生成可执行文件初学者最容易忽略的是声明与定义的区别。声明告诉编译器有这么个名字类型是这样定义则真正分配存储或给出函数体。一个函数可以声明多次但只能定义一次。头文件里通常放声明源文件里放定义原因就在这里。最小的 C 程序长这样// 适用于 C17#include iostreamint main() {std::cout Hello, C\n;return 0;}main的返回类型必须是intvoid main()从来不是标准写法某些编译器容忍它只是扩展。main函数比较特殊不写return语句时标准规定等价于返回 0。std::cout是标准输出流\n是换行符比std::endl少一次强制刷新通常更合适。源文件的扩展名.cpp、.cc、.cxx由编译器约定标准并不规定。编译命令因工具链而异本文不展开具体命令行参数。二、类型、变量与初始化C 是静态类型语言每个变量在编译期就有确定的类型。常用基础类型包括int、long long、double、char、bool。标准只规定了最小范围例如int至少 16 位实际宽度由实现决定sizeof(long)在 Windows 的 LLP64 模型下通常是 4 字节在 Linux 的 LP64 模型下通常是 8 字节。写跨平台代码时不要假设具体宽度需要固定宽度就用cstdint里的std::int32_t、std::int64_t。初始化方式有好几种推荐使用花括号初始化// 适用于 C17int a 0; // 复制初始化int b(0); // 直接初始化int c{0}; // 列表初始化推荐double d{1.5};bool flag{false};花括号初始化的好处是会拒绝窄化转换。例如int x{3.14};在标准 C 中是编译错误而int x 3.14;只会悄悄截断。这类把错误提前到编译期的特性入门阶段就应该养成习惯。auto让编译器从初始化表达式推导类型从 C11 起可用。它减少了冗长的类型名但不要滥用当读者需要从代码直接看出类型时写明确类型更清晰。const表示初始化后不可修改应当优先使用它能帮助编译器优化也能防止误改。constexpr表示可以在编译期求值从 C11 起可用C14 起对函数体的限制放宽。// 适用于 C17const int kMaxCount 100;constexpr int kSquare(int x) { return x * x; }auto name std::string{C};命名习惯上常量常用k前缀或全大写变量用小驼峰或下划线分隔团队统一即可标准不做要求。三、控制流与函数控制流语句与 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) {// 只读遍历避免拷贝}函数由返回类型、名字、参数列表和函数体组成。C 支持重载同名函数只要参数列表不同即可共存返回类型不参与重载决议。默认参数只能从右向左连续给出。函数声明放在头文件定义放在源文件或者对短小的函数使用inline。参数传递有三种常见方式选择哪种直接影响语义和开销方式写法适用场景传值void f(int x)小型平凡类型或需要在函数内修改副本传引用void f(std::string s)需要修改调用方对象传常量引用void f(const std::string s)只读且对象较大避免拷贝传右值引用void f(std::string s)移动语义通常配合std::move返回局部变量的引用或指针是典型的悬垂引用属于未定义行为。返回局部变量本身按值是安全的编译器会做返回值优化或移动。四、类与对象入门类把数据和对数据的操作放在一起。一个最基本的类包含构造函数、成员函数和访问控制// 适用于 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_;};这里有几个入门必须掌握的点。构造函数初始化列表按成员声明顺序执行不是按书写顺序所以成员初始化最好与声明顺序一致。area() const末尾的const表示该函数不修改对象状态const 对象只能调用 const 成员函数。数据成员设为private外部通过公开接口访问这就是封装。如果类需要被继承并且会通过基类指针删除派生类对象基类析构函数必须是virtual否则是未定义行为。类如果管理了资源比如裸指针要么手写析构、拷贝构造、拷贝赋值规则三要么用 RAII 成员让编译器生成零规则。现代 C 强烈推荐零规则用std::string、std::vector、std::unique_ptr作为成员不手写资源管理代码。对象切片也值得提前知道把派生类对象按值传给基类参数只会保留基类部分派生类的数据和行为丢失。需要多态时应传引用或指针。五、标准库先用起来再谈原理初学者不必急着实现容器。标准库已经提供了高质量的基础设施std::string动态长度的字符串替代char[]和strcpy。std::vectorT动态数组连续存储随机访问 O(1)尾部追加均摊 O(1)。std::arrayT, N固定长度数组不涉及堆分配。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与有符号整数比较时要注意转换规则编译器通常会给出警告。六、综合示例统计学生成绩下面这个程序把结构体、容器、范围 for、lambda、排序和函数组合在一起适合作为第一阶段的练手目标。// 适用于 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转成double再除避免整数除法截断。按标准语义输出顺序应为 Carol、Alice、Bob平均分为 84.333...具体小数位数由std::cout的默认精度决定。常见坑点❌ 写void main()。 ✅ 标准要求int main()末尾可省略return 0;。❌ 用int x 3.14;做初始化窄化转换被静默接受。 ✅ 用int x{3.14};让编译器在窄化时报错。❌ 返回函数内局部变量的引用或指针。 ✅ 按值返回或返回生命周期覆盖调用方的对象。❌ 在头文件里写using namespace std;。 ✅ 头文件用完整限定名源文件中也优先显式写std::。❌ 用std::vector::operator[]访问不确定是否越界的位置。 ✅ 需要检查时用at()或先判断下标小于size()。❌std::sort的比较器写成a.score b.score。 ✅ 使用严格弱序a.score b.score相等时返回false。❌ 认为sizeof(long)在所有平台都是 8 字节。 ✅ 它是实现定义行为需要固定宽度用cstdint的类型。❌ 手写new[]/delete[]管理数组。 ✅ 用std::vector或std::array让 RAII 负责释放。总结主题入门要点编译模型预处理、编译、汇编、链接声明可多次定义只有一次类型与初始化静态类型优先花括号初始化const、auto、constexpr常用控制流与函数范围 for 遍历容器按值/引用/const 引用按需选择类构造函数初始化列表、const 成员函数、封装、虚析构标准库先用std::string、std::vector、std::sort不要重复造轮子安全习惯不返回局部引用、不越界、比较器满足严格弱序入门阶段最重要的不是记住所有语法而是建立类型—生命周期—所有权这条主线。把这条主线抓住后面学移动语义、智能指针、模板都会顺很多。