文章目錄
- 1、前言
- 2、簡介
- 2.1、如何看待MISRA-C 2012
- 2.2、準則(guidelines)里面的指示(Directive)和規則(Rule)
- 2.3、準則(guidelines)的級別(Category)
- 3、若干重要的Directive和Rule
- 3.1、指示(Directive)
- Dir 2.1(必要) 所有的源文件編譯過程不得有編譯錯誤
- Dir 4.4(建議)不應該注釋掉代碼
- Dir 4.11(必要)檢查傳遞給庫函數參數值的有效性
- 3.2、規則(Rule)
- Rule 2.1(必要)不能含有不可達代碼
- Rule 2.2(必要)不能含有死代碼
- Rule 2.3~2.7(建議)不應該含有未使用的類型聲明、標簽、宏、形參
- Rule 5.1~5.9(必要)宏、類型定義/聲明、函數定義/聲明、變量定義/聲明都不得重名
- Rule 5.3(必要)內部作用域聲明的標識符不得隱藏外部作用域聲明的標識符
- Rule 7.2(必要)無符號整形常量都是用"u"或者"U"后綴
- Rule 7.3(必要)小寫字母"l"不能作為常量后綴
- Rule 7.4(必要)不能把字符串常量賦值給對象,除非對象類型為const char*
- Rule 8.4(必要)具備外部鏈接的標識符必須有顯式可見的聲明
- Rule 8.5(必要)具備外部鏈接的標識符只能在一個文件聲明一次
- Rule 8.8(必要)具備內部鏈接的變量和函數都必須使用static修飾
- Rule 8.11(建議)具備外部鏈接的數組聲明,應該顯式指定長度
- Rule 9.1(強制)具備自動儲存周期的對象,在設定它的值之前不能讀取
- Rule 10.3(必要)表達式的值不得賦給更窄的基本類型,也不得賦給不同的基本類型類別
- Rule 11.9(必要)宏NULL必須為整數類型空指針常量的唯?允許形式
- Rule 12.1(建議)應明確表達式中操作數的優先級
- Rule 12.3(建議)不得使用逗號表達式
- Rule 12.4(建議)對常量表達式進?求值不應導致整數回繞
- Rule 15.4(建議)對于任何迭代語句,最多只應使??個break或goto語句進?終?
- Rule 15.5(建議)函數結尾應只有?個退出點
- Rule 15.7(必要)所有if … else if構造都必須以?個else語句終?
- Rule 16.4(必要)每個switch語句都必須有default標記
- Rule 17.2(必要)不得使用遞歸調用
- Rule 17.8(建議)不應該修改函數的形參
- Rule 21.3~21.11(必要)不得使用下面標準庫中的接口
??推薦的個人網站MISRA-C中文翻譯,但是它少了Directives部分,Rule部分是比較詳細的;
??推薦的MISRA-C條目整理文檔,百度云盤NaiveSystems Analyze 2022.1 Tech Spec CN 提取碼:fau8
1、前言
??首先,MISRA-C是一個C語言標準,是一個文檔,是收費的,所以網上的解讀以及翻譯都是有人在官網買過,然后根據官網的文檔而來;
??其次,為什么要搞濃縮版,因為MISRA-C實在太長了,而且很多東西我們極其少用到,比如初始化數組:
uint8_t array[2] = {[0] = 1, [1] = 2}
??或者已經是默認行為如禁止使用goto等,所以小白在這邊精煉一些對實際項目和工作有幫助的內容進行展開;
2、簡介
2.1、如何看待MISRA-C 2012
??MISRA-C 2012是屬于第三版的MISRA-C,一開始是為了汽車行業而專門定的C語言編程規范(不包含代碼風格),說白了就是以前從事汽車行業開發的大佬,在積累N年的C語言編程經驗后得出的寶典,寶典中沉淀著他們實際項目中遇到的奇葩問題的預防措施,就是只要你按照寶典的要求去寫代碼,那么就會減少很多奇奇怪怪的問題,把這些問題扼殺在搖籃當中,而不是等問題出現后再這樣去寫代碼。
??其實每一個嵌入式公司基本都會有內部的編程規范,這些規范更多是軟件總監等級別的人編寫的,像華為的編程規范,阿里巴巴的編程規范,但是這些商業的編程規范都比較傾向于他們所屬的業務,具有領域屬性,很多規定都是為業務而生,所以不必詫異為什么里面會規定這樣的內容,因為每一條內容的背后都會有血的教訓。但是MISRA-C在第二版之后就不具備領域屬性了,而是面向所有領域,跨行業的應用,因此現在很多公司的C語言編程規范,多多少少都帶著MISRA-C的內容在里面,因為MISRA-C道出了C語言編程本身的各種問題,而不是某個領域的問題;
??所以你可以當MISRA-C是一個字典工具,偶爾看一看,或者編程的時候突然想起有相關的就查一下,不必一直啃食,因為很多內容如果你沒經歷過,大概就是從左耳進,右耳就出去了,當你先過一遍,腦子里大概知道有提及哪些內容,然后實際真的遇到后,你自然會回來查閱的;
2.2、準則(guidelines)里面的指示(Directive)和規則(Rule)
??話不多說,MISRA-C原文檔的各種總覽、背景、工具等我都忽略過,直接進入主題,MISRA-C既然是一個編程規范,里面肯定是各種條例規定,理所當然最重要的內容叫準則(guidelines),可以理解為最頂層的規范,它由2部分組成:
-
Directive,翻譯為指示,引導你需要做這個事情,但是這個事情沒有判斷對錯的標準。實際項目是否符合規范是需要借助其它條件來證實的,單純依賴MISAR-C是不能證實的。比如Dir 4.3提及匯編語言必須被封裝并隔離,但是封裝和隔離都是每一個項目根據實際情況來進行的,接口不統一、做法不統一,統一的只有這個思維,這就是Directive;
-
Rule,翻譯為規則,明確代碼就是要這樣做,否則就違反標準。它不需要根據其它情況可以進行判定,比如Rule 15.1 說明不應該使用goto語句,你用了而且沒有其它預防措施就是不符合MISRA-C規范,沒有用或者用了但是有各種預防措施就符合這一條的規范,這就是Rule;
2.3、準則(guidelines)的級別(Category)
??其中每一條Dir或者Rule都有3種類型的級別:
????mandatory(強制):不允許違反;
????required(必要):只有在明確限制、要求和預防措施的情況下才可以違反;
????advisory(建議):在合理可行的范圍內建議遵守;
??這3個級別是相同重要的,差異點在于是否允許偏離標準,偏離標準的例子如下,為了某些特殊情況偏離標準,比如將 int 類型值強制轉為指針來實現訪問內存地址空間映射的 I/O,需要專門的文檔記錄這種違反MISRA-C的地方:
// 內存中的0x0002地址內數據映射了某一I/O端口數據
#define PORT (*(volatile unsigned char *)0x0002)
// 修改該位置數據就相當于修改了該I/O端口數據
PORT = 0x10u;
3、若干重要的Directive和Rule
??Directive和Rule加起來會有170+條,每一條在官網文檔種都有描述,但是我們這里只選擇若干重點的內容來展開,這些都是養成良好編程習慣的重要規范,對工作和實際項目有好處無壞處;
3.1、指示(Directive)
Dir 2.1(必要) 所有的源文件編譯過程不得有編譯錯誤
小白理解:每一次編譯成功后,都需要保持項目的 0 warning,0 error
??很多人會容忍warning的存在,但這是不對的,可能項目存在定義但未使用的代碼然后觸發warning,雖然無關緊要,但是太多的時候會把真正的warning掩蓋掉,比如if (x = y)
這種寫法會觸發warning,如果你的項目本來就有幾十個warning的時候,那么多這1個也很難去看到,于是你就錯過了編譯器幫你排查BUG的機會,當出現問題的時候需要花費額外的時間去排查。因此永遠遵守0 warning,0 error能讓編譯器幫你識別很多問題;
Dir 4.4(建議)不應該注釋掉代碼
小白理解:注釋代碼容易和真正的注釋產生混淆,如果真的想暫時屏蔽一段代碼,應該要明確標注出起點和終點,使用#if或者#ifdef起點然后#endif重點,但是我覺得最好的做法是刪掉他們,利用git等代碼管理工具來保存記錄
??很多人在實際項目中暫時取消某個功能就是使用注釋,因為注釋一遍都是有快捷鍵的,很方便就能選中代碼然后注釋掉,但是由于真正的注釋和這種行為的注釋用法是一樣的,會讓其他人看這個代碼產生混淆,它到底有沒有作用,如果真的存在一個能符合語法的注釋它真的是一個注釋,而且混淆在這些屏蔽代碼里面,那么程序員將很難分清代碼行為。
void FuncA(void)
{uint8_t Cnt = 0;//符合Dir 4.4寫法
#if 0Cnt++;
#endif//不符合Dir 4.4寫法
// Cnt++;
}
Dir 4.11(必要)檢查傳遞給庫函數參數值的有效性
小白理解:在調用庫函數之前,必須先檢查參數是否有效,再進行調用,而不是相信庫函數會自己檢查,原則上就是不相信其它庫的接口在遇到非法數據時行為是正常的,都要假設是異常的。
??其實這種思維在Linux早已根深蒂固,Linux內核的設計思維之一就是不相信用戶會正確使用內核函數,因此給用戶加了各種權限,讓用戶需要通過各種驗證后才能使用接口,所有我們在調用別人的庫之前就需要檢查入口,同時編寫接口的時候也要做到入口參數的檢查。
/* A.h */
extern void FuncA(uint8_t* pMsg);
extern uint8_t* GetMsgPointer(void);/* B.c */
void FuncB(void)
{uint8_t* pM = GetMsgPointer();//符合Dir 4.11寫法if (NULL != pM) {FuncA(pM);}//不符合Dir 4.11寫法FuncA(pM);
}
3.2、規則(Rule)
Rule 2.1(必要)不能含有不可達代碼
小白理解:原因是1這些代碼占用Ram和Flash而且毫無意義純屬浪費,2是可能導致編譯器產生一些又臭又長的跳轉指令但實際并不需要,3是可能導致整個循環變慢。
??很多不可達的代碼都有warning,請遵守Dir 2.1保持0 warning,0 error。另外就是有比較難檢查到的不可達代碼,就比較依賴靜態檢查工具來進行了,但是一般遵守if-else或者switch-case的時候不要提前return,而且確保判斷的值范圍都在內部一般不會有什么大問題。
#include "stdafx.h"
typedef enum ErrStatus {Success = 0,Err_1,Err_2
} ErrStatus;ErrStatus f(int x) {if (x < 0) {return Err_1;}else{return Success;}
}int main()
{ErrStatus x = f(5);switch (x){case Err_1:printf("err 1"); break;case Err_2:printf("err 2 "); break; /* unreachable code */default:printf("Success"); break;}
}
Rule 2.2(必要)不能含有死代碼
小白理解:把調試代碼和取消的功能刪干凈再上傳業務代碼。
??意在提醒實際項目中調試或者刪除功能時,要把代碼弄干凈,寫了一個debug接口,結束后忘記刪除然后停留在業務上面,如果不調試它是沒有任何作用的,占用Ram和Flash純屬浪費,還容易引起胡混淆;
extern volatile uint16_t v;
extern char *p;
void f(void)
{uint16_t x;(void)v; /* Compliant - 這種方式用于抑制編譯器的未使用告警,是有意義的,如果刪除就會產生編譯器告警,不視為dead code */(int32_t) v; /* Non-compliant - the cast operator is dead */v >> 3; /* Non-compliant - the >> operator is dead */x = 3; /* Non-compliant - the = operator is dead* - x is not subsequently read */*p++; /* Non-compliant - result of * operator is not used */(*p)++; /* Compliant - *p is incremented */
}
Rule 2.3~2.7(建議)不應該含有未使用的類型聲明、標簽、宏、形參
小白理解:我覺得這一堆跟Rule 2.2是一樣的含義,沒用的代碼弄干凈點,我推薦直接刪掉,連#if和#ifdef都不要用,除非是必須的調試代碼。
Rule 5.1~5.9(必要)宏、類型定義/聲明、函數定義/聲明、變量定義/聲明都不得重名
小白理解:在 C99 中規定外部鏈接標識符的有效識別長度為 31 個字符(是否大小寫敏感取決于編譯器),也就是前 31 個字符需要唯一,才能區分兩個外部鏈接標識符表示不同的項。
我建議每一個對象的命名,都跟隨其所屬模塊作為前綴,這樣重名的概率低很多,比如EEPROM模塊,那么里面的函數、變量、struct、enum等都是EEPROM_作為前綴,這樣即使另一個模塊比如Timer也有一個名字教Data的,那么EEPROM_Data就和Timer_Data不相同,另外是命名不要過長,模塊_動作_名稱,控制在31字符內;
/* 1234567890123456789012345678901********* Characters */
int32_t engine_exhaust_gas_temperature_raw;
int32_t engine_exhaust_gas_temperature_scaled; /* Non-compliant,兩個變量名的前31個字符相同 */
/* 1234567890123456789012345678901********* Characters */
int32_t engine_exhaust_gas_temp_raw;
int32_t engine_exhaust_gas_temp_scaled; /* Compliant */
Rule 5.3(必要)內部作用域聲明的標識符不得隱藏外部作用域聲明的標識符
小白理解:函數內部的局部變量不要和全局變量重名
??特別是循環計數變量,習慣用i,j,k沒問題,但是一個函數內部每一個循環的計數名字都要不一樣,可以有i,j,k,m,n,z,y……都行,最好就是明確當前循環計數的含義,比如當前循環是找A的,計數值變量就叫A_i或者其它具體含義,下一個循環是找B的就叫B_i或者其它具體含義,只有一個循環直接叫i或者其它具體含義;
void fn1(void)
{int16_t i; /* Declare an object "i" */{int16_t i; /* Non-compliant - hides previous "i" ,第三種情況連續嵌套塊*/i = 3; /* Could be confusing as to which "i" this refers */}
}
struct astruct
{int16_t m;
};
extern void g(struct astruct *p);
int16_t xyz = 0; /* Declare an object "xyz" */
void fn2(struct astruct xyz) /* Non-compliant - outer "xyz" is* now hidden by parameter name */
{g(&xyz);
}
uint16_t speed;
void fn3(void)
{typedef float32_t speed; /* Non-compliant - type hides object */
}
Rule 7.2(必要)無符號整形常量都是用"u"或者"U"后綴
小白理解:不要相信編譯器可以很好幫你處理常量類型
??當你的的寫法不一樣的時候,4095是uint16_t或者uint32_t或者sint32_t都是不知道的,但是你只要寫成4095u,那他無論怎么都是無符號型,而且必須是uint16_t起步,這樣子就算和有符號的進行計算,也不會認為最高位是符號位了;
??例如,整數常量 40000 在 32 位環境中屬于帶符號 int 類型,但在 16 位環境中屬于 signed long 類型。數值 0x8000 在 16 位環境中屬于 unsigned int 類型,但在 32 位環境中屬于 signed int 類型。在 2-bit int 和 64-bit long 環境中:
void R_7_2(void)
{use_int32(2147483647); /* int constant */use_int32(0x7FFFFFFF); /* int constant */use_int64(2147483648); /* long constant */use_uint32(2147483648U); /* unsigned int constant */use_uint32(0x80000000); /* unsigned int constant - Non-compliant */use_uint32(0x80000000U); /* unsigned int constant */
}
Rule 7.3(必要)小寫字母"l"不能作為常量后綴
小白理解:純屬和數字1長得很像,肉眼難以區分,干脆不用
const int64_t a = 0L;
const int64_t b = 0l; /* Non-compliant */
const uint64_t c = 0Lu;
const uint64_t d = 0lU; /* Non-compliant */
const uint128_t e = 0ULL;
const uint128_t f = 0Ull; /* Non-compliant */
const int128_t g = 0LL;
const int128_t h = 0ll; /* Non-compliant */
const float128_t m = 1.2L;
const float128_t n = 2.4l; /* Non-compliant */
Rule 7.4(必要)不能把字符串常量賦值給對象,除非對象類型為const char*
小白理解:道理很簡單大家都懂,字符串常量不可被改變,但是寫接口的時候很多會忽略,只能說帶來隱患,寫上就100%沒問題
extern void f1(char *s1);extern void f2(const char *s2);static void g2(void)
{f1("string"); /* Non-compliant,形參為非const,實參是字符串常量 */f2("string"); /* Compliant */
}static char *name1(void)
{return ("MISRA"); /* Non-compliant,返回參數類型非const */
}static const char *name2(void)
{return ("MISRA"); /* Compliant*/
}void R_7_4(void)
{char *s = "string"; /* Non-compliant */const volatile char *p = "string"; /* Compliant */"0123456789"[0] = '*'; /* Non-compliant,未定義行為 */g2();(void)name1();(void)name2();use_const_char_ptr(s);use_const_volatile_char_ptr(p);
}
Rule 8.4(必要)具備外部鏈接的標識符必須有顯式可見的聲明
小白理解:說白了就是調用文件以外的變量、函數、宏等都需要有顯式的聲明,而且變量和函數都必須帶extern
extern void func1(void);
extern void func2(int16_t x, int16_t y);
extern void func3(int16_t x, int16_t y);
void func1(void)
{/* Compliant */
}
void func2(int16_t x, int16_t y)
{/* Compliant */
}
void func3(int16_t x, uint16_t y)
{/* Non-compliant - parameter types different,違反規則8.3 */
}
void func4(void)
{/* Non-compliant - no declaration of func4 before this definition */
}
static void func5(void)
{/* Compliant - rule does not apply to objects/functions with internal* linkage */
}
Rule 8.5(必要)具備外部鏈接的標識符只能在一個文件聲明一次
小白理解:聲明都放在.h里面,不要越界操作
??這個是和Rule 8.4有關系的,Rule 8.4是說外部鏈接的標識需要有聲明,這里則說只能聲明一次,稍微轉化一下就是,規定外部鏈接的標識符只能在.h里面聲明且只有1次聲明,換言之,不要在.c文件里面用extern把其它文件的變量或者函數給包含進來,這種屬于不合規的做法,正常使用你只能#include一個.h頭文件進來,如果只是臨時調試使用,調試結束后記得刪干凈。
/* featureX.h */
extern int16_t a; /* Declare a *//* file.c */
#include "featureX.h"int16_t a = 0; /* Define a */
Rule 8.8(必要)具備內部鏈接的變量和函數都必須使用static修飾
小白理解:不給別人用的變量和函數,都加上static修飾
??1來可以防止重名問題,2來可以清晰閱讀一個.c里面哪些只限于內部使用,3來添加權限屬性,別人在.c里面強制extern你的static變量或者函數是會編譯失敗的,他必須再去你文件里面刪掉static,也就是讓越界行為變得復雜,稍微再防止一下
static int32_t x = 0; /* definition: internal linkage */
extern int32_t x; /* Non-compliant,先前已存在x的聲明,* 導致這個x的鏈接性就是內部的,* 而不是我們平常認為的用extern修飾的是外部鏈接 */
static int32_t f(void); /* declaration: internal linkage */
int32_t f(void) /* Non-compliant */
{return 1;
}
static int32_t g(void); /* declaration: internal linkage */
extern int32_t g(void) /* Non-compliant */
{return 1;
}
Rule 8.11(建議)具備外部鏈接的數組聲明,應該顯式指定長度
小白理解:不要用extern uint8_t array[],一定要用#define LENGTH_MAX 10 extern uint8_t array[LENGTH_MAX]
??沒什么好說的,給被人用的數組一定要讓別人有方法知道長度,否則就存在越界的隱患,你可以用函數返回,可以用全局變量,可以用宏,可以用枚舉等等,一定要用其中一個
extern int32_t array1[10]; /* Compliant */
extern int32_t array2[]; /* Non-compliant */
Rule 9.1(強制)具備自動儲存周期的對象,在設定它的值之前不能讀取
小白理解:局部變量定義時即初始化,不要偷懶
??這也是減少隱患的預防性編程,定義局部變量的時候順手寫一個賦值操作,這帶來了確定性,天曉得下一個人會不會在局部變量還沒賦值的時候使用,或者你不會,但下一個維護你代碼的人會
static void f(bool_t b, uint16_t *p)
{if (b){*p = 3U;}
}static void g(void)
{uint16_t u; /* Non-compliant declaration,u未被顯式賦值 */f(false, &u);if (u == 3U) /* Non-compliant use - "u" has not been assigned a value. */{use_uint16(u); /* */}
}static void jmp_over_init(void)
{goto L1; /* violates R.15.1 */uint16_t x = 10u;
L1:// 此處的x聲明雖然被跳過,但x還是被正常聲明了,可編譯通過x = x + 1u; /* Non-compliant - x has not been been assigned a value */use_uint16(x);
}void R_9_1(void)
{bool_t b = get_bool();uint16_t val = 3u;f(b, &val);use_uint16(val);g();jmp_over_init();
}
Rule 10.3(必要)表達式的值不得賦給更窄的基本類型,也不得賦給不同的基本類型類別
小白理解:運算類型始終保持一致性,多使用強制類型轉換
??這是避免編譯器的隱形轉換導致數據截斷或者精度問題,編譯器不一定會按照你的想法進行隱式轉換,自己強制轉換能掌控全局
u8a = 2; /* Compliant By 例外1 */
u8a = 2 * 24; /* Compliant By 例外1 */uint8_t u8f = 1.0f; /* Non-compliant - unsigned and floating */
bool_t bla = 0; /* Non-compliant - boolean and signed,不符合例外1,因為bla不是無符號整型 */
cha = 7; /* Non-compliant - character and signed */
u8a = 'a'; /* Non-compliant - unsigned and character */
u8b = 1 - 2; /* Non-compliant - unsigned and signed,不符合例外1,因為1-2不是非負的 */
u8c += 'a'; /* Non-compliant - u8c = u8c + 'a' assigns character to unsigned */s8a = K2; /* Non-compliant - Constant value does not fit */
u16a = u32a; /* Non-compliant - uint32_t to uint16_t */s8a = -123L; /* Non-compliant - signed long to int8_t */u8a = 6L; /* Non-compliant - signed long to uint8_t,不符合例外1 *//* Standard Type has rank greater than int,* so exception does not apply *//* integer constant expression from + with value 5U and UTLR of unsigned char */
u8a = (uint16_t)2U + (uint16_t)3U; /* Compliant,例外1? *//* integer constant expression from + with value 100000U and UTLR of unsigned int */
u16a = (uint16_t)50000U + (uint16_t)50000U; /* Non-compliant,不符合例外1,超過了u16的最大值 *//* Top-level cast returns C standard type of unsigned short */
u8a = (uint16_t)(2U + 3U); /* Non-compliant,經過轉換后不再是常量表達式,不符合例外1 */
Rule 11.9(必要)宏NULL必須為整數類型空指針常量的唯?允許形式
小白理解:只能使用NULL來判斷空指針,不能使用0
??這個網上有很多解釋了,(void*)0和0的含義是不一樣的,你不能相信編譯器會把他們認為是一樣,所以統一使用NULL的(void*)0
#define MY_NULL_1 0
#define MY_NULL_2 (void *)0
#define MY_NULL_3 NULLextern void f9(uint8_t *p);int32_t *p1 = 0; /* Non-compliant */
int32_t *p2 = (void *)0; /* Compliant */
int32_t *p3 = MY_NULL_3; /* Compliant */if (p1 == MY_NULL_1) /* Non-compliant - also breaks R.14.3 */
{
}
if (p2 == MY_NULL_2) /* Compliant - but breaks R.14.3 */
{
}f9(NULL); /* Compliant for any conforming definition of* NULL, such as:* 0* (void *)0* (((0)))* (((1 - 1)))*/
Rule 12.1(建議)應明確表達式中操作數的優先級
小白理解:不要憐惜括號的使用,每一層計算都需要加
??預防性編程的一種,把結果掌控在程序員手里,而不是選擇相信編譯器,比如很多人喜歡這樣寫if ( a >= b && c <= d),這完全沒問題,但是MISRA-C更加推薦你這樣寫,if ( (a >= b) && (c <= d)),給每一個運算都加上括號維護起來,這是100%不會出現符號優先級問題的
Rule 12.3(建議)不得使用逗號表達式
小白理解:很少人用,優先級和副作用問題多,干脆不要用
??逗號表達式最多出現在for的子句里面,比如for(i = 0, j = 0; ; i++, j++)這樣子,看起來是完全沒問題的如果只是如此簡單,不過如果你的計算稍微復雜比如突然有一天j要從i開始,就改成了for(i = 0, j = i; ; i++, j++),請問如果你選擇相信編譯器,j的值是0還是沒有賦值0之前的i值?還是那句話,不要依賴編譯器,能自己完成的事情就不要讓編譯器去做,取消逗號表達式的使用
/* also violates R.14.2 */
for (i = 0, p = &a[0]; /* Non-compliant */i < N;++i, ++p) /* Non-compliant */
{
}
Rule 12.4(建議)對常量表達式進?求值不應導致整數回繞
小白理解:常量的加法不應該溢出,這個是很難人工檢查出來的,一般依賴靜態檢查,或者編程的時候注重常量的加法
// 與 case 標簽關聯的表達式必須是常量表達式。
// 如果在 case 表達式求值期間發生無符號環繞,則很可能是無意的。
// 在具有 16 位 int 類型的計算機上會導致以下示例中的回繞:
#define BASE 65024u
switch (x)
{
case BASE + 0u:f();break;
case BASE + 1u:g();break;
case BASE + 512u: /* Non-compliant - wraps to 0 */h();break;
}
Rule 15.4(建議)對于任何迭代語句,最多只應使??個break或goto語句進?終?
小白理解:禁止!禁止!禁止!使用goto,另外遵循單點退出原則,并且退出點必須在模塊的最后一句
??單點退出原則也就是一個模塊只能有一個退出點,這個的好處是約束了模塊的行為是穩定的,它必然會跑到固定的退出點,而退出點固定在模塊最后一句,意味著模塊內每一句語句都會被執行,防止了一些鎖的成對操作缺漏,或者模塊的退出操作缺漏,預防性編程
#define LIMIT 100u/* Note: All uses of goto also break R.15.1 */void R_15_4(void)
{uint32_t x;uint32_t y;uint32_t z;for (x = 0; x < LIMIT; ++x){if (ExitNow(x)){break; /* compliant - single exit from outer loop */}for (y = 0; y < x; ++y){if (ExitNow(LIMIT - y)){break; /* compliant - single exit from inner loop* 這個break僅用來退出本for循環而不是上級for,* 所有和上面那個break不沖突 */}}}for (x = 0; x < LIMIT; ++x){if (BreakNow(x)){break;}else if (GotoNow(x)){goto EXIT; /* Non-compliant - break and goto in loop */}else{KeepGoing(x);}}EXIT:;while (x != 0u){if (x == 1u){break;}while (y != 0u){if (y == 1u){// 這個goto直接退了兩層while,和上面的break沖突goto L1; /* Non-compliant (outer loop) Compliant (inner loop) *//* goto causes early exit of both inner and outer loop */}}}
L1:z = x + y;
}
Rule 15.5(建議)函數結尾應只有?個退出點
小白理解:和Rule 15.4的一個意思,遵守單點退出原則;
static bool_t f(uint16_t n, const char *p)
{if (n > MAX){return false; /* Non-compliant */}if (p == NULL){return false; /* Non-compliant */}return true;
}
Rule 15.7(必要)所有if … else if構造都必須以?個else語句終?
小白理解:強制讓你思考else的內容,也就是讓你多思考一個分支情況,防止發生錯漏,實際項目很多時候就是說”啊,少考慮了這種情況“
if (flag_1)
{action_f1();
}
else if (flag_2)
{action_f2();
}
/* Non-compliant */if (flag_1)
{action_f1();
}
else if (flag_2)
{action_f2();
}
else
{; /* No action required - ; is optional */
}
Rule 16.4(必要)每個switch語句都必須有default標記
小白理解:switch或許有你考慮不到的值,如果沒有default,則switch會退出,你想debug的機會都難,多寫一個default,里面加打印語句,100%不會錯漏
switch (x)
{case 0:++x;break;case 1:case 2:break;/* Non-compliant - default label is required */
}
Rule 17.2(必要)不得使用遞歸調用
小白理解:嵌入式的RAM太寶貴了,就算你有1M的RAM,也經不住遞歸的堆棧開辟,隨時棧溢出的風險,除非你有G級別的RAM,但這已經不太屬于嵌入式了,不要用是100%不會出現遞歸導致的棧溢出問題
static uint16_t fn_a(uint16_t parama)
{uint16_t ret_val;if (parama > 0U){ret_val = parama * fn_a(parama - 1U); /* Non-compliant */}else{ret_val = parama;}return ret_val;
}
Rule 17.8(建議)不應該修改函數的形參
小白理解:參數或許需要拿來判斷原始值,留住備份總是一個好習慣
int16_t glob = 0;
void proc(int16_t para)
{para = glob; /* Non-compliant */
}
void f(char *p, char *q)
{p = q; /* Non-compliant */*p = *q; /* Compliant */
}
Rule 21.3~21.11(必要)不得使用下面標準庫中的接口
小白理解:不要相信標準庫能很好兼容你的芯片,直接不用是100%不出標準庫問題
??stdlib.h中的動態內存接口、sethmp.h所有接口、signal.h所有接口、stdlib的標準IO接口、atof、atoi、atoll、atol、system終止函數、bsearch、qsort、time、date、tgmath.h所有接口
#include <stdlib.h>void R_21_3(void)
{char_t *p;p = (char_t *)malloc(11U); /* Non-compliant: use of malloc */if (p != NULL){(void)realloc(p, 20U); /* Non-compliant: use of realloc */}free(p); /* Non-compliant: use of free */p = (char_t *)calloc(10, sizeof(char_t)); /* Non-compliant: use of calloc */free(p); /* Non-compliant: use of free */
}