国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁(yè) > 學(xué)院 > 開(kāi)發(fā)設(shè)計(jì) > 正文

對(duì)C/C++可變參數(shù)表的深層探索

2019-11-17 05:20:35
字體:
來(lái)源:轉(zhuǎn)載
供稿:網(wǎng)友
引言

  C/C++語(yǔ)言有一個(gè)不同于其它語(yǔ)言的特性,即其支持可變參數(shù),典型的函數(shù)如PRintf、scanf等可以接受數(shù)量不定的參數(shù)。如:

printf ( "I love you" );
printf ( "%d", a );
printf ( "%d,%d", a, b );
  第一、二、三個(gè)printf分別接受1、2、3個(gè)參數(shù),讓我們看看printf函數(shù)的原型:

int printf ( const char *format, ... );
  從函數(shù)原型可以看出,其除了接收一個(gè)固定的參數(shù)format以外,后面的參數(shù)用"…"表示。在C/C++語(yǔ)言中,"…"表示可以接受不定數(shù)量的參數(shù),理論上來(lái)講,可以是0或0以上的n個(gè)參數(shù)。

  本文將對(duì)C/C++可變參數(shù)表的使用方法及C/C++支持可變參數(shù)表的深層機(jī)理進(jìn)行探索。

  可變參數(shù)表的用法

  1、相關(guān)宏

  標(biāo)準(zhǔn)C/C++包含頭文件stdarg.h,該頭文件中定義了如下三個(gè)宏:

void va_start ( va_list arg_ptr, prev_param ); /* ANSI version */
type va_arg ( va_list arg_ptr, type );
void va_end ( va_list arg_ptr );
  在這些宏中,va就是variable argument(可變參數(shù))的意思;arg_ptr是指向可變參數(shù)表的指針;prev_param則指可變參數(shù)表的前一個(gè)固定參數(shù);type為可變參數(shù)的類型。va_list也是一個(gè)宏,其定義為typedef char * va_list,實(shí)質(zhì)上是一char型指針。char型指針的特點(diǎn)是++、--操作對(duì)其作用的結(jié)果是增1和減1(因?yàn)閟izeof(char)為1),與之不同的是int等其它類型指針的++、--操作對(duì)其作用的結(jié)果是增sizeof(type)或減sizeof(type),而且sizeof(type)大于1。

  通過(guò)va_start宏我們可以取得可變參數(shù)表的首指針,這個(gè)宏的定義為:

#define va_start ( ap, v ) ( ap = (va_list)&v + _INTSIZEOF(v) )
  顯而易見(jiàn),其含義為將最后那個(gè)固定參數(shù)的地址加上可變參數(shù)對(duì)其的偏移后賦值給ap,這樣ap就是可變參數(shù)表的首地址。其中的_INTSIZEOF宏定義為:

#define _INTSIZEOF(n) ((sizeof ( n ) + sizeof ( int ) - 1 ) & ~( sizeof( int ) - 1 ) )
  va_arg宏的意思則指取出當(dāng)前arg_ptr所指的可變參數(shù)并將ap指針指向下一可變參數(shù),其原型為:

#define va_arg(list, mode) ((mode *)(list =/
(char *) ((((int)list + (__builtin_alignof(mode)<=4?3:7)) &/
(__builtin_alignof(mode)<=4?-4:-8))+sizeof(mode))))[-1]
  對(duì)這個(gè)宏的具體含義我們將在后面深入討論。

  而va_end宏被用來(lái)結(jié)束可變參數(shù)的獲取,其定義為:

#define va_end ( list )
  可以看出,va_end ( list )實(shí)際上被定義為空,沒(méi)有任何真實(shí)對(duì)應(yīng)的代碼,用于代碼對(duì)稱,與va_start對(duì)應(yīng);另外,它還可能發(fā)揮代碼的"自注釋"作用。所謂代碼的"自注釋",指的是代碼能自己注釋自己。

  下面我們以具體的例子來(lái)說(shuō)明以上三個(gè)宏的使用方法。

  2、一個(gè)簡(jiǎn)單的例子

#include <stdarg.h>
/* 函數(shù)名:max
* 功能:返回n個(gè)整數(shù)中的最大值
* 參數(shù):num:整數(shù)的個(gè)數(shù) ...:num個(gè)輸入的整數(shù)
* 返回值:求得的最大整數(shù)
*/
int max ( int num, ... )
{
 int m = -0x7FFFFFFF; /* 32系統(tǒng)中最小的整數(shù) */
 va_list ap;
 va_start ( ap, num );
 for ( int i= 0; i< num; i++ )
 {
  int t = va_arg (ap, int);
  if ( t > m )
  {
   m = t;
  }
 }
 va_end (ap);
 return m;
}
/* 主函數(shù)調(diào)用max */
int main ( int argc, char* argv[] )
{
 int n = max ( 5, 5, 6 ,3 ,8 ,5); /* 求5個(gè)整數(shù)中的最大值 */
 cout << n;
 return 0;
}
  函數(shù)max中首先定義了可變參數(shù)表指針ap,而后通過(guò)va_start ( ap, num )取得了參數(shù)表首地址(賦給了ap),其后的for循環(huán)則用來(lái)遍歷可變參數(shù)表。這種遍歷方式與我們?cè)跀?shù)據(jù)結(jié)構(gòu)教材中經(jīng)??吹降谋闅v方式是類似的。   函數(shù)max看起來(lái)簡(jiǎn)潔明了,但是實(shí)際上printf的實(shí)現(xiàn)卻遠(yuǎn)比這復(fù)雜。max函數(shù)之所以看起來(lái)簡(jiǎn)單,是因?yàn)椋?br />
  (1) max函數(shù)可變參數(shù)表的長(zhǎng)度是已知的,通過(guò)num參數(shù)傳入;

  (2) max函數(shù)可變參數(shù)表中參數(shù)的類型是已知的,都為int型。

  而printf函數(shù)則沒(méi)有這么幸運(yùn)。首先,printf函數(shù)可變參數(shù)的個(gè)數(shù)不能輕易的得到,而可變參數(shù)的類型也不是固定的,需由格式字符串進(jìn)行識(shí)別(由%f、%d、%s等確定),因此則涉及到可變參數(shù)表的更復(fù)雜應(yīng)用。

  下面我們以實(shí)例來(lái)分析可變參數(shù)表的高級(jí)應(yīng)用。
  高級(jí)應(yīng)用

  下面這個(gè)程序是我們?yōu)槟城度胧较到y(tǒng)(該系統(tǒng)中CPU的字長(zhǎng)為16位)編寫的在屏幕上顯示格式字符串的函數(shù)DrawText,它的用法類似于int printf ( const char *format, ... )函數(shù),但其輸出的目標(biāo)為嵌入式系統(tǒng)的液晶顯示屏幕(LED)。

///////////////////////////////////////////////////////////////////////////////
// 函數(shù)名稱: DrawText
// 功能說(shuō)明: 在顯示屏上繪制文字
// 參數(shù)說(shuō)明: XPos ---橫坐標(biāo)的位置 [0 .. 30]
// yPos ---縱坐標(biāo)的位置 [0 .. 64]
// ... 可以同數(shù)字一起顯示,需設(shè)置標(biāo)志(%d、%l、%x、%s)
///////////////////////////////////////////////////////////////////////////////
extern void DrawText ( BYTE xPos, BYTE yPos, LPBYTE lpStr, ... )
{
 BYTE lpData[100]; //緩沖區(qū)
 BYTE byIndex;
 BYTE byLen;
 DWord dwTemp;
 WORD wTemp;
 int i;
 va_list lpParam;

 memset( lpData, 0, 100);
 byLen = strlen( lpStr );
 byIndex = 0;
 va_start ( lpParam, lpStr );

 for ( i = 0; i < byLen; i++ )
 {
  if( lpStr[i] != ’%’ ) //不是格式符開(kāi)始
  {
   lpData[byIndex++] = lpStr[i];
  }
  else
  {
   switch (lpStr[i+1])
   {
    //整型
    case ’d’:
    case ’D’:
     wTemp = va_arg ( lpParam, int );
     byIndex += IntToStr( lpData+byIndex, (DWORD)wTemp );
     i++;
     break;
     //長(zhǎng)整型
    case ’l’:
    case ’L’:
     dwTemp = va_arg ( lpParam, long );
     byIndex += IntToStr ( lpData+byIndex, (DWORD)dwTemp );
     i++;
     break;
     //16進(jìn)制(長(zhǎng)整型)
    case ’x’:
    case ’X’:
     dwTemp = va_arg ( lpParam, long );
     byIndex += HexToStr ( lpData+byIndex, (DWORD)dwTemp );
     i++;
     break;
    default:
     lpData[byIndex++] = lpStr[i];
     break;
   }
  }
 }
 va_end ( lpParam );
 lpData[byIndex] = ’/0’;
 DisplayString ( xPos, yPos, lpData, TRUE); //在屏幕上顯示字符串lpData
}
  在這個(gè)函數(shù)中,需通過(guò)對(duì)傳入的格式字符串(首地址為lpStr)進(jìn)行識(shí)別來(lái)獲知可變參數(shù)個(gè)數(shù)及各個(gè)可變參數(shù)的類型,具體實(shí)現(xiàn)體現(xiàn)在for循環(huán)中。譬如,在識(shí)別為%d后,做的是va_arg ( lpParam, int ),而獲知為%l和%x后則進(jìn)行的是va_arg ( lpParam, long )。格式字符串識(shí)別完成后,可變參數(shù)也就處理完了。

  在項(xiàng)目的最初,我們一直苦于不能找到一個(gè)好的辦法來(lái)混合輸出字符串和數(shù)字,我們采用了分別顯示數(shù)字和字符串的方法,并分別指定坐標(biāo),程序條理被破壞。而且,在混合顯示的時(shí)候,要給各類數(shù)據(jù)分別人工計(jì)算坐標(biāo),我們感覺(jué)頭疼不已。以前的函數(shù)為:

//顯示字符串
showString ( BYTE xPos, BYTE yPos, LPBYTE lpStr )
//顯示數(shù)字
showNum ( BYTE xPos, BYTE yPos, int num )
//以16進(jìn)制方式顯示數(shù)字
showHexNum ( BYTE xPos, BYTE yPos, int num )
  最終,我們用DrawText ( BYTE xPos, BYTE yPos, LPBYTE lpStr, ... )函數(shù)代替了原先所有的輸出函數(shù),程序得到了簡(jiǎn)化。就這樣,兄弟們用得爽翻了?! ∵\(yùn)行機(jī)制探索

  通過(guò)第2節(jié)我們學(xué)會(huì)了可變參數(shù)表的使用方法,相信喜歡拋根問(wèn)底的讀者還不甘心,必然想知道如下問(wèn)題:

 ?。?)為什么按照第2節(jié)的做法就可以獲得可變參數(shù)并對(duì)其進(jìn)行操作?

  (2)C/C++在底層究竟是依靠什么來(lái)對(duì)這一語(yǔ)法進(jìn)行支持的,為什么其它語(yǔ)言就不能提供可變參數(shù)表呢?

  我們帶著這些疑問(wèn)來(lái)一步步進(jìn)行摸索。

  3.1 調(diào)用機(jī)制反匯編

  反匯編是研究語(yǔ)法深層特性的終極良策,先來(lái)看看2.2節(jié)例子中主函數(shù)進(jìn)行max ( 5, 5, 6 ,3 ,8 ,5)調(diào)用時(shí)的反匯編:


1. 004010C8 push 5
2. 004010CA push 8
3. 004010CC push 3
4. 004010CE push 6
5. 004010D0 push 5
6. 004010D2 push 5
7. 004010D4 call @ILT+5(max) (0040100a)
  從上述反匯編代碼中我們可以看出,C/C++函數(shù)調(diào)用的過(guò)程中:

  第一步:將參數(shù)從右向左入棧(第1~6行);

  第二步:調(diào)用call指令進(jìn)行跳轉(zhuǎn)(第7行)。

  這兩步包含了深刻的含義,它說(shuō)明C/C++默認(rèn)的調(diào)用方式為由調(diào)用者治理參數(shù)入棧的操作,且入棧的順序?yàn)閺挠抑磷螅@種調(diào)用方式稱為_(kāi)cdecl調(diào)用。x86系統(tǒng)的入棧方向?yàn)閺母叩刂返降偷刂?,故?至n個(gè)參數(shù)被放在了地址遞增的堆棧內(nèi)。在被調(diào)用函數(shù)內(nèi)部,讀取這些堆棧的內(nèi)容就可獲得各個(gè)參數(shù)的值,讓我們反匯編到max函數(shù)的內(nèi)部:

int max ( int num, ...)
{
1. 00401020 push ebp
2. 00401021 mov ebp,esp
3. 00401023 sub esp,50h
4. 00401026 push ebx
5. 00401027 push esi
6. 00401028 push edi
7. 00401029 lea edi,[ebp-50h]
8. 0040102C mov ecx,14h
9. 00401031 mov eax,0CCCCCCCCh
10. 00401036 rep stos dword ptr [edi]
va_list ap;
int m = -0x7FFFFFFF; /* 32系統(tǒng)中最小的整數(shù) */
11. 00401038 mov dword ptr [ebp-8],80000001h
va_start ( ap, num );
12. 0040103F lea eax,[ebp+0Ch]
13. 00401042 mov dword ptr [ebp-4],eax
for ( int i= 0; i< num; i++ )
14. 00401045 mov dword ptr [ebp-0Ch],0
15. 0040104C jmp max+37h (00401057)
16. 0040104E mov ecx,dword ptr [ebp-0Ch]
17. 00401051 add ecx,1
18. 00401054 mov dword ptr [ebp-0Ch],ecx
19. 00401057 mov edx,dword ptr [ebp-0Ch]
20. 0040105A cmp edx,dword ptr [ebp+8]
21. 0040105D jge max+61h (00401081)
{
int t= va_arg (ap, int);
22. 0040105F mov eax,dword ptr [ebp-4]
23. 00401062 add eax,4
24. 00401065 mov dword ptr [ebp-4],eax
25. 00401068 mov ecx,dword ptr [ebp-4]
26. 0040106B mov edx,dword ptr [ecx-4]
27. 0040106E mov dword ptr [t],edx
if ( t > m )
28. 00401071 mov eax,dword ptr [t]
29. 00401074 cmp eax,dword ptr [ebp-8]
30. 00401077 jle max+5Fh (0040107f)
m = t;
31. 00401079 mov ecx,dword ptr [t]
32. 0040107C mov dword ptr [ebp-8],ecx
}
33. 0040107F jmp max+2Eh (0040104e)
va_end (ap);
34. 00401081 mov dword ptr [ebp-4],0
return m;
35. 00401088 mov eax,dword ptr [ebp-8]
}
36. 0040108B pop edi
37. 0040108C pop esi
38. 0040108D pop ebx
39. 0040108E mov esp,ebp
40. 00401090 pop ebp
41. 00401091 ret
  分析上述反匯編代碼,對(duì)于一個(gè)真正的程序員而言,將是一種很大的享受;而對(duì)于初學(xué)者,也將使其受益良多。所以請(qǐng)一定要賴著頭皮認(rèn)真研究,千萬(wàn)不要被嚇倒!

  行1~10進(jìn)行執(zhí)行函數(shù)內(nèi)代碼的預(yù)備工作,保存現(xiàn)場(chǎng)。第2行對(duì)堆棧進(jìn)行移動(dòng);第3行則意味著max函數(shù)為其內(nèi)部局部變量預(yù)備的堆??臻g為50h字節(jié);第11行表示把變量n的內(nèi)存空間安排在了函數(shù)內(nèi)部局部棧底減8的位置(占用4個(gè)字節(jié))。

  第12~13行非常要害,對(duì)應(yīng)著va_start ( ap, num ),這兩行將第一個(gè)可變參數(shù)的地址賦值給了指針ap。另外,從第12行可以看出num的地址為ebp+0Ch;從第13行可以看出ap被分配在函數(shù)內(nèi)部局部棧底減4的位置上(占用4個(gè)字節(jié))。

  第22~27行最為要害,對(duì)應(yīng)著va_arg (ap, int)。其中,22~24行的作用為將ap指向下一可變參數(shù)(可變參數(shù)的地址間隔為4個(gè)字節(jié),從add eax,4可以看出);25~27行則取當(dāng)前可變參數(shù)的值賦給變量t。這段反匯編很希奇,它先移動(dòng)可變參數(shù)指針,再在賦值指令里面回過(guò)頭來(lái)取先前的參數(shù)值賦給t(從mov edx,dword ptr [ecx-4]語(yǔ)句可以看出)。Visual C++同學(xué)玩得有意思,不知道碰見(jiàn)同樣的情況Visual Basic等其它同學(xué)怎么玩?

  第36~41行恢復(fù)現(xiàn)場(chǎng)和堆棧地址,執(zhí)行函數(shù)返回操作。

  痛苦的反匯編之旅差不多結(jié)束了,看了這段反匯編我們總算弄明白了可變參數(shù)的存放位置以及它們被讀取的方式,頓覺(jué)全省輕松!

  2、非凡的調(diào)用約定

  除此之外,我們需要了解C/C++函數(shù)調(diào)用對(duì)參數(shù)占用空間的一些非凡約定,因?yàn)樵赺cdecl調(diào)用協(xié)議中,有些變量類型是按照其它變量的尺寸入棧的。

  例如,字符型變量將被自動(dòng)擴(kuò)展為一個(gè)字的空間,因?yàn)槿霔2僮麽槍?duì)的是一個(gè)字。

  參數(shù)n實(shí)際占用的空間為( ( sizeof(n) + sizeof(int) - 1 ) & ~( sizeof(int) - 1 ) ),這就是第2.1節(jié)_INTSIZEOF(v)宏的來(lái)歷!

  既然如此,前面給出的va_arg ( list, mode )宏為什么玩這么大的飛機(jī)就很清楚了。這個(gè)問(wèn)題就留個(gè)讀者您來(lái)分析。


發(fā)表評(píng)論 共有條評(píng)論
用戶名: 密碼:
驗(yàn)證碼: 匿名發(fā)表
主站蜘蛛池模板: 柏乡县| 肇庆市| 新巴尔虎右旗| 托克逊县| 体育| 鹤壁市| 雅安市| 沽源县| 黄梅县| 南丰县| 榕江县| 天水市| 康保县| 台南县| 芮城县| 高碑店市| 四平市| 台中县| 同心县| 资兴市| 涿鹿县| 兴化市| 资中县| 时尚| 射阳县| 高要市| 嵩明县| 玛沁县| 惠安县| 会理县| 资兴市| 龙井市| 普陀区| 保定市| 梓潼县| 栖霞市| 腾冲县| 堆龙德庆县| 塘沽区| 额尔古纳市| 维西|