關於c++變長參數列表總結

分類:IT技術 時間:2016-10-07

參考資料

C語言函數可變參數詳解 - ranpanf的專欄 - 博客頻道 - CSDN.NET
http://blog.csdn.net/ranpanf/article/details/4693130

stdarg.h解析

stdarg.h是ANSI C 的標準頭文件。它對可變參數的函數(vararg function)提供了支持。什麽是可變參數的函數呢?舉個例子:printf 與scanf就是。

stdarg.h對vararg function支持的關鍵是定義了幾個非常有用的宏。

1.typedef char *va_list;

註解:本質是一個指向程序運行棧中某一個地址的指針(以後提到的棧都為程序運行棧)

2.#define __va_size(type) /

(((sizeof(type) + sizeof(long) - 1) / sizeof(long)) */ sizeof(long))

註解:計算某種數據類型的參數在棧中占有的空間。/是連接宏定義中的兩行:如

#define sum(a,b)((a)+(b))

等效於:

#define sum(a,b)((/

a)+(b))

在IA32(32位機器程序或匯編程序)的程序中,PUSH和POP指令的操作數是4Bytes(DWORD)。如:char變量為一個字節,但入棧時需要一個DWORD。一個

sizeof為5Bytes的結構變量需要2個DWORD。

3#define va_start(ap, last) /

((ap) = (va_list)&(last) + __va_size(last))

註解:讓直指指向第一個可變參數的首地址。

4. #define va_arg(ap, type) /

(*(type *)((ap) += __va_size(type), (ap) -/ __va_size(type)))

註解:這是我見過用逗號操作數最巧妙地例子。

5. #define va_end(ap) ((void)0)

註解:出現一個空值 標準庫裏將指向可變參數的指針重置為NULL。

為了詳細解析,我先說一下幾個要點。

1. 函數參數的傳遞機制:這裏不是指值傳遞和引用傳遞。而是以從匯編(機器)語言程序員看參數傳遞。

我們知道有三種傳遞方式:寄存器傳值,存儲器傳值,堆棧傳值。C支持那種參數傳值方式呢?C程序員怎麽選擇函數參數傳遞方式呢?在C語言的標準引入了一個概念:調用規則(calling convention),調用規則有:_cdecl,_stdcall,_fastcall,_thiscall(僅C++支持)等,請大家參閱MSDN獲得詳情。_cdecl 默認C/C++的調用規則,_stdcall win32API的調用規則,_fastcall一般不用,_thiscall C++的類成員函數的調用規則。詳細情況見下表:

image

                    表1 函數調用規則詳解

 

 

函數的調用過程:用戶函數的調用是由程序運行棧來管理的,

標準庫函數和系統調用一般也會用到棧,還有共享代碼段(dll),陷入(軟中斷)等概念,我就不詳細說了。現在只要知道用戶函數是由程序運行棧管理的即可,棧的功能體現在三點:1.用棧保存函數的返回地址,2.用棧傳遞函數參數,3.在棧中創建局部變量。這裏有個概念叫棧幀,它是指函數在調用時占用的棧中一部分連續的空間。函數的嵌套反映在棧中就是調用函數的棧幀的地址高於被調用者的棧幀的地址並順序存放(假設棧是從高向低增長的)。函數的返回會伴隨著他的棧幀的銷毀,這也是為什麽局部變量會在作用域之外沒法應用。上面的表中有一列為“清棧”,想必你讀到此處,它的含義你明白了:調用者清棧,是指被調用的函數參數保存在它調用者的棧幀中;而被調用者清棧,是指被調用的函數參數保存在它自己的棧幀中,通常用RET n指令返回。

現在我解析一段小程序:

#include<stdio.h>
#include<stdlib.h>
#include<stdarg.h>   //用於變參函數,變長參數列表
using namespace std;

int  average(int first, ...)
{
    int count = 0, sum = 0, i = first;
    va_list marker;                 //typedef char *va_list 可以僅將va_list看作一個指向一片內存空間的指針。
    va_start(marker, first);     // Initialize variable arguments 將maker指向變長參數列表中第一個參數的起始地址
    while (i != -1)
    {
        count++;
        sum += i;
        i = va_arg(marker, int);  
    }
    va_end(marker);              // Reset variable arguments 相當於將指針置為NULL
    return(sum ? (sum / count) : 0);
}

int main(){
    int a = 20;
    printf("Average(%d,%d)=%d\n", a, 400, average(a, 400, -1));
    return printf("Average(%d,%d,%d)=%d\n", 100, 20, 400, average(100, 20, 400, -1));
}

//這裏是標準庫中與變長參數相關的定義.

在標準庫中所有帶參數的宏最展開的語句中最外面都帶一層括號。
下面這條語句的編寫真的是很見功力。自己經過測試發現,short,int在此語句下,如果假定short-2,int-4,此語句求得的short占用4字節。int占用4字節。如果是long long類型,假定本身為8byte,求得的長度為8. 通過這一條宏考慮了所有字節對其的需要。雖然毫無可讀性,應該是我能力不夠,但真讓人佩服。一條語句融合了位運算、sizeof運算符,並且內在地包含了對內存對齊等機制的理解,即一個類型或變量的sizeof長度與其在內存棧中“占據”的空間並不完全相同。這條語句不會是通過枚舉各種可能的情況歸納出來的吧!!!???感覺比24點遊戲難多了。哈哈
#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

----------------------------------

 

因為變參函數要求最左邊的參數必須給定,所以參數的起始地址由最左邊的參數確定。另外貌似變參參數的類型要與最左邊的參數類型相同,這裏的+號後面的數,剛好指向變參列表的第一個參數。

 

#define _ADDRESSOF(v)   ( &reinterpret_cast<const char &>(v) )
#define _crt_va_start(ap,v)  ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) ) 

-----------------------------------

下面這條宏對應上面的va_arg(marker,int)。不要認為是編寫者抽風,先加了一個數,再減去一個相同的數,在語義上這兩個量是不能抵銷的,因為前面的一個量是具有累加效果的,使用的+=,屬於賦值運算符的範疇,會修改ap的值,而後面的減法運算,只是將ap減去這個量的值作為整個表達式的值,並不改變ap的值。雖然這個宏第一次運行相當ap未變化,但之後的每次都向後偏移。之所以這樣編寫是將所有的情況統一起來。實際上如果ap在va_start中指向的是第一個顯式參數的地址時,就不再需要後面的減法。  註意區別賦值運算與非賦值運算。

#define _crt_va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) 

補充-另外一種實現方式(更好理解)

“i = va_arg( marker, int);”:取函數當前可變參數,並且讓指針指向下一個可變參數,該宏的定義如下:

#define va_arg(ap, type) /

(*(type *)((ap) += __va_size(type), (ap) - __va_size(type)))

上面用到了續行符貌似續行符後面不能多加空格,會報錯的,最好也不要在後面寫註釋,會不會報錯沒測試過。

這裏用到了逗號運算符“,”。逗號運算符的定義是:expr1,expr2

先計算expr1,在計算expr2,整個表達式的結果為expr2的值。

顯然逗號運算符的左側操作數:(ap) += __va_size(type)是將修改指針移向下一個可變參數,但不會作為整個表達式的結果;而右側操作數(ap) - __va_size(type)的不會修該指針的值,但會將恢復到指針被修改之前結果並將其作為整個表達式的結果(但要記住ap的值還是被向後改變了)。該結果為char型的變量的地址值,經過強制類型轉化和取指針指向的值,終於得到了type類型的可變參數。

另外值得指出的是,對於變參函數,函數調用方式使用的是__cdecl,參數是從右到左入棧,即本例中first在棧頂,而棧位於內存的高端,棧頂的地址小於棧底的地址。所以每次訪問要將相應的地址加上一個數,而不是減去一個數。
---------------------------

#define _crt_va_end(ap)      ( ap = (va_list)0 ) 

幾點需要註意

1. 有可變參數的函數必須擁有非可變參數,並且非可變參數必須位於可變參數的左面(原文是前面,感覺用左邊更好一些)。

2. 程序員必須自己控制可變參數的類型以及可變參數的數目。

 


Tags: function 參考資料 C語言 博客 程序

文章來源:


ads
ads

相關文章
ads

相關文章

ad