本文约14,200字,建议收藏阅读
作者 | 直观解
出品 | 汽车电子与软件
工业开发几乎不得不面对和遵从各种严格的编程规范,有些规则是从代码安全角度,有些则是从代码可读性易读性出发,有些是为以后方便扩展避免架构腐化而考虑的。
1、背景
AUTOSAR C++ 14 指南文件旨在作为 MISRA C++ 2008 的一次更新,并为 ISO/IEC 14882:2014 定义的现代C++提供编码指南。该编码标准主要应用于汽车行业,但同样也可用于需要嵌入式编程的其他行业,比如无人机编程和IoT编程。
MISRA C++:2008 是由汽车行业软件可靠性协会(MISRA)开发的一套针对C++语言的编码标准,旨在帮助开发者编写出更加安全和可靠的代码 。
AUTOSAR C++14 则是基于MISRA C++:2008 进行补充和完善的一套适用于汽车软件的编码规范,它增加了对现代C++(如C++11和C++14)的支持,并根据最新的编程实践改进了部分规则。
2、AUTOSAR C++14 编程规范
AUTOSAR C++14 编程规范的主要特点如下:
扩展而非替代:AUTOSAR C++14 并不是取代现有的MISRA C++标准,而是对其进行了扩展。它增加了新的C++编码规范、更新了现有规范,并删除了那些已经过时的规范(这是autosar组织的原文描述)。
支持现代C++:该规范特别强调对C++11和C++14的支持,以适应现代C++编程的最佳实践 。
适用于安全关键系统:AUTOSAR C++14 的目标是确保代码能够在安全关键系统中使用,特别是在汽车行业中 。
标准化API和服务AP接口:作为Adaptive AUTOSAR平台(也就是autosar AP,所谓自适应平台,用于高端计算)的一部分,AUTOSAR C++14 规范为API和服务接口提供了详细的定义 。
3、MISRA C++:2008 编程规范
MISRA C++:2008 编程规范的主要特点如下:
覆盖广泛:MISRA C++:2008 包含了一系列规则和指导原则,涵盖了从编程实践到代码设计的多个方面,旨在帮助开发者避免潜在的编程错误。
面向安全性:该规范特别关注嵌入式系统中的安全性和可靠性问题,尤其是在汽车、航空和医疗设备等安全性至关重要的领域。
规则详尽:MISRA C++:2008 针对符合C++03标准的代码,共有228条编码规则,这些规则详细规定了如何编写安全可靠的代码。
4、对比与联系
起点相同:AUTOSAR C++14 是基于MISRA C++:2008 开发的,因此两者在许多规则上是一致的 。
扩展与更新:AUTOSAR C++14 在MISRA C++:2008 的基础上增加了对现代C++的支持,并根据最新的编程实践进行了调整和优化 。
应用范围:虽然两者都适用于汽车软件开发,但AUTOSAR C++14 更加专注于Adaptive AUTOSAR平台的需求,而MISRA C++:2008 则是一个更为通用的标准 。
从具体数据上讲,AUTOSAR C++14标准规定了342条规则,其中组成如下:
MISRA C++ 2008 中的 154 条规则未经修改直接采用 (67%);
131条规则基于现有的C++标准;
57条规则基于研究或其他文献或资源;
另外,AUTOSAR C++标准采用C++14而不采用C++17的主要原因之一是C++17标准中引入的新功能可能会给系统带来安全风险——检测和理解安全漏洞需要一些时间。此外,符合C++17标准的C++编译器仍然是新的,需要更多的测试和更好的支持才能用于安全关键型开发。因此,选择依靠C++14标准作为合理的中间选项。
首先确实有些规则被AUTOSAR C++14规范取消(rejected)了,而且数量还不少。按笔者统计有如下18条:
2-10-3 (Required) A typedef name (including qualification, if any) shall be a unique identifier. Rejected This rule is considered as too restrictive.
2-10-4 (Required) A class, union or enum name (including qualification, if any) shall be a unique identifier. Rejected This rule is considered as too restrictive.
statement there shall be no more than one break or goto statement used for loop termination. Rejected The goto statement shall not be used, see: A6-6-1. There can be more than one break in an iteration statement.
6-6-5 (Required) A function shall have a single point of exit at the end of the function. Rejected Single point of exit approach does not necessarily improve readability, maintainability and testability. A function can have multiple points of exit.
9-6-2 (Required) Bit-fields shall be either bool type or an explicitly unsigned or signed integral type. Rejected - Permitted types changed. New rule introduced: A9-6-1.
9-6-3 (Required) Bit-fields shall not have enum type. Rejected - Permitted types changed. New rule introduced: A9-6-1.
9-6-4 (Required) Named bit-fields with signed integer type shall have a length of more than one bit. Rejected - Permitted types changed. New rule introduced: A9-6-1.
10-3-1 (Required) There shall be no more than one definition of each virtual function on each path through the inheritance hierarchy. Rejected - Rule already covered by A10-1-1
14-5-1 (Required) A nonmember generic function shall only be declared in a namespace that is not an associated namespace. Rejected - Usage of the ADL functionality is allowed. It is also used in STL for overloaded operators lookup in e.g. out streams, STL containers.
14-6-2 (Required) The function chosen by overload resolution shall resolve to a function declared previously in the translation unit. Rejected - Usage of the ADL functionality is allowed. It is also used in STL for overloaded operators lookup in e.g. out streams, STL containers.
14-7-1 (Required) All class templates, function templates, class template member functions and class template static members shall be instantiated at least once. Rejected - It is allowed to not use all of the public methods of a class.
15-4-1 (Required) If a function is declared with an exceptionspecification, then all declarations of the same function (in other translation units) shall be declared with the same set of type-ids. Rejected - Dynamic exception specification was prohibited. Noexcept specifier shall be used instead.
16-0-3 (Required) #undef shall not be used. Rejected - The rule replaced with global rule: A16-0-1.
16-0-4 (Required) Function-like macros shall not be defined. Rejected - The rule replaced with global rule: A16-0-1.
16-2-6 (Required) The #include directive shall be followed by either aor “filename” sequence. Rejected - These are the only forms allowed by the C++ Language Standard; No need for a new rule.
16-6-1 (Required) All uses of the #pragma directive shall be documented. Rejected - The #pragma directive shall not be used, see: A16-7-1
17-0-4 (Required) All library code shall conform to MISRA C++. Rejected - The rule replaced with A17-0-2 saying that all code shall conform to AUTOSAR C++14 Coding Guidelines.
18-4-1 (Required) Dynamic heap memory allocation shall not be used. Rejected - Dynamic heap memory allocation usage is allowed conditionally, see: A18-5-1, A18-5-2, A18-5-3
以下我们以新旧对比的方式来分析这些规则,旧规则是指MISRA C++ 2008,新规则是指autosar c++ 14标准:
1、2-10-3 (Required) A typedef name (including qualification, if any) shall be a unique identifier.
**翻译:**
typedef 名称(包括限定符,如果有)应是唯一的标识符。
**拒绝原因:**
该规则被认为过于严格。
**代码示例:**
**反例:**
namespace N {
typedef int T;
}
namespace M {
typedef int T; // 违反规则,这是说旧规则里面即使不同命名空间typedef的name也必须唯一
}
在新规则,也就是autosar c++ 14标准里面,允许typedef name在合法的情况下重名。
2、2-10-4 (Required) A class, union or enum name (including qualification, if any) shall be a unique identifier.
类、联合或枚举名称(包括限定符,如果有)应是唯一的标识符。
**拒绝原因:**
该规则被认为过于严格。
**代码示例:**
**反例:**
namespace N {
class T {};
}
namespace M {
class T {}; // 违反旧规则,这是说旧规则里面即使不同命名空间class, union or enum name也必须唯一,新规则认为没必要
}
3、statement there shall be no more than one break or goto statement used for loop termination.
在循环终止时,break 或 goto 语句的使用不得超过一个。
**拒绝原因:**
goto 语句不应使用,参见 A6-6-1。在迭代语句中可以有多个 break。
**代码示例:**
**反例:**
void example() {
for (int i = 0; i < 10; ++i) {
if (i == 5) {
break; // 正确
}
}
for (int j = 0; j < 10; ++j) {
if (j == 5) {
goto end; // 违反规则
}
}
end:
;
}
也就是说新规则彻底禁止goto语句,但允许多个break;语句来退出循环。
4、6-6-5 (Required) A function shall have a single point of exit at the end of the function.
函数应在末尾有一个单一的出口点。
**拒绝原因:**
单一出口点的方法不一定提高可读性、可维护性和测试性。函数可以有多处出口。
**代码示例:**
**正例:**
int find(int arr[], int size, int target) {
for (int i = 0; i < size; ++i) {
if (arr[i] == target) {
return i; // 多处出口点
}
}
return -1; // 多处出口点
}
这个新规则实际上和禁止goto语句紧密联系,如果一个函数确实有提早退出的需求,但又不能使用goto语句跳过后面的语句直达单一退出点,这会让程序员难于处理。所以只好放开允许多个退出点。
5、9-6-2 (Required) Bit-fields shall be either bool type or an explicitly unsigned or signed integral type.
位字段应为 bool 类型或显式无符号或有符号整数类型。
**拒绝原因:**
允许的类型已更改。引入了新规则:A9-6-1。
规则 A9-6-1(必选,实现,自动化)
位域应为无符号整数类型,或者是枚举类型(其底层类型为无符号整数类型)。
明确声明位域为无符号类型可以防止意外的符号扩展、溢出和实现定义的行为。具体来说,这条规则要求在定义位域时,必须使用无符号整数类型(如 unsigned int),或者使用枚举类型,并且该枚举类型的底层类型是无符号整数类型。这样做是为了避免由于符号扩展引起的潜在问题,例如当位域值被赋给有符号整数时可能出现的符号扩展问题。此外,这也可以防止因编译器实现差异导致的未定义行为或实现定义的行为。
**代码示例:**
1 // $Id: A9-6-1.cpp 271715 2017-03-23 10:13:51Z piotr.tanski $
2
3 enum class E1 : std::uint8_t
4 {
5 E11,
6 E12,
7 E13
8 };
9 enum class E2 : std::int16_t
10 {
11 E21,
12 E22,
13 E23
14 };
15 enum class E3
16 {
17 E31,
18 E32,
19 E33
20 };
21 enum E4
22 {
23 E41,
24 E42,
25 E43
26 };
27 class C
28 {
29 public:
30 std::int32_t a : 2; // Non-compliant - signed integral type,位域,指定了bit长度
31 std::uint8_t b : 2U; // Compliant,位域,指定了bit长度
32 bool c : 1; // Non-compliant - it is implementation-defined whether bool is
33 // signed or unsigned
34 char d : 2; // Non-compliant
35 wchar_t e : 2; // Non-compliant
36 E1 f1 : 2; // Compliant
37 E2 f2 : 2; // Non-compliant - E2 enum class underlying type is signed
38 // int
39 E3 f3 : 2; // Non-compliant - E3 enum class does not explicitly define
40 // underlying type
41 E4 f4 : 2; // Non-compliant - E4 enum does not explicitly define underlying
42 // type
43 };
44 void fn() noexcept
45 {
46 C c;
47 c.f1 = E1::E11;
48 }
位域(bit field)是C语言中的一种特殊数据结构,它允许在结构体或联合体中以位为单位来指定成员的长度。位域主要用于节省存储空间,并简化对某些硬件寄存器的操作。根据C语言标准,位域应为无符号整数类型(unsigned int),或者是枚举类型(其底层类型为无符号整数类型)。这是因为无符号整数类型可以确保所有分配给该字段的位都是有效的,而不会出现符号扩展等问题。
位域的定义格式如下:
struct {
unsigned int member_name : number_of_bits;
} struct_name;
其中:
member_name 是位域成员的名称。
number_of_bits 是该成员占用的位数。
简而言之,autosar C++14的新规则简化了MISRA的旧规则,位域只允许无符号整数或者底层为无符号整数的枚举类型。而MISRA的旧规则还允许位域使用bool类型。
6、9-6-3 (Required) Bit-fields shall not have enum type.
位字段不应具有枚举类型。
**拒绝原因:**
允许的类型已更改。引入了新规则:A9-6-1(参见前一条)。
**代码示例:**
**反例:**
enum E { A, B };
struct S {
E x : 1; // 违反规则
};
**正例:**
struct S {
bool x : 1; // 符合规则
unsigned int y : 1; // 符合规则
};
7、9-6-4 (Required) Named bit-fields with signed integer type shall have a length of more than one bit.
带有有符号整数类型的命名位字段应长度超过一个位。
**拒绝原因:**
允许的类型已更改。引入了新规则:A9-6-1。
MISRA的这条旧规则彻底取消了,新规则不允许位域bit-fields使用有符号整数。
8、10-3-1 (Required) There shall be no more than one definition of each virtual function on each path through the inheritance hierarchy.
在继承层次结构中的每条路径上,每个虚函数最多只能有一个定义。
**拒绝原因:**
该规则已被 A10-1-1 覆盖。
规则 A10-1-1(必选,实现,自动化)
类不应从超过一个基类派生,除非这个基类是接口类。
多重继承会使派生类暴露于多个实现中。这使得代码更难以维护和跟踪。具体来说,这条规则建议一个类不应该从多于一个非接口类派生。多重继承可能会引入复杂性和潜在的冲突,因为派生类需要同时处理多个基类的实现细节。这不仅增加了理解和维护代码的难度,还可能导致一些难以调试的问题。通过限制类只从一个非接口类派生,可以简化类的设计和实现,使代码更加清晰和易于维护。
**代码示例:**
1 // $Id: A10-1-1.cpp 271715 2017-03-23 10:13:51Z piotr.tanski $
2
3 class A
4 {
5 public:
6 void f1() noexcept(false) {}
7
8 private:
9 std::int32_t x{0};
10 std::int32_t y{0};
11 };
12 class B
13 {
14 public:
15 void f2() noexcept(false) {}
16
17 private:
18 std::int32_t x{0};
19 };
20 class C : public A,
21 public B // Non-compliant - A and B are both not interface classes
22 {
23 };
简而言之,新旧规则都不允许多重继承(除了多重继承接口类之外),新规则比旧规则更严格。
9、14-5-1 (Required) A nonmember generic function shall only be declared in a namespace that is not an associated namespace.
非成员泛型函数只能在不是关联命名空间的命名空间中声明。
**拒绝原因:**
新规则允许使用 ADL 功能。它也用于 STL 中的操作符重载查找,例如输出流、STL 容器。
C++中的ADL(Argument-Dependent Lookup,参数依赖查找)是一种编译器查找未限定函数名称的机制。它允许编译器根据传递给函数的参数类型来决定在哪些命名空间中查找函数定义。这意味着编译器不仅会在当前作用域内查找函数,还会扩展到包含这些参数类型的命名空间中进行查找。
这个功能非常像Java中根据函数参数列表来动态加载满足函数参数列表的备选函数。
ADL的一个常见应用场景是STL库中的 swap 函数。std::swap 是一个广为人知的例子,它利用了ADL机制,使得用户可以在不使用 std:: 前缀的情况下调用 swap 函数。此外,ADL还广泛用于标准库中的其他算法函数,如 std::begin 和 std::end,它们可以操作自定义容器类型而无需显式指定命名空间。
**代码示例:**
**反例:**
namespace N {
template
void print(T t) {
std::cout << t << std::endl;
}
}
**正例:**
namespace N {
template
void print(T t) {
std::cout << t << std::endl;
}
}
void example() {
N::print(5); // 使用 ADL
}
10、14-6-2 (Required) The function chosen by overload resolution shall resolve to a function declared previously in the translation unit.
通过重载解析选择的函数应解析为在翻译单元中先前声明的函数。
这里是说原来在MISRA中的规则14-6-2 在autosar c++14中被取消了
**拒绝原因:**
新规则允许使用 ADL 功能。它也用于 STL 中的操作符重载查找,例如输出流、STL 容器。
11、14-7-1 (Required) All class templates, function templates, class template member functions and class template static members shall be instantiated at least once.
所有类模板、函数模板、类模板成员函数和类模板静态成员至少实例化一次。
**拒绝原因:**
旧规则太严格了,新规则允许不实例化类的所有公共方法。
**代码示例:**
**反例:**
template
class MyClass {
public:
void method() {}
};
void example() {
MyClass
// obj.method(); // 不实例化obj.method();,在autosar的新规则中是允许的
}
12、15-4-1 (Required) If a function is declared with an exceptionspecification, then all declarations of the same function (in other translation units) shall be declared with the same set of type-ids.
如果一个函数被声明了一个异常规范,则在同一函数的不同翻译单元中的所有声明都应具有相同的一组类型 id。
**拒绝原因:**
动态异常规范被禁止。应使用 noexcept 规范。
实践表明,动态异常规范即使对程序员老手也会很难掌握,由于它复杂的规则。
**代码示例:**
**反例:**
void f() throw(); // 违反规则
void g() throw(int) {
// 函数定义,表示只会抛出int型异常
}
**正例:**
void f() noexcept;
void g() noexcept {
// 函数定义,表示不会抛出任何异常
}
实际上,动态异常规范(dynamic exception specifications)在 C++11 中被弃用,并在 C++17 中完全移除,主要原因在于它们在实践中存在诸多问题。相比之下,noexcept 规范提供了一种更简洁、更可靠的方式来声明函数不会抛出异常。
动态异常规范的问题
弱强制性:动态异常规范并不是严格强制的。如果一个函数声明了它只会抛出某些类型的异常,但实际上抛出了其他类型的异常,编译器并不会立即报错,而是会在运行时调用 std::unexpected 函数。
性能开销:动态异常规范会引入额外的运行时开销,因为编译器需要检查和处理这些规范。这使得代码执行效率受到影响。
不直观的行为:动态异常规范的行为并不总是符合开发者的预期。例如,它们不是函数类型的一部分,因此不能用于重载解析或模板推导。
noexcept 规范的优势
简单明了:noexcept 关键字明确地告诉编译器和开发者,该函数不会抛出任何异常。这种声明更加直观,易于理解和维护。
优化机会:当编译器知道某个函数不会抛出异常时,它可以进行更多的优化。例如,编译器可以省略与异常处理相关的代码,从而提高程序的性能。
更好的错误处理:使用 noexcept 可以帮助确保在异常传播过程中不会发生意外终止。如果一个标记为 noexcept 的函数确实抛出了异常,程序将立即调用 std::terminate 终止执行,避免了未定义行为。
使用建议
尽量使用 noexcept:对于那些确实不会抛出异常的函数,尤其是移动构造函数和移动赋值运算符,应该使用 noexcept 标记。这不仅提高了代码的可读性和安全性,还能带来潜在的性能提升。
避免使用动态异常规范:由于动态异常规范已被弃用且存在诸多问题,应尽量避免在新代码中使用它们。对于旧代码,建议逐步迁移到 noexcept 或其他更现代的异常处理机制。通过采用 noexcept 规范,开发者可以获得更清晰、更高效的代码,同时避免动态异常规范带来的复杂性和潜在问题。
13、16-0-3 (Required) #undef shall not be used.
不得使用 #undef。
**拒绝原因:**
该规则已被全局规则 A16-0-1 替换。
A16-0-1 原文和解释如下:
Rule A16-0-1 (required, implementation, automated) The pre-processor shall only be used for unconditional and conditional file inclusion and include guards, and using the following directives: (1) #ifndef, (2) #ifdef, (3) #if, (4) #if defined, (5) #elif, (6) #else, (7) #define, (8) #endif, (9) #include.
Rationale C++ provides safer, more readable and easier to maintain ways of achieving what is often done using the pre-processor. The pre-processor does not obey the linkage, lookup and function call semantics.
规则A16-0-1(必需,实现,自动化)预处理器应仅用于无条件和条件文件包含以及包含保护,并使用以下指令:(1)#ifndef,(2)#ifdef,(3)#if,(4)#if defined,(5)#elif,(6)#else,(7)#define,(8)#endif,(9)#include。 注意,这里允许使用(1)-(9)不包含#undef。
理由是C++提供了更安全、更易读和更易于维护的方法来实现通常使用预处理器所做的事情。预处理器不遵循链接、查找和函数调用语义。
**代码示例:**
**反例:**
#define MACRO 1
#undef MACRO // 违反规则
**正例:**
#define MACRO 1
// 不使用 #undef
就是说新旧规则都不允许用#undef,工业编程一般不允许#undef主要是安全性和可读性考虑:
理由 | 描述 |
避免混淆和错误 | 宏定义取消后可能导致标识符重复定义,增加错误风险 |
提高代码可读性和维护性 | 减少复杂度,使代码更易理解和维护 |
遵循工业标准 | 符合《MISRA-C-2004》等标准的要求 |
防止宏定义冲突 | 通过设计避免宏定义冲突,而不是依赖 #undef |
14、16-0-4 (Required) Function-like macros shall not be defined.
不得定义函数样式的宏。
**拒绝原因:**
该规则已被全局规则 A16-0-1 替换。
**代码示例:**
**反例:**
#define add(x, y) ((x) + (y)) // 违反规则
**正例:**
int add(int x, int y) {
return x + y; // 符合规则
}
全局规则 A16-0-1 已经确定预定义语句,比如宏,只能用于有条件或者无条件的文件包含或者包含检查,而不能用于计算。简单说新规则就是不要用宏来干函数的事情。
15、16-2-6 (Required) The #include directive shall be followed by either a or “filename” sequence.
#include 指令后面应跟一个或 "filename" 序列。
**拒绝原因:**
这是 C++ 语言标准所允许的唯一形式;不需要新的规则。
**代码示例:**
#include
16、16-6-1 (Required) All uses of the #pragma directive shall be documented.
#pragma 指令的所有使用都应记录。
**拒绝原因:**
新规则决定#pragma 指令不应使用,参见 A16-7-1。
pragma 指令是一种预处理指令用于控制编译器,为程序员提供了一种机制,可以在保持与C和C++语言完全兼容的情况下,向编译器传递特定于主机或操作系统的特征信息。这些指令通常用于控制编译器的行为,例如优化、警告处理、代码布局等,而不改变程序的逻辑 。
禁止 #pragma 指令的原因包括:
1. 编译器依赖性:#pragma 指令是编译器特定的,不同编译器对同一 #pragma 指令的解释可能不同,甚至有些编译器根本不支持某些 #pragma 指令。这会导致代码的可移植性问题。
2. 维护复杂性:#pragma 指令的使用可能会增加代码的复杂性和维护难度。对于大型项目,过多的 #pragma 指令可能会使代码难以理解和调试,尤其是在团队开发环境中 。
3. 潜在的副作用:某些 #pragma 指令可能会导致意外的行为或副作用。例如,禁用编译器警告可能会隐藏潜在的问题,影响代码质量。因此,在严格的质量控制环境下,可能会限制或禁止使用 #pragma 指令。
4. 标准化问题:由于 #pragma 指令不是C或C++标准的一部分,过度依赖它们可能会导致代码不符合标准规范,从而在未来的版本中出现问题 。
17、17-0-4 (Required) All library code shall conform to MISRA C++.
所有库代码应符合 MISRA C++。
**拒绝原因:**
该规则已被 A17-0-2 取代,要求所有代码符合 AUTOSAR C++14 编码准则。
这一条是自然必有的。
18、18-4-1 (Required) Dynamic heap memory allocation shall not be used.
不得使用动态堆内存分配。
**拒绝原因:**
新规则中动态堆内存分配的使用是条件允许的,参见 A18-5-1, A18-5-2, A18-5-3。
**代码示例:**
**反例:**
int* p = new int; // 违反旧规则
delete p;
**正例:**
// 条件允许的动态堆内存分配
void safe_alloc() {
int* p = nullptr;
try {
p = new int(10);
} catch (const std::bad_alloc&) {
// 处理异常
}
delete p;
}
这条在旧规则中是为了防止segment error,段错误。这种错误极难排查,是代码运行时分配的堆内存覆盖了其他已经分配的内存。但是C++ 14的智能指针已经防止了这种错误。
如前所述,MISRA 2008和autosar c++14 90%以上的规则都是共用的,或者只有微小差异。如果要像前面分析它们的差异一样分析它们的共同点,那么等价于几乎是分析所有MISRA 2008和autosar c++14规则,这是单篇文章做不到的。
限于篇幅,本文选取一些有代表性的共同点分析如下:
0-1-1 (Required) A project shall notcontain unreachable code.
0-1-2 (Required) A project shall not contain infeasible paths.
0-1-3 (Required) A project shall not contain unused variables.
0-1-4 (Required) A project shall not contain non-volatile POD variables having only one use.
0-1-5 (Required) A project shall not contain unused type declarations.
0-1-8 (Required) All functions with void return type shall have external side effect(s).
0-1-9 (Required) There shall be no dead code.
0-1-11 (Required) There shall be no unused parameters (named or unnamed) in non-virtual functions. 1 - Identical M0-1-11 -
0-1-12 (Required) There shall be no unused parameters (named or unnamed) in the set of parameters for a virtual function and all the functions that override it. 1 - Identical M0-1-12 -
0-2-1 (Required) An object shall not be assigned to an overlapping object.
1 - Identical M0-2-1 -
0-3-1 (Document) Minimization of run-time failures shall be ensured by the
use of at least one of: (a) static analysis tools/techniques; (b) dynamic analysis tools/techniques; (c) explicit coding of checks to handle run-time faults.
0-3-2 (Required) If a function generates error information, then that error information shall be tested.
0-4-1 (Document) Use of scaled-integer or fixed-point arithmetic shall be documented.
0-4-2 (Document) Use of floating-point arithmetic shall be documented.
1-0-2 (Document) Multiple compilers shall only be used if they have a
common, defined interface.
1-0-3 (Document) The implementation of integer division in the chosen
compiler shall be determined and documented.
3-2-2 (Required) The One Definition Rule shall not be violated.
3-2-3 (Required) A type, object or function that is used in multiple translation units shall be declared in one and only one file.
3-2-4 (Required) An identifier with external linkage shall have exactly one
definition.
3-3-1 (Required) Objects or functions with external linkage shall be declared
in a header file.
3-3-2 (Required) If a function has internal linkage then all re-declarations shall include the static storage class specifier.
3-4-1 (Required) An identifier declared to be an object or type shall be defined in a block that minimizes its visibility.
3-9-1 (Required) The types used for an object, a function return type, or a
function parameter shall be token-for-token identical in all declarations and
re-declarations.
9-5-1 (Required) Unions shall not be used.
0-1-1 (必选) 项目中不得包含不可达代码。
解释:
不可达代码是指在程序执行过程中永远不会被执行到的代码。这些代码会增加程序的复杂性,也可能会导致混淆和错误。示例:
void foo() {
int x = 10;
if (false) { // 始终为假
x = 20;
}
std::cout << x << std::endl; // 输出10
}
在这个例子中,x = 20; 是不可达代码,因为它永远不会被执行。
0-1-2 (必选) 项目中不得包含不可行路径。
解释:
不可行路径是指在正常运行条件下不可能被执行的代码路径。这通常是因为某些条件始终不满足导致的。示例:
void bar(int y) {
if (y > 100 && y < 0) { // 不可能同时大于100且小于0
...
} else {
...
}
}
0-1-3 (必选) 项目中不得包含未使用的变量。
解释:
未使用的变量是指声明了但从未在程序中使用的变量。这会增加代码的复杂性并可能导致误解。示例:
void baz() {
int unusedVariable = 10; // 未使用
}
0-1-4 (必选) 项目中不得包含仅使用一次的非易变POD变量。
解释:
POD(Plain Old Data)类型是指简单的数据结构,如整数、浮点数等。如果一个非易变的POD变量只使用一次,那么它可能是冗余的。示例:
void qux() {
const int temp = 10; // 仅使用一次
...
}
0-1-5 (必选) 项目中不得包含未使用的类型声明。
解释:
未使用的类型声明typedef是指声明了但从未在程序中使用的类型。这会增加代码的复杂性并可能导致误解。
0-1-8 (必选) 所有返回类型为void的函数必须具有外部副作用。
解释:
如果一个函数的返回类型是void,那么它应该至少有一个外部副作用,例如修改全局状态或调用其他函数。
示例:
void modifyGlobalState() {
globalVar = 10; // 修改全局变量
}
0-1-9 (必选) 项目中不得包含死代码。
解释:
死代码是指在程序执行过程中永远不会被执行的代码。这会增加代码的复杂性并可能导致误解。示例:
void deadCodeExample() {
int x = 10;
if (false) {
x = 20;
}
...
}
0-1-11 (必选) 在非虚函数中不得包含未使用的参数(命名或未命名)。
解释:
未使用的参数是指在函数中声明了但从未使用的参数。这会增加代码的复杂性并可能导致误解。示例:
void unusedParameterExample(int unusedParam) { // 未使用参数
...
}
0-1-12 (必选) 在虚函数及其所有重载函数中不得包含未使用的参数(命名或未命名)。
解释:
未使用的参数是指在虚函数及其所有重载函数中声明了但从未使用的参数。这会增加代码的复杂性并可能导致误解。示例:
class Base {
public:
virtual void virtualFunction(int unusedParam) { // 未使用参数
...
}
};
class Derived : public Base {
public:
void virtualFunction(int unusedParam) override { // 未使用参数
...
}
};
0-2-1 (必选) 对象不能赋值给重叠的对象。
解释:
如果两个对象在内存中重叠,直接赋值可能会导致意外的行为。示例:
struct OverlappingStruct {
int a;
int b;
};
void overlapAssignment(OverlappingStruct& s1, OverlappingStruct& s2) {
s1 = s2; // 直接赋值可能导致重叠问题
}
0-3-1 (文档) 应使用至少一种方法来确保运行时故障最小化:
(a) 静态分析工具/技术;
(b) 动态分析工具/技术;
(c) 显式编码检查以处理运行时故障。
0-3-2 (必选) 如果函数生成错误信息,则该错误信息必须进行测试。
解释:
如果函数生成错误信息,那么这些错误信息必须经过测试以确保其正确性。示例:
void generateErrorInfo() {
int errorCode = getErrorCode(); // 假设getErrorCode()返回错误码
if (errorCode != 0) {
throw std::runtime_error("Error occurred.");
}
}
每一种抛出的错误信息都必须有对应的测试用例覆盖住。
0-4-1 (文档) 使用缩放整数或定点算术应进行文档记录。
解释:
如果使用缩放整数或定点算术,应进行文档记录以便其他开发者理解。示例:
// 文档记录缩放整数或定点算术
int scaleInteger(int value, int scale) {
return value * scale;
}
0-4-2 (文档) 使用浮点算术应进行文档记录。
解释:
如果使用浮点算术,应进行文档记录以便其他开发者理解。示例:
// 文档记录浮点算术
double performFloatingPointOperation(double a, double b) {
return a + b;
}
1-0-2 (文档) 只有在它们具有共同定义的接口的情况下才应使用多种编译器。
解释:
只有在多种编译器之间存在共同定义的接口时,才能使用多种编译器。并且要在文档中记录使用了哪些编译器。
1-0-3 (文档) 应确定并记录所选编译器中的整数除法实现。
解释:
应确定并记录所选编译器中的整数除法实现。不同编译器的整数除法实现是不同的,有不同的行为,必须在文档中记录所用编译器特定的整数除法行为。
3-2-2 (必选) 不得违反单定义规则。
解释:
单定义规则意味着在一个项目中,对于一个给定的标识符,只能有一个定义。示例:
// 违反单定义规则
int x = 10;
int x = 20; // 重复定义
3-2-3 (必选) 在多个翻译单元中使用的类型、对象或函数应在其中一个文件中声明。
解释:
如果一个类型、对象或函数在多个翻译单元中使用,应在其中一个且仅一个文件中声明。
在多个翻译单元中使用的类型、对象或函数应在其中一个文件(不要更多,不要重复)中声明,以确保编译器能够正确识别和处理这些实体。具体来说,翻译单元是指一个源文件(如 .cpp 文件)及其直接或间接包含的所有头文件(如 .h 或 .hpp 文件)。每个翻译单元可以独立编译成目标文件(如 .obj 或 .o 文件),最终通过链接器组合成可执行文件或库。
为了确保不同翻译单元之间的一致性和避免重复定义问题,应该遵循以下几点:
a、单一声明原则:
类型、对象或函数的声明应当只在一个地方进行,通常是头文件中。这可以通过在头文件中使用 #include 指令来实现,使得所有需要该声明的源文件都能访问到它。
b、内联函数和静态函数的特殊处理:
内联函数可以在多个翻译单元中定义,但必须确保每个定义都是相同的。这是因为内联函数的定义会在每个使用它的翻译单元中展开。
静态函数的作用域仅限于当前翻译单元,因此可以在多个翻译单元中定义相同名称的静态函数而不发生冲突。
c、外部链接和内部链接:
具有外部链接的对象或函数可以在多个翻译单元中引用,但只能在一个翻译单元中定义。
具有内部链接的对象或函数(如静态变量或函数)仅限于当前翻译单元,不会与其它翻译单元中的同名实体冲突。
d、头文件保护:
为了避免头文件被多次包含,通常会使用预处理器指令(如 #ifndef、#define 和 #endif)来防止重复定义。
示例:
假设有一个类 MyClass 和一个全局函数 myFunction,它们将在多个翻译单元中使用。可以按照以下方式组织代码:
头文件
myclass.h
#ifndef MYCLASS_H
#define MYCLASS_H
class MyClass {
public:
void doSomething();
};
void myFunction();
#endif // MYCLASS_H
源文件 myclass.cpp
#include "myclass.h"
void MyClass::doSomething() {
// 实现细节
}
void myFunction() {
// 实现细节
}
使用 MyClass 和 myFunction 的其他源文件
#include "myclass.h"
int main() {
MyClass obj;
obj.doSomething();
myFunction();
return 0;
}
通过这种方式,MyClass 和 myFunction 的声明只在头文件 myclass.h 中出现一次,而定义则在 myclass.cpp 中提供。这确保了不同翻译单元之间的声明一致性,并避免了重复定义的问题。
3-2-4 (必选) 具有外部链接的标识符应仅仅有一个定义。
解释:
具有外部链接的标识符应仅仅有一个定义,否则会导致链接错误。示例:
外部链接就是跨模块调用,也就是公有变量或者函数,可以被其定义所在模块之外的模块调用。这样就容易导致定义重复。
3-3-1 (必选) 具有外部链接的对象或函数应在头文件中声明。
解释:
具有外部链接的对象或函数应在头文件中声明,以便其他文件可以引用它们。示例:
// 在header.h中
#ifndef HEADER_H
#define HEADER_H
extern int globalVar; //外部变量
#endif // HEADER_H
3-3-2 (必选) 如果函数具有内部链接,则所有重新声明应包括静态存储类说明符。
解释:
在C和C++编程语言中,如果一个函数具有内部链接(internal linkage),那么所有对该函数的重新声明都应当包含静态存储类说明符(static)。这是因为static关键字不仅影响变量的存储持续时间,还决定了函数或变量的链接属性。具体来说,使用static修饰的函数仅在定义它的源文件内可见,即它具有内部链接。
静态存储类说明符的作用
内部链接:当函数或全局变量被声明为static时,它们只在当前源文件中可见。这意味着其他源文件无法直接访问这些函数或变量。这种特性有助于减少命名冲突,并提高代码的模块化程度。
生命周期:对于局部变量,static关键字使得该变量在整个程序运行期间保持存在,而不是每次进入和离开作用域时创建和销毁。这可以用于保存状态信息,例如计数器或缓存结果。
内存分配:静态变量在程序启动时分配内存,并在程序结束时释放。因此,静态变量在整个程序执行期间始终存在。
如果函数具有内部链接,所有重新声明应包括静态存储类说明符static。示例:
// 在source.cpp中
static void internalFunction() {
// 函数体
}
3-4-1 (必选) 声明为对象或类型的标识符应在最小可见性的块中定义。
解释:
声明为对象或类型的标识符应在最小可见性的块中定义,以减少代码的耦合度。
3-9-1 (必选) 对象、函数返回类型或函数参数使用的类型在所有声明和重新声明中应逐个字符相同。
解释:
对象、函数返回类型或函数参数使用的类型在所有声明和重新声明中应逐个字符相同,以避免类型不匹配的问题。示例:
// 在header.h中
#ifndef HEADER_H
#define HEADER_H
typedef int MyInt;
#endif // HEADER_H
// 在source.cpp中
#include "header.h"
void function(MyInt param) {
// 函数体
}
9-5-1 (必选) 不得使用联合体union。
解释:
联合union是一种特殊的数据结构,同一时刻只能存储其中的一个成员。这可能导致难以追踪的数据问题,因此应避免使用。示例:
// 不推荐使用联合
union UnionExample {
int a;
float b;
};
笔者个人认为,最值得注意的规则是要求不要用递归,也就是规则7-5-4 :
7-5-4 (Advisory) Functions should not call themselves, either directly or indirectly. 2 - Small differences A7-5-1 Obligation level changed to “Required”. Example reworked.
MISRA里面这个规则是建议,到autosar C++14则是“Required”.
背后原因很好理解,递归程序在依靠栈展开的,递归层次较深时很容易栈溢出StackOverflow造成程序崩溃,在车载程序中禁止是可以理解的。
其次是autosar C++ 14不准用/**/这种C风格的注释,而MISRA是可以用的。
2-7-1 (Required)
The character sequence /* shall not be used within a C-style comment.
3 - Significant differences A2-8-4 Using the C-style comments is not allowed.
再就是自加自减运算符建议不要和其它运算符混用,这个目的是为了可读性。
5-2-10 (Advisory) The increment (++) and decrement (–) operators shall not
be mixed with other operators in an expression.
5-18-1 (Required) The comma operator shall not be used.
要求不允许使用逗号运算符
for 循环中的多变量初始化就是一个典型:逗号运算符常用于 for 循环中,以便在同一行代码中初始化多个变量。例如:
for (int i = 0, j = 10; i < j; ++i, --j) {
// 循环体
}
整个逗号表达式的值是最后一个表达式的值。例如,在表达式 a = 1, b = 2 中,表达式的值为 b = 2 的结果,即 2。
如果用在条件判断中,会引起难于阅读的问题。比如:
if ((a = getValue()), a > 0) {
// 条件成立时的代码
}
意思是先获取a的值,再返回a > 0的真伪值,但这样写违反good Style。
再一个对C语言程序员不太适应的规则是不让用
27-0-1 (Required) The stream input/output library
对C程序员耳熟能详的
1. 使用 printf 和 scanf 进行基本输入输出
#include
int main() {
int num;
printf("请输入一个整数: "); // 输出提示信息
scanf("%d", &num); // 读取用户输入的整数
printf("你输入的整数是: %d\\n", num); // 输出用户输入的整数
return 0;
}
2. 使用 fopen、fprintf、fscanf 和 fclose 处理文件
#include
int main() {
FILE *file = fopen("example.txt", "w"); // 打开文件进行写入
if (file == NULL) {
printf("无法打开文件\\n");
return 1;
}
fprintf(file, "Hello, World!\\n"); // 向文件中写入字符串
fclose(file); // 关闭文件
file = fopen("example.txt", "r"); // 打开文件进行读取
if (file == NULL) {
printf("无法打开文件\\n");
return 1;
}
char buffer[100];
fscanf(file, "%s", buffer); // 从文件中读取字符串
printf("文件内容: %s\\n", buffer);
fclose(file); // 关闭文件
return 0;
}
3. 使用 fgets 安全地读取字符串
fgets 函数可以安全地读取一行文本,并且可以指定最大读取长度,避免缓冲区溢出问题。
#include
int main() {
char line[100];
printf("请输入一行文本: ");
fgets(line, sizeof(line), stdin); // 从标准输入读取一行文本
printf("你输入的文本是: %s", line); // 输出用户输入的文本
return 0;
}
4. 使用 perror 处理错误
perror 函数用于输出系统错误信息,通常在文件操作失败时使用。
#include
int main() {
FILE *file = fopen("nonexistent.txt", "r");
if (file == NULL) {
perror("fopen"); // 输出错误信息
return 1;
}
fclose(file);
return 0;
}
而不推荐使用
全局命名空间污染:
使用<cstdio>会将所有符号引入全局命名空间,这可能会导致命名冲突和其他问题。相比之下,
性能问题:
C++的
线程安全:
标准化和未来兼容性:
C++标准更倾向于使用
使用
autosar c++14标准,及其前身MISRA 2008,规则定义十分详细和丰富,几乎涵盖了C和C++语言全部的语言特性,而且很多规则都是从一线实战编程中反复总结得出。
由于C和C++语言的语言特性本身是高度贴合计算机底层原理的,C语言素有“最接近机器语言的高级语言”之称。这更加增添了对这些工业级编程标准的理解难度。
因此其学习和理解不仅具有一定难度,部分规则甚至在新手程序员看来是“反直觉的” ,“反习惯的”。
但是AUTOSAR C++14工业编程规范的必要性在于它为汽车行业的软件开发提供了全面且严格的标准,确保了代码的安全性、可靠性和可维护性,同时适应了现代C++语言的发展。此外,它还促进了跨厂商的标准化开发,支持复杂功能的实现,特别是autosar Adaptive平台上OTA,流媒体处理、智能座舱、自动驾驶等等高级复杂功能的实现。
学习和遵循这些标准中会领会到大量编程实践的实战经验和c++新语言特性,成为现代汽车电子开发的标配标准。