<2024年4月>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

统计

  • 随笔 - 13
  • 文章 - 0
  • 评论 - 2
  • 引用 - 0

常用链接

留言簿

随笔分类

随笔档案

搜索

  •  

最新评论

阅读排行榜

评论排行榜

C变参

今天在学习C++primer的时候 有一个章节是写省略号的,突然很感兴趣,可惜书上介绍的资料很少,于是去网上查了些资料,发现原来 这个是来源于C函数的,估计在C primer中有详细说明,我收集的如下,以及相关例子

简单的说

变长参数
变参是用省略号...来表示,声明的。
#include <stdarg.h>
标准头文件,提供va_start、va_arg、va_end

va_start实际上声明一个指针,指向一个参数,然后根据va_arg中的参数类型移动指针,得到每个参数的值。

C语言参数是通过堆栈传送的: printf( "%d%d...", p1, p2, p3..., pN );
push pN
...
push p3
push p2
push p1
push strPTR    压指针
call _printf
pop ax
pop ax
pop ax
...
pop ax         清参数(第N个)

显而易见,转入printf后所有的参数都在堆栈中,多少个不知道,但都在.
只须确定参数个数,但参数最下面的那个是String,里面包含了个数信息(%d,%f...)不就ok了?
即使参数大小各有不同,也无所谓,照样取出.但%d多于实际参数,可能会将其他堆栈数据取出显示(只要不修改,一般影响不大).
/////////////////////////////////////////////////////////////////

我们在C语言编程中会遇到一些参数个数可变的函数,例如printf()
这个函数,它的定义是这样的:
int printf( const char* format, ...);
它除了有一个参数format固定以外,后面跟的参数的个数和类型是
可变的,例如我们可以有以下不同的调用方法:
printf("%d",i);
printf("%s",s);
printf("the number is %d ,string is:%s", i, s);
究竟如何写可变参数的C函数以及这些可变参数的函数编译器是如何实
现的呢?本文就这个问题进行一些探讨,希望能对大家有些帮助.会C++的
网友知道这些问题在C++里不存在,因为C++具有多态性.但C++是C的一个
超集,以下的技术也可以用于C++的程序中.限于本人的水平,文中如果有
不当之处,请大家指正.

(一)写一个简单的可变参数的C函数

下面我们来探讨如何写一个简单的可变参数的C函数.写可变参数的
C函数要在程序中用到以下这些宏:
void va_start( va_list arg_ptr, prev_param );

type va_arg( va_list arg_ptr, type );

void va_end( va_list arg_ptr );
va在这里是variable-argument(可变参数)的意思.
这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个
头文件.下面我们写一个简单的可变参数的函数,改函数至少有一个整数
参数,第二个参数也是整数,是可选的.函数只是打印这两个参数的值.
void simple_va_fun(int i, ...)
{
va_list arg_ptr;
int j=0;

va_start(arg_ptr, i);
j=va_arg(arg_ptr, int);
va_end(arg_ptr);
printf("%d %d\n", i, j);
return;
}
我们可以在我们的头文件中这样声明我们的函数:
extern void simple_va_fun(int i, ...);
我们在程序中可以这样调用:
simple_va_fun(100);
simple_va_fun(100,200);
从这个函数的实现可以看到,我们使用可变参数应该有以下步骤:
1)首先在函数里定义一个va_list型的变量,这里是arg_ptr,这个变
量是指向参数的指针.
2)然后用va_start宏初始化变量arg_ptr,这个宏的第二个参数是第
一个可变参数的前一个参数,是一个固定的参数.
3)然后用va_arg返回可变的参数,并赋值给整数j. va_arg的第二个
参数是你要返回的参数的类型,这里是int型.
4)最后用va_end宏结束可变参数的获取.然后你就可以在函数里使
用第二个参数了.如果函数有多个可变参数的,依次调用va_arg获
取各个参数.
如果我们用下面三种方法调用的话,都是合法的,但结果却不一样:
1)simple_va_fun(100);
结果是:100 -123456789(会变的值)
2)simple_va_fun(100,200);
结果是:100 200
3)simple_va_fun(100,200,300);
结果是:100 200
我们看到第一种调用有错误,第二种调用正确,第三种调用尽管结果
正确,但和我们函数最初的设计有冲突.下面一节我们探讨出现这些结果
的原因和可变参数在编译器中是如何处理的.

(二)可变参数在编译器中的处理

我们知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的,
由于1)硬件平台的不同 2)编译器的不同,所以定义的宏也有所不同,下
面以VC++中stdarg.h里x86平台的宏定义摘录如下('\'号表示折行):

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 )

定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统.C语言的函
数是从右向左压入堆栈的,图(1)是函数的参数在堆栈中的分布位置.我
们看到va_list被定义成char*,有一些平台或操作系统定义为void*.再
看va_start的定义,定义为&v+_INTSIZEOF(v),而&v是固定参数在堆栈的
地址,所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在堆
栈的地址,如图:

高地址
|-----------------------------|
|....... |
|-----------------------------|
|第n个参数(第一个可变参数) |
|----------------------------|<--va_start后ap指向
|第n-1个参数(最后一个固定参数)|
|-----------------------------|<-- &v
|函数返回地址 |
|----------------------------|
低地址

图( 1 )

然后,我们用va_arg()取得类型t的可变参数值,以上例为int型为例,我
们看一下va_arg取int型的返回值:
j= ( *(int*)((ap += _INTSIZEOF(int))-_INTSIZEOF(int)) );
首先ap+=sizeof(int),已经指向下一个参数的地址了.然后返回
ap-sizeof(int)的int*指针,这正是第一个可变参数在堆栈里的地址
(图2).然后用*取得这个地址的内容(参数值)赋给j.

高地址
|-----------------------------|
|....... |
|-----------------------------|<--va_arg后ap指向
|第n个参数(第一个可变参数) |
|-----------------------------|<--va_start后ap指向
|第n-1个参数(最后一个固定参数)|
|-----------------------------|<-- &v
|函数返回地址 |
|----------------------------|
低地址
图( 2 )

最后要说的是va_end宏的意思,x86平台定义为ap=(char*)0;使ap不再
指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不
会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的.
在这里大家要注意一个问题:由于参数的地址用于va_start宏,所
以参数不能声明为寄存器变量或作为函数或数组类型.
关于va_start, va_arg, va_end的描述就是这些了,我们要注意的
是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.

(三)可变参数在编程中要注意的问题

因为va_start, va_arg, va_end等定义成宏,所以它显得很愚蠢,
可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能
地识别不同参数的个数和类型.
有人会问:那么printf中不是实现了智能识别参数吗?那是因为函数
printf是从固定参数format字符串来分析出参数的类型,再调用va_arg
的来获取可变参数的.也就是说,你想实现智能识别可变参数的话是要通
过在自己的程序里作判断来实现的.
另外有一个问题,因为编译器对可变参数的函数的原型检查不够严
格,对编程查错不利.如果simple_va_fun()改为:
void simple_va_fun(int i, ...)
{
va_list arg_ptr;
char *s=NULL;

va_start(arg_ptr, i);
s=va_arg(arg_ptr, char*);
va_end(arg_ptr);
printf("%d %s\n", i, s);
return;
}
可变参数为char*型,当我们忘记用两个参数来调用该函数时,就会出现
core dump(Unix) 或者页面非法的错误(window平台).但也有可能不出
错,但错误却是难以发现,不利于我们写出高质量的程序.
以下提一下va系列宏的兼容性.
System V Unix把va_start定义为只有一个参数的宏:
va_start(va_list arg_ptr);
而ANSI C则定义为:
va_start(va_list arg_ptr, prev_param);
如果我们要用system V的定义,应该用vararg.h头文件中所定义的
宏,ANSI C的宏跟system V的宏是不兼容的,我们一般都用ANSI C,所以
用ANSI C的定义就够了,也便于程序的移植.

例子

void F(int arg_count, ...)
{
 va_list ap;
 va_start(ap, arg_count);
 while (arg_count--)
  cout << va_arg(ap, char);// char 这里需要确定你的参数类型,像 printf 是通过掩码
 va_end(ap);

}

#include <stdio.h>
#include <stdarg.h>
// 不定参数函数例子,大家交流交流。
void sum(char *msg, ...)
{
   int total = 0;
   va_list ap;
   int arg;
   va_start(ap, msg);
   while ((arg = va_arg(ap,int)) != 0) {
      total += arg;
   }
   printf(msg, total);
   va_end(ap);
}

int main(void) {
   sum("The total of 1+2+3+4 is %d\n", 1,2,3,4,0);
   return 0;

 

posted @ 2010-11-05 20:08 @Koven.Z 阅读(415) | 评论 (0)编辑 收藏
成员函数作为线程函数

关于this指针的传递问题总结
1:__cdecl成员函数 通过ECX传递this指针
     mov ecx, 对象的地址
      call 成员函数

2:__stdcall成员函数 通过堆栈传递this指针
     push 对象的地址
      call 成员函数

用非静态成员作为线程函数
原理分析:
1.该线程的主函数为类的非静态成员函数,所以它认为他的调用者会为他传递一个this指针,通过堆栈传递.因为__stdcall的函数
2.而操作系统认为的线程主函数只有一个参数通过堆栈传递.
所以线程的this指针被&b覆盖了,刚好把非静态成员函数作为线程主函数
class CObject
{
public:
        CObject(int a){ m_nData = a;};
        DWORD WINAPI ThreadFuc()
        {
                printf("Thread Run\nThread Data:%d \n", m_nData);
                return 0;
        };
private:
        int m_nData;
};
typedef DWORD ( CObject::*MyThread)(
    LPVOID lpThreadParameter
    );
int main(int argc, char** argv)
{
        MyThread My = (MyThread)&CObject::ThreadFuc;
        CObject b(10);
        HANDLE hThread = ::CreateThread(NULL, NULL, *(LPTHREAD_START_ROUTINE*)&My, &b, NULL, NULL);

        system("pause");
         ::CloseHandle(hThread);
        return 0;
}

以上内容转自VCKBASE,经过测试,可以正常运行。

使用时注意一下几点:

1.如果使用AfxBeginThread,强制转换的类型应该是AFX_THREADPROC

以下是我使用时候的代码:

 typedef UINT ( CDlgMutex_Test::*WOLF_HTREADPROC)(LPVOID);

 WOLF_HTREADPROC myThread1 = (WOLF_HTREADPROC)&CDlgMutex_Test::Thread1;
 WOLF_HTREADPROC myThread2 = (WOLF_HTREADPROC)&CDlgMutex_Test::Thread2;

 AfxBeginThread(*(AFX_THREADPROC*)&myThread1,this);
 AfxBeginThread(*(AFX_THREADPROC*)&myThread2,this);


本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/haoxing168/archive/2009/08/16/4452360.aspx

 

 


 

附:其他一个.CPP

#include <windows.h>
#include <conio.h>
#include <stdio.h>


class t
{
public:
    void Run();
    t();

protected:
    char c[256];
    DWORD WINAPI ThreadFunc();
};

typedef DWORD (WINAPI  t::* pThreadFunc)();

t::t()
{
    strcpy(c, "Welcome to use thread proc");
}

DWORD WINAPI t::ThreadFunc()
{
    MessageBox(NULL, c, "xixi", MB_OK);
    return 0;
}

typedef DWORD (WINAPI * PTHREADFUNC)(LPVOID);

void t::Run()
{

    pThreadFunc p = ThreadFunc;
    DWORD dwAddress;
    memcpy(&dwAddress,&p,sizeof(p));

    PTHREADFUNC p1;
    memcpy(&p1, &dwAddress, sizeof(p1));
 
   
    DWORD dwTID;
    HANDLE hf;
    hf = CreateThread(NULL, 0, p1, this, 0, &dwTID);
    if (hf){
        CloseHandle(hf);
    }
}

void main()
{
   
    t t1;
    t1.Run();
    getch();
}

posted @ 2010-11-05 20:05 @Koven.Z 阅读(2035) | 评论 (0)编辑 收藏
函数调用约定和名称修饰

调用约定(Calling Convention)
      调用约定用来处理决定函数参数传送时入栈和出栈的顺序(由调用者还是被调用者把参数弹出栈),以及编译器用来识别函数名称的名称修饰约定等问题。在Microsoft VC++ 6.0中定义了下面几种调用约定,我们将结合汇编语言来一一分析它们:

1、__cdecl
      __cdecl是C/C++和MFC程序默认使用的调用约定,也可以在函数声明时加上__cdecl关键字来手工指定。采用__cdecl约定时,函数参数按照从右到左的顺序入栈,并且由调用函数者把参数弹出栈以清理堆栈。因此,实现可变参数的函数只能使用该调用约定。由于每一个使用__cdecl约定的函数都要包含清理堆栈的代码,所以产生的可执行文件大小会比较大。__cdecl可以写成_cdecl。
      C调用约定(即用__cdecl关键字说明)按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数的函数只能使用该调用约定)。另外,在函数名修饰约定方面也有所不同。
      _cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。函数采用从右到左的压栈方式。VC将函数编译后会在函数名前面加上下划线前缀。是MFC缺省调用约定。
      由于参数按照从右向左顺序压栈,因此最开始的参数在最接近栈顶的位置,因此当采用不定个数参数时,第一个参数在栈中的位置肯定能知道,只要不定的参数个数能够根据第一个后者后续的明确的参数确定下来,就可以使用不定参数,例如对于CRT中的sprintf函数,定义为:

int sprintf(char* buffer,const char* format,...)

由于所有的不定参数都可以通过format确定,因此使用不定个数的参数是没有问题的。

2、__stdcall

        __stdcall调用约定用于调用Win32 API函数。采用__stdcal约定时,函数参数按照从右到左的顺序入栈,被调用的函数在返回前清理传送参数的栈,函数参数个数固定。由于函数体本身知道传进来的参数个数,因此被调用的函数可以在返回前用一条ret n指令直接清理传递参数的堆栈。__stdcall可以写成_stdcall。
        __stdcall调用约定相当于16位动态库中经常使用的PASCAL调用约定。在32位的VC++5.0中PASCAL调用约定不再被支持(实际上它已被定义为__stdcall。除了__pascal外,__fortran和__syscall也不被支持),取而代之的是__stdcall调用约定。两者实质上是一致的,即函数的参数自右向左通过栈传递,被调用的函数在返回前清理传送参数的内存栈,但不同的是函数名的修饰部分(关于函数名的修饰部分在后面将详细说明)。
        _stdcall是Pascal程序的缺省调用方式,通常用于Win32 Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。VC将函数编译后会在函数名前面加上下划线前缀,在函数名后加上"@"和参数的字节数。

3、__fastcall

        __fastcall约定用于对性能要求非常高的场合。__fastcall约定将函数的从左边开始的两个大小不大于4个字节(DWORD)的参数分别放在ECX和EDX寄存器,其余的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的堆栈。__fastcall可以写成_fastcall。
        __fastcall调用约定是"人"如其名,它的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面,它和前两者均不同。
        _fastcall方式的函数采用寄存器传递参数,VC将函数编译后会在函数名前面加上"@"前缀,在函数名后加上"@"和参数的字节数。

4、thiscall

      thiscall调用约定是C++中的非静态类成员函数的默认调用约定。thiscall只能被编译器使用,没有相应的关键字,因此不能被程序员指定。采用thiscall约定时,函数参数按照从右到左的顺序入栈,被调用的函数在返回前清理传送参数的栈,只是另外通过ECX寄存器传送一个额外的参数:this指针。
 

5、naked属性

      采用上面所述的四种调用约定的函数在进入函数时,编译器会产生代码来保存ESI、EDI、EBX、EBP寄存器中的值,退出函数时则产生代码恢复这些寄存器的内容。对于定义了naked属性的函数,编译器不会自动产生这样的代码,需要你手工使用内嵌汇编来控制函数实现中的堆栈管理。由于naked属性并不是类型修饰符,故必须和__declspec共同使用。下面的这段代码定义了一个使用了naked属性的函数及其实现:

      naked属性与本节关系不大,具体请参考MSDN。

 

6、WINAPI

      还有一个值得一提的是WINAPI宏,它可以被翻译成适当的调用约定以供函数使用。该宏定义于windef.h之中。下面是在windef.h中的部分内容:

#define CDECL      _cdecl
#define WINAPI     CDECL
#define CALLBACK   __stdcall
#define WINAPI     __stdcall
#define APIENTRY   WINAPI

由此可见,WINAPI、CALLBACK、APIENTRY等宏的作用。

2)名字修饰约定
1、修饰名(Decoration name)
     "C"或者"C++"函数在内部(编译和链接)通过修饰名识别。修饰名是编译器在编译函数定义或者原型时生成的字符串。有些情况下使用函数的修饰名是必要的,如在模块定义文件里头指定输出"C++"重载函数、构造函数、析构函数,又如在汇编代码里调用"C""或"C++"函数等。
     修饰名由函数名、类名、调用约定、返回类型、参数等共同决定。
2、名字修饰约定随调用约定和编译种类(C或C++)的不同而变化。函数名修饰约定随编译种类和调用约定的不同而不同,下面分别说明。
a、C编译时函数名修饰约定规则:
       __stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个"@"符号和其参数的字节数,格式为_functionname@number。
      __cdecl调用约定仅在输出函数名前加上一个下划线前缀,格式为_functionname。
      __fastcall调用约定在输出函数名前加上一个"@"符号,后面也是一个"@"符号和其参数的字节数,格式为    @functionname@number。
     它们均不改变输出函数名中的字符大小写,这和PASCAL调用约定不同,PASCAL约定输出的函数名无任何修饰且全部大写。
b、C++编译时函数名修饰约定规则:
__stdcall调用约定:
1、以"?"标识函数名的开始,后跟函数名;
2、函数名后面以"@@YG"标识参数表的开始,后跟参数表;
3、参数表以代号表示:
X--void ,
D--char,
E--unsigned char,
F--short,
H--int,
I--unsigned int,
J--long,
K--unsigned long,
M--float,
N--double,
_N--bool,
....
PA--表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以"0"代替,一个"0"代表一次重复;
4、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;
5、参数表后以"@Z"标识整个名字的结束,如果该函数无参数,则以"Z"标识结束。
其格式为"?functionname@@YG*****@Z"或"?functionname@@YG*XZ",例如
          int Test1(char *var1,unsigned long)-----"?Test1@@YGHPADK@Z"
          void Test2()                       -----"?Test2@@YGXXZ"

      __cdecl调用约定:
      规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的"@@YG"变为"@@YA"。
      __fastcall调用约定:
      规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的"@@YG"变为"@@YI"。
      VC++对函数的省缺声明是"__cedcl",将只能被C/C++调用.

      关键字__cdecl、__stdcall和__fastcall可以直接加在要输出的函数前,也可以在编译环境的Setting...->C/C++->Code Generation项选择。它们对应的命令行参数分别为/Gd、/Gz和/Gr。缺省状态为/Gd,即__cdecl。当加在输出函数前的关键字与编译环境中的选择不同时,直接加在输出函数前的关键字有效。

      因此,为了使其它语言编写的模块(如Visual Basic应用程序、Pascal或Fortran的应用程序等)可以 调用C/C++编写的DLL的函数,必须使用正确的调用约定来导出函数,并且不要让编译器对要导出的函数进行任何名称修饰。

      所谓对齐,对Intel80x86机器来说就是要求每个变量的地址都是sizeof(int)的倍数。

      要完全模仿PASCAL调用约定首先必须使用__stdcall调用约定,至于函数名修饰约定,可以通过其它方法模仿。还有一个值得一提的是WINAPI宏,Windows.h支持该宏,它可以将出函数翻译成适当的调用约定,在WIN32中,它被定义为__stdcall。使用WINAPI宏可以创建自己的APIs。

函数调用约定导致的常见问题
如果定义的约定和使用的约定不一致,则将导致堆栈被破坏,导致严重问题,下面是两种常见的问题:

函数原型声明和函数体定义不一致
DLL导入函数时声明了不同的函数约定
以后者为例,假设我们在dll种声明了一种函数为:

__declspec(dllexport) int func(int a,int b);//注意,这里没有stdcall,使用的是cdecl
使用时代码为:

      typedef int (*WINAPI DLLFUNC)func(int a,int b);
      hLib = LoadLibrary(...);
      DLLFUNC func = (DLLFUNC)GetProcAddress(...)//这里修改了调用约定
      result = func(1,2);//导致错误

由于调用者没有理解WINAPI的含义错误的增加了这个修饰,上述代码必然导致堆栈被破坏,MFC在编译时插入的checkesp函数将告诉你,堆栈被破坏了。


相关链接:
http://dev.csdn.net/article/29/article/33/article/31/31511.shtm
http://www.ddvip.net/program/vc/index1/47.htm
http://www.donews.net/n9871009/archive/2004/11/14/169122.aspx
http://blog.vckbase.com/arong/archive/2004/06/09/409.aspx



 

个人感觉要注意thiscall 和_stdcall 的区别。成员函数默认为thiscall,但可强制转换为 _stdcall(WINAPI),同时采取相应的措施,可实现static和成员函数一定程度上的转换。

posted @ 2010-11-05 20:03 @Koven.Z 阅读(612) | 评论 (0)编辑 收藏
仅列出标题
共2页: 1 2