☰
企业级C/C++编码规范:核心条款、易漏点与提交检查表
2026/11/2 3:43:50 网站建设 项目流程

简介:《中兴通讯软件编程规范——C/C++》是一份面向C/C++开发工程师的企业级编码规范文档,适合需要统一团队代码风格、提升源码可读性与可维护性的开发场景。内容围绕基本原则、代码布局、注释、命名规则、变量与类型、表达式与语句、函数与过程、可靠性、可测试性以及断言与错误处理展开,并附有编程模板和规范检查表,便于在项目中对照执行。压缩包共1个doc文件,大小约486KB,结构单一但内容完整,可直接作为团队编码约定或代码评审参考。已有301人学习,适合初中级开发者查漏补缺,也能为技术负责人制定编码标准、开展代码审查和新人培训提供较系统的依据。

1. 一份企业级 C/C++ 编码规范,究竟在管什么

拿到一份 2003 年发布的企业级 C/C++ 编码规范,很多人第一反应是翻到命名规则那一章,看看变量该怎么起名。真读进去会发现,最值得先记住的并不是命名,而是它把每一条约定分成了「规则」和「建议」两级强度:规则是所有开发人员必须严格执行的硬性条款,建议则由各项目根据实际开发特性选择执行。这一分级直接决定了复习时的优先级——规则要能做到条件反射,建议理解意图即可。整份规范的地图大致是三层:底层是 8 条基本原则(为人写程序、保持简明、贴近 ANSI C、正确性优先于效率、少用全局变量、慎用 GOTO、复用老代码、减少重复错误);中层是布局、注释、命名、变量与类型、表达式与语句、函数与过程六块「写法约定」;上层是可靠性、可测试性、断言与错误处理三块「质量约定」。附录还给了编程模板和规范检查表,等于把规范落成了可勾选的动作。这套东西适合两类人:正在接手一份历史代码库、需要快速对齐团队风格的开发者,以及准备技术评审、想把「代码写得让人看得下去」这条底线讲清楚的人。

2. 布局与格式:从文件骨架到缩进、空格、断行的硬约定

布局这一章在整份规范里篇幅不小,原因很实在:程序布局的目的是显示良好的逻辑结构,提高准确性、连续性、可读性和可维护性。统一的布局和编程风格不只是个人审美问题,它直接影响项目开发质量和开发成本。复习这一章的关键,是把它拆成「文件骨架」和「行内细节」两层,前者决定一个文件长什么样,后者决定一行代码长什么样。

2.1 文件布局:头文件和实现文件都有固定次序

规范给出的头文件次序是:文件头注释块 →#ifndef 文件名_H/#define 文件名_H(全大写)→ 其它条件编译选项 →#include(依次为标准库头文件、非标准库头文件)→ 常量定义 → 全局宏 → 全局数据类型 → 类定义 → 模板(类模板和函数模板)→ 全局函数原型 →#endif。实现文件的次序是:文件头 →#include→ 常量定义 → 文件内部使用的宏 → 文件内部使用的数据类型 → 全局变量 → 本地变量(即静态全局变量)→ 局部函数原型 → 类的实现 → 全局函数 → 局部函数。

注意这些次序里藏着一个容易忽略的设计意图:越「公共」的东西越靠前,越「私有」的东西越靠后。头文件对外暴露接口,所以类型和函数原型在前;实现文件里有大量只在本文件可见的东西,所以静态全局变量和局部函数原型被单独归位。规范还要求用注释块把上面这些节分隔开,头文件必须避免重复包含,包含标准库用尖括号、包含非标准库用双引号。

/* ========================================================== * 模块名 : 数据缓冲区管理 * 功能 : 提供缓冲区的申请、释放与状态查询 * 修改记录: * 2003-01-06 初版 * ========================================================== */ #ifndef BUFFER_MGR_H /* 全大写,与文件名对应 */ #define BUFFER_MGR_H #include <stdio.h> /* 标准库用尖括号 */ #include <string.h> #include "buffer_type.h" /* 非标准库用双引号 */ /* ------------------- 常量定义 ------------------- */ #define MAX_BUFFER_NUM 64 /* ------------------- 数据类型 ------------------- */ typedef unsigned char BOOLEAN; /* ------------------- 全局函数原型 ------------------- */ int BufferMgrInit(void); int BufferMgrAlloc(unsigned int uiSize, void **ppBuf); #endif /* BUFFER_MGR_H */

这段示例对应的是规则 2-1-1、2-1-4、2-1-5 三条。#ifndef/#define这对组合是防重复包含的标准做法,宏名与文件名一致且全大写,是为了在几十个同名头文件的工程里一眼定位来源;#include的顺序不是随便排的,标准库在前可以尽早暴露「某个非标准头偷偷依赖了标准头」这类隐式依赖。类定义的书写顺序也有规定:公有属性、公有函数、保护属性、保护函数、私有属性、私有函数;类的实现文件里则是构造函数、析构函数、公有函数、保护函数、私有函数。这个顺序的价值在于,读接口的人只需要看文件上半部分,读实现细节的人才往下翻。

2.2 缩进、括号与基本格式:四条不能破的底线

这一小节是全篇最容易被当成「格式洁癖」的部分,但它恰恰是代码评审里最容易起争议的地方。归纳下来有四条底线。

第一条是一行代码和注释都不能超过 80 列,包括空格在内。这条限制在宽屏时代看起来过时,实际作用是逼你把深层嵌套和超长表达式拆开,间接控制了函数复杂度。

第二条是if、else、else if、for、while、do等语句必须自占一行,执行语句不得紧跟其后,并且不论执行语句有多少都要加{}。规范给的反例正是那种「一行写完 if」的写法:if (varible1 < varible2) varible1 = varible2;。加花括号的理由不是好看,是防止后续加第二行语句时忘了补括号,这类 bug 在维护期出现频率极高。

第三条是禁止使用 TAB 键,必须用空格缩进,缩进量为 4 个空格。规范把原因写得很直白:消除不同编辑器对 TAB 处理的差异。同一份代码在不同编辑器里显示成 4 格还是 8 格,会让「对齐」这件事彻底失效。

第四条是分界符{和}独占一行、位于同一列,并与引用它们的语句左对齐;花括号内的代码块按 4 格缩进。switch语句里每个case分支和default要用{}括起来,括号内容再缩进。声明类时,public、protected、private关键字与分界符{}对齐,这些部分的内容再进行缩进。

class CCount { public: // 与 { 对齐 CCount(void); // 内容缩进 4 格 ~CCount(void); int GetCount(void); void SetCount(int iCount); private: int m_iCount; };

除了这四条,还有几条散落的细节值得一并记住:指针类型的变量,*应放在变量名前,写float *pfBuffer;而不是float* pfBuffer;;结构型数组、多维数组如果在定义时初始化,按矩阵结构分行书写;相关的赋值语句等号对齐,比如一组tPDBRes.wHead、tPDBRes.wTail、tPDBRes.wFree连续赋值时把=排在同一列;关系较为紧密的代码尽可能相邻,比如矩形的长和宽赋值放一起,中间不要插一句字符串赋值。

2.3 空格、空行与断行:80 列之后怎么续

空格规则是这份规范里最琐碎、也最容易在评审时被挑出来的部分。它的逻辑其实很统一:一元操作符贴着操作数,二元操作符两边留空,括号向内收。

一元操作符如!、~、++、--、*(解引用)、&(取地址)前后不加空格;[]、.、->这类操作符前后也不加空格。多元运算符和它们的操作数之间至少需要一个空格,比如fValue = fOldValue;、iNumber += 2;。关键字之后要留空格,if、for、while后面跟左括号时要空一格,以突出关键字;函数名之后不要留空格,让函数名紧跟左括号,从而与关键字区分。左括号(向后紧跟,右括号)、逗号,、分号;向前紧跟,紧跟处不留空格;逗号之后要留空格,分号不是行结束符号时其后也要留空格。注释符与注释内容之间用一个空格分隔,写成/* 注释内容 */、// 注释内容。

/* 正例:注意关键字后空格、逗号后空格、函数名后不留空格 */ for (i = 0; i < MAX_BSC_NUM; i++) { DoSomething(iWidth, iHeight); } /* 反例:函数名后多空格、逗号后缺空格、括号内外侧留空 */ for( i = 0 ; i < MAX_BSC_NUM ; i++ ) { DoSomething ( iWidth,iHeight ); }

两段代码做的是同一件事,但反例在团队协作里的代价是实实在在的:做代码 diff 时,格式差异会淹没真正的逻辑改动,评审者要花额外精力分辨哪一行是「改格式」、哪一行是「改行为」。

空行规则相对宽松但意图明确:不同逻辑程序块之间要用空行分隔。规范举的例子是两个成员函数的实现之间空一行,因为它们是两个逻辑块。断行规则则更考验功底:长表达式(超过 80 列)要在低优先级操作符处拆分成新行,操作符放在新行之首,以便突出操作符,拆分出的新行要适当缩进、使排版整齐。续行的对齐位置有四种对应关系——条件表达式的续行在第一个条件处对齐,for循环语句的续行在初始化条件语句处对齐,函数调用和函数声明的续行在第一个参数处对齐,赋值语句的续行在赋值号处对齐。

/* 条件表达式:续行与第一个条件对齐 */ if ((iFormat == CH_A_Format_M) && (iOfficeType == CH_BSC_M)) { DoSomething(); } /* 函数声明:续行与第一个参数对齐,且类型与名称不许分行 */ BYTE ReportStatusCheckPara(HWND hWnd, BYTE ucCallNo, BYTE ucStatusReportNo); /* 赋值语句:续行在赋值号处对齐 */ fTotalBill = fTotalBill + faCustomerPurchases[iID] + fSalesTax(faCustomerPurchases[iID]);

这里有个容易被忽略的细则:函数声明时,类型与名称不允许分行书写。也就是说extern double FAR CalcArea(double dWidth, double dHeight);是正例,把double dWidth拆成两行就是反例。原因是一旦允许类型和名称分行,搜索函数名时就可能漏掉声明处,而声明恰恰是排查接口变更的第一现场。

3. 注释与命名规则:可读性的两条腿

如果只能从这份规范里带走两样东西,我会选注释和命名。布局可以靠工具格式化,注释和命名却没有任何工具能替你完成——它们直接决定了别人读你代码时是「三分钟看懂」还是「三天看不懂」。

3.1 注释写在意图层,不写在语法层

规范对注释的定义很克制:注释有助于理解代码,有效的注释是指在代码的功能、意图层次上进行注释,提供有用、额外的信息,而不是代码表面意义的简单重复。这句话可以当成判断注释好坏的标准——iCount++; // iCount 加一是无效注释,iCount++; // 统计已成功入队的消息数,用于触发批量提交才是有效注释。

注释符的选择有明确规定:C 语言的注释符为/* … */;C++ 语言中多行注释采用/* … */,单行注释采用// …。这条看似基础,实际在混合编译的场景里经常出错,比如在 C 代码里用了//,遇到只支持 C89 的编译器就直接编译失败。

真正需要靠经验补齐的是注释的位置和内容。规范在文件布局一节提到「文件头(参见注释一章)」,说明文件头注释是强制项,梳理下来它通常要包含模块名、功能描述、修改记录这几项;函数头注释则应覆盖功能、参数含义、返回值、注意事项。这类注释的价值在半年后才体现:当你想改一个函数的返回值语义时,函数头注释是唯一能告诉你「当初为什么这么设计」的地方。

提示:注释最常见的失效方式不是写错,是代码改了注释没改。把「注释与代码同步更新」当成提交前检查项,比事后补注释划算得多。

还有一类注释需要特别处理:被注释掉的代码。规范没有鼓励保留大段注释代码,从可维护性角度看,要么删掉(版本管理里有记录),要么在注释里说明为什么暂时保留、什么条件下删除。否则半年后没人敢动那一段,它就成了代码里的黑匣子。

3.2 命名规则:从示例里反推出来的那套前缀体系

规范正文的命名规则章节没有在摘录里展开细则,但从全篇的正例反例里可以反推出一套相当一致的命名体系:类型前缀 + 有意义的名字,再叠加作用域前缀。这套体系在当年的 C/C++ 工程里非常典型,复习时把它整理成一张表最有效。

前缀含义规范中的示例
iint 型iLength、iWidth、iCount
w短整型 / WORD 型wHead、wTail、wFree
ddouble 型dWidth、dHeight
ffloat 型fSum、fTotalBill
bBOOLEAN 型bValue
ucunsigned char 型ucCallNo、ucStatusReportNo
p指针pfBuffer、pBuf
sz/str字符串strCaption、strSource
h句柄hWnd
aiint 数组aiNumbers
t结构体类型tPDBRes、tBox
m_类的成员变量m_iCount
C类名前缀CCount

从这张表能读出三条隐含规则。第一,前缀本身携带类型信息,m_iCount一眼可知是成员变量且为 int,这在没有 IDE 跳转的年代是刚需。第二,函数名用动宾结构,GetCount、SetCount、DoSomething、ReportStatusCheckPara,动词在前说明行为,名词在后说明对象。第三,宏和自定义类型全大写,MAX_BUFFER_NUM、BOOLEAN,与变量名形成明显区分。

命名上最容易踩的坑不是前缀用错,是「见名不知义」。int iFlag;这种名字前缀对了但语义为零,半年后没人知道 flag 的哪一位代表什么。规范强调命名要有描述性,落到实操就是:宁可名字长一点,也不要用tmp、data、flag这类万能词;循环变量i、j、k是唯一被广泛接受的单字母例外,因为它们的语义由循环本身定义。

4. 变量、类型、表达式、函数与可靠性:规范里最硬的那部分

前半部分管「写得像不像一个团队」,后半部分管「跑起来会不会出事」。这一章把变量常量与类型、表达式与语句、函数与过程、可靠性与可测试性、断言与错误处理串成一条线,核心思路只有一句:把错误暴露在编译期和开发期,而不是留给现场。

4.1 变量、常量与类型:先管住全局变量和隐式转换

基本原则里有一条分量很重:避免或少用全局变量。理由写得很清楚——过多使用全局变量会导致模块间的紧耦合,违反模块化要求。复习时可以把「少用全局变量」拆成三个可执行的判断:这个变量是否被两个以上模块写?是否只在初始化时赋值一次(那它应该是常量)?是否可以改成通过参数传递?三个问题里有一个答「是」,就值得重新设计。

类型层面的重点是显式优于隐式。变量在使用前必须初始化,避免依赖编译器的默认清零行为;类型转换要写出来,不要把double赋给int这种截断藏在表达式里;常量的表达方式要统一,用const修饰或用全大写宏定义,不要在代码里散落魔法数字——if (iState == 3)这种写法,三个月后连作者本人都不敢确定 3 代表什么。

提示:无符号类型与有符号类型混用是比较运算出错的高发区。unsigned int和int比较时,有符号数会被静默转换成无符号数,-1变成极大值,循环边界直接失效。这类问题编译器只给警告,不给错误。

4.2 表达式与语句:括号、边界与 switch 的收尾

表达式与语句这一块,规范的建议集中在「少炫技」上。基本原则里有一条是保持代码简明清晰、避免过分的编程技巧,理由是过分追求技巧会降低可读性。落到具体条款,可以整理成四个检查点。

一是用括号明确优先级。a & b == c和(a & b) == c在语义上完全不同,前者实际等价于a & (b == c)。这类错误不会报错,只会算错。

二是避免在同一表达式里堆叠副作用。i++ + i++这类写法在不同编译器下结果可能不同,属于典型的未定义行为。

三是条件判断里不要做赋值。if (iValue = 0)永远为假,且编译器只给一个「可能是有意赋值」的提示,很多人看都不看。

四是switch的每个分支都要有break,default分支必须写。规范在布局一节已经要求每个case用{}括起来,配合break形成固定的收尾结构,这套写法唯一的成本是多打几个字符,收益是把「漏写 break 导致的穿透」这类问题从根上消除。

另外两条来自基本原则:尽量避免使用 GOTO 语句;编程时首先达到正确性,其次才考虑效率。后一条在性能优化盛行的语境下容易被忽略,但它的顺序不能反——一个跑得快但算错的函数,优化得越好损失越大。

4.3 函数与过程:单一职责落到参数和返回值上

函数层面的规范目标很明确:保持函数小而专注,减少函数间的耦合,强调参数和返回值的合理性。拆开来看是四件事。

第一,一个函数只做一件事。判断标准可以粗放一点:如果函数名里出现了「And」,或者需要三段注释才能说清它干了什么,就该拆。

第二,参数个数要控制。参数越多,调用方出错的概率越高,而且参数的顺序错误编译器往往发现不了——两个同为int的参数传反了,编译照过,运行时才炸。

第三,入参必须先校验。指针参数判空、长度参数判零、枚举参数判范围,这些检查放在函数入口,比让错误往下传再回头排查便宜得多。

第四,返回值风格要统一。要么统一用错误码(0 表示成功,非 0 表示具体错误),要么统一用布尔值加输出参数,不要一个模块里两种风格混用。

/* 返回值:0 成功,非 0 为错误码;出参用二级指针 */ int BufferMgrAlloc(unsigned int uiSize, void **ppBuf) { if (NULL == ppBuf) /* 入参先校验,避免后续解引用崩溃 */ { return ERR_INVALID_PARAM; } *ppBuf = NULL; /* 出参先置空,调用方可安全释放 */ if (0 == uiSize) /* 边界值单独判断 */ { return ERR_INVALID_PARAM; } *ppBuf = malloc(uiSize); if (NULL == *ppBuf) /* 申请失败必须检查,不能假定成功 */ { return ERR_NO_MEMORY; } return ERR_OK; }

这段代码对应三个检查动作:入参校验放在最前面,出参在函数一开始就置空(这样即使后续失败,调用方free(NULL)也是安全的),内存申请结果必须判空。三点都不复杂,但少任何一点,问题都会推迟到运行期才暴露。

4.4 可靠性、可测试性与断言:让错误早点暴露

可靠性部分的核心是资源管理和边界检查。资源申请与释放必须配对,函数有多条返回路径时尤其容易漏;内存释放后要把指针置为NULL,防止二次释放;数组和缓冲区操作必须做边界检查,长度来自外部的拷贝一律用带长度限制的版本。这些都是血泪经验堆出来的条款,不是理论推导。

可测试性部分的要求更偏设计:代码要容易被验证和调试,意味着减少隐藏依赖。一个函数如果在内部直接读全局配置、直接开文件、直接调时间函数,测试时就没法构造确定的输入。把依赖通过参数传进来,可测试性自然就上去了。

断言与错误处理是这一章最容易被混为一谈的一组概念,必须分清:

维度断言错误处理
检查对象程序逻辑上「不可能发生」的情况运行环境、外部输入导致的「可能发生」的情况
典型场景内部函数入参违反约定、状态机进入非法状态内存申请失败、文件打开失败、网络超时
发布版本行为通常被裁掉,不产生开销必须保留,返回错误码或降级处理
使用禁忌不能在断言表达式里做赋值、函数调用等有副作用的操作不能吞掉错误,不能只打日志不返回

最容易翻车的写法是把断言当错误处理用:assert(pBuf != NULL);之后直接解引用。发布版本里断言被裁掉,判空逻辑随之消失,线上就是一个空指针崩溃。正确做法是两者并存——断言用于在开发期抓住逻辑错误,判空和错误码用于在运行期兜住外部异常。

5. 易漏点:五条最容易被跳过的条款

复习这份规范时,真正拉开差距的不是记住了多少条,而是知道哪几条最容易在执行时被跳过。下面五条,每一条都在代码评审里反复出现。

第一条:头文件的防重复包含和包含顺序。易漏点在于,很多人在写头文件时直接跳到内容,忘了#ifndef/#define/#endif这三行。为什么漏?因为单文件编译时不出错,只有在多个源文件互相包含、或者头文件形成环形依赖时才暴露问题,而那时错误信息往往是「重复定义」,很难第一时间联想到是头文件保护缺失。怎么补?把项目里的公共头文件模板固化下来,新建文件一律从模板复制;同时检查#include顺序,标准库在前、非标准库在后、本模块自己的头文件最后,这个顺序能提前暴露隐式依赖。

第二条:80 列限制包含注释和续行。易漏点在于,很多人只盯着代码行,忘了注释行也计数。为什么漏?因为编辑器不会主动提示列宽,只有开了标尺线或者提交时跑格式检查才会发现。怎么补?在编辑器里把第 80 列标尺打开,并在提交前过一遍长注释;续行时记住操作符放在新行行首,续行位置按条件表达式、for 初始化、第一个参数、赋值号四种情况分别对齐。

第三条:一元操作符和二元操作符的空格规则记反。易漏点集中在*和&这两个符号上——作为解引用和取地址时是一元操作符,前后不加空格;作为乘法和按位与时是二元操作符,两边要加空格。为什么漏?因为符号长得一样,写的时候脑子在想逻辑,手上就顺手打了空格或者没打。怎么补?记住判断口诀:看它后面有没有第二个操作数。*pfBuffer后面没有操作数,是一元;dWidth * dHeight后面有操作数,是二元。

第四条:switch的case花括号和break遗漏。易漏点在于case分支里的变量定义。为什么漏?因为 C++ 里case分支不构成独立作用域,在分支里定义变量如果不用{}包起来,会直接编译报错「跳转绕过变量初始化」,而报错位置往往指向switch那一行,容易让人误以为是switch本身的问题。怎么补?养成「写case先打一对花括号」的条件反射,把{}和break当成case的固定收尾,和写if必加{}一起记。

第五条:断言表达式里带副作用。易漏点在于assert(Func() == ERR_OK);这种写法——断言里调用了函数。为什么漏?因为在调试版本里它工作正常,测试全过。怎么补?把断言表达式限制成纯判断,只读变量、不做赋值、不调函数;如果确实需要先取结果再断言,就把调用提到断言外面单独一行,先拿返回值再判断。发布版本会裁掉断言,任何写在断言里的逻辑都会随之消失,这条规则没有例外。

6. 复习清单:把规范变成提交前能跑一遍的动作

规范读一遍容易,用起来难,难在它是一份静态文档,而写代码是动态过程。我的做法是把它压缩成一张提交前能逐项走一遍的检查表,按「文件级 → 行级 → 逻辑级 → 质量级」四层排列,每层只留最关键的几项。

层级检查项判定标准常用手段
文件级头文件保护#ifndef/#define/#endif齐全且宏名与文件名一致从模板新建文件
文件级包含顺序标准库 → 非标准库 → 本模块人工过一遍文件头
文件级节分隔注释常量、类型、函数原型各节有注释块分隔对照规范模板
行级列宽代码和注释均不超过 80 列编辑器标尺线
行级缩进无 TAB,4 空格提交前跑格式检查
行级括号if/for/while/switch的case均有{}目视扫描控制流
行级空格一元贴、二元空、函数名后不空对比正例反例
逻辑级入参校验所有指针判空、长度判零、枚举判范围看函数入口前 10 行
逻辑级返回值检查所有可能失败的调用都有判断分支搜函数名逐个确认
逻辑级资源配对申请与释放成对,释放后指针置空按返回路径走一遍
质量级断言纯度断言表达式内无赋值、无函数调用搜assert(
质量级命名可读无tmp/data/flag类万能名抽查新增变量
质量级注释有效注释解释意图而非复述语法抽查新增函数头

这张表的使用方式比表本身更重要:不要等项目结束再整体过一遍,那时改造成本已经很高。我的习惯是把它拆成两个时点——写代码时只盯「行级」四项,因为它们可以边写边做,几乎零成本;提交前再看「逻辑级」和「质量级」,这时候改动集中在函数入口和资源路径上,范围可控。

复习这份规范还有个更省力的办法:把它当成两套规则来记,规则性条款列成必过清单,建议性条款只记意图。规则性条款是硬性的,忘了就是违规,比如缩进、括号、断言纯度;建议性条款是权衡性的,比如「相关性强的代码相邻」「尽量复用老代码」,知道它想解决什么问题即可,具体怎么落地取决于项目。这个二分法能让复习量减少一半,同时不丢掉真正会被评审拦下来的那些点。

最后说一个自己的教训。早些年接手一份历史模块,图快,改完逻辑就直接提交,缩进用的是 TAB,switch里两个case忘了写break,断言里还顺手调了个函数。功能测试全过,代码评审被打回来三次,第三次评审意见只有一句话:「这份代码别人没法维护。」从那以后我每次提交前都强制走一遍上面那张表的前两行——文件级和行级,五分钟能过完,但省下的是别人半小时的阅读时间,以及自己三轮返工的来回。规范这东西,背下来不算会,能变成提交前的肌肉记忆才算。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询