程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 淺析C/C++中的可變參數與默許參數

淺析C/C++中的可變參數與默許參數

編輯:關於C++

淺析C/C++中的可變參數與默許參數。本站提示廣大學習愛好者:(淺析C/C++中的可變參數與默許參數)文章只能為提供參考,不一定能成為您想要的結果。以下是淺析C/C++中的可變參數與默許參數正文


萬萬要留意,C不支撐默許參數

C/C++支撐可變參數個數的函數界說,這一點與C/C++說話函數參數挪用時入棧次序有關,起首援用其他網友的一段文字,來描寫函數挪用,及參數入棧:

------------ 援用開端 ------------
C支撐可變參數的函數,這裡的意思是C支撐函數帶有可變數目的參數,最多見的例子就是我們非常熟習的printf()系列函數。我們還曉得在函數挪用時參數是自右向左壓棧的。假如可變參數函數的普通情勢是:
    f(p1, p2, p3, …)
那末參數進棧(和出棧)的次序是:
    …
    push p3
    push p2
    push p1
    call f
    pop p1
    pop p2
    pop p3
    …
我可以獲得如許一個結論:假如支撐可變參數的函數,那末參數進棧的次序簡直必定是自右向左的。而且,參數出棧也不克不及由函數本身完成,而應當由挪用者完成。

這個結論的後半部門是不難懂得的,由於函數本身不曉得挪用者傳入了若干參數,然則挪用者曉得,所以挪用者應當擔任將一切參數出棧。

在可變參數函數的普通情勢中,右邊是曾經肯定的參數,左邊省略號代表未知參數部門。關於曾經肯定的參數,它在棧上的地位也必需是肯定的。不然意味著曾經肯定的參數是不克不及定位和找到的,如許是沒法包管函數准確履行的。權衡參數在棧上的地位,就是分開確實的函數挪用點(call f)有多遠。曾經肯定的參數,它在棧上的地位,不該該依附參數的詳細數目,由於參數的數目是未知的!

所以,選擇只能是,曾經肯定的參數,分開函數挪用點有肯定的間隔(較近)。知足這個前提,只要參數入棧服從自右向左規矩。也就是說,右邊肯定的參數後入棧,離函數挪用點有肯定的間隔(最右邊的參數最初入棧,離函數挪用點比來)。

如許,當函數開端履行後,它能找到一切曾經肯定的參數。依據函數本身的邏輯,它擔任尋覓息爭釋前面可變的參數(在分開挪用點較遠的處所),平日這依附於曾經肯定的參數的值(典范的如prinf()函數的格局說明,遺憾的是如許的方法具有軟弱性)。

聽說在pascal中參數是自左向右壓棧的,與C的相反。關於pascal這類只支撐固定參數函數的說話,它沒有可變參數帶來的成績。是以,它選擇哪一種參數進棧方法都是可以的。
乃至,其參數出棧是由函數本身完成的,而不是挪用者,由於函數的參數的類型和數目是完整已知的。這類方法比采取C的方法的效力更好,由於占用更少的代碼量(在C中,函數每次挪用的處所,都生成了參數出棧代碼)。

C++為了兼容C,所以依然支撐函數帶有可變的參數。然則在C++中更好的選擇經常是函數重載。
------------ 援用停止 ------------

依據上文描寫,我們檢查printf()及sprintf()等函數的界說,可以驗證這一點:
_CRTIMP int __cdecl printf(const char *, ...);
_CRTIMP int __cdecl sprintf(char *, const char *, ...);

這兩個函數界說時,都應用了__cdecl症結字,__cdecl症結字商定函數挪用的規矩是:
挪用者擔任消除挪用客棧,參數經由過程客棧傳遞,入棧次序是從右到左。

下一步,我們來看看printf()這類函數是若何應用變個數參數的,上面是摘錄MSDN上的例子,
只援用了ANSI體系兼容部門的代碼,UNIX體系的代碼請直接參考MSDN。

------------ 例子代碼 ------------

#include <stdio.h>
#include <stdarg.h>
int average( int first, ... );

void main( void )
{
   printf( "Average is: %d/n", average( 2, 3, 4, -1 ) );
}

int average( int first, ... )
{
   int count = 0, sum = 0, i = first;
   va_list marker;

   va_start( marker, first );     /* Initialize variable arguments. */
   while( i != -1 )
   {
      sum += i;
      count++;
      i = va_arg( marker, int);
   }
   va_end( marker );              /* Reset variable arguments.      */
   return( sum ? (sum / count) : 0 );
}

上例代碼功效是盤算均勻數,函數許可用戶輸出多個整型參數,請求作後一個參數必需是-1,表現參數輸出終了,然後前往均勻數盤算成果。

邏輯很簡略,起首界說
   va_list marker;
表現參數列表,然後挪用va_start()初始化參數列表。留意va_start()挪用時不只應用了marker
這個參數列表變量,還應用了first這個參數,解釋參數列表的初始化與函數給定的第一個肯定參數是有關系的,這一點很症結,後續剖析會看到緣由。

挪用va_start()初始化後,便可挪用va_arg()函數拜訪每個參數列表中的參數了。留意va_arg()
的第二個參數指定了前往值的類型(int)。

當法式肯定一切參數拜訪停止後,挪用va_end()函數停止參數列表拜訪。

如許看起來,拜訪變個數參數是很輕易的,也就是應用va_list,va_start(),va_arg(),va_end()
如許一個類型與三個函數。然則關於函數變個數參數的機制,感到還是一頭霧水。看來須要持續深刻探討,能力的到確實的謎底了。

找到va_list,va_start(),va_arg(),va_end()的界說,在.../VC98/include/stdarg.h文件中。
.h中代碼以下(只摘錄了ANSI兼容部門的代碼,UNIX等其他體系完成略有分歧,感興致的同伙可以本身研討):

typedef char *  va_list;

#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 )

從代碼可以看出,va_list只是一個類型本義,其實就是界說成char*類型的指針了,如許就是為了以字節為單元拜訪內存。
其他三個函數其實只是三個宏界說,且慢,我們先看夾在中央的這個宏界說_INTSIZEOF:

#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

這個宏的功效是對給定變量或許類型n,盤算其按整型字節長度停止字節對齊後的長度(size)。在32位體系中int占4個字節,16位體系中占2字節。
表達式
 (sizeof(n) + sizeof(int) - 1)
的感化是,假如sizeof(n)小於sizeof(int),則盤算後
的成果數值,會比sizeof(n)的值在二進制上向左進一名。

如:sizeof(short) + sizeof(n) - 1 = 5
5的二進制是0x00000101,sizeof(short)的二進制是0x00000010,所以5的二進制值比2的二進制值
向左高一名。

表達式
 ~(sizeof(int) - 1)
的感化時生成一個蒙版(mask),以便捨去後面誰人盤算值的"零頭"部門。
如上例,~(sizeof(int) - 1) = 0x00000011(感謝glietboys的提示,此處應當是0xFFFFFF00)
同5的二進制0x00000101做"與"運算獲得的是0x00000100,也就是4,而直接盤算sizeof(short)應當獲得2。
如許經由過程_INTSIZEOF(short)如許的表達式,便可以獲得依照整型字節長度對齊的其他類型字節長度。
之所以采取int類型的字節長度停止對齊,是由於C/C++中的指針變量其實就是整型數值,長度與int雷同,而指針的偏移量是前面的三個宏停止運算時所須要的。

關於編程中字節對齊的內容請有興致的同伙到網上參考其他文章,這裡不再贅述。

持續,上面這個三個宏界說:

第一:
#define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )

編程中如許應用
   va_list marker;
   va_start( marker, first );
可以看出va_start宏的感化是使給定的參數列表指針(marker),依據第一個肯定參數(first)所屬類型的指針長度向後偏移響應地位,盤算這個偏移的時刻就用到了後面的_INTSIZEOF(n)宏。

第二:
#define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

此處乍一看有點隱晦,(ap += _INTSIZEOF(t)) - _INTSIZEOF(t)表達式的一加一減,對前往值是不起感化的啊,也就是前往值都是ap的值,甚麼緣由呢?
本來這個盤算前往值是一方面,另外一方面,請記住,va_start(),va_arg(),va_end這三個宏的挪用是有聯系關系性的,ap這個變量是挪用va_start()時給定的參數列表指針,所以

(ap += _INTSIZEOF(t)) - _INTSIZEOF(t)

表達式不只僅是為了前往以後指向的參數的地址,照樣為了讓ap指向下一個參數(留意ap跳向下一參數是,是依照類型t的_INTSIZEOF長度停止盤算的)。

第三:
#define va_end(ap)      ( ap = (va_list)0 )

這個很好懂得了,不外是將ap指針置為空,算作參數讀取停止。

至此,C/C++變個數函數參數的機制曾經很清楚了。最初還要說一點要留意的成績:
在用va_arg()次序跳轉指針讀取參數的進程中,並沒無方法去斷定所獲得的下一個指針能否是有用地址,也沒有處所可以或許明白得知究竟要讀取若干個參數,這就是這類變個數參數的風險地點。後面的求均勻數的例子中,請求輸出者必需在參數列表最初供給一個特別值(-1)來表現參數列表停止,所以可以假定,萬一挪用者沒有遵守這類規矩,將招致指針拜訪越界。

那末,能夠有同伙會問,printf()函數就沒有供給如許的特別值停止標識啊。

別急,printf()應用的是另外一種參數個數辨認方法,能夠比擬隱藏。留意他的第一個肯定參數,也就是被我們用作格局掌握的format字符串,他的外面有"%d","%s"如許的參數描寫符,printf()函數在解析format字符串時,可以依據參數描寫符的個數,肯定須要讀取前面幾個參數。我們無妨做上面如許的實驗:

printf("%d,%d,%d,%d/n",1,2,3,4,5);

現實供給的參數多於後面給定的參數描寫符,如許履行的成果是

1,2,3,4

也就是printf()依據format字符串以為前面只要4個參數,其他的就不論了。那末再做一個實驗:

printf("%d,%d,%d,%d/n",1,2,3);

現實供給的參數少於給定的參數描寫符,如許履行的成果是(假如沒有異常的話)

1,2,3,2367460

這個處所,每一個人的履行成果能夠都不雷同,緣由是讀取最初一個參數的指針曾經指向了不法的地址。這也是應用printf()這類函數須要特殊留意的處所。

總結:
變個數的函數參數在應用時須要留意的處所比擬多。我小我建議盡可能躲避應用這類形式。好比後面的盤算均勻數,寧可以使用數組或其他列表作為參數將一系列數值傳遞給函數,也不消寫如許的失常函數。一方面是輕易湧現指針拜訪越界,另外一方面,在現實的函數挪用時,要把一切盤算值順次作為參數寫在代碼裡,很骯髒。

固然這麼說,但有些處所這個功效照樣很有效處的,好比字符串的格局化分解,像printf()函數;在現實運用中,我還常常應用一個本身寫的WriteLog()函數,用於記載文件日記,界說與printf()雷同,應用起來異常靈巧方便,如:

WriteLog("用戶%s, 登錄次數%d","guanzhong",10);

寫在文件裡的內容就是

用戶guanzhong, 登錄次數10

編程說話的應用,在遵守根本規矩的條件下,是仁者見仁,智者見智。總之,透辟懂得以後,選擇一個相符本身的好的習氣便可

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved