带你迅速了解MISRA-C

MISRA-C

作为一个从事汽车软件开发人员,我自己对MISRA-C进行了一些整理,如果内容有问题,希望大佬们可以指出。

一.什么是MISRA-C

MISRA-C是一组针对C语言编程的编码规范,旨在提高软件质量、可靠性和可维护性。MISRA代表Motor Industry Software Reliability Association(汽车工业软件可靠性协会),该协会最初为汽车行业开发了这些规范。虽然最初是为汽车行业设计的,但MISRA-C规范已经成为其他行业的编码标准,如航空航天、医疗设备和铁路系统。

MISRA-C规范包含了一系列关于C语言编码的规则和指导,这些规则旨在减少代码中的错误、提高代码的可读性,并确保代码在不同的编译器和平台上的可移植性。这些规则通常涵盖了诸如类型转换、指针使用、内存分配、代码风格等方面。在一些高度安全和可靠性要求的行业,如汽车电子控制单元(ECU)、航空航天和医疗设备领域,遵循MISRA-C规范是一种常见的实践,有助于降低软件缺陷的风险。

MISRA-C规范的最新版本通常会随着时间的推移而更新,以反映新的技术、实践和行业标准的变化。

二.标准规范示例

1.语言扩展

1.1 汇编语言应该被封装并隔离。

​ 使用具体的宏定义对汇编语言进行封装。

#define NOP asm (“ NOP”);

1.2 源代码应该使用 /*…*/ 类型的注释

​ 所有的C语言代码都要使用/*…*/类型的注释。

/* int a =10; */

1.3 字符序列 /* 不应出现在注释中。

​ 即不可以进行循环嵌套。

2.类型

2.1 单纯的 char 类型应该只用做存储和使用字符值。

char char1 = 'b';
char1 == 'b';
char1 != 'b';

2.2 signed char unsigned char 类型应该只用做存储和使用数字值。

2.3 应该使用指示了大小和符号的 typedef 以代替基本数据类型。

typedef char char_t; 
typedef signed char int8_t; 
typedef signed short int16_t; 
typedef signed int int32_t; 
typedef signed long int64_t; 
typedef unsigned char uint8_t; 
typedef unsigned short uint16_t; 
typedef unsigned int uint32_t; 
typedef unsigned long uint64_t; 
typedef float float32_t; 
typedef double float64_t; 
typedef long double float128_t; 
在位域类型的说明中,typedef 是不必要的。

2.4 位域只能被定义为 unsigned int singed int 类型,unsigned int类型的位域至少应该为 2 bits 长度。

struct idt 
{
	uint8_t a : width(>= 2bits)
}

3.常量

3.1 不可以使用八进制与八进制的escape 序列。

4.声明与定义

4.1 函数应当具有原型声明,且原型在函数的定义和调用范围内都是可见的。
4.2 不论何时声明或定义了一个对象或函数,它的类型都应显式声明
extern int16_t x;
4.3 函数的每个参数类型在声明和定义中必须是等同的,函数的返回类型也该是等同的。
4.4 如果对象或函数被声明了多次,那么它们的类型应该是兼容的。
4.5 头文件中不应有对象或函数的定义。
4.6 头文件中不应有对象或函数的定义。
4.7 外部对象或函数应该声明在唯一的文件中
/*在feature.h中进行声明*/
extern int16_t a;

/*在其他文件*/
#include "feature.h"
int16_t a = 1;
4.8 具有外部链接的标识符应该具有准确的外部定义。
  • 一个对象在外部只能有一个准确的定义
4.9 在文件范围内声明和定义的所有对象或函数应该具有内部链接,除非是在需要外部链接的情况下。
  • 使用 static 存储类标识符将确保标识符只是在声明它的文件中是可见的,并且避免了和其他文件或库中的相同标识符发生混淆的可能

    性。

4.10 static 存储类标识符应该用于具有内部链接的对象和函数的定义和声明。
  • static 和 extern 存储类标识符常常是产生混淆的原因。良好的编程习惯是,把 static 关键字一致地应用在所有具有内部链接的对象和函数的声明上。
4.11 当一个数组声明为具有外部链接,它的大小应该显式声明或者通过初始化进行隐式定义
int array1[10] ; /* Compliant */ 
extern int array2[] ; /* Not compliant */ 
int array2[] = { 0, 10, 15 }; /* Compliant */

5. 初始化

5.1 所有自动变量在使用前都应被赋值
#include <stdio.h>

int main() {
    int x; // 未初始化的自动变量
    printf("The value of x is: %d\n", x); // 未定义行为,x的值是未知的
    return 0;
}
5.2 应该使用大括号以指示和匹配数组和结构的非零初始化构造
int16_t y[3][2] = { 1, 2, 3, 4, 5, 6 }; /* not compliant */ 
int16_t y[3][2] = { { 1, 2 }, { 3, 4 }, { 5, 6 } } ; /* compliant */
5.3 在枚举列表中,“=”不能显式用于除首元素之外的元素上,除非所有的元素都是显式初始化的。
enum colour { red = 3, blue, green, yellow = 5 } ; /* not compliant */ 
/* green and yellow represent the same value – this is duplication */ 
enum colour { red = 3, blue = 4, green = 5, yellow = 5 }; /* compliant */ 
/* green and yellow represent the same value – this is duplication */

6 数值类型转换

6.1 隐式和显式类型转换
  • 为了代码清晰的目的而插入的强制转换通常是有用的,但如果过多使用就会导致程序的可读性下降。正如下面所描述的,一些隐式转换是可以安全地忽略的,而另一些则不能。
6.2 隐式转换的类型
  • 仅仅应用在 small integer 类型上
  • 应用在一元、二元和三元操作数上
  • 不能用在逻辑操作符(&&、||、!)的操作数上
  • 用在 switch 语句的控制表达式上

6.3 整型复杂表达式的值只能强制转换到更窄的类型且与表达式的基本类型具有相同的符号

#include <stdio.h>

int main() {
    int a = -100;
    unsigned int b = 200;
    
    // 整型表达式的值是有符号的,强制转换到更窄的有符号类型
    char c = (char)a;
    
    // 整型表达式的值是无符号的,强制转换到更窄的有符号类型
    char d = (char)b;
    
    printf("c: %d\n", c); // 输出: -100
    printf("d: %d\n", d); // 输出: -56 (在有些平台上可能输出不同的值)
    
    return 0;
}

6.4 浮点类型复杂表达式的值只能强制转换到更窄的浮点类型。
#include <stdio.h>

int main() {
    float a = 123.456789; // 单精度浮点数
    double b = 9876.54321; // 双精度浮点数
    
    // 浮点类型表达式的值被强制转换到更窄的浮点类型
    float c = (float)b;
    
    printf("c: %f\n", c); // 输出: 9876.543945 (可能会有舍入误差)
    
    return 0;
}

6.5 如果位运算符 ~ << 应用在基本类型为 unsigned char unsigned short 的操作数,结果应该立即强制转换为操作数的基本类型。
 uint8_t port = 0x5aU; 
 uint8_t result_8; 
 uint16_t result_16; 
 uint16_t mode; 
 result_8 = (~port) >> 4; /* not compliant */

/* 的值在 16 位机器上是 0xffa5,而在 32 位机器上是 0xffffffa5。在每种情况下,result
的值是 0xfa,然而期望值可能是 0x0a。这样的危险可以通过如下所示的强制转换来避免:*/

result_8 = ( ( uint8_t ) (~port ) ) >> 4; /* compliant */ 
result_16 = ( ( uint16_t ) (~(uint16_t) port ) ) >> 4 ; /* compliant */


6.6 后缀“U”应该用在所有 unsigned 类型的常量上。
  • 任何带有“U”后缀的值是 unsigned 类型
  • 一个不带后缀的小于 231的十进制值是 signed 类型
  • 不带后缀的大于或等于 215 的十六进制数可能是 signed 或 unsigned 类型
  • 不带后缀的大于或等于 231 的十进制数可能是 signed 或 unsigned 类型

7 指针类型转换

  • 对象指针
  • 函数指针
  • void 指针
  • 空(null)指针常量(即由数值 0 强制转换为 void*类型)
  • 涉及指针类型的转换需要明确的强制,除非在以下时刻:
  • 转换发生在对象指针和 void 指针之间,而且目标类型承载了源类型的所有类型标识
  • 当空指针常量(void*)被赋值给任何类型的指针或与其做等值比较时,空指针常量
  • 被自动转化为特定的指针类型
7.1 转换不能发生在函数指针和其他除了整型之外的任何类型指针之间。
#include <stdio.h>

void func1() {
    printf("This is func1\n");
}

void func2() {
    printf("This is func2\n");
}

int main() {
    void (*ptr1)() = func1; // 定义一个指向 func1 函数的函数指针
    int *ptr2 = (int *)ptr1; // 将函数指针 ptr1 转换为指向整型的指针 ptr2
    
    // 尝试通过指针 ptr2 调用函数
    // 这是未定义的行为,因为函数指针和整型指针之间的转换会导致未定义的行为
    // 在某些情况下可能会导致程序崩溃或产生意外结果
    (*ptr2)(); // 尝试调用函数,这可能会导致未定义的行为

    return 0;
}

7.2 对象指针和其他除整型之外的任何类型指针之间、对象指针和其他类型对象的指针之间、对象指针和void 指针之间不能进行转换。
#include <stdio.h>

int main() {
    int num = 10;
    int *ptr_int = &num;
    char *ptr_char = (char *)ptr_int; // 试图将指向整型的指针转换为指向字符的指针

    printf("Value at ptr_char: %c\n", *ptr_char); // 这可能会导致未定义的行为,因为ptr_char指向的内存区域可能包含的是整型数据的高字节

    return 0;
}

7.3 如果指针所指向的类型带有 const volatile 限定符,那么移除限定符的强制转换是不允许的
uint16_t x; 
 uint16_t * const cpi = &x; /* const pointer */ 
 uint16_t * const * pcpi ; /* pointer to const pointer */ 
 const uint16_t * * ppci ; /* pointer to pointer to const */ 
 uint16_t * * ppi; 
 const uint16_t * pci; /* pointer to const */ 
 volatile uint16_t * pvi; /* pointer to volatile */ 
 uint16_t * pi; 
 … 
 pi = cpi; /* Compliant – no conversion 
 no cast required */ 
 pi = (uint16_t *)pci; /* Not compliant */ 
 pi = (uint16_t *)pvi ; /* Not compliant */ 
 ppi = (uint16_t *)pcpi ; /* Not compliant */ 
 ppi = (uint16_t *)ppci ; /* Not compliant */

8 表达式

8.1 表达式的值在标准所允许的任何运算次序下都应该是相同的。
8.2 不能在具有副作用的表达式中使用 sizeof 运算符。
  • C 当中存在的一个可能的编程错误是为一个表达式使用了 sizeof 运算符并期望计算表达式。然而表达式是不会被计算的:sizeof 只对表达式的类型有用。
 int32_t i; 
 int32_t j; 
 j = sizeof (i = 1234); 
 /* j is set to the sizeof the type of i which is an int */ 
 /* i is not set to 1234 */
8.3 逻辑运算符 && | | 的右手操作数不能包含副作用。
  • 副作用就是指一些有关的操作符,会引起相应状态的改变

     if ( ishigh && ( x == i++ ) ) /* Not compliant */ 
     if ( ishigh && ( x == f (x) ) ) /* Only acceptable if f(x) is known to have no side effects */
    
8.4 逻辑 && | | 的操作数应该是 primary-expressions
 if ( ( x == 0 ) && ishigh ) /* make x == 0 primary */ 
 if ( x || y || z ) /* exception allowed, if x, y and z are Boolean */ 
 if ( x || ( y && z ) ) /* make y && z primary */ 
 if ( x && ( !y ) ) /* make !y primary */ 
 if ( ( is_odd (y) ) && x ) /* make call primary */ 
 /*如果表达式只由逻辑 && 序列组成或逻辑 | | 序列组成,就不需要使用括号。*/
 if ( ( x > c1 ) && ( y > c2 ) && ( z > c3 ) ) /* compliant */ 
 if ( ( x > c1 ) && ( y > c2 ) || (z > c3 ) ) /* not compliant */ 
 if ( ( x > c1 ) && ( ( y > c2 ) || ( z > c3 ) ) ) /* compliant extra ( ) used */
8.5 逻辑运算符(&&、| | !)的操作数应该是有效的布尔数。有效布尔类型的表达式不能用做非逻辑运算符(&&| | !)的操作数
  • 逻辑运算符 &&、| | 和 ! 很容易同位运算符 &、| 和 ~ 混淆。见术语表中的“Boolean expressions”。
8.6 位运算符不能用于基本类型(underlying type)是有符号的操作数上。
  • 位运算(~、<<、>>、&、^ 和 | )对有符号整数通常是无意义的。比如,如果右移运算把符号位移动到数据位上或者左移运算把数据位移动到符号位上,就会产生问题。
8.7 移位运算符的右手操作数应该位于零和某数之间,这个数要小于左手操作数的基本类型的位宽。
 u8a = (uint8_t) (u8a << 7); /* compliant */ 
 u8a = (uint8_t) (u8a << 9); /* not compliant */ 
 u16a = (uint16_t) ( (uint16_t) u8a << 9 ); /* compliant */
8.8 一元减运算符不能用在基本类型无符号的表达式上。
#include <stdio.h>

int main() {
    unsigned int num = 10;
    unsigned int result = -num;

    printf("Result: %u\n", result); // 输出:4294967286

    return 0;
}

8.9 不要使用逗号运算符。
#include <stdio.h>

int main() {
    int a = 10, b = 20, c;
    
    // 使用逗号运算符将多个表达式放在一起
    c = (a++, b++, a + b);
    
    printf("c: %d\n", c); // 输出:31,a和b分别增加了1,然后计算了a+b的值

    return 0;
}

8.10 **无符号整型常量表达式的计算不应产生折叠(**wrap-around)。
8.11 **不应使用浮点数的基本(**underlying)的位表示法(bit representation)。
#include <stdio.h>
#include <math.h>

int main() {
    double num1 = 3.14;
    double num2 = 2.71;
    double result;

    // 直接依赖存储方法的浮点操作
    result = num1 + num2;

    printf("Result using direct floating point addition: %f\n", result); // 输出:5.850000

    // 使用内置函数进行浮点操作
    result = fmax(num1, num2); // 使用 fmax 函数获取两个数中的最大值

    printf("Result using fmax function: %f\n", result); // 输出:3.140000

    return 0;
}

8.12 在一个表达式中,自增(++)和自减(- -)运算符不应同其他运算符混合在一起。
u8a = ++u8b + u8c--; /* Not compliant */ 
/*下面的序列更为清晰和安全:*/
 ++u8b; 
 u8a = u8b + u8c; 
 u8c --;

9.控制语句表达式

9.1 赋值运算符不能使用在产生布尔值的表达式上。
x = y; 
 if (x != 0) 
 { 
 foo (); 
 } 
/*不能写成:*/
 if ( ( x = y ) != 0 ) /* Boolean by context */ 
 { 
 foo (); 
 } 
/*或者更坏的:*/
 if (x = y) 
 { 
 foo (); 
 }
9.2 数的非零检测应该明确给出,除非操作数是有效的布尔类型。
if ( x != 0) /* Correct way of testing x is non-zero */ 
if ( y ) /* Not compliant, unless y is effectively Boolean data (e.g. a flag). */
9.3 浮点表达式不能做相等或不等的检测。
float32_t x, y; 
 /* some calculations in here */ 
 if ( x == y) /* not compliant */ 
 { /* … */ } 
 if (x == 0.0f) /* not compliant */ 
/*间接的检测同样是有问题的,在本规则内也是禁止的。例如:*/
 if ( ( x <= y ) && ( x >= y ) ) 
 { /* … */ }
9.4 for 语句的控制表达式不能包含任何浮点类型的对象。
9.5 for 语句的三个表达式应该只关注循环控制。
  • 第一个表达式

​ 初始化循环计数器 (例子中的 i)

  • 第二个表达式

​ 应该包含对循环计数器(i)和其他可选的循环控制变量的测试

  • 第三个表达式

​ 循环计数器(i)的递增或递减

9.6 for 循环中用于迭代计数的数值变量不应在循环体中修改
 flag = 1; 
 for ( i = 0; ( i < 5 ) && (flag == 1 ); i++) 
 { 
 /* … */ 
 flage = 0; /* Compliant – allows early termination of loop */ 
 i = i + 3; /* Not compliant – altering the loop counter */ 
 }
9.7 不允许进行结果不会改变的布尔运算。

10.控制流

10.1 **不能有不可到达(**unreachable)的代码。
 switch ( event ) 
 { 
 case E_wakeup: 
 do_wakeup (); 
 break; /* unconditional control transfer */ 
         
         
 do_more (); /* Not compliant – unreachable code */ 
         
         
 /* … */ 
 default: 
 /* … */ 
 break; 
 }
10.2 所有非空语句(non-null statement)应该:
a)不管怎样执行都至少有一个副作用(side-effect),或者
b) 可以引起控制流的转移
 /* assume uint16_t x; 
 and uint16_t i; */ 
 … 
 x >= 3u; /* not compliant: x is compared to 3, 
 and the answer is discarded */
10.3 不应使用 goto 语句。
10.4 不应使用 continue 语句。
10.5 对任何迭代语句至多只应有一条 break 语句用于循环的结束。
10.6 一个函数在其结尾应该有单一的退出点。
10.7 组成 switchwhile、**do…while **或for 结构体的语句应该是复合语句。
10.8 if(表达式)结构应该跟随有复合语句。else关键字应该跟随有复合语**句或者另外的if **语句
if ( test1 ) 
 { 
 x = 1 ; /* Even a single statement must be in braces */ 
 } 
 else if ( test2 ) 
 { 
 x = 0; /* No need for braces in else if */ 
 } 
 else 
 x = 3; /* This was (incorrectly) not enclosed in braces */ 
 y = 2; /* This line was added later but, despite the appearance 
 (from the indent) it is actually not part of the else, 
 and is executed unconditionally */
10.9 所有的 if … else if 结构应该由 else 子句结束。

11.switch 语句

11.1 switch 标签只能用在当最紧密封闭(closely-enclosing)的复合语句是switch语句体的时候
  • case 或 default 标签的范围应该是做为 switch 语句体的复合语句。所有 case 子句和 default子句应该具有相同的范围。
11.2 无条件的 break 语句应该终止每个非空的 switch 子句。
  • 每个 switch 子句中的最后一条语句应该是 break 语句,或者如果 switch 子句是复合语句,那么复合语句的最后一条语句应该是 break 语句。
11.3 switch 语句的最后子句应该是 default 子句
  • 对最后的 default 子句的要求是出于保护性编程。该子句应该执行适当的动作,或者包含合适的注释以说明为何没有执行动作。
11.4 switch 表达式不应是有效的布尔值
 switch ( x == 0) /* not compliant – effectively Boolean */ 
 { 
 …
 }

12.函数

12.1 函数定义不得带有可变数量的参数
#include <stdio.h>

// 错误的函数定义,带有可变数量的参数
void sum(int num1, ...){
    // 函数体
}

#include <stdio.h>
#include <stdarg.h>

// 函数接受可变数量的参数,并计算它们的和
double sum(int num_args, ...) {
    
    /* 初始化一个变量*/
    va_list args;
    double total = 0.0;

    // 初始化参数列表
    va_start(args, num_args);

    // 从参数列表中依次获取参数,并累加到 total 中
    for (int i = 0; i < num_args; ++i) {
        double arg = va_arg(args, double);
        total += arg;
    }

    // 结束参数列表的处理
    va_end(args);

    return total;
}

int main() {
    // 调用 sum 函数,传递不定数量的参数
    printf("Sum: %.2f\n", sum(3, 2.5, 3.7, 1.2)); // 输出:Sum: 7.40

    return 0;
}

12.2 函数不能调用自身,不管是直接还是间接的。
// 递归计算阶乘的函数
unsigned long long factorial(int n) {
    // 基本情况:当 n 等于 0 或 1 时,阶乘为 1
    if (n == 0 || n == 1) {
        return 1;
    } else {
        // 递归情况:计算 n 的阶乘为 n * (n-1)!
        return n * factorial(n - 1);
    }
}
12.3 在函数的原型声明中应该为所有参数给出标识符
#include <stdio.h>

// 声明一个函数原型,参数有标识符
int add(int x, int y);

int main() {
    int result = add(3, 5);
    printf("Result: %d\n", result); // 输出:Result: 8
    return 0;
}

// 定义函数,参数有标识符
int add(int x, int y) {
    return x + y;
}

12.4 函数的声明和定义中使用的标识符应该一致
12.5 不带参数的函数应当声明为具有 void 类型的参数
 void myfunc ( void );
12.6 传递给一个函数的参数应该与声明的参数匹配。
12.7 带有 non-void 返回类型的函数其所有退出路径都应具有显式的带表达式的return 语句。
  • 表达式给出了函数的返回值。如果 return 语句不带表达式,将导致未定义的行为(而且编译器不会给出错误)。
12.8 如果函数返回了错误信息,那么错误信息应该进行测试。

13.指针和数组

13.1 指针的数学运算只能用在指向数组或数组元素的指针上。
#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *ptr = arr; // 指向数组的第一个元素的指针

    // 使用指针递增和循环遍历数组,并访问每个元素
    printf("Array elements: ");
    for (int i = 0; i < 5; i++) {
        printf("%d ", *ptr); // 输出当前指针指向的数组元素的值
        ptr++; // 将指针递增,指向下一个数组元素
    }
    printf("\n");

    return 0;
}

13.2 指针减法只能用在指向同一数组中元素的指针上。
#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *ptr1 = &arr[2]; // 指向数组的第三个元素的指针
    int *ptr2 = &arr[0]; // 指向数组的第一个元素的指针

    // 使用指针减法计算两个指针之间的距离
    ptrdiff_t distance = ptr1 - ptr2;

    printf("Distance between pointers: %td\n", distance); // 输出:Distance between pointers: 2

    return 0;
}

13.3 >、>=、<、<= 不应用在指针类型上,除非指针指向同一数组。
#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *ptr1 = &arr[2]; /** 指向数组的第三个元素的指针 */
    int *ptr2 = &arr[4]; /** 指向数组的最后一个元素的指针 */

    /** 对指针进行比较,由于指针不指向同一对象,将导致未定义行为
        应避免这样的操作 */
    /** if (ptr1 == ptr2) {
            printf("Pointers are equal.\n");
        } else {
            printf("Pointers are not equal.\n");
        } */

    /** 允许指向超出数组尾部的元素,但访问是禁止的 */
    /** int *ptr3 = &arr[5]; // 指向数组尾部后面的一个元素
        printf("Value at arr[5]: %d\n", *ptr3); // 未定义行为,应避免访问超出数组范围的元素 */

    return 0;
}

13.4 数组的索引应当是指针数学运算的唯一可允许的方式
#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *ptr = arr; /** 指向数组的第一个元素的指针 */

    /** 使用数组索引进行访问,这是指针数学运算的唯一可接受方式,
        因为它比其他的指针操作更为清晰并且更少出现错误倾向 */
    printf("Value at arr[2]: %d\n", arr[2]); /** 输出:Value at arr[2]: 30 */

    /** 本规则禁止了指针数值的显式运算,因为任何显式计算的指针值
        潜在地会访问不希望访问的或无效的内存地址 */
    /** int *ptr2 = ptr + 2; // 不允许的显式指针数学运算 */

    /** 指针可以超出数组的范围,但访问超出范围的元素是未定义行为 */
    /** int *ptr3 = &arr[5]; // 指向数组尾部后面的一个元素 */

    return 0;
}

14. 结构与联合

14.1 所有结构与联合的类型应该在转换单元(translation unit)的结尾是完善的。
/* File: example.h */
#ifndef EXAMPLE_H
#define EXAMPLE_H

/* Incomplete structure declaration */
struct Point;

/* Function declaration */
void printPoint(struct Point p);

#endif /* EXAMPLE_H */

/* File: example.c */
#include "example.h"

/* Structure definition */
struct Point {
    int x;
    int y;
};

/* Function definition */
void printPoint(struct Point p) {
    printf("(%d, %d)\n", p.x, p.y);
}

14.2
#include <stdio.h>
#include <string.h>

int main() {
    char str[] = "Hello, world!";
    strcpy(str + 7, str);  // 将字符串复制到自身的一部分
    printf("%s\n", str);   // 输出结果可能是未定义的
    return 0;
}

在这个例子中,我们试图将字符串str的一部分复制到另一部分,但是目标和源之间存在重叠。具体来说,我们将字符串从位置7开始的部分复制到了字符串的起始位置。这种重叠导致了未定义行为,因为strcpy函数的行为不被规定在这种情况下。

为了避免这种情况,可以使用memmove函数,它被设计用来处理重叠的情况:

#include <stdio.h>
#include <string.h>

int main() {
    char str[] = "Hello, world!";
    memmove(str, str + 7, strlen(str) + 1);  // 使用memmove处理重叠
    printf("%s\n", str);   
    return 0;
}

14.3 不能为了不相关的目的重用一块内存区域。

在这个例子中,我们本意是将变量x的值赋给变量y,但是却错误地使用了指向x的指针,将x的地址赋给了y。这种重用内存的方式导致了不相关的行为,使得程序的逻辑变得混乱,从而可能引发错误。

为了避免这种问题,应该确保在程序中使用内存时,每块内存都只用于其相关的目的。在上面的例子中,应该直接将x的值赋给y,而不是通过指针来间接赋值。

#include <stdio.h>

int main() {
    int x = 5;
    int y = 10;

    // 将x的地址赋值给y,用于不相关的目的
    int *ptr = &x;
    y = *ptr;

    printf("x: %d, y: %d\n", x, y);

    return 0;
}

14.4 不要使用联合。
#include <stdio.h>

union MyUnion {
    int x;
    float y;
};

int main() {
    union MyUnion u;

    u.x = 10;
    printf("x: %d\n", u.x);

    u.y = 3.14;
    printf("x: %d\n", u.x);  // 这里的输出结果是未定义的,因为x和y共享同一内存位置

    return 0;
}

以上这个例子是有可能出现相关未定义的问题,但是为了解决一些特殊应用上的场景,是可以使用联合体的:

  • 数据的打包与解包
  • 变量录取

数据的打包与解包

  • uint32_t 类型占据 32 位
  • uint8_t 类型占据 8 位
  • 实现把数据字的最高有效字节存储在最低的内存地址
 typedef union { 
 uint32_t word; 
 uint8_t bytes[4]; 
 } word_msg_t; 
 uint32_t read_word_big_endian (void) 
 { 
 word_msg_t tmp; 
 tmp.bytes[0] = read_byte (); 
 tmp.bytes[1] = read_byte (); 
 tmp.bytes[2] = read_byte (); 
 tmp.bytes[3] = read_byte ();
 return (tmp.word); 
 }

也可以使用另一种方式进行存储

 uint32_t read_word_big_endian (void) 
 { 
 uint32_t word; 
 word = ( ( uint32_t ) read_byte () ) << 24; 
 word = word | ( ( ( uint32_t ) read_byte () ) << 16 ); 
 word = word | ( ( ( uint32_t ) read_byte () ) << 8 ); 
 word = word | ( ( uint32_t ) read_byte () ) ; 
 return (word); 
 } 

15. 预处理指令

15.1 文件中的#include 语句之前只能是其他预处理指令或注释。(建议)
// 这是一个注释
#include "header.h"

int some_variable = 42; // 代码可以放在 #include 语句之后

int main() {
    // 程序代码
    return 0;
}

在这个符合规则的例子中,#include语句之前只有一个注释,没有其他代码。因此,这个代码结构是符合MISRA-C标准的。

这样做的目的是确保文件的结构清晰且易于理解,提高代码的可读性,并降低预处理过程中出现错误的可能性。

15.2 #include 指令中的头文件名字里不能出现非标准字符。(建议)
#include <my'header.h>  // 使用了非标准字符 '(单引号),违反规则

15.3 #include *预处理指令应该跟随<*filename>或”filename”**序列。
 #include “ filename.h” 
 #include <filename.h> 
 #define FILE “filename.h” 
 #include FILE
15.4 C 的宏只能扩展为用大括号括起来的初始化、常量、小括号括起来的表达式、类型限定符、存储类标识符或 do-while-zero 结构。
  • 在宏定义中,括号必须要成对出现
#include <stdio.h>

#define INIT {0}  // 用大括号括起来的初始化
#define PI 3.14   // 常量
#define SQUARE(x) ((x) * (x))  // 小括号括起来的表达式
#define CONST const  // 类型限定符
#define STATIC static  // 存储类标识符
#define MACRO_DO_WHILE_ZERO \
    do { \
        printf("This is a macro with do-while-zero structure.\n"); \
    } while (0)

int main() {
    int arr[] = INIT;  // 使用宏 INIT
    printf("PI: %f\n", PI);  // 使用宏 PI
    printf("SQUARE of 3: %d\n", SQUARE(3));  // 使用宏 SQUARE

    MACRO_DO_WHILE_ZERO;  // 使用宏 MACRO_DO_WHILE_ZERO

    return 0;
}

15.5 宏不能在块中进行 #define #undef
  • MISRA-C标准中的这条规则要求不要在块(即函数或其他语句块)中使用#define#undef预处理指令。这是为了避免误解,即人们可能认为这些预处理指令仅在块作用域内生效,而实际上它们在整个文件范围内都生效。以下是一个错位的示例

    #include <stdio.h>
    
    int main() {
        // 在函数块中定义宏,这违反了MISRA-C的规则
        #define MAX_VALUE 100
        
        printf("Max value: %d\n", MAX_VALUE);
        
        return 0;
    }
    
    
  • 正确的示例:

    #include <stdio.h>
    
    int main() {
        // 在函数块中定义宏,这违反了MISRA-C的规则
        #define MAX_VALUE 100
        
        printf("Max value: %d\n", MAX_VALUE);
        
        return 0;
    }
    
    
15.6 **不要使用 **#undef。

在C语言中,#undef预处理指令用于取消之前的宏定义。尽管C语言允许在代码中的任何地方使用#undef指令,但MISRA-C标准建议避免使用#undef。这是因为#undef指令的使用可能导致宏的定义和作用范围混淆,从而降低代码的可读性和可维护性。

以下是为什么要避免使用#undef的几个原因:

  1. 混淆宏定义#undef指令会取消之前的宏定义,这可能导致代码中对宏的理解产生混淆。宏的定义和取消应该保持明确和一致。
  2. 代码可读性降低:使用#undef可能导致代码在不同的地方定义和取消宏,这使得代码难以追踪宏的作用范围和含义,降低了代码的可读性。
  3. 不必要的复杂性:在代码中频繁使用#undef可能增加代码的复杂性,使得代码维护变得困难。通常,通过合理设计和组织宏定义可以避免使用#undef

以下是一个违反规则的例子

#include <stdio.h>

#define MAX_VALUE 100

int main() {
    printf("Max value: %d\n", MAX_VALUE);
    
    #undef MAX_VALUE  // 取消宏定义,这可能导致混淆
    
    return 0;
}

15.7 **函数的使用优先选择函数宏(**function-like macro)。(建议)
  • 由于宏能提供比函数优越的速度,函数提供了一种更为安全和鲁棒的机制。在进行参数的类型检查时尤其如此。函数宏的问题在于它可能会多次计算参数。
15.8 函数宏的调用不能缺少参数

在C语言中,函数宏(function-like macro)是一种宏定义,其语法类似于函数调用,宏定义中可以有参数。在调用函数宏时,必须提供所有参数,否则可能导致预处理器错误或未定义的行为。

MISRA-C标准中的这条规则指出,调用函数宏时,不能缺少参数。也就是说,如果函数宏定义了某些参数,那么在调用该宏时,必须提供相应数量的实际参数。

例如,下面是一个函数宏的定义和正确的调用:

#define ADD(x, y) ((x) + (y))

int main() {
    int result = ADD(3, 5);  // 正确的调用,提供了两个参数
    return 0;
}

在这个例子中,ADD是一个函数宏,它接受两个参数xy。在调用ADD时,正确地提供了两个实际参数35,因此代码运行正常。

如果在调用函数宏时缺少参数,例如:

int main() {
    int result = ADD(3);  // 错误的调用,缺少参数
    return 0;
}

15.9 传递给函数宏的参数不能包含看似预处理指令的标记。

传递给函数宏的参数不应包含看似预处理指令的标记。这是因为在C语言中,预处理器会在编译之前处理宏定义和宏调用,如果参数中包含看似预处理指令的标记,可能导致预处理阶段出现错误或未定义的行为。

一些预处理指令的标记包括但不限于以下内容:

  • 预处理指令的符号:例如,### 等,它们分别是字符串化运算符和标记连接运算符。
  • 预处理关键字:例如,#include#define#if#endif 等关键字,它们用于执行预处理任务。

传递给函数宏的参数中不应包含这些标记或类似的内容,以避免预处理阶段出现问题。例如,如果传递的参数中包含字符串化或标记连接等运算符,可能导致预处理器无法正确解析并展开宏,从而引起编译错误或其他问题。

以下是一个示例,说明传递给函数宏的参数不应包含看似预处理指令的标记:

#define SQUARE(x) ((x) * (x))

int main() {
    int a = 5;
    int b = SQUARE(#a);  // 不应传递类似预处理指令的标记
    return 0;
}

在这个例子中,SQUARE宏的参数为#a(字符串化运算符和变量a),这不符合规则,因为它可能被解释为预处理指令,并导致预处理阶段出现问题。

总之,传递给函数宏的参数应避免包含任何看似预处理指令的标记,以确保宏的展开行为是可预测的,并且代码的运行符合预期。

15.10 在定义函数宏时,每个参数实例都应该以小括号括起来,除非它们做为#或##的操作数。

在C语言中,定义函数宏时,为了确保宏展开时参数的计算和优先级正确,应该将每个参数实例以小括号括起来,除非它们作为###运算符的操作数。这是一种最佳实践,可以避免在宏展开时出现意外的行为。

下面是一个示例,展示了正确的宏定义方式以及为什么使用小括号括起参数实例是重要的:

#define ADD(x, y) ((x) + (y))

在这个宏定义中,参数xy都被小括号括起来。这确保了在宏展开时,参数的计算优先级是正确的,并且表达式(x) + (y)按照预期执行。

如果不将参数括起来,宏展开时可能会导致意外的结果。例如:

#define ADD(x, y) x + y

int main() {
    int result = ADD(3, 5) * 2;  // 展开后是 3 + 5 * 2
    printf("%d\n", result);  // 输出 13,而不是预期的 16
    return 0;
}

在这个例子中,宏ADD的定义没有将参数括起来。调用ADD(3, 5) * 2展开后变成了3 + 5 * 2,由于运算符的优先级,表达式先计算5 * 2,然后再加上3,结果是13,这与预期的16不符。

通过将参数括起来,可以确保宏展开时参数的计算优先级是正确的,从而避免类似的错误。这也是为什么在定义函数宏时,每个参数实例都应该以小括号括起来,除非它们作为###运算符的操作数的原因。

15.11 **预处理指令中所有宏标识符在使用前都应先定义,除了#ifdef #ifndef指令及defined()**操作符。

在C语言中,预处理指令中所有宏标识符在使用前都应先定义,这是一个好的编程习惯,可以确保代码的可读性和可维护性。

通常,宏标识符是在使用#define指令时定义的。这些宏可以用于替换代码中的常量、表达式、函数样式的代码片段等。如果在使用宏标识符之前没有定义它们,可能导致编译器无法正确识别宏标识符,并导致编译错误。

有两个例外情况:

  1. #ifdef#ifndef 指令:这些指令用于条件编译,检查宏是否被定义(#ifdef)或是否未被定义(#ifndef)。在这两种情况下,宏标识符的定义状态用于决定是否执行特定代码块。

  2. defined() 操作符:这个操作符用于检查宏是否被定义。在条件编译中使用defined()操作符时,可以根据宏的定义状态执行不同的代码块。

下面是一些示例,展示了预处理指令中宏标识符的使用方式:

// 定义宏标识符
#define MAX_SIZE 100

// 使用宏标识符
int array[MAX_SIZE];

// 使用 #ifdef 指令
#ifdef MAX_SIZE
    // 这部分代码将在 MAX_SIZE 宏被定义时执行
    printf("MAX_SIZE is defined\n");
#endif

// 使用 #ifndef 指令
#ifndef MIN_SIZE
    // 这部分代码将在 MIN_SIZE 宏未被定义时执行
    printf("MIN_SIZE is not defined\n");
#endif

// 使用 defined() 操作符
#if defined(MAX_SIZE)
    printf("MAX_SIZE is defined\n");
#endif

在这些示例中,MAX_SIZE宏标识符被定义并在代码中使用。#ifdef#ifndef指令以及defined()操作符用于检查宏的定义状态,并根据结果执行相应的代码块。

总之,为了确保代码的正确性和可维护性,除了在#ifdef#ifndefdefined()中使用外,应该在使用宏标识符之前先定义它们。

15.12 在单一的宏定义中最多可以出现一次 # ## 预处理器操作符。

在C语言的预处理阶段中,###是两个特殊的预处理器操作符,用于实现不同的功能。

  1. # 运算符被称为字符串化运算符,它将宏参数转换为字符串字面量。它在宏定义中出现时,可以将宏参数转换为字符串形式。例如:

    #define TO_STRING(x) #x
    
    printf("%s\n", TO_STRING(Hello); // 输出 "Hello"
    
  2. ## 运算符被称为标记连接运算符,它将两个标记连接成一个标记。这通常用于连接宏参数或字面量。例如:

    #define CONCATENATE(x, y) x##y
    
    int var = CONCATENATE(my, Var); // 连接成 myVar
    

根据MISRA-C标准,在单一的宏定义中,最多只能出现一次###预处理器操作符。这是为了避免复杂的宏定义可能导致的代码混乱和难以维护的情况。

在单一宏定义中,只能出现一次###运算符的规则有助于保持代码的简单和可读性,并防止在宏展开时出现意外的行为。这种限制也是为了确保代码的可靠性和稳定性。

如果你在宏定义中使用了###运算符,请确保只使用一次,并确保其用途明确。如果你需要在宏定义中多次使用这些运算符,可能需要重新考虑你的宏设计,以避免复杂性和潜在的问题。

15.13 不要使用# ## 预处理器操作符。(建议)
  • 与 # 或 ## 预处理器操作符相关的计算次序如果未被指定则会产生问题。编译器对这些操作符的实现是不一致的。为避免这些问题,最好不要使用它们。
15.14 defined 预处理操作符只能使用两种标准形式之一。

defined 预处理操作符在C语言中用于检查宏标识符是否被定义。它有两种标准形式:

  1. defined (identifier): 使用括号形式,例如 #if defined(MY_MACRO)
  2. defined identifier: 使用非括号形式,例如 #if defined MY_MACRO

在这些形式中,identifier 是宏标识符的名称,可以是任何有效的标识符名称。

任何其他形式的 defined 操作符使用都会导致未定义的行为。例如:

  • #if defined (X > Y):这是一种非法形式,因为 defined 操作符的参数应该是一个标识符,而不是表达式。
  • #define DEFINED defined 后,再在 #if 中使用 DEFINED:这也是非法的,因为 defined 操作符应该直接使用,而不应该通过宏定义进行间接调用。

以下是一些正确使用 defined 操作符的示例:

#define MY_MACRO

#if defined(MY_MACRO)
    // 这个代码块将在 MY_MACRO 宏被定义时执行
    printf("MY_MACRO is defined\n");
#endif

#if defined MY_MACRO
    // 这个代码块将在 MY_MACRO 宏被定义时执行
    printf("MY_MACRO is defined\n");
#endif

这些示例展示了两种标准形式的 defined 操作符。它们都正确地检查了 MY_MACRO 宏是否被定义,并在宏被定义时执行相应的代码块。

总之,为了确保代码的正确性和可维护性,应当使用 defined 操作符的两种标准形式之一,避免任何其他形式的使用。

15.15 应该采取防范措施以避免一个头文件的内容被包含两次。

主要有两个方法去预防

  • 方法一:
 #ifndef AHDR_H 
 #define AHDR_H
  • 方法二:
 #ifdef AHDR_H 
 #error Header file is already included 
 #else 
 #define AHDR_H
15.16 预处理指令在句法上应该是有意义的,即使是在被预处理器排除的情况下

在C语言中,预处理指令通常出现在源代码文件的开头或其他位置,用于在编译前控制代码的编译过程。这些预处理指令包括 #define#include#ifdef#ifndef#if#elif#else#endif#error#pragma#line 等。

这些预处理指令在语法上应该是有意义的,即使它们在被预处理器排除的情况下。例如,在条件编译块中,某些代码块可能因为条件为假而被排除,但这些代码块中的预处理指令仍然必须符合C语言的语法规则。这是为了确保代码在将来可能的修改或维护时保持一致性。

举一个例子:

#define DEBUG

#ifdef DEBUG
    #define VALUE 42
#else
    #define VALUE 0
#endif

int main() {
    int x = VALUE;
    return x;
}

在这个例子中,宏 DEBUG 被定义了,因此 #ifdef 块中的代码会被编译,而 #else 块中的代码会被排除。然而,即使 #else 块被排除,它仍然包含一个有效的 #define 预处理指令,这在语法上是正确的。

确保预处理指令在语法上有意义,即使在被预处理器排除的情况下,这样做有助于保持代码的正确性和可读性。即使条件编译块被排除,代码仍然应该是完整和有意义的,这样在条件发生变化时,代码也可以正确地编译和运行。

15.17 所有的 #else、**#elif **和 #endif 预处理指令应该同与它们相关的 #if #ifdef 指令放在相同的文件中。

在C语言中,预处理指令是用于在编译前控制代码的编译过程的重要工具。其中,条件编译预处理指令(如 #if#ifdef#elif#else#endif)用于控制代码块的包含和排除。这些指令通常成对出现,形成一个完整的条件编译块。

根据最佳实践,所有的 #else#elif#endif 预处理指令应该与相关的 #if#ifdef 指令放在相同的文件中。这是为了确保代码的结构清晰,并避免维护性问题。将这些相关联的预处理指令放在同一个文件中可以确保代码块的范围和逻辑清晰,减少对其他文件中定义的宏的依赖,从而降低代码的复杂性。

遵循这一规则有助于保持代码的一致性和可读性。在条件编译块中,代码逻辑更容易理解,因为所有的条件编译指令都放在一个文件中,避免了跨文件的混乱情况。

以下是一个示例,展示了将条件编译预处理指令放在同一个文件中的正确方式:

#define CONDITION

#ifdef CONDITION
    // 这个代码块将在 CONDITION 宏被定义时编译
    printf("CONDITION is defined\n");
#elif OTHER_CONDITION
    // 这个代码块将在 OTHER_CONDITION 宏被定义且 CONDITION 宏未被定义时编译
    printf("OTHER_CONDITION is defined\n");
#else
    // 这个代码块将在 CONDITION 和 OTHER_CONDITION 宏都未被定义时编译
    printf("Neither CONDITION nor OTHER_CONDITION is defined\n");
#endif

在这个示例中,#ifdef#elif#else#endif 指令被放在同一个文件中,这样可以确保代码的逻辑清晰,减少了维护性问题。

总之,将所有相关的预处理指令放在同一个文件中是一个良好的编程实践,可以提高代码的结构和可读性。

16.标准库

16.1标准库中保留的标识符、宏和函数不能被定义、重定义或取消定义。

在C语言标准库中,存在一些保留的标识符、宏和函数。这些标识符、宏和函数是由标准库定义的,用于提供基本的编程功能,如输入/输出、字符串处理、内存管理、数学计算等。

你不应该定义、重定义或取消定义标准库中的保留标识符、宏和函数。这是因为这样做可能会破坏标准库的行为,导致程序的不正确运行和不可预期的结果。以下是一些与保留标识符、宏和函数相关的注意事项:

  1. 保留标识符

    • 标准库中定义了一些保留的标识符,例如库中的数据类型、函数名、宏名等。这些标识符通常以下划线 _ 开头,或在双下划线 __ 的形式中出现。例如,_Bool_Complex__FILE__ 等。
    • 你不应该定义或重定义这些保留标识符,因为它们已经在标准库中有明确的含义。
  2. 保留宏

    • 标准库中定义了一些宏,如 NULLEOFEXIT_SUCCESS 等。这些宏具有特定的意义和用途。
    • 不应该定义或重定义这些保留宏,否则可能会导致库函数的错误行为或不可预期的结果。
  3. 保留函数

    • 标准库中定义了一些函数,例如 printfmallocstrlen 等。这些函数是标准库的一部分,用于提供基本的功能。
    • 你不应该重新定义这些函数,否则可能会破坏程序的正确运行。
  4. 取消定义

    • 你不应该使用 #undef 指令来取消定义标准库中的保留宏,这可能导致库功能的中断或错误。
#include <stdio.h>

int main() {
    // 错误地重定义标准库宏 NULL
    #define NULL 42

    // 当你尝试将一个指针与 NULL 比较时,预期结果是指针为空时为真
    // 但由于 NULL 被错误地重定义为 42,这种比较可能永远不会为真
    int *ptr = NULL;
    if (ptr == NULL) {
        printf("Pointer is NULL\n");
    } else {
        printf("Pointer is not NULL\n");
    }

    return 0;
}

16.2 不能重用标准库中宏、对象和函数的名字。

在C语言编程中,标准库提供了一系列的函数、宏和对象,这些都是用于特定功能的库中定义的。这些名称在标准库中是固定的,因此你不应该重新定义它们或更改其行为

#include <math.h>
#include <stdio.h>

// 自定义的 sqrt_safe 函数,确保输入值非负
double sqrt_safe(double x) {
    if (x < 0) {
        fprintf(stderr, "Error: Negative input to sqrt_safe function\n");
        return -1; // 返回错误代码或处理错误
    }
    return sqrt(x);
}

int main() {
    double value = 9.0;
    double result = sqrt_safe(value);
    if (result >= 0) {
        printf("Square root of %.2f is %.2f\n", value, result);
    } else {
        printf("Failed to calculate square root of %.2f\n", value);
    }
    return 0;
}

16.3 传递给库函数的值必须检查其有效性。

在C语言编程中,传递给库函数的值必须进行有效性检查,以确保参数符合函数的预期输入域。这对于使用标准库、第三方库或自定义库函数尤为重要,因为许多库函数并不一定在内部检查传递给它们的参数的有效性。如果传递无效的参数,会导致未定义的行为或不正确的结果。

以下是一些检查传递给库函数的参数有效性的方法:

  1. 函数调用前的输入值检查

    • 在调用库函数之前,检查传递的参数是否在函数的有效输入域内。例如,在调用 sqrt 函数之前,确保输入值非负;在调用 log 函数之前,确保输入值为正。
    • 使用条件语句检查参数值,并在不满足条件时采取适当的处理措施,如打印错误消息或返回错误代码。
  2. 函数内部的输入值检查

    • 在函数内部进行输入值检查,确保函数参数符合预期输入域。这种方法特别适用于自定义的库函数。
    • 使用条件语句检查参数值,并在不满足条件时采取适当的处理措施。
  3. 封装函数

    • 创建一个封装函数,对输入参数进行检查后再调用原始库函数。
    • 封装函数可以用于第三方库中的函数,通过检查输入参数的有效性来确保安全。
  4. 静态声明

    • 静态地声明输入参数永远不会采取无效的值。这可以通过编译时检查或限制输入参数的类型和范围来实现。
  5. 示例

    • 以下示例展示了如何在调用库函数 sqrt 之前进行输入值的检查:
    #include <stdio.h>
    #include <math.h>
    
    double safe_sqrt(double x) {
        // 检查输入值是否非负
        if (x < 0) {
            fprintf(stderr, "Error: Cannot calculate square root of negative value\n");
            return -1; // 返回错误代码
        }
        // 调用 sqrt 函数
        return sqrt(x);
    }
    
    int main() {
        double value = -4.0;
        double result = safe_sqrt(value);
        if (result >= 0) {
            printf("Square root of %.2f is %.2f\n", value, result);
        }
        return 0;
    }
    

在这个示例中,safe_sqrt 函数在调用 sqrt 函数之前先检查输入值是否为负。如果输入值为负,则打印错误消息并返回错误代码。通过这种方式,可以确保函数的输入值有效,并避免传递无效参数导致的问题。

在实际编程中,应该始终注意传递给库函数的参数的有效性,以确保程序的正确性和稳定性。

16.4 不能使用动态堆的内存分配

不能使用alloc、malloc、realloc 和 free这些函数。

也包括相似的库中的string.h 中的函数。

16.5 不要使用错误指示 errno
16.6 不应使用库<stddef.h>**中的宏 **offsetof。

在 C 语言中,<stddef.h> 头文件提供的 offsetof 宏用于计算结构体或联合体中某个成员的偏移量。然而,在使用 offsetof 宏时,需要注意其操作数的类型是否兼容,并避免使用位域成员作为参数,因为这可能导致未定义的行为。

#include <stddef.h>
#include <stdio.h>

struct Example {
    char a;      // 字节对齐的成员
    int b : 8;   // 位域成员,这里是一个 8 位的位域
};

int main() {
    // 使用 offsetof 宏计算结构体中位域成员 'b' 的偏移量
    size_t offset_b = offsetof(struct Example, b);
    
    // 输出结果
    printf("Offset of 'b' in struct Example: %zu\n", offset_b);

    return 0;
}

16.7 不应使用 setjmp 宏和 longjmp 函数
#include <stdio.h>
#include <setjmp.h>

jmp_buf env;

void riskyFunction() {
    printf("Entering riskyFunction...\n");
    // 使用 longjmp 跳转回 setjmp 处
    longjmp(env, 1);
    // 这个语句将不会被执行,因为 longjmp 已经跳转
    printf("Exiting riskyFunction...\n");
}

int main() {
    // 在 main 函数中使用 setjmp 设置跳转点
    if (setjmp(env) == 0) {
        printf("First time through setjmp\n");
        // 调用可能发生错误的函数
        riskyFunction();
    } else {
        // longjmp 返回到这里
        printf("Longjmp returned control to main\n");
    }
    
    printf("Program continues normally...\n");
    return 0;
}

16.8 不应使用信号处理工具<signal.h>

在 C 语言中,<signal.h> 头文件提供了一些功能来处理信号,包括信号的捕获和信号处理函数的定义。然而,在许多情况下,直接使用信号处理工具可能会导致代码的不稳定性和难以维护。这里是一些可能的问题,以及为什么在一般情况下应避免使用 <signal.h> 头文件

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

void handle_signal(int signal) {
    printf("Received signal: %d\n", signal);
    exit(1);
}

int main() {
    // 设置 SIGINT 信号的处理函数
    if (signal(SIGINT, handle_signal) == SIG_ERR) {
        perror("Failed to set signal handler");
        return 1;
    }

    printf("Running program, press Ctrl+C to send SIGINT signal...\n");

    while (1) {
        // 程序在这里一直运行,等待信号
    }

    return 0;
}

在这个示例中,我们设置了一个信号处理函数来处理 SIGINT 信号(通常由 Ctrl+C 产生)。然而,在信号处理函数中,我们直接调用了 exit 函数,这可能导致资源泄漏,因为没有机会清理资源。此外,这种信号处理也可能导致程序的不稳定性,因为它打乱了正常的程序执行流程。

16.9 在产品代码中不应使用输入输出库<stdio.h>。
16.10 不应使用库<stdlib.h>中的函数atof,atoi和 atol。

当字符串不能被转换时,这些函数具有未定义的行为。

16.11 不应使用库<stdlib.h>中的函数 abort、exit、getenv和 system
16.12 不应使用库<time.h>中的时间处理函数。
  • 8
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值