天天看點

高品質嵌入式軟體的開發技巧

作者:嵌入式小美老師

1 劍宗氣宗之争

《笑傲江湖》中華山派的劍宗和氣宗之争,可謂異常激烈。那麼問題就來了,既然有劍宗氣宗之争,到底應該先練劍,還是先練氣呢?引申到軟體開發行業有沒劍氣之争呢?

前面釋出很多理論方面的文章,諸如4篇:基于RTOS的軟體開發理論,嵌入式軟體的設計模式(上),嵌入式軟體的設計模式(下),嵌入式軟體分層隔離的典範。這些都是具備一定基礎後在架構上的描述,類似于氣宗性質,這種比較抽象、見效慢。但高品質的軟體開發,也是存在見效快的套路,針對有一定嵌入式C語言開發基礎的,以劍宗之法進行描述,可重點關注if判斷和記憶體管理相關的講解,抛磚引玉。

2 檔案結構

1、C 程式通常分為兩類檔案,一種是程式的聲明稱為頭檔案,以“.h”為字尾,另一種是程式的實作,以“.c”為字尾,一般每個c檔案有個同名的h檔案。

2、軟體的頭檔案數目比較多,應将頭檔案和定義檔案分别儲存于不同的目錄,例如将頭檔案儲存于 include或者inc 目錄,将定義檔案儲存于 source 或src目錄;如果某些頭檔案是私有的,它不會被使用者的程式直接引用,則沒有必要公開其“聲明”。為了加強資訊隐藏,這些私有的頭檔案可以和定義檔案存放于同一個目錄,即私有的h檔案放在src目錄。

3、在檔案頭添加版權和版本的聲明等資訊,主要包括版權和功能,以及修改記錄,必要時可以為整個功能檔案夾單獨建立readme說明文檔。

4、為了防止頭檔案被重複引用,必須用 ifndef/define/endif 結構産生預處理塊。

5、頭檔案中隻存放“聲明”而不存放“定義”,更别提放變量,這是嚴重的錯誤。

6、用 #include <filename.h> 格式來引用标準庫的頭檔案,用 #include “filename.h” 格式來引用非标準庫的頭檔案(編譯器将從使用者的工作目錄開始搜尋)。

7、檔案可按層或者功能元件劃分不同的檔案夾,便于其他人閱讀。

3 程式版式

版式雖然不會影響程式的功能,但會影響可讀性,程式的風格統一則是賞心悅目。

代碼排版在編碼時确實很難把握,但可以編碼完成後統一用工具格式化,不管編碼使用Keil/MDK、Qt等內建工具,或者純粹的代碼編輯工具Source Insight,一般都支援自定義運作可執行檔案,如Astyle。可以客制化新菜單,一鍵執行Astyle,将代碼一鍵格式化,排版統一、層次分明。

Astyle官網 http://astyle.sourceforge.net/ 按要求下載下傳安裝,隻需要AStyle.exe即可。關于其使用和參數,可以再進入Documentation。對代碼基本風格,{}如何對齊、是否換行,switch-case如何排版,tab鍵占位寬度,運算符或變量前後的空格等等,基本上代碼排版涉及的方方面面都有參數說明。個人選擇的編碼參數是

--style=allman -S -U -t -n -K -p -s4 -j -q -Y -xW -xV fileName
           

效果如下

//微信公衆号:嵌入式系統
int Foo(bool isBar)
{
    if (isBar)
    {
        bar();
        return 1;
    }
    else
    {
        return 0;
    }
}
           

也可以參考 代碼的保養 第3章。關于注釋,重要函數或段落必不可少,修改代碼同時修改相應的注釋,以保證注釋與代碼的一緻性。

4 命名規則

比較著名的命名規則當推 Microsoft 公司的“匈牙利”法,該命名規則的主要思想是“在變量和函數名中加入字首以增進人們對程式的了解”。例如所有的字元變量均以 ch 為字首,若是指針變量則追加字首 p。但沒有一種命名規則可以讓所有的程式員滿意,制定一種令大多數項目成員滿意的命名規則,重點是在整個團隊和項目中貫徹實施。

事實上開發大多數基于SDK,一般底層命名規則盡量與SDK風格保持一緻,至于上層就按團隊标準,個人比較傾向全部小寫字母,用下劃線分割的風格,例如 set_apn、timer_start。

不要出現辨別符完全相同的局部變量和全局變量,盡管兩者的作用域不同而不會發生文法錯誤,但會使人誤解,全局變量也不要過于簡短。

變量的名字應當使用“名詞”或者“形容詞+名詞”,函數的名字應當使用“動詞”或者“動詞+名詞”,用正确的反義詞組命名具有互斥意義的變量或相反動作的函數等。

5 基本語句

表達式和語句都屬于C 文法基礎,看似簡單,但使用時隐患比較多,提供一些建議。

5.1 if

if 語句是 C 語言中最簡單、最常用的語句,然而很多程式員卻用隐含錯誤的方式,僅以不同類型的變量與零值比較為例,展開讨論。

1、布爾變量與零值比較

不可将布爾變量直接與 TRUE、FALSE 或者 1、0 進行比較。根據布爾類型的語義,零值為“假”(記為 FALSE),任何非零值都是“真”(記為TRUE)。TRUE 的值究竟是什麼并沒有統一的标準。

假設布爾變量名字為 flag,它與零值比較的标準 if 語句如下:

//微信公衆号:嵌入式系統
if (flag)   // 表示 flag 為真 
if (!flag)   // 表示 flag 為假 
           

其它的用法都屬于不良風格,例如:

//錯誤範例
 if (flag == TRUE) 
 if (flag == 1 ) 
 if (flag == FALSE) 
 if (flag == 0) 
           

2、整型變量與零值比較

整型變量用“==”或“!=”直接與 0 比較,假設整型變量的名字為 value,它與零值比較的标準 if 語句如下:

if (value == 0) 
if (value != 0) 
           

不可模仿布爾變量的風格而寫成

//錯誤範例
if (value)   // 會讓人誤解 value 是布爾變量 
if (!value) 
           

3、 浮點變量與零值比較

不可将浮點變量用“==”或“!=”與任何數字比較,無論是 float 還是 double 類型的變量,都有精度限制。不能将浮點變量用“==”或“!=”與數字比較,應該設法轉化成“>=”或“<=”形式。假設浮點變量的名字為 x,應當将

if (x == 0.0) // 隐含錯誤的比較,錯誤
           

轉化為

const float EPSINON = 0.00001
if ((x>=-EPSINON) && (x<=EPSINON)) 
//其中 EPSINON 是允許的誤差(即精度),即x無限趨近于0.0
           

4、指針變量與零值比較

指針變量用“==”或“!=”與 NULL 比較, 指針變量的零值是“空”(記為 NULL),盡管 NULL 的值與 0 相同,但是兩者意義不同。假設指針變量的名字為 p,它與零值比較的标準 if 語句如下:

if (p == NULL) // p 與 NULL 顯式比較,強調 p 是指針變量 
 if (p != NULL) 
           

不要寫成

if (p == 0)  // 容易讓人誤解 p 是整型變量 
if (p != 0) 
if (p)    // 容易讓人誤解 p 是布爾變量 
if (!p) 
           

嵌入式物聯網需要學的東西真的非常多,千萬不要學錯了路線和内容,導緻工資要不上去!

無償分享大家一個資料包,差不多150多G。裡面學習内容、面經、項目都比較新也比較全!某魚上買估計至少要好幾十。

點選這裡找小助理0元領取:加微信領取資料

高品質嵌入式軟體的開發技巧

5.2 for

在多重循環中,如果有可能,應當将最長的循環放在最内層,最短的循環放在最外層,以減少 CPU 切換循環層的次數。

//不良範例
for (row=0; row<100; row++) 
{ 
 for ( col=0; col<5; col++ ) 
 { 
  sum = sum + a[row][col]; 
 } 
} 

//微信公衆号:嵌入式系統  較高效率
for (col=0; col<5; col++ ) 
{ 
 for (row=0; row<100; row++) 
 { 
   sum = sum + a[row][col]; 
 } 
} 
           

5.3 switch

switch 是多分支選擇語句,而 if 語句隻有兩個分支可供選擇;雖然可以用嵌套的if 語句來實作多分支選擇,但那樣的程式冗長難讀。這是 switch 語句存在的理由。

switch-case 即使不需要 default 處理,也應該保留語句 default : break; 這樣做并非多此一舉,而是為了防止别人誤以為你忘了 default 處理。确實不需要break的case,務必加上注釋标明。

5.4 goto

很多人建議禁止使用 goto 語句,但實事求是地說,錯誤是程式員自己造成的,不是 goto 的過錯。goto 語句至少有一處可顯神通,它能從多重循環體中一下子跳到外面,特殊場景下可以使用,在很多if嵌套的場景,比如都有同樣的錯誤處理,或者成對操作的檔案開關,或者記憶體申請釋放,就比較适合goto統一處理。

//微信公衆号:嵌入式系統
//代碼隻是表意,可能無法編譯
#include <stdlib.h>

void test(void)
{
    char *p1,*p2;
    p1=(char *)malloc(100);
    p1=(char *)malloc(200);

    if(0)
    {
        //do something
        goto exit;
    }
    else if(0)
    {
        //do something
        goto exit;
    }
    //do something
    //...
exit:
    free(p1);
    free(p2);
}

int main()
{
    goto_test();
    return 0;
}
           

對于記憶體申請釋放、檔案打開關閉這種成對操作,或者各種異常處理的統一支援場景,就比較适合goto。類似的還有do-while(0)這種語句。

關于運算優先級,熟記運算符優先級是比較困難的,如果代碼行中的運算符比較多,為了防止産生歧義并提高可讀性,全部加括号明确表達式的操作順序,雖然愚笨但是可靠。

6 常量

常量是一種辨別符,它的值在運作期間恒定不變。C 語言用 #define 來定義常量(稱為宏常量),但用 const 來定義常量(稱為 const 常量)其實更佳。

#define MAX 100 
const float PI = 3.14159; 
           

const 常量有資料類型,而宏常量沒有資料類型。編譯器可以對前者進行類型安全檢查,而對後者隻進行字元替換,沒有類型安全檢查,并且在字元替換可能會産生意料不到的錯誤,是以複雜參數宏必須為每個參數加上()限制。

但也有特例

const int SIZE = 100; 
 int array[SIZE]; // 有的編譯器認為是錯誤,這就必須用define了
           

需要對外公開的常量放在頭檔案中,不需要對外公開的常量放在定義檔案的頭部。為便于管理,可以把不同子產品的常量集中存放在一個公共的頭檔案中。

7函數

函數設計的細微缺點很容易導緻該函數被錯用,函數接口的兩個要素是參數和傳回值,C 語言中函數的參數和傳回值的傳遞方式有值傳遞(pass by value)和指針傳遞(pass by pointer)兩種。

7.1參數的規則

參數的書寫要完整,不要貪圖省事隻寫參數的類型而省略參數名字,如果函數沒有參數,則用 void 填充。

void set_size(int width, int height); // 良好的風格 
void set_size(int, int); // 不良的風格 
int get_size(void); // 良好的風格 
int get_size(); // 不良的風格 
           

參數命名要恰當,順序要合理。例如字元串拷貝函數

char *strcpy(char* dest, const char *src);
           

從名字上就可以看出應該把 src 拷貝到 dest。還有一個問題,兩個參數哪個該在前哪個該在後?參數的順序要遵循程式員的習慣。一般地,應将目的參數放在前面,源參數放在後面。

這裡也說明下const的意義,如果參數僅作輸入用,則應在類型前加 const,以防止在函數體内被意外修改。

避免函數有太多的參數,參數個數盡量控制在 5 個以内,如果參數太多,在使用時容易将參數類型或順序搞錯,可以定為結構體指針,但盡量帶上參數注釋。

除了printf、sprintf标準庫或基于這類的日志輸出接口,盡量不要使用類型和數目不确定的參數。

7.2 傳回值的規則

不要省略傳回值的類型,預設不加類型說明的函數一律自動按整型處理。為了避免混亂,如果函數沒有傳回值,應聲明為 void 類型。

不要将正常值和錯誤标志混在一起傳回。正常值用輸出參數獲得,而錯誤标志用 return 語句傳回。

7.3 函數内部實作的規則

不同功能的函數其内部實作各不相同,看起來似乎無法就“内部實作”達成一緻的觀點。但根據經驗,我們可以在函數體的“入口處”和“出口處”從嚴把關,進而提高函數的品質。

在函數體的“入口處”,對參數的有效性進行檢查,很多程式錯誤是由非法參數引起的,我們應該充分了解并正确使用“斷言”(assert)來防止此類錯誤。

在函數體的“出口處”,對 return 語句的正确性和效率進行檢查。如果函數有傳回值,那麼函數的“出口處”是 return 語句。調用處應該盡量關注傳回值,對異常進行處理

關于return的值,不可傳回指向“棧記憶體”的“指針,該記憶體在函數體結束時被自動銷毀。例如

char * Func(void) 
 { 
  char str[] = “hello world”; // str 的記憶體位于棧上 
  … 
  return str; // 将導緻錯誤 
 } 
           

盡量避免函數帶有“記憶”功能,相同的輸入應當産生相同的輸出。帶有“記憶”功能的函數,其行為可能是不可預測的,因為它的行為可能取決于某種“記憶狀态”。這樣的函數既不易了解又不利于測試和維護。在 C語言中,函數 的 static 局部變量是函數的“記憶”存儲器。建議盡量少用 static 局部變量,除非必需。

7.4 斷言

程式一般分為 Debug 版本和 Release 版本,Debug 版本用于内部調試,Release 版本發行給使用者使用。斷言 assert 是僅在 Debug 版本起作用的宏,它用于檢查“不應該”發生的情況。在運作過程中,如果 assert 的參數為假,那麼程式就會中止。

void *memcpy(void *pvTo, const void *pvFrom, size_t size) 
{ 
 assert((pvTo != NULL) && (pvFrom != NULL)); // 【使用斷言】 
 byte *pbTo = (byte *) pvTo; // 防止改變 pvTo 的位址 
 byte *pbFrom = (byte *) pvFrom; // 防止改變 pvFrom 的位址 
 while(size -- > 0 ) 
 *pbTo ++ = *pbFrom ++ ; 
 return pvTo; 
}
           

assert 不應該産生任何副作用。是以 assert 不是函數,而是宏。可以把assert 看成一個在任何系統狀态下都可以安全使用的無害測試手段。如果程式在 assert處終止了,并不是說含有該 assert 的函數有錯誤,而是調用者出了差錯,assert 有助于找到發生錯誤的原因。

軟體有必要進行防錯設計,如果“不可能發生”的事情的确發生了,則要使用斷言進行報警。

8 記憶體管理

C語言的記憶體管理既是它的優勢,也是劣勢。了解它的原理了才能更好的管理記憶體。

8.1 記憶體配置設定方式

記憶體配置設定方式有三種:

1、從靜态存儲區域配置設定。記憶體在程式編譯的時候就已經配置設定好,這塊記憶體在程式的整個運作期間都存在。例如全局變量,static 變量。

2、在棧上建立。在執行函數時,函數内局部變量的存儲單元都可以在棧上建立,函數執行結束時這些存儲單元自動被釋放。棧記憶體配置設定運算内置于處理器的指令集中,效率很高,但是配置設定的記憶體容量有限。

3、從堆上配置設定,亦稱動态記憶體配置設定。程式在運作的時候用 malloc 或 new 申請任意多少的記憶體,程式員自己負責在何時用 free 或 delete 釋放記憶體。動态記憶體的生存期由我們決定,使用非常靈活,但風險也大。

8.2 記憶體錯誤及其對策

發生記憶體錯誤是件非常麻煩的事情。編譯器不能自動發現這些錯誤,通常是在程式運作時才能捕捉到,而這些錯誤大多沒有明顯的症狀,時隐時現,增加了改錯的難度。常見的記憶體錯誤及其對策如下:

1、記憶體配置設定未成功,卻使用了它

程式設計新手常犯這種錯誤,因為他們沒有意識到記憶體配置設定會不成功。常用解決辦法是,在使用記憶體之前檢查指針是否為 NULL。如果指針 p 是函數的參數,可在函數的入口處用 assert(p!=NULL)進行檢查,或者用 if(p==NULL) 或 if(p!=NULL)進行防錯處理。

2、記憶體配置設定雖然成功,但是尚未初始化就引用它

犯這種錯誤主要有兩個起因:一是沒有初始化的觀念;二是誤以為記憶體的預設初值全為零,導緻引用初值錯誤。記憶體的預設初值究竟是什麼并沒有統一的标準(盡管有些時候為零值),為了安全,對配置設定的記憶體都進行清零。

3、記憶體配置設定成功并且已經初始化,但操作越過了記憶體的邊界

數組使用時經常會發生下标“多 1”或“少 1”的操作。特别是在 for 循環語句中,循環次數很容易搞錯,導緻數組操作越界。

4、忘記釋放記憶體,造成記憶體洩露

含有這種錯誤的函數每被調用一次就丢失一塊記憶體。剛開始時系統的記憶體充足,運作正常,但随着運作時間加長,程式突然死掉,記憶體耗盡。動态記憶體的申請與釋放必須配對,程式中 malloc 與 free 的成對使用。

5、已經釋放的記憶體卻繼續使用它

程式中的調用關系過于複雜,邏輯順序錯誤,或者使用了指向“棧記憶體”的“臨時指針,使用 free 或 delete 釋放了記憶體後,務必将指針設定為 NULL,使用前判斷是否為NULL。

關于指針的使用建議,用 malloc 申請記憶體之後,應該立即檢查指針值是否為 NULL,非NULL的賦初值;使用結束後用 free 釋放,且将指針設定為 NULL,防止誤用“野指針”。對動态記憶體的一些防護性操作,可以參考微信公衆号【嵌入式系統】的文章動态記憶體管理及防禦性程式設計。

8.3 指針與數組的對比

C 程式中指針和數組在不少地方可以互相替換着用,讓人産生一種錯覺,以 為兩者是等價的。

數組要麼在靜态存儲區被建立(如全局數組),要麼在棧上被建立。數組名對應着(而不是指向)一塊記憶體,其位址與容量在生命期内保持不變,隻有數組的内容可以改變。

指針可以随時指向任意類型的記憶體塊,它的特征是“可變”,是以我們常用指針來操作動态記憶體。指針遠比數組靈活,但也更危險。

下面以字元串為例比較指針與數組的特性。

1、修改内容

字元數組 a 的容量是 6 個字元,其内容為 hello\0。a 的内容可以改變,如 a[0]= ‘X’。指針 p 指向常量字元串“world”(位于靜态存儲區,内容為 world\0),常量字元串的内容是不可以被修改的。從文法上看,編譯器并不覺得語句 p[0]= ‘X’有什麼不妥,但是該語句企圖修改常量字元串的内容而導緻運作錯誤。

char a[] = “hello”; 
a[0] = ‘X’; 
cout << a << endl; 
char *p = “world”; // 注意 p 指向常量字元串 
p[0] = ‘X’; // 編譯器不能發現該錯誤 
cout << p << endl;
           

2、 内容複制與比較

不能對數組名進行直接複制與比較,若想把數組 a 的内容複制給數組 b,不能用語句 b = a ,否則将産生編譯錯誤。應該用标準庫函數 strcpy 進行複制。同理,比較 b 和 a 的内容是否相同,不能用 if(b == a) 來判斷,應該用标準庫函數 strcmp進行比較。

語句 p = a 并不能把 a 的内容複制指針 p,而是把 a 的位址賦給了 p。要想複制 a的内容,可以先用庫函數 malloc 為 p 申請一塊容量為 strlen(a)+1 個字元的記憶體,再用 strcpy 進行字元串複制。同理,語句 if(p==a) 比較的不是内容而是位址,應該用庫函數 strcmp 來比較。

// 數組 
 char a[] = "hello"; 
 char b[10]; 
 strcpy(b, a); // 不能用 b = a; 
 if(strcmp(b, a) == 0 )  // 不能用 if ( b ==  a) 

 // 指針
 int len = strlen(a); 
 char *p = (char *)malloc(sizeof(char)*(len+1)); 
 strcpy(p,a); // 不要用 p = a; 
 if(strcmp(p, a) == 0) // 不要用 if (p == a) 
           

3、計算記憶體容量

用運算符 sizeof 可以計算出數組的容量(位元組數)。sizeof(a)的值是 12(注意别忘了’\0’)。指針 p 指向 a,但是 sizeof(p)的值卻是 4。這是因為sizeof(p)得到的是一個指針變量的位元組數,相當于 sizeof(char*),而不是 p 所指的記憶體容量。/C 語言沒有辦法知道指針所指的記憶體容量,隻能在申請記憶體時記住它。

char a[] = "hello world"; 
 char *p = a; 
 cout<< sizeof(a) << endl; // 12 位元組 
 cout<< sizeof(p) << endl; // 4 位元組 
           

當數組作為函數的參數進行傳遞時,該數組自動退化為同類型的指針。不論數組 a 的容量是多少,sizeof(a)始終等于 sizeof(char *)。

void Func(char a[100]) 
 { 
    cout<< sizeof(a) << endl; // 4 位元組而不是 100 位元組 
} 
           

4、指針參數是如何傳遞記憶體

如果函數的參數是一個指針,不要指望用該指針去申請動态記憶體。

void get_memory(char *p, int num) 
{ 
 p = (char *)malloc(sizeof(char) * num); 
} 
void test(void) 
{ 
 char *str = NULL; 
 get_memory(str, 100); // str 仍然為 NULL 
 strcpy(str, "hello"); // 運作錯誤 
} 
           

test 函數的get_memory(str, 100) 并沒有使 str 獲得期望的記憶體,str 依舊是 NULL,為什麼?

問題出在函數 get_memory,編譯器總是要為函數的每個參數制作臨時副本,指針參數 p 的副本是 _p,編譯器使 _p = p。如果函數體内的程式修改了_p 的内容,就導緻參數 p 的内容作相應的修改。這就是指針可以用作輸出參數的原因。而範例中_p 申請了新的記憶體,隻是把_p 所指的記憶體位址改變了,但是 p 絲毫未變。是以函數 get_memory并不能輸出任何東西。事實上,每執行一次 get_memory就會洩露一塊記憶體,因為沒有用free 釋放記憶體。

如果非得要用指針參數去申請記憶體,那麼應該改用“指向指針的指針”,正确範例如下:

void get_memory2(char **p, int num) 
{ 
 *p = (char *)malloc(sizeof(char) * num); 
}
void test2(void) 
{ 
 char *str = NULL; 
 get_memory2(&str, 100); // 注意參數是 &str,而不是 str 
 strcpy(str, "hello"); 
 free(str); 
} 
           

由于“指向指針的指針”這個概念不容易了解,可以用函數傳回值來傳遞動态記憶體,這種方法更加簡單。

char *get_memory3(int num) 
{ 
 char *p = (char *)malloc(sizeof(char) * num); 
 return p; 
}
void test3(void) 
{ 
 char *str = NULL; 
 str = get_memory3(100); 
 //建議增加str指針是否為NULL判斷,并清零内容
 strcpy(str, "hello"); 
 free(str); 
} 
           

用函數傳回值來傳遞動态記憶體這種方法雖然好用,但是常常有人把 return 語句用錯,不要用 return 語句傳回指向“棧記憶體”的指針,因為該記憶體在函數結束時自動消亡,錯誤範例如下:

//錯誤範例
char *get_string(void) 
{ 
 char p[] = "hello world"; 
 return p; // 編譯器将提出警告 
} 
void test4(void) 
{ 
 char *str = NULL; 
 str = get_string(); // str 的内容是随機垃圾
} 
           

執行str = get_string()後 str 不再是 NULL 指針,但是 str 的内容不是“hello world”而是垃圾。

char *get_string2(void) 
{ 
 char *p = "hello world"; 
 return p; 
} 
void test5(void) 
{ 
 char *str = NULL; 
 str = get_string2(); 
} 
           

函數 test5 運作雖然不會出錯,但是函數 get_string2的設計概念卻是錯誤的。因為 get_string2内的“hello world”是常量字元串,位于靜态存儲區,它在程式生命期内恒定不變。無論什麼時候調用 get_string2,它傳回的始終是同一個“隻讀”的記憶體塊,也就是test5是無法修改str的。

5、 free 把指針怎麼了

free 隻是把指針所指的記憶體給釋放掉,但并沒有把指針本身幹掉;指針 p 被 free 以後其位址仍然不變(非 NULL),隻是該位址對應的記憶體是垃圾,p 成了“野指針”。如果此時不把 p 設定為 NULL,會讓人誤以為 p 是個合法的指針。

如果程式比較長,我們有時記不住 p 所指的記憶體是否已經被釋放,在繼續使用 p 之前,通常會用語句 if (p != NULL)進行防錯處理。很遺憾,此時 if 語句起不到防錯作用,此時 p 不是 NULL 指針,但它也不指向合法的記憶體塊。

char *p = (char *) malloc(100); 
strcpy(p, “hello”); 
free(p); // p 所指的記憶體被釋放,但是 p 所指的位址仍然不變 

if(p != NULL) // 沒有起到防錯作用 
{ 
 strcpy(p, “world”); // 出錯 
} 
           

6、動态記憶體會被自動釋放嗎

函數體内的局部變量在函數結束時自動消亡。

void func(void) 
{ 
 char *p = (char *) malloc(100); // 動态記憶體會自動釋放嗎? 
}
           

但是,變量p 是局部的指針變量,它消亡的時候并不會讓它所指的動态記憶體一起完蛋。發現指針有一些“似是而非”的特征:

(1)指針消亡了,并不表示它所指的記憶體會被自動釋放。

(2)記憶體被釋放了,并不表示指針會消亡或者成了 NULL 指針。

7、杜絕“野指針”

“野指針”不是 NULL 指針,是指向“垃圾”記憶體的指針。人們一般不會錯用 NULL指針,因為用 if 語句很容易判斷;但是“野指針”是很危險的,if 語句對它不起作用。“野指針”的成因主要有三種:

(1)指針變量沒有被初始化。任何指針變量剛被建立時不會自動成為 NULL 指針,它的預設值是随機的,是以,指針變量在建立的同時應當被初始化。

(2)指針 p 被 free 或者 delete 之後,沒有置為 NULL,讓人誤以為 p 是個合法的指針。

(3)指針操作超越了變量的作用範圍。這種情況讓人防不勝防。

8、記憶體耗盡怎麼辦

如果在申請動态記憶體時找不到足夠大的記憶體塊,malloc 将傳回 NULL 指針, 宣告記憶體申請失敗。判斷指針是否為 NULL,如果是則馬上用 return 語句終止本函數,或者用 exit(1)終止整個程式的運作。如果發生“記憶體耗盡”,一般說來應用程式已經無藥可救,嵌入式裝置隻能重新開機了。

9、心得體會

很少有人能拍拍胸脯說通曉指針與記憶體管理,越是怕指針,就越要使用指針。不會正确使用指針,肯定算不上是合格的嵌入式程式員。

9 其它程式設計經驗

9.1 使用 const 提高函數的健壯性

const 是 constant 的縮寫,“恒定不變”的意思。被 const 修飾的東西都受到強制保護,可以預防意外的變動,能提高程式的健壯性。很多 C++程式設計書籍建議:“Use const whenever you need”。

1、用 const 修飾函數的參數 如果參數作輸出用,不論它是什麼資料類型,都不能加 const 修飾,否則該參數将失去輸出功能。const 隻能修飾輸入參數,如果輸入參數采用“指針傳遞”,那麼加 const 修飾可以防止意外地改動該指針,起到保護作用。例如 strcpy函數:

char *strcpy(char* dest, const char *src);
           

其中 src是輸入參數,dest是輸出參數。給 src加上 const修飾後,如果函數體内的語句試圖改動 src 的内容,編譯器将指出錯誤。

2、如果輸入參數采用“值傳遞”,由于函數将自動産生臨時變量用于複制該參數,該輸入參數本來就無需保護,是以不要加 const 修飾。

void func1(int x) 寫成 void func1(const int x)  //const無意義
           

3、對于非内部資料類型的參數而言,如 void func(A a) 這樣聲明的函數注定效率比較低,其中 A 為使用者自定義的資料類型,可以了解為大結構。

函數體内将産生 A 類型的臨時對象用于複制參數 a,而臨時對象的構造、 複制、析構過程都将消耗時間。為了提高效率,可以将函數聲明改為:

void func(A &a)
           

因為“引用傳遞”僅借用一下參數的别名而已,不需要産生臨時對象。但是函數 存在一個缺點,“引用傳遞”有可能改變參數 a,這是我們不期望的。解決這個問題很容易,加 const修飾即可,是以函數最終成為

void func(const A &a)
           

4、用 const 修飾函數的傳回值,如果給以“指針傳遞”方式的函數傳回值加 const 修飾,那麼函數傳回值(即指針)的内容不能被修改,該傳回值隻能被賦給加 const 修飾的同類型指針。例如函數

const char * get_string(void); 
 char *str = get_string(); //出現編譯錯誤: 
 const char *str = get_string(); //正确的用法
           

9.2 提高程式的效率

程式的時間效率是指運作速度,空間效率是指程式占用記憶體或者外存的狀況。

不要一味地追求程式的效率,應當在滿足正确性、可靠性、健壯性、可讀性等品質因素的前提下,設法提高程式的效率。

在優化程式的效率時,應當先找出限制效率的“瓶頸”,不要在無關緊要之處優化。有時候時間效率和空間效率可能對立,此時應當分析那個更重要,作出适當的折衷。例如多花費一些記憶體來提高性能。

關于其它C關鍵字用法,可以參考 C語言關鍵字應用技巧。

10 小結

不論劍宗、氣宗優劣,先把功能跑通再反推代碼原理和實作流程,還是先理清時序和原理再編碼實作功能,短期内劍宗效率高,加工資快,但後期發展有限;氣宗則面臨前期可能被淘汰,尤其在勢利的小公司,不注重新人培養,但前期積累,後期融會貫通,在技術方面成為權威。如果合二為一,項目緊急則拿來就用,空閑時專研總結,取長補短,則是進階程式員的素質。

轉載自:嵌入式系統

文章來源于高品質嵌入式軟體的開發技巧

原文連結:https://mp.weixin.qq.com/s/5q5VAdwVTPR0Tw5XQWcu0w

繼續閱讀