天天看點

C stdarg.h:可變參數va_list、va_arg等宏的使用及原理簡介

<stdarg.h>标準庫的使用

va_list、va_arg宏及 …的使用

va_list 可變參數宏,同辨別符…相同,用于傳遞可變參數

當函數需要傳遞的參數個數不能确定時,如 printf,使用…聲明接下來的多個參數,

在函數實作中使用va_list、va_arg等宏取出參數使用

具體使用方法如下

void func(first_type first_arg, ...){
	va_list argptr;//聲明參數清單指針
	va_start(argptr, first_arg);//初始化參數清單指針,将其指向第二個參數
	second_type var = va_arg(list, second_type);//将參數按類型取出,并将指針指向下一個參數
	//int intval = va_arg(list, int);
	...
	va_end(list);//list = NULL,使用結束将指針懸空
}
           

可以看出,可變參數要求我們必須傳遞第一個參數,并且需要知道參數的個數和類型才能使用

對于 printf 來說,它從第一個參數——格式化字元串中對未知個數的參數進行處理,并得知類型資訊

以下是 va_list 在标準庫中的一些使用,比如可以自己封裝I/O函數

int vscanf(const char *format, va_list ap); // 從标準輸入/輸出格式化字元串
int vfscanf(FILE *stream, const char *format, va_list ap); // 從檔案流
int vsscanf(char *s, const char *format, va_list ap); // 從字元串
           

C/C++參考手冊給出的使用示例

void error( char *fmt, ... ) {
	va_list args;
        
	va_start(args, fmt);
	fprintf(stderr, "Error: ");
	vfprintf(stderr, fmt, args);
	fprintf(stderr, "\n");
	va_end(args);
	exit(1);
}
           

va_list宏的實作原理

//Intel x64, VC/gcc  環境下可以得到預期效果
void var_args_func(const char * fmt, ...){
    
    uintptr_t ap = ((char*)&fmt) + sizeof(uintptr_t);
    
    va_list list;
    va_start(list, fmt);
    
    if (list == ap) { printf("equal\n"); }
    printf("list = %p\n  ap = %p\n", list, ap);
    
    printf("%d\n", va_arg(list, int));
    printf("%d\n", va_arg(list, int));
    printf("%s\n", va_arg(list, char *));

    va_end(list);//list = NULL

    printf("%d\n", *(int*)ap);

    ap += sizeof(uintptr_t);
    printf("%d\n", *(int*)ap);

    ap += sizeof(uintptr_t);
    printf("%s\n", *((char**)ap));
}

int main(){
	var_args_func("%d %d %s\n", 0, 1, "hello world");
}
           

C函數的預設調用方式将函數參數從右向左儲存在棧上(由高位址向低位址),

這裡從第一個參數的位址入手,依次将指針偏移,通路棧中的每個參數,

但是實際上,參數的壓棧要遵循記憶體對齊的規則,恰巧x64、VC/gcc 環境下參數在棧中的位置是按pointer大小對齊的,

這裡不加修飾地偏移指針去通路參數才能得到正确結果

va_list、va_arg在VC中的具體實作

這裡隻介紹x86及x64的實作

C stdarg.h:可變參數va_list、va_arg等宏的使用及原理簡介

此處的 va_copy 是C99的新增内容,用于 va_list 間的複制,可以看到VC的實作即是簡單的指派

繼續向下看

C stdarg.h:可變參數va_list、va_arg等宏的使用及原理簡介
C stdarg.h:可變參數va_list、va_arg等宏的使用及原理簡介

這裡定義了va_list,可以看到它實際上是一個char *

接下來的實作對x86與x64架構做了不同的處理

x64實作
C stdarg.h:可變參數va_list、va_arg等宏的使用及原理簡介

__crt_va_start_a(ap, x) 被擴充為函數調用 ((void)(__va_start(&ap,x)))

//這裡有一個__va_start 函數第二個參數為 … 的問題

即 va_list 指針與函數第一個參數傳遞給 __va_start

按上面的實驗代碼,可以将 __va_start 簡單看為由第一個參數獲得參數清單的位址,

并将 ap 指向下一個參數

接下來 __crt_va_arg(ap,t)

當變量所占位元組數超過8或者3、5、7時,擴充為

表明此時棧中儲存的不是參數本身,而是參數的指針

當變量位元組數為1、2、4、6、8時,擴充為

即參數被儲存在棧上,且按8位元組對齊(将參數"壓棧"後,不足8位元組的用零占位)

最後 va_end 将 ap 懸空

x86實作
C stdarg.h:可變參數va_list、va_arg等宏的使用及原理簡介
C stdarg.h:可變參數va_list、va_arg等宏的使用及原理簡介

C中_ADDRESSOF宏的實作就是簡單的取址

可以想到,_INTSIZEOF 宏的作用是用來取到位元組對齊的偏移量,_int size,得到 int 整數倍的位元組數

看一下它的具體操作

要将 x 向上取為數字 n 的整數倍時,可以将 x 表示為 kn + r,r ∈ [0, n-1]

容易想到将 (kn + r + n-1)/n 即可得到向上取整的倍數k(k = k 或 k+1)

相當于 (x + n-1)/n*n,n 為2的 m 次方時,(x + n-1)/n*n 即為 ((x + n-1)>>m)<<m

即相當于将低 m 位置零

即x86、VC下,從右向左将參數依次儲存在棧幀上,

__va_start_a 将 ap 指向參數 v 的下一個參數的實際起始位址

__va_arg 将 ap 移動到下一個參數位置并傳回目前參數位址

__va_end 同樣是将指針懸空

GNU下,va_list 及 va_arg 等宏實作如下

C stdarg.h:可變參數va_list、va_arg等宏的使用及原理簡介
C stdarg.h:可變參數va_list、va_arg等宏的使用及原理簡介

實作為gcc内部資料結構及函數,原理應當類似,暫且不表

進一步了解 記憶體對齊

進一步了解 函數調用過程與棧幀

2019/12/7

繼續閱讀