C语言编程规范这部分一直想总结一下。现在终于付诸行动了。
其实之前讲过一些面试题,参看:嵌入式面试知识点总结 – C语言篇
里面已经有包含一部分了,比如《高质量C++ C编程指南》.林锐着.pdf。
此次主要参考 华为技术有限公司c语言编程规范
和 MISRA C2012
再详细讲一下C语言的编程规范。
下载:编程规范
这篇文章主要以MISRA C 2012 中文版为基础,再将华为C语言编程规范融入其中。
看来之前总结两年的 C语言再学习 专栏,又有用武之地了。
参看: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 {};
参看:C语言再学习 – 存储类、链接
分为三类,外部连接(链接)(external linkage)、内部连接(链接)(internal linkage)和无连接(链接)(no 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;
}
本规范的编制,具有普适性,故会出现如“对象”、“类”这些标准 C 中不提及的概念,对象在 C 语言中的直接对应是变量。当前对象不仅仅是变量,但本译文仅限考虑标准 C(准确的说是嵌入式 C),故不过多描述,我们将其当成“变量”理解即可。
每条 MISRA C 准则都可以被归类为“规则”或“指令”。
MISRA C2012将规则和指令均分为三个级别:
- 级别:必要
- 解读:程序应仅使用所选标准版本中指定的 C 语言及其库的功能
- 级别:建议
- 解读:不要用编程语言扩展属性,否则会降低程序的可移植性
- 级别:必要
- 解读:一些未定义或未指定的行为有特定的规则处理。此规则意在防止其他未定义和关键的未指定行为。MISRA C 的许多准则旨在避免某些未定义和未指定的行为。 例如,遵守 Rule 11.4、Rule 11.8 和 Rule 19.2 的所有内容可确保在
C 中不能创建指向使用 const 限定类型声明的对象的非 const 限定指针。这避免了 C90 [Undefined 39]和 C99
[Undefined 61]。
- 级别:必要
- 解读:如果一个程序没有表现出任何未定义的行为,那么无法到达的代码就不能被执行,也不能对程序的输出产生任何影响。因此,无法到达的代码的存在可能表明程序逻辑中的错误。
无法到达的代码会占用目标机器的内存空间,可能会导致编译器在围绕无法到达的代码传输控制时选择更长的、更慢的跳转指令。而且在循环中,它可以防止整个循环驻留在指令缓存中。
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; /* 违规 - 此语句肯定不可达 */
}
- 级别:必要
- 解读:任何可以删除掉但是不影响程序正常运行的代码都是无效代码,由于无效代码可能被编译器删除,所以它的存在可能会引起混乱。
void g(void)
{
/* 合规 - 此函数中无任何操作 */
}
void h(void)
{
g(); /* 违规 - 该调用可以被移除 */
}
- 级别:建议
- 解读:如果声明了类型但没有使用,那么审阅者就不清楚该类型是冗余的还是错误地未使用。
int16_t unusedtype(void)
{
typedef int16_t local_Type; /* 违规 */
return 67;
}
- 级别:建议
- 解读:如果一个类型标签被声明但从未被使用过,对于审阅者来说,无法确定该类型标签是多余的还是被错 误闲置的。
typedef struct record_t /* 违规 */
{
uint16_t key;
uint16_t val;
} record1_t;
typedef struct /* 合规 */
{
uint16_t key;
uint16_t val;
} record2_t;
- 级别:建议
- 解读:如果一个宏被声明但从未被使用过,对于审阅者来说,无法确定该宏是多余的还是被错误闲置的。
void use_macro(void)
{
#define SIZE 4
#define DATA 3 /* 违规 - DATA 未被使用 */
use_int16(SIZE);
}
- 级别:建议
- 解读:如果一个执行标签(label)被声明但从未被使用过,对于审阅者来说,无法确定该执行标签是多余的还是被错误闲置的。
void unused_label(void)
{
int16_t x = 6;
label1: /* 违规 */
use_int16(x);
}
tag 和 label,两者翻译为中文都是标签,差别在于tag为枚举、结构体、联合体类型的标签,label为goto语句执行目的地的标签,本文中为区分,将它们分别描述为了类型标签与执行标签。
- 级别:建议
- 解读:绝大多数函数都将使用它们所定义的每一个参数。如果函数中的参数未被使用,则可能函数的实现与其预期定义不匹配。本规则强化描述了这一潜在的不匹配。
void withunusedpara(uint1 6_t *para1, int16_t unusedpara) /* 违规 - 参数未使用 */
{
*para1 = 42U;
}
- 级别:必要
- 解读:“/”和“//”均为注释起始的字符序列,如果在一段由“/”起始的注释中,又出现了“/”或“//”,那么很可能是由缺少“/”引起的。如果这两个注释起始的字符序列出现在由“//”起始的注释中,则很可能是因为使用“//”注释掉了代码。
x = y // /*
+ z
// */
;
此示例得出的结果是 x=y+z,但在没有两个“//”的情况下,结果是 x=y。
- 级别:必要
- 解读:如果包含“//”注释的源代码行在源字符集中以“\”字符结尾,则下一行将成为注释的一部分。 这可能会导致意外删除代码。
extern bool_t b;
void f(void)
{
uint16_t x = 0; // comment \
if (b)
{
++x; /* if 语句被作为注释处理, 这里无条件执行 */
}
}
参看:C语言再学习 – 关于注释
**面试题:**以下注释哪条是错误的??
#include
int main (void)
{
int /*...*/i;
char* s = "abcd //efg";
//hello \
world!
//in/*...*/t i;
return 0;
前三条注释都是对的,有没有想到。
- 级别:必要
- 解读:若八进制或十六进制转译序列后跟随其他字符,会造成混淆。例如,字符串“\x1f”仅由一个字符组成,而字符串“\x1g”则是由两个字符“\x1”和“g”组成。如果给字符常量或字符串文字中的每个八进制或十六进制转义序列增加显示的终止标识,则可以减少混淆的可能性。
const char *s1 = "\x41g"; /* 违规 - 无法区分哪个是转译序列, 哪个又是普通字符 */
const char *s2 = "\x41" "g"; /* 合规 - 以字符串结束标识转译序列的结束 */
const char *s3 = "\x41\x67"; /* 合规 - 以新的转译序列起始标识前一个转译序列的结束 */
int c1 = '\141t'; /* 违规 - 无法区分哪个是转译序列, 哪个又是普通字符 */
int c2 = '\141\t'; /* 合规 - 以新的转译序列起始标识前一个转译序列的结束 */
参看:C语言再学习 – 转义字符
建议,不使用八进制和十六进制转义序列。
"(Date should be in the form ??-??-??)"
会被编译器解析为
"(Date should be in the form ~~]"
参看:C语言再学习 – 三字母词(转)
下面是我们很容易犯的一个错误(摘自《C和指针》):
#include
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"。
- 级别:必要
- 解读:“不重名”取决于实现和所使用的 C 语言版本:在 C90 中,最小有效字符范围是前 6 个字符,且不区分大小写;在 C99 中,最小有效字符范围是前 31 个字符,而其通用字符和扩展字符的有效范围是 6 到 10 个字 符。
/* 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.1。如果每个标识符都是宏标识符,则本准则不适用,因为这种情况已被 Rule 5.4 和 Rule 5.5 涵盖。 “不重名”的定义取决于实现和所使用的 C 语言版本:
◆ 在 C90 中,最低要求是前 31 个字符有效。
◆ 在 C99 中,最低要求是前63 个字符有效,通用字符或扩展源字符视为一个字符。
/* 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; /* 合规 */
- 级别:必要
- 解读:如果在内部作用域中声明一个标识符,与在外部作用域中已经存在的标识符重名,则最内部的声明将“隐藏”外部的声明。 这可能会导致开发人员混乱。
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变量的作用域可以是代码块作用域、函数原型作用域,或者文件作用域。
这跟上面的内部外部作用域也不同~~
- 级别:必要
- 解读:本准则要求在定义一个宏时,其命名必须不同于已定义的其他宏的名称,和已定义的参数的名称。它还要求给定宏的参数名称彼此不同,但不要求宏参数名称在两个不同的宏之间不同。
“不重名”的定义取决于实现和所使用的 C 语言版本:
◆ 在 C90 中,最低要求是宏标识符的前 31 个字符有效。
◆ 在 C99中,最低要求是宏标识符的前 63 个字符有效。
/* 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 /* 合规 */
- 级别:必要
- 解读:宏名称和标识符保持不同有助于避免开发人员混淆。
在这里插入代码片
因为它后面没有“(”字符。因此,标识符在进行预处理后仍存在。#define Sum(x, y) ((x) + (y))
int16_t Sum; /* 违规 - 上面的宏Sum 与该变量重命名 */
- 级别:必要
- 解读:如果多个 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 不关联 */
- 级别:必要
- 解读:重用标签(tag)名称可能会导致开发人员混乱。这里的标签tag为枚举、结构体、联合体类型的标签。
struct stag
{
uint16_t a;
uint16_t b;
};
struct stag a1 = { 0, 0 }; /* 合规 - 与前面的定义一致 */
union stag a2 = { 0, 0 }; /* 违规 - 与声明的 struct stag 不一致。
* 同时也违背了C99的约束 */