C語言可變引數詳解

ARM的程式設計師敲著詩歌的夢發表於2018-12-31

C語言可變引數詳解

什麼是可變引數函式

在C語言程式設計中有時會遇到一些引數可變的函式,例如printf()、scanf(),其函式原型為:

int printf(const char* format,)
int scanf(const char *format,)

就拿 printf 來說吧,它除了有一個引數 format 固定以外,後面的引數其個數和型別都是可變的,用三個點“…”作為引數佔位符。

引數列表的構成

任何一個可變引數的函式都可以分為兩部分:固定引數和可選引數。至少要有一個固定引數,其宣告與普通函式引數宣告相同;可選引數由於數目不定(0個或以上),宣告時用"…"表示。固定引數和可選引數共同構成可變引數函式的引數列表。

實現原理

C語言中使用 va_list 系列變參巨集實現變參函式,此處va意為variable-argument(可變引數)。

x86平臺VC6.0編譯器中,stdarg.h標頭檔案內變參巨集定義如下:

typedef char * va_list;

// 把 n 圓整到 sizeof(int) 的倍數
#define _INTSIZEOF(n)       ( (sizeof(n)+sizeof(int)-1) & ~(sizeof(int)-1) )

// 初始化 ap 指標,使其指向第一個可變引數。v 是變參列表的前一個引數
#define va_start(ap,v)      ( ap = (va_list)&v + _INTSIZEOF(v) )

// 該巨集返回當前變參值,並使 ap 指向列表中的下個變參
#define va_arg(ap, type)    ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) )

// /將指標 ap 置為無效,結束變參的獲取
#define va_end(ap)             ( ap = (va_list)0 )

_INTSIZEOF(n)

_INTSIZEOF巨集考慮到某些系統需要記憶體地址對齊。從巨集名看應按照sizeof(int)即棧粒度對齊,引數在記憶體中的地址均為sizeof(int)=4的倍數。

例如,若1≤sizeof(n)≤4,則_INTSIZEOF(n)=4;若5≤sizeof(n)≤8,則_INTSIZEOF(n)=8

va_start(ap,v)

va_start巨集首先根據(va_list)&v得到引數 v 在棧中的記憶體地址,加上_INTSIZEOF(v)即v所佔記憶體大小後,使 ap 指向 v 的下一個引數。在使用的時候,一般用這個巨集初始化 ap 指標,v 是變參列表的前一個引數,即最後一個固定引數,初始化的結果是 ap 指向第一個變參。

va_arg(ap, type)

這個巨集取得 type 型別的可變引數值。首先ap += _INTSIZEOF(type),即 ap 跳過當前可變引數而指向下個變參的地址;然後ap-_INTSIZEOF(type)得到當前變參的記憶體地址,型別轉換後解引用,最後返回當前變參值。

va_end(ap)

va_end 巨集使 ap 不再指向有效的記憶體地址。該巨集的某些實現定義為((void*)0),編譯時不會為其產生程式碼,呼叫與否並無區別。但某些實現中 va_end 巨集用於在函式返回前完成一些必要的清理工作:如 va_start 巨集可能以某種方式修改棧,導致返回操作無法完成,va_end 巨集可將有關修改復原;又如 va_start 巨集可能為引數列表動態分配記憶體以便於遍歷,va_end 巨集可釋放此記憶體。因此,從使用 va_start 巨集的函式中退出之前,必須呼叫一次 va_end 巨集。

程式碼示例

變參巨集無法智慧識別可變引數的數目和型別,因此實現變參函式時需自行判斷可變引數的數目和型別。所以我們就要想一些辦法,比如

  1. 顯式提供變引數目或設定遍歷結束條件
  2. 顯式提供變參型別列舉值,或在固定引數中包含足夠的型別資訊(如printf函式通過分析format字串即可確定各變參型別)
  3. 主調函式和被調函式可約定變參的數目和型別

例1:函式通過固定引數指定可變引數個數,列印所有變參值。

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

void parse_valist_by_num(int arg_cnt, ...);

int main(void)
{
    parse_valist_by_num(4,1,2,3,4);
    parse_valist_by_num(4,1,2,3); 
    parse_valist_by_num(4,1,2,3,4,5); //多餘的變參被忽略
}


//第一個引數定義可變引數的個數
void parse_valist_by_num(int arg_cnt, ...)
{
    
    va_list p_args;
    va_start(p_args, arg_cnt);
    
    int idx;
    int val;
    
    for(idx = 1; idx <= arg_cnt; ++idx){
        val = va_arg(p_args, int);
        printf("第 %d 個引數: %d\n", idx, val);
    }
    printf("---------------\n");
    va_end(p_args);
}

執行結果如下:

在這裡插入圖片描述

注意第2個結果,第4個引數是一個魔數,這是因為列印出了棧中引數3上方的引數值。

例2:函式定義一個結束標記(-1),呼叫時通過最後一個引數傳遞該標記,列印標記前所有變參值。

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

void parse_valist_by_flag(int num_1, ...);

int main(void)
{
    parse_valist_by_flag(1,-1);
    parse_valist_by_flag(1,2,3,5,-1);
    parse_valist_by_flag(-1);
    
}

//函式定義一個結束標記(-1),呼叫時通過最後一個引數傳遞該標記,以結束變參的遍歷列印。
//最後一個引數作為變參結束符(-1),用於迴圈獲取變參內容
void parse_valist_by_flag(int num_1, ...)
{
    va_list p_args;
    va_start(p_args, num_1);
    int idx = 0;
    int val = num_1;
    while(val != -1){
        ++idx;
        printf("第 %d 個引數: %d\n", idx, val);
        val = va_arg(p_args, int); //得到下個變參值
    }
    va_end(p_args);
    printf("---------------\n");
}

執行結果是:
在這裡插入圖片描述

需要注意

va_arg(ap, type)巨集中的 type 不可指定為以下型別:

  • char
  • short
  • float

在C語言中,呼叫不帶原型宣告或宣告為變參的函式時,主調函式會在傳遞未顯式宣告的引數前對其執行預設引數提升(default argument promotions),將提升後的引數值傳遞給被調函式。

​ 提升操作如下:

  • float 型別的引數提升為 double 型別
  • char、short 和相應的 signed、unsigned 型別引數提升為 int 型別
  • 若 int 型別不能容納原值,則提升為 unsigned int 型別

最後來一張圖,幫助大家理解前文講的巨集。

在這裡插入圖片描述

【完】

參考資料

https://www.cnblogs.com/clover-toeic/p/3736748.html

相關文章