1. 程式人生 > >stdarg.h可變引數列表(非格式控制)

stdarg.h可變引數列表(非格式控制)

1.C語言可變引數的概念

最常見的就是scanf和printf函式:

int scanf(const char * restrict format,...);
int printf(const char *fmt, ...);

你可以輸入任意型別的任意個引數,但是必須在格式化字串中確定輸入引數的個數和型別。

那麼我們如何自定義可變引數函式呢?
就需要使用stdarg.h標頭檔案了。stdarg的全稱就是standard arguments(標準引數),主要目的就是為了讓函式能夠接收可變引數。
它為使用者定義了4個標準巨集:

/* Define the standard macros for the user,
   if this invocation was from the user program.  */
#ifdef _STDARG_H
 
#define va_start(v,l)	__builtin_va_start(v,l)
#define va_end(v)	__builtin_va_end(v)
#define va_arg(v,l)	__builtin_va_arg(v,l)
#if !defined(__STRICT_ANSI__) || __STDC_VERSION__ + 0 >= 199900L || defined(__GXX_EXPERIMENTAL_CXX0X__)
#define va_copy(d,s)	__builtin_va_copy(d,s)
#endif

注意:如果想要使用stdarg.h中的巨集定義和型別物件,必須顯示定義標頭檔案#include <stdarg.h>

同時它定義了一個型別va_list。
函式首先需要定義一個va_list型的變數,這個變數是指向引數的指標. 用來存放可變引數的型別。va_list的定義如下:

typedef struct
 {
	char *a0; /* pointer to first homed integer argument */
	int offset; /* byte offset of next parameter */
} va_list

接下來先介紹4個巨集定義:

void va_start(va_list ap, last);

va_start函式初始化了va_list物件ap,為之後的va_arg和va_end函式作準備,所以必須首先呼叫。
引數last指的是變數引數列表之前的引數名,也就是呼叫函式中最後一個已知引數型別的引數。比如,printf函式中的fmt
因為last引數的地址會在va_start函式中使用,所以last不應該是一個暫存器變數,函式或者陣列型別。

type va_arg(va_list ap, type);

va_arg函式返回ap當前指向的引數的值。
引數ap就是va_start初始化的va_list物件;
引數type是一個型別名,比如“char”,“int”等,表示當前ap指向的引數的型別
每次呼叫va_arg後,ap就會指向下一個引數。但如果已經遍歷完引數列表,或者引數type並不是當前引數的實際型別名,此時呼叫va_arg函式將會發生隨機錯誤。
ap被引數va_arg函式使用過後,將無法回到最開始的位置

void va_end(va_list ap);

va_end函式和va_start相對應。在同一個函式中,呼叫過va_start之後就必須呼叫va_end。
使用va_end以後,變數ap將重置為空,並釋放記憶體。

void va_end(va_list ap);

C99標準。如果想要多次使用引數列表,那麼可以使用va_copy函式。
每次呼叫過va_copy函式後,必須相應的在同一個函式中呼叫va_end函式,比如:

va_list aq;
va_copy(aq, ap);

va_end(aq)

有些情況下,va_copy函式已經在其他地方有所定義,所以使用相同功能的函式__va_copy。
注意:va_start/va_arg/va_end函式符合C89標準。而va_copy是C99定義的

2.例子1(不用格式控制,引數同一型別int,可變引數個數固定)

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

//v1.0 最簡單的實現,只處理兩個引數
void my_format(int a,...)
{
	va_list var;
	int b,c;
	
	va_start(var,a);
	b = va_arg(var,int);
	c = va_arg(var,int);
	va_end(var);

	printf("a=%d,b=%d,c=%d\n",a,b,c);
}
int main()
{
	my_format(6,7);
	my_format(6,7,8);
	my_format(6,7,8,9);
	
	return 0;
}

在這裡插入圖片描述
說明:
1.定義一個va_list型別的變數,這裡是var;
2.定義兩個int型的變數,用來接收可變引數,這裡是b和c,只定義了兩個,所以只能接收兩個引數(這裡先討論幾個巨集的使用,後面再慢慢討論可變引數)。
3.使用va_start巨集初始化,把最後一個確定的引數a指標告訴va_list變數var,並且告訴a的型別(相當於知道了指標移動的距離,就是偏移量),也就相當於知道了第一個可變引數的位置。
4.通過va_start初始化後,知道了第一個可變引數(這裡的b,下同)的位置後,通過va_arg獲取b的值。var知道b的開始指標,後面的引數接著告訴需要指標移動的距離(取幾個位元組),這樣就能正確的獲取b的值。如果這裡使用float或者char與實際傳入不一樣的型別,是錯誤的(或許巧合可以得到正確的結果)。
5.再次使用va_arg獲取c的值。
6.va_end釋放變數var的記憶體,銷燬var變數。

這裡函式實現的是兩個可變引數,main裡面分別用三種情況呼叫:
1.可變引數少於my_fmt處理的個數,1個
2.可變引數等於my_fmt處理的個數,2個
3.可變引數大於my_fmt處理的個數,3個
通過結果大家可以看到,當引數個數小於實際處理的引數個數後,後面va_arg獲取的值是隨機的。當引數個數大於實際處理的引數個數後,後面的不再處理。所以實現可變引數的函式一定要考慮到各種情況。

3.例子2(不用格式控制,引數型別相同int,可變引數個數隨機)

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

//v2.0 處理可變引數(傳入特殊值控制)
void my_format(int a,...)
{
	va_list var;
	int temp;
	
	va_start(var,a);
	printf("a=%d\n",a);
	while (-1 != (temp = va_arg(var,int)))
	{
		printf("temp=%d\n",temp);
	}
	va_end(var);
	printf("\n");
}
int main()
{
	int a = 427653;
	my_format(a,-1);
	my_format(a,-6,7,-1);
	my_format(15,16,-17,18,-1);
	
	return 0;
}
結果:

在這裡插入圖片描述
這裡要說明的是,va_arg獲取完可變引數後如果再往後獲取,就會得到一個不確定的值,一般我們採取引數傳入引數個數和給特定值表示可變引數結束。這裡採用的是後者,比如如果是-1則表示可變引數結束。

下面介紹的是前面的情況,傳入可變引數的個數

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

//v2.1 實現可變引數(傳入引數個數控制)
void my_format(int size,...)
{
	va_list var;
	int temp;
	
	va_start(var,size);

	while (0 < (size--))
	{	
		temp = va_arg(var,int);
		printf("%d,",temp);
	}
	va_end(var);
	
}
int main()
{
	int size = 1;
	
	printf("\n1.1: ");
	my_format(size);
	printf("\n1.2: ");	
	my_format(size,5);
	printf("\n1.3: ");	
	my_format(size,5,6);

	size = 2;
	printf("\n2.1: ");
	my_format(size,5);
	printf("\n2.2: ");
	my_format(size,5,6);
	printf("\n2.3: ");
	my_format(size,5,6,7);

	printf("\n");
	
	return 0;
}

在這裡插入圖片描述

4.例子3(不用格式控制,引數型別不相同,引數個數固定)

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

//v3.0 引數型別不同
void my_format(char a,...)
{
	va_list var;
	char b;
	float c;
	
	va_start(var,a);
	b = va_arg(var,char);
	c = va_arg(var,float);
	va_end(var);

	printf("a=%c,b=%d,c=%f\n",a,b,c);
	
}
int main()
{
	char a = 'a';
	short b = 5;
	float c = 3.14;
	my_format(a,b,c);
	
	return 0;
}

編譯發現報錯:
在這裡插入圖片描述
根據提示,發現是預設引數提升造成的,至於什麼是預設引數提升,詳情請參考:
https://blog.csdn.net/qq_33706673/article/details/84679343
所以修改程式碼如下:

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

//v3.0 引數型別不同
void my_format(char a,...)
{
	va_list var;
	char b;
	float c;
	
	va_start(var,a);
	b = (char)va_arg(var,int);
	c = (float)va_arg(var,double);
	va_end(var);

	printf("a=%c,b=%d,c=%f\n",a,b,c);
	
}
int main()
{
	char a = 'a';
	short b = 5;
	float c = 3.14;
	my_format(a,b,c);
	
	return 0;
}

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