关于va和字节对齐

这是一个使用可变函数参数的示例程序,如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//#include <Stdarg.h>


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 )

void va_start (char * var_arg, int n_values)
{
int i = (sizeof(n_values) + sizeof(int) - 1) & ~(sizeof(int) - 1);
var_arg = (char*)&n_values + i;
}

template <class T>
T va_arg(char* var_arg, T arg_kind)
{
int i = (sizeof(T) + sizeof(int) - 1) & ~(sizeof(int) - 1);
return *(T *)((var_arg += i) - i);
}

void va_end(char* var_arg)
{
var_arg = (char*)0;
}


float
average( int n_values, ... )
{
char * var_arg;
int count;
float sum = 0;
int int_kind;

/*
** Prepare to access the variable arguments.
*/
va_start ( var_arg, n_values );

/*
** Add the values from the variable argument list.
*/
for( count = 0; count < n_values; count += 1 )
{
sum += va_arg( var_arg, int_kind );
}

/*
** Done processing variable arguments.
*/
va_end( var_arg );

return sum / n_values;
}

void main( void )
{
    float a = average(2, 1, 3);
    printf("a=%f/n", a);
    return ;
}

我们知道使用可变参数的函数需要包含Stdarg.h文件,在X86平台下主要是使用了Stdarg.h文件的如下部分(将它们直接拷贝到程序中可以省略对Stdarg.h文件的包含):
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 )
为了理解这些宏定义,我在示例程序中将它们注释掉,并且添加了三个函数(这样可以进行跟踪调试,宏定义是不行的):
void va_start (char * var_arg, int n_values);
T va_arg(char* var_arg, T arg_kind);
void va_end(char* var_arg);

这个示例程序能够编译通过,但是运行时发生错误。

那位帮我看看到底是什么原因?或者帮我解释一下那些宏定义,谢谢了!

可变参哦,呵呵!
va_start 指定第一个参数的位置
va_arg获得参数的值(需要指定类型)
va_end结束可变参数的获取

可变参数
像printf 输出函数是可变参函数一样,在函数实现里面会用到
va_start 指定第一个参数的位置
va_arg获得参数的值(需要指定类型)
va_end结束可变参数的获取

va_start 指定第一个参数的位置
va_arg获得参数的值(需要指定类型)
va_end结束可变参数的获取

谢谢楼上的,这些我都知道。我想问的是:
(  (sizeof(n)  +  sizeof(int)  -  1)  &  ~(sizeof(int)  -  1)  )
比如这句话是一个什么意思?这样当出现类似的问题而又没有现成的解决方法(前人没有做过类似的工作)的时候,自己可以找到解决的办法。同时通过解析c的源码,达到向大师们学习的目的。

C/C++ code
    
    
    
    

((
sizeof (n) + sizeof ( int ) - 1 ) & ~ ( sizeof ( int ) - 1 ))

字节对齐 ,这里是应该是32位对齐(32bit)

楼上,请问如何看出是32位对齐?
在Stdarg.h中的注释说:
要注意char,short隐式上升为int型;float隐式上升为double型。

((sizeof(n)  +  sizeof(int) -  1) & ~(sizeof(int)-1))
在本例程中表示32位对齐,但也可以支持64位对齐。

我想知道这句话为什么实现了数据对齐的功能(看不懂哇,想将宏转换为函数调试一下,还发生了异常),谢谢:-)

guanzhu xuexi

你自己随便找个数字,然后拿笔计算一下就知道了,这种对齐方法在一些原代码中常见。

呵呵,等我给楼主一个详细的解释吧。顺便给我的变参笔记一个完美的结尾:-)

所谓变参,是被调函数直接操作栈空间的一种应用。

原本的操作宏,不改变栈空间。
你定义的函数,修改了栈空间。

这就是你改的“函数”方式的变参操作不能用的原因。

C/C++ code
    
    
    
    
#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 )


仔细分析这段定义,你会发现:

⒈ 对 _INTSIZEOF(n) :
  在 32 位系统(32位编译器)上,sizeof(int) 尺寸为 4 ,该宏计算出 4 字节对齐 后存储 n 的数据所需字节数。
  在其他系统(相应的编译器)上类似。你可以自己分析。

⒉ 对 va_start (ap,v) :
  考虑 v 是第一个函数参数。ap 得到的是(对齐后) v 后面的相邻参数的地址。
  实际上也就是将“第一个可变的参数”地址存进 ap 。

⒊ 对 va_arg(ap,t) :
  先 += 对齐后 t 类型数据所需字节数,再 - 该字节数,再转换成指针取值。
  换句话说,返回 *(t*)ap ,再修改 ap ,增量为 t 类型的数据尺寸。
  这时, ap 就是“下一个可变参数”地址。

⒋ va_end 类似。你可以自己分析。

对于函数方式的实现。
你可以考虑将参数传递改成引用的方式。
在此基础上,再把变参逻辑实现进去。
goodluck

字节对齐 的学习笔记
一、问题的提出
    两年之前我写过一篇可变参数学习笔记,里面曾经简单的解释过一句:
    代码
    ((sizeof(n)+sizeof(int)-1)&~(sizeof(int)-1))
    的作用是在考虑字节对齐 的因素下计算第一个可变参数的起始地址。
    当时限于时间和水平,未能做更详细的解释。
    今天(2007-11-26)在csdn论坛上看到了一个帖子
    http://topic.csdn.net/u/20071123/16/c8d17d3f-9f49-49af-a6d8-1d7a7d84dc1c.html?seed=303711257
问题:CRT源码分析中一个关于可变函数参数的问题 
提问者:Sun_Moon_Stars
里面又问到了这个宏,于是决定抽出半天时间,把这个问题详细的说清楚。也算是把我的那篇文章
做一个完美的结尾。
 

二、引子
  先看一个日常生活中的问题,
问题1:假设有要把一批货物放到集装箱里,货物有12件,
一个箱子最多能装6件货物,求箱子的数目。
解答:显然我们需要12/6=2个箱子,并且每个箱子都是满的。这个连小学生都会算:-)

问题2:  把问题1的条件改一下,假设一个箱子最多能装5件货物,那么现在的箱子数是多少?
解答:  12/5=2.4个,但是根据实际情况,箱子的个数必须为整数,(有不知道这个常识的就不要再往下看了,
回小学重读吧,呵呵)自然我们就要取3,
  下面把问题一般化

三、一般数学模型
问题3:设一个箱子最多可以装M件货物,且现有N件货物,
则至少需要多少个箱子,给出一般的计算公式。
这里要注意两点
1、箱子的总数必须为整数
2、N不一定大于M,很显然,即使N <M,也得需要一只箱子     

四、通项公式
1、预备知识
在讨论之问题3的解答之前,我们先明确一下/运算符的含义。
定义/运算为取整运算,即
对任意两个整数N,M,必然有且只有唯一的整数X,满足
X*M <= N < (X+1)*M,那么记N/M=X。
这个也正是c里/运算的确切含义。x的存在性和唯一性的严格证明可以见数论教材。
以后如无额外说明,/运算的含义均和本处一致。

/运算有一个基本的性质
若N=MX+Y,则N/M=X+Y/M,证明略

注意:N不是可以随便拆的,设N=A+B,那么一般情况下N/M 不一定等于 A/M+B/M,
A和B至少有一个是M的倍数才行。


2、分步讨论
根据上面的/运算符的定义,我们可以得到问题三的解答,分情况讨论一下
已知N/M=X,那么当
(1)、当N正好是M的倍数时即N=M*X时,那么箱子数就是X=N/M
(2)、如果N不是M的倍数,即N=M*X+Y(1 <=Y <M)时
  那么显然还要多一个箱子来装余下的Y件货物,
  则箱子总数为X+1 = N/M+1

3、一般公式
上面的解答虽然完整,但是用起来并不方便,因为每次都要去判断N和M的倍数关系,
我们自然就要想一个统一的公式,于是,下面的公式出现了
  箱子数目为  (N+M-1)/M

这个式子用具体数字去验证是很简单的,留给读者去做。
我这里给一个完整的数学推导:
现在已经假定 /运算的结果为取整(或者说取模),即
N/M=X,则XM <=N <(X+1)M
那么,
(1)、当N=MX时,(N+M-1)/M= MX/M+(M-1)/M=X
(2)、当N=MX+Y(1 <=Y <M)时,
   由1 <=Y < M,同时加上M-1,得到M <= Y-1+M <= 2M-1 <2M
    根据 /运算的定义 (Y-1+M) /M = 1

  所以 (N+M-1)/M = (MX+Y+M-1)/M= MX/M+(Y+M-1)/M= X+1
显然 公式 (N+M-1)/M与2中的分步讨论结果一致。
可能有的读者还会问,这个公式是怎么想出来的,怎么就想到了加上那个M-1?
这个问题可以先去看看数论中的余数理论。


五、对齐代码的分析   
有了上面的数学基础,我们再来看看开头所说的对齐代码的含义
  ((sizeof(n)+sizeof(int)-1)&~(sizeof(int)-1))
意义就很明显了   
这里。机器字长度sizeof(int)相当于箱子的容量M,变量的真实字节大小相于
货物总数N,整个代码就是求n所占的机器字数目。

顺便仔细的解释一下
~(sizeof(int)-1))

这里用到了一个位运算的技巧,即若M是2的幂,那么
N/M = N &(~(M-1)),
这个读者可以用具体的数自己验证这个结论。
这里给出数学的解释:
设N,M都是二进制数字,且M为2的幂,即M=power(2,Y);
那么必有
N=M*X+Z(1 < =Y < M)
而注意到这里的N,M,Z都是二进制表示,所以把N的最右边的Y位数字就是余数Z.
剩下的左边数字就是模X.我们的任务就是把左边的模求出来就可以。

注意:
(1)这里最关键的一点就是M必须是2的幂(有人常常理解成2的倍数也可以,那是不对的),
否则上面的结论是不成立的
(2) ~(M-1)更专业的叫法就是掩码(mask)。因为数字和这个掩码进行与运算后,数字的最右边Y位的
数字被置0("掩抹"掉了).即掩码最右边的0有多少位,数字最右边就有多少位被清0。

小结:
1、字节对齐 的数学本质就是数论中的取模运算。在计算机上的含义就是求出一个对象占用的机器字数目。
2、在c中/运算可以用位运算和掩码来实现以加快速度,前提是机器字长度必须为2的幂。

ls是个有爱心的大叔,pfpf

好贴

我将你的程序稍加改动,可以实现变参的要求.
你主要的问题就是为了和标准宏调用方式一致而采用了传值的方式,而这样一来,变量在栈中的存储方式已经不是你想象的那样了.
我下面的程序就是在你的基础之上将值传递改为了地址传递,我试过了,执行起来应该没有什么问题,你可以参考一下,呵呵
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef char *va_list;

float average( int &n_values, ... );
void va_start ( char **var_arg, int &n_values );
template < class T > T va_arg( char **var_arg, T arg_kind );
void va_end( char **var_arg );

int main( void )
{
int nCount = 2;
int nTemp1 = 1;
int nTemp2 = 300;
    float fAverage = average( nCount, nTemp1, nTemp2 );
    printf( "average = %f/n", fAverage );
    return 0;
}

float average( int &n_values, ... )
{
    char *var_arg;
    int count;
    float sum = 0;
    int int_kind;   
   
    // Prepare to access the variable arguments.
    va_start ( &var_arg, n_values ); 
   
    // Add the values from the variable argument list.   
    for( count = 0; count < n_values; count += 1 )
    {
        sum += va_arg( &var_arg, int_kind );
    }   
   
    // Done processing variable arguments.       
    va_end( &var_arg );
   
    return sum / n_values;
}

void va_start ( char **var_arg, int &n_values )
{
    int i = ( sizeof(n_values) + sizeof(int) - 1 ) & ~( sizeof(int) - 1 );
    *var_arg = ( char* )&n_values - i;
}

template < class T >
T va_arg( char **var_arg, T arg_kind )
{
    int i = ( sizeof(T) + sizeof(int) - 1 ) & ~( sizeof(int) - 1 );
    return *( T * )(( *var_arg -= i ) + i );
}

void va_end( char **var_arg )
{
    *var_arg = ( char* )0;

}

对了
template  <  class  T  > 
T  va_arg(  char  **var_arg,  T  arg_kind  ) 

        int  i  =  (  sizeof(T)  +  sizeof(int)  -  1  )  &  ~(  sizeof(int)  -  1  ); 
        return  *(  T  *  )((  *var_arg  -=  i  )  +  i  ); 
}
里面的 *var_arg  -=  i ,需要说明一下:
因为我所用的环境存储变量的方法是,对于先定义的变量存在栈的下部,后定义的放在栈的上部,因为nCount是最后定义的,nTemp1,nTemp2都在它上面,所以用的是-=,而不是你用的+=.

呵呵,还有关于int  i  =  (  sizeof(n_values)  +  sizeof(int)  -  1  )  &  ~(  sizeof(int)  -  1  );  的分析,
看了楼上各位大虾的分析,我也来说一下我自己的看法:

首先,变参函数的标准宏定义是直接对栈进行操作,参考如下代码
double sum(int lim,...)
{
    va_list ap;                  // declare object to hold arguments
    double tot = 0;
    int i;

    va_start (ap, lim);            // initialize ap to argument list
    for (i = 0; i < lim; i++)
      tot += va_arg(ap, double); // access each item in argument list
    va_end(ap);                  // clean up

    return tot;
}
1)首先创建一个参数列表ap,也就是一个字符型指针;
2)初始化参数列表,其中va_start (ap, lim)中的lim不仅指定了该变参函数中参数的个数(次要的),更主要的是将其地址赋给ap,以便其能操作栈中的各个操作数(理想情况下在栈中会以此存储lim,及...所代表的各参数),但问题在于ap指向的是lim,而非我们真正要用到的参数,于是
int  i  =  (  sizeof(n_values)  +  sizeof(int)  -  1  )  &  ~(  sizeof(int)  -  1  ); 就起作用了,它计算出了lim在栈中所占用的字节数n,然后使ap偏移n各位置,就能找到我们所要操作的数了.并不是楼主所说的字节对齐 .
我说的仅仅是个大概的意思,也是今天看了这个帖子之后给我的启发.如果有说得不对的地方还请大家指出来,一起学习. 

经典,mark一下

你可能感兴趣的:(关于va和字节对齐)