ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

2003年C/C++编码规范:八大原则与落地避坑指南

2003年C/C++编码规范:八大原则与落地避坑指南 简介《中兴通讯软件编程规范——C/C》是中兴通讯2003年发布的企业标准主要面向使用C/C的软件工程师用于统一编码风格、提升代码质量。资源为1个doc文档体积约486KB内容涵盖基本原则、布局、注释、命名规则、变量常量与类型、表达式与语句、函数与过程、可靠性、可测试性、断言与错误处理等C/C开发的核心环节并在附录A、B中给出了编程模版和规范检查表方便在项目中落地执行。规范按规则性和建议性分类既有必须严格执行的规则也有可根据项目情况采纳的建议并辅以大量正例、反例说明其“首先为人编写程序其次才是计算机”的原则对提高代码可读性、可维护性和减少缺陷很有帮助。已有301人学习这份资料适合需要建立或完善编码规范的研发团队、企业内训和自学的开发者。1. 先说结论一份2003年的 C/C 编码规范最值钱的是那八个原则接手一个跑了七八年的通信设备驱动模块让人崩溃的往往不是算法本身而是代码风格有些文件用 Tab有些用空格指针星号一会儿在类型后面一会儿贴在变量名上同一种错误码在三个文件里有三种写法。某通信设备厂商 2003 年发布的这份《软件编程规范——C/C》就是为了治这种乱象。它把“为人编写程序”“先正确性后效率”“避免全局变量”等 8 条原则放在最前后面用 12 个章节把布局、注释、命名、可靠性、可测试性全部量化成可审核的规则。适合写设备端、驱动、嵌入式协议栈或者要带团队收口代码风格的开发者。想减少代码评审吵架次数的人更值得从头读一遍。2. 规范总览原则、规则、建议三层结构以及文件布局的硬性要求一份编码规范能不能落地不看条目多少看它把约束分成几个强度等级。这份规范开篇先定义术语再给基本原则然后才展开布局、注释、命名等具体章节。所有条款被明确分成“原则”“规则”“建议”三层。原则管思想规则管行动建议留余地。后面十几章的每一节基本都按“说明、正例、反例”的格式走读起来不像法律条文更像一份带示例的检查单。2.1 三层约束原则管思想规则管行动建议留余地规范的总则部分一共 8 条原则很多人只把它当作口号实际上整份标准的逻辑都从这 8 条长出来。第 1 条“首先是为人编写程序其次才是计算机”解释了为什么要定这么多看起来琐碎的布局规则。第 4 条“编程时首先达到正确性其次考虑效率”直接决定了后面可靠性和错误处理的优先级。第 5 条“避免或少用全局变量”影响的是变量、常量与类型那一章的写法。第 6 条“尽量避免使用 GOTO”在表达式与语句、函数与过程章节里都有呼应。从这里能看出这套规范的设计思路先统一价值观再定操作规矩。如果把所有规则平铺开发者容易陷入“为格式而格式”的误区。先讲清楚代码是给人读的后面的 80 列限制、注释规范、命名规则才立得住。规则和建议的区分同样重要。规则必须执行比如不使用 Tab、{与}独占一行、函数名后不留空格。建议则保留弹性比如某个 switch 分支是否一定要用{}包起来项目组可以根据实际情况决定。落地时不把规则和建议混为一谈评审才不会被细节拖垮。2.2 头文件布局先防重复包含再排四类声明规范第二章“布局”首先规定的不是缩进而是文件布局顺序。头文件要按固定次序书写文件头注释、#ifndef宏定义、其他条件编译选项、#include然后才是常量定义、全局宏、全局数据类型、类定义和全局函数原型最后以#endif收尾。#ifndef DEVICE_MODULE_H #define DEVICE_MODULE_H #include stdio.h /* 标准库头文件用尖括号 */ #include dev_config.h /* 非标准库头文件用双引号 */ #define MAX_BUF_LEN 128 /* 常量定义 */ typedef unsigned char BOOLEAN; /* 全局数据类型 */ class CDevice { public: CDevice(void); ~CDevice(void); int GetStatus(void); void SetStatus(int iStatus); private: int m_iStatus; }; #endif代码里的#ifndef DEVICE_MODULE_H / #define DEVICE_MODULE_H / #endif就是防重复包含的标准做法。宏名通常取文件名大写加下划线避免和其他模块撞名。#include的次序也有讲究标准库在前非标准库在后这样自定义头文件里的依赖错误会更快暴露出来而不是被标准库的隐藏包含掩盖。在这个布局里常量、宏、数据类型、类的顺序不能乱。先定义常量再定义依赖这些常量的宏接着定义类型最后定义类。把类的定义放在最靠后的位置保证类能引用前面所有声明。我一般在新建头文件时直接按这个模板填空比每次重新排布少犯错。2.3 实现文件布局从包含到函数体的固定次序实现文件的布局同样有明确顺序文件头注释、#include、常量定义、文件内部使用的宏、内部数据类型、全局变量、静态全局变量、局部函数原型、类的实现、全局函数、局部函数。#include device_module.h #include string.h #define INTERNAL_MARK 0xAA /* 文件内部使用的宏 */ static int s_iInternalTotal 0; /* 静态全局变量 */ static int ValidateInput(int iInput); /* 局部函数原型 */ int DoProcess(int iInput) { int iRet ValidateInput(iInput); if (iRet ! 0) { return iRet; } s_iInternalTotal iInput; return iRet; } static int ValidateInput(int iInput) { if (iInput 0) { return -1; } return 0; }这种顺序的本质是“先声明后使用”。局部函数原型放在文件前面后面实现顺序就可以任意排列不用再担心编译器报隐式声明的错。静态全局变量集中声明也方便维护者一眼看出哪些状态被文件内部共享。实现文件还有一个容易被忽略的点类的实现放在全局函数之前。规范要求构造函数、析构函数、公有函数、保护函数、私有函数依次实现和类的声明顺序呼应。新人照着这个顺序写类代码的可读性会高很多。如果实现文件和头文件声明顺序不一致查接口定义时要来回跳转效率很低。3. 核心规则拆解布局、注释、命名里那些一眼能查的硬指标布局章节是整份规范里最“密”的部分几乎没有废话。从单行长度、缩进、空行到运算符两侧空格、断行位置全部给了正例和反例。这些规则单独看像强迫症合在一起却构成了一套完整的视觉系统代码结构通过缩进和空行呈现逻辑边界通过换行和括号表达。3.1 基础格式和指针写法80列、空行、星号位置规范要求一行代码和注释不超过 80 列包含空格。这条规则在今天的高分屏下看显得保守但如果你需要对比老旧代码、在终端里 diff80 列仍然有现实价值。另一个硬规则是一元操作符前后不加空格二元运算符和操作数之间至少留一个空格。if (iLen MAX_BUF_LEN) { iResult iLen 1; /* 二元运算符两侧有空格 */ }这里、、两侧都有空格关键字if和左括号之间留一个空格而函数名和左括号之间不留空格。视觉上关键字更突出函数调用更紧凑习惯了之后扫一眼就能区分if (...)和Function(...)。指针星号的位置是这个规范的一个特别之处要求*放在变量名前面而不是类型后面。float *pfBuffer; /* 正例星号靠近变量名 */ float* pfBuffer; /* 反例星号靠近类型规范明确反对 */站在 C 语言语法角度*确实是声明符的一部分放在变量名前更准确。比如float *pfBuffer, *pfBuffer2;这种写法能直接看出两个变量都是指针。我见过很多团队用float* pfBuffer风格也不算错但如果整个存量工程都按规范走新代码就必须跟随老代码的风格否则会有刺眼的混排。3.2 对齐与缩进4个空格、大括号独占一行、等号排齐规范明确规定禁止使用 Tab必须用空格缩进缩进量为 4 个空格。{和}独占一行并且和引用它们的语句左对齐。这一条和现代 clang-format 的默认风格不太一样更像是 Allman 风格。if (iRet 0) { ProcessData(); } else { HandleError(); }大括号独占一行的最大好处是括号配对非常直观尤其在函数很长、嵌套很深时不会出现}和else粘在一起的情况。代价是代码纵向变长一屏能看到的内容变少。这正是规范里“开始讲究可读性”的体现牺牲信息密度换取结构清晰。赋值语句的等号对齐属于容易被忽视的细节。规范要求相关赋值语句的等号上下对齐我实际用下来发现在结构体初始化场景里特别有用tConfig.wHead 0; tConfig.wTail wMaxNum - 1; tConfig.wFree wMaxNum; tConfig.wAddress wConfigAddr; tConfig.wSize wConfigSize;对齐之后每一行赋值语句处理哪个字段、值是多少扫一眼就能看出来。如果不齐读的时候需要对列逐个比对增加无意义的视觉负担。类似的还有 switch 里的 case 分支建议用{}包起来避免变量跨 case 意外共享。switch (iCode) { case 1: { DoSomething(); break; } default: { DoNothing(); break; } }每个 case 用括号隔离能够防止两个分支共享同一个局部变量也减少了后面补代码时不小心把变量声明到 switch 外的情况。3.3 断行规则长表达式从低优先级操作符处拆超过 80 列的表达式必须断行规范给出的原则是从低优先级操作符处拆分操作符放新行之首。比如逻辑与、逻辑或||优先级低于比较运算符就应该作为断行的位置。if ((iFormat FORMAT_M) (iOfficeType TYPE_BSC_M)) { DoSomething(); }这里把放在第二行开头阅读时会先看到上一行条件再自然接上第二行条件逻辑流向更清楚。函数调用的断行则在第一个参数处对齐iRet ReportStatusCheckPara(hWnd, ucCallNo, ucStatusReportNo);参数表逐行排开Ctrl 加光标可以快速上下移动查看每个参数。命名一个最容易踩的坑断行时运算符留在上一行末尾很多 IDE 的自动折行会这样处理但规范明确要求放在新行之首。这是因为行首运算符能提醒读者“这行是上一行条件的延续”。3.4 注释与命名写意图而不是复述代码注释部分的核心观点是注释要在功能、意图层次上提供额外信息而不是重复代码表面意思。/* 反例这行注释完全多余 */ iCount; // iCount 加一 /* 正例解释为什么这里需要加一 */ iCount; /* 补偿上次分包丢弃的一帧数据 */判断一条注释是否值得写可以看删掉它之后是否损失信息。如果删掉注释代码变得难懂说明注释有价值如果代码本身已经很清楚注释就是在制造噪音。规范还要求注释符和内容之间用一个空格分隔多行注释统一用/* */单行注释用//。命名规则这部分原文没有给出完整清单但从正反例可以明显看出它采用的是匈牙利前缀风格。指针变量用p开头如pfBuffer整型用i如iLen、iCount无符号用u字符串用str结构体变量用t前缀如tConfig全局变量用g_静态变量用s_成员变量用m_前缀。类名用C开头如CDevice。这种命名在 2003 年的环境里很有价值因为当时的 IDE 补全能力弱看到名字就知道类型和变量作用域。今天的工程里是否继续全量沿用取决于团队代码库的年龄和工具链后面会专门说。4. 可靠性、可测试性与断言把错误挡在上线之前这份规范的第 10 到第 12 章是整份文档的精华可靠性、可测试性、断言与错误处理。前面布局和命名规则决定代码好不好读这三章决定代码能不能少出事故。老团队愿意把“软件编程规范”这份文档当成硬标准很大程度是因为这些章节里的经验都是从线上故障里反推出来的。4.1 可靠性优先先正确后高效再谈资源占用可靠性一章的基本思路在总则里已经埋下先正确性后健壮性再考虑效率和资源占用。落实到代码上常见的处理方式是所有对外输入先做合法性检查所有可能失败的操作都要有明确定义不能静默失败。int InitBuffer(Buffer *ptBuf, int iCapacity) { if (ptBuf NULL) { return -1; } if (iCapacity 0) { return -2; } ptBuf-pData malloc(iCapacity); if (ptBuf-pData NULL) { return -3; } ptBuf-iCapacity iCapacity; ptBuf-iLength 0; return 0; }这种逐层检查的写法看起来过度防御但是在通信设备这类长时间运行、资源受限的场景里任何一处空指针解引用都可能引起整机重启。每个错误码-1、-2、-3分别对应空指针、非法容量、内存分配失败调用方可以根据返回值做不同处理。规范鼓励这种显式错误返回而不是用一个笼统的false掩盖全部问题。另外需要强调的是规范第 5 章“变量、常量与类型”里还有一条相关要求局部变量必须初始化。常见的内存错乱里很大比例来自未初始化的指针和数组下标。老代码里经常见到声明了一堆变量然后中间路径直接返回变量没有被赋值而后续代码误以为它已经被初始化了。4.2 断言与错误处理不变量交给 assert外部异常交给返回值断言和错误处理是两种不同性质的机制很多新人在同一段代码里混用。规范的思路是用断言检查程序内部不该发生的不变量用错误码或返回值处理外部输入和资源失败。#include assert.h int SetBufferLength(Buffer *ptBuf, int iLen) { assert(ptBuf ! NULL); /* 内部不变量绝不应为空 */ assert(iLen 0 iLen ptBuf-iCapacity); if (ptBuf-pData NULL) { return -1; /* 外部条件需要返回错误 */ } ptBuf-iLength iLen; return 0; }这里assert(ptBuf ! NULL)断言的是调用者不可能传空指针给我如果传了说明调用方代码有 bug。断言在发布版本里可能被NDEBUG编译选项删除所以它不能承担错误处理职责。而pData NULL是运行时外部条件即使发生也能通过返回值让上层恢复必须用错误处理机制兜底。我在看老项目的代码时一个判断代码成熟度的标准就是内部接口用断言外部接口用错误返回。如果反过来在内部函数里把所有不变量都返回错误码发布版本虽然还在检查但大量无意义的检查会掩盖真正的关键路径。如果外部输入被assert处理发布版本一旦上线直接放空指针进来程序就裸奔了。4.3 可测试性习惯函数边界清晰副作用收敛可测试性章节的核心要求是最小化外部依赖和全局状态。全局变量在这里不仅是耦合问题还是测试失败时定位成本的问题。一个函数直接读写全局变量测试时想构造几种不同输入就得先想办法重置全局状态。/* 反例结果直接写进全局变量 */ int g_iPacketCount 0; void ProcessPacket(const unsigned char *pucBuf, int iLen) { g_iPacketCount iLen; } /* 正例通过指针把结果交给调用方 */ int ProcessPacket(const unsigned char *pucBuf, int iLen, int *piOutCount) { if (pucBuf NULL || iLen 0) { return -1; } *piOutCount iLen; return 0; }正例把函数设计成“输入参数 输出参数 返回值”的标准结构不依赖任何文件级状态。测试时只需要准备输入和输出缓冲断言返回值和输出即可。规范在函数与过程章节里也同样强调单一职责、函数不宜过长这两章合在一起基本就是现代单元测试的雏形。2003 年还没有今天这么成熟的测试框架但这些思想放到现在完全不过时。可测试的代码天然容易 review因为它把副作用摆在了明面上。5. 落地避坑指南2003年的规范用进今天工程的五个注意点好的规范如果不能落地就只是一份漂亮的 PDF。把这份 2003 年的标准引入现代团队最大的障碍是工具链和代码库的现状。下面这几个坑我基本都踩过写出来供参考。5.1 编辑器差异导致缩进翻车统一配置不是口号现象一个仓库里有人用 Vim、有人用 VS Code、有人用 Qt Creator明明规范写着“禁用 Tab、缩进 4 空格”最终提交的代码仍然出现混排。原因是每个编辑器的默认 Tab 宽度不同有的转成空格有的保留 Tab 字符。解决把规则写进.editorconfig让所有主流 IDE 自动读取再在 Git 钩子里拦截混合缩进。[*.{c,cpp,h}] indent_style space indent_size 4 end_of_line lf这个配置解决了“新建文件”的问题存量文件里的历史缩进问题还得靠格式化工具分目录处理。我一般会先对增量代码强制检查存量代码只在改动到的函数内顺手清理不搞一次性全仓格式化否则 diff 会大得没法 review。5.2 匈牙利前缀和现代C风格打架现象规范要求成员变量带m_前缀、指针带p前缀但团队引入现代 C 之后STL 容器、智能指针、模板代码根本塞不进这套命名体系。结果出现m_iCount和readerIndex_混在一个文件里的情况风格分裂。原因不是规范的命名思路错了而是它生前的语境是补全能力弱、类型信息藏在名字里的时代。今天的 IDE 能直接悬浮看到变量类型过度前缀反而让名字变长。解决对老模块保留既有匈牙利风格保持存量代码一致性对新模块只保留m_成员前缀去掉类型前缀。团队内部明确这条边界不搞一刀切。代码评审时应重点关注命名是否达意而不是拘泥于前缀字母。5.3 把规则和建议混在一起评审评论区当场吵起来现象代码 review 时A 说 switch 的 case 必须用括号包起来B 说规范里这只是建议。双方都有依据但评审会开成了辩论赛。原因规范的普遍分级没有被评审清单继承大家对“建议”到底要不要执行没有共识。解决在评审清单里把规则分成“必须”和“建议”两栏必须项纳入 CI 检查建议项只在有明确理由时才需要讨论。我先按规范附录 B 的检查表过一遍把条目标记为强制的才放进提交门禁。这样做之后评审讨论减少了很多无意义的分歧。5.4 人工检查容易漏工具扫描要跟上现象人工 review 布局规范时总会漏掉等号对齐、运算符空格这类细节因为人脑不擅长做高频低难度检查。原因规则条目分散在多个章节里单靠肉眼逐条对照效率很低而且容易疲倦。解决把布局规则和命名基础规则转成 clang-format 配置把空指针解引用、资源未释放等可靠性风险交给 cppcheck 或 clang-tidy。规范是最好的输入材料你只需要把规则逐条翻译成工具参数剩下交给 CI 输出违例文件列表。从附录 B 的检查表出发一张表基本就能映射出一套检查脚本。5.5 老代码一次性整改的包袱太大先做增量切换现象团队想在旧项目里推行规范有人提议用格式工具全库刷一遍结果生成上万行 diff主干无法合并其他开发被冲突淹没。原因存量代码不符合规范是常态把格式规范当作整容手术一次做完必然产生巨大成本而且格式 diff 会掩盖真正的逻辑改动review 形同虚设。解决采取“增量切换”策略。新代码从第一天起强制规范旧代码只在新功能改动时顺手治理。启动阶段用 CI 对新增文件检查格式和静态分析不检查历史文件。经过一个版本周期的治理存量违规覆盖率会明显下降。我见过一个团队用三个版本周期把十万行存量代码的违规率降到了 10% 以内过程中业务迭代没有停过。6. 二次开发把企业标准剪裁成团队自己的编码规范拿来即用很诱人但真正有效的落地方式是先做剪裁再转成工具配置最后用数据看效果。这套规范最大的资产是附录 A 的编程模板和附录 B 的规范检查表二次开发的起点就是这张检查表。第一步把检查表逐条过一遍分类为“强制规则”“建议规则”“本团队不适用的规则”。比如左括号换行的 Allman 风格如果团队所有成员都写 KR 风格很多年了不建议强行翻新把“大括号独占一行”降级为“允许并保持完全一致”。这一步是在做减法不是做加法。第二步把强制规则映射到工具链上。布局和缩进由 clang-format 接管命名和可测性规则放进 review 清单。BasedOnStyle: LLVM IndentWidth: 4 UseTab: Never BreakBeforeBraces: Allman PointerAlignment: Right ColumnLimit: 80上面这份 clang-format 配置基本对应规范里的三条核心布局规则。这里有一个常见误区生成配置之后要拿几个真实源文件跑一遍看配置和规范的细节是否有冲突。例如 Allman 风格会让else占一行如果你的团队不习惯可以直接关掉这项不要为了“严格对标”把全员都逼疯。第三步把检查表放进 pre-commit 钩子只检查本次提交新增的文件。#!/bin/sh FILES$(git diff --cached --name-only --diff-filterACM -- *.c *.cpp *.h) if [ -n $FILES ]; then clang-format -i $FILES git add $FILES fi这个钩子会在提交前自动格式化保证进入仓库的代码始终符合布局规则。不过要注意这里git add $FILES会把格式化后的文件重新纳入暂存区提交前要确认没有引入非预期的改动。最后一步是验证。我通常看三个指标代码评审里格式类评论占比、新代码静态检查违规数、单次合并请求的文件改动量。这三个数字如果持续下降说明规范已经在起作用。从那以后我每次启动新项目都会先做一次规范裁剪把强弱规则写进 README再把检查表转成 CI 脚本最后才让团队开始写第一行代码。这套流程走完新增代码基本不再需要讨论格式问题评审讨论集中在真正的逻辑和设计上。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表