分类:
2012-06-14 23:56:50
原文地址:C语言中指针及回调函数 作者:yulianliu1218
一.关于指针传递的思考
例1-1:
#include
char* test()
{
char str[] = "this is a test";
return str;
}
int main(void)
{
char* pStr = NULL;
pStr = test();
puts(pStr);
return 0;
}
运行之后,会出现什么情况呢?
答:很可能是一些乱码。
因为test返回的的是指向栈,原先开辟出来的内存生存周存在函数执行完之后,就被系统回收。故输出的值不确定。
例1-2:
#include
#include
#include
void test(char* str)
{
strcpy(str,"this is a test");
}
int main(void)
{
char str[100] = {0};
test(str);
printf("str=%s\n",str);
return 0;
}
运行结果是什么呢?
答:str=this is a test
传给test函数的参数是指针值,本质上可以说指针传递也是值传递,即传给test函数的是指向str[100]数组的首地址。在test内对指定地址内容进行操作。
例1-3:test若改成:
void test2(char* str)
{
str = malloc(sizeof("this is a test"));
strcpy(str,"this is a test");
}
main函数改成:
int main(void)
{
char* str2=NULL;
test2(str2);
printf("str2=%s\n",str2);
free(str2);
return 0;
}
输出结果又是什么呢?
答:输出NULL
因为str = malloc(sizeof(“this is a test”));语句使str里的地址发生改变,因此后面的strcpy()操作不影响数组str2。
那如何解决这个问题呢?可以采用指针的指针,即将str2的地址传给test3,如下:
void test3(char** str)
{
*str = malloc(sizeof("this is a test"));
strcpy(*str,"this is a test");
}
来一个比较有意思的例子
例1-4:char * test2(void)
{
char *str = "this is my test";
return str;
}
int main(void)
{
char* str2=NULL;
str2 = test2();
printf("str=%s\n",str2);
return 0;
}
运行会出现什么情况?
答:在debug下,会出现“degub assertion failed”,同时也会输出:str=this is my test
因为”this is my test”是字符串常量,保存在内存区域的文字常量区域,它的生存周期是程序运行的整个过程。利用这一点,将”this is my test”字符串的首地址返回保存起来,之后用这个地址来输出字符串是可行的。但是,函数结束完之后,”this is my test”这个字符串的生存期也就结束了,以后再用它就会有问题。即出现野指针问题。
不改程序,照样也想运行程序,可以嘛?在vc6.0下,将debug改成release模式(build->configuration,删除debug),就不会再出现degub assertion failed的提示。
二.函数指针
如何定义一个指向函数的指针变量?
一般定义形式为:数据类型 (* 指针变量名) (函数参数列表)
这里“数据类型”指函数返回值的类型。
实例:定义一个函数参数是int型,返回类型为空的函数指针变量
void (*f)(int);
可以说变量f的类型为void (* )(int),既然如此,那不是可以用typedef给这种类型定义一个别名。
typedef void (*F)(int);
此时就可以用F定义变量了。
例2-1:
#include
#include
#include
void test(int n)
{
printf("n=%d\n",n);
}
typedef void(*F)(int);
int main(void)
{
F f = test; //定义函数指针f,并初始化指向test函数
F *p = NULL;
f(100);
printf("%p %p\n", test,&test);
return 0;
}
三.关于const关键字
被const修饰的变量,可以被修改嘛?
答:可以
如:
int* p2;
const int n2 = 200;
p2 = &n2;
*p2 = 300;
printf("n2=%d\n",n2);
编译的时候有个警告:warning C4090: '=' : different 'const' qualifiers
但执行完后,发现n2值变为300了。
警告提示是类类型不匹配,改成const int * p2之后,就不会出现警告,因为此时类类型都为const int,但会出现错误:error C2166: l-value specifies const object.
说时const类型不能作为左值。
关于const比较难理解的是修饰指针变量。如:
const int a;
int const a;
const int *a;
int * const a;
const int * const a;
前两个的作用是一样,a是一个常整型数。第三个意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。第四个意思a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。如果应试者能正确回答这些问题,那么他就给我留下了一个好印象。顺带提一句,也许你可能会问,即使不用关键字 const,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关键字const呢?我也如下的几下理由:
1) 关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const的程序员很少会留下的垃圾让别人来清理的。)
2) 通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。
3) 合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。
四. 回调函数
1 什么是回调
软 件模块之间总是存在着一定的接口,从调用方式上,可以把他们分为三类:同步调用、回调和异步调用。同步调用是一种阻塞式调用,调用方要等待对方执行完毕才 返回,它是一种单向调用;回调是一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口;异步调用是一种类似消息或事件的机制,不过它的 调用方向刚好相反,接口的服务在收到某种讯息或发生某种事件时,会主动通知客户方(即调用客户方的接口)。回调和异步调用的关系非常紧密,通常我们使用回 调来实现异步消息的注册,通过异步调用来实现消息的通知。同步调用是三者当中最简单的,而回调又常常是异步调用的基础,因此,下面我们着重讨论回调机制在 不同软件架构中的实现。
对于不同类型的语言(如结构化语言和对象语言)、平台(Win32、JDK)或构架(CORBA、DCOM、WebService),客户和服务的交互除了同步方式以外,都需要具备一定的异步通知机制,让服务方(或接口提供方)在某些情况下能够主动通知客户,而回调是实现异步的一个最简捷的途径。
对于一般的结构化语言,可以通过回调函数来实现回调。回调函数也是一个函数或过程,不过它是一个由调用方自己实现,供被调用方使用的特殊函数。
在面向对象的语言中,回调则是通过接口或抽象类来实现的,我们把实现这种接口的类成为回调类,回调类的对象成为回调对象。对于象C++或Object Pascal这些兼容了过程特性的对象语言,不仅提供了回调对象、回调方法等特性,也能兼容过程语言的回调函数机制。
Windows平台的消息机制也可以看作是回调的一种应用,我们通过系统提供的接口注册消息处理函数(即回调函数),从而实现接收、处理消息的目的。由于Windows平台的API是用C语言来构建的,我们可以认为它也是回调函数的一个特例。
对于分布式组件代理体系CORBA,异步处理有多种方式,如回调、事件服务、通知服务等。事件服务和通知服务是CORBA用来处理异步消息的标准服务,他们主要负责消息的处理、派发、维护等工作。对一些简单的异步处理过程,我们可以通过回调机制来实现。
下面我们集中比较具有代表性的语言(C、Object Pascal)和架构(CORBA)来分析回调的实现方式、具体作用等。
过程语言中的回调(C)
函数指针
回调在C语言中是通过函数指针来实现的,通过将回调函数的地址传给被调函数从而实现回调。因此,要实现回调,必须首先定义函数指针,请看下面的例子:
void Func(char *s);// 函数原型 void (*pFunc) (char *);//函数指针 |
可以看出,函数的定义和函数指针的定义非常类似。
一般的化,为了简化函数指针类型的变量定义,提高程序的可读性,我们需要把函数指针类型自定义一下。
typedef void(*pcb)(char *); |
回调函数可以象普通函数一样被程序调用,但是只有它被当作参数传递给被调函数时才能称作回调函数。
被调函数的例子:
void GetCallBack(pcb callback) { /*do something*/ } 用户在调用上面的函数时,需要自己实现一个pcb类型的回调函数: void fCallback(char *s) { /* do something */ } 然后,就可以直接把fCallback当作一个变量传递给GetCallBack, GetCallBack(fCallback); |
如果赋了不同的值给该参数,那么调用者将调用不同地址的函数。赋值可以发生在运行时,这样使你能实现动态绑定。
参数传递规则
到目前为止,我们只讨论了函数指针及回调而没有去注意ANSI C/C++的编译器规范。许多编译器有几种调用规范。如在Visual C++中,可以在函数类型前加_cdecl,_stdcall或者_pascal来表示其调用规范(默认为_cdecl)。C++ Builder也支持_fastcall调用规范。调用规范影响编译器产生的给定函数名,参数传递的顺序(从右到左或从左到右),堆栈清理责任(调用者或者被调用者)以及参数传递机制(堆栈,CPU寄存器等)。
将调用规范看成是函数类型的一部分是很重要的;不能用不兼容的调用规范将地址赋值给函数指针。例如:
// 被调用函数是以int为参数,以int为返回值 __stdcall int callee(int);
// 调用函数以函数指针为参数 void caller( __cdecl int(*ptr)(int));
// 在p中企图存储被调用函数地址的非法操作 __cdecl int(*p)(int) = callee; // 出错 |
指针p和callee()的类型不兼容,因为它们有不同的调用规范。因此不能将被调用者的地址赋值给指针p,尽管两者有相同的返回值和参数列
应用举例
C语言的标准库函数中很多地方就采用了回调函数来让用户定制处理过程。如常用的快速排序函数、二分搜索函数等。
快速排序函数原型:
void qsort(void *base, size_t nelem, size_t width, int (_USERENTRY *fcmp)(const void *, const void *)); 二分搜索函数原型: void *bsearch(const void *key, const void *base, size_t nelem, size_t width, int (_USERENTRY *fcmp)(const void *, const void *)); |
其中fcmp就是一个回调函数的变量。
下面给出一个具体的例子:
#include #include
int sort_function( const void *a, const void *b); int list[5] = { 54, 21, 11, 67, 22 };
int main(void) { int x;
qsort((void *)list, 5, sizeof(list[0]), sort_function); for (x = 0; x < 5; x++) printf("%i\n", list[x]); return 0; }
int sort_function( const void *a, const void *b) { return *(int*)a-*(int*)b; } |
2.4 面向对象语言中的回调(Delphi)
Dephi与C++一样,为了保持与过程语言Pascal的兼容性,它在引入面向对象机制的同时,保留了以前的结构化特性。因此,对回调的实现,也有两种截然不同的模式,一种是结构化的函数回调模式,一种是面向对象的接口模式。
2.4.1 回调函数
回调函数类型定义:
type TCalcFunc=function (a:integer;b:integer):integer; |
按照回调函数的格式自定义函数的实现,如
function Add(a:integer;b:integer):integer begin result:=a+b; end; function Sub(a:integer;b:integer):integer begin result:=a-b; end; |
回调的使用
function Calc(calc:TcalcFunc;a:integer;b:integer):integer |
下面,我们就可以在我们的程序里按照需要调用这两个函数了
c:=calc(add,a,b);//c=a+b c:=calc(sub,a,b);//c=a-b |
2.4.2 回调对象
什 么叫回调对象呢,它具体用在哪些场合?首先,让我们把它与回调函数对比一下,回调函数是一个定义了函数的原型,函数体则交由第三方来实现的一种动态应用模 式。要实现一个回调函数,我们必须明确知道几点:该函数需要那些参数,返回什么类型的值。同样,一个回调对象也是一个定义了对象接口,但是没有具体实现的 抽象类(即接口)。要实现一个回调对象,我们必须知道:它需要实现哪些方法,每个方法中有哪些参数,该方法需要放回什么值。
因此,在回调对象这种应用模式中,我们会用到接口。接口可以理解成一个定义好了但是没有实现的类,它只能通过继承的方式被别的类实现。Delphi中的接口和COM接口类似,所有的接口都继承与IInterface(等同于IUnknow),并且要实现三个基本的方法QueryInterface, _AddRef, 和_Release。
· 定义一个接口
type IShape=interface(IInterface) procedure Draw; end |
· 实现回调类
type TRect=class(TObject,IShape) protected function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall; function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; public procedure Draw; end;
type TRound=class(TObject,IShape) protected function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall; function _AddRef: Integer; stdcall; function _Release: Integer; stdcall; public procedure Draw; end; |
· 使用回调对象
procedure MyDraw(shape:IShape); var shape:IShape; begin shape.Draw; end; |
如果传入的对象为TRect,那么画矩形;如果为TRound,那么就为圆形。用户也可以按照自己的意图来实现IShape接口,画出自己的图形:
MyDraw(Trect.Create); MyDraw(Tround.Create); |
2.4.3 回调方法
回调方法(Callback Method)可以看作是回调对象的一部分,Delphi对windows消息的封装就采用了回调方法这个概念。在有些场合,我们不需要按照给定的要求实现整个对象,而只要实现其中的一个方法就可以了,这是我们就会用到回调方法。
回调方法的定义如下:
TNotifyEvent = procedure(Sender: TObject) of object; TMyEvent=procedure(Sender:Tobject;EventId:Integer) of object; |
TNotifyEvent 是Delphi中最常用的回调方法,窗体、控件的很多事件,如单击事件、关闭事件等都是采用了TnotifyEvent。回调方法的变量一般通过事件属性的方式来定义,如TCustomForm的创建事件的定义:
property OnCreate: TNotifyEvent read FOnCreate write FOnCreate stored IsForm; |
我们通过给事件属性变量赋值就可以定制事件处理器。
用户定义对象(包含回调方法的对象):
type TCallback=Class procedure ClickFunc(sender:TObject); end; procedure Tcallback.ClickFunc(sender:TObject); begin showmessage('the caller is clicked!'); end; |
窗体对象:
type TCustomFrm=class(TForm) public procedure RegisterClickFunc(cb:procedure(sender:Tobject) of object); end;
procedure TcustomFrm..RegisterClickFunc(cb:TNotifyEvent); begin self.OnClick=cb; end; |
使用方法:
var frm:TcustomFrm; begin frm:=TcustomFrm.Create(Application); frm.RegisterClickFunc(Tcallback.Create().ClickFunc); end; |
回调在分布式计算中的应用(CORBA)
回调接口模型
CORBA的消息传递机制有很多种,比如回调接口、事件服务和通知服务等。回调接口的原理很简单,CORBA客户和服务器都具有双重角色,即充当服务器也是客户客户。
回调接口的反向调用与正向调用往往是同时进行的,如果服务端多次调用该回调接口,那么这个回调接口就变成异步接口了。因此,回调接口在CORBA中常常充当事件注册的用途,客户端调用该注册函数时,客户函数就是回调函数,在此后的调用中,由于不需要客户端的主动参与,该函数就是实现了一种异步机制。
从CORBA规范我们知道,一个CORBA接口在服务端和客户端有不同的表现形式,在客户端一般使用桩(Stub)文件,服务端则用到框架(Skeleton)文件,接口的规格采用IDL来定义。而回调函数的引入,使得服务端和客户端都需要实现一定的桩和框架。下面是回调接口的实现模型:
范例
下面给出了一个使用回调的接口文件,服务端需要实现Server接口的框架,客户端需要实现CallBack的框架:
module cb { interface CallBack; interface Server;
interface CallBack { void OnEvent(in long Source,in long msg); }; interface Server { long RegisterCB(in CallBack cb); void UnRegisterCB(in long hCb); }; }; |
客户端首先通过同步方式调用服务端的接口RegistCB,用来注册回调接口CallBack。服务端收到该请求以后,就会保留该接口引用,如果发生某种事件需要向客户端通知的时候就通过该引用调用客户方的OnEvent函数,以便对方及时处理。