资讯详情

20年前的C/C++编码规范:可读性优先,仍是团队落地标准

📅 2026/10/24 18:19:38 | 华诺云谱 👁 阅读
20年前的C/C++编码规范:可读性优先,仍是团队落地标准
简介《中兴通讯软件编程编码规范》是一份由中兴通讯技术中心研究部发布的企业级C/C编码标准适用于公司内使用C/C编码的所有软件项目也为外部开发者提供了系统化的编码指导。压缩包共1个文件为doc格式文档整体大小486KB内容完整包含规范正文、附录A编程模板与附录B规范检查表。规范从基本原则入手强调代码是为人编写、应保持简明清晰并针对布局缩进、注释写法、命名规则、变量与类型使用、表达式与语句、函数设计、可靠性、可测试性、断言与错误处理等核心环节给出了规则性约束和建议性指引同时提供大量正反例帮助理解。已有301人学习下载适合希望统一团队编码风格、提升代码可维护性与健壮性的开发者和技术管理者参考。1. 一份20年前的C/C编码规范凭什么还能当团队的落地标准很多项目组的C/C编码规范最后都长成了一个样子没人看也没人执行。我手头这份《软件编程规范——C/C》是某通信设备企业2003年发布的企业标准覆盖布局、注释、命名规则、变量与类型、可靠性、可测试性、断言与错误处理十几个章节附录还带了一份编程模板和一份规范检查表。它的核心立场很直接代码首先是写给人看的其次才给计算机。对于正在搭团队规范的技术负责人以及想统一自己C/C编码习惯的嵌入式与通信软件开发者这份标准给出的不是口号而是能直接抄进项目里的条款。工具链换了多少代可读性、可维护性这些诉求一点没变所以它到今天依然能当落地标准用。2. 先把总原则立住文件布局与类定义的基本盘这一章把规范里最“宏观”的两块拆开开头的基本原则决定代码的气质文件布局决定新人打开工程时先从哪读起。这两块没立住后面再细的格式条款都只是表面功夫。2.1 七条基本原则从“为人写程序”到“少用全局变量”规范开篇给了七条原则头一条就是“首先是为人编写程序其次才是计算机”。这句话不是鸡汤。通信设备软件的生命周期很长从开发、测试、生产到现场维护代码被阅读的次数远多于被执行的次数。一份难读的代码后续维护成本会成倍吃掉开发时省下的时间。其余几条也值得逐条对照保持代码简明清晰避免过分的编程技巧尽量遵循ANSI C标准不用编译器扩展先满足正确性再谈效率避免或少用全局变量尽量避免GOTO尽可能重用老代码减少同样错误出现的次数。其中“正确性优先于效率”在嵌入式领域经常被反着执行——有人为了省几条指令写出位运算炫技代码结果一换编译器就翻车。效率应该建立在清晰的逻辑之上让编译器去做优化比让人去猜你的意图可靠得多。这份规范还做了一层区分条款分成“规则性”和“建议性”。规则性条款要求所有开发人员严格执行建议性条款由项目组根据实际情况取舍。落地的时候我会把规则性条款直接写进团队的红线清单建议性条款放进编码模板里引导既保证底线不被突破又给项目留出灵活空间。提示原则不是摆设。代码评审时拿这七条逐项过一遍能挡住大部分“看着能跑但没法维护”的提交。2.2 头文件布局用固定次序消除“打开文件就懵”的问题规范要求头文件按固定次序书写文件头、防重复包含宏、条件编译选项、按标准库到非标准库排列的include、常量定义、全局宏、全局数据类型、类定义模板、全局函数原型、#endif。这个次序的核心逻辑是“从稳定到易变”先看到不常变的基础设施再看到业务类型与接口。下面是我按这份规范整理出的头文件骨架/* * 文件头模块名、创建日期、功能简述、修改记录 */ #ifndef MODULE_H #define MODULE_H #include stdio.h /* 标准库头文件用尖括号 */ #include string.h #include local_cfg.h /* 非标准库头文件用双引号 */ #define MODULE_MAX_BUF 256 #define MODULE_VERSION 1.0 typedef struct { int iLen; char *pcBuf; } ModuleBuf; int ModuleInit(void); int ModuleProcess(int iCmd, void *pvData); #endif /* MODULE_H */这里有两个容易忽略的约定。第一#include的顺序不能乱写标准库在前、非标准库在后能尽早暴露头文件自身的依赖缺失如果某个非标准头文件里用了标准库类型却没包含对应头文件编译时会在这个次序下快速报出来。第二防重复包含宏用MODULE_H这种全大写形式与文件名对应宏名别在多个头文件之间撞车否则后包含的那个头文件会被整个跳过报出一堆莫名其妙的未定义符号。条件编译选项排在防重复包含宏之后、include之前是为了让跨平台分支在一打开文件时就能看到。2.3 实现文件与类定义先声明后实现先公有后私有实现文件的布局更细文件头、include、常量定义、文件内部宏、文件内部数据类型、静态全局变量、局部函数原型、类实现、全局函数、局部函数。这条规则的意义在于把“对外可见”和“文件内部私有”严格分开。静态全局变量和局部函数原型放在靠前位置阅读者一眼就能看出哪些东西不跨文件评审时也方便确认封装边界。类的定义与实现同样有次序。定义文件里依次是公有属性、公有函数、保护属性、保护函数、私有属性、私有函数实现文件里依次是构造函数、析构函数、公有函数、保护函数、私有函数。我见过不少团队把私有函数插在公有函数中间理由是“顺手写的”结果头文件读起来像一道迷宫。按这份规范排序后接口区、继承区、私有区界限分明新成员改代码时不会误碰不该碰的东西。class CCount { public: CCount(void); ~CCount(void); int GetCount(void); void SetCount(int iCount); protected: int m_iProtectedCount; private: int m_iCount; }; CCount::CCount(void) /* 实现文件构造、析构、公有、保护、私有 */ { m_iCount 0; m_iProtectedCount 0; }这套布局规则不是排版洁癖。它让每个文件的结构可预期而可预期本身就是可维护性的一部分。下一章进入更具体的格式条款那是新手最容易“看着规范也对不上号”的地方。3. 布局细节落地缩进、大括号、空格与断行的可抄作业写法如果说第2章是骨架这一章就是血肉。布局部分的条款最细也最适合直接抄进编辑器配置和代码模板里。照着写代码风格立刻统一。3.1 缩进禁用TAB统一4个空格规范写得很绝对禁止使用TAB键必须使用空格进行缩进缩进为4个空格。当年这么定是因为不同编辑器的TAB宽度设置五花八门同一个\t在A编辑器里是4格到B编辑器里变成8格代码对齐瞬间崩塌。这个坑今天依然存在——只要跨IDE协作TAB宽度就有差异用空格就没有这个争端。void Function(int iVar) { if (iVar 0) { DoSomething(); /* 块内缩进4个空格 */ } }注意上面的写法里{独占一行并与函数名左对齐}也独占一行并回到同一列。我曾经在混合使用TAB和空格的代码里排查过一次对齐问题最终定位是某行前面混进了两个\t和六个空格肉眼完全看不出来。从那以后我所在的项目组编辑器一律配置“插入空格代替TAB”提交前再跑一次检查脚本这个问题就绝迹了。3.2 大括号规则if、else、for、while一律强制加{ }规范要求if、else、else if、for、while、do等语句自占一行执行语句不得紧跟其后不论执行语句有多少都要加{ }。这个条款最直接的价值是防止“悬挂else”和后续增加语句时忘记加括号。/* 反例执行语句紧跟if且没有大括号 */ if (iVar1 iVar2) iVar1 iVar2; /* 正例条件自占一行执行语句用大括号包住 */ if (iVar1 iVar2) { iVar1 iVar2; }反例在只有一行语句时没问题但下一个人往里加第二行时就容易写错归属。项目里有过血泪教训某段网上流传的代码维护的人想当然地在iVar1 iVar2;下面加了一行日志没加括号日志就变成了无条件执行。规范要求“不论多少都要加{ }”实际上是用一行代码的成本买断这一类低级故障。3.3 空格一元操作符不加二元操作符加关键字后留这一组规则看似琐碎作用却很大。一元操作符!、~、、--、*、地址前后不加空格[]、.、-前后不加空格二元操作符与操作数之间至少一个空格关键字if、for、while之后留空格再接左括号而函数名之后不留空格。!bValue iCount *strSource fValue fOldValue fNewValue; iCount 2; if (iVar 0) { DoSomething(iWidth, iHeight); } for (i 0; i MAX_BUF_NUM; i) { /* ... */ }这套约定的真正价值在于区分“关键字”和“函数”。if (和Foo(在视觉上一眼可辨带空格的是语法关键字紧跟括号的是函数调用。函数名后不留空格、括号向后紧跟、,之后留空格都服务于同一个目的——让代码的语法结构通过空白直接显形减少阅读时的解析负担。注释符与注释内容之间留一个空格也是同一套逻辑让注释正文不与定界符黏在一起。3.4 长表达式断行在低优先级操作符处拆操作符放行首规范要求超过80列的长表达式在低优先级操作符处拆行操作符放在新行之首方便在行首看到“这是个加法还是比较”。续行要对齐条件表达式在第一个条件处对齐for循环在初始化语句处对齐函数调用在第一个参数处对齐赋值语句在赋值号处对齐。if ((iFormat CH_A_FORMAT_M) (iOfficeType CH_BSC_M)) { /* 放行首条件对齐 */ DoSomething(); } for (iIndex 0; iIndex MAX_BUF_NUM; iIndex) { /* for续行对齐初始化处 */ /* ... */ } fTotalBill fTotalBill faCustomerPurchases[iID] /* 对齐赋值号 */ fSalesTax(faCustomerPurchases[iID]);行首放操作符的做法在评审时特别好用扫一眼每行行首的、||、就能看出这一段表达式是叠加还是分支。80列的限制则保证了代码在任何终端、任何对比工具里都完整可读不至于出现横向滚动才能看清的尴尬。函数声明时还有一条配套约束类型与名称不允许分行书写保证函数签名在搜索和diff时都是一个完整整体。4. 命名、注释与函数设计可读性是约定出来的布局解决“形”这一章解决“名”和“意”。命名规则、注释规范和函数设计要求是这份规范里被引用得最多的部分也是团队风格差距最容易暴露的地方。4.1 命名规则前缀表达类型后缀表达语义规范里的示例透露了这套命名体系的大致样貌指针变量用p前缀整数用i无符号字用w浮点用f字符用c字符串数组用str类名用C开头成员变量用m_打头再加类型前缀类型定义用大写或特定命名宏和常量用全大写加下划线。前缀/标记含义示例p指针float *pfBufferi整型int iLengthw字宽无符号WORD wTotalf浮点float fValuec字符char cFlagstr字符串数组char strName[32]m_类成员变量int m_iCountC类名开头class CCountfloat *pfBuffer; /* p指针f浮点 */ int iLength; /* i整型 */ WORD wTotal; /* w字宽 */ char cFlag; /* c字符 */ char strName[32]; /* str字符串 */ class CCount { public: CCount(void); int GetCount(void); void SetCount(int iCount); private: int m_iCount; /* m_成员变量 */ };这套前缀体系最大的价值在于“不用跳转定义就能猜出类型”看到pf开头就知道是指向浮点的指针看到m_就知道是类内部状态。它也有代价——类型一变名字就得跟着改否则就是货不对板。我一般会在团队里把前缀表做成固定对照表评审时按表核对比各人凭感觉起名可靠得多。变量名的语义部分用驼峰补齐函数名尽量动词开头像GetCount、SetCount这样调用点读起来已经像一句人话。4.2 注释规则写意图不写表面注释部分的条款相对简洁C语言注释用/* ... */C里多行用/* ... */、单行用// ...注释符与注释内容之间要留一个空格。更重要的是规范强调的注释层次——在功能、意图层次上做注释而不是重复代码表面。/* 反例注释复述代码毫无信息量 */ iCount; // 把iCount加1 /* 正例注释解释为什么这么做 */ iCount; // 补偿上一次查询遗漏的超时记录避免计数偏低写注释最容易犯的毛病就是把代码翻译成中文。无效注释不仅不帮忙还会随着代码修改变得越来越假最后误导后来人。我习惯的检查标准是把注释遮住只读代码能不能知道这行为什么存在如果不能说明注释缺的是“原因”而不是“动作”。文件头注释建议用固定模板模块名、功能简述、创建日期、修改记录这是附录里编程模板给出的做法能让一份文件的变更历史不依赖版本管理工具就快速追溯。4.3 函数与过程单一职责、参数检查、声明不断行函数设计的规则可以提炼成三条函数尽量小只做一件事参数和返回值要清晰必要时做有效性检查函数声明时类型与名称不允许分行书写保证签名一眼可读。/* 正例一次只做一件事参数检查前置 */ int ModuleSetBuf(ModuleBuf *ptBuf, int iLen) { if (ptBuf NULL) { return -1; } if (iLen 0 || iLen MODULE_MAX_BUF) { return -2; } ptBuf-iLen iLen; return 0; }函数内部逻辑清晰的重要标志是“读起来像文章”开头是参数校验中间是主流程结尾是返回值。参数校验放在最前面的好处是失败路径集中不会等执行到中段才发现参数不合法把状态改了一半才返回错误码。返回值方面我推荐用0表示成功、负数枚举错误的约定调用点统一处理比到处散落return 1、return -1这种魔法数字好维护得多。规范里“类型与名称不允许分行书写”那条则是防止函数签名被断行拆碎导致搜索和diff都不方便。5. 避坑排查可靠性、可测试性与断言的实战翻车记录前四章讲的都是“怎么写”这一章讲“写坏了会怎样”。可靠性、可测试性、断言与错误处理这三章是规范里最有实战分量、也最容易在评审时被跳过的地方。下面四条踩坑记录每条都按“现象—原因—解决”展开你大概率在项目里见过同款。5.1 全局变量失控改一处崩一片现象一个用全局数组做通信缓冲区的模块某次改造给数组加了个保护锁结果消息处理线程和解复用线程同时改了相邻字段现场偶发死机问题压了两周。原因模块间通过全局变量隐式共享数据谁都可以读写开发和维护过程中根本数不清有多少调用点。全局变量的每一次修改都像在黑匣子里做手术看不见牵连面。解决按规范原则“避免或少用全局变量”把共享数据封装进结构体通过函数接口访问读写加锁逻辑收拢到两个函数里。从那以后我在代码里看到新引入的extern变量评审时都会追着问一句这个必须全局吗5.2 函数多个return出口单元测试无从下手现象一个函数里有三处return分别藏在两个if嵌套和一个switch分支里。测例覆盖了主干漏掉了中间那个提前返回的分支结果线上触发后数据没有落盘。原因控制流出口太多测试分支数量成倍增长人脑根本数不全。多出口函数还有一个隐蔽问题每个出口前都要记得释放资源漏掉一个就是泄漏。解决规范里可测试性部分的导向就是控制流清晰、尽量单入口单出口。把提前返回的条件先集中判断失败统一走末尾的错误处理函数出口收敛到一两个测试路径立刻可数。不是所有函数都必须单出口但出口越多越要怀疑函数是不是塞了太多逻辑。5.3 断言当错误处理用发布版本直接崩溃现象代码里用assert(ptr ! NULL)校验从消息里解析出来的指针自测时没事上线后收到一个空指针程序直接终止。原因断言是用来发现“程序自身编程错误”的不是用来处理“外部输入、运行时异常”的。正式发布版本里断言要么被编译掉要么触发abort拿它挡用户输入等于把崩溃开关交给对方。解决按规范第12章的划分编程错误用断言内部不变量被破坏运行时错误用条件判断加错误码返回。我在团队里立了条习惯断言只出现在“这里不可能为假”的地方凡是数据可能不合法的地方一律走if分支。5.4 GOTO清理资源控制流变成意大利面现象某模块在错误处理时用goto跳转到统一释放资源的标签起初只有三处跳转后来加了两个分支标签前的逻辑越来越长阅读时需要在多个位置之间来回跳。原因GOTO本身不可怕可怕的是它让控制流变成一张网。规范原则说“尽量避免使用GOTO”实际项目里资源清理场景确实需要集中出口但跳转满天飞就违背了初衷。解决优先把每个分支的资源释放写进各自作用域内或者用do { ... } while(0)包裹用break代替goto即使保留GOTO也要保证跳转方向单一、标签只在函数末尾出现一次。评审时看到goto不是一票否决而是重点审查它的跳转距离和位置数量。6. 把规范变成提交前动作检查表与两个自动化小脚本附录里的规范检查表和编程模板是这份标准里最容易被忽略的宝藏。检查表的用法不是评审时拿在手里一条条念而是把它拆成提交前的自检清单布局是否用了4空格缩进、有没有TAB混入、行宽有没有超过80列、新文件头注释是否齐全、命名前缀是否符合对照表、函数出口是否收敛。我习惯把这张表简化成十个问题贴在工位旁每次提交前过一遍五分钟能挡住八成格式问题。检查表里“可执行”的部分可以交给脚本自动化。第一个脚本扫TAB和行宽grep -rnP \t src include echo find TAB, please replace with 4 spaces awk length($0) 80 {print FILENAME:NR length:length($0)} src/*.c include/*.hgrep -P \t用Perl风格正则匹配真正的TAB字符跑出来就是需要替换的位置awk按行统计长度超过80列就打印文件名和行号。这两个命令在Linux和Windows的Git Bash环境下都能直接跑不需要装额外工具。第二个脚本做命名抽查比如搜索类成员声明里有没有漏掉m_前缀或者检查全局函数原型之前是否都有注释块。这种检查不需要覆盖全部规则抓住高成本的几项就够了剩下人工评审重点放在逻辑上。从那以后我每次改完代码都强制自己跑一遍这两个命令再加提交算是对20年前那份标准最基本的尊重。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑