參數可變函數的實現(上)

此文獻給如我一般還在探索C語言之路的朋友們。

 

注:本文中測試程序的編譯環境爲win2000VC6.0

緣起:

作爲一個程序員,我沒有寫過參數可變的函數,我相信大部分朋友也沒有涉及過,或者我的境界層次太低了。那麼緣何我要去揭這一層面紗呢?因爲好奇!

我是個思維具有極大惰性的人,曾經識得參數可變函數,也懶得去深究,但是它的三點(函數聲明時參數列表中的“”)卻深刻的映入了我的記憶裏,而且是帶着若干個閃耀的問號。可是就在昨天,在拜讀某君的高論時,它再一次出現了。我的資質真的是不太夠,因爲某君在談到它時只是給出了<stdarg.h>中關於它的宏定義,我想大概在高手眼裏,點這一下就神會了吧。可是他這麼輕輕一點卻使留在記憶裏曾經的那幾個問號無限的膨脹,以至於我這個又菜又懶的所謂程序員也萌生了莫大的好奇。

 

破題:

   但凡所謂“實現”都是從沒有到有的過程,但是我只是想去解惑它的實現,因爲它原本就是好端端的正爲成千上萬的程序員們服務。

   還是從我們熟悉的printf說起:

   如果你是個C語言的程序員,無論你是初學者還是高高手,對於printf都不會陌生,甚至你已經用了無數次了。我已經說過我是個有極大惰性的人,所以每次用printf都是照本宣科,規規矩矩的按教科書上說的做,從來沒有問過一個爲什麼,這就是所謂的“熟視無睹”吧。

其實,printf函數是一個典型的參數可變的函數。在保證它的第一個參數是字符串的條件下,你可以輸任意數量任意合法類型的參數。只要你在第一個字符串參數中使用了對應的格式化字符串,你就可以輸出正確的值。這難道不是件很有趣的事嗎?那它是怎麼做到的?

1,首先,怎麼得到參數的值。對於一般的函數,我們可以通過參數對應在參數列表裏的標識符來得到。但是參數可變函數那些可變的參數是沒有參數標識符的,它只有“”,所以通過標識符來得到是不可能的,我們只有另闢途徑。

我們知道函數調用時都會分配棧空間,而函數調用機制中的棧結構如下圖所示:

                       |     ......     |

                       ------------------

                       |     參數2      |

                       ------------------

                       |     參數1      |

                       ------------------

                       |    返回地址    |

                       ------------------

                       |調用函數運行狀態|

                       ------------------

可見,參數是連續存儲在棧裏面的,那麼也就是說,我們只要得到可變參數的前一個參數的地址,就可以通過指針訪問到那些可變參數。但是怎麼樣得到可變參數的前一個參數的地址呢?不知道你注意到沒有,參數可變函數在可變參數之前必有一個參數是固定的,並使用標識符,而且通常被聲明爲char*類型,printf函數也不例外。這樣的話,我們就可以通過這個參數對應的標識符來得到地址,從而訪問其他參數變得可能。我們可以寫一個測試程序來試一下:

#include <stdio.h>

 

void va_test(char* fmt,...);//參數可變的函數聲明

 

void main()

{

    int a=1,c=55;

       char b='b';

    va_test("",a,b,c);//用四個參數做測試

}

 

void va_test(char* fmt,...) //參數可變的函數定義,注意第一個參數爲char* fmt

{

   char *p=NULL;

 

      p=(char *)&fmt;//注意不是指向fmt而是指向&fmt並且強制轉化爲char *,以便一個一個字節訪問

      for(int i = 0;i<16;i++)//16是通過計算的值(參數個數*4個字節),只是爲了測試,暫且將就一下

      {

                printf("%.4d ",*p);//輸出p指針指向地址的值

        p++;

      }

}

 

編譯運行的結果爲

  0056 0000 0066 0000 | 0001 0000 0000 0000 | 0098 0000 0000 0000 | 0055 0000 0000 0000

 

由運行結果可見,通過這樣方式可以逐一獲得可變參數的值。

至於爲什麼通常被聲明爲char*類型,我們慢慢看來。

2,怎樣確定參數類型和數量

通過上述的方式,我們首先解決了取得可變參數值的問題,但是對於一個參數,值很重要,其類型同樣舉足輕重,而對於一個函數來講參數個數也非常重要,否則就會產生了一系列的麻煩來。通過訪問存儲參數的棧空間,我們並不能得到關於類型的任何信息和參數個數的任何信息。我想你應該想到了——使用char *參數。Printf函數就是這樣實現的,它把後面的可變參數類型都放到了char *指向的字符數組裏,並通過%來標識以便與其它的字符相區別,從而確定了參數類型也確定了參數個數。其實,用何種方式來到達這樣的效果取決於函數的實現。比如說,定義一個函數,預知它的可變參數類型都是int,那麼固定參數完全可以用int類型來替換char*類型,因爲只要得到參數個數就可以了。

3,言歸正傳

   我想到了這裏,大概的輪廓已經呈現出來了。本來想就此作罷的(我的惰性使然),但是一想到如果不具實用性便可能是一堆廢物,枉費我打了這麼些字,決定還是繼續下去。

   我是比較抵制用那些不明所以的宏定義的,所以在上面的闡述裏一點都沒有涉及定義在<stdarg.h>va(variable-argument)宏。事實上,當時讓我產生極大疑惑和好奇的正是這幾個宏定義。但是現在我們不得不要去和這些宏定義打打交道,畢竟我們在討生計的時候還得用上他們,這也是我曰之爲“言歸正傳”的理由。

   好了,我們來看一下那些宏定義。

   打開<stdarg.h>文件,找一下va_*的宏定義,發現不單單隻有一組,但是在各組定義前都會有宏編譯。宏編譯指示的是不同硬件平臺和編譯器下用怎樣的va宏定義。比較一下,不同之處主要在偏移量的計算上。我們還是拿個典型又熟悉的——X86的相關宏定義:

1)typedef char * va_list;

2)#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

 

3)#define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )

4)#define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

5)#define va_end(ap)      ( ap = (va_list)0 )

 

我們逐一看來:

第一個我想不必說了,類型定義罷了。第二個是頗有些來頭的,我們也不得不搞懂它,因爲後面的兩個關鍵的宏定義都用到了。不知道你夠不夠細心,有沒有發現在上面的測試程序中,第二個可變參數明明是char類型,可是在輸出結果中佔了4byte。難道所有的參數都會佔4byte的空間?那如果是double類型的參數,且不是會丟失數據!如果你不嫌麻煩的話,再去做個測試吧,在上面的測試程序中用一個double類型(長度爲8byte)和一個long double類型(長度爲10byte)做可變參數。發現什麼?double類型佔了8byte,long double佔了12byte。好像都是4的整數倍哦。不得不引出另一個概念了“對齊(alignment)”,所謂對齊,對Intel80x86機器來說就是要求每個變量的地址都是sizeof(int)的倍數。原來我們搞錯了,char類型的參數只佔了1byte,但是它後面的參數因爲對齊的關係只能跳過3byte存儲,而那3byte也就浪費掉了。那爲什麼要對齊?因爲在對齊方式下,CPU 的運行效率要快得多(舉個例子吧,要說明的是下面的例子是我從網上摘錄下來的,不記得出處了。

示例:如下圖,當一個long 型數(如圖中long1)在內存中的位置正好與內存的字邊界對齊時,CPU 存取這個數只需訪問一次內存,而當一個long 型數(如圖中的long2)在內存中的位置跨越了字邊界時,CPU 存取這個數就需要多次訪問內存,如i960cx 訪問這樣的數需讀內存三次(一個BYTE、一個SHORT、一個BYTE,由CPU 的微代碼執行,對軟件透明),所以對齊方式下CPU 的運行效率明顯快多了。

1       8       16      24      32   

------- ------- ------- ---------

| long1 | long1 | long1 | long1 |

------- ------- ------- ---------

|        |        |         | long2 |

------- ------- ------- ---------

| long2 | long2 | long2 |        |

------- ------- ------- ---------

| ....)好像扯得有點遠來,但是有助於對_INTSIZEOF(n)的理解。位操作對於我來說是玄的東東。單個位運算還應付得來,而這樣一個表達式擺在面前就暈了。怎麼辦?菜鳥自有菜的辦法。(待續) 

發佈了26 篇原創文章 · 獲贊 1 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章