程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C >> C語言入門知識 >> C語言可變參函數的實現

C語言可變參函數的實現

編輯:C語言入門知識

1 C語言中函數調用的原理

函數是大多數編程語言都實現的編程要素,調用函數的實現原理就是:執行跳轉+參數傳遞。對於執行跳轉,所有的CPU都直接提供跳轉指令;對於參數傳遞,CPU會提供多種方式,最常見的方式就是利用棧來傳遞參數。C語言標准實現了函數調用,但是卻沒有限定實現細節,不同的C編譯器廠商可以根據底層硬件環境自行確定實現方式。

函數調用的一般實現原理,請參考我的博文 C語言中利用setjmp和longjmp做異常處理中的第一段。

2 可變參實現思路

2.1 如何取得後續實參地址

我們以X86架構上的VC++編譯器為例進行舉例說明。例子代碼如下。

void f(int x, int y, int z)
{
    printf("%p, %p, %p\n", &x, &y, &z);
}
int main()
{
    f(100, 200, 300);
    return 0;
}

可能的執行結果:

00FFF674, 00FFF678, 00FFF67C

VC++中函數的參數是通過堆棧傳遞的,參數按照從右向左的順序入棧。調用f時參數在堆棧中的情況如下圖所示:
這裡寫圖片描述

可見,我們只要知道x的地址,就可以推算出y,z的地址,從而通過其地址取得參數y,z的值,而不用其參數名稱取值。如下代碼所示。

void f(int x, int y, int z)
{
    char* px = (char*)&x;
    char *py = px + sizeof(x);
    char *pz = py + sizeof(int);

    printf("x=%d, y=%d, z=%d\n", x, *(int*)py, *(int*)pz);
}
int main()
{
    f(100, 200, 300);
    return 0;
}

可見根據函數的第一個參數,以及後續參數的類型,就可以根據偏移量計算出後續參數的地址,從而取得後續參數值。
於是可以把上述代碼改寫成可變參數的形式。

void f(int x, ...)
{
    char* px = (char*)&x;
    char *py = px + sizeof(x);
    char *pz = py + sizeof(int);

    printf("x=%d, y=%d, z=%d\n", x, *(int*)py, *(int*)pz);
}
int main()
{
    f(100, 200, 300);
    return 0;
}

2.2 如何標識後續參數個數和類型

雖然寫成了可變參形式,但是函數如何判斷後續實參的個數和類型呢?這就需要在固定參數中攜帶這些信息,如printf(char*, …)使用的格式化字符串方法,通過第一個參數來攜帶後續參數個數以及類型的信息。我們實現一個簡單點的,只能識別%s,%d,%f三種標志。

void f(char* fmt, ...)
{
    char* p0 = (char*)&fmt;
    char* ap = p0 + sizeof(fmt);

    char* p = fmt;
    while (*p) {
        if (*p == '%' && *(p+1) == 'd') {
            printf("參數類型為int,值為 %d\n", *((int*)ap));
            ap += sizeof(int);
        }
        else if (*p == '%' && *(p+1) == 'f') {
            printf("參數類型為double,值為 %f\n", *((double*)ap));
            ap += sizeof(double);
        }
        else if (*p == '%' && *(p+1) == 's') {
            printf("參數類型為char*,值為 %s\n", *((char**)ap));
            ap += sizeof(char*);
        }
        p++;
    }

}
int main()
{
    f("%d,%f,%s", 100, 1.23, "hello world");
    return 0;
}

輸出:

參數類型為int,值為 100
參數類型為double,值為 1.230000
參數類型為char*,值為 hello world

為簡化分析參數代碼,定義一些宏來簡化,如下。

#define va_list char*   /* 可變參數地址 */
#define va_start(ap, x) ap=(char*)&x+sizeof(x) /* 初始化指針指向第一個可變參數 */
#define va_arg(ap, t)   (ap+=sizeof(t),*((t*)(ap-sizeof(t)))) /* 取得參數值,同時移動指針指向後續參數 */
#define va_end(ap)  ap=0 /* 結束參數處理 */

void f(char* fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);

    char* p = fmt;
    while (*p) {
        if (*p == '%' && *(p+1) == 'd') {
            printf("參數類型為int,值為 %d\n", va_arg(ap, int));
        }
        else if (*p == '%' && *(p+1) == 'f') {
            printf("參數類型為double,值為 %f\n", va_arg(ap, double));
        }
        else if (*p == '%' && *(p+1) == 's') {
            printf("參數類型為char*,值為 %s\n", va_arg(ap, char*));
        }
        p++;
    }
    va_end(ap);
}
int main()
{
    f("%d,%f,%s,%d", 100, 1.23, "hello world", 200);
    return 0;
}

3 正確的變參函數實現方法

上面的例子中,我們沒有使用任何庫函數就輕松實現了可變參數函數。別高興太早,上述代碼在X86平台的VC++編譯器下可以順利編譯、正確執行。但是在gcc編譯後,運行卻是錯誤的。可見GCC對於可變參數的實參傳遞實現與VC++並不相同。

gcc下編譯運行:
[smstong@cf-19 ~]$ ./a.out
參數類型為int,值為 0
參數類型為double,值為 0.000000
Segmentation fault

可見,上述代碼是不可移植的。為了在使得可變參函數能夠跨平台、跨編譯器正確執行,必須使用C標准頭文件stdarg.h中定義的宏,而不是我們自己定義的。(這些宏的名字和作用與我們自己定義的宏完全相同,這絕不是巧合!)每個不同的C編譯器所附帶的stdarg.h文件中對這些宏的定義都不相同。再次重申一下這幾個宏的使用范式:

va_list ap;
va_start(ap, 固定參數名); /* 根據最後一個固定參數初始化 */
可變參數1類型 x1 = va_arg(ap, 可變參數類型1); /* 根據參數類型,取得第一個可變參數值 */
可變參數2類型 x2 = va_arg(ap, 可變參數類型2); /* 根據參數類型,取得第二個可變參數值 */
...
va_end(ap);     /* 結束 */

這次,把我們自己的宏定義去掉,換成#include

#include 
#include 
void f(char* fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);

    char* p = fmt;
    while (*p) {
        if (*p == '%' && *(p+1) == 'd') {
            printf("參數類型為int,值為 %d\n", va_arg(ap, int));
        }
        else if (*p == '%' && *(p+1) == 'f') {
            printf("參數類型為double,值為 %f\n", va_arg(ap, double));
        }
        else if (*p == '%' && *(p+1) == 's') {
            printf("參數類型為char*,值為 %s\n", va_arg(ap, char*));
        }
        p++;
    }
    va_end(ap);

}
int main()
{
    f("%d,%f,%s,%d", 100, 1.23, "hello world", 200);
    return 0;
}

代碼在VC++和GCC下均可以正確執行了。

4 幾個需要注意的問題

4.1 va_end(ap); 必須不能省略

也許在有些編譯器環境中,va_end(ap);確實沒有什麼作用,但是在其他編譯器中卻可能涉及到內存的回收,切不可省略。

4.2 可變參數的默認類型提升

《C語言程序設計》中提到:

在沒有函數原型的情況下,char與short類型都將被轉換為int類型,float類型將被轉換為double類型。實際上,用...標識的可變參數總是會執行這種類型提升。

引用《C陷阱與缺陷》裡的話:

**va_arg宏的第2個參數不能被指定為char、short或者float類型**。
因為char和short類型的參數會被轉換為int類型,而float類型的參數會被轉換為double類型 ……
例如,這樣寫肯定是不對的:
c = va_arg(ap,char);
因為我們無法傳遞一個char類型參數,如果傳遞了,它將會被自動轉化為int類型。上面的式子應該寫成:
c = va_arg(ap,int);

4.3 編譯器無法進行參數類型檢查

對於可變參數,編譯器無法進行任何檢查,只能靠調用者的自覺來保證正確。

4.4 可變參數函數必須提供一個或更多的固定參數

可變參數必須靠固定參數來定位,所以函數中至少需要提供固定參數,f(固定參數,…)。
當然,也可以提供更多的固定參數,如f(固定參數1,固定參數2,…)。注意的是,當提供2個或以上固定參數時,va_start(ap, x)宏中的x必須是最後一個固定參數的名字(也就是緊鄰可變參數的那個固定參數)。

5 C的可變參函數與C++的重載函數

C++的函數重載特性,允許重復使用相同的名稱來定義函數,只要同名函數的參數(類型或數量)不同。例如,

void f(int x);
void f(int x, double d);
void f(char* s);

雖然源代碼中函數名字相同,其實編譯器處理後生成的是三個具有不同函數名的函數(名字改編name mangling)。雖然在使用上有些類似之處,但這顯然與C的可變參數函數完全不是一個概念。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved