简介《中兴通讯软件编程规范——C/C》是一份面向C/C开发者的企业级编码规范文档适合需要统一代码风格、提升代码可读性与可维护性的研发团队及个人查阅。文档围绕基本原则、代码布局、注释、命名规则、变量常量与类型、表达式与语句、函数与过程、可靠性、可测试性、断言与错误处理等主题展开并附有编程模版和规范检查表便于在项目开发与代码评审中对照执行。压缩包内共1个doc文件大小约486KB内容为完整标准文本结构清晰可直接检索相关条款。目前已有301人学习适合用于团队规范制定、代码审查参考以及个人编码习惯的查漏补缺。1. 一份写在 2003 年的 C/C 编码规范今天还值得翻吗想象一家餐馆的后厨十个厨师、八口锅如果每个人切葱的粗细、放盐的时机、装盘的顺序全凭手感那菜端出去是什么味道就是随机的。所以后厨墙上会贴一张操作卡葱切几毫米、油温几成、出餐几分。C/C 编码规范就是软件团队墙上那张操作卡——它不管你的算法怎么想只管代码写出来长什么样、文件怎么排、名字怎么取、出错怎么退。值得搞懂的地方在于这些条款背后全是钱。一份 2003 年发布的企业级 C/C 编码规范正文从基本原则、布局、注释、命名、变量与类型、表达式与语句一路写到函数、可靠性、可测试性、断言与错误处理末尾还挂着编程模板和一张规范检查表。能写这么厚说明一件事代码写得乱不是审美问题是维护成本问题。2. 为什么一份“格式清单”能撑起一份企业标准2.1 规则、建议、原则三种硬度的约定翻开术语定义那一节会看到一种很克制的分类。原则是编程时应该坚持的指导思想规则是必须遵守的约定建议是必须加以考虑的约定。再往下还有说明、正例、反例三种配套材料。这个分层不是文字游戏它决定了每一条条款的“硬度”。举个例子原则 1-1 写的是“首先是为人编写程序其次才是计算机”。这属于原则它没法用工具检查也没法量化但它决定了后面所有条款的走向。而“程序中一行的代码和注释不能超过 80 列”是规则白纸黑字超了就是超了。至于“源程序中关系较为紧密的代码应尽可能相邻”是建议——它讲的是倾向具体怎么算“紧密”得看写代码的人判断。规范前言里交代得很清楚规则性规范要求所有软件开发人员严格执行建议性规范各项目编程人员可以根据实际情况选择执行。这一句话其实是在承认现实——不同产品线、不同项目阶段的约束条件不一样一刀切的强制只会让人阳奉阴违地绕过规范。2.2 为什么把“好看”写进企业标准很多人第一次看这类文档都会有一个疑问缩进几个空格、括号放哪一行这种事凭什么上升到企业标准的高度前言的回答很直接统一编程风格是为了提高源程序的可读性、可靠性和可重用性减少维护成本最终提高软件产品生产力。关键在于“生命周期”这四个字。一段代码从写下到报废要经历开发、测试、生产、用户使用、版本升级、后期维护。写的时候花十分钟省下的排版功夫可能在某次故障排查里让三五个人多花三天。规范里反复强调一条只有易读、易维护的代码才有生命力。这不是修辞是成本核算。再直白一点代码是写给下一个接手的人看的而那个人很可能就是半年后的你自己。半年后的你记不住当时为什么在这里绕了一个弯只能靠命名和注释去还原意图。规范里那句“保持代码的简明清晰避免过分的编程技巧”说的就是别给未来的自己挖坑。2.3 80 列、4 个空格这些数字是怎么来的这套标准里有一批看起来很“玄学”的数字一行不超过 80 列、缩进 4 个空格、禁止使用 TAB 键。放在大屏显示器的今天80 列显得很抠门。但如果回到这类标准成型的年代答案就很朴素了——当时的终端和打印纸宽度就是这个量级代码评审经常是把源文件打印出来一群人围着一张纸逐行看。80 列还有一个隐性好处它逼着你把复杂的表达式拆开。一个 if 条件写到 130 列说明它塞了太多判断本身就该重构。至于禁止 TAB理由规范里写得很实在——不同编辑器对 TAB 的展开宽度处理不一样你屏幕上对齐得好好的换个人打开就是一团乱麻。用空格代替 TAB等于把“排版结果”写死进文件而不是留给编辑器去解释。这些数字本身不重要重要的是它们代表的那种思路凡是能靠工具和约定消除的差异就不要留给人的自觉。3. 这份规范到底管住了哪些地方3.1 文件布局先把骨架摆正再往里填肉布局这一章在讲一件很基础但很少人系统想过的事一个源文件里各种东西该按什么顺序出现。规范给头文件和实现文件分别规定了次序并且明确某些节如果不需要可以省略但保留下来的节必须保持这个次序。头文件的推荐顺序是文件头注释、防重复包含的宏、其它条件编译选项、include、常量定义、全局宏、全局数据类型、类定义、模板、全局函数原型。实现文件的顺序是文件头、include、常量定义、文件内部宏、文件内部数据类型、全局变量、静态全局变量、局部函数原型、类的实现、全局函数、局部函数。/* 文件头模块名、功能说明、修改记录具体格式见注释章节 */ #ifndef MODULE_NAME_H /* 宏名与文件名对应并全大写用于防重复包含 */ #define MODULE_NAME_H #include stdio.h /* 标准库头文件用尖括号 */ #include heads.h /* 项目内头文件用双引号 */ #define MAX_BUF_SIZE 256 /* 常量与宏集中在前部定义 */ typedef unsigned char BOOLEAN; /* 全局类型定义 */ int DoSomething(void); /* 全局函数原型统一放最后方便一眼扫全 */ #endif这段模板对应三条规则头文件必须避免重复包含包含标准库头文件用尖括号、包含非标准库头文件用双引号遵循统一的布局顺序。第三条最容易被忽视——顺序乱掉不会编译失败但会让每个新接手的人都要重新搜一遍“这个文件里到底定义了什么”。规范还建议用注释块把上面这些节分隔开本质上是给文件加目录。类的定义也有固定次序公有属性、公有函数、保护属性、保护函数、私有属性、私有函数。这个顺序的潜台词是——先给外部看的接口再给派生类看的最后才是自己内部的东西。读的人从上往下看先知道“它能干什么”再知道“它怎么干的”。3.2 基本格式括号、缩进与空格的真实作用基本格式这一节是整份规范里条款最密的地方。核心几条一行代码加注释不能超过 80 列if、else、for、while、do 这些语句自占一行执行语句不得紧跟其后且不论执行语句有多少都要加花括号定义指针变量时星号放在变量名前禁止使用 TAB缩进为 4 个空格分界符{和}各占一行、位于同一列、与引用它的语句左对齐switch 里每个 case 分支和 default 都要用花括号包起来。/* 正例条件自占一行执行体必加花括号 */ if (variable1 variable2) { variable1 variable2; } /* 反例执行语句紧跟条件之后且没有花括号 */ if (variable1 variable2) variable1 variable2;反例的问题不在于它现在会出错而在于它以后会出错。某天有人在这个 if 后面加第二行语句忘了补花括号那第二行就成了无条件执行——这类事故在 C/C 里太常见了编译器通常还不报错。花括号强制条款就是用一点点书写成本换掉一整类隐蔽 bug。指针星号的位置也是同理。float *pfBuffer;和float* pfBuffer;编译结果完全一样但后者会让人误以为float*是一个整体类型进而写出float* a, b;这种只有 a 是指针的声明。星号贴变量是把“谁是指针”这件事写在脸上。再看空格规则它的颗粒度细到有点好笑一元操作符如!、~、、--、取地址前后不加空格[]、.、-前后不加空格多元运算符和操作数之间至少一个空格关键字之后要留一个空格再跟左括号函数名之后不要留空格逗号之后留空格分号不是行尾时其后留空格注释符与注释内容之间用一个空格分隔。这些规则的共同目标只有一个让“关键字”和“函数调用”在视觉上可区分。if (x)里的空格在告诉你if 是语言结构DoSomething(x)里没有空格在告诉你这是你自己起的名字。人眼扫代码时靠的就是这些细微的形状差异。对齐规则也值得一提相关的赋值语句等号对齐结构型数组和多维数组在定义时按矩阵结构分行书写。前者让一组赋值看起来像一张表后者让数据本身的形状直接呈现在源码里。3.3 注释与命名写给人看的那一半注释这一章开宗明义有效的注释是在代码的功能、意图层次上进行的注释提供有用、额外的信息而不是代码表面意义的简单重复。这句话值得抄下来贴显示器上。i; // i 加一这种注释等于没说还多占了一行维护成本。真正有价值的注释回答的是“为什么”为什么这里要跳过一条记录、为什么这个分支不能删、为什么这个魔数不能改成 8。规范给的注释规则很简C 语言用/* */C 里多行用/* */、单行用//注释符与注释内容之间要留一个空格。命名规则单独成章从目录看篇幅不小。摘要里点出的方向是变量、函数、类等命名应具有描述性遵循一定的命名约定。这一条听起来像废话但落到实际项目里“描述性”三个字经常和“短”打架。规范的取向很明确——宁可名字长一点也别让人靠猜。一个叫iCount的变量比叫n的变量在半年后省下的时间远超过当初多敲的那几个字符。3.4 断行长表达式怎么拆才不糊断行这一节处理的场景很具体表达式超过 80 列怎么办。规则是在低优先级操作符处拆分成新行操作符放在新行之首拆分出的新行要适当缩进使排版整齐。/* 条件表达式续行在第一个条件处对齐 放在新行开头 */ if ((iFormat CH_A_Format_M) (iOfficeType CH_BSC_M)) { DoSomething(); } /* 函数声明的续行在第一个参数处对齐 */ BYTE ReportStatusCheckPara(HWND hWnd, BYTE ucCallNo, BYTE ucStatusReportNo);操作符放行首而不是行尾是个很实用的选择。人读多行条件时眼睛是从左边缘开始扫的、||在最左边等于每一行开头就告诉你“这一行和上一行是什么关系”。如果放在行尾你得先读完一整行才在末尾看到连接词效率差一截。函数声明不允许把类型和名称分行书写也是同一个思路——名字和类型必须待在一起。4. 可靠性与可测试性规范里最不“格式”的两章4.1 可靠性把错误挡在函数边界前面几章解决的是“看得懂”可靠性这一章解决的是“扛得住”。基本原则里的第 4 条说得很清楚编程时首先达到正确性其次考虑效率首先要满足正确性、健壮性、可维护性、可移植性等质量因素最后才考虑程序的效率和资源占用。这个优先级排序在很多团队里是反的。为了省一次内存拷贝把代码写成迷宫为了少一个分支把参数校验删掉短期看性能涨了几个点长期看每次线上问题都要重新推演一遍那段迷宫。可靠性章节的立场是先把边界守住再谈快不快。具体的做法可以归纳成几句朴素的话函数的入口先校验参数别等解引用了空指针再回头查每条可能失败的路径都要给出明确的返回资源申请和释放要在同一个层次上配对别让释放散落在十几个 return 后面。int ReadConfig(const char *pcPath, char *pcBuf, int iLen) { FILE *pFile NULL; /* 入口先校验参数把非法输入挡在函数边界 */ if ((pcPath NULL) || (pcBuf NULL) || (iLen 0)) { return ERR_INVALID_PARAM; } pFile fopen(pcPath, r); if (pFile NULL) { return ERR_OPEN_FAIL; } /* ... 读取内容到 pcBuf ... */ fclose(pFile); /* 释放与申请在同一层次完成 */ pFile NULL; /* 指针置空防止后续误用 */ return OK; }pcPath是配置文件路径属于外部输入必须当成不可信数据处理pcBuf是调用方给的缓冲区长度由iLen约束返回值为错误码枚举调用方可以据此决定重试还是上报。这段代码里没有一处炫技全部是防御性写法——可靠性章节要的就是这个效果。4.2 可测试性代码要能被单独叫出来验证可测试性单独成章这一点在同类规范里不算常见。它的底层逻辑很简单如果一段逻辑只能在整个系统跑起来之后才能验证那它的 bug 就只能等到联调阶段才暴露修起来的价格是单元阶段的很多倍。让代码可测的做法说穿了就是降低耦合。函数职责单一别把读配置、算业务、写日志揉在一个两百行的函数里少用全局变量因为全局状态会让每次测试之间互相污染对外部依赖文件、网络、时间留出可替换的接口测试时才能塞一个假实现进去。基本原则里那条“避免或少用全局变量”理由是会导致模块间紧耦合、违反模块化要求——这个理由在可测试性上体现得最直接。4.3 断言不是错误处理断言与错误处理放在最后单独一章因为它俩最容易被混为一谈。断言检查的是“绝不该发生”的内部不变量比如某个指针在函数中段必须非空、某个状态机不可能跳到这个分支。这类检查服务于开发阶段是给写代码的人自己看的一旦触发说明代码逻辑本身错了继续跑下去没有意义。错误处理面对的是另一类情况外部输入不合法、文件打不开、内存申请失败。这些是正常世界里会发生的事程序必须给出可预期的反应而不是直接崩掉。#include assert.h /* 断言校验内部不变量属于“不该发生”发布版本通常会被裁掉 */ assert(pNode ! NULL); /* 错误处理校验外部输入属于“可能发生”必须返回给调用方 */ if (pNode NULL) { return ERR_NULL_POINTER; }assert的语义是“我确信这里不会空”一旦为空就是逻辑 bugif的语义是“这里可能空我得告诉调用方”。把该用错误处理的地方写成断言等于让用户替你承担崩溃把该用断言的地方写成 if 静默返回等于把 bug 藏起来。5. 五个最容易搞混的地方5.1 易混点一规则和建议当成一回事为什么混文档里两者混排出现编号格式也接近读快了根本分不清哪条必须做、哪条看着办。再加上很多团队内部传达时一句“按规范来”打包处理硬度差异就被抹平了。怎么区分看术语定义。规则是必须遵守的约定执行层面没有商量余地建议是必须加以考虑的约定也就是你得想过想完之后可以基于项目实际情况选择不做。前言的表述更直白——规则性规范要求所有开发人员严格执行建议性规范各项目可以自行选择。判断方法很简单一条条款如果被违反后能靠工具稳定检出、且没有合理例外它多半是规则如果它依赖“紧密”“适当”这类形容词它多半是建议。5.2 易混点二编码规范和格式化工具为什么混现在很多编辑器和检查工具能自动缩进、自动补括号、自动排空格看起来把规范里的格式章节全干了于是有人觉得规范已经过时。怎么区分格式化工具能处理的是“形状”处理不了“意图”。工具能保证{独占一行但保证不了这个函数只做一件事能保证行宽 80 列但保证不了这个 80 列的条件表达式是不是该拆成两个函数能检查命名长度但检查不出这个名字是否准确描述了它的职责。规范里布局和格式那部分确实最容易被工具替代可它同时包含了可靠性、可测试性、函数职责这些工具看不见的部分。工具是执行手段规范是判断标准两者不是替代关系。5.3 易混点三遵循规范就等于代码质量高为什么混规范里条款密密麻麻很容易产生一种错觉——全部打勾了代码就没问题了。怎么区分规范约束的是“下限”不是“上限”。一个变量名叫iTotalCount、缩进四空格、括号独占一行、注释齐全的函数完全可能算法选错、边界漏判、复杂度爆炸。规范原文的原则 1-4 已经把话说在前面首先达到正确性其次考虑效率。正确性来自设计和验证不来自排版。规范能消除的是“因为风格不统一导致的沟通成本和低级错误”它替不了设计评审和测试。5.4 易混点四断言和参数校验是一回事为什么混两者写出来都是一个 if 或者一个 assert位置也常常都在函数开头看起来只是写法差异。怎么区分看这条检查在发布版本里该不该保留。参数校验必须保留因为外部输入永远可能不合规用户、上游模块、配置文件都不受你控制。断言在发布版本里通常被裁掉因为它检查的是“如果这条不成立说明我的代码写错了”而代码写错了这件事应该在测试阶段就被消灭。判断口诀这条检查失败时该报警给用户看还是该报警给程序员看。5.5 易混点五参照执行等于照搬全文为什么混规范范围那一节写了“本规范适用于公司内使用 C/C 语言编码的所有软件”同时前言又说其它不同编程语言可以参照此规范执行。一句“参照”很容易被理解成“把 C 那套原封不动搬到别的语言”。怎么区分能搬的是思路不能搬的是语法细节。命名要有描述性、函数职责要单一、错误处理要分层、断言和校验要分开——这些跨语言成立。但花括号独占一行、指针星号贴变量、#ifndef防重复包含这些条款离开 C/C 就没有落点。规范自己也承认这一点本规范是针对 C/C 语言的编程规范其它语言参照执行。参照的意思是取其原则不是抄其条文。6. 什么时候该翻它什么时候别死磕先说该用的场合。接手一个多人协作的 C/C 项目第一步不是读业务代码而是把这份规范的布局和命名两章过一遍你会立刻看懂这个项目的文件为什么这么排、变量为什么这么叫。做代码评审时拿它当检查表用效率很高——附录里本来就给了一张规范检查表等于把“该看哪些点”提前列好了。带新人的时候它比口头说“注意代码风格”有用得多因为有正例反例争论可以停在纸面上。再看别硬套的场合。维护一套已经运行多年、风格自成体系的老代码全面按规范重排的收益极低而风险极高——一次大规模格式化 diff 会淹没真正的逻辑改动代码评审直接失效。合理做法是新写的文件按规范来老文件只在动到具体函数时顺手整理那一个函数。跨语言项目也别照搬条文前面说过能迁移的是原则不是语法。还有一种情况如果团队已经有成熟的自动化检查流水线规范里那些能被工具覆盖的格式条款就不必再靠人工记忆人力应该花在工具查不出来的地方比如函数职责划分和错误处理策略。场景建议做法新项目起步布局、命名、注释三章直接落地写进项目模板老项目接手先读规范理解现有风格不做全量重排代码评审用附录检查表逐项过重点看可靠性与错误处理跨语言项目只迁移原则性条款格式条款按目标语言惯例已有检查工具格式条款交给工具人力转向设计与边界审查有一个坑值得单独拎出来说。规范里建议性条款占比不小团队里最常见的一种翻车方式是某个人把建议当规则执行在评审里死磕“这两个赋值语句等号没对齐”而真正该讨论的“这个函数是不是承担了三个职责”反倒没人提。规范的硬度分层本来就是为了让人把注意力放在对的地方结果被用反了。从那以后我每次拿到一份编码规范都强制自己先做一遍分类哪些是能靠工具自动执行的哪些是需要人在评审时判断的哪些只是指导思想不用逐条对照。分完再决定哪几章要背下来哪几章扫一眼就行。这一步花不了二十分钟但能省掉后面无数次没意义的争论。希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
子空间算法模式识别全流程:PCA/LDA降维、源码、万字报告与讲解 1. 先看清这三个交付物的真实权重子空间算法做模式识别,是那种"课本上看起来很干净、落到代码和数据上全是毛刺"的课题。标题里同时挂着源码、万字报告和讲解,说明它不是一篇纯理论综述,而是一个完整交付物组合,这也是判… · 2026/11/2 20:04:32
AI日报:智能体训练方法开源,多模态生成与工程实践全面提速 早上起来刷了一圈信息流,今天AI圈的动静是真不小。模型训练、视频生成、编程工具、垂直场景应用,几乎每条线都有新东西冒出来,我干脆把今天值得看的整理成一份日报,给同样在关注AI动态的朋友做个参考。这条日报既有模型侧的大事&a… · 2026/11/2 20:03:31
大模型硬件选型避坑指南:显存带宽、PCIe通路与驱动栈深度解析 1. 这不是“买显卡指南”,而是一份大模型从业者用三年踩坑换来的硬件决策手册你手头正跑着一个LoRA微调任务,显存占用刚冲到92%,训练速度却掉到0.8步/秒;或者你刚部署完Qwen3.8-27B的4-bit量化版本,vLLM报错说“CUDA o… · 2026/11/2 20:03:31
OpenClaw与LINE集成提升养虾业沟通效率 1. OpenClaw与LINE集成的核心价值OpenClaw作为一款多平台即时通讯聚合工具,其与LINE的深度整合为养虾行业带来了前所未有的沟通效率提升。传统养虾场管理中,技术人员、养殖工人、销售团队之间的信息传递往往存在滞后性,而通过OpenClaw接入LIN… · 2026/11/2 23:59:44
Java record 规范构造器能否被反序列化或反射绕过 问题现象
record 类型通常靠规范构造器来把守不变式,比如禁止名字为空:
public record User(String name) {public User {if (name == null || name · 2026/11/2 23:59:44
Unity Package Manager刷新错误排查与解决方案 1. Unity Package Manager刷新资源报错问题解析最近在Unity开发过程中,Package Manager提示"Error Refreshing Asset"的问题困扰了不少开发者。作为Unity项目依赖管理的核心组件,Package Manager一旦出现问题,轻则影响资源加载&… · 2026/11/2 23:59:44
淘宝商品详情API开发实战与性能优化指南 1. 淘宝商品详情API核心功能解析淘宝开放平台的taobao.item.get接口是电商开发者最常调用的核心接口之一,它提供了获取单个商品完整信息的能力。这个接口的独特之处在于它能返回商品的基础信息、SKU数据、促销信息等全维度数据,且响应速度控制在200ms以内… · 2026/11/2 23:59:44
JJencode编码解密工具:前端JS代码混淆与源码还原实操 做前端的人大概都遇到过这种场面:交付给客户的是一个纯前端跑的小工具,或者一个 H5 小游戏,代码打包压缩完往服务器一放,懂行的人按 F12 就能把逻辑看个七七八八,改个变量名、换个皮肤就成他自己的了。这时候就会有人翻… · 2026/11/2 23:57:44
Apache HttpAsyncClient选型指南:Default与Minimal对比 1. 项目概述在构建高性能HTTP客户端时,Apache HttpAsyncClient提供了两个核心实现:DefaultHttpAsyncClient和MinimalHttpAsyncClient。这两个客户端的选择往往让开发者陷入纠结,特别是在生产环境部署时。作为在分布式系统领域深耕多年的架构师… · 2026/11/2 23:55:43
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18