此系列文章,于振南老師向大家講述嵌入式C語言的一些高階知識,俗稱“騷操作”,助你水平再上一個臺階!
C語言,是一門非常靈活而強大的編程語言。同樣一個算法、一個功能,我們可以把它寫得中規中矩,也可以把它寫得晦澀難懂。而且很多自詡為編程高手的人,偏偏就喜歡把程序寫成天書,認為讓別人看不懂,卻能實現正確的功能,此乃技術高超的表現。
我不評價這樣的作法是否可取,因為每個人都有各自的風格和個性。讓他違背意愿去編程,那么編程可能就會變得索然無味,毫無樂趣。很多時候不是我們想把程序寫得難懂,而是我們要去看懂別人的程序。 在本文中,振南列舉一些我曾經見過和使用過的編程技巧,并進行深入的解析。
一、字符串的實質就是指針
字符串是C語言中最基礎的概念,也是最常被用到的。在嵌入式開發中,我們經常要將一些字符串通過串口顯示到串口助手或調試終端上,作為信息提示,以便讓我們了解程序的運行情況;或者是將一些常量的值轉為字符串,來顯示到液晶等顯示設備上。 那么,C語言中的字符串到底是什么?其實字符串本身就是一個指針,它的值(即指針所指向的地址)就是字符串首字符的地址。 為了解釋這個問題,我經常會舉這樣一個例子:如何將一個數值轉化為相應的16進制字符串。比如,把100轉為”0X64”。 我們可以寫這樣一個函數:
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]; }
字符串常量實質是內存中的字節序列,如下圖所示:
上面,振南說“字符串本身就是指針”,那么見證這句話真正意義的時刻來了,我們將上面程序進行簡化:
void Value2String(unsigned char value,char *str) { str[0]='0';str[1]='X';str[4]=0; str[2]="0123456789ABCDEF"[value>>4]; str[3]="0123456789ABCDEF"[value&0X0F]; }
Hex_Char_Table 這個指針變量其實是多余的,“字符串本身就是指針”,所以它后面可以直接用 [] 配合下標來取出其中的字符。凡是實質上為指針類型(即表達的是地址意義)的變量或常量,都可以直接用[]或*來訪問它所指向的數據序列中的數據元素。
二、轉義符
C語言中要表達一個字節數據序列(內存中連續存儲的若干個字節),我們可以使用字節數組,比如unsigned char array[10]={0,1,2,3,4,5,6,7,8,9}。 其實字符串,本質上也是一個字節序列,但是通常情況下它所存儲的字節的值均為 ASCII 中可打印字符的碼值,如’A’、’ ‘、’|’等。那在字符串中是否也可以出現其它的值呢?這樣,我們就可以用字符串的形式來表達一個字節序列了。很多時候,它可能比字節數組要方便一些。字符串中的轉義符就是用來干這個的。請看如下程序:
const unsigned char array[10]={0,1,2,3,4,5,6,7,8,9}; char *array="x00x01x02x03x04x05x06x07x08x09";這兩種寫法,array所指向的內存字節序列是基本一樣的(后者最后還有一個0)。當然,如果我們把array傳到strlen去計算長度,返回的值為0。因為它第一個字節的值為0。但是我們仍然可以使用array[n]的方式去訪問序列中的數據。
char *str="ABCDEFG"; char *str="x41x42x43x44x45x46x47";
上面程序中的兩種寫法,是完成等價的。
字符串中的轉義符的目的是為了在本應該只能看到ASCII可打印字符的序列中,可以表達其它數值或特殊字符。如經常使用的回車換行” ”,其實質就是”x0dx0a”;通常我們所說的字符串結束符?,其實就是0的八進制轉義表達形式。
三、字符串常量的連接
在研讀一些開源軟件的源代碼時,我見到了字符串常量的一個比較另類的用法,在這里介紹給大家。 有些時候,為了讓字符串常量內容層次更加清晰,就可以把一個長字符串打散成若干個短字符串,它們順序首尾相接,在意義上與長字符串是等價的。比如“0123456789ABCDEF”可以分解為“0123456789”“ABCDEF”,即多個字符串常量可以直接連接,夠成長字符串。這種寫法,在 printf 打印調試信息的時候可能會更多用到。
printf("A:%d B:%d C:%d D:%d E:%d F:%d ",1,2,3,4,5,6); printf("A:%d " "B:%d " "C:%d " "D:%d " "E:%d " "F:%d ",1,2,3,4,5,6);在 printf 的格式化串很長的時候,我們把它合理的打散,分為多行,程序就會顯得更多工整。
四、長字符串的拆分技巧
很多時候我們需要進行長字符串的拆分。問題是如何實現它? 很多人可能都會想到使用那個分隔字符,比如空格、逗號。然后去一個個數要提取的參數前面有幾個分隔字符,然后后將相應位置上的字符組成一個新的短字符串。如下圖所示: 這種方法固然可行,但是略顯笨拙。其實,對于這種有明顯分隔符的長字符串,我們可以采用“打散”或“爆炸”的思想,具體過程是這樣的:將長字符串中的所有分隔符全部替換為’?’,即字符串結束符。此時,長字符串就被分解成了在內存中順序存放的若干個短字符串。 如果要取出第n個短字符串,可以用這個函數:
char * substr(char *str,n) { unsigned char len=strlen(str); for(;len>0;len--) {if(str[len-1]==' ') str[len-1]=0;} for(;n>0;n--) { str+=(strlen(str)+1); } return str; }很多時候我們需要一次性訪問長字符串中的多個短字符串,此時振南經常會這樣來作:通過一個循環,將長字符串中的所有分隔符替換為’?’,在此過程中將每一個短字符串首字符的位置記錄到一個數組中,代碼如下:
unsigned char substr(unsigned char *pos,char *str) { unsigned char len=strlen(str); unsigned char n=0,i=0; for(;i舉個例子:我們要提取”abc 1000 50 off 2500”中的”abc”、”50”和”off”,可以使用上面的函數來實現。 unsigned char pos[10]; char str[30]; strcpy(str,"abc 1000 50 off 2500"); substr(pos,str); str+pos[0]; //"abc" str+pos[2]; //"50" str+pos[3]; //"off"
五、取出數值的各位數碼
在實際項目中,我們經常需要提取一個數值的某些位的數碼,比如用數碼管來顯示數值或將一個數值轉成字符串,都會涉及到這一操作。 那如何實現這一操作呢?雖然這個問題看似很簡單,但提出這一問題的人還不在少數。 請看下面的函數:
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; }它的主要操作就是除法和取余。這個函數只是取出一個整型數各位的數碼,那浮點呢?
其實一樣的道理,請看下面函數(我們默認整數與小數部分均取4位)。
void getdigi(unsigned char *digi1,unsigned char *digi2,unsigned float num) { unsigned int temp1=num; unsigned int temp2=((num-temp1)*10000); digi1[0]=(temp1/1000)%10; digi1[1]=(temp1/100)%10; digi1[2]=(temp1/10)%10; digi1[3]=(temp1)%10; digi2[0]=(temp2/1000)%10; digi2[1]=(temp2/100)%10; digi2[2]=(temp2/10)%10; digi2[3]=(temp2)%10; }有人說,我更喜歡用sprintf函數,直接將數值格式化打印到字符串里,各位數碼自然就得到了。char digi[10]; sprintf(digi,"%d",num); //**整型 char digi[10]; sprintf(digi,"%f",num); //**浮點
沒問題。但在嵌入式平臺上使用sprintf函數,通常代價是較大的。
作為嵌入式工程師,一定要惜字如金,尤其是在硬件資源相對較為緊張的情況下。sprintf非常強大,我們只是一個簡單的提取數值數碼或將數值轉為相應的字符串的操作,使用它有些暴殄天物。這種時候,我通常選擇寫一個小函數或者宏來自己實現。
六、printf的實質與使用技巧
printf是我們非常熟悉的一個入門級的標準庫函數,每當我們說出計算機金句”Hello World!”時,其實無意中就提到了它:printf(“hello world!”); 它可以某種特定的格式、進制或形式輸出任何變量、常量和字符串,為我們提供了極大的方便,甚至成為了很多人調試程序時重要的Debug手段。但是在嵌入式中,我們就需要剖析一下它的實質了。 printf 函數的底層是基于一個 fputc 的函數,它用于實現單個字符的具體輸出方式,比如是將字符顯示到顯示器上,或是存儲到某個數組中(類似sprintf),或者是通過串口發送出去,甚至不是串口,而是以太網、CAN、I2C等接口。 以下是一個STM32項目中fputc函數的實現:
int fputc(int ch, FILE *f) { while((USART1->SR&0X40)==0); { USART1->DR = (u8) ch; } return ch; }
fputc中將ch通過USART1發出。這樣,我們在調用printf的時候,相應的信息就會從USART1打印出來。
“上面你說的這些,我都知道,有什么新鮮的!”確實,通過串口打印信息是我們司空見慣的。 那么,下面的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將字符顯示在了液晶上(同時維護了字符的顯示位置信息),這樣當我們調用printf的時候,信息會直接顯示在液晶上。
說白了,fputc 就是對數據進行了定向輸出。這樣我們可以把 printf 變得更靈活,來應對更多樣的應用需求。 在振南經歷的項目中,曾經有過這樣的情況:單片機有多個串口,串口1用于打印調試信息,串口2與ESP8266 WIFI模塊通信,串口3與SIM800 GPRS模塊通信。3個串口都需要格式化輸出,但printf只有一個,這該怎么辦? 我們解決方法是,修改fputc使得printf可以由3個串口分時復用,具體實現如下:
unsigned char us=0; int fputc(int ch,FILE *f) { switch(us) { case 0: while((USART1->SR&0X40)==0); USART1->DR=(u8)ch; break; case 1: while((USART2->SR&0X40)==0); USART2->DR=(u8)ch; break; case 2: while((USART3->SR&0X40)==0); USART3->DR=(u8)ch; break; } return ch; }在調用的時候,根據需要將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 "); U_TO_SIM800 printf("AT ");
七、關于浮點數的傳輸
很多人不能很好的使用和處理浮點,其主要根源在于對它的表達與存儲方式不是很理解。最典型的例子就是經常有人問我:“如何使用串口來發送一個浮點數?” 我們知道C語言中有很多數據類型,其中unsigned char、unsigned short、unsigned int、unsigned long我們稱其為整型,顧名思義它們可以表達整型數。而能夠表達的數值范圍與數據類型所占用的字節數有關。數值的表達方法比如簡單,如下圖所示: 一個字節可以表達0~255,兩個字節(unsigned short)自然就可以表達0~65535,依次類推。 當需要把一個整型數值發送出去的時候,我們可以這樣作:
unsigned short a=0X1234; UART_Send_Byte(((unsigned char *)&a)[0]); UART_Send_Byte(((unsigned char *)&a)[1]);也就是將構成整型的若干字節順序發送即可。當然,接收方一定要知道如何還原數據,也就是說,它要知道自己接收到的若干字節拼在一起是什么類型,這是由具體通信協議來保障的。unsigned char buf[2]; usnigned short a; UART_Receive_Byte(buf+0); UART_Receive_Byte(buf+1); a=(*(usnigned short *)buf);OK,關于整型比較容易理解。但換成float,很多人就有些迷糊了。因為float的數值表達方式有些復雜。有些人使用下面的方法來進行浮點的發送:float a=3.14; char str[10]={0}; ftoa(str,a); //**浮點轉為字符串* *即3.14**轉為"3.14" UART_Send_Str(str); //**通過串口將字符串發出很顯然,這種方法非常的“業余”。
還有人問我:“浮點小數字前后的數字可以發送,但是小數點怎么發?”這赤裸裸的體現了他對浮點類型的誤解。
不要被float數值的表象迷惑,它實質上只不過是4個字節而已,如下圖所示:
所以,正確的發送浮點數的方法是這樣的:
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]);接收者將數據還原為浮點:unsigned char buf[4]; float a; UART_Receive_Byte(buf+0); UART_Receive_Byte(buf+1); UART_Receive_Byte(buf+2); UART_Receive_Byte(buf+3); a=*((float *)buf);
其實我們應該發現數據類型的實質:不論是什么數據類型,它的基本組成無非就是內存中存儲的若干個字節。只是我們人為的賦予了這些字節特定的編碼方式或數值表達。看穿了這些,我們就認識到了數據的本質了,我們甚至可以直接操作數據。
八、關于數據的直接操作
直接操作數據?我們來舉個例子:取一個整型數的相反數。一般的實現方法是這樣的:
int a=10; int b=-a; //-1*a;這樣的操作可能會涉及到一次乘法運算,花費更多的時間。當我們了解了整型數的實質,就可以這樣來作:int a=10; int b=(~a)+1;這也許還不足以說明問題,那我們再來看一個例子:取一個浮點數的相反數。似乎只能這樣來作:float a=3.14; float b=a*-1.0;其實,我們可以這樣來作:float a=3.14; float b; ((unsigned char *)&a)[3]^=0X80; b=a;
沒錯,我們可以直接修改浮點在內存中的高字節的符號位。這比乘以-1.0的方法要高效的多。
當然,這些操作都需要你對 C 語言中的指針有爐火純青的掌握。
九、浮點的四舍五入與比較
我們先說第一個問題:如何實現浮點的四舍五入?很多人遇到過這個問題,其實很簡單,只需要把浮點+0.5然后取整即可。 OK,第二個問題:浮點的比較。這個問題還有必要好好說一下。 首先我們要知道,C語言中的判等,即==,是一中強匹配的行為。也就是,比較雙方必須每一個位都完全一樣,才認定它們相等。這對于整型來說,是可以的。但是float類型則不適用,因為兩個看似相等的浮點數,其實它們的內存表達不能保證每一個位都完全一樣。 這個時候,我們作一個約定:兩個浮點只要它們之差m足夠小,則認為它們相等,m一般取10e-6。也就是說,只要兩個浮點小數點后6位相同,則認為它們相等。也正是因為這個約定,很多C編譯器把float的精度設定為小數點后7位,比如ARMCC(MDK的編譯器)。
float a,b; if(a==b) ... //**錯誤 if(fabs(a-b) <= 0.000001) ...//**正確
十、出神入化的for循環
for循環我們再熟悉不過了,通常我們使用它都是中規中矩的,如下例:
int i; for(i=0;i<100;i++) {...}
但是,如果我們對for循環的本質有更深刻的理解的話,就可以把它用得出神入化。
for后面的括號中的東西我稱之為“循環控制體”,分為三個部分,如下圖所示: A、B、C三個部分,其實隨意性很大,可以是任意一個表達式。所以,我們可以這樣寫一個死循環:
for(1;1;1) //1**本身就是一個表達式:常量表達式 { ... }當然,我們經常會把它簡化成:for(;;) { ... }既然循環控制體中的A只是在循環開始前作一個初始化的操作,那我這樣寫應該也沒毛病:int i=0; for(printf("Number: ");i<10;i++) { printf(" %d ",i); }B是循環執行的條件,而C是循環執行后的操作,那我們就可以把一個標準的if語句寫成for的形式,而實現同樣的功能:if(strstr("hello world!","abc")) { printf("Find Sub-string"); }char *p; for(p=strstr("hello world!","abc");p;p=NULL) { printf("Find Sub-string"); }
以上的例子可能有些雞肋,“一個if能搞定的事情,我為什么要用for?”,沒錯。我們這里主要是為了解釋for循環的靈活用法。深入理解了它的本質,有助于我們在實際開發中讓工作事半功倍,以及看懂別人的代碼。
以下我再列舉幾個for循環靈活應用的例子,供大家回味。 例1:
char *p; for(p="abcdefghijklmnopqrstuvwxyz"; printf(p); p++) printf(" ");
提示:printf我們太熟悉了,但有幾個人知道printf是有返回值的?輸出應該是怎樣的?
例2:
char *p; unsigned char n; for(p="ablmnl45ln",n=0;((*p=='l')?(n++):0),*p;p++);提示:還記得C語言中的三目運算和逗號表達式嗎?n應該等于幾?
例3:
unsigned char *index="C[XMZA[C[NK[RDEX@"; char *alphabet="EHUIRZWXABYPOMQCTGSJDFKLNV "; int i=0; for(;(('@'!=index[i])?1:(printf("!!Onz "),0));i++) { printf("%c",alphabet[index[i]-'A']); }
-
嵌入式
+關注
關注
5046文章
18817瀏覽量
298537 -
C語言
+關注
關注
180文章
7575瀏覽量
134068 -
編程
+關注
關注
88文章
3521瀏覽量
93268
原文標題:C語言的一些“騷操作”及其深層理解
文章出處:【微信號:玩點嵌入式,微信公眾號:玩點嵌入式】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論