在學習C語言的過程中我們可能很少會去寫變參函數,印象中大學老師好像也沒有提及過,但我發現變參函數的實現很巧妙,所以還是特地在此分析下變參函數的實現原理。無需標準C的支持,我們自己寫代碼來實現。 先來看看一個實現代碼: #include #define va_list void* #define va_arg(arg, type) *(type*)arg; arg = (char*)arg + sizeof(type); #define va_start(arg, start) arg = (va_list)(((char*)&(start)) + sizeof(start)) int sum(int nr, ...) { int i = 0; int result = 0; va_list arg = NULL; va_start(arg, nr); for(i = 0; i #define va_list void*通過這句代碼我們實現了定義va_list是一個指針,參數類型不定,它可以指向任意類型的指針。為了讓arg指向第一個可變參數,我們用nr的地址加上nr的數據類型大小就行了,采用如下的定義可以實現。 #define va_start(arg, start) arg = (va_list)(((char*)&(start)) + sizeof(start)) 。 通過(((char*)&(start)) + sizeof(start)) 可以得到第一個可變參數的地址,再將其強制轉換為va_list類型。 成功取出了第一個可變參數后,接下來的任務就是繼續取出可變參數,方法跟上面求第一個可變參數的方法一樣,通過arg = (char*)arg + sizeof(type);來實現讓arg指向下一個可變參數,type為可變參數的類型,通過這種方法可以一一取出可變參數。 在這里順便給出上面實現代碼的匯編代碼,有興趣的可以讀讀,加深下對于底層匯編代碼的閱讀能力。 .file "varargs.c" .text .globl sum .type sum, @function sum: pushl %ebp movl %esp, %ebp subl $16, %esp movl $0, -4(%ebp) movl $0, -8(%ebp) movl $0, -12(%ebp) leal 12(%ebp), %eax movl %eax, -12(%ebp) movl $0, -4(%ebp) jmp .L2 .L3: movl -12(%ebp), %eax movl (%eax), %eax addl %eax, -8(%ebp) addl $4, -12(%ebp) addl $1, -4(%ebp) .L2: movl 8(%ebp), %eax cmpl %eax, -4(%ebp) jl .L3 movl -8(%ebp), %eax leave ret .size sum, .-sum .section .rodata .LC0: .string "%d\n" .text .globl main .type main, @function main: pushl %ebp movl %esp, %ebp andl $-16, %esp subl $32, %esp movl $100, 16(%esp) movl $100, 12(%esp) movl $100, 8(%esp) movl $100, 4(%esp) movl $4, (%esp) call sum movl $.LC0, %edx movl %eax, 4(%esp) movl %edx, (%esp) call printf movl $200, 12(%esp) movl $200, 8(%esp) movl $200, 4(%esp) movl $3, (%esp) call sum movl $.LC0, %edx movl %eax, 4(%esp) movl %edx, (%esp) call printf movl $0, %eax leave ret .size main, .-main .ident "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2" .section .note.GNU-stack,"",@progbits樹莓派文章專題:樹莓派是什么?你不知道樹莓派的知識和應用 其中有幾條指令在此講解下。 leave指令所做的操作相當于如下兩條指令: movl %ebp, %esp popl %ebp ret指令所做的操作相當于如下指令: pop %eip 如果有對AT&T匯編語法規則不懂的,可以看看我前面寫的那篇文章。 到這兒為止是乎應該是說結束的時候了,但是細心的讀者可能發現了一個問題,就是我們在最初給出的代碼部分有一句紅色標記的代碼,如下: #define va_start(arg, start) arg = (va_list)(((char*)&(start)) + sizeof(start)) 為什么要把這句代碼單獨拿出來講解呢,肯定是有原因的,因為((char*)&(start)) +sizeof(start)這句代碼的特殊性在于使用了(char*)進行強制轉換,在這里為什么不使用(int*)進行強制轉換呢,如改為如下代碼: #include #include #define va_list void* #define va_arg(arg, type) *(type*)arg; arg = (char*)arg + sizeof(type); #define va_start(arg, start) arg = (va_list)(((int*)&(start)) + sizeof(start)) //修改為(int*) int sum(int nr, ...) { int i = 0; int result = 0; va_list arg = NULL; va_start(arg, nr); for(i = 0; i 顯然運行結果是錯誤的,為什么會出現這樣的錯誤呢,我們暫且不分析,先來看看我們接下來做的修改: #include #include #define va_list void* #define va_arg(arg, type) *(type*)arg; arg = (char*)arg + sizeof(type); #define va_start(arg, start) arg = (va_list)(((int*)&(start)) + sizeof(start)/4) //注意對比紅色部分的變化 int sum(int nr, ...) { int i = 0; int result = 0; va_list arg = NULL; va_start(arg, nr); for(i = 0; i 運行結果正確。 現在來分析下為什么會出現這兩種結果呢,看看下面我給出的這個圖解和代碼應該就能夠很清楚的理解為什么會出現以上的兩種運行結果了。 代碼如下: #include int main() { int a = 12; int *p_int = &a; char *p_char = (char*)&a; printf( "%d \t", sizeof(char)); printf( "%d \t", sizeof(int)); printf( "%d \t", p_int+1); printf( "%d \t", p_char+1); return 0; } 運行結果為: 修改以上紅色部分的代碼,得到如下代碼: #include int main() { int a = 12; int *p_int = &a; char *p_char = (char*)&a; printf( "%d \t", sizeof(char)); printf( "%d \t", sizeof(int)); printf( "%d \t", p_int+1); printf( "%d \t", p_char+4); return 0; } 注意對比前后代碼的變化部分!!! 運行結果如下: 首先看看給出的圖,int指針所指向的單元占有四個字節的空間,而char指針所指向的單元只占有一個字節的空間。所以如果是整形指針想要取下一個參數,只需加1,但是如果是char指針,如果當前參數是int型,那么想要取下一個參數就要加4才能實現。但是值得注意的是,int*和char*所占的存儲單元都是4個字節,這是由我們所使用的32位計算機本身確定的。為了加深大家的印象,特地給出如下代碼: #include int main() { int a = 12; int *p_int = &a; char *p_char = (char*)&a; printf( "%d \t", sizeof(char*)); printf( "%d \t", sizeof(int*)); return 0; } 運行結果如下: 到此為止就是真的該結束了,總不能沒完沒了的講下去吧,呵呵…… 很多代碼僅僅是修改了一點,我都貼出了完整的代碼,是希望你在閱讀的過程中能直接copy過去,看看運行效果,加深下印象。還是那句話,C語言博大精深,我還是C語言菜鳥,以上內容難免有錯。 |