分类: C/C++
2008-08-01 17:06:55
未引用参数
添加任务栏命令
我看到过一些 C 代码针对没有使用过的参数用 UNREFERENCED_PARAMETER,例如:
int SomeFunction(int arg1, int arg2) { UNREFERENCED_PARAMETER(arg2) ... }我还看到过这样的代码:
int SomeFunction(int arg1, int /* arg2 */) { ... }你能解释它们的差别吗?哪一种用法更好?
#define UNREFERENCED_PARAMETER(P) (P)
换句话说 UNREFERENCED_PARAMETER 展开传递的参数或表达式。其目的是避免编译器关于未引用参数的警告。许多程序员,包括我在内,喜欢用最高级别的警告 Level 4(/W4)进行编译。Level 4 属于“能被安全忽略的事件”的范畴。虽然它们可能使你难堪,但很少破坏你的代码。例如,在你的程序中可能会有这样一些代码行:
int x=1;
但你从没用到过 x。也许这一行是你以前使用 x 时留下来的,只删除了使用它的代码,而忘了删除这个变量。Warning Level 4
能找到这些小麻烦。所以,为什么不让编译器帮助你完成可能是最高级别的专业化呢?用Level 4
编译是展示你工作态度的一种方式。如果你为公众使用者编写库,Level 4
则是社交礼节上需要的。你不想强迫你的开发人员使用低级选项清洁地编译他们的代码。
问题是,Level 4 实在是太过于注意细节,在 Level 4
上,编译器连未引用参数这样无伤大雅的事情也要抱怨(当然,除非你真的有意使用这个参数,这时便相安无事)。假设你有一个函数带来两个参数,但你只使用其中一个:
int SomeFunction(int arg1, int arg2) { return arg1 5; }
使用 /W4,编译器抱怨:
“warning C4100: ''arg2'' : unreferenced formal parameter.”
为了骗过编译器,你可以加上 UNREFERENCED_PARAMETER(arg2)。现在编译器在编译你的引用 arg2 的函数时便会住口。并且由于语句:
arg2;
实际上不做任何事情,编译器不会为之产生任何代码,所以在空间和性能上不会有任何损失。
细心的人可能会问:既然你不使用 arg2,那当初为何要声明它呢?通常是因为你实现某个函数以满足某些API固有的署名需要,例如,MFC的
OnSize 处理例程的署名必须要像下面这样:
void OnSize(UINT nType, int cx, int cy);
这里 cx/cy 是窗口新的宽/高,nType 是一个类似 SIZE_MAXIMIZED 或 SIZE_RESTORED
这样的编码,表示窗口是否最大化或是常规大小。一般你不会在意 nType,只会关注 cx 和 xy。所以如果你想用 /W4,则必须使用 UNREFERENCED_PARAMETER(nType)。OnSize
只是上千个 MFC 和 Windows 函数之一。编写一个基于 Windows 的程序,几乎不可能不碰到未引用参数。
说了这么多关于 UNREFERENCED_PARAMETER 内容。Judy 在她的问题中还提到了另一个 C 程序员常用的并且其作用与 UNREFERENCED_PARAMETER
相同的诀窍,那就是注释函数署名中的参数名:
void CMyWnd::OnSize(UINT /* nType */, int cx, int cy) { }
现在 nType 是未命名参数,其效果就像你敲入 OnSize(UINT,
int cx, int cy)一样。那么现在的关键问题是:你应该使用哪种方法——未命名参数,还是 UNREFERENCED_PARAMETER?
大多数情况下,两者没什么区别,使用哪一个纯粹是风格问题。(你喜欢你的 java 咖啡是黑色还是奶油的颜色?)但我认为至少有一种情况必须使用 UNREFERENCED_PARAMETER。假设你决定窗口不允许最大化。那么你便禁用
Maximize 按钮,从系统菜单中删除,同时阻止每一个用户能够最大化窗口的操作。因为你是偏执狂(大多数好的程序员都是偏执狂),你添加一个
ASSERT (断言)以确保代码按照你的意图运行:
void CMyWnd::OnSize(UINT nType, int cx, int cy) { ASSERT(nType != SIZE_MAXIMIZE); ... // use cx, cy }
质检团队竭尽所能以各种方式运行你的程序,ASSERT 从没有弹出过,于是你认为编译生成 Release 版本是安全的。但是此时
_DEBUG 定义没有了,ASSERT(nType != SIZE_MAXIMIZE)展开为 ((void)0),并且 nType
一下子成了一个未引用参数!这样进入你干净的编译。你无法注释掉参数表中的 nType,因为你要在 ASSERT
中使用它。于是在这种情况下——你唯一使用参数的地方是在 ASSERT 中或其它 _DEBUG 条件代码中——只有 UNREFERENCED_PARAMETER
会保持编译器在 Debug 和 Release 生成模式下都没有问题。知道了吗?
结束讨论之前,我想还有一个问题我没有提及,就是你可以象下面这样用 pragma 指令抑制单一的编译器警告:
#pragma warning( disable : 4100 )
4100 是未引用参数的出错代码。pragma 抑制其余文件/模块的该警告。用下面方法可以重新启用这个警告:
#pragma warning( default : 4100 )
不管怎样,较好的方法是在禁用特定的警告之前保存所有的警告状态,然后,等你做完之后再回到以前的配置。那样,你便回到的以前的状态,这个状态不一定是编译器的默认状态。
所以你能象下面这样在代码的前后用 pragma 指令抑制单个函数的未引用参数警告:
#pragma warning( push ) #pragma warning( disable : 4100 ) void SomeFunction(...) { } #pragma warning( pop )
当然,对于未引用参数而言,这种方法未免冗长,但对于其它类型的警告来说可能就不是这样了。库生成者都是用 #pragma warning
来阻塞警告,这样他们的代码可以用 /W4 进行清洁编译。MFC 中充满了这样的 pragmas 指令。还有好多的 #pragma warning
选项我没有在本文讨论。有关它们的信息请参考相关文档。
我注意到一些应用程序,当右键单击其任务栏最小化按钮时,在弹出的上下文菜单中具备特殊的命令。例如,WinAmp(一个流行的媒体播放器)有一个附加的
“WinAmp”菜单项,其中是 WinAmp 特有的命令。我如何在程序的任务栏按钮中添加我自己的菜单项?
CMainFrame::OnSysCommand(UINT nID, LPARAM lp) { if (nID==ID_MY_COMMAND) { ... // 处理它 return 0; } // 传递到基类:这一步很重要! return CFrameWnd::OnSysCommand(nID, lp); }
如果该命令不是你的,不要忘了将它传递到你的基类处理——典型地,那就是 CFrameWnd 或 CMDIFrameWnd。否则,Windows
将无法得到此消息,并且会破坏内建的命令。
在主框架中处理 WM_SYSCOMMAND
固然可以,但这样做感觉太业余。为什么要用特殊的机制来处理呢?就因为它们是系统菜单吗?如果你想在视图或文档对象中处理系统命令会怎样呢?有一个常见的命令放到了系统菜单中,它就是“关于”(ID_APP_ABOUT),大多数
MFC 程序都是在应用程序对象中处理 ID_APP_ABOUT:
void CMyApp::OnAppAbout() { static CAboutDialog dlg; dlg.DoModal(); }
MFC 一个真正很酷的特性是它的命令路由系统,它使得象 CMyApp
这样的非窗口对象也能处理菜单命令。许多程序员甚至都不了解怎么会有这样的例外。如果你已经在应用程序对象中处理 ID_APP_ABOUT,那把ID_APP_ABOUT
添加到系统菜单后,为什么还要去实现一套单独的机制?
处理外加系统命令的比较好的,或者说更 MFC 的方法应该是通过常规的命令路由机制传递它们。然后按 MFC 常规方法编写 ON_COMMAND
处理例程来处理系统命令。你甚至可以用 ON_UPDATE_COMMAND_UI
来更新你的系统菜单项,例如禁用某个菜单项或在菜单项旁边显示一个检讫标志。
Figure 2 是我写的一个类,CSysCmdRouter,这个类将系统命令转成常规命令。为了使用这个类,你要做的只是在主框架中实例化 CSysCmdRouter,并从
OnCreate 中调用其 Init 方法即可:
int CMainFrame::OnCreate(...) { // 将我的菜单项添加到系统菜单 CMenu* pMenu = GetSystemMenu(FALSE); pMenu->AppendMenu(..ID_MYCMD1..); pMenu->AppendMenu(..ID_MYCMD2..); // 通过 MFC 路由系统命令 m_sysCmdHook.Init(this); return 0; }
一旦你调用 CSysCmdRouter::Init,你便可以按常规方式处理 ID_MYCMD1 和 ID_MYCMD2,为 MFC
命令路由机制中的任何对象编写 ON_COMMAND 处理例程——视图,文档,框架,应用程序或通过改写 OnCmdMsg 添加的任何其它命令对象。CSysCmdRouter
还让你用 ON_UPDATE_COMMAND_UI
处理器更新系统菜单。唯一要注意的是确保命令IDs不要与其它菜单命令(除非他们确实代表相同的命令)或内建系统命令发生冲突,内建系统命令从
SC_SIZE = 0xF000 开始。Visual Studio .NET 指定的命令 IDs 从 0x8000 = 32768
开始,所以如果你让 Visual Studio 来指定 IDs,只要不超过 0xF000-0x8000 = 0x7000
个命令即可。也就是十进制的 28,762。如果你的应用程序有超过 28000 个命令,那么你需要咨询编程精神病专家。
CSysCmdRouter 是如何实现其魔法的呢?简单:它使用我那个以前专栏中无处不在的 CSubclassWnd。CSubclassWnd
使你不用从其派生便能子类化 MFC 窗口对象。CSysCmdRouter 派生自 CSubclassWnd
并使用它子类化主框架。尤其是它截获发送到框架的 WM_SYSCOMMAND 消息。如果命令 ID 属于系统命令(大于 SC_SIZE =
0xF000),则 CSysCmdRouter 沿着 Windows 一路传递该消息;否则便吃掉 WM_SYSCOMMAND 并重新将它作为
WM_COMMAND 发送,于是 MFC 按照其常规路由过程,调用你的 ON_COMMAND 处理器。很聪明,是不是?
那么 ON_UPDATE_COMMAND_UI 处理器呢?CSysCmdRouter 是如何让它处理系统菜单命令的呢?很简单。就在
Windows 显示菜单前,他向你的主窗口发送一个 WM_INITMENUPOPUP
消息。这是你更新菜单项的最佳时机——启用或禁用它们,添加检讫标志等等。MFC 为每个菜单项创建一个 CCmdUI
对象并将它传递到你的消息映射中相应的 ON_UPDATE_COMMAND_UI 处理器。以它为参数的 MFC 函数是 CFrameWnd::OnInitMenuPopup,这个函数是这样的:
void CFrameWnd::OnInitMenuPopup(CMenu* pMenu, UINT nIndex, BOOL bSysMenu) { if (bSysMenu) return; // don''t support system menu ... }
MFC 初始化系统菜单时不做任何事情。为什么要去关心这种事呢?万一你要让 bSysMenu 为 FALSE,即使是系统菜单,那该怎么办?这恰恰是 CSysCmdRouter 做的事情。它截取 WM_INITMENUPOPUP 并清除 bSysMenu 标志,也就是 LPARAM 的 HIWORD:
if (msg==WM_INITMENUPOPUP) { lp = LOWORD(lp); // (set HIWORD = 0) }
现在,当 MFC 获得 WM_INITMENUPOPUP,它认为该菜单是常规菜单。只要你的命令 IDs
与真正的系统菜单不冲突,一切都运行得很好。如果你改写 OnInitMenuPopup,唯一丢失的东西是不能从主窗口菜单中区分系统菜单。嘿,你不能什么都想要!通过改写 CWnd::WindowProc,你总是能处理 WM_INITMENUPOPUP
的,或你想要区分,就比较 HMENUs。但你确实不用关心命令来自何处。
Figure 3 任务栏菜单
为了展示所有的实践,我写了一个小测试程序: TBMenu。如图 Figure 3 所示,当你右键单击任务栏上 TBMenu
的最小化按钮,便会显示出菜单。你可以看到在菜单底部有两个额外的命令。TBMenu 的 CMainFrame代码如
Figure 4
所示。便知道在 OnCreate 的什么地方添加命令并在 CMainFrame 的消息映射中用 ON_COMMAND 以及 ON_UPDATE_COMMAND_UI
处理器处理它们。TBMenu 在其应用程序类中处理 ID_APP_ABOUT(代码未列出)。CSysCmdRouter
使系统命令的工作机制类似其它命令。
说到命令,我们来看看一个小资料:
在我一月份的专栏中,我问是否有人知道 Ctrl Alt Del 的由来。显然,有几个读者知道如何使用
Google,因为他们发给我的是相同的链接:《今日美国》上的一篇文章:“Thank
this guy for 'control-alt-delete'”,我在一月发问之前就发现了这篇文章。Ctrl Alt Del 是由一个名叫
David J. Bradley 的人发现的,他在 IBM 工作过。
IBM 觉得应该有一种方法不用关闭电源就能重置(reset)其新的 PC 机。为什么要专门用 Ctrl Alt Del 这三个键呢?从技术上来说,David
需要使用两个修饰键。他想要一种没有人可能意外敲入的键组合。所以他选择了
Ctrl Alt 作为修饰键(比 Shift 用得少)和 Delete,此键位于键盘的另一端,所以敲击 Ctrl Alt Del
需要两只手,至少在过去是这样做的。
当今现代键盘在右边也有 Ctrl 和 Alt 键。重启特性的初衷是为 IBM
的人设计的秘密安全出口,但不可避免地,它已经成为一个不是秘密的秘密。一旦开发人员知道了它,他们便开始告诉客户使用这个特性来解决机器挂起问题。随着历史的发展,Ctrl Alt Del
被人们亲切地成为“三指敬礼”,即便是在当今的 Windows 中仍然具有生命力,用它调出任务管理器,以便你能杀死挂起的任务或终止系统(更多有关类似 Ctrl Alt Del
安全键序列的内容,参见本月的
Security Briefs 专栏)。那么,如果 Ctrl Alt Del 失败了怎么办?为什么会失败,请按住它保持 5 秒钟。
David Bradley 是当初建立 IBM 个人计算机的 12 个工程师之一。他编写了 ROM BIOS,有关 David 的简介,参见
David J. Bradley。
祝编程愉快!
您的提问和评论可发送到 Paul 的信箱:cppqa@microsoft.com