类函数回调





1.00/5 (6投票s)
2005年4月28日
5分钟阅读

90908
演示了如何回调类中非静态的函数。
引言
在这篇文章中,我将向您展示如何回调类中非静态的函数。这将利用函数、指针和一些汇编。据我所知,没有汇编是不可能实现的。但是,本指南不需要您了解任何汇编知识;它将解释其工作原理。本指南无意教授汇编,网络上有很多优秀的汇编教程。不过,这是一种高级技术,即使是初学者也能从中受益。
我们需要什么?一个能够进行汇编的编译器和一个类!在本文中,我还将向您展示我如何使用此技术通过窗口过程转发消息。那么,让我们开始吧!
首先,我们需要一个基类。在本示例中,我使用 MFC 来演示此技术的工作原理。因此,如果您想按照本示例进行操作,请使用 MFC 创建一个对话框窗口。该类是主窗口的对话框类。
class CMyTestDlg { public: ... CProcEdit m_MyEdit; protected: LRESULT WindowProc(CWnd* pCtrl, UINT message, WPARAM wParam, LPARAM lParam); };
也许您注意到了 `WindowProc` 不是虚函数,并且带有一个额外的参数:`pCtrl`。这是因为,在本例中,我打算将其设为一个“全局”的 `WindowProc`。所有控件都将消息转发给它,有点像原始窗口过程(非 MFC)。
既然有了这个,我们就需要创建整个转发函数。怎么做?这很简单。我们只需派生出我们想要转发消息的控件,创建一个新类。让我们也定义一下这个类……
typedef LRESULT (WndProcPtr)(UINT,WPARAM,LPARAM); class CProcEdit: public CEdit { public: struct CallbackPtr { CWnd* pThisPtr; WndProcPtr* pWndProcPtr; } pCallbackPtr; protected: virtual LRESULT WindowProc(UINT message, WPARAM wParam, LPARAM lParam); };
我们不需要更复杂的类。它继承了 MFC 的 `CEdit` 类(编辑类)的所有内容。无论如何,这个类的目的只是转发其消息。这就是为什么我们重写 `WindowProc`。您可能还在想为什么会有 `struct` 和 **两个** 参数?函数指针只有一个变量。是的……但我们需要两个参数:一个用于函数地址,另一个指向包含该函数的类本身。明白了吗?很好。至于为什么需要第二个……我稍后会再解释。
现在,为了使其能够进行回调,我们必须在初始化(或创建)对话框或类时设置这些变量。所以,我们在主对话框的 `InitDialog` 中这样做。现在,我们开始使用汇编的第一部分。先给您看代码,然后讨论其作用……
BOOL CMyTestDlg::OnInitDialog()
{
WndProcPtr* pCallback;
__asm
{
mov eax, WindowProc;
mov pCallback, eax;
};
m_MyEdit.pCallbackPtr.pThisPtr = this;
m_MyEdit.pCallbackPtr.pWndProcPtr = pCallback;
};
就这样。现在让我们看看这段代码的作用……为什么需要汇编?……汇编到底做了什么?让我解释一下……如果您对此有经验,应该知道……
pCallback = WindowProc;
……会导致编译错误。这就是为什么我们必须使用汇编!在那里没有这样的限制。“mov
”操作码将数据从内存移出或移入内存。所以,首先,我们将 `WindowProc` 函数的偏移量移入 `eax` 寄存器。然后,我们将 `eax` 寄存器的内容移入我们的 `pCallback` 变量。请注意,由于汇编语言的限制,我们不能直接将内容移入变量。之后,`pCallback` 变量就包含了我们函数的地址(或偏移量)。然后,我们将编辑类的两个变量设置为指向我们的对话框类和函数。好了,跟上了吗?希望您跟上了!
既然我们已经初始化了类,就必须确保编辑类转发我们的消息。这可能有点棘手……总之,这是代码
LRESULT CProcEdit::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) { if (pWndProc) { __asm { // Pass arguments (right to left) mov eax, [lParam]; push eax; mov eax, [wParam]; push eax; mov eax, [message]; push eax; mov eax, this; push eax; // Fill ecx with the class placement mov eax, this; add eax, pThisPtr; mov ecx, [eax]; // Call function mov eax, pWndProc; add eax, this; mov eax, [eax]; call eax; } } return CEdit::WindowProc(message, wParam, lParam); }
呼,这确实是一堆汇编代码。它做了什么?为什么我们需要汇编来调用函数指针?嗯,这不像您想的那么简单……首先,让我告诉您为什么普通调用会失败。您知道,在类中有一个“this
”指针。这个“this
”指针存储在 `ecx` 寄存器中。`ecx` 寄存器必须指向类所在的偏移量。那么,如果我们正常调用函数,会发生什么?`ecx` 中的偏移量是**当前**类的偏移量——换句话说,是 `CProcEdit` 类的偏移量!当这种情况发生时,我们调用的函数将无法正确访问其成员,甚至可能导致访问冲突。为什么会这样?函数就在类里面,所以……为什么不能直接调用它们并使其工作?
好问题……不幸的是,事情并非如此……您是否曾在 DLL 中导出过类的一部分函数?这是可以工作的。但是当您调用它们时,“this
”指针是 `NULL`!为什么?那是因为函数**不**存储在类中!看看这个类……
class CAnotherClass { int arg1; int arg2; void myfunc() { } };
对这个类执行 `sizeof` 将返回 8!因为有两个 `int` 参数。函数**不**被计算在内!所以……当调用类中的函数时,函数必须知道类存在的位置才能访问其数据。这就是为什么有一个“this
”指针,它存储在 `ecx` 寄存器中。
这就是为什么我们需要汇编。我们需要确保我们正确地填充了 `ecx` 寄存器。因此,我们还获取了指向类本身的指针。好了,那么代码做了什么?让我们看看……
首先,我们将参数推入堆栈。要将某物推入堆栈,我们必须先将其放入寄存器。所有参数都是从右到左传递的,所以是这样。其次,我们必须用类地址(或偏移量)填充 `ecx`!第三,我们调用函数。所有这些是如何工作的,我将不再详细解释……如果您想知道,那就去找个汇编课程吧。
最后,我们调用基类的 `windowproc` 函数,以便它能够进一步处理消息。这就是我们需要做的所有事情!不过,关于此示例,我有一个注意事项。在 `CMyTestDlg` 的被调用 `windowproc` 中,**不要调用基类的窗口过程函数!** 为什么?因为消息不是为该类设计的。如果您将它们传递下去,您可能会在窗体上看到奇怪的绘制和其他奇怪的事情。相反,返回 0 或其他值;这无关紧要,因为我们不返回此窗口过程返回的值。
……这就是全部了!现在您可能可以理解为什么类函数回调是不可能的。如果还不明白……我猜我可以告诉您。很简单。编译器**不知道函数属于哪个类**。
编程愉快!