【C++地基】C++必备知识点 - 12 把基础语法串成一个小程序

发布时间:2026/9/4 6:30:47
【C++地基】C++必备知识点 - 12 把基础语法串成一个小程序 博主介绍程序喵大人35 - 资深C/C/Rust/Android/iOS客户端开发10年大厂工作经验嵌入式/人工智能/自动驾驶/音视频/游戏开发入门级选手《C20高级编程》《C23高级编程》等多本书籍著译者更多原创精品文章首发gzh见文末记得订阅专栏以防走丢C基础系列专栏C语言基础系列专栏C大佬养成攻略专栏C训练营个人网站好文推荐【AIAgent项目】从零构建一个代码PRAgent【C地基】C必备知识点 - 01 一个最小 C 程序是怎么跑起来的【C地基】C必备知识点 - 02 类型和变量给一块数据定规则、起名字【C地基】C必备知识点 - 03 表达式和运算符一行代码怎样算出结果【C地基】C必备知识点 - 04 控制流程序为什么走这条路【C地基】C必备知识点 - 05 函数把一段逻辑装进一个可调用的盒子【C地基】C必备知识点 - 06 作用域和生命周期名字在哪能用对象活到什么时候【C地基】C必备知识点 - 07 引用给同一个对象再起一个名字https://blog.csdn.net/qq_29426201/article/details/164094785?spm1001.2014.3001.5501)【C地基】C必备知识点 - 08 const把“这里是否允许修改“写进类型里【C地基】C必备知识点 - 09 指针入门变量的地址也是一种数据【C地基】C必备知识点 - 10 auto让编译器替你写类型名【C地基】C必备知识点 - 11 枚举和命名空间给值和名字划边界前面的 11 章讲的是一块块独立的基础能力。变量负责保存数据表达式负责计算结果控制流负责决定执行路线函数负责拆分逻辑作用域负责管理名字的可见性引用和指针提供了不同的访问路径const把修改权限写进了类型声明auto简化了局部的类型书写枚举和命名空间则为值和名字划出了清晰的边界。在这最后一章中我们要做的事情就是把所有这些零散的积木拼装到一起组成一个完整的小程序成绩报告器。程序的目标非常简单这个程序仍然只是一个命令行小程序但它的内部结构会比之前任何一个最小示例都完整得多。通过它你能够亲眼看到这些基础语法是如何各司其职共同服务于一个真实业务任务的。作为收束章本章的重点并不在于引入新概念而是将前面学过的所有知识放进同一条主线里运行。一个能够被顺畅阅读、安全修改和灵活扩展的小程序依赖的正是每个语法点在各自岗位上承担起清晰的职责——变量保存状态函数拆开步骤枚举表达固定结果const引用声明只读访问命名空间把工具函数收拢到统一的范围之下。先看数据流在动手写代码之前更稳妥的做法是先把数据在程序中的流动路径想清楚。成绩报告器需要一组int类型的分数作为输入std::vectorintscores;从外部输入进来的分数首先要经过范围检查。合法的分数被收入scores容器非法的分数则被跳过或者打印提示信息。拿到一份完整的合法分数列表之后程序就可以着手计算平均值doubleaverageAverage(scores);随后根据平均值判断出对应的成绩等级GradeLevel levelGetGradeLevel(average);最后将所有结果组织成一份报告输出到屏幕上PrintReport(scores);这条数据流决定了函数应当如何拆分。当每个函数只负责其中一个环节的小任务时主流程就会变得非常清晰。很多初学者的习惯是一上来就直接动手写main写着写着就会发现变量越堆越多、分支越嵌越深。相比之下更稳健的做法是在写代码之前先把数据从哪里进入、经过哪些加工步骤、最终以什么形态离开这条线索捋清楚。成绩报告器的数据流虽然很短但它已经具备了真实程序的基本骨架输入、校验、计算、转换、输出。这条数据流同时也能帮助我们判断每个变量应该安放在哪里。score只是在读取输入时需要的一个临时变量它的作用域应该被限制在main函数的输入循环附近。scores保存着经过筛选的合法成绩数据会贯穿后续的计算和输出环节。average是从scores中派生出来的中间结果适合放在PrintReport函数的内部。当变量的作用范围跟着数据流的走向来安排时程序里就不会到处散落着难以追踪的临时状态。数据流还能帮助我们判断函数之间的边界应该划在哪里。输入校验是一个独立的判断动作适合单独封装成IsValidScore求平均值是一个纯粹的计算过程适合单独封装成Average等级映射是一次状态转换适合单独封装成GetGradeLevel输出报告则是程序与外部世界交互的边界适合交由PrintReport来负责。函数的拆分不应该按照代码行数来机械划分而应该按照数据所经历的处理步骤来自然切割。用枚举表达等级成绩等级是一组固定的、可以穷举的值非常适合用enum class来建模enumclassGradeLevel{kExcellent,kGood,kPass,kFail,};相比直接使用字符串或整数来表示等级这种做法要稳固得多。GradeLevel这个类型名本身就在明确告诉阅读者等级只能从这组预定义的值中选取一个不存在其他可能性。后续用switch处理等级逻辑时每个分支的含义也会更加清晰。之所以在这里选择枚举而不是字符串是因为等级在本质上属于一组固定的内部状态。字符串适合在最终展示给用户的环节中使用而枚举则更适合在程序的内部逻辑中承载状态判断。把内部状态和外部展示这两个关注点分开处理是一种非常基础但极其重要的设计意识。将枚举值转换为展示字符串的逻辑可以封装成一个独立的函数std::stringToString(GradeLevel level){switch(level){caseGradeLevel::kExcellent:returnexcellent;caseGradeLevel::kGood:returngood;caseGradeLevel::kPass:returnpass;caseGradeLevel::kFail:returnfail;}returnunknown;}这个函数把等级应该如何展示这一关注点集中收拢到了一个可控的位置。将来如果想把excellent改成中文显示只需要修改这一个函数即可。这也体现了函数拆分的一个常见工程价值把变化点收拢起来。等级的划分规则发生了变化去改GetGradeLevel展示文字需要调整去改ToString分数的合法范围需要重新定义去改IsValidScore。每个函数的职责越单一后续需要改动时定位起来就越容易。将内部状态和外部展示分离开来是很多小程序走向工程级代码的第一步。程序内部始终使用GradeLevel::kExcellent来进行逻辑判断好处是类型系统能够提供稳定的安全保障到了输出环节再将其转换为excellent字符串好处是展示格式可以独立调整而不影响核心逻辑。如果从头到尾都用字符串来表达等级那么拼写错误、大小写差异、多语言切换等问题都会渗透进业务判断的代码中导致逻辑和展示纠缠在一起。枚举的作用就是把参与判断的状态牢牢约束住而字符串只需要在最终的输出环节各尽其责。用函数拆开每个步骤按照前面分析的数据流成绩报告器可以被拆分为以下几个函数boolIsValidScore(intscore);doubleAverage(conststd::vectorintscores);GradeLevelGetGradeLevel(doubleaverage);std::stringToString(GradeLevel level);voidPrintReport(conststd::vectorintscores);每个函数的职责都非常单一各自只负责数据流中的一个处理环节。这份函数列表本身就可以当作数据流的文字版来阅读IsValidScore决定输入数据能否进入合法列表Average把一组数据压缩成一个平均值GetGradeLevel把平均分转换为一个枚举等级ToString把内部等级转换成展示文本PrintReport负责将最终结果组合输出。主函数只需要按照这个顺序依次调用它们程序的整体结构自然就浮现出来了。IsValidScore负责判断分数是否处于合法范围内boolIsValidScore(intscore){returnscore0score100;}Average负责计算一组成绩的平均值。它的参数声明为const std::vectorint表示函数只需要以只读方式访问外部的成绩列表同时避免了复制整个容器所带来的性能开销doubleAverage(conststd::vectorintscores){if(scores.empty()){return0.0;}inttotal0;for(constautoscore:scores){totalscore;}returnstatic_castdouble(total)/scores.size();}GetGradeLevel负责将平均分映射到对应的成绩等级GradeLevelGetGradeLevel(doubleaverage){if(average90.0){returnGradeLevel::kExcellent;}if(average80.0){returnGradeLevel::kGood;}if(average60.0){returnGradeLevel::kPass;}returnGradeLevel::kFail;}PrintReport负责最终的报告输出。有了它的存在主函数的逻辑会变得非常干净voidPrintReport(conststd::vectorintscores){if(scores.empty()){std::coutno valid scores\n;return;}constdoubleaverageAverage(scores);constGradeLevel levelGetGradeLevel(average);std::coutcount scores.size()\n;std::coutaverage average\n;std::coutlevel ToString(level)\n;}当函数拆分完成后main只需要表达最顶层的主线逻辑读取输入、保存合法成绩、调用输出报告。这就是一个小程序从能跑通进化到结构清楚的关键所在。把所有逻辑一股脑塞进main当然也能完成任务但阅读者就不得不在一个庞大的函数体内同时追踪输入、校验、求平均、评级和输出等多个逻辑层次。拆成函数之后main就像一本书的目录概括了程序运行的主要步骤而具体的实现细节则各自归位到了对应的函数中。后续在调试时也可以围绕某一个特定的函数进行独立检查而不需要在整个程序里大海捞针。这份函数列表还可以充当一份职责检查清单。IsValidScore只返回布尔值不负责任何输出Average只计算平均值不负责判断等级GetGradeLevel只将平均分映射为枚举不负责打印任何内容PrintReport负责组织和展示报告但不负责读取输入。一旦某个函数开始同时承担两件不同的事情它的命名通常就会变得含混不清后续的修改也更容易产生意料之外的连锁反应。函数拆分还有一个不那么显眼但很有价值的好处每个函数都可以用前面某一章的知识来独立解读。阅读IsValidScore时关注的主要是布尔表达式的逻辑阅读Average时关注的是const引用参数、循环遍历和类型转换阅读GetGradeLevel时关注的是控制流和枚举的配合阅读PrintReport时关注的是提前返回和函数之间的调用组合。基础语法从来不是一堆孤立的知识条目它们在这些小函数中各自找到了自己的用武之地。const引用和auto放在哪里在这一章的小程序中const引用最频繁的出场位置是函数参数doubleAverage(conststd::vectorintscores);voidPrintReport(conststd::vectorintscores);scores是一个可能包含大量元素的容器复制它毫无必要。函数只需要读取其中的数据所以参数声明为const std::vectorint。这个参数形态同时向外传达了两层信息不产生数据复制且承诺只读访问。在这里你能看到第 7 章和第 8 章的知识是如何在实际工程中组合运用的。表明函数直接接触的是外部的原始列表const则宣告函数通过这条参数路径只会进行读取操作。调用者甚至不需要打开Average的函数体去逐行检查仅凭函数签名就能确信它不会对传入的成绩列表做任何修改——这就是类型系统赋予接口的信息密度。auto在这个程序中最适合出现的位置是局部的循环遍历for(constautoscore:scores){totalscore;}这里容器中的元素类型是int按值复制的开销极低。写成const auto score则进一步表明循环体内部不会对这份临时副本进行修改。如果将来遍历的是一个字符串列表写法就应该相应调整for(constautoname:names){std::coutname\n;}归根结底判断的准则依然是前面章节中确立的那一套小对象遍历 → auto value 需要修改原元素 → auto value 只读大对象 → const auto valueauto在这个程序中的角色也是非常具体的。遍历std::vectorint时元素类型一目了然写const auto score可以减少不必要的类型重复遍历字符串或复杂对象列表时const auto既能表达只读语义又能避免复制开销。auto服务的是局部代码的简洁性但它不能替代你对是否复制、是否引用、是否只读这些本质问题的判断。在这个程序中选择const auto score而非const auto score理由很简单元素是int复制一个整数的成本几乎可以忽略不计循环体也没有任何修改它的意图。但如果将来scores的类型变成了std::vectorstd::string就必须重新审视这个决定不能机械照搬。auto的具体写法永远要和元素的大小、是否需要修改、是否只需只读访问这三个因素绑定在一起做判断。这正是第 10 章中着重强调的读写意图分析方法。完整代码下面是完整的、可编译运行的代码。在阅读完整代码时不建议从第一行开始一路硬啃到底。更有效的方式是先看命名空间里提供了哪些能力再看main函数是如何调用这些能力的。也可以反过来先读main的主流程把握全局遇到不清楚的函数名时再跳回到对应的定义去了解细节。真实工程中阅读代码的方式往往也是如此——先抓住主干流程再按需展开局部细节。#includeiostream#includestring#includevectornamespacegrade_report{enumclassGradeLevel{kExcellent,kGood,kPass,kFail,};boolIsValidScore(intscore){returnscore0score100;}doubleAverage(conststd::vectorintscores){if(scores.empty()){return0.0;}inttotal0;for(constautoscore:scores){totalscore;}returnstatic_castdouble(total)/scores.size();}GradeLevelGetGradeLevel(doubleaverage){if(average90.0){returnGradeLevel::kExcellent;}if(average80.0){returnGradeLevel::kGood;}if(average60.0){returnGradeLevel::kPass;}returnGradeLevel::kFail;}std::stringToString(GradeLevel level){switch(level){caseGradeLevel::kExcellent:returnexcellent;caseGradeLevel::kGood:returngood;caseGradeLevel::kPass:returnpass;caseGradeLevel::kFail:returnfail;}returnunknown;}voidPrintScores(conststd::vectorintscores){std::coutscores:;for(constautoscore:scores){std::cout score;}std::cout\n;}voidPrintReport(conststd::vectorintscores){if(scores.empty()){std::coutno valid scores\n;return;}constdoubleaverageAverage(scores);constGradeLevel levelGetGradeLevel(average);PrintScores(scores);std::coutcount scores.size()\n;std::coutaverage average\n;std::coutlevel ToString(level)\n;}}// namespace grade_reportintmain(){std::vectorintscores;std::coutEnter scores, non-number to finish:\n;intscore0;while(std::cinscore){if(grade_report::IsValidScore(score)){scores.push_back(score);}else{std::coutignored invalid score: score\n;}}grade_report::PrintReport(scores);return0;}编译方式如下clang-stdc20-Wallgrade_report.cc-ograde_report运行后输入100 80 70 -1 200 x程序会自动忽略-1和200这两个超出合法范围的输入在遇到非数字字符x时结束输入循环然后输出由合法成绩构成的完整报告。把完整代码放在这里目的是为前面分散在各个章节中的语法知识提供一个共同的落脚点。阅读这段程序时可以先从main的主流程入手把握全貌遇到某个函数的细节不太确定时再跳回定义去核实——这种阅读方式比从第一行开始逐行死磕要高效得多也更接近真实工程中的代码阅读习惯。这段代码在设计上还做了一个重要的取舍它没有为了追求表面上的完整性而引入尚未讲解的概念。输入失败时程序通过while (std::cin score)自然结束循环无效分数只做简单的提示打印成绩列表为空时由PrintReport提前返回处理。它没有涉及异常机制、文件读写、类的封装或者复杂的错误处理体系。第一阶段小程序的目标是把结构写清楚而不是一口气覆盖所有的工程能力。这份代码里串起了哪些基础语法std::vectorint scores;使用一个容器来保存多份成绩数据。int score 0;则是输入环节中使用的临时变量。while (std::cin score)利用输入表达式的返回值来控制循环的持续与终止。只要程序能够成功读取到一个整数循环就继续执行当输入流遇到非数字字符导致读取失败时循环自然结束。if (grade_report::IsValidScore(score))根据函数返回的bool值来选择接下来的执行路线。合法分数被收入scores容器非法分数则打印一条提示信息后跳过。namespace grade_report把成绩报告器相关的所有函数和枚举收纳在同一个命名范围之内。主函数通过grade_report::前缀来访问它们。enum class GradeLevel将成绩等级表达为一组有限的固定值集合。它让GetGradeLevel函数的返回值比使用普通字符串要稳定和安全得多。Average(const std::vectorint scores)和PrintReport(const std::vectorint scores)都使用了只读引用参数来访问成绩列表。函数既不需要复制整个容器的数据也无法通过这条参数路径去修改容器的内容。for (const auto score : scores)在循环中遍历每一个成绩元素。由于元素类型是int复制的开销微乎其微。如果将来元素换成了体积更大的对象类型可以相应改写为const auto来避免不必要的复制。static_castdouble(total) / scores.size()通过显式的类型转换确保除法运算走的是浮点数计算路径。这里呼应的正是表达式章节中讨论过的整数除法截断问题。这份程序没有引入类、异常、文件 IO、动态内存管理和智能指针中的任何一项。它仅仅使用基础语法就完成了一个结构清晰的小任务这也正是第一阶段的核心目标先把小程序写清楚。在这段代码中基础语法之间的分工协作关系也展现得非常充分。while负责驱动输入循环if负责处理合法性分支函数负责拆分各个处理步骤const引用负责声明只读传参的接口意图枚举负责精确表达结果状态命名空间负责为一组相关名字划定归属。每一个语法工具都只做了一件朴素的事情但当它们协同起来程序就具备了清晰的结构。阅读这段程序时可以按照以下这条数据流路线来进行完整的复盘理清这条路线比逐行背诵语法规则要重要得多。只要你能够说出数据是从哪里进入程序的、中途经过了哪些函数的加工处理、最终又以什么形式被呈现出来你就已经在用工程化的方式去阅读 C 代码了。这个小程序还能怎么扩展呢如果要沿着工程化的方向继续发展可以在此基础上添加很多能力。比如从文件中读取成绩数据把无效输入记录到错误日志中用类来封装报告对象用异常机制来处理输入错误用单元测试来覆盖各种边界情况用std::optional来表达可能不存在结果的语义。这些都属于后续阶段的内容。当前这份程序已经完整地展示了基础 C 的核心骨架数据进入程序类型约束数据的合法性表达式计算结果控制流选择执行路线函数拆分逻辑作用域控制名字的可见范围引用和const管理访问方式与修改权限枚举表达固定状态命名空间组织代码结构。当基础程序能够写得清楚明白之后下一步就该深入到对象本身的世界中去了。对象是怎样被创建和销毁的复制一个对象时究竟会发生什么移动语义为什么在现代 C 中如此重要资源又是如何跟随对象的生命周期自动释放的——这些问题会把你带向后续的对象生命周期、资源管理和 STL 系列。本章的收束点是把语法知识点转化为程序的组织结构变量、表达式、控制流、函数、作用域、引用、const、指针、auto、枚举、命名空间每一个都可以被单独拎出来讲解但在真正编写程序时它们永远是协同出场的。在基础阶段不需要急于追求写出多么庞大的项目先把这种规模的小程序写清楚数据流清楚函数职责清楚类型意图清楚名字边界清楚。当这些基本功扎实之后后面再去学习对象模型、容器库、资源管理和工程实践时才会拥有一个稳定的底座。码字不易欢迎大家点赞关注评论谢谢