C/C++语言可变参数表深层探索

简介:

C/C++语言可变参数表深层探索

作者:宋宝华  e-mail:[email]21cnbao@21cn.com[/email]
1. 引言
C/C++ 语言有一个不同于其它语言的特性,即其支持可变参数,典型的函数如 printf scanf 等可以接受数量不定的参数。如:
printf ( "I love you" );
printf ( "%d", a );
printf ( "%d,%d", a, b );
第一、二、三个 printf 分别接受 1 2 3 个参数,让我们看看 printf 函数的原型:
int printf ( const char *format, ... );
从函数原型可以看出,其除了接收一个固定的参数 format 以外,后面的参数用“ ”表示。在 C/C++ 语言中,“ ”表示可以接受不定数量的参数,理论上来讲,可以是 0 0 以上的 n 个参数。
本文将对 C/C++ 可变参数表的使用方法及 C/C++ 支持可变参数表的深层机理进行探索。
2. 可变参数表的用法
2.1 相关宏
标准 C/C++ 包含头文件 stdarg.h ,该头文件中定义了如下三个宏:
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( 可变参数 ) 的意思; arg_ptr 是指向可变参数表的指针; prev_param 则指可变参数表的前一个固定参数; type 为可变参数的类型。 va_list 也是一个宏,其定义为 typedef char * va_list ,实质上是一 char 型指针。 char 型指针的特点是 ++ -- 操作对其作用的结果是增 1 和减 1 (因为 sizeof(char) 1 ),与之不同的是 int 等其它类型指针的 ++ -- 操作对其作用的结果是增 sizeof(type) 或减 sizeof(type) ,而且 sizeof(type) 大于 1
通过 va_start 宏我们可以取得可变参数表的首指针,这个宏的定义为:
#define va_start ( ap, v )  ( ap = (va_list)&v + _INTSIZEOF(v) )
显而易见,其含义为将最后那个固定参数的地址加上可变参数对其的偏移后赋值给 ap ,这样 ap 就是可变参数表的首地址。其中的 _INTSIZEOF 宏定义为:
#define _INTSIZEOF(n) ((sizeof ( n ) + sizeof ( int ) – 1 ) & ~( sizeof( int ) – 1 ) )
va_arg 宏的意思则指取出当前 arg_ptr 所指的可变参数并将 ap 指针指向下一可变参数,其原型为:
#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]
对这个宏的具体含义我们将在第 3 节深入讨论。
va_end 宏被用来结束可变参数的获取,其定义为:
#define va_end ( list )
可以看出, va_end ( list ) 实际上被定义为空,没有任何真实对应的代码,用于代码对称,与 va_start 对应;另外,它还可能发挥代码的“自注释”作用。所谓代码的“自注释”,指的是代码能自己注释自己。
下面我们以具体的例子来说明以上三个宏的使用方法。
2.2 一个简单的例子
#include <stdarg.h>
/*  函数名: max
 *  功能:返回 n 个整数中的最大值
 *   参数: num :整数的个数  ... num 个输入的整数
 *   返回值:求得的最大整数
 */
int max ( int num, ... )
{
int m = -0x7FFFFFFF;  /* 32 系统中最小的整数  */
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;
}
 
/*  主函数调用 max */
int main ( int argc, char* argv[] )
{     
int n = max ( 5, 5, 6 ,3 ,8 ,5);  /*  5 个整数中的最大值  */
cout << n;
return 0;
}
函数 max 中首先定义了可变参数表指针 ap ,而后通过 va_start ( ap, num ) 取得了参数表首地址(赋给了 ap ),其后的 for 循环则用来遍历可变参数表。这种遍历方式与我们在数据结构教材中经常看到的遍历方式是类似的。
函数 max 看起来简洁明了,但是实际上 printf 的实现却远比这复杂。 max 函数之所以看起来简单,是因为:
(1)   max 函数可变参数表的长度是已知的,通过 num 参数传入;
(2)   max 函数可变参数表中参数的类型是已知的,都为 int 型。
printf 函数则没有这么幸运。首先, printf 函数可变参数的个数不能轻易的得到,而可变参数的类型也不是固定的,需由格式字符串进行识别(由 %f %d %s 等确定),因此则涉及到可变参数表的更复杂应用。
下面我们以实例来分析可变参数表的高级应用。
2.3 高级应用
下面这个程序是我们为某嵌入式系统(该系统中 CPU 的字长为 16 位)编写的在屏幕上显示格式字符串的函数 DrawText ,它的用法类似于 int printf ( const char *format, ... ) 函数,但其输出的目标为嵌入式系统的液晶显示屏幕( LED )。
///////////////////////////////////////////////////////////////////////////////
//    函数名称 :    DrawText
//    功能说明 :     在显示屏上绘制文字
//    参数说明 :    xPos --- 横坐标的位置   [0 .. 30]
//             yPos --- 纵坐标的位置   [0 .. 64]
//             ...   可以同数字一起显示,需设置标志 (%d %l %x %s)
///////////////////////////////////////////////////////////////////////////////
extern   void DrawText ( BYTE xPos, BYTE yPos, LPBYTE lpStr, ... )
{
     BYTE    lpData[100];  // 缓冲区
     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] != '%' )    // 不是格式符开始
            {
                   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;
                   // 长整型
                   case       'l':
                   case       'L':
                          dwTemp = va_arg ( lpParam, long );
                          byIndex += IntToStr ( lpData+byIndex, (DWORD)dwTemp );
                          i++;
                          break;
                   //16 进制(长整型)
                   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
}
在这个函数中,需通过对传入的格式字符串(首地址为 lpStr )进行识别来获知可变参数个数及各个可变参数的类型,具体实现体现在 for 循环中。譬如,在识别为 %d 后,做的是 va_arg ( lpParam, int ) ,而获知为 %l %x 后则进行的是 va_arg ( lpParam, long ) 。格式字符串识别完成后,可变参数也就处理完了。
在项目的最初,我们一直苦于不能找到一个好的办法来混合输出字符串和数字,我们采用了分别显示数字和字符串的方法,并分别指定坐标,程序条理被破坏。而且,在混合显示的时候,要给各类数据分别人工计算坐标,我们感觉头疼不已。以前的函数为:
// 显示字符串
showString ( BYTE xPos, BYTE yPos, LPBYTE lpStr )
// 显示数字
showNum ( BYTE xPos, BYTE yPos, int num )
// 16 进制方式显示数字
showHexNum ( BYTE xPos, BYTE yPos, int num )
最终,我们用 DrawText ( BYTE xPos, BYTE yPos, LPBYTE lpStr, ... ) 函数代替了原先所有的输出函数,程序得到了简化。就这样,兄弟们用得爽翻了。
 3. 运行机制探索
通过第 2 节我们学会了可变参数表的使用方法,相信喜欢抛根问底的读者还不甘心,必然想知道如下问题:
1 )为什么按照第 2 节的做法就可以获得可变参数并对其进行操作?
2 C/C++ 在底层究竟是依靠什么来对这一语法进行支持的,为什么其它语言就不能提供可变参数表呢?
我们带着这些疑问来一步步进行摸索。
3.1  调用机制反汇编
反汇编是研究语法深层特性的终极良策,先来看看 2.2 节例子中主函数进行 max ( 5, 5, 6 ,3 ,8 ,5) 调用时的反汇编:
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++ 函数调用的过程中:
第一步:将参数从右向左入栈(第 1 6 行);
第二步:调用 call 指令进行跳转(第 7 行)。
这两步包含了深刻的含义,它说明 C/C++ 默认的调用方式为由调用者管理参数入栈的操作,且入栈的顺序为从右至左,这种调用方式称为 _cdecl 调用。 x86 系统的入栈方向为从高地址到低地址,故第 1 n 个参数被放在了地址递增的堆栈内。在被调用函数内部,读取这些堆栈的内容就可获得各个参数的值,让我们反汇编到 max 函数的内部:
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 系统中最小的整数  */
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
分析上述反汇编代码,对于一个真正的程序员而言,将是一种很大的享受;而对于初学者,也将使其受益良多。所以请一定要赖着头皮认真研究,千万不要被吓倒!
1 10 进行执行函数内代码的准备工作,保存现场。第 2 行对堆栈进行移动;第 3 行则意味着 max 函数为其内部局部变量准备的堆栈空间为 50h 字节;第 11 行表示把变量 n 的内存空间安排在了函数内部局部栈底减 8 的位置(占用 4 个字节)。
12 13 行非常关键,对应着 va_start ( ap, num ) ,这两行将第一个可变参数的地址赋值给了指针 ap 。另外,从第 12 行可以看出 num 的地址为 ebp+0Ch ;从第 13 行可以看出 ap 被分配在函数内部局部栈底减 4 的位置上(占用 4 个字节)。
   22 27 行最为关键,对应着 va_arg (ap, int) 。其中, 22~24 行的作用为将 ap 指向下一可变参数(可变参数的地址间隔为 4 个字节,从 add eax,4 可以看出); 25~27 行则取当前可变参数的值赋给变量 t 。这段反汇编很奇怪,它先移动可变参数指针,再在赋值指令里面回过头来取先前的参数值赋给 t (从 mov edx,dword ptr [ecx-4] 语句可以看出)。 Visual C++ 同学玩得有意思,不知道碰见同样的情况 Visual Basic 等其它同学怎么玩?
36 41 行恢复现场和堆栈地址,执行函数返回操作。
痛苦的反汇编之旅差不多结束了,看了这段反汇编我们总算弄明白了可变参数的存放位置以及它们被读取的方式,顿觉全省轻松!
3.2 特殊的调用约定
除此之外,我们需要了解 C/C++ 函数调用对参数占用空间的一些特殊约定,因为在 _cdecl 调用协议中,有些变量类型是按照其它变量的尺寸入栈的。
例如,字符型变量将被自动扩展为一个字的空间,因为入栈操作针对的是一个字。
参数 n 实际占用的空间为 ( ( sizeof(n) + sizeof(int) – 1 ) & ~( sizeof(int) – 1 ) ) ,这就是第 2.1 _INTSIZEOF(v) 宏的来历!
既然如此, 2.1 节给出的 va_arg ( list, mode ) 宏为什么玩这么大的飞机就很清楚了。这个问题就留个读者您来分析。




 本文转自 21cnbao 51CTO博客,原文链接: http://blog.51cto.com/21cnbao/120781 ,如需转载请自行联系原作者



相关文章
|
8天前
|
算法 编译器 C++
【C++入门到精通】新的类功能 | 可变参数模板 C++11 [ C++入门 ]
【C++入门到精通】新的类功能 | 可变参数模板 C++11 [ C++入门 ]
24 1
|
8天前
|
Linux 程序员 图形学
C++语言在现代软件开发中的应用与实践
C++语言在现代软件开发中的应用与实践
21 2
|
8天前
|
存储 程序员 C语言
深入理解C++:从语言特性到实践应用
深入理解C++:从语言特性到实践应用
27 3
|
8天前
|
存储 算法 安全
C++语言深度探索:从基础到实践
C++语言深度探索:从基础到实践
15 2
|
8天前
|
机器学习/深度学习 人工智能 大数据
开发语言漫谈-C++
C++最初的名字为“带类的C”
|
8天前
|
缓存 编译器 API
NumPy与其他语言(如C/C++)的接口实践
【4月更文挑战第17天】本文介绍了NumPy与C/C++的接口实践,包括Python与C/C++交互基础、NumPy的C API和Cython的使用。通过案例展示了如何将C++函数与NumPy数组结合,强调了内存管理、类型匹配、错误处理和性能优化的最佳实践。掌握这些技能对于跨语言交互和集成至关重要。
|
8天前
|
存储 C++
C++语言学习指针和引用应用案例
C++中的指针和引用用于高效操作内存。示例展示指针和引用的基本用法:指针`*p`存储变量`a`的地址,引用`&x`在函数调用中实现值交换而无需复制。此外,引用`update(&x)`可直接修改原变量,指针`p`在数组操作中用于遍历和访问不同部分。
13 2
|
8天前
|
C++
C++语言学习数组和字符串应用案例
【4月更文挑战第8天】该文展示了C++中数组和字符串的应用案例。数组示例定义了一个整数数组并访问、修改其元素,计算了元素之和。字符串示例中,定义了一个字符串并遍历、修改字符,进行了字符串拼接、查找子字符串及替换操作。
12 3
|
1天前
|
设计模式 安全 Java
【C++】特殊类设计
【C++】特殊类设计
|
2天前
|
编译器 C++
【C++】类和对象(下)
【C++】类和对象(下)