摘要
本文首先分析了C語言的陷阱和缺陷,對容易犯錯的地方進行歸納整理;分析了編譯器語義檢查的不足之處并給出防范措施,以Keil MDK編譯器為例,介紹了該編譯器的特性、對未定義行為的處理以及一些高級應(yīng)用;在此基礎(chǔ)上,介紹了防御性編程的概念,提出了編程過程中就應(yīng)該防范于未然的多種措施;提出了測試對編寫優(yōu)質(zhì)嵌入式程序的重要作用以及常用測試方法;最后,本文試圖以更高的層次看待編程,討論一些通用的編程思想。
1 編程風(fēng)格
《計算機程序的構(gòu)造和解釋》一書在開篇寫到:程序?qū)懗鰜硎墙o人看的,附帶能在機器上運行。
1.1 整潔的樣式
使用什么樣的編碼樣式一直都頗具爭議性的,比如縮進和大括號的位置。因為編碼的樣式也會影響程序的可讀性,面對一個亂放括號、對齊都不一致的源碼,我們很難提起閱讀它的興趣。我們總要看別人的程序,如果彼此編碼樣式相近,讀起源碼來會覺得比較舒適。但是編碼風(fēng)格的問題是主觀的,永遠不可能在編碼風(fēng)格上達成統(tǒng)一意見。因此只要你的編碼樣式整潔、結(jié)構(gòu)清晰就足夠了。除此之外,對編碼樣式再沒有其它要求。
提出匈牙利命名法的程序員、前微軟首席架構(gòu)師Charles Simonyi說:我覺得代碼清單帶給人的愉快同整潔的家差不多。你一眼就能分辨出家里是雜亂無章還是整潔如新。這也許意義不大。因為光是房子整潔說明不了什么,它仍可能藏污納垢!但是第一印象很重要,它至少反映了程序的某些方面。我敢打賭,我在3米開外就能看出程序拙劣與否。我也許沒法保證它很不錯,但如果從3米外看起來就很糟,我敢保證這程序?qū)懙貌挥眯?。如果寫得不用心,那它在邏輯上也許就不會優(yōu)美。
1.2 清晰的命名
變量、函數(shù)、宏等等都需要命名,清晰的命名是優(yōu)秀代碼的特點之一。命名的要點之一是名稱應(yīng)能清晰的描述這個對象,以至于一個初級程序員也能不費力的讀懂你的代碼邏輯。我們寫的代碼主要給誰看是需要思考的:給自己、給編譯器還是給別人看?我覺得代碼最主要的是給別人看,其次是給自己看。如果沒有一個清晰的命名,別人在維護你的程序時很難在整個全貌上看清代碼,因為要記住十多個以上的糟糕命名的變量是件非常困難的事;而且一段時間之后你回過頭來看自己的代碼,很有可能不記得那些糟糕命名的變量是什么意思。
為對象起一個清晰的名字并不是簡單的事情。首先能認識到名稱的重要性需要有一個過程,這也許跟譚式C程序教材被大學(xué)廣泛使用有關(guān):滿書的a、b、c、x、y、z變量名是很難在關(guān)鍵的初學(xué)階段給人傳達優(yōu)秀編程思想的;其次如何恰當(dāng)?shù)臑閷ο竺埠苡刑魬?zhàn)性,要準(zhǔn)確、無歧義、不羅嗦,要對英文有一定水平,所有這些都要滿足時,就會變得很困難;此外,命名還需要考慮整體一致性,在同一個項目中要有統(tǒng)一的風(fēng)格,堅持這種風(fēng)格也并不容易。
關(guān)于如何命名,Charles Simonyi說:面對一個具備某些屬性的結(jié)構(gòu),不要隨隨便便地取個名字,然后讓所有人去琢磨名字和屬性之間有什么關(guān)聯(lián),你應(yīng)該把屬性本身,用作結(jié)構(gòu)的名字。
1.3 恰當(dāng)?shù)淖⑨?/p>
注釋向來也是爭議之一,不加注釋和過多的注釋我都是反對的。不加注釋的代碼顯然是很糟糕的,但過多的注釋也會妨礙程序的可讀性,由于注釋可能存在的歧義,有可能會誤解程序真實意圖,此外,過多的注釋會增加程序員不必要的時間。如果你的編碼樣式整潔、命名又很清晰,那么,你的代碼可讀性不會差到哪去,而注釋的本意就是為了便于理解程序。
這里建議使用良好的編碼樣式和清晰的命名來減少注釋,對模塊、函數(shù)、變量、數(shù)據(jù)結(jié)構(gòu)、算法和關(guān)鍵代碼做注釋,應(yīng)重視注釋的質(zhì)量而不是數(shù)量。如果你需要一大段注釋才能說清楚程序做什么,那么你應(yīng)該注意了:是否是因為程序變量命名不夠清晰,或者代碼邏輯過于混亂,這個時候你應(yīng)該考慮的可能就不是注釋,而是如何精簡這個程序了。
2 數(shù)據(jù)結(jié)構(gòu)
數(shù)據(jù)結(jié)構(gòu)是程序設(shè)計的基礎(chǔ)。在設(shè)計程序之前,應(yīng)該先考慮好所需要的數(shù)據(jù)結(jié)構(gòu)。
前微軟首席架構(gòu)師Charles Simonyi:編程的第一步是想象。就是要在腦海中對來龍去脈有極為清晰的把握。在這個初始階段,我會使用紙和鉛筆。我只是信手涂鴉,并不寫代碼。我也許會畫些方框或箭頭,但基本上只是涂鴉,因為真正的想法在我腦海里。我喜歡想象那些有待維護的結(jié)構(gòu),那些結(jié)構(gòu)代表著我想編碼的真實世界。一旦這個結(jié)構(gòu)考慮得相當(dāng)嚴(yán)謹和明確,我便開始寫代碼。我會坐到終端前,或者換在以前的話,就會拿張白紙,開始寫代碼。這相當(dāng)容易。我只要把頭腦中的想法變換成代碼寫下來,我知道結(jié)果應(yīng)該是什么樣的。大部分代碼會水到渠成,不過我維護的那些數(shù)據(jù)結(jié)構(gòu)才是關(guān)鍵。我會先想好數(shù)據(jù)結(jié)構(gòu),并在整個編碼過程中將它們牢記于心。
開發(fā)過以太網(wǎng)和操作系統(tǒng)SDS 940的Butler Lampson:(程序員)最重要的素質(zhì)是能夠把問題的解決方案組織成容易操控的結(jié)構(gòu)。
開發(fā)CP/M操作系統(tǒng)的Gary.A:如果不能確認數(shù)據(jù)結(jié)構(gòu)是正確的,我是決不會開始編碼的。我會先畫數(shù)據(jù)結(jié)構(gòu),然后花很長時間思考數(shù)據(jù)結(jié)構(gòu)。在確定數(shù)據(jù)結(jié)構(gòu)之后我就開始寫一些小段的代碼,并不斷地改善和監(jiān)測。在編碼過程中進行測試可以確保所做的修改是局部的,并且如果有什么問題的話,能夠馬上發(fā)現(xiàn)。
微軟創(chuàng)始人比爾**·**蓋茨:編寫程序最重要的部分是設(shè)計數(shù)據(jù)結(jié)構(gòu)。接下來重要的部分是分解各種代碼塊。
編寫世界上第一個電子表格軟件的Dan Bricklin:在我看來,寫程序最重要的部分是設(shè)計數(shù)據(jù)結(jié)構(gòu),此外,你還必須知道人機界面會是什么樣的。
我們舉個例子來說明。在介紹防御性編程的時候,提到公司使用的LCD顯示屏抗干擾能力一般,為了提高LCD的穩(wěn)定性,需要定期讀出LCD內(nèi)部的關(guān)鍵寄存器值,然后跟存在Flash中的初始值相比較。需要讀出的LCD寄存器有十多個,從每個寄存器讀出的值也不盡相同,從1個到8個字節(jié)都有可能。如果不考慮數(shù)據(jù)結(jié)構(gòu),編寫出的程序?qū)苋唛L。
void lcd_redu(void)
{ 。
讀第一個寄存器值;
if(第一個寄存器值==Flash存儲值)
{
讀第二個寄存器值;
if(第二個寄存器值==Flash存儲值)
{
。..
讀第十個寄存器值;
if(第十個寄存器值==Flash存儲值)
{
返回;
}
else
{
重新初始化LCD;
}
}
else
{
重新初始化LCD;
}
}
else
{
重新初始化LCD;
}
}
我們分析這個過程,發(fā)現(xiàn)能提取出很多相同的元素,比如每次讀LCD寄存器都需要該寄存器的命令號,都會經(jīng)過讀寄存器、判斷值是否相同、處理異常情況這一過程。所以我們可以提取一些相同的元素,組織成數(shù)據(jù)結(jié)構(gòu),用統(tǒng)一的方法去處理這些數(shù)據(jù),將數(shù)據(jù)與處理過程分開來。
我們可以先提取相同的元素,將之組織成數(shù)據(jù)結(jié)構(gòu):
這里lcd_command表示的是LCD寄存器命令號;lcd_get_value是一個數(shù)組,表示寄存器要初始化的值,這是因為對于一個LCD寄存器,可能要初始化多個字節(jié),這是硬件特性決定的;lcd_value_num是指一個寄存器要多少個字節(jié)的初值,這是因為每一個寄存器的初值數(shù)目是不同的,我們用同一個方法處理數(shù)據(jù)時,是需要這個信息的。
就本例而言,我們將要處理的數(shù)據(jù)都是事先固定的,所以定義好數(shù)據(jù)結(jié)構(gòu)后,我們可以將這些數(shù)據(jù)組織成表格:
/*LCD部分寄存器設(shè)置值列表*/
lcd_redu_list_struct const lcd_redu_list_str[]= {
{SSD1963_Get_Address_Mode,{0x20}
,1}, /*1*/
{SSD1963_Get_Pll_Mn
,{0x3b,0x02,0x04}
,3}, /*2*/
{SSD1963_Get_Pll_Status
,{0x04}
,1}, /*3*
{SSD1963_Get_Lcd_Mode
,{0x24,0x20,0x01,0xdf,0x01,0x0f,0x00}
,7}, /*4*/
{SSD1963_Get_Hori_Period ,{0x02,0x0c,0x00,0x2a,0x07,0x00,0x00,0x00},8}, /*5*/
{SSD1963_Get_Vert_Period ,{0x01,0x1d,0x00,0x0b,0x09,0x00,0x00}
,7}, /*6*/ {SSD1963_Get_Power_Mode ,{0x1c}
,1}, /*7*/ {SSD1963_Get_Display_Mode,{0x03}
,1}, /*8*/ {SSD1963_Get_Gpio_Conf ,{0x0F,0x01}
,2}, /*9*/ {SSD1963_Get_Lshift_Freq ,{0x00,0xb8}
,2}, /*10* };
至此,我們就可以用一個處理過程來完成數(shù)十個LCD寄存器的讀取、判斷和異常處理了:
/** * lcd 顯示冗余
* 每隔一段時間調(diào)用該程序一次 */ void lcd_redu(void) {
uint8_t tmp[8];
uint32_t i,j;
uint32_t lcd_init_flag;
lcd_init_flag =0;
for(i=0;i《sizeof(lcd_redu_list_str)/sizeof(lcd_redu_list_str[0]);i++)
{
LCD_SendCommand(lcd_redu_list_str[i].lcd_command);
uyDelay(10);
for(j=0;j《lcd_redu_list_str[i].lcd_value_num;j++)
{
tmp[j]=LCD_ReadData();
if(tmp[j]!=lcd_redu_list_str[i].lcd_get_value[j])
{
lcd_init_flag=0x55;
//一些調(diào)試語句,打印出錯的具體信息
goto handle_lcd_init;
}
}
}
handle_lcd_init:
if(lcd_init_flag==0x55)
{
//重新初始化LCD
//一些必要的恢復(fù)措施
}
}
通過合理的數(shù)據(jù)結(jié)構(gòu),我們可以將數(shù)據(jù)和處理過程分開,LCD冗余判斷過程可以用很簡潔的代碼來實現(xiàn)。更重要的是,將數(shù)據(jù)和處理過程分開更有利于代碼的維護。比如,通過實驗發(fā)現(xiàn),我們還需要增加一個LCD寄存器的值進行判斷,這時候只需要將新增加的寄存器信息按照數(shù)據(jù)結(jié)構(gòu)格式,放到LCD寄存器設(shè)置值列表中的任意位置即可,不用增加任何處理代碼即可實現(xiàn)!這僅僅是數(shù)據(jù)結(jié)構(gòu)的優(yōu)勢之一,使用數(shù)據(jù)結(jié)構(gòu)還能簡化編程,使復(fù)雜過程變的簡單,這個只有實際編程后才會有更深的理解。
審核編輯 :李倩
-
C語言
+關(guān)注
關(guān)注
180文章
7581瀏覽量
135589 -
嵌入式開發(fā)
+關(guān)注
關(guān)注
18文章
1012瀏覽量
47431
原文標(biāo)題:嵌入式開發(fā)中的C語言編程思想
文章出處:【微信號:c-stm32,微信公眾號:STM32嵌入式開發(fā)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論