別再糾結(jié)指針了?。。?/h1>
來源:裸機(jī)思維
作者:GorgonMeducer
【在前面的話】
不得不說,看了太多的人在各種地方討論指針……越發(fā)看下去,越發(fā)覺得簡單的事情被搞那么復(fù)雜,真是夠了,求求你們,放開那個變量,讓我來!
【萬能轉(zhuǎn)換公式】
1、從變量的三要素開始談起
為了把復(fù)雜的事情說簡單,我們拋開指針先從變量談起。(好吧,不知道這個笑話是不是夠冷)一個變量(Variable),或者順便兼容下面向?qū)ο螅ǎ希希┑母拍?,我們統(tǒng)一稱為對象(Object),除了保存于其中的內(nèi)容以外,只有三個要素:
由一定寬度無符號整數(shù)(Unsigned Integer)所表示的地址“數(shù)值”(Address Value)
對象的大?。⊿ize)和對齊
可對該對象適用的“方法”(Method)和“運(yùn)算”(Operation)
其中,我們習(xí)慣于把后兩者合并在一起稱之為,變量的"類型"。
> 地址數(shù)值(Address Value)
地址的數(shù)值是一個無符號整數(shù),其位寬由CPU的地址總線寬度所決定。話雖如此,其實主要還是編譯器在權(quán)衡了“用戶編寫代碼的便利性”以及“生成機(jī)器碼的效率”后為我們提供的解決方案:例如,針對8位機(jī),編譯器普遍以等效為uint16_t的整數(shù)來保存地址信息;針對16位機(jī)和32位機(jī),編譯器則普遍選擇uint32_t的整數(shù)來保存地址信息;針對64位機(jī),編譯器則可能會提供兩種指針類型,分別用來對應(yīng)uint32_t的4G地址空間和由uint64_t所代表的恐怖地址空間……
提問,8086有20根地址線,請問用哪種整型來表示其地址呢?(uint16_t、uint32_t還是uint20_t)——由于uint20_t并不存在,也并不適合CPU進(jìn)行地址運(yùn)算,所以統(tǒng)一用uint32_t來表示最為方便。
總而言之,地址的數(shù)值是一個無符號整數(shù)。知道這個有什么用呢?我們待一會再說。這里我們需要強(qiáng)調(diào)一句廢話:地址的數(shù)值既然是整數(shù),那么它就可以用另外的變量(類型合適的整形變量或者指針變量)進(jìn)行保存——任何指針變量,其本質(zhì),首先是一個無符號整形變量。任何指針常量,其本質(zhì)首先是一個無符號整數(shù)。
請一定要記?。ㄖ匾氖虑檎f三遍):
變量的三要素中,僅有地址值有可能會占用物理存儲空間。
變量的三要素中,僅有地址值有可能會占用物理存儲空間。
變量的三要素中,僅有地址值有可能會占用物理存儲空間。
> 大?。⊿ize)和對齊
如果僅從變量的大小來看整個計算機(jī)世界,就好像一副彩色圖片被二值化了,到處是Memory Block,他們的尺寸通常是1個字節(jié)、2個字節(jié)、4個字節(jié)、8個字節(jié)、16個字節(jié)或者由他們組合而成的長度各異Block。這些Block通常被編譯器在代碼生成的時候?qū)ζ涞降刂返膶挾壬?,比如地址寬度?2bit的,就對齊到4字節(jié),地址寬度是16bit的,就對齊到2字節(jié)……
如果你習(xí)慣于使用匯編語言來進(jìn)行開發(fā),你一定能體會我所描述的這種感覺。這些你統(tǒng)統(tǒng)都可以忘記,但有一點絕對要記?。ㄖ匾氖虑檎f三遍):
變量的三要素中,大小值從不會額外占用物理存儲空間。
變量的三要素中,大小值從不會額外占用物理存儲空間。
變量的三要素中,大小值從不會額外占用物理存儲空間。
注意:地址的大小信息描述的是這個變量占用幾個字節(jié),這里說大小信息并不占用物理存儲器空間,并不是說,變量中保存的內(nèi)容不占用存儲器空間。請注意區(qū)別。
C語言中,可以用sizeof( )來獲取一個變量的大小。前面我們說過,指針首先是一個整形變量,那么容易知道:
uint8_t?*pchObj;uint16_t?*phwObj;uint32_t?*pwObj;
sizeof(pchObj) 、sizeof(phwObj)、sizeof(pwObj)以及sizeof任意其它指針的結(jié)果都是一樣的,都是當(dāng)前系統(tǒng)保存地址數(shù)值的整形變量的寬度。對32位機(jī)來說,這個數(shù)值就是4——因為,sizeof( ) 求的是括號內(nèi)變量的寬度,而指針變量首先是一個整形變量!同一CPU中同一尋址能力的指針,其寬度是一樣一樣一樣的!
一個類型的大小信息除了描述一個變量所占用的存儲器尺寸以外,還隱含了該變量的對齊信息。從結(jié)論來說,32位處理器架構(gòu)下: 對普通的變量類型來說,編譯器“傾向于”將小于等于64Bit的數(shù)據(jù)類型自動對齊到與其大小相同的整數(shù)倍上;比如2字節(jié)大小的變量會被對齊到2的整數(shù)倍地址上,4字節(jié)大小的變量會被對齊到4的整數(shù)倍地址上,以此類推。
對結(jié)構(gòu)體和共用體來說,它會以所有成員中最大的那個對齊作為自己的對齊值。比如,下面的結(jié)構(gòu)體就是對齊到4的整倍數(shù),因為結(jié)構(gòu)體內(nèi)最大的對齊類型來自于一個指針(pTarget),而指針在32位系統(tǒng)下是4字節(jié),因此整個結(jié)構(gòu)體的對齊就是4:
struct example_t {????uint8_t?chID; //!< 對齊到1字節(jié)????uint16_t?hwCMDList[4];????//!????void?*pTarget;????????????//!< 對齊到4字節(jié)};????????????????????????????//!< 整個結(jié)構(gòu)體對齊到4字節(jié)
> 適用的方法(Method)和運(yùn)算(Operation)
對面向?qū)ο笾械膶ο髞碚f,方法就是該對象類中描述的各種成員函數(shù)(Method);
對數(shù)據(jù)結(jié)構(gòu)中的各類抽象數(shù)據(jù)類型(ADT,Abstract Data Type)來說,就是各類針對該數(shù)據(jù)類型的操作函數(shù),比如鏈表的添加(Add)、插入(Insert)、刪除(Delete)、和查找(Search)操作;比如隊列對象的入隊(enqueue)、出隊(Dequeue)函數(shù);比如棧對象的入棧(PUSH)、出棧(POP)等等……
對普通數(shù)值類的變量來說,就是所適用的各類運(yùn)算,比如針對 int的四則運(yùn)算(+、-、*、/、>、<、==、!=...)。你不能對float型的數(shù)據(jù)進(jìn)行移位操作,為什么呢?因為不同的類型擁有不同的適用方法和運(yùn)算。
也許你已經(jīng)猜到了,類型所適用的方法和運(yùn)算也不會占用物理存儲空間。由于變量的“大小信息”和“適用的方法和運(yùn)算信息”統(tǒng)稱為“類型(Type)信息”,我們可以簡化為:
變量的三要素中,類型信息從不會額外占用物理存儲空間。
變量的三要素中,類型信息從不會額外占用物理存儲空間。
變量的三要素中,類型信息從不會額外占用物理存儲空間。
2、化繁為簡的威力
前面說了那么多,實際上可以簡化為下面的等式:
Variable = Address Value + Type Info
變量 = 地址數(shù)值 + 類型信息
其中,地址數(shù)值的保存、表達(dá)和運(yùn)算是(有可能)實實在在需要占用物理存儲器空間的(RAM和ROM);而類型信息則是編譯器專用的——僅僅在編譯時刻會用到,用來為編譯器語法檢測和生成代碼提供信息的——話句話說,你只需要知道,類型信息是一個邏輯上的信息,是虛的,在最終生成的程序中并不占用任何存儲器空間。你也可以理解為,類型信息最終以程序行為的方式體現(xiàn)在代碼中,而并不占用任何額外的數(shù)據(jù)存儲器空間。
既然知道了變量的本質(zhì),我們就可以隨心所欲了,比如,我們可以隨意創(chuàng)建一個全局變量:
s_wMyVariable是一個 uint32_t類型的全局變量,它的地址是0x12345678。它和我們通過普通方式生成的全局變量使用起來沒有任何區(qū)別——當(dāng)然,它是個黑戶,簡單說就是它所占用的空間是非法的,無證的,在編譯器的戶口本看來,這塊空地上什么都沒有,因此它仍然會將0x12345678開始的4個字節(jié)用作其它目的。
一方面,是不是突然覺得手上擁有了神一般的權(quán)利?其實,這種方法非常常用,MCU的寄存器就是這么定義的,例如:
我們可以將上述定義全局變量的方法提煉成所謂的全局變量公式:
甚至,我們干脆定義一個宏來替我們批量生產(chǎn)全局變量:
使用起來也很方便,例如:
__VAR( float, 0x20004000 ) = 3.1415926;
總結(jié)來說:只要給我一個整數(shù),我就可以把它變成任何類型的全局變量!你可以的!我看好你哦。
3、萬能類型轉(zhuǎn)換
只要你牢記了那句話:給我一個整數(shù),我就能翹起地球,那么我們就可以用它玩出更好玩的東西。
首先,整數(shù)從何而來呢?除了前面的直接使用常數(shù)以外,當(dāng)然還可以從整形變量中來,例如,前面的例子可以簡單的改寫成:
uint32_t?wTemp?=?0x20004000;__VAR(??float,?wTemp??)?=?3.1415926;
毫無壓力!整數(shù)還可以從指針中來,例如:
//!我們定義一個全局變量 wDemo,其地址是0x20004000uint32_t?*pwSrc?=?&wDemo;????????????????//!//!uint32_t?wTemp?=?(uint32_t)?pwSrc;????__VAR(??float,?wTemp??)?=?3.1415926;
是不是覺得wTemp有點多余?因此我們可以直接寫成:
//!我們定義一個全局變量?wDemo,其地址是0x20004000uint32_t?*pwSrc?=?&wDemo;????????????????//!__VAR(??float,?(uint32_t)?pwSrc??)?=?3.1415926;
是不是pwSrc也多余了?好,我們繼續(xù)來:
//!我們定義一個全局變量 wDemo,其地址是0x20004000__VAR(??float,?(uint32_t)??&wDemo??)?=?3.1415926;
當(dāng)然,如果這個時候你說直接填0x20004000不就行了,要么你已經(jīng)懂了,要么你還糊涂著,仔細(xì)想想:
如果wDemo是任意由編譯器生成的對象(變量),意味著什么呢?(前面說過,作為全局變量,我們土法制造的和compiler原裝的用起來沒有任何區(qū)別)
如果我們有任意的指針,我們需要對指針指向的類型進(jìn)行轉(zhuǎn)換(轉(zhuǎn)換后才好操作),應(yīng)該怎么辦?
接下來,我們很容易根據(jù)前面的討論,得出第二個萬能公式,可以將任意變量(或地址)轉(zhuǎn)換成我們想要的類型:
__VAR(??(__TYPE),??(uint32_t)?(__ADDR)??)
例如,我們可以直接將字節(jié)數(shù)組中某一段內(nèi)容截取出來,當(dāng)做某種類型的變量來訪問:
//!?某數(shù)據(jù)幀解析函數(shù)void?command_handler(??uint8_t?*pchStream,?uint16_t?hwLength??){ //?offset?0,?uint16_t??????uint16_t?hwID?=?CONVERT(?pchStream,?uint16_t);?? // offset 4, float float fReceivedValue = CONVERT( &pchStream[ 4 ], float ) ; ...}
4、請忘記指針
如果你是一個指針苦手,那么請忘記之前所學(xué)的一切。記住一句話:指針只是一個用法怪異的整形變量,專門用來保存變量的地址數(shù)值。指針的類型都是用來欺騙編譯器的,我是聰明的人類,我操縱類型,我不是愚蠢的編譯器。
推論:因為指針變量的本質(zhì)是整形變量,所以指向指針的指針,只不過是一個指向普通整形變量的普通指針,因此指向指針的指針并不存在——世界上只存在普通指針——世界上只存在用法怪異的整形變量,專門用來保存目標(biāo)變量的地址數(shù)值。
推論:世界上并不存在指向指針的指針的指針的指針……
給我一個整數(shù),我自己造自己的變量。
指針的數(shù)值運(yùn)算太坑?轉(zhuǎn)換成整數(shù),加減乘除,隨便整。
5、小結(jié)
地址:所謂地址就是一個整形的數(shù)值(常數(shù))。地址不包含任何類型信息
指針:指針分為指針常量和指針變量,單獨說指針的時候,通常指指針常量。其中:
指針常量 = 地址數(shù)值(常數(shù))+ 類型信息
指針變量 = 整形變量 + 類型信息
變量 = (* 指針)
指針 = &變量
類型信息可以通過強(qiáng)制類型轉(zhuǎn)換來實現(xiàn),也就是大家熟悉的 ?() 用法。地址數(shù)值的改變,則統(tǒng)一轉(zhuǎn)化為普通整數(shù)以后再說。
指針常量 = 整數(shù)常量 + 類型信息 ? ? ?
也就是:
指針常量 = (<類型信息> *)整數(shù) 常量
反過來也成立:
整數(shù)常數(shù) = 指針常量 - 類型信息
也就是:
整數(shù)常數(shù) = (unsigned int)指針常量
同理,可以獲得整形變量和指針之間的轉(zhuǎn)換關(guān)系,這里就不一一列舉了。
怎么樣,事情是不是變得簡單了?哪有什么指針,哪有那么多麻煩事情?統(tǒng)統(tǒng)都是整數(shù)。下回我們將一起來捅一個馬蜂窩。哈哈哈哈哈
【后記】
說在后面的話:
其實,每次看到一群人熱熱鬧鬧的談?wù)撝羔?,我心里真實的想法是:這么簡單的事情被你們搞這么復(fù)雜——把復(fù)雜的事情變簡單,把簡單的事情做可靠才是使用C語言進(jìn)行工程設(shè)計的關(guān)鍵。指針不是炫技,請各位老司機(jī)們安全駕駛。
【說在前面的話】
如果說指針在一些人心中是導(dǎo)致代碼“極其不穩(wěn)定的奇技淫巧”,那么“函數(shù)指針”則是導(dǎo)致代碼跑飛和艱澀難懂的罪魁禍?zhǔn)?。然而,函?shù)指針的定義和使用其實非可以非常簡單——請暫時忘記原本你從課本上所學(xué)的知識,讓我們來看一種函數(shù)指針的正確打開方式。
【函數(shù)指針】
假設(shè)有一個目標(biāo)函數(shù),其函數(shù)原型是這樣的:
extern?bool?serial_out(uint8_t chByte);
那么如何定義指向該函數(shù)原型的函數(shù)指針呢?
步驟1:用typedef定義一個函數(shù)原型類型:
typedef?bool?serial_out_t(uint8_t?chByte);
或者省略形參的變量名:
typedef?bool?serial_out_t(uint8_t);
步驟2:使用新類型按照普通指針的使用方法來使用。
使用新的類型來定義指向該類型的指針——函數(shù)指針
serial_out_t?*fnPutChar = NULL;...fnPutChar = &serial_out;
如果用傳統(tǒng)的方法,上面的代碼等效為:
bool?(*)(uint8_t)?fnPutChar = NULL;...fnPutChar = serial_out;
使用函數(shù)指針的來訪問函數(shù)
...if (NULL != fnPutChar) {????//!?調(diào)用函數(shù)指針?biāo)赶虻暮瘮?shù)????bResult?=?(*fnPutChar)('H');?????//!}...
需要特別注意:
我們并不是通過typedef來直接定義指針類型,而是定義一個專門針對目標(biāo)函數(shù)原型的新類型——這樣在定義函數(shù)指針變量時就和普通變量類型一樣需要使用“*”——任何時候都知道這是一個指針,不會迷惑。
雖然這里"&"在C語言語法上是可以省略的,但是為了簡化規(guī)則(簡化需要記憶的特殊情況),這里我們要遵守普通指針的使用規(guī)則——取地址的時候要使用取地址運(yùn)算符“&”,訪問指針?biāo)赶蚩臻g的時候,“*”也不能省略。
使用這種方法定義和使用函數(shù)指針好處非常明顯: 極大的提高了代碼的可讀性——與函數(shù)指針有關(guān)的代碼,任何時候一眼看就知道是一個指針;
極大的降低了函數(shù)指針的使用難度——通過typedef定義一個針對函數(shù)原型的類型,將函數(shù)指針的使用變得跟普通指針一摸一樣,從而省去了額外的記憶負(fù)擔(dān);
允許輕松套娃
關(guān)于最后一點,我們不妨做一個極端一點的例子:
假設(shè)有一個函數(shù),其輸入?yún)?shù)是一個函數(shù)指針,其返回函數(shù)也是一個函數(shù)指針:
typedef struct task_cb_t task_cb_t;
typedef?const char *?get_err_string_t(task_cb_t *ptTask);typedef?void?on_task_cpl_evt_t(task_cb_t *ptTask);
extern?get_err_code_t?*run_task( task_cb_t?*ptTask,? on_task_cpl_evt_t?*fnTaskCPLEvtHandler);
為了讓這個例子顯得更為合理,我假想了一個調(diào)度器,而run_task就是這個調(diào)度器執(zhí)行用戶任務(wù)的函數(shù)。分析上面的代碼容易清晰的獲得以下信息:
task_cb_t?是用戶任務(wù)的控制塊,具體內(nèi)容未知,但我們可以用它來聲明指針變量;
函數(shù)指針(get_err_code_t *)指向的函數(shù)可以返回指定任務(wù)的錯誤代碼;
函數(shù)指針(on_task_cpl_evt_t *)所指向的函數(shù)是一個事件處理程序;
函數(shù)?run_task會執(zhí)行指定的任務(wù),“可能”會在任務(wù)執(zhí)行完成的時候通過函數(shù)指針?fnTaskCPLEvtHandler調(diào)用一個用戶指定的事件處理程序;
函數(shù)run_task在執(zhí)行指定任務(wù)的時候,如果發(fā)生了錯誤,“可能”會返回一個非NULL的函數(shù)指針,類型是:(get_err_code_t *),用戶可以通過這個函數(shù)指針獲取任務(wù)ptTask專屬的錯誤信息(字符串);
怎么樣,是不是看起來一切都簡單自然?那你考慮過,如果要做一個指向run_task的函數(shù)指針應(yīng)該是什么樣么?套娃開始:
typedef?get_err_code_t?*run_task_t(task_cb_t?*, on_task_cpl_evt *);
【注意】run_task_t 前面的“*”是 (get_err_code_t *)的一部分。
我們可以用新類型run_task_t定義一個函數(shù)指針:
static?run_task_t?*s_fnDispatcher = NULL;
最后,作為一個挑戰(zhàn),我很懷疑有沒有人能不借助typedef的方法,重新寫出函數(shù)指針 s_fnDispatcher 的定義?


歡迎在評論區(qū)留言,寫下你的答案。
【后記】
借助typedef,函數(shù)指針的使用可以極大的簡化。與傳統(tǒng)方式不同的是,這里typedef定義的不是函數(shù)指針本身,而是一個“函數(shù)原型的類型”——借助這一小技巧,我們成功的貫徹了“復(fù)雜的事情變簡單、簡單的事情變可靠”的原則。
推薦閱讀:
嵌入式編程專輯 Linux 學(xué)習(xí)專輯
C/C++編程專輯
Qt進(jìn)階學(xué)習(xí)專輯
長按前往圖中包含的公眾號關(guān)注
瀏覽
54
來源:裸機(jī)思維
作者:GorgonMeducer
【在前面的話】
【萬能轉(zhuǎn)換公式】
1、從變量的三要素開始談起
為了把復(fù)雜的事情說簡單,我們拋開指針先從變量談起。(好吧,不知道這個笑話是不是夠冷)一個變量(Variable),或者順便兼容下面向?qū)ο螅ǎ希希┑母拍?,我們統(tǒng)一稱為對象(Object),除了保存于其中的內(nèi)容以外,只有三個要素:
由一定寬度無符號整數(shù)(Unsigned Integer)所表示的地址“數(shù)值”(Address Value)
對象的大?。⊿ize)和對齊
可對該對象適用的“方法”(Method)和“運(yùn)算”(Operation)
其中,我們習(xí)慣于把后兩者合并在一起稱之為,變量的"類型"。
> 地址數(shù)值(Address Value)
地址的數(shù)值是一個無符號整數(shù),其位寬由CPU的地址總線寬度所決定。話雖如此,其實主要還是編譯器在權(quán)衡了“用戶編寫代碼的便利性”以及“生成機(jī)器碼的效率”后為我們提供的解決方案:例如,針對8位機(jī),編譯器普遍以等效為uint16_t的整數(shù)來保存地址信息;針對16位機(jī)和32位機(jī),編譯器則普遍選擇uint32_t的整數(shù)來保存地址信息;針對64位機(jī),編譯器則可能會提供兩種指針類型,分別用來對應(yīng)uint32_t的4G地址空間和由uint64_t所代表的恐怖地址空間……
提問,8086有20根地址線,請問用哪種整型來表示其地址呢?(uint16_t、uint32_t還是uint20_t)——由于uint20_t并不存在,也并不適合CPU進(jìn)行地址運(yùn)算,所以統(tǒng)一用uint32_t來表示最為方便。
總而言之,地址的數(shù)值是一個無符號整數(shù)。知道這個有什么用呢?我們待一會再說。這里我們需要強(qiáng)調(diào)一句廢話:地址的數(shù)值既然是整數(shù),那么它就可以用另外的變量(類型合適的整形變量或者指針變量)進(jìn)行保存——任何指針變量,其本質(zhì),首先是一個無符號整形變量。任何指針常量,其本質(zhì)首先是一個無符號整數(shù)。
請一定要記?。ㄖ匾氖虑檎f三遍):
變量的三要素中,僅有地址值有可能會占用物理存儲空間。
變量的三要素中,僅有地址值有可能會占用物理存儲空間。
變量的三要素中,僅有地址值有可能會占用物理存儲空間。
> 大?。⊿ize)和對齊
如果僅從變量的大小來看整個計算機(jī)世界,就好像一副彩色圖片被二值化了,到處是Memory Block,他們的尺寸通常是1個字節(jié)、2個字節(jié)、4個字節(jié)、8個字節(jié)、16個字節(jié)或者由他們組合而成的長度各異Block。這些Block通常被編譯器在代碼生成的時候?qū)ζ涞降刂返膶挾壬?,比如地址寬度?2bit的,就對齊到4字節(jié),地址寬度是16bit的,就對齊到2字節(jié)……
如果你習(xí)慣于使用匯編語言來進(jìn)行開發(fā),你一定能體會我所描述的這種感覺。這些你統(tǒng)統(tǒng)都可以忘記,但有一點絕對要記?。ㄖ匾氖虑檎f三遍):
變量的三要素中,大小值從不會額外占用物理存儲空間。
變量的三要素中,大小值從不會額外占用物理存儲空間。
變量的三要素中,大小值從不會額外占用物理存儲空間。
注意:地址的大小信息描述的是這個變量占用幾個字節(jié),這里說大小信息并不占用物理存儲器空間,并不是說,變量中保存的內(nèi)容不占用存儲器空間。請注意區(qū)別。
C語言中,可以用sizeof( )來獲取一個變量的大小。前面我們說過,指針首先是一個整形變量,那么容易知道:
uint8_t?*pchObj;uint16_t?*phwObj;uint32_t?*pwObj;
sizeof(pchObj) 、sizeof(phwObj)、sizeof(pwObj)以及sizeof任意其它指針的結(jié)果都是一樣的,都是當(dāng)前系統(tǒng)保存地址數(shù)值的整形變量的寬度。對32位機(jī)來說,這個數(shù)值就是4——因為,sizeof( ) 求的是括號內(nèi)變量的寬度,而指針變量首先是一個整形變量!同一CPU中同一尋址能力的指針,其寬度是一樣一樣一樣的!
對普通的變量類型來說,編譯器“傾向于”將小于等于64Bit的數(shù)據(jù)類型自動對齊到與其大小相同的整數(shù)倍上;比如2字節(jié)大小的變量會被對齊到2的整數(shù)倍地址上,4字節(jié)大小的變量會被對齊到4的整數(shù)倍地址上,以此類推。
對結(jié)構(gòu)體和共用體來說,它會以所有成員中最大的那個對齊作為自己的對齊值。比如,下面的結(jié)構(gòu)體就是對齊到4的整倍數(shù),因為結(jié)構(gòu)體內(nèi)最大的對齊類型來自于一個指針(pTarget),而指針在32位系統(tǒng)下是4字節(jié),因此整個結(jié)構(gòu)體的對齊就是4:
struct example_t {????uint8_t?chID; //!< 對齊到1字節(jié)????uint16_t?hwCMDList[4];????//!????void?*pTarget;????????????//!< 對齊到4字節(jié)};????????????????????????????//!< 整個結(jié)構(gòu)體對齊到4字節(jié)
> 適用的方法(Method)和運(yùn)算(Operation)
對面向?qū)ο笾械膶ο髞碚f,方法就是該對象類中描述的各種成員函數(shù)(Method);
對數(shù)據(jù)結(jié)構(gòu)中的各類抽象數(shù)據(jù)類型(ADT,Abstract Data Type)來說,就是各類針對該數(shù)據(jù)類型的操作函數(shù),比如鏈表的添加(Add)、插入(Insert)、刪除(Delete)、和查找(Search)操作;比如隊列對象的入隊(enqueue)、出隊(Dequeue)函數(shù);比如棧對象的入棧(PUSH)、出棧(POP)等等……
對普通數(shù)值類的變量來說,就是所適用的各類運(yùn)算,比如針對 int的四則運(yùn)算(+、-、*、/、>、<、==、!=...)。你不能對float型的數(shù)據(jù)進(jìn)行移位操作,為什么呢?因為不同的類型擁有不同的適用方法和運(yùn)算。
也許你已經(jīng)猜到了,類型所適用的方法和運(yùn)算也不會占用物理存儲空間。由于變量的“大小信息”和“適用的方法和運(yùn)算信息”統(tǒng)稱為“類型(Type)信息”,我們可以簡化為:
變量的三要素中,類型信息從不會額外占用物理存儲空間。
變量的三要素中,類型信息從不會額外占用物理存儲空間。
變量的三要素中,類型信息從不會額外占用物理存儲空間。
2、化繁為簡的威力
前面說了那么多,實際上可以簡化為下面的等式:
Variable = Address Value + Type Info
變量 = 地址數(shù)值 + 類型信息
其中,地址數(shù)值的保存、表達(dá)和運(yùn)算是(有可能)實實在在需要占用物理存儲器空間的(RAM和ROM);而類型信息則是編譯器專用的——僅僅在編譯時刻會用到,用來為編譯器語法檢測和生成代碼提供信息的——話句話說,你只需要知道,類型信息是一個邏輯上的信息,是虛的,在最終生成的程序中并不占用任何存儲器空間。你也可以理解為,類型信息最終以程序行為的方式體現(xiàn)在代碼中,而并不占用任何額外的數(shù)據(jù)存儲器空間。
既然知道了變量的本質(zhì),我們就可以隨心所欲了,比如,我們可以隨意創(chuàng)建一個全局變量:
s_wMyVariable是一個 uint32_t類型的全局變量,它的地址是0x12345678。它和我們通過普通方式生成的全局變量使用起來沒有任何區(qū)別——當(dāng)然,它是個黑戶,簡單說就是它所占用的空間是非法的,無證的,在編譯器的戶口本看來,這塊空地上什么都沒有,因此它仍然會將0x12345678開始的4個字節(jié)用作其它目的。
一方面,是不是突然覺得手上擁有了神一般的權(quán)利?其實,這種方法非常常用,MCU的寄存器就是這么定義的,例如:
我們可以將上述定義全局變量的方法提煉成所謂的全局變量公式:
甚至,我們干脆定義一個宏來替我們批量生產(chǎn)全局變量:
使用起來也很方便,例如:
__VAR( float, 0x20004000 ) = 3.1415926;總結(jié)來說:只要給我一個整數(shù),我就可以把它變成任何類型的全局變量!你可以的!我看好你哦。
3、萬能類型轉(zhuǎn)換
只要你牢記了那句話:給我一個整數(shù),我就能翹起地球,那么我們就可以用它玩出更好玩的東西。
首先,整數(shù)從何而來呢?除了前面的直接使用常數(shù)以外,當(dāng)然還可以從整形變量中來,例如,前面的例子可以簡單的改寫成:
uint32_t?wTemp?=?0x20004000;__VAR(??float,?wTemp??)?=?3.1415926;
毫無壓力!整數(shù)還可以從指針中來,例如:
//!我們定義一個全局變量 wDemo,其地址是0x20004000uint32_t?*pwSrc?=?&wDemo;????????????????//!//!uint32_t?wTemp?=?(uint32_t)?pwSrc;????__VAR(??float,?wTemp??)?=?3.1415926;
是不是覺得wTemp有點多余?因此我們可以直接寫成:
//!我們定義一個全局變量?wDemo,其地址是0x20004000uint32_t?*pwSrc?=?&wDemo;????????????????//!__VAR(??float,?(uint32_t)?pwSrc??)?=?3.1415926;
是不是pwSrc也多余了?好,我們繼續(xù)來:
//!我們定義一個全局變量 wDemo,其地址是0x20004000__VAR(??float,?(uint32_t)??&wDemo??)?=?3.1415926;
當(dāng)然,如果這個時候你說直接填0x20004000不就行了,要么你已經(jīng)懂了,要么你還糊涂著,仔細(xì)想想:
如果wDemo是任意由編譯器生成的對象(變量),意味著什么呢?(前面說過,作為全局變量,我們土法制造的和compiler原裝的用起來沒有任何區(qū)別)
如果我們有任意的指針,我們需要對指針指向的類型進(jìn)行轉(zhuǎn)換(轉(zhuǎn)換后才好操作),應(yīng)該怎么辦?
接下來,我們很容易根據(jù)前面的討論,得出第二個萬能公式,可以將任意變量(或地址)轉(zhuǎn)換成我們想要的類型:
__VAR(??(__TYPE),??(uint32_t)?(__ADDR)??)
例如,我們可以直接將字節(jié)數(shù)組中某一段內(nèi)容截取出來,當(dāng)做某種類型的變量來訪問:
//!?某數(shù)據(jù)幀解析函數(shù)void?command_handler(??uint8_t?*pchStream,?uint16_t?hwLength??){//?offset?0,?uint16_t??????uint16_t?hwID?=?CONVERT(?pchStream,?uint16_t);??// offset 4, floatfloat fReceivedValue = CONVERT( &pchStream[ 4 ], float ) ;...}
4、請忘記指針
如果你是一個指針苦手,那么請忘記之前所學(xué)的一切。記住一句話:指針只是一個用法怪異的整形變量,專門用來保存變量的地址數(shù)值。指針的類型都是用來欺騙編譯器的,我是聰明的人類,我操縱類型,我不是愚蠢的編譯器。
推論:因為指針變量的本質(zhì)是整形變量,所以指向指針的指針,只不過是一個指向普通整形變量的普通指針,因此指向指針的指針并不存在——世界上只存在普通指針——世界上只存在用法怪異的整形變量,專門用來保存目標(biāo)變量的地址數(shù)值。
推論:世界上并不存在指向指針的指針的指針的指針……
給我一個整數(shù),我自己造自己的變量。
指針的數(shù)值運(yùn)算太坑?轉(zhuǎn)換成整數(shù),加減乘除,隨便整。
5、小結(jié)
地址:所謂地址就是一個整形的數(shù)值(常數(shù))。地址不包含任何類型信息
指針:指針分為指針常量和指針變量,單獨說指針的時候,通常指指針常量。其中:
指針常量 = 地址數(shù)值(常數(shù))+ 類型信息
指針變量 = 整形變量 + 類型信息
變量 = (* 指針)
指針 = &變量
類型信息可以通過強(qiáng)制類型轉(zhuǎn)換來實現(xiàn),也就是大家熟悉的 ?(
指針常量 = 整數(shù)常量 + 類型信息 ? ? ?
也就是:
指針常量 = (<類型信息> *)整數(shù) 常量
反過來也成立:
整數(shù)常數(shù) = 指針常量 - 類型信息
也就是:
整數(shù)常數(shù) = (unsigned int)指針常量
同理,可以獲得整形變量和指針之間的轉(zhuǎn)換關(guān)系,這里就不一一列舉了。
怎么樣,事情是不是變得簡單了?哪有什么指針,哪有那么多麻煩事情?統(tǒng)統(tǒng)都是整數(shù)。下回我們將一起來捅一個馬蜂窩。哈哈哈哈哈
【后記】
說在后面的話:
其實,每次看到一群人熱熱鬧鬧的談?wù)撝羔?,我心里真實的想法是:這么簡單的事情被你們搞這么復(fù)雜——把復(fù)雜的事情變簡單,把簡單的事情做可靠才是使用C語言進(jìn)行工程設(shè)計的關(guān)鍵。指針不是炫技,請各位老司機(jī)們安全駕駛。
【說在前面的話】
【函數(shù)指針】
extern?bool?serial_out(uint8_t chByte);那么如何定義指向該函數(shù)原型的函數(shù)指針呢?
步驟1:用typedef定義一個函數(shù)原型類型:
typedef?bool?serial_out_t(uint8_t?chByte);或者省略形參的變量名:
typedef?bool?serial_out_t(uint8_t);步驟2:使用新類型按照普通指針的使用方法來使用。
使用新的類型來定義指向該類型的指針——函數(shù)指針
serial_out_t?*fnPutChar = NULL;...fnPutChar = &serial_out;
如果用傳統(tǒng)的方法,上面的代碼等效為:
bool?(*)(uint8_t)?fnPutChar = NULL;...fnPutChar = serial_out;
使用函數(shù)指針的來訪問函數(shù)
...if (NULL != fnPutChar) {????//!?調(diào)用函數(shù)指針?biāo)赶虻暮瘮?shù)????bResult?=?(*fnPutChar)('H');?????//!}...
需要特別注意:
我們并不是通過typedef來直接定義指針類型,而是定義一個專門針對目標(biāo)函數(shù)原型的新類型——這樣在定義函數(shù)指針變量時就和普通變量類型一樣需要使用“*”——任何時候都知道這是一個指針,不會迷惑。
雖然這里"&"在C語言語法上是可以省略的,但是為了簡化規(guī)則(簡化需要記憶的特殊情況),這里我們要遵守普通指針的使用規(guī)則——取地址的時候要使用取地址運(yùn)算符“&”,訪問指針?biāo)赶蚩臻g的時候,“*”也不能省略。
極大的提高了代碼的可讀性——與函數(shù)指針有關(guān)的代碼,任何時候一眼看就知道是一個指針;
極大的降低了函數(shù)指針的使用難度——通過typedef定義一個針對函數(shù)原型的類型,將函數(shù)指針的使用變得跟普通指針一摸一樣,從而省去了額外的記憶負(fù)擔(dān);
允許輕松套娃
關(guān)于最后一點,我們不妨做一個極端一點的例子:
假設(shè)有一個函數(shù),其輸入?yún)?shù)是一個函數(shù)指針,其返回函數(shù)也是一個函數(shù)指針:
typedef struct task_cb_t task_cb_t;typedef?const char *?get_err_string_t(task_cb_t *ptTask);typedef?void?on_task_cpl_evt_t(task_cb_t *ptTask);extern?get_err_code_t?*run_task(task_cb_t?*ptTask,?on_task_cpl_evt_t?*fnTaskCPLEvtHandler);
為了讓這個例子顯得更為合理,我假想了一個調(diào)度器,而run_task就是這個調(diào)度器執(zhí)行用戶任務(wù)的函數(shù)。分析上面的代碼容易清晰的獲得以下信息:
task_cb_t?是用戶任務(wù)的控制塊,具體內(nèi)容未知,但我們可以用它來聲明指針變量;
函數(shù)指針(get_err_code_t *)指向的函數(shù)可以返回指定任務(wù)的錯誤代碼;
函數(shù)指針(on_task_cpl_evt_t *)所指向的函數(shù)是一個事件處理程序;
函數(shù)?run_task會執(zhí)行指定的任務(wù),“可能”會在任務(wù)執(zhí)行完成的時候通過函數(shù)指針?fnTaskCPLEvtHandler調(diào)用一個用戶指定的事件處理程序;
函數(shù)run_task在執(zhí)行指定任務(wù)的時候,如果發(fā)生了錯誤,“可能”會返回一個非NULL的函數(shù)指針,類型是:(get_err_code_t *),用戶可以通過這個函數(shù)指針獲取任務(wù)ptTask專屬的錯誤信息(字符串);
怎么樣,是不是看起來一切都簡單自然?那你考慮過,如果要做一個指向run_task的函數(shù)指針應(yīng)該是什么樣么?套娃開始:
typedef?get_err_code_t?*run_task_t(task_cb_t?*, on_task_cpl_evt *);【注意】run_task_t 前面的“*”是 (get_err_code_t *)的一部分。
我們可以用新類型run_task_t定義一個函數(shù)指針:
static?run_task_t?*s_fnDispatcher = NULL;


歡迎在評論區(qū)留言,寫下你的答案。【后記】
推薦閱讀:嵌入式編程專輯 Linux 學(xué)習(xí)專輯 C/C++編程專輯 Qt進(jìn)階學(xué)習(xí)專輯 長按前往圖中包含的公眾號關(guān)注
