1. 程式人生 > >C語言可變引數的原理

C語言可變引數的原理

轉自:http://blog.csdn.net/bigloomy/article/details/6588354

這個寫得比較簡單,明瞭,看了這個才真正理解了變長引數怎麼實現的。

在學習C語言的過程中我們可能很少會去寫變參函式,印象中大學老師好像也沒有提及過,但我發現變參函式的實現很巧妙,所以還是特地在此分析下變參函式的實現原理。無需標準C的支援,我們自己寫程式碼來實現。

先來看看一個實現程式碼:

#include <stdio.h>

#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 < nr; i++)
    {
        result += va_arg(arg, int);
    }
    return result;
}

int main(int argc, char* argv[])
{
    printf("%d\n", sum(4, 100,100,100,100));
    printf("%d\n", sum(3, 200, 200, 200));

    return 0;
}

執行結果如下:

#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 <stdio.h> 
#include <stdlib.h>

#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 < nr; i++)
    {
        result += va_arg(arg, int);
    }
    return result;
}

int main(int argc, char* argv[])
{
    printf("%d\n", sum(4, 100,100,100,100));
    printf("%d\n", sum(3, 200, 200, 200)); 
    return 0;
}

執行結果為:

顯然執行結果是錯誤的,為什麼會出現這樣的錯誤呢,我們暫且不分析,先來看看我們接下來做的修改:

#include <stdio.h> 
#include <stdlib.h>

#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 < nr; i++)
    {
        result += va_arg(arg, int);
    }
    return result;
}

int main(int argc, char* argv[])
{
    printf("%d\n", sum(4, 100,100,100,100));
    printf("%d\n", sum(3, 200, 200, 200)); 
    return 0;
}

執行結果如下:

執行結果正確。

現在來分析下為什麼會出現這兩種結果呢,看看下面我給出的這個圖解和程式碼應該就能夠很清楚的理解為什麼會出現以上的兩種執行結果了。

程式碼如下:

#include <stdio.h>

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 <stdio.h>

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 <stdio.h>

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語言菜鳥,以上內容難免有錯。如需轉載,請註明出處!