C語言可變參數詳解【轉】

(轉自:https://blog.csdn.net/longintchar/article/details/85490103?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522158890188319725256728493%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=158890188319725256728493&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_v2~rank_v25-1&utm_term=


什麼是可變參數函數


在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 宏。

代碼示例


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

顯式提供變參數目或設定遍歷結束條件
顯式提供變參類型枚舉值,或在固定參數中包含足夠的類型信息(如printf函數通過分析format字符串即可確定各變參類型)
主調函數和被調函數可約定變參的數目和類型


例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
————————————————
版權聲明:本文爲CSDN博主「ARM的程序員敲着詩歌的夢」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/longintchar/article/details/85490103

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章