C语言编程规范这部分一直想总结一下。现在终于付诸行动了。
其实之前讲过一些面试题,参看:嵌入式面试知识点总结 – C语言篇
里面已经有包含一部分了,比如《高质量C++ C编程指南》.林锐着.pdf。
此次主要参考 华为技术有限公司c语言编程规范
和 MISRA C2012
再详细讲一下C语言的编程规范。
下载:编程规范
这篇文章主要以MISRA C 2012 中文版为基础,再将华为C语言编程规范融入其中。
看来之前总结两年的 C语言再学习 专栏,又有用武之地了。
一、名词解释
1、声明(declare)和定义(define)
参看:C语言再学习 – 声明与定义
声明一个变量只是将变量名标识符的有关信息告诉编译器,使编译器“认识”该标识符,但声明不一定引起内存的分配。而定义变量意味着给变量分配内存空间,用于存放对应类型的数据,变量名就是对相应的内存单元的命名。在C/++程序中,大多数情况下变量声明也就是变量定义,声明变量的同时也就完成了变量的定义,只有声明外部变量时例外。函数类似,声明只是告诉编译器有这个名称、类型的函数,而定义则是函数的真实实现。
简单一句话,定义创建了对象并为这个对象分配了内存,声明没有分配内存。
以下这些就是声明:
extern int bar;
extern int g(int, int);
double f(int, double); // 对于函数声明,extern关键字是可以省略的。
class foo; // 类的声明,前面是不能加class的。
与上面的声明相应的定义如下:
int bar;
int g(int lhs, int rhs) {return lhs*rhs;}
double f(int i, double d) {return i+d;}
class foo {};
2、连接/链接(linkage)
参看:C语言再学习 – 存储类、链接
分为三类,外部连接(链接)(external linkage)、内部连接(链接)(internal linkage)和无连接(链接)(no linkage)。
- 外部连接(链接)(external linkage):对于变量,即无“static”修饰的全局可访问的变量;对于函数,即无“static”修饰的全局可调用的函数。它们即使没有在头文件中用“extern”做外部声明,仍然被识别为外部连接(链接)(external linkage)。
- 内部连接(链接)(internal linkage):即由“static”修饰的全局变量和函数,它们尽可在所在文件内访问和调用,无法被全局访问/调用。
- 无连接(链接)(no linkage):即函数内部变量。所有函数都是有连接(链接,linkage)的。内部变量包含临时变量和静态变量两种,它们的共同特征是均无法在本函数外被访问。
外部链接:
一个具有外部链接的变量可以在一个多文件程序的任何地方使用。
int n = 5; /*文件作用域,外部链接,未使用 static */
int main (void)
{
...
return 0;
}
内部链接:
一个具有内部链接的变量可以在一个文件的任何地方使用。
static int dodgers = 3; /*文件作用域,内部链接,使用了 static ,该文件所私有*/
int main (void)
{
...
return 0;
}
空链接:
具有代码块作用域或者函数原型作用域的变量有空链接,意味着它们是由其定义所在的代码块或函数原型所私有的。
double blocky (double cleo)
{
double patcrick = 0.0; /*代码块作用域,空链接,该代码块所私有*/
int i;
for (i = 0; i < 10; i++)
{
double q = cleo * i; /*q作用域的开始*/
...
patrick * = q;
} /*q作用域的结束*/
return patrick;
}
3、对象(object)
本规范的编制,具有普适性,故会出现如“对象”、“类”这些标准 C 中不提及的概念,对象在 C 语言中的直接对应是变量。当前对象不仅仅是变量,但本译文仅限考虑标准 C(准确的说是嵌入式 C),故不过多描述,我们将其当成“变量”理解即可。
二、规则
每条 MISRA C 准则都可以被归类为“规则”或“指令”。
- 规则:仅对源代码进行分析,即可对规则进行合规性判定,静态分析工具应该具有判定规则的能力,不需要结合人工判定。
- 指令:仅对源代码进行分析,无法对指令进行合规性判定,往往需要结合设计文档或开发人员的经验进行综合判定,静态分析工具可能提供辅助,但不同性能的工具提供的解释可能大不相同。
MISRA C2012将规则和指令均分为三个级别:
- 强制类:必须满足;
- 必要类:应该满足,若不满足应该说明原因;
- 建议类:应该满足,若不满足应该说明原因。
1、标准 C 环境(A standard C environment)
Rule 1.1 程序不得违反标准 C 语法和约束,并且不得超出具体实现的编译限制
- 级别:必要
- 解读:程序应仅使用所选标准版本中指定的 C 语言及其库的功能
- 示例:比如你用C90的编译器你就要符合C90的规则和特性,而不能去使用到C11的特性,其实即使你使用了,编译器也不支持,但不一定都能检测出来,具有一定的风险。
Rule 1.2 不应该使用语言扩展
- 级别:建议
- 解读:不要用编程语言扩展属性,否则会降低程序的可移植性
- 示例: if ((NULL != FuncPointer) && (*FuncPointer())),这样的语句是符合语法的,且就是利用了“一旦确定结果立即停止评估”的特性,在 FuncPointer 值为
NULL 时执行“&&”的右操作数会非常危险,程序会跑到哪完全不可预知。
Rule 1.3 不得发生未定义或严重的未指定行为
- 级别:必要
- 解读:一些未定义或未指定的行为有特定的规则处理。此规则意在防止其他未定义和关键的未指定行为。MISRA C 的许多准则旨在避免某些未定义和未指定的行为。 例如,遵守 Rule 11.4、Rule 11.8 和 Rule 19.2 的所有内容可确保在
C 中不能创建指向使用 const 限定类型声明的对象的非 const 限定指针。这避免了 C90 [Undefined 39]和 C99
[Undefined 61]。
2、未使用的代码(Unused code)
Rule 2.1 项目不得包含不可达代码(unreachable code)
- 级别:必要
- 解读:如果一个程序没有表现出任何未定义的行为,那么无法到达的代码就不能被执行,也不能对程序的输出产生任何影响。因此,无法到达的代码的存在可能表明程序逻辑中的错误。
无法到达的代码会占用目标机器的内存空间,可能会导致编译器在围绕无法到达的代码传输控制时选择更长的、更慢的跳转指令。而且在循环中,它可以防止整个循环驻留在指令缓存中。
- 示例:switch中某个case分支是永远运行不到的,程序员应该删除这种代码。
enum light { red, amber, red_amber, green };
enum light next_light ( enum light c )
{
enum light res;
switch ( c )
{
case red:
res = red_amber;
break;
case red_amber:
res = green;
break;
case green:
res = amber;
break;
case amber:
res = red;
break;
default:
{
/* 当参数 c 的值不是枚举型 light 的成员时, 此 default 分支才可达 */
error_handler ( );
break;
}
}
return res;
res = c; /* 违规 - 此语句肯定不可达 */
}
Rule 2.2 不得有无效代码(dead code)
- 级别:必要
- 解读:任何可以删除掉但是不影响程序正常运行的代码都是无效代码,由于无效代码可能被编译器删除,所以它的存在可能会引起混乱。
- 示例: 函数 g 不包含无效代码,且其本身也不是无效代码,因为它不含任何操作。但是对它的调用无效,因为删除它不影响程序行为。
void g(void)
{
/* 合规 - 此函数中无任何操作 */
}
void h(void)
{
g(); /* 违规 - 该调用可以被移除 */
}
Rule 2.3 项目不应包含未被使用的类型(type)声明
- 级别:建议
- 解读:如果声明了类型但没有使用,那么审阅者就不清楚该类型是冗余的还是错误地未使用。
- 示例:
int16_t unusedtype(void)
{
typedef int16_t local_Type; /* 违规 */
return 67;
}
Rule 2.4 项目不应包含未被使用的类型标签(tag)声明
- 级别:建议
- 解读:如果一个类型标签被声明但从未被使用过,对于审阅者来说,无法确定该类型标签是多余的还是被错 误闲置的。
- 示例:类型标签 record_t 仅在 record1_t 的类型声明中使用,而在需要使用该类型的位置均使用了 record1_t。此时,我们可以以省略标签的方式声明类型以满足本规则要求,如 record2_t。
typedef struct record_t /* 违规 */
{
uint16_t key;
uint16_t val;
} record1_t;
typedef struct /* 合规 */
{
uint16_t key;
uint16_t val;
} record2_t;
Rule 2.5 项目不应包含未被使用的宏(macro)声明
- 级别:建议
- 解读:如果一个宏被声明但从未被使用过,对于审阅者来说,无法确定该宏是多余的还是被错误闲置的。
- 示例:
void use_macro(void)
{
#define SIZE 4
#define DATA 3 /* 违规 - DATA 未被使用 */
use_int16(SIZE);
}
Rule 2.6 函数不应包含未被使用的执行标签(label)声明
- 级别:建议
- 解读:如果一个执行标签(label)被声明但从未被使用过,对于审阅者来说,无法确定该执行标签是多余的还是被错误闲置的。
- 示例:
void unused_label(void)
{
int16_t x = 6;
label1: /* 违规 */
use_int16(x);
}
tag 和 label,两者翻译为中文都是标签,差别在于tag为枚举、结构体、联合体类型的标签,label为goto语句执行目的地的标签,本文中为区分,将它们分别描述为了类型标签与执行标签。
Rule 2.7 函数中不应有未使用的变量
- 级别:建议
- 解读:绝大多数函数都将使用它们所定义的每一个参数。如果函数中的参数未被使用,则可能函数的实现与其预期定义不匹配。本规则强化描述了这一潜在的不匹配。
- 示例
void withunusedpara(uint1 6_t *para1, int16_t unusedpara) /* 违规 - 参数未使用 */
{
*para1 = 42U;
}
3、注释(Comments)
Rule 3.1 字符序列“/*”和“//”不得在注释中使用
- 级别:必要
- 解读:“/”和“//”均为注释起始的字符序列,如果在一段由“/”起始的注释中,又出现了“/”或“//”,那么很可能是由缺少“/”引起的。如果这两个注释起始的字符序列出现在由“//”起始的注释中,则很可能是因为使用“//”注释掉了代码。
- 示例:在下面 C99 代码的示例中,“//”的出现改变了程序的含义:
x = y // /*
+ z
// */
;
此示例得出的结果是 x=y+z,但在没有两个“//”的情况下,结果是 x=y。
Rule 3.2 “//”注释中不得使用换行(即“//”注释中不得使用行拼接符“\”)
- 级别:必要
- 解读:如果包含“//”注释的源代码行在源字符集中以“\”字符结尾,则下一行将成为注释的一部分。 这可能会导致意外删除代码。
- 示例:在下面的违规示例中,包含 if 关键字的物理行在逻辑上是前一行的一部分,因此是注释。
extern bool_t b;
void f(void)
{
uint16_t x = 0; // comment \
if (b)
{
++x; /* if 语句被作为注释处理, 这里无条件执行 */
}
}
参看:C语言再学习 – 关于注释
**面试题:**以下注释哪条是错误的??
#include <stdio.h>
int main (void)
{
int /*...*/i;
char* s = "abcd //efg";
//hello \
world!
//in/*...*/t i;
return 0;
前三条注释都是对的,有没有想到。
4、字符集和词汇约定(Character sets and lexical conventions)
Rule 4.1 八进制和十六进制转译序列应有明确的终止识别标识
- 级别:必要
- 解读:若八进制或十六进制转译序列后跟随其他字符,会造成混淆。例如,字符串“\x1f”仅由一个字符组成,而字符串“\x1g”则是由两个字符“\x1”和“g”组成。如果给字符常量或字符串文字中的每个八进制或十六进制转义序列增加显示的终止标识,则可以减少混淆的可能性。
- 示例:在此示例中,由 s1,s2 和 s3 指向的每个字符串都等效于字符串“Ag”。
const char *s1 = "\x41g"; /* 违规 - 无法区分哪个是转译序列, 哪个又是普通字符 */
const char *s2 = "\x41" "g"; /* 合规 - 以字符串结束标识转译序列的结束 */
const char *s3 = "\x41\x67"; /* 合规 - 以新的转译序列起始标识前一个转译序列的结束 */
int c1 = '\141t'; /* 违规 - 无法区分哪个是转译序列, 哪个又是普通字符 */
int c2 = '\141\t'; /* 合规 - 以新的转译序列起始标识前一个转译序列的结束 */
参看:C语言再学习 – 转义字符
建议,不使用八进制和十六进制转义序列。
Rule 4.2 禁止使用三字母词(trigraphs)
- 级别:建议
- 解读:三字母词(或叫三联符序列)由两个问号起始,后跟一个特定字符组成。截至目前(2020 年),三字母词只有 9 个:
源代码中的“三字母词”,在编译阶段会被替换为“对应的字符”。 而它们会与两个问号的其他用法引起意外混淆。
- 示例:
例如,字符串
"(Date should be in the form ??-??-??)"
会被编译器解析为
"(Date should be in the form ~~]"
参看:C语言再学习 – 三字母词(转)
下面是我们很容易犯的一个错误(摘自《C和指针》):
#include <stdio.h>
int main (void)
{
printf("??( \n");
printf("??) \n");
return 0;
}
root@# gcc test.c
test.c: 在函数‘main’中:
test.c:4:10: 警告: 三元符 ??( 被忽略,请使用 -trigraphs 来启用 [-Wtrigraphs]
test.c:5:10: 警告: 三元符 ??) 被忽略,请使用 -trigraphs 来启用 [-Wtrigraphs]
root@# gcc -trigraphs test.c
输出结果:
[
]
注意 :由于编译器的种类各样,对ANSI C的支持也不一样,所以可能会有些C编译器不处理“三字母词”,会将它们当做普通的字符串来处理。 以上测试是在VC++ 6.0下进行的,对于GCC编译器,需要在编译的时候添加选择"-ansi"或者"-trigraphs"。
5、标识符(Identifiers)
Rule 5.1 外部标识符不得重名
- 级别:必要
- 解读:“不重名”取决于实现和所使用的 C 语言版本:在 C90 中,最小有效字符范围是前 6 个字符,且不区分大小写;在 C99 中,最小有效字符范围是前 31 个字符,而其通用字符和扩展字符的有效范围是 6 到 10 个字 符。
- 示例:
在以下示例中,所有定义均出现在同一翻译单元中。 该实现中外部标识符中支持 31 个区分大小写的字符。
/* 1234567890123456789012345678901********* Characters */
int32_t engine_exhaust_gas_temperature_raw;
int32_t engine_exhaust_gas_temperature_scaled; /* 违规 */
/* 1234567890123456789012345678901********* Characters */
int32_t engine_exhaust_gas_temp_raw;
int32_t engine_exhaust_gas_temp_scaled; /* 合规 */
全局变量、宏、全局函数等,均需符合此准则,以 C99 为例,前 31 个字符必须不相同,一个有效的办法是,命名少于 31 个字符,且不重名。
Rule 5.2 同范围和命名空间内的标识符不得重名
- 级别:必要
- 解读:如果两个标识符都是外部标识符,则本准则不适用,因为此情况适用于 Rule 5.1。如果每个标识符都是宏标识符,则本准则不适用,因为这种情况已被 Rule 5.4 和 Rule 5.5 涵盖。 “不重名”的定义取决于实现和所使用的 C 语言版本:
◆ 在 C90 中,最低要求是前 31 个字符有效。
◆ 在 C99 中,最低要求是前63 个字符有效,通用字符或扩展源字符视为一个字符。
- 示例:
在下面的示例中,所讨论的实现为:在不具有全局属性的标识符中支持 31 个区分大小写的字符。
/* 1234567890123456789012345678901********* Characters */
extern int32_t engine_exhaust_gas_temperature_raw;
static int32_t engine_exhaust_gas_temperature_scaled; /* 违规 */
void f(void)
{
/* 1234567890123456789012345678901********* Characters */
int32_t engine_exhaust_gas_temperature_local; /* 合规 */
}
/* 1234567890123456789012345678901********* Characters */
static int32_t engine_exhaust_gas_temp_raw;
static int32_t engine_exhaust_gas_temp_scaled; /* 合规 */
Rule 5.3 内部声明的标识符不得隐藏外部声明的标识符
- 级别:必要
- 解读:如果在内部作用域中声明一个标识符,与在外部作用域中已经存在的标识符重名,则最内部的声明将“隐藏”外部的声明。 这可能会导致开发人员混乱。
- 示例:
extern void g(struct astruct *p);
int16_t xyz = 0; /* 定义变量 "xyz" */
void fn2 (struct astruct xyz) /* 违规 - 外部定义的 "xyz" 被同名形参隐藏 */
{
g(&xyz);
}
uint16_t speed;
void fn3(void)
{
typedef float32_t speed; /* 违规 - 类型将变量给隐藏 */
}
参看:C语言再学习 – 存储类、链接
按照C语言作用域划分:
一个C变量的作用域可以是代码块作用域、函数原型作用域,或者文件作用域。
这跟上面的内部外部作用域也不同~~
Rule 5.4 宏标识符不得重名
- 级别:必要
- 解读:本准则要求在定义一个宏时,其命名必须不同于已定义的其他宏的名称,和已定义的参数的名称。它还要求给定宏的参数名称彼此不同,但不要求宏参数名称在两个不同的宏之间不同。
“不重名”的定义取决于实现和所使用的 C 语言版本:
◆ 在 C90 中,最低要求是宏标识符的前 31 个字符有效。
◆ 在 C99中,最低要求是宏标识符的前 63 个字符有效。
- 示例:
在以下示例中,讨论的实现为:宏标识符中支持 31 个区分大小写的有效字符。
/* 1234567890123456789012345678901********* Characters */
#define engine_exhaust_gas_temperature_raw egt_r
#define engine_exhaust_gas_temperature_scaled egt_s /* 违规 */
/* 1234567890123456789012345678901********* Characters */
#define engine_exhaust_gas_temp_raw egt_r
#define engine_exhaust_gas_temp_scaled egt_s /* 合规 */
Rule 5.5 宏标识符与其他标识符不得重名
- 级别:必要
- 解读:宏名称和标识符保持不同有助于避免开发人员混淆。
- 示例:
在下面的违规示例中,类似函数的宏 Sum 的名称也用作标识符。对象(变量)Sum 的声明不进行宏展开,在这里插入代码片
因为它后面没有“(”字符。因此,标识符在进行预处理后仍存在。
#define Sum(x, y) ((x) + (y))
int16_t Sum; /* 违规 - 上面的宏Sum 与该变量重命名 */
typedef 名称应是唯一标识符
- 级别:必要
- 解读:如果多个 typedef 名称命名相同而它们实际指代又是不同的函数、对象或枚举常量时,开发人员会被困扰。
- 示例:
void func ( void )
{
{
typedef unsigned char u8_t;
}
{
typedef unsigned char u8_t; /* 违规 - 重复使用 */
}
}
typedef float mass;
void func1 ( void )
{
float32_t mass = 0.0f; /* 违规 - 重复使用 */
}
typedef struct list
{
struct list *next;
uint16_t element;
} list; /* 合规 - 符合例外的情况 */
typedef struct
{
struct chain
{
struct chain *list;
uint16_t element;
} s1;
uint16_t length;
} chain; /* 违规 - 标记 "chain" 与 typedef 不关联 */
Rule 5.7 标签(tag)名称应是唯一标识符
- 级别:必要
- 解读:重用标签(tag)名称可能会导致开发人员混乱。这里的标签tag为枚举、结构体、联合体类型的标签。
- 示例:
struct stag
{
uint16_t a;
uint16_t b;
};
struct stag a1 = { 0, 0 }; /* 合规 - 与前面的定义一致 */
union stag a2 = { 0, 0 }; /* 违规 - 与声明的 struct stag 不一致。
* 同时也违背了C99的约束 */
Rule 5.8 全局(external linkage)对象和函数的标识符应是唯一的
- 级别:必要