首页
社区
课程
招聘
[旧帖] [原创][邀请码已发]边玩边谈:函数与指针 0.00雪花
发表于: 2010-8-30 22:35 1857

[旧帖] [原创][邀请码已发]边玩边谈:函数与指针 0.00雪花

2010-8-30 22:35
1857
C/C++中函数与指针是很重要的内容,包括:函数指针、返回指针的函数、指针数组、指向指针的指针等等。以前学习中对它的理解很少。所以自己编程实验了一下,从而有了新的体会和认识。因为是新手,所有难免有些错误和不好的地方,还请大家批评指正。玩的过程中发现:有时候真不好玩!会把程序玩崩溃的!人脑会超频到几乎死机的~第一次发帖,有点紧张~嘿嘿~

<1>众所周知,C/C++对应的汇编中函数的返回值是通过eax来传递的。先看一个简单的程序。

//----------------------------------------------------------

#include<iostream>

using namespace std;

int max(int x,int y)
{
        return x>y?x:y;
}

int main()
{
        int t=max(7,9);
       
        cout<<t<<endl;
       
        return 0;
}

//----------------------------------------------------------

Debug版本max()函数的Ollydbg反汇编代码:

00401560 >/> \55            push ebp

00401561  |.  8BEC          mov ebp,esp

00401563  |.  83EC 44       sub esp,44

00401566  |.  53            push ebx

00401567  |.  56            push esi

00401568  |.  57            push edi

00401569  |.  8D7D BC       lea edi,dword ptr ss:[ebp-44]

0040156C  |.  B9 11000000   mov ecx,11

00401571  |.  B8 CCCCCCCC   mov eax,CCCCCCCC

00401576  |.  F3:AB         rep stos dword ptr es:[edi]

00401578  |.  8B45 08       mov eax,dword ptr ss:[ebp+8]             ;  以下为关键代码

0040157B  |.  3B45 0C       cmp eax,dword ptr ss:[ebp+C]

0040157E  |.  7E 08         jle short pointer.00401588

00401580  |.  8B4D 08       mov ecx,dword ptr ss:[ebp+8]

00401583  |.  894D FC       mov dword ptr ss:[ebp-4],ecx

00401586  |.  EB 06         jmp short pointer.0040158E

00401588  |>  8B55 0C       mov edx,dword ptr ss:[ebp+C]

0040158B  |.  8955 FC       mov dword ptr ss:[ebp-4],edx

0040158E  |>  8B45 FC       mov eax,dword ptr ss:[ebp-4]             ;  到这里为止

00401591  |.  5F            pop edi

00401592  |.  5E            pop esi

00401593  |.  5B            pop ebx

00401594  |.  8BE5          mov esp,ebp

00401596  |.  5D            pop ebp

00401597  \.  C3            retn                                     ;__cdecl调用规范

//----------------------------------------------------------

Release版本max()函数的Ollydbg反汇编代码:

00401000  /$  8B4424 04     mov eax,dword ptr ss:[esp+4]

00401004  |.  8B4C24 08     mov ecx,dword ptr ss:[esp+8]

00401008  |.  3BC1          cmp eax,ecx

0040100A  |.  7F 02         jg short pointer.0040100E

0040100C  |.  8BC1          mov eax,ecx

0040100E  \>  C3            retn

从上面的例子中,我们可以看到,Release版本对max()函数进行了优化,更加有效地利用了寄存器和减少了一些调试方面的编译设置。而且我们也可以验证书上说的,形如上述max()函数的形参到实参值传递方式是通过值拷贝实现的。值拷贝到底是什么东东?留待<3>中讨论。但是还引申出一个问题,你怎么知道函数的地址呢?我确实不知道,不过我敢确信编译器知道。猜测如下:对于自己定义的函数,编译器都能显式(这里说的显式是指不经地址映射机制而直接寻址)给出函数的地址。但是,对于API函数呢?这我确实不知道。在<2>中我们再编程讨论。

<2>用函数指针变量调用函数

//----------------------------------------------------------

#include<iostream>

using namespace std;

int max(int x,int y)
{
        return x>y?x:y;
}

int main()
{
        int (*p)(int,int);                        //function pointer declaration
       
        p=max;                                                //point to max
       
        int t=p(7,9);
       
        cout<<t<<endl;
       
        return 0;
}

//----------------------------------------------------------

Debug版本的Ollydbg反汇编代码:

004015C8   .  C745 FC 03124>mov dword ptr ss:[ebp-4],pi.00401203     ;  将函数指针指向max()

004015CF   .  8BF4          mov esi,esp                              ;  用于后面esp校验,不用管

004015D1   .  6A 09         push 9

004015D3   .  6A 07         push 7

004015D5   .  FF55 FC       call dword ptr ss:[ebp-4]                ;  call max()

004015D8   .  83C4 08       add esp,8

004015DB   .  3BF4          cmp esi,esp

004015DD   .  E8 AEEF0100   call pi.__chkesp

进入max()看看,很显然和前面<1>中的Debug版本一样嘛!汗!

//----------------------------------------------------------

Release版本的Ollydbg反汇编代码:

00401012  |.  6A 09         push 9

00401014  |.  6A 07         push 7

00401016  |.  E8 E5FFFFFF   call pi.00401000

0040101B  |.  83C4 08       add esp,8

可见,编译器已经对其进行了优化,直接使用max()函数的地址进行调用。所以,函数指针在汇编级别只不过是一个内存变量来保存了函数的地址而已,其指向是可以变的。这个特点有很重要的应用。例如用指向函数的指针作为函数参数来实现一个基本程序框架下的程序功能的多元化。复习完了指向函数的指针,我们来讨论<1>中遗留的问题:API函数地址是显式的吗?编程如下:

//----------------------------------------------------------

#include<iostream>

#include<windows.h>

using namespace std;

int main()
{
        int (*p)(LPTSTR);
       
        p=lstrlen;
       
        int n=p("I will know.");
       
        cout<<n;
       
        return 0;
}

F5编译一下,咦,怎么回事?p=lstrlen("I will know.")这句对应了一个错误:

error C2440: '=' : cannot convert from 'int (__stdcall *)(const char *)' to 'int (__cdecl *)(char *)'

This conversion requires a reinterpret_cast, a C-style cast or function-style cast

我们知道API是__stdcall调用规范。我们的C/C++是__cdecl规范。不一样的啊!呵呵~不过有一个API是C调用规范的----对啦,就是wsprintf。我们换它试试。

//----------------------------------------------------------

#include<iostream>

#include<windows.h>

using namespace std;

int main()
{
        char szBuff[20];
       
        int (*p)(LPTSTR,LPCTSTR,...);
       
        p=wsprintf;
       
        int n=p(szBuff,"%c","I will know.");
       
        cout<<"Function Address:"<<p<<endl;
       
        cout<<"Return Str Lenth:"<<n<<endl;
       
        return 0;
}

编译通过~Oh,Yeah!运行窗口中,我们看到,它居然显示了wsprintf函数的地址了(而且显示字符串长度为1,哦,对了,它以null为结束符的。呵呵,又巩固点知识)!莫非很多API都可以被编译器显式给出函数内存地址还是由于Windows将API区别对待?我可不敢说。不过像这样的诸多限制下,指向函数的指针也该不会有什么用武之地了吧~(API都是用户层的吗?有的书上这样说的。核心层呢?我没接触过底层,所以不知道。不过我知道像dll的API函数寻址在汇编写的病毒中是很重要的一块内容,由此,高级语言与汇编语言的鲜明差异可见一斑。)不说了继续看。发现自己懂的东西太少了,汗!

//----------------------------------------------------------

<3>返回指针值的函数

返回指针的函数我一直都没用过~那先编个试试吧~

//----------------------------------------------------------

#include<iostream>

using namespace std;

int *max(int x,int y)
{
        int *z=NULL;
       
        *z=x>y?x:y;                                                //运算符优先级 (?:)>(=)
       
        return z;
}

int main()
{
        int *t=max(7,9);
       
        cout<<*t<<endl;
       
        return 0;
}

编译通过。运行一下试试。咦,又怎么了?程序停止工作!咋回事?仔细想了一下,原来如此:max函数里面的指针变量是局部变量,函数运行完就释放啦!函数返回值不是“合法”的内存地址(其实,还可以使用这个地址,也可以操作这个地址的变量,但显然这是不安全的)!呵呵。又学到点东东。定义一个全局变量试试,把int*z=NULL放到max函数外面去。我的天呐,怎么还是同样的错误?又自作多情了~想啊想~换个编译环境试试吧~(注:此时int *z=NULL已经又放回max里面了) devC-4.9.9.2 嘿嘿~一试,也不行!!!继续想啊想~调试~跟踪~指针!!!指针是如何初始化的呢?查了查书上,其中有一条很重要的说明:不能用一个整数给指针变量赋初值!看max函数中这句:*z=x>y?x:y  是不是很可疑啊?削掉!换为 z=&(x>y?x:y)(注:为什么换成这种形式呢?因为书上都这么写。可是,网络上流传的代码中,形如*z=x>y?x:y的代码可不少呢。这究竟为什么呢?编译环境关系?且看~) 再试试。哈哈!在devC++中通过了!运行结果正确,为9!到VC6.0上试试。晕!怎么运行结果是个几百万大的数4198605啊?先不管devC++了,还在VC上试~汗啊~对啊,还没把int*z=NULL放到max外面使z成为全局指针变量呢。然后呢,还是4198605!我有点崩溃!一气之下将max整了个容:

int *max(int x,int y)
{
        int *z=&(x>y?x:y);                                                //运算符优先级 (?:)>(=)
       
        return z;
}

这样看起来舒服多了~这种山寨优化技术你蒙谁呢?!这证明你还没晕~恭喜~可我还能干什么呢?继续汗啊汗~微软好狡猾啊!!!事实让我相信Borland的C/C++编译技术的确是不错的!!!先玩点其他的吧~前面玩晕了的时候不小心Ctrl+H把int改成char了,结果还没错,为大写的E~代码如下:

//----------------------------------------------------------

#include<iostream>

using namespace std;

char *max(char x,char y)
{
        char *z=&(x>y?x:y);
       
        return z;
}

char main()
{
        char *t=max(69,68);
       
        cout<<*t<<endl;
       
        return 0;
}

好家伙!main函数都整容了~对照前面的错误结果,产生诸多怀疑:整数表示范围问题?不是啊!没有溢出啊!在-2147483648~+2147483647内啊!还是再玩点其他的吧~看3种不同版本的max函数:

#1#:

int *max(int x,int y)
{
        *z=x>y?x:y;                        ;z指针是全局变量
       
        return z;
}

#2#:

int *max(int x,int y)
{
        int *z=NULL;
       
        *z=x>y?x:y;
       
        return z;
}

#3#:

int *max(int x,int y)
{
        int *z=&(x>y?x:y);
       
        return z;
}

这3种版本有什么区别吗?

第1种很好理解:

将x、y中较大值拷贝给z指针所指向的全局的内存变量,然后将z指针返回。第2种和第3种有什么区别呢?嘿嘿~

第2种:

局部整型指针z初始化为谁都不指向,然后x、y值拷贝给指针z指向的变量!!!bang!bang!bang!如梦方醒!z指针哪个内存都不指向,你要这个较大值赋值给谁啊!怪不得VC运行出错呢(估计DevC++将其处理为:把x、y较大值的内存地址赋值给z指针)。所以网络上盛传的*z=x>y?x:y这种代码是很危险的,尤其在现在的非ANSI C++时代,除非你能100%确信z指针是一个指向合法内存的指针,或者说z指针是个合法的指针!然后,返回z指针后释放局部指针变量z。

第3种:

将x、y中的较大值的地址初始化局部整型指针变量z,然后返回z。由此,我们可以回答前面提到的问题:值拷贝是什么东东?我们知道指针只不过是个整数值,用eax返回。作为值,指针也需要保存,或在寄存器,或在内存。我们只讨论在内存的情况,寄存器也一样。指针变量和指针所指向的变量是两个变量!看起来像句废话,其实书本上多次重点强调过,只是是我们注意不够!既然是两个变量,那么用x、y较大值的地址初始化z指针时,是将x、y的较大值的内存地址拷贝给z指针变量而不是z指针指向的变量。为什么是x、y的较大值的内存地址呢,怎么没有寄存器的事了呢?试想:寄存器哪有地址之说呢,更不会有什么取址操作了!这足以证明,编译器已经给给形参x、y分配了内存了!可这内存是实参之外另外开辟的内存,还是直接使用了实参的内存了呢?不好说。我脑袋里的CPU(虽然很慢~)告诉我:编译器具体情况具体编译优化处理。别忘了,寄存器可比内存速度快嘛!不放心?反汇编看看嘛!Ollydbg可不是吃素的,WinDbg更不是(虽然我本人喜欢吃素)!WinDbg的源码调试功能可不错!这是我最喜欢的微软的产品!嘿嘿!反汇编代码我就不贴了,其实大家都熟见,仔细想想,还的确有点意思。人脑CPU运行结果如下:

#1:对PE文件,映射到内存中,实参无疑会被放在内存中,或PE程序执行过程中加载到寄存器中。

#2:有2种常见参数传递方法:堆栈,寄存器。寄存器速度比堆栈快点~对于堆栈,就是把实参压入堆栈,然后子程序通过堆栈来取得参数。一般情况下,子程序中将堆栈中取得的参数(可能为内存地址,即指针)mov到寄存器。至于是否将取得的实参再mov到另外开辟的内存中,这就要编译器根据实际情况定夺了。Debug版本许多都用到了另外开辟的内存。至于直接用寄存器传递参数,显然已经包括在刚说的利用堆栈的方法中了,不再赘述。再加之C++中特有的this指针的传递,真让人脑CPU超频了~其实,实参到形参的值拷贝机制的复杂性远比我们想的大。这一块有点乱,但绝对值的大家深究一下。欲继续玩者,参看《加密与解密(第三版)》P74。

<4>指针数组

没有什么其他要说的。但要记住以下几点:

#1 指针数组的定义原型为:类型名 *数组名[数组长度]。

#2 int *p=[4]                ;[]比*优先级高。定义了一个包含4个指针元素的数组

#3 int (*p)[4]。        ;定义了一个指向4个整型元素大小数组的指针

#4 留待大家的CPU来探索运行...

<5>指向指针的指针

妈呀!一个指针就够呛了,还来个指向指针的指针!这个世界真疯狂!在掌握了指针数组的基础上,我的人脑CPU又运行了...指向指针的指针如何定义呢?例如:char *(*p); 或者干脆写成:char **p; 因为*的结合性是从右到左。发现课本上的例子是很好的:

//----------------------------------------------------------

#include<iostream>

using namespace std;

int main()
{
        char **p;
        char *name[]={"BASIC","FORTRAN","C++","Pascal","COBOL"};
       
        p=name+2;
       
        cout<<*p<<endl;                        //输出name[2]指向的字符串
       
        cout<<**p<<endl;                //输出name[2]指向的字符串中的第一个字符
       
        return 0;
}

程序输出:

C++

C

嘿嘿~这个例子很好吧~这就是间接访问中的“二级间址”方法。还可以有多级呢~如果你脑袋CPU不怕死循环的话~一般最多使用到二级间址。

<6>感想

终于总结完了,匆忙之中难免有所疏漏,希望大家继续探索!我的确懂的东西不多,可我会把遇到的问题一个一个弄明白的!今天早上突然想到指针和函数,于是早上上完课,中午回到宿舍就开始Play it 了~思考、编程、发现新问题、分析新问题、解决新问题、录入文章、修改文章...直到晚上才弄完~很舒服,很痛快!哈哈!学习的乐趣也就在此吧!哈哈!临末,把课本上的指针的小结弄下来~

        int i;                        | 定义整型变量
       
        int *p;                        | p为指向整型数据的指针变量
       
        int a[n];                | 定义整型数组a,它有n个元素
       
        int *p[n];                | 定义指针数组p,它由n个指向整型数据的指针元素组成
       
        int (*p)[n];        | p为指向含n个元素的一维数组的指针变量
       
        int f();                | f为返回整型函数值的函数
       
        int *p();                | p为返回一个指针的函数,该指针指向整型数据
       
        int (*p)();                | p为指向函数的指针,该函数返回一个整型值
       
        int **p;                | p是一个指向指针的指针变量,它指向一个指向整型数据的指针变量

The End .

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 0
支持
分享
最新回复 (11)
雪    币: 187
活跃值: (26)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
2
不小心发了两次。另外,我想申请邀请码。嘿嘿~
2010-8-30 22:57
0
雪    币: 74
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
LZ很强悍啊,我现在也正在学习C语言的指针这一块,感觉有点复杂,看书头都大了,呵呵,**!
2010-8-31 21:51
0
雪    币: 421
活跃值: (83)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
4
字体太难看了,也太小了。看的眼晕
2010-9-1 10:12
0
雪    币: 61
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
将指针传给函数做参数时一不小心就会出错,楼主想过这个问题没?
2010-9-1 11:28
0
雪    币: 317
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
学到点东西,有点晕
2010-9-1 11:32
0
雪    币: 187
活跃值: (26)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
7
我的编程经验也不多,不过我感觉能够保证3点就应该不会有太大问题吧(一家之言,何况自己还是菜鸟)
<1>指针初始化正确;<2>指针传递时不被意外修改;<3>指针指向的是可以被正确使用的内存。能想到的就这些了。。。
2010-9-1 12:36
0
雪    币: 61
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
自己也写了一段时间的C了,很多东西还是用得不熟,包括指针。看来还是码得太少了。
2010-9-1 12:39
0
雪    币: 35
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
学习了解一下啊
2010-9-1 13:10
0
雪    币: 2
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
指针很让人头痛
2010-9-1 16:28
0
雪    币: 13
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
本人菜鸟,呵呵,挨个帖子学习,呵呵!
2010-9-1 19:47
0
雪    币: 36
活跃值: (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
不错的帖子,谢谢楼主分享
2010-9-3 17:20
0
游客
登录 | 注册 方可回帖
返回
//