分类: C/C++
2008-03-17 11:16:24
未引用参数
添加任务栏命令
针对没有使用过的参数用 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,因为他们发给我的是相同的链接:《今日美国》上的一篇文章:“”,我在一月发问之前就发现了这篇文章。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 的简介,参见 。
祝编程愉快!
您的提问和评论可发送到 Paul 的信箱: