關(guān)于C語言枚舉類型不得不說的故事
經(jīng)濟(jì)學(xué)家說過,路邊是不會(huì)有100元的,但是如果有,你還是要撿起來。
本文引用地址:http://2s4d.com/article/202005/413512.htm同理,在貌似萬物免費(fèi)的網(wǎng)絡(luò)時(shí)代,你是很難找到有針對(duì)性的好資料的,但是如果有,希望你能認(rèn)真學(xué)習(xí)吸收。
比如筆者今天寫的這一篇:)
一
今天這篇文章要分享兩個(gè)案例,第一個(gè)案例關(guān)于枚舉,第二個(gè)案例也是關(guān)于枚舉。
照舊例,先來幾句簡(jiǎn)單的照本宣科。C語言枚舉類型用于針對(duì)某一類對(duì)象定義一個(gè)集合,根據(jù)該類對(duì)象的實(shí)際意義將集合中的元素逐一列舉出來,然后用實(shí)際取值為整數(shù)(枚舉值)的文本式變量描述這些元素。
這些枚舉值相當(dāng)于一種助記符,可以提供對(duì)某一類對(duì)象更加貼近實(shí)際的描述,所以不僅能夠增加程序的可讀性,還能幫助碼農(nóng)們分別并記憶它們。當(dāng)然,在具體的編程活動(dòng)中,枚舉型也會(huì)暫時(shí)把碼農(nóng)從枯燥的計(jì)算機(jī)世界解脫出來,找回一點(diǎn)人間煙火的感覺。
科普完畢,大家可能開始納悶了。既然從數(shù)學(xué)概念上來理解,枚舉定義了一個(gè)“集合”,用整型取值來表示集合中的“元素”,邏輯上如此清晰而且簡(jiǎn)單,這還可能出什么問題?
你想,平地里可以起驚雷,陰溝里也會(huì)翻了船,編程寫出個(gè)bug來,難道不是意料之外、情理之中的事情嗎?
只不過,我始終搞不清楚,編程時(shí),到底一帆風(fēng)順無驚無喜是幸福的,還是遇到問題百轉(zhuǎn)千回更幸福?
說到幸福,我不禁想起范偉的一段經(jīng)典臺(tái)詞,腦袋大脖子粗的范偉端著個(gè)大臉盤子,無神的眼睛里透露著看破紅塵的滄桑,慢條斯理地回答:“什么是幸福?幸福就是我餓了,看別人拿個(gè)肉包子,那他就比我幸福;我冷了,看別人穿了一件厚棉襖,他就比我幸福;我想上茅房,就一個(gè)坑,你蹲那了,你就比我幸福?!?/p>
同樣是簡(jiǎn)單的枚舉,你用時(shí)沒碰到問題,而我碰上了,你說咱倆到底誰比誰幸福?
二
道家有一句很玄妙的話:天下本無事,庸人自擾之!
堅(jiān)定地秉持唯物主義的四有青年們對(duì)這句話當(dāng)然是嗤之以鼻孔兼鼻毛的。
你見或者不見,事兒就在那里,不來不去,但是按照老莊的思想,合著是我們自己沒事找事了?
對(duì)此等斷語,筆者只能微微一笑很傾城,接著苦笑很悲情了。因?yàn)槲矣龅降拿杜e問題就是自己瞎搞出來的。
本來,同事小周給我的代碼里有這么兩段代碼:
void SendI2cAck(void)
{
SetSdaDir(IO_DIR_OUTPUT);
SetSdaLow();
ToogleScl();
}
void SendI2cNak(void)
{
SetSdaDir(IO_DIR_OUTPUT);
SetSdaHigh();
ToogleScl();
}
明眼人一眼就看出來了,盡管每段代碼都很簡(jiǎn)單,完全沒有必要改寫,但是由于這兩段代碼的重復(fù)度很高,它們完全可以改寫成一個(gè)帶參量的函數(shù)。
尤其對(duì)我們這種對(duì)代碼清理和重構(gòu)有著偏執(zhí)型沖動(dòng)的人來說,讓我們不重構(gòu)簡(jiǎn)直比殺了我們還難受,此時(shí)不改,更待可時(shí)?
于是我三下五除二,把代碼改成了下面的樣子:
void i2c_ack(e_I2cAck ack)
{
SetSdaDir(IO_DIR_OUTPUT);
if(I2C_ACK == ack){
SetSdaLow();
}else{
SetSdaHigh();
}
ToogleScl();
}
在這里,筆者定義了一個(gè)枚舉類型:
typedef enum{
I2C_ACK = 0,
I2C_NAK = 1
}e_I2cAck;
然后,因?yàn)楣聿胖赖脑颍P者給出了如下函數(shù)聲明,也在不經(jīng)意間埋下了一顆炸彈。
void i2c_ack(uint8_t ack);
看到這里,大咖們可能在捏著下巴上唏噓的胡茬子會(huì)心一笑了,但是小白們也許還是不知所以。
盡管函數(shù)的聲明誤寫成了i2c_ack(uint8_t ack),但是它的定義i2c_ack(e_I2cAck ack)還是對(duì)的,在調(diào)用函數(shù)傳遞函數(shù)參量的過程中,傳進(jìn)去的I2C_ACK難道不還是0,I2C_NAK不還是1吧?
筆者也是這么想的,當(dāng)然,剛開始的時(shí)候,我根本沒有發(fā)現(xiàn)把聲明寫錯(cuò)的“筆誤”。
不過,埋下的炸彈終會(huì)暴雷。由于重構(gòu)后的程序運(yùn)行不正常,我很快發(fā)現(xiàn)了聲明和定義不一致,但是,so what?我依然不得要領(lǐng),于是只好架上仿真器單步調(diào)試,看看到底會(huì)發(fā)生什么。
我追蹤調(diào)試到調(diào)用i2c_ack的地方,眼見著把I2C_ACK=0傳了進(jìn)去,到了函數(shù)里面后,竟然沒有執(zhí)行if(I2C_ACK == ack)這個(gè)分支。于是我試著添加了一個(gè)uint16_t型的臨時(shí)變量,將函數(shù)參量賦值給它。
不看不知道,一看嚇一跳,傳遞進(jìn)來的參量竟然成了0x5A00。
追蹤到這里,又查閱了相關(guān)資料后,我似乎有些開竅了。
盡管8位整型便可以涵蓋這次枚舉定義中的最大值,但是枚舉類型的尺寸是16位,而非所想象的8位。
這樣一來,如果函數(shù)聲明中的參量是16位,那么,在參量傳遞時(shí),傳遞進(jìn)來的枚舉類型的I2C_ACK會(huì)被處理成16位整型的‘0’,函數(shù)會(huì)按照‘0’分支進(jìn)行正確的處理。但是,由于函數(shù)聲明中的參量是8位,所以,實(shí)際上傳遞進(jìn)來的枚舉類型的I2C_ACK只取了1個(gè)8位整型的‘0’,進(jìn)入函數(shù)內(nèi)部后,它又會(huì)被擴(kuò)展成16位整型,而函數(shù)內(nèi)部的變量是局部變量,地址空間都在stack里面,它擴(kuò)展時(shí)會(huì)采用相鄰的高位地址來填充該16位整型的高8位。這樣,在傳遞0時(shí),數(shù)據(jù)低八位依然是0,但是高八位就不一定了。
本來不改程序,還不會(huì)遇到這些問題,看看,是不是天下本無事,庸人自擾之?
千百年來,多少人苦苦思索,到底是什么力量,掌握著我們的命運(yùn),讓我們經(jīng)歷痛苦和歡樂?
現(xiàn)在我明白了,生命不息,折騰不止,正是這種沒事找事瞎折騰的力量主宰了我們的喜怒哀樂呀!
三
筆者分享的第二個(gè)關(guān)于枚舉類型的案例,是更加便利地使用枚舉類型進(jìn)行數(shù)組索引的一種新用法,不敢藏私,與諸君共享之。
如前所述,枚舉的一個(gè)重要作用是增加程序的可讀性,以助記符的形式幫助程序員記憶和理解代碼。比如,筆者在實(shí)現(xiàn)軟件定時(shí)器時(shí)(見文章《如何用單個(gè)定時(shí)器統(tǒng)一地實(shí)現(xiàn)多種定時(shí)應(yīng)用》)就曾經(jīng)以枚舉類型定義了軟件定時(shí)器的ID或者說軟件定時(shí)器的名稱。
為了讓讀者更加便于理解,還是要花開兩朵各表一枝,叨咕叨咕軟件定時(shí)器。
一個(gè)嵌入式產(chǎn)品中會(huì)有很多定時(shí)邏輯,最好也是最通用的處理方式便是設(shè)計(jì)一種結(jié)構(gòu)體形式的軟件定時(shí)器,令一個(gè)軟件定時(shí)器對(duì)應(yīng)一種定時(shí)邏輯,所有軟件定時(shí)器構(gòu)成一個(gè)結(jié)構(gòu)體數(shù)組,各種定時(shí)邏輯的實(shí)現(xiàn)時(shí)便是在結(jié)構(gòu)體數(shù)組中的成員變量上進(jìn)行處理。
在這里,以可讀性較強(qiáng)的枚舉類型定義軟件定時(shí)器的ID,枚舉值根據(jù)各個(gè)定時(shí)應(yīng)用的具體邏輯命名,比如說,檢測(cè)輸入信號(hào)的周期性定時(shí)器INPUT_DETECT_PTMR、喂看門狗的周期性定時(shí)器FEED_WATCHDOG_PTMR、監(jiān)測(cè)系統(tǒng)狀態(tài)的周期性定時(shí)器SYS_MONITOR_PTMR、蜂鳴器報(bào)警的多次定時(shí)器BEEPTWEET_TTMR、總線busoff后恢復(fù)通信的單次定時(shí)器BUSOFF_TTMR等。
高智商的程序猿們打眼一看,就能從枚舉值的命名上看出定時(shí)器背后的邏輯來,枚舉增強(qiáng)程序可讀性的功能可見一斑,但是,問題是,您老人家看明白了,單片機(jī)呢?
這么說吧,我們?cè)谟肨imer[INPUT_DETECT_PTMR]處理定時(shí)邏輯時(shí),怎么保證這個(gè)定時(shí)器節(jié)點(diǎn)就能具體對(duì)應(yīng)到檢測(cè)輸入信號(hào)的周期性定時(shí)器嗎?
智商在線的你肯定不會(huì)因?yàn)镮NPUT_DETECT_PTMR這個(gè)文本化的枚舉寫得如此得昭彰就想當(dāng)然地認(rèn)為單片機(jī)也能“心同此心”的。實(shí)際上,如果你不做一些特殊的處理,單片機(jī)肯定不知道Timer[INPUT_DETECT_PTMR]就可以表征檢測(cè)輸入信號(hào)的周期性定時(shí)器的。
愿你三冬暖,愿你春不寒,愿你天黑有燈,下雨有傘。程序猿想和單片機(jī)接下此等心心相映的緣,需要做點(diǎn)編程工作,主動(dòng)手拉手線牽線。
四
顯然,INPUT_DETECT_PTMR此類軟件定時(shí)器節(jié)點(diǎn)ID想在數(shù)組中充當(dāng)下標(biāo)使用,下標(biāo)和枚舉之間要具有天然的一致性。
所幸,數(shù)組Timer[N]的下標(biāo)范圍是[0,N-1]間的正整數(shù),而整型取值正是枚舉類型的天然屬性。所以,第一步是要保證定時(shí)器枚舉也從0開始取值,然后取值依次加一,在[0,N-1]間一一占位。
第二步,在定時(shí)器數(shù)組的初始化階段,要用整數(shù)型下標(biāo)進(jìn)行一次for循環(huán),將各個(gè)軟件定時(shí)器節(jié)點(diǎn)的ID初始化為對(duì)應(yīng)的數(shù)組成員的下標(biāo),即Timer[i].timer_id = i,這里的i有三個(gè)作用,一是for循環(huán)體中的循環(huán)變量,二是數(shù)組成員下標(biāo),三是賦值給定時(shí)器ID。
在系統(tǒng)運(yùn)行階段,引用某個(gè)軟件定時(shí)器時(shí),以該軟件定時(shí)器對(duì)應(yīng)的枚舉類型常量做為數(shù)組下標(biāo),引用以該ID標(biāo)識(shí)的軟件定時(shí)器節(jié)點(diǎn),即用Timer[timer_id]直接尋址具體的軟件定時(shí)器,
這里有一個(gè)好處是,避免了以整型變量為下標(biāo)引用定時(shí)器時(shí)需要查找該定時(shí)器節(jié)點(diǎn)在軟件定時(shí)器數(shù)組中對(duì)應(yīng)的下標(biāo)的繁瑣,而且提高了程序的可讀性。
其中妙處,你品,你仔細(xì)品!
評(píng)論