<menu id="w8yyk"><menu id="w8yyk"></menu></menu>
  • <dd id="w8yyk"><nav id="w8yyk"></nav></dd>
    <menu id="w8yyk"></menu>
    <menu id="w8yyk"><code id="w8yyk"></code></menu>
    <menu id="w8yyk"></menu>
    <xmp id="w8yyk">
    <xmp id="w8yyk"><nav id="w8yyk"></nav>
  • 網站首頁 > 物聯資訊 > 技術分享

    嵌入式 C 語言的可變參數表函數的設計

    2016-09-28 00:00:00 廣州睿豐德信息科技有限公司 閱讀
    睿豐德科技 專注RFID識別技術和條碼識別技術與管理軟件的集成項目。質量追溯系統、MES系統、金蝶與條碼系統對接、用友與條碼系統對接 首先在介紹可變參數表函數的設計之前,我們先來介紹一下最經典的可變參數表printf函數的實現原理。
    一、printf函數的實現原理
    在C/C++中,對函數參數的掃描是從后向前的。C/C++的函數參數是通過壓入堆棧的方式來給函數傳參數的(堆棧是一種先進后出的數據結構),最先壓入的參數最后出來,在計算機的內存中,數據有2塊,一塊是堆,一塊是棧(函數參數及局部變量在這里),而棧是從內存的高地址向低地址生長的,控制生長的就是堆棧指針了,最先壓入的參數是在最上面,就是說在所有參數的最后面,最后壓入的參數在最下面,結構上看起來是第一個,所以最后壓入的參數總是能夠被函數找到,因為它就在堆棧指針的上方。printf的第一個被找到的參數就是那個字符指針,就是被雙引號括起來的那一部分,函數通過判斷字符串里控制參數的個數來判斷參數個數及數據類型,通過這些就可算出數據需要的堆棧指針的偏移量了,下面給出printf("%d,%d",a,b);(其中a、b都是int型的)的匯編代碼
    [cpp] view plaincopy  
    1. .section  
    2. .data  
    3. string out = "%d,%d"  
    4. push b  
    5. push a  
    6. push $out  
    7. call printf  
    你會看到,參數是最后的先壓入棧中,最先的后壓入棧中,參數控制的那個字符串常量是最后被壓入的,所以這個常量總是能被找到的。
    二、可變參數表函數的設計
          標準庫提供的一些參數的數目可以有變化的函數。例如我們很熟悉的printf,它需要有一個格式串,還應根據需要為它提供任意多個“其他參數”。這種函數被稱作“具有變長度參數表的函數”,或簡稱為“變參數函數”。我們寫程序中有時也可能需要定義這種函數。要定義這類函數,就必須使用標準頭文件<stdarg.h>,使用該文件提供的一套機制,并需要按照規定的定義方式工作。本節介紹這個頭文件提供的有關功能,它們的意義和使用,并用例子說明這類函數的定義方法。
          C中變長實參頭文件stdarg.h提供了一個數據類型va-list和三個宏(va-start、va-arg和va-end),用它們在被調用函數不知道參數個數和類型時對可變參數表進行測試,從而為訪問可變參數提供了方便且有效的方法。va-list是一個char類型的指針,當被調用函數使用一個可變參數時,它聲明一個類型為va-list的變量,該變量用來指向va-arg和va-end所需信息的位置。下面給出va_list在C中的源碼:
    [cpp] view plaincopy  
    1. typedef char *  va_list;  
         void va-start(va-list ap,lastfix)是一個宏,它使va-list類型變量ap指向被傳遞給函數的可變參數表中的第一個參數,在第一次調用va-arg和va-end之前,必須首先調用該宏。va-start的第二個參數lastfix是傳遞給被調用函數的最后一個固定參數的標識符。va-start使ap只指向lastfix之外的可變參數表中的第一個參數,很明顯它先得到第一個參數內存地址,然后又加上這個參數的內存大小,就是下個參數的內存地址了。下面給出va_start在C中的源碼:
    [cpp] view plaincopy  
    1. #define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )  
    2. #define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )   //得到可變參數中第一個參數的首地址  
          type va-arg(va-list ap,type)也是一個宏,其使用有雙重目的,第一個是返回ap所指對象的值,第二個是修改參數指針ap使其增加以指向表中下一個參數。va-arg的第二個參數提供了修改參數指針所必需的信息。在第一次使用va-arg時,它返回可變參數表中的第一個參數,后續的調用都返回表中的下一個參數,下面給出va_arg在C中的源碼:
    [cpp] view plaincopy  
    1. #define va_arg(ap,type)    ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) )    //將參數轉換成需要的類型,并使ap指向下一個參數  
         在使用va-arg時,要注意第二個參數所用類型名應與傳遞到堆棧的參數的字節數對應,以保證能對不同類型的可變參數進行正確地尋址,比如實參依次為char型、char * 型、int型和float型時,在va-arg中它們的類型則應分別為int、char *、int和double.
         void va-end(va-list ap)也是一個宏,該宏用于被調用函數完成正常返回,功能就是把指針ap賦值為0,使它不指向內存的變量。下面給出va_end在C中的源碼:
    [cpp] view plaincopy  
    1. #define va_end(ap)      ( ap = (va_list)0 )  
         va-end必須在va-arg讀完所有參數后再調用,否則會產生意想不到的后果。特別地,當可變參數表函數在程序執行過程中不止一次被調用時,在函數體每次處理完可變參數表之后必須調用一次va-end,以保證正確地恢復棧。
        一個變參數函數至少需要有一個普通參數,其普通參數可以具有任何類型。在函數定義中,這種函數的最后一個普通參數除了一般的用途之外,還有其他特殊用途。下面從一個例子開始說明有關的問題。
    假設我們想定義一個函數sum,它可以用任意多個整數類型的表達式作為參數進行調用,希望sum能求出這些參數的和。這時我們應該將sum定義為一個只有一個普通參數,并具有變長度參數表的函數,這個函數的頭部應該是(函數原型與此類似):
    int sum(int n, ...)
    我們實際上要求在函數調用時,從第一個參數n得到被求和的表達式個數,從其余參數得到被求和的表達式。在參數表最后連續寫三個圓點符號,說明這個函數具有可變數目的參數。凡參數表具有這種形式(最后寫三個圓點),就表示定義的是一個變參數函數。注意,這樣的三個圓點只能放在參數表最后,在所有普通參數之后。
    下面假設函數sum里所用的va_list類型的變量的名字是vap。在能夠用vap訪問實際參數之前,必須首先用宏a_start對這個變量進行初始化。宏va_start的類型特征可以大致描述為:
    va_start(va_list vap, 最后一個普通參數)
    在函數sum里對vap初始化的語句應當寫為:

    va_start(vap, n); 相當于  char *vap= (char *)&n + sizeof(int);
    此時vap正好指向n后面的可變參數表中的第一個參數。

    在完成這個初始化之后,我們就可以通過另一個宏va_arg訪問函數調用的各個實際參數了。宏va_arg的類型特征可以大致地描述為:
    類型 va_arg(va_list vap, 類型名)
    在調用宏va_arg時必須提供有關實參的實際類型,這一類型也將成為這個宏調用的返回值類型。對va_arg的調用不僅返回了一個實際參數的值(“當前”實際參數的值),同時還完成了某種更新操作,使對這個宏va_arg的下次調用能得到下一個實際參數。對于我們的例子,其中對宏va_arg的一次調用應當寫為:
    v = va_arg(vap, int);
    這里假定v是一個有定義的int類型變量。
    在變參數函數的定義里,函數退出之前必須做一次結束動作。這個動作通過對局部的va_list變量調用宏va_end完成。這個宏的類型特征大致是:
    void va_end(va_list vap);
    三、棧中參數分布以及宏使用后的指針變化說明如下:
    RFID設備管理軟件
    下面是函數sum的完整定義,從中可以看到各有關部分的寫法:
    [cpp] view plaincopy  
    1. #include<iostream>  
    2. using namespace std;  
    3. #include<stdarg.h>  
    4.   
    5. int sum(int n,...)  
    6. {  
    7.     int i , sum = 0;  
    8.     va_list vap;  
    9.     va_start(vap , n);     //指向可變參數表中的第一個參數  
    10.     for(i = 0 ; i < n ; ++i)  
    11.         sum += va_arg(vap , int);     //取出可變參數表中的參數,并修改參數指針vap使其增加以指向表中下一個參數  
    12.     va_end(vap);    //把指針vap賦值為0  
    13.     return sum;  
    14. }  
    15. int main(void)  
    16. {  
    17.     int m = sum(3 , 45 , 89 , 72);  
    18.     cout<<m<<endl;  
    19.     return 0;  
    20. }  
    這里首先定義了va_list變量vap,而后對它初始化。循環中通過va_arg取得順序的各個實參的值,并將它們加入總和。最后調用va_end結束。
    下面是調用這個函數的幾個例子:
    k = sum(3, 5+8, 7, 26*4);
    m = sum(4, k, k*(k-15), 27, (k*k)/30);
    函數sum中首先定義了可變參數表指針vap,而后通過va_start ( vap, n )取得了參數表首地址(賦值給了vap),其后的for循環則用來遍歷可變參數表。這種遍歷方式與我們在數據結構教材中經常看到的遍歷方式是類似的。
      函數sum看起來簡潔明了,但是實際上printf的實現卻遠比這復雜。sum函數之所以看起來簡單,是因為:
      1、sum函數可變參數表的長度是已知的,通過num參數傳入;
      2、sum函數可變參數表中參數的類型是已知的,都為int型。
      而printf函數則沒有這么幸運。首先,printf函數可變參數的個數不能輕易的得到,而可變參數的類型也不是固定的,需由格式字符串進行識別(由%f、%d、%s等確定),因此則涉及到可變參數表的更復雜應用。
    在這個函數中,需通過對傳入的格式字符串(首地址為lpStr)進行識別來獲知可變參數個數及各個可變參數的類型,具體實現體現在for循環中。譬如,在識別為%d后,做的是va_arg ( vap, int ),而獲知為%l和%lf后則進行的是va_arg ( vap, long )、va_arg ( vap, double )。格式字符串識別完成后,可變參數也就處理完了。
    在編寫和使用具有可變數目參數的函數時,有幾個問題值得注意。
    第一:調用va_arg將更新被操作的va_list變量(如在上例的vap),使下次調用可以得到下一個參數。在執行這個操作時,va_arg并不知道實際有幾個參數,也不知道參數的實際類型,它只是按給定的類型完成工作。因此,寫程序的人應在變參數函數的定義里注意控制對實際參數的處理過程。上例通過參數n提供了參數個數的信息,就是為了控制循環。標準庫函數printf根據格式串中的轉換描述的數目確定實際參數的個數。如果這方面信息有誤,函數執行中就可能出現嚴重問題。編譯程序無法檢查這里的數據一致性問題,需要寫程序的人自己負責。在前面章節里,我們一直強調對printf等函數調用時,要注意格式串與其他參數個數之間一致性,其原因就在這里。
    第二:編譯系統無法對變參數函數中由三個圓點代表的那些實際參數做類型檢查,因為函數的頭部沒有給出這些參數的類型信息。因此編譯處理中既不會生成必要的類型轉換,也不會提供類型錯誤信息。考慮標準庫函數printf,在調用這個函數時,不但實際參數個數可能變化,各參數的類型也可能不同,因此不可能有統一方式來描述它們的類型。對于這種參數,C語言的處理方式就是不做類型檢查,要求寫程序的人保證函數調用的正確性。
    假設我們寫出下面的函數調用:
    k = sum(6, 2.4, 4, 5.72, 6, 2);

    編譯程序不會發現這里參數類型不對,需要做類型轉換,所有實參都將直接傳給函數。函數里也會按照內部定義的方式把參數都當作整數使用。編譯程序也不會發現參數個數與6不符。這一調用的結果完全由編譯程序和執行環境決定,得到的結果肯定不會是正確的。

    四、簡單的練習

     

    問題1:可變長參數的獲取
      有這樣一個具有可變長參數的函數,其中有下列代碼用來獲取類型為float的實參:
      va_arg (argp, float);
      這樣做可以嗎?
      答案與分析:
      不可以。在可變長參數中,應用的是"加寬"原則。也就是float類型被擴展成double;char、 short類型被擴展成int。因此,如果你要去可變長參數列表中原來為float類型的參數,需要用va_arg(argp, double)。對char和short類型的則用va_arg(argp, int)。
      問題2:定義可變長參數的一個限制
      為什么我的編譯器不允許我定義如下的函數,也就是可變長參數,但是沒有任何的固定參數?
    [cpp] view plaincopy  
    1. int f(...)  
    2. {  
    3.     ......  
    4.     ......  
    5.     ......  
    6. }  
    答案與分析:
      不可以。這是ANSI C 所要求的,你至少得定義一個固定參數。這個參數將被傳遞給va_start(),然后用va_arg()和va_end()來確定所有實際調用時可變長參數的類型和值。
          問題3:如何判別可變參數函數的參數類型?
    函數形式如下:
    [cpp] view plaincopy  
    1. void fun(char *str ,...)  
    2. {  
    3.     ......  
    4.     ......  
    5.     ......  
    6. }  
    若傳的參數個數大于1,如何判別第2個以后傳參的參數類型???
    答案與分析:
    這個是沒有辦法判斷的,例如printf( "%d%c%s ",   ....)是通過格式串中的%d、 %c、 %s來確定后面參數的類型,其實你也可以參考這種方法來判斷不定參數的類型。
    最后,奉獻上自己寫的一個printf函數
    [cpp] view plaincopy  
    1. #include<stdio.h>  
    2. #include<stdarg.h>  
    3.   
    4. void myitoa(int n, char str[], int radix)  
    5. {  
    6.     int i , j , remain;  
    7.     char tmp;  
    8.     i = 0;  
    9.     do  
    10.     {  
    11.         remain = n % radix;  
    12.         if(remain > 9)  
    13.             str[i] = remain  - 10 + 'A';  
    14.         else  
    15.             str[i] = remain + '0';  
    16.         i++;  
    17.     }while(n /= radix);  
    18.     str[i] = '\0';  
    19.   
    20.     for(i-- , j = 0 ; j <= i ; j++ , i--)  
    21.     {  
    22.         tmp = str[j];  
    23.         str[j] = str[i];  
    24.         str[i] = tmp;  
    25.     }  
    26.   
    27. }  
    28.   
    29. void myprintf(const char *format, ...)  
    30. {  
    31.     char c, ch, str[30];  
    32.     va_list ap;  
    33.   
    34.     va_start(ap, format);  
    35.     while((c = *format))  
    36.     {  
    37.         switch(c)  
    38.         {  
    39.         case '%':  
    40.             ch = *++format;  
    41.             switch(ch)  
    42.             {  
    43.             case 'd':  
    44.                 {  
    45.                     int n = va_arg(ap, int);  
    46.                     myitoa(n, str, 10);  
    47.                     fputs(str, stdout);  
    48.                     break;  
    49.                 }  
    50.             case 'x':  
    51.                 {  
    52.                     int n = va_arg(ap, int);  
    53.                     myitoa(n, str, 16);  
    54.                     fputs(str, stdout);  
    55.                     break;  
    56.                 }  
    57.             case 'f':  
    58.                 {  
    59.                     double f = va_arg(ap, double);  
    60.                     int n;  
    61.                     n = f;  
    62.                     myitoa(n, str, 10);  
    63.                     fputs(str, stdout);  
    64.                     putchar('.');  
    65.                     n = (f - n) * 1000000;  
    66.                     myitoa(n, str, 10);  
    67.                     fputs(str, stdout);  
    68.                     break;  
    69.                 }  
    70.             case 'c':  
    71.                 {  
    72.                     putchar(va_arg(ap, int));  
    73.                     break;  
    74.                 }  
    75.             case 's':  
    76.                 {  
    77.                     char *p = va_arg(ap, char *);  
    78.                     fputs(p, stdout);  
    79.                     break;  
    80.                 }  
    81.             case '%':  
    82.                 {  
    83.                     putchar('%');  
    84.                     break;  
    85.                 }  
    86.             default:  
    87.                 {  
    88.                     fputs("format invalid!", stdout);  
    89.                     break;  
    90.                 }  
    91.             }  
    92.             break;  
    93.         default:  
    94.             putchar(c);  
    95.             break;  
    96.         }  
    97.         format++;  
    98.     }  
    99.     va_end(ap);  
    100. }  
    101.   
    102. int main(void)  
    103. {  
    104.     myprintf("%d, %x, %f, %c, %s, %%,%a\n", 10, 15, 3.14, 'B', "hello");  
    105.     return 0;  
    106. }  
     RFID管理系統集成商 RFID中間件 條碼系統中間層 物聯網軟件集成
    最近免费观看高清韩国日本大全