一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

C語言的一些“騷操作”及其深層理解

 新用戶0118F7lQ 2023-08-28 發(fā)布于江蘇

此系列文章,于振南老師向大家講述嵌入式C語言的一些高階知識,俗稱“騷操作”,助你水平再上一個臺階!
C語言,是一門非常靈活而強(qiáng)大的編程語言。同樣一個算法、一個功能,我們可以把它寫得中規(guī)中矩,也可以把它寫得晦澀難懂。而且很多自詡為編程高手的人,偏偏就喜歡把程序?qū)懗商鞎?,認(rèn)為讓別人看不懂,卻能實(shí)現(xiàn)正確的功能,此乃技術(shù)高超的表現(xiàn)。
我不評價這樣的作法是否可取,因?yàn)槊總€人都有各自的風(fēng)格和個性。讓他違背意愿去編程,那么編程可能就會變得索然無味,毫無樂趣。很多時候不是我們想把程序?qū)懙秒y懂,而是我們要去看懂別人的程序。
在本文中,振南列舉一些我曾經(jīng)見過和使用過的編程技巧,并進(jìn)行深入的解析。

一、字符串的實(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)
{

str[0]='0';str[1]='X';str[4]=0;

str[2]='0123456789ABCDEF'[value>>4];

str[3]='0123456789ABCDEF'[value&0X0F];
}

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';

char *str='\x41\x42\x43\x44\x45\x46\x47';

上面程序中的兩種寫法,是完成等價的。

字符串中的轉(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)
{
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;
}

很多時候我們需要一次性訪問長字符串中的多個短字符串,此時振南經(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];
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'

圖片

五、取出數(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)
{
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函數(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)
{
while((USART1->SR&0X40)==0);
{
USART1->DR = (u8) ch;
}

return ch;
}

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; 

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;
}

在調(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; 

UART_Send_Byte(((unsigned char *)&a)[0]);

UART_Send_Byte(((unsigned char *)&a)[1]);

也就是將構(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;

char str[10]={0};

ftoa(str,a); //**浮點(diǎn)轉(zhuǎn)為字符串* *即3.14**轉(zhuǎn)為'3.14'

UART_Send_Str(str); //**通過串口將字符串發(fā)出

很顯然這種方法非常的“業(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];
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);
其實(shí)我們應(yīng)該發(fā)現(xiàn)數(shù)據(jù)類型的實(shí)質(zhì):不論是什么數(shù)據(jù)類型,它的基本組成無非就是內(nèi)存中存儲的若干個字節(jié)。只是我們?nèi)藶榈馁x予了這些字節(jié)特定的編碼方式或數(shù)值表達(dá)??创┝诉@些,我們就認(rèn)識到了數(shù)據(jù)的本質(zhì)了,我們甚至可以直接操作數(shù)據(jù)。

八、關(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;

int b=(~a)+1;

這也許還不足以說明問題,那我們再來看一個例子:取一個浮點(diǎn)數(shù)的相反數(shù)。似乎只能這樣來作:

float a=3.14;

float b=a*-1.0;

其實(shí)我們可以這樣來作:

float a=3.14;
float b;

((unsigned char *)&a)[3]^=0X80;
b=a;

沒錯,我們可以直接修改浮點(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(i=0;i<100;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'))
{
printf('Find Sub-string');
}
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; 

for(p='abcdefghijklmnopqrstuvwxyz'; printf(p); p++)
printf('\r\n');

提示: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@';

char *alphabet='EHUIRZWXABYPOMQCTGSJDFKLNV ';

int i=0;

for(;(('@'!=index[i])?1:(printf('!!Onz\r\n'),0));i++)
{
printf('%c',alphabet[index[i]-'A']);
}

提示:天書模式已開啟。如果看不懂,你可能會錯過什么哦!

圖片


    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    91日韩欧美国产视频| 青青草草免费在线视频| 欧美中文字幕日韩精品| 国产又粗又猛又大爽又黄同志 | 国产欧美日韩精品自拍| 日本男人女人干逼视频| 1024你懂的在线视频| 久热人妻中文字幕一区二区| 亚洲人午夜精品射精日韩| 国产欧美日韩精品自拍| 亚洲午夜av一区二区| 福利专区 久久精品午夜| 日本视频在线观看不卡| 白白操白白在线免费观看| 黄色片一区二区在线观看| 加勒比人妻精品一区二区| 午夜福利激情性生活免费视频 | 大胆裸体写真一区二区| 欧美人妻免费一区二区三区| 色婷婷激情五月天丁香| 久久亚洲精品成人国产| 九九热在线视频精品免费| 五月天六月激情联盟网| 日本福利写真在线观看| 五月婷婷六月丁香在线观看| 亚洲一区精品二人人爽久久| 九七人妻一区二区三区| 亚洲综合天堂一二三区| 国产精品人妻熟女毛片av久久| 亚洲一区二区三区有码| 国产一级二级三级观看| 国产成人精品视频一二区| 熟女少妇一区二区三区蜜桃| 黄色片一区二区在线观看| 亚洲视频一区二区久久久| 久久精品国产亚洲av久按摩| 乱女午夜精品一区二区三区 | 国产欧美亚洲精品自拍| 一区二区日本一区二区欧美| 午夜视频成人在线观看| 日韩免费午夜福利视频|