1. 程式人生 > >C之函數參數(三十九)

C之函數參數(三十九)

C語言 函數參數 調用約定 可變參數

我們上節博文講了函數的意義,那麽我們今天來講下函數參數函數參數在本質上與局部變量相同在棧上分配空間,函數參數的初始值是函數調用時的實參值。用下圖來實際說明

技術分享圖片

函數參數的求值順序依賴於編譯器的實現,我們來看看下面代碼的輸出是什麽?為什麽呢?

#include <stdio.h>

int func(int i, int j)
{
    printf("i = %d, j = %d\n", i, j);
    
    return 0;
}

int main()
{
    int k = 1;
    
    func(k++, k++);
    
    printf("%d\n", k);
    
    return 0;
}

我們理論上分析,func 函數先進行 k++,那麽 i 就對應為 1,再次進行 k++,對應於 j 為 2。那麽第 14 行應打印 i = 1, j = 2,。這時 k 為 3,所以第 16 行打印的值應為 3。我們來看看編譯結果是否如我們所分析的那樣,編譯結果如下

技術分享圖片

我們看到 i 和 j 和我們所分析的正好相反,那麽這是怎麽回事呢?原來在gcc 編譯器中,函數參數的實現是從右向左進行操作的,並非是我們所想的從左向右進行計算的。我們再在 BCC 編譯器中進行編譯,看看結果是怎樣?

技術分享圖片

那麽我們看到在 BCC 編譯器中也是這樣實現的。函數參數的操作是從右向左的,在現代的編譯器中,基本上是按照從右向左的順序進行函數參數的操作的。在一些古老的編譯器中,也有從左向右的實現,這個的實現就依賴於具體的編譯器的實現了。

下來我們來講一個 C 語言中的知識點:順序點!那麽在程序中存在一定的順序點,順序點是指執行過程中修改變量值的最晚時刻在程序到達順序點的時候,之前所做的一切操作必須完成。那麽 C 語言中的順序點都在那些時刻呢?a> 每個完整表達式結束時,即分號處;b> &&,||,?: 以及逗號表達式的每個參數計算之後;c> 函數調用時所有實參求值完成後(即進入函數之前)

下面我們以代碼為例進行分析,代碼如下

#include <stdio.h>

int main()
{
    int k = 2;
    int a = 1;
    
    k = k++ + k++;
    
    printf("k = %d\n", k);
    
    if( a-- && a )
    {
        printf("a = %d\n", a);
    }
    
    return 0;
}

我們看到第 8 行的進行兩次 k++ 的相加,我們分析結果應該為 5;第 12 行的 a-- 執行完之後 a 為 0,但是此時它和 a 相與之後條件仍然為真,所以 第14行應該打印出 a = 0;我麽來看看結果是這樣嗎?

技術分享圖片

那麽我們看到我們分析的第一個是正確的,但是 a = 0 並沒有打印出來,我們再來看看 BCC 編譯器是多少

技術分享圖片

我們看到竟然 k = 6,a = 0 依然沒有打印出來。我們再來看看 VS 編譯器

技術分享圖片

我們進到反匯編看看它是怎麽執行的

技術分享圖片

我們看到它是這樣執行的,先是進行相加操作,這時的++操作被懸掛起來,程序看到;才意識到到了順序點了,所以執行完那兩次++操作,所以最後 k 的值為6。我們再來看看第14行怎麽執行的

技術分享圖片

我們看到它是執行完 a-- 後看到 && 操作便意識到順序點到了,便返回了。那麽這時 a 的值已經變為 0 了,此時 if 語句條件為假,所以不會執行到它裏面的打印語句。

下來我們再來看看參數入棧的順序,函數參數的計算次序是依賴編譯器實現的。那麽函數參數的入棧次序是如何確定的呢?這塊就涉及到裏一個概念:調用約定。當函數調用發生時:a> 參數會傳遞給被調用的函數;b> 而返回值會被返回給函數調用者;調用約定描述參數如何傳遞到棧中以及棧的維護方式,參數傳遞順序,調用棧清理。

調用約定是預定義的可理解為調用協議,調用約定通常用於庫調用和庫開發的時候。我們來看看一些常用的操作:a>從右到左依次入棧:__stfcall, __cdecl, __thiscall;b> 從左到右依次入棧:__pascall, __fastcall;那麽我們一般的 C 程序開發遵循的就是上面的 __cdecl 這種方式的。

那麽我們如果要編寫一個計算平均數的函數,我們肯定首先想到的是下面這種

#include <stdio.h>

float average(int array[], int size)
{
    int i = 0;
    float avr = 0;
    
    for(i=0; i<size; i++)
    {
        avr += array[i];
    }
    
    return avr / size;
}

int main()
{
    int array[] = {1, 2, 3, 4, 5};
    
    printf("%f\n", average(array, 5));
    
    return 0;
}

我們利用一個數組就完成這個功能,那麽我們還得去定義一個數組。有什麽辦法可以使我們不用定義數組就可以完成這個功能呢?答案就是我們可以利用可變參數的函數來實現這個功能。在 C 語言中可以定義參數可變的函數,參數可變函數的實現依賴於 stdarg.h 頭文件。我們得介紹幾個概念:a> va_list -- 參數集合;b> va_arg -- 取具體參數值;c> va_start -- 標識參數訪問的開始;d> va_end -- 標識參數訪問的結束

下來我們來看看可變參數版的程序是怎樣實現的,代碼如下

#include <stdio.h>
#include <stdarg.h>

float average(int n, ...)
{
    va_list args;
    int i = 0;
    float sum = 0;
    
    va_start(args, n);
    
    for(i=0; i<n; i++)
    {
        sum += va_arg(args, int);
    }
    
    va_end(args);
    
    return sum / n;
}

int main()
{
    printf("%f\n", average(5, 1, 2, 3, 4, 5));
    printf("%f\n", average(4, 1, 2, 3, 4));
    
    return 0;
}

我們在第 6 行定義了 args 參數,在第 10 行開始,14 行進行參數的相加,在 17 行結束。我們來看看第24, 25 行的這樣的定義可行嗎?來看看編譯結果

技術分享圖片

結果已經正確實現了,這樣是不是很方便呢?我們可以隨時定義它的大小和內容。那麽可變參數也有限制:a> 可變參數必須從頭到尾按照順序逐個訪問;b> 參數列表中至少要存在一個確定的命名參數;c> 可變參數函數無法確定實際存在的參數的數量,同樣也無法確定參數的實際類型,只能我們手動指定;註意:va_arg 中指定了錯誤的類型,那麽結果是不可預測的!

通過對函數參數的學習,總結如下:1、函數的參數在棧上分配空間;2、函數的實參並沒有固定的計算次序;3.順序點是 C 語言中變量修改的最晚時機;4、調用約定指定了函數參數的入棧順序以及棧的清理方式;5、可變參數的函數提供了一種函數設計技巧,提供了一種更方便的函數調用方式;6、可變參數必須順序的訪問,無法直接訪問中間的參數值。


歡迎大家一起來學習 C 語言,可以加我QQ:243343083

C之函數參數(三十九)