一、字符串的實(shí)質(zhì)就是指針字符串是C語言中最基礎(chǔ)的概念,也是最常被用到的。在嵌入式開發(fā)中,我們經(jīng)常要將一些字符串通過串口顯示到串口助手或調(diào)試終端上,作為信息提示,以便讓我們了解程序的運(yùn)行情況;或者是將一些常量的值轉(zhuǎn)為字符串,來顯示到液晶等顯示設(shè)備上。 那么C語言中的字符串到底是什么?其實(shí)字符串本身就是一個指針,它的值(即指針?biāo)赶虻牡刂罚┚褪亲址鬃址牡刂贰?/span> 為了解釋這個問題,我經(jīng)常會舉這樣一個例子:如何將一個數(shù)值轉(zhuǎn)化為相應(yīng)的16進(jìn)制字符串。比如,把100轉(zhuǎn)為”0X64”。 我們可以寫這樣一個函數(shù): void Value2String(unsigned char value,char *str) { char *Hex_Char_Table='0123456789ABCDEF'; str[0] = '0'; str[1] = 'X'; str[4] = 0; str[2]=Hex_Char_Table[value>>4]; str[3]=Hex_Char_Table[value&0X0F]; } 字符串常量實(shí)質(zhì)是內(nèi)存中的字節(jié)序列。如圖所示。 上面,振南說“字符串本身就是指針”,那么見證這句話真正意義的時刻來了,我們將上面程序進(jìn)行簡化: void Value2String(unsigned char value,char *str) Hex_Char_Table 這個指針變量其實(shí)是多余的,“字符串本身就是指針”,所以它后面可以直接用 [] 配合下標(biāo)來取出其中的字符。凡是實(shí)質(zhì)上為指針類型(即表達(dá)的是地址意義)的變量或常量,都可以直接用[]或*來訪問它所指向的數(shù)據(jù)序列中的數(shù)據(jù)元素。 二、轉(zhuǎn)義符 \C語言中要表達(dá)一個字節(jié)數(shù)據(jù)序列(內(nèi)存中連續(xù)存儲的若干個字節(jié)),我們可以使用字節(jié)數(shù)組,如unsigned char array[10]={0,1,2,3,4,5,6,7,8,9}。 其實(shí)字符串,本質(zhì)上也是一個字節(jié)序列,但是通常情況下它所存儲的字節(jié)的值均為 ASCII 中可打印字符的碼值,如’A’、’ '、’|’等。那在字符串中是否也可以出現(xiàn)其它的值呢?這樣,我們就可以用字符串的形式來表達(dá)一個字節(jié)序列了。很多時候,它可能比字節(jié)數(shù)組要方便一些。字符串中的轉(zhuǎn)義符就是用來干這個的。請看如下程序: const unsigned char array[10]={0,1,2,3,4,5,6,7,8,9}; char *array='\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09'; 這兩種寫法,array所指向的內(nèi)存字節(jié)序列是基本一樣的(后者最后還有一個0)。當(dāng)然,如果我們把a(bǔ)rray傳到strlen去計(jì)算長度,返回的值為0。因?yàn)樗谝粋€字節(jié)的值為0。但是我們?nèi)匀豢梢允褂胊rray[n]的方式去訪問序列中的數(shù)據(jù)。 char *str='ABCDEFG'; 上面程序中的兩種寫法,是完成等價的。 字符串中的轉(zhuǎn)義符的目的是為了在本應(yīng)該只能看到ASCII可打印字符的序列中,可以表達(dá)其它數(shù)值或特殊字符。如經(jīng)常使用的回車換行”\r\n”,其實(shí)質(zhì)就是”\x0d\x0a”;通常我們所說的字符串結(jié)束符\0,其實(shí)就是0的八進(jìn)制轉(zhuǎn)義表達(dá)形式。 三、字符串常量的連接在研讀一些開源軟件的源代碼時,我見到了字符串常量的一個比較另類的用法,在這里介紹給大家。 有些時候,為了讓字符串常量內(nèi)容層次更加清晰,就可以把一個長字符串打散成若干個短字符串,它們順序首尾相接,在意義上與長字符串是等價的。比如'0123456789ABCDEF'可以分解為”0123456789””ABCDEF”,即多個字符串常量可以直接連接,夠成長字符串。這種寫法,在 printf 打印調(diào)試信息的時候可能會更多用到。 printf('A:%d B:%d C:%d D:%d E:%d F:%d\r\n',1,2,3,4,5,6); printf('A:%d ' \ 'B:%d ' \ 'C:%d ' \ 'D:%d ' \ 'E:%d ' \ 'F:%d\r\n',1,2,3,4,5,6); 在 printf 的格式化串很長的時候,我們把它合理的打散,分為多行,程序就會顯得更多工整。 四、長字符串的拆分技巧很多時候我們需要進(jìn)行長字符串的拆分。問題是如何實(shí)現(xiàn)它? 很多人可能都會想到使用那個分隔字符,比如空格、逗號。然后去一個個數(shù)要提取的參數(shù)前面有幾個分隔字符,然后后將相應(yīng)位置上的字符組成一個新的短字符串。如圖2.3所示。 這種方法固然可行,但是略顯笨拙。其實(shí)對于這種有明顯分隔符的長字符串,我們可以采用“打散”或“爆炸”的思想,具體過程是這樣的:將長字符串中的所有分隔符全部替換為’\0’,即字符串結(jié)束符。此時,長字符串就被分解成了在內(nèi)存中順序存放的若干個短字符串。 如果要取出第n個短字符串,可以用這個函數(shù): char * substr(char *str,n) 很多時候我們需要一次性訪問長字符串中的多個短字符串,此時振南經(jīng)常會這樣來作:通過一個循環(huán),將長字符串中的所有分隔符替換為’\0’,在此過程中將每一個短字符串首字符的位置記錄到一個數(shù)組中,代碼如下: unsigned char substr(unsigned char \*pos,char \*str) { unsigned char len=strlen(str); unsigned char n=0,i=0; for(;i<len;i++) { if(str[i]==' ') { str[i]=0;pos[n++]=(i+1); } } return n; } 好,舉個例子:我們要提取”abc 1000 50 off 2500”中的”abc”、”50”和”off”,可以使用上面的函數(shù)來實(shí)現(xiàn)。 unsigned char pos[10]; 五、取出數(shù)值的各位數(shù)碼在實(shí)際項(xiàng)目中,我們經(jīng)常需要提取一個數(shù)值的某些位的數(shù)碼,比如用數(shù)碼管來顯示數(shù)值或?qū)⒁粋€數(shù)值轉(zhuǎn)成字符串,都會涉及到這一操作。 那如何實(shí)現(xiàn)這一操作呢?雖然這個問題看似很簡單,但提出這一問題的人還不在少數(shù)。 請看下面的函數(shù)。 void getdigi(unsigned char *digi,unsigned int num) { digi[0]=(num/10000)%10; digi[1]=(num/1000)%10; digi[2]=(num/100)%10; digi[3]=(num/10)%10; digi[4]=num%10; } 它的主要操作就是除法和取余。這個函數(shù)只是取出一個整型數(shù)各位的數(shù)碼,那浮點(diǎn)呢?其實(shí)一樣的道理,請看下面函數(shù)(我們默認(rèn)整數(shù)與小數(shù)部分均取4位)。 void getdigi(unsigned char *digi1,unsigned char *digi2,unsigned float num) 有人說,我更喜歡用sprintf函數(shù),直接將數(shù)值格式化打印到字符串里,各位數(shù)碼自然就得到了。 char digi[10]; sprintf(digi,'%d',num); //**整型 char digi[10]; sprintf(digi,'%f',num); //**浮點(diǎn) 沒問題。但是在嵌入式平臺上使用 sprintf 函數(shù),通常代價是較大的。 作為嵌入式工程師,一定要惜字如金,尤其是在硬件資源相對較為緊張的情況下。sprintf非常強(qiáng)大,我們只是一個簡單的提取數(shù)值數(shù)碼或?qū)?shù)值轉(zhuǎn)為相應(yīng)的字符串的操作,使用它有些暴殄天物。這種時候,我通常選擇寫一個小函數(shù)或者宏來自己實(shí)現(xiàn)。 六、printf 的 實(shí)質(zhì)與使用技巧printf是我們非常熟悉的一個入門級的標(biāo)準(zhǔn)庫函數(shù),每當(dāng)我們說出計(jì)算機(jī)金句”Hello World!”時,其實(shí)無意中就提到了它:printf(“hello world!”); 它可以某種特定的格式、進(jìn)制或形式輸出任何變量、常量和字符串,為我們提供了極大的方便,甚至成為了很多人調(diào)試程序時重要的Debug手段。但是在嵌入式中,我們就需要剖析一下它的實(shí)質(zhì)了。 printf 函數(shù)的底層是基于一個 fputc 的函數(shù),它用于實(shí)現(xiàn)單個字符的具體輸出方式,比如是將字符顯示到顯示器上,或是存儲到某個數(shù)組中(類似sprintf),或者是通過串口發(fā)送出去,甚至不是串口,而是以太網(wǎng)、CAN、I2C等接口。 以下是一個STM32項(xiàng)目中fputc函數(shù)的實(shí)現(xiàn): int fputc(int ch, FILE \*f) fputc中將ch通過USART1發(fā)出。這樣,我們在調(diào)用printf的時候,相應(yīng)的信息就會從USART1打印出來。 “上面你說的這些,我都知道,有什么新鮮的!”確實(shí),通過串口打印信息是我們司空見慣的。那么下面的fputc你見過嗎? int fputc(int ch, FILE *f) { LCD_DispChar(x,y,ch); x++; if(x>=X_MAX) { x=0;y++; if(y>=Y_MAX)* { y=0; } } return ch; } 這個fputc將字符顯示在了液晶上(同時維護(hù)了字符的顯示位置信息),這樣當(dāng)我們調(diào)用printf的時候,信息會直接顯示在液晶上。 說白了,fputc 就是對數(shù)據(jù)進(jìn)行了定向輸出。這樣我們可以把 printf 變得更靈活,來應(yīng)對更多樣的應(yīng)用需求。 在振南經(jīng)歷的項(xiàng)目中,曾經(jīng)有過這樣的情況:單片機(jī)有多個串口,串口1用于打印調(diào)試信息,串口2與ESP8266 WIFI模塊通信,串口3與SIM800 GPRS模塊通信。3個串口都需要格式化輸出,但是printf只有一個,這該怎么辦? 我們解決方法是,修改 fputc 使得 printf 可以由 3 個串口分時復(fù)用。具體實(shí)現(xiàn)如下。 unsigned char us=0; 在調(diào)用的時候,根據(jù)需要將us賦以不同的值,printf就歸誰所用了。 #define U_TO_DEBUG us=0; #define U_TO_ESP8266 us=1; #define U_TO_SIM800 us=2; U_TO_DEBUG printf('hello world!'); U_TO_ESP8266 printf('AT\r\n'); U_TO_SIM800 printf('AT\r\n'); 七、關(guān)于浮點(diǎn)數(shù)的傳輸很多人不能很好的使用和處理浮點(diǎn),其主要根源在于對它的表達(dá)與存儲方式不是很理解。最典型的例子就是經(jīng)常有人問我:“如何使用串口來發(fā)送一個浮點(diǎn)數(shù)?” 我們知道C語言中有很多數(shù)據(jù)類型,其中unsigned char、unsigned short、unsigned int、unsigned long我們稱其為整型,顧名思義它們可以表達(dá)整型數(shù)。而能夠表達(dá)的數(shù)值范圍與數(shù)據(jù)類型所占用的字節(jié)數(shù)有關(guān)。數(shù)值的表達(dá)方法比如簡單,如下圖所示。 一個字節(jié)可以表達(dá)0~255,兩個字節(jié)(unsigned short)自然就可以表達(dá)0~65535,依次類推。 當(dāng)需要把一個整型數(shù)值發(fā)送出去的時候,我們可以這樣作: unsigned short a=0X1234; 也就是將構(gòu)成整型的若干字節(jié)順序發(fā)送即可。當(dāng)然接收方一定要知道如何還原數(shù)據(jù),也就是說它要知道自己接收到的若干字節(jié)拼在一起是什么類型,這是由具體通信協(xié)議來保障的。 unsigned char buf[2]; usnigned short a; UART_Receive_Byte(buf+0); UART_Receive_Byte(buf+1); a=(*(usnigned short *)buf); OK,關(guān)于整型比較容易理解。但是換成 float,很多人就有些迷糊了。因?yàn)?float 的數(shù)值表達(dá)方式有些復(fù)雜。有些人使用下面的方法來進(jìn)行浮點(diǎn)的發(fā)送。 float a=3.14; 很顯然這種方法非常的“業(yè)余”。還有人問我:“浮點(diǎn)小數(shù)字前后的數(shù)字可以發(fā)送,但是小數(shù)點(diǎn)怎么發(fā)?”這赤裸裸的體現(xiàn)了他對浮點(diǎn)類型的誤解。 不要被float數(shù)值的表象迷惑,它實(shí)質(zhì)上只不過是4個字節(jié)而已,如圖2.5所示。 所以,正確的發(fā)送浮點(diǎn)數(shù)的方法是這樣的: float a=3.14; UART_Send_Byte(((unsigned char *)&a)[0]); UART_Send_Byte(((unsigned char *)&a)[1]); UART_Send_Byte(((unsigned char *)&a)[2]); UART_Send_Byte(((unsigned char *)&a)[3]); 接收者將數(shù)據(jù)還原為浮點(diǎn): unsigned char buf[4]; 八、關(guān)于數(shù)據(jù)的直接操作直接操作數(shù)據(jù)?我們來舉個例子:取一個整型數(shù)的相反數(shù)。一般的實(shí)現(xiàn)方法是這樣的: int a=10; int b=-a; //-1*a; 這樣的操作可能會涉及到一次乘法運(yùn)算,花費(fèi)更多的時間。當(dāng)我們了解了整型數(shù)的實(shí)質(zhì),就可以這樣來作: int a=10; 這也許還不足以說明問題,那我們再來看一個例子:取一個浮點(diǎn)數(shù)的相反數(shù)。似乎只能這樣來作: float a=3.14; float b=a*-1.0; 其實(shí)我們可以這樣來作: float a=3.14; 沒錯,我們可以直接修改浮點(diǎn)在內(nèi)存中的高字節(jié)的符號位。這比乘以-1.0的方法要高效的多。 當(dāng)然,這些操作都需要你對 C 語言中的指針有爐火純青的掌握。 九、浮點(diǎn)的四舍五入與比較我們先說第一個問題:如何實(shí)現(xiàn)浮點(diǎn)的四舍五入?很多人遇到過這個問題,其實(shí)很簡單,只需要把浮點(diǎn)+0.5然后取整即可。 OK,第二個問題:浮點(diǎn)的比較。這個問題還有必要好好說一下。 首先我們要知道,C語言中的判等,即==,是一中強(qiáng)匹配的行為。也就是,比較雙方必須每一個位都完全一樣,才認(rèn)定它們相等。這對于整型來說,是可以的。但是float類型則不適用,因?yàn)閮蓚€看似相等的浮點(diǎn)數(shù),其實(shí)它們的內(nèi)存表達(dá)不能保證每一個位都完全一樣。 這個時候,我們作一個約定:兩個浮點(diǎn)只要它們之差m足夠小,則認(rèn)為它們相等,m一般取10e-6。也就是說,只要兩個浮點(diǎn)小數(shù)點(diǎn)后6位相同,則認(rèn)為它們相等。也正是因?yàn)檫@個約定,很多C編譯器把float的精度設(shè)定為小數(shù)點(diǎn)后7位,比如ARMCC(MDK的編譯器)。 float a,b; if(a==b) ... //**錯誤 if(fabs(a-b) <= 0.000001) ...//**正確 十、出神入化的for循環(huán)for循環(huán)我們再熟悉不過了,通常我們使用它都是中規(guī)中矩的,如下例: int i; 但是如果我們對for循環(huán)的本質(zhì)有更深刻的理解的話,就可以把它用得出神入化。 for后面的括號中的東西我稱之為“循環(huán)控制體”,分為三個部分,如下圖所示。 A、B、C三個部分,其實(shí)隨意性很大,可以是任意一個表達(dá)式。所以,我們可以這樣寫一個死循環(huán): for(1;1;1) //1**本身就是一個表達(dá)式:常量表達(dá)式 { ... } 當(dāng)然,我們經(jīng)常會把它簡化成: for(;;) 既然循環(huán)控制體中的A只是在循環(huán)開始前作一個初始化的操作,那我這樣寫應(yīng)該也沒毛?。?/p> int i=0; for(printf('Number:\r\n');i<10;i++) { printf(' %d\r\n',i); } B是循環(huán)執(zhí)行的條件,而C是循環(huán)執(zhí)行后的操作,那我們就可以把一個標(biāo)準(zhǔn)的if語句寫成for的形式,而實(shí)現(xiàn)同樣的功能: if(strstr('hello world!','abc')) char *p; for(p=strstr('hello world!','abc');p;p=NULL) { printf('Find Sub-string'); } 以上的例子可能有些雞肋,“一個if能搞定的事情,我為什么要用for?”,沒錯。我們這里主要是為了解釋for循環(huán)的靈活用法。深入理解了它的本質(zhì),有助于我們在實(shí)際開發(fā)中讓工作事半功倍,以及看懂別人的代碼。 以下我再列舉幾個for循環(huán)靈活應(yīng)用的例子,供大家回味。 例1: char *p; 提示:printf 我們太熟悉了,但是有幾個人知道 printf 是有返回值的?輸出應(yīng)該是怎樣的? 例2: char *p; unsigned char n; for(p='ablmnl45ln',n=0;((\*p=='l')?(n++):0),\*p;p++); 提示:還記得C語言中的三目運(yùn)算和逗號表達(dá)式嗎?n應(yīng)該等于幾? 例3: unsigned char *index='C[XMZA[C[NK[RDEX@'; 提示:天書模式已開啟。如果看不懂,你可能會錯過什么哦! |
|