您的位置:首页 > 运维架构 > Linux

多学多得呀,关于函数的可变参数问题

2011-12-30 14:40 323 查看
              C语言中的可变参数是用va_list等几个宏来实现的。其原理就是获取参数进栈的地址,然后分析出各个参数。

这是我查看别人的写东西,下面代码是从别人文章里得到的,在理解下面代码之前,我们先浏览一下下面代码是如何confuse 我们。

VC中IX86平台的:

#ifndef _VA_LIST_DEFINED
#ifdef  _M_ALPHA
typedef struct {

        char *a0;       /* pointer to first homed integer argument */

        int offset;     /* byte offset of next parameter */

} va_list;

#else

typedef char *  va_list;

#endif

#define _VA_LIST_DEFINED

#endif

#ifdef  _M_IX86

#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )//靠,这里的运算就是为了得到地址

 

/**********************************************************************
~是位取反的意思。
_INTSIZEOF(n)整个做的事情就是将n的长度化为int长度的整数倍。
比如n为5,二进制就是101b,int长度为4,二进制为100b,那么n化为int长度的整数倍就应该为8。
~(sizeof(int) – 1) )就应该为~(4-1)=~(00000011b)=11111100b,这样任何数& ~(sizeof(int) – 1) )后最后两位肯定为0,就肯定是4的整数倍了。
(sizeof(n) + sizeof(int) – 1)就是将大于4m但小于等于4(m+1)的数提高到大于等于4(m+1)但小于4(m+2),这样再& ~(sizeof(int) – 1) )后就正好将原长度补齐到4的倍数了。

**********************************************************************/

//下面几个宏参数也没有注释

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

#elif   defined(_M_MRX000)

……

 

NND

下面是TC2.0下的这几个宏的实现:
#if	!defined(__STDARG)
 
#define __STDARG
typedef void	*va_list;

#define va_start(ap, parmN)	(ap = ...) //TC写法更加变态
 
#define va_arg(ap, type)	(*((type *)(ap))++)
 
#define va_end(ap)
 
#define _va_ptr			(...)
#endif
 
/********靠,linux的也不容易理解呀**********/
下面是linux下对这几个宏的实现。很简单,整个文件也就下面这些句:
#ifndef __STDARG_H11
#define __STDARG_H
#ifdef sparc
#  define _VA_ALIST_            "__builtin_va_alist"
   typedef char *va_list;
#  define va_start(ap, p)       (ap = (char *) &__builtin_va_alist)
#  define va_arg(ap, type)      ((type *) __builtin_va_arg_incr((type *) ap))[0]
#  define va_end(ap)
#else /* vax, mc68k, 80*86 */
   typedef char *va_list;
#  define va_start(ap, p)       (ap = (char *) (&(p)+1))
#  define va_arg(ap, type)      ((type *) (ap += sizeof(type)))[-1]
#  define va_end(ap)
#endif
#endif /* __STDARG_H */
#if __FIRST_ARG_IN_AX__
#error First arg is in a register, stdarg.h cannot take its address
#endif

只区分了sparc平台和其他平台。sparc平台的那个内建函数还没看到源代码,但是看其他平台的代码,没有对齐的考虑。linux下的其他平台为什么没有对齐机制呢?这样的宏定义能很好的工作吗?当出现需要对齐的地方怎么办?

因为测试下面的程序的时候,在我用的linux系统上,我用#include <stdarg.h>;的方法,用gcc的-E参数预编译了后,代码是使用"__builtin_va_alist"的。所以我暂时认定所使用的这个linux是装在sparc平台上的。(后经证实是错误的)

为此,就下面的程序在sparc和x86两平台(32位)上做了测试:
#include <stdio.h>;#include <stdlib.h>;
 
#include <stdarg.h>;void tva(int i, ...);
 
int main(void) {      
 
  int     i,int1,int2;
 
        char    ch;
 
        i = 5;
        int1 = 11;
        int2 = 22;
        ch = 'a';
        tva(i, int1, ch, int2);
        exit(0);
}
void tva(int i, ...) {
        int     int1;
        char    ch;
        int     int2;
        va_list ap;
 
        va_start(ap, i);
        int1 = va_arg(ap, int);
        printf("%d\n", int1);
        ch = va_arg(ap, char);	// 问题部分。
        printf("%c\n", ch);
        int2 = va_arg(ap, int);
        printf("%d\n", int2);
        va_end(ap);
        return;
}

在sparc平台上的linux上,用gcc编译的时候会有warning:
test.c: In function `tva':
 
test.c:35: warning: `char' is promoted to `int' when passed through `...'
test.c:35: warning: (so you should pass `int' not `char' to `va_arg')

运行的时候有段错误,显示:
11
Segmentation fault

只有把问题部分的改成ch = va_arg(ap, int),才完全没有问题。这相当与人为的用参数控制内存对齐了。

另外,在这个linux下,直接使用非sparc平台的那种定义(自己手动定义),则出现了如下面win下模拟那样的内存对齐问题!莫非这就是一台x86的平台,只不过是gcc中的宏定义的有问题了?这个问题先放一边,看来不行的话,得联系一下服务器的管理员了:-(

在x86平台的win上,用VC中的CL编译器,以上的写法完全没有问题,也可改成int,丝毫不受影响。

在x86平台的linux上,没有环境测试。不过模拟了一个(^_^,群众的智慧是无敌的!)。在x86平台的主机上,在win下,用VC中的CL编译器,直接使用linux下的那种宏定义方式(自己定义的)。哈哈,果然有内存对齐的问题!
结果如下:
11
a

369098752



因为cl是32位的编译器,所以默认的是4内存对齐。
用16进制表示x86的内存:
-------------------------------------------------------
| 11             a                  22                |
| 0B 00 00 00    61 00 00 00    16 00 00 00           |
| ^              ^    ^             ^                 |
| 1              2                  4                 |
----------------------------------
9c9c
---------------------

如上图,当考虑内存对齐的时候,取第三个参数的时候,指针的位置是在4的地方,而如果不考虑内存对齐,则是在3的地方。如果在3的地方,则第三个参数就取成0x16000000即是369098752了。

x86平台,win下TC2.0中的结果是:
11
a

5632



这是因为TC中把CPU看成16位的,其中int是2个字节,所以是2内存对齐的。

需要注意的是,本文中所说的内存对齐是指栈内存对齐,这和结构体成员的内存对齐是不同的。

 

/***************************************************************************************************************

*************************************我们先给自己充充电,知识补漏哈**************************************

 

函数参数的传递原理

函数参数是以数据结构:栈的形式存取,从右至左入栈.eg:

先介绍一下可变参数表的调用形式以及原理:

首先是参数的内存存放格式:参数存放在内存的堆栈段中,在执行函数的时候,从最后一个开始入栈。因此栈底高地址,栈顶低地址,举个例子如下:

void func(int x, float y, char z);

那么,调用函数的时候,实参 char z 先进栈,然后是 float y,最后是 int x,因此在内存中变量的存放次序是 x->y->z,因此,从理论上说,我们只要探测到任意一个变量的地址,并且知道其他变量的类型,通过指针移位运算,则总可以顺藤摸瓜找到其他的输入变量。

 

***************************************************************************************************************/

以下是更详细的解释:

下面是 <stdarg.h> 里面重要的几个宏定义如下:

typedef char* va_list;

void va_start ( va_list ap, prev_param ); /* ANSI version */

type va_arg ( va_list ap, type );

void va_end ( va_list ap );

va_list 是一个字符指针,可以理解为指向当前参数的一个指针,取参必须通过这个指针进行。

<Step 1> 在调用参数表之前,定义一个 va_list 类型的变量,(假设va_list 类型变量被定义为ap);

<Step 2> 然后应该对ap 进行初始化,让它指向可变参数表里面的第一个参数,这是通过 va_start 来实现的,第一个参数是 ap 本身,第二个参数是在变参表前面紧挨着的一个变量,即“...”之前的那个参数;

<Step 3> 然后是获取参数,调用va_arg,它的第一个参数是ap,第二个参数是要获取的参数的指定类型,然后返回这个指定类型的值,并且把 ap 的位置指向变参表的下一个变量位置;

<Step 4> 获取所有的参数之后,我们有必要将这个 ap 指针关掉,以免发生危险,方法是调用 va_end,他是输入的参数 ap 置为 NULL,应该养成获取完参数表之后关闭指针的习惯。

例如 int max(int n, ...); 其函数内部应该如此实现:

int max(int n, ...) {                // 定参 n 表示后面变参数量,定界用,输入时切勿搞错

 va_list ap;                         // 定义一个 va_list 指针来访问参数表

     va_start(ap, n);                       // 初始化 ap,让它指向第一个变参,n之后的参数

    int maximum = -0x7FFFFFFF;          // 这是一个最小的整数                //经用计算转换十进制为 -2147483647                     7F = 127   = 111,1111    留一位表示符号正负

    int temp;

     for(int i = 0; i < n; i++) {

    temp = va_arg(ap, int);          // 获取一个 int 型参数,并且 ap 指向下一个参数

    if(maximum < temp) maximum = temp;

     }

    va_end(ap);                         // 善后工作,关闭 ap

    return max;

}

// 在主函数中测试 max 函数的行为(C++ 格式)

int main() {

   cout << max(3, 10, 20, 30) << endl;

   cout << max(6, 20, 40, 10, 50, 30, 40) << endl;

}

 

看到这里,建议大家回去再看一次。再回到这里相信大家已会写可变参数函数了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息