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