1. 程式人生 > >C語言可變長引數函式與預設引數提升

C語言可變長引數函式與預設引數提升

學習本章內容的時候,首先需要知道可變引數提升相關的知識。
原文地址:https://blog.csdn.net/astrotycoon/article/details/8284501

1、概述

C標準中有一個預設引數提升(default argument promotions)規則。
預設引數提升有時會給我們帶來疑惑。本文結合C語言的可變長引數函式來說明預設引數提升存在的陷阱。

2、預設引數提升的定義

標準中的定義如下:

If the expression that denotes the called function has a type that does not include a prototype, the integer promotions are performed on each argument, and arguments that have type float are promoted to double. These are called the default argument promotions. – C11 6.5.2.2 Function calls (6)

意思大概是:如果一個函式的形參型別未知, 例如使用了Old Style C風格的函式宣告,或者函式的引數列表中有 …,那麼呼叫函式時要對相應的實參做Integer Promotion,此外,相應的實參如果是float型的也要被提升為double型別,這條規則稱為Default Argument Promotion。

3、可變長引數函式

熟悉C的人都知道,C語言支援可變參長數函式(Variable Argument Functions),即引數的個數可以是不定個,在函式定義的時候用(…)表示,比如我們常用的printf()\execl()函式等;printf函式的原型如下:

int printf(const char format, …);
注意,採用這種形式定義的可變引數函式,至少需要一個普通的形參,比如上面程式碼中的

format,後面的省略號是函式原型的一部分。

C語言定義了一系列巨集來完成可變引數函式引數的讀取和使用:巨集va_start、va_arg和va_end;在ANSI C標準下,這些巨集定義在stdarg.h中。三個巨集的原型如下:

void va_start(va_list ap, last);// 取第一個可變引數(如上述printf中的i)的指標給ap,
// last是函式宣告中的最後一個固定引數(比如printf函式原型中的*fromat);
type va_arg(va_list ap, type); // 返回當前ap指向的可變引數的值,然後ap指向下一個可變引數;
// type表示當前可變引數的型別(支援的型別位int和double);
void va_end(va_list ap); // 將ap置為NULL
當一個函式被定義為可變引數函式時,其函式體內首先要定義一個va_list的結構體型別,這裡沿用原型中的名字,ap。va_start使ap指向第一個可選引數。va_arg返回引數列中的當前引數並使ap指向引數列表中的下一個引數。va_end把ap指標清為NULL。函式體內可以多次遍歷這些引數,但是都必須以va_start開始,並以va_end結尾。

下面是一個具體的示例(摘自wikipedia):

#include <stdarg.h>
 
double average(int count, ...)
{
    va_list ap;
    int j;
    double tot = 0;
    va_start(ap, count);	//使va_list指向起始的引數
    for(j=0; j<count; j++)
        tot+=va_arg(ap, double);//檢索引數,必須按需要指定型別
    va_end(ap);			//釋放va_list
    return tot/count;
}

4、預設引數提升在可變引數函式的陷阱

如果明白了C語言的可變引數函式,讓我們實現一個簡易的my_printf

  1. 它只返回void, 不記錄輸出的字元數目
  2. 它只接受"%d"按整數輸出、"%c"按字元輸出、"%%"輸出’%'本身
    很多人的答案如下:
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
 
void my_printf(const char* fmt, ... )
{
	va_list ap;
	va_start(ap,fmt); /* 用最後一個具有引數的型別的引數去初始化ap */
	for (;*fmt;++fmt)
	{
		/* 如果不是控制字元 */
		if (*fmt!='%')
		{
			putchar(*fmt); /* 直接輸出 */
			continue;
		}
 
		/* 如果是控制字元,檢視下一字元 */
		++fmt;
		if ('\0'==*fmt) /* 如果是結束符 */
		{
			assert(0);  /* 這是一個錯誤 */
			break;
		}
 
		switch (*fmt)
		{
			case '%': /* 連續2個'%'輸出1個'%' */
				putchar('%');
				break;
			case 'd': /* 按照int輸出 */
			{
				/* 下一個引數是int,取出 */
				int i = va_arg(ap,int);
				printf("%d",i);
			}
			break;
			case 'c': /* 按照字元輸出 */
			{
				/** 但是,下一個引數是char嗎*/
				/*  可以這樣取出嗎? */
				char c = va_arg(ap,char);
				printf("%c",c);
			}
			break;
		}
	}
	va_end(ap);  /* 釋放ap—— 必須! 見下文分析*/
}

很可惜,這樣的程式碼是錯誤的!

簡單的說,我們用va_arg(ap,type)取出一個引數的時候,
type絕對不能為以下型別:
——char、signed char、unsigned char
——short、unsigned short
——signed short、short int、signed short int、unsigned short int
——float

一個簡單的理由是:
——呼叫者絕對不會向my_printf傳遞以上型別的實際引數。

為什麼呢?-- 這裡就牽扯到預設引數提升問題。

看標準:

If the expression that denotes the called function has a type that does include a prototype, the arguments are implicitly converted, as if by assignment, to the types of the corresponding parameters, taking the type of each parameter to be the unqualied versionof its declared type. The ellipsis notation in a function prototype declarator causes argument type conversion to stop after the last declared parameter. The default argument promotions are performed on trailing arguments. – C11 6.5.2.2 Function calls (7)

C語言中什麼時候會牽扯到預設引數提升呢?

在C語言中,呼叫一個不帶原型宣告的函式時:呼叫者會對每個引數執行“預設實際引數提升(default argument promotions)。

同時,對可變長引數列表超出最後一個有型別宣告的形式引數之後的每一個實際引數,也將執行上述提升工作。

提升工作如下:
——float型別的實際引數將提升到double
——char、short和相應的signed、unsigned型別的實際引數提升到int
——如果int不能儲存原值,則提升到unsigned int

然後,呼叫者將提升後的引數傳遞給被呼叫者。
所以,my_printf是絕對無法接收到上述型別的實際引數的。

上面的程式碼的42與43行,應該改為:

int c = va_arg(ap,int);
printf("%c",c);

同理, 如果需要使用short和float, 也應該這樣:

short s = (short)va_arg(ap,int);
float f = (float)va_arg(ap,double);

再來看一個具體的例子吧:

#include <stdarg.h>
#include <stdio.h>
 
void read_args_from_va_good(int i, ...)
{
    va_list arg_ptr;
    va_start(arg_ptr, i);
 
    /* This is right. */
    printf("%c\n", va_arg(arg_ptr, int));
    printf("%d\n", va_arg(arg_ptr, int));
    printf("%f\n", va_arg(arg_ptr, double));
 
    va_end(arg_ptr);
}
 
void read_args_from_va_bad(int i, ...)
{
    va_list arg_ptr;
    va_start(arg_ptr, i);
 
    /* This is wrong. */
    printf("%c\n", va_arg(arg_ptr, char));
    printf("%d\n", va_arg(arg_ptr, short));
    printf("%f\n", va_arg(arg_ptr, float));
 
    va_end(arg_ptr);
}
 
int main()
{
    char c = 'c';
    short s = 0;
    float f = 1.1f;
 
    read_args_from_va_good(0, c, s, f);
    read_args_from_va_bad(0, c, s, f);
 
    return 0;
}

上面的程式碼用gcc4.4.0編譯,會有警告:

va_arg.c: In function ‘read_args_from_va_bad’:
va_arg.c:47: warning: ‘char’ is promoted to ‘int’ when passed through ‘...’
va_arg.c:47: note: (so you should pass ‘int’ not ‘char’ to ‘va_arg’)
va_arg.c:47: note: if this code is reached, the program will abort
va_arg.c:48: warning: ‘short int’ is promoted to ‘int’ when passed through ‘...’
va_arg.c:48: note: if this code is reached, the program will abort
va_arg.c:49: warning: ‘float’ is promoted to ‘double’ when passed through ‘...’
va_arg.c:49: note: if this code is reached, the program will abort

執行gcc4.4.6生成的程式時,執行到第23行時,輸出Illegal instruction,程式退出。查看了一下gcc4.4.6生成的彙編程式碼,發現沒有為read_args_from_va_bad()生成有效的程式碼。

[email protected]:~/c$ gdb va_arg -q
Reading symbols from /home/astrol/c/va_arg...done.
(gdb) run
Starting program: /home/astrol/c/va_arg
c 0 1.100000
c
0
1.100000
 
Program received signal SIGILL, Illegal instruction.
0x08048452 in read_args_from_va_bad (i=0) at va_arg.c:44
44              va_start(arg_ptr, i);
(gdb) x/i $pc
=> 0x8048452 <read_args_from_va_bad+12>:        ud2
(gdb)

UD2是一種讓CPU產生invalid opcode exception的軟體指令. 核心發現CPU出現這個異常, 會立即停止執行

在VC中執行的結果是不正確的:
在這裡插入圖片描述
以下摘自《C陷阱與缺陷》
這裡有一個陷阱需要避免:
va_arg巨集的第2個引數不能被指定為char、short或者float型別。
因為char和short型別的引數會被轉換為int型別,而float型別的引數會被轉換為double型別 ……
例如,這樣寫肯定是不對的:
c = va_arg(ap,char);
因為我們無法傳遞一個char型別引數,如果傳遞了,它將會被自動轉化為int型別。上面的式子應該寫成:
c = va_arg(ap,int);
——《C陷阱與缺陷》p164

可能有人會問,VC中的三個巨集不是已經實現了自動int對齊了嗎? 如下:

#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
 
#define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap)      ( ap = (va_list)0 )

下面是linux 2.6.22中的實現,其實是一樣的意思

#define  _AUPBND                (sizeof (acpi_native_int) - 1)
#define  _ADNBND                (sizeof (acpi_native_int) - 1)
 
/*
 * Variable argument list macro definitions
 */
#define _bnd(X, bnd)            (((sizeof (X)) + (bnd)) & (~(bnd)))
#define va_arg(ap, T)           (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
#define va_end(ap)              (void) 0
#define va_start(ap, A)         (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))

不過我想說的是:

① C標準對預設實際引數提升規則有明確規定。
也就是說, 帶有可變長引數列表的函式, 絕對不會接受到char型別的實際引數。

② C標準對va_arg是否自動對齊沒有任何說明。

也就是說自動對齊工作,編譯器可做可不做。

在所有C實現上,能保證第①點,不能保證第②點,所以儘管編譯器實現了自動對齊,也要按標準來。

參考連結:

http://www.cppblog.com/ownwaterloo/archive/2009/04/21/unacceptable_type_in_va_arg.html

(可變長引數列表誤區與陷阱——va_arg不可接受的型別)

http://www.spongeliu.com/331.html

(C語言可變引數函式取參方法)

http://wildpointer.net/2012/04/01/argument_promotion/

(引數型別提升)

http://www.spongeliu.com/325.html

(弱型別?C語言引數提升帶來的一個陷阱)