接手一个十年资历的MFC桌面项目时,我几乎不需要读太多注释。m_bIsRunning、lpszCmdLine、dwStyle、hWndMain——这些挂在变量名上的前缀,像一套约定俗成的暗号,把类型、作用域、生命周期一次说清楚。后来给某跨平台系统做代码评审,发现新模块里的变量命名五花八门:有人用完整匈牙利式前缀,有人只写CamelCase,还有人把m_加到局部变量上。这不是洁癖问题,MFC这套命名前缀是在没有现代IDE智能提示的年代撑起大型工程可读性的关键。今天虽然工具强大了,但它依然是接手老工程、维护跨时代代码的必修课。这篇把C++和MFC里常见的前缀系统整理一遍,分成类型前缀、作用域前缀、Windows特有约定三大类,配合对照表和实操坑点,给准备碰MFC源码的人一份可以直接参考的速查手册。
1. 不贴标签的代码,是怎么把人坑哭的
1.1 一个变量名引发的半小时排查
我之前维护过一套老的图形处理Demo,里面有一段逻辑大概长这样:
void CImageView::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent == TIMER_ID_PREVIEW) { BOOL bReady = CheckBufferReady(); if (bReady) { m_bImageUpdated = TRUE; RedrawWindow(); } } CDialog::OnTimer(nIDEvent); }乍看没什么问题。但有一次我重构CheckBufferReady()内部实现,把原来直接返回BOOL的逻辑改成了先调用一个内部函数,局部变量没写前缀:
BOOL CImageView::CheckBufferReady() { BOOL bBufferReady = FALSE; bBufferReady = DoSomething(); // 这里本意是给成员变量 m_bImageUpdated 赋值 return bBufferReady; }如果此时类里已经有一个成员变量叫bBufferReady,那么上面这段代码里bBufferReady指向的是谁?C++的作用域规则会优先匹配局部变量,于是成员变量被完全遮蔽,返回值永远是FALSE。当时界面图像不定期丢失,查了半个多小时,最后是盯着局部变量列表才发现的。如果调用方严格按照m_前缀区分成员变量,这个乌龙从一开始就不会出现。
这件事给我的教训很直接:前缀不是装饰,它是代码阅读时的“路标”。它能让你在肉眼扫代码的瞬间判断出一个变量是局部的、成员的、全局的还是指针类型的,从而直接跳过很多不必要的上下文切换。
1.2 前缀产生的历史原因:C语言不查类型
现在IDE里鼠标悬停上去就能看到类型,但MFC时代之初并没有这种便利。C语言本身是弱类型检查语言,编译器面对int和long*互相赋值的警告相当温和,甚至某些老式编译器直接睁一只眼闭一只眼。而Windows SDK提供给开发者的数据结构,到处都是HWND、HINSTANCE、LPSTR、DWORD这类别名,它们底层大多是指针或整数,光看类型名的字面意思根本猜不出变量用途。
再加上当时的屏幕分辨率低、代码编辑器没有高亮、不能折叠函数,程序员只能通过命名来传达“这个变量是什么”。匈牙利命名法因此流行起来:把变量的类型信息编码进名字里,让代码变成自描述的文档。MFC继承了这套习惯,并加入了类成员前缀m_,形成了我们现在看到的经典风格。
这套体系在Windows桌面开发社区里被认可了几十年,不是因为它美观,而是因为它能直接降低沟通成本。你看到hWndParent,就知道这是个窗口句柄;看到nCount,就知道是个数值;看到lpszPath,就知道是个以零结尾的长指针字符串。对一个文件动刀之前,名称本身就能先把类型安全的第一道防线立起来。
2. 变量前缀速查表:从基础类型到句柄的一站式对照
前缀种类看起来很多,但真正高频出现的其实可以分成三组:基础类型、指针与句柄、作用域。我把常用前缀整理成表,后面再逐个解释关键点。
2.1 基础类型前缀:整型、浮点与字符
| 前缀 | 对应类型 | 典型示例 | 说明 |
|---|---|---|---|
b | BOOL/bool | bEnable、bRunning | 布尔值。老MFC代码里较多指Windows API的BOOL,即int类型 |
by | BYTE | byFlag | 无符号单字节,常用于二进制数据 |
c/ch | char | cKey、chInput | 单个ASCII字符。ch比c更不容易和const缩写混淆 |
n | int、UINT、short | nCount、nIndex | 数值的通用缩写,MFC中最常见的整型前缀 |
i | int | iLoop | 与n混用,跨平台C++里更多见,MFC里相对少 |
f | float | fRatio | 单精度浮点 |
d | double | dRadius | 双精度浮点 |
l | LONG | lOffset | 32位长整型。注意小写l容易和数字1混淆,变量名里建议配合大写字母开头含义段 |
dw | DWORD | dwStyle | 32位无符号整型。Windows API返回样式的经典前缀 |
w | WORD | wParam | 16位无符号整型,也用于Windows消息参数名 |
hr | HRESULT | hrResult | COM/ActiveX错误码类型 |
sz | char[]零结尾字符串 | szName | String Zero的首字母缩写,表示字符数组,而不是指针 |
str | CString或std::string | strPath | MFC的CString对象,注意和sz区分 |
n和i没有本质区别,但在MFC工程里混用会显得杂乱。我的建议是:如果代码库以MFC对话框和文档视图结构为主,统一用n;如果是跨平台C++模块,可以用i。关键不是哪个更标准,而是整个团队保持一致。
sz和str的差异很多人一开始容易忽视。char szPath[MAX_PATH]是数组,可以直接被CString::Format、lstrcpy等函数操作;CString strPath是对象,支持+=、==等重载运算。老编码习惯里,它们都表示字符串,但底层模型完全不同,命名时就要区分开。
2.2 指针、句柄与字符串指针前缀
| 前缀 | 对应类型 | 典型示例 | 说明 |
|---|---|---|---|
p | 任意指针 | pData、pNode | 泛用指针前缀,指向结构体、类实例、数组都可以 |
lp | 长指针 | lpCmdLine | 在32位内存模型出现之前,长指针和短指针区别很大;现在lp与p已无本质区别,更多是历史习惯 |
lpsz | 指向零结尾字符串的指针 | lpszCmdLine | Long Pointer String Zero。最常见于命令行、文件路径等 |
lptstr | 指向TCHAR字符串的指针 | lptstrText | 可移植字符串指针,编译为ANSI时是char*,编译为Unicode时是wchar_t* |
h | 句柄 | hWnd、hInstance、hMenu | Windows句柄,本质是内核对象标识,通常是指针大小的整数 |
pfn | 函数指针 | pfnCallback | Pointer to Function,回调函数用 |
这里有个容易踩的坑:到了32位以后,lp和p已经没有实际区别,很多新项目干脆统一用p。但老MFC源码里大量存在lpsz、lpfn这类写法,如果你要接手老代码,不建议一边改一边骂,让自己看懂才是优先任务。
hWnd这类句柄,最容易出的问题是被当成普通整数存储。句柄在某些32位环境下可以是内核对象的表索引,在64位环境下则是指针宽度,如果你把它塞进int再转回来,很可能在64位下丢高32位。命名前缀在这里起的作用就是提醒你别干这种蠢事。
2.3 作用域前缀:m_、s_、g_和它们的分工
| 前缀 | 作用域 | 典型示例 | 说明 |
|---|---|---|---|
m_ | 类的成员变量 | m_nID、m_strName | Member,MFC类向导生成的标准前缀 |
s_ | 静态成员变量 | s_pInstance | Static,单例、共享缓存常见 |
g_ | 全局变量 | g_hInstance | Global,项目级共享变量 |
k | 常量 | kMaxRetry | Constant,现代C++风格,MFC老代码里少见但好用 |
m_大概是MFC给C++社区留下的最大习惯遗产。它挂在变量名最前面,一眼就能看出这是对象内部状态,而且能够有效避免局部变量遮蔽成员变量的问题。我见过很多非MFC的现代C++项目也沿用m_,原因就是这一点太实用了。
g_在MFC工程里比在通用C++工程里更少见,因为MFC本身把很多“全局”状态封装到了CWinApp等类实例中,你不太需要裸全局变量。但老式DLL或者独立模块里,依然会看到g_hInstance之类的写法,意思是程序实例句柄在整个进程里共享。
3. MFC与Windows API专属的那套约定
除了通用C++习惯,MFC还有几套特有的前缀和命名模式。不了解这套约定,看MFC头文件就像看天书。
3.1 类名、Afx、On:框架层的命名符号
MFC的类名普遍以C开头:CWinApp、CDialog、CView、CString、CFile。用户自己从MFC向导生成的类,也会在类名的C后面跟模块名,比如CImagePreviewDlg。这不是强制语法,但是一种体系性的标识:看到C开头你就知道这是个MFC类,它有消息映射、有DoDataExchange之类的框架方法。
afx前缀更微妙。AfxMessageBox、AfxGetApp这类全局函数,以及消息映射里的afx_msg,前缀名取自“Application Framework Extension”。这是MFC框架内部的保留命名空间,用来表示“这个函数由框架提供,不归用户业务模块管”。自己写代码时不要随便以afx开头,否则很容易让下一个维护者误以为你在调用框架接口。
On前缀专属于消息处理函数。OnPaint、OnTimer、OnBnClickedOk、OnSize,统统表示“某个事件触发时回调”。MFC消息映射宏ON_MESSAGE、ON_COMMAND就是靠这个统一约定把消息ID桥接到成员函数上的。如果你看到On开头的函数且类里没有显式调用它的地方,不用奇怪,它不是没人调用,而是被消息映射表在事件来临时间接触发了。
3.2 消息参数和常用控件变量的命名套路
Windows消息机制中,wParam和lParam是参数名字本身,而不是变量命名前缀。它们的类型分别是WPARAM和LPARAM,在32位下都是4字节,在64位下会变成8字节。MFC代码里经常能看到把lParam强转成结构体指针的写法:
case WM_NOTIFY: NMHDR* pNMHDR = (NMHDR*)lParam;这里的lParam名字是系统给的,不需要加m_或p前缀,但本地接收变量pNMHDR加了p,用起来就很顺。
MFC控件相关变量也有一套心照不宣的缩写:point取pt,rect取rc,size取sz,color取clr。例如CPoint ptStart、CRect rcClient、CSize szParent、COLORREF clrBack。这些简写没有写进任何标准,但从控件坐标到绘图接口的衔接非常常见。你如果在老代码里看到rc、pt,基本可以默认它们和矩形、点相关。
3.3 两个最容易被新代码带偏的地方
第一,BOOL和bool不能混用。MFC老代码里的BOOL本质是int,占4字节;C++原生bool占1字节。跨模块传参时,如果一个函数用BOOL返回,另一个用bool接收,在特定编译器下可能截断高字节,也可能因符号扩展造成判断异常。命名上用b前缀没有争议,但要保持类型自身一致。我建议新写的模块内部用bool,和Windows API或MFC框架交互的边界用BOOL,两者之间显式转换,别靠隐式。
第二,lpsz并不一定是char*。在非Unicode编译环境下LPCTSTR是const char*,但在Unicode环境下它是const wchar_t*。老代码中用lpsz命名的变量,配合CString的运算符重载,经常能自动完成编码转换,但如果你把它直接传给一个只接受const char*的函数,编译会报错。讨论“为什么名字叫psz却不是char*”没有意义,正确的做法是:想要ANSI宽字符的语义,就写成LPSTR并配上sz后缀;想要可移植传参,就用LPTSTR。
4. 命名纪律的现代边界:哪些前缀值得坚持,哪些可以放手
讲完所有前缀之后,真正要面对的问题是:今天写新的C++代码,我们还要不要继续这套MFC命名法?
我的答案分三层。
第一层,和MFC框架直接打交道的代码,必须尊重它。消息处理函数保留On前缀,类的成员变量保留m_,Windows API返回的句柄变量用h开头。原因很简单:MFC的宏、类向导、调试器输出都围绕这套命名组织,你脱离它,只是给自己添麻烦。
第二层,纯C++算法和业务逻辑,可以适度放松。比如局部变量用i或j做循环计数,而不必叫nLoopIndex;std::string对象直接叫path或filePath,不会因为少了str前缀就看不懂。现代IDE的悬停提示能快速显示类型,强类型语言本身也保证了大部分错误会被编译器拦截,所以“每个变量都贴标签”已经没有那么必要了。
第三层,也是我最想强调的:一致性比前缀本身重要得多。一个工程里如果一半人写nCount、另一半人写count,代码评审时浪费的注意力远大于前缀带来的一点点阅读成本。MFC老代码的“标准”之所以可贵,不在于每个字母都有绝对正确的答案,而在于它是很多人共同维护了二十年的公共约定。
真正应该被淘汰的,是一些“伪前缀”——为了显得专业而发明的私人标记。比如给所有局部变量加l_,或者把所有类名都加C却不遵循MFC的继承体系,再比如把m_和p不加区分地堆起来写个m_pstrName。这种命名看似信息量大,实则让维护者在脑子里做多重解码,不如干脆拆成短小清晰的变量。
我自己处理老项目时的做法是:头一个月不修改任何旧命名,新写的代码严格遵守项目原有风格,哪怕是让我觉得老气的lpsz也照用。等真正理解了模块边界之后,再在有把握的地方逐步统一。命名前缀不是什么玄学,就是一群程序员为了少动脑子而定的规矩,你入乡随俗,它就能帮上忙;你叛逆到底,它就变成处处给你使绊子的暗雷。
最后给一个小技巧:如果你想快速摸清一个陌生MFC工程的模块职责,不用读业务逻辑,先全局搜索m_开头最多的文件。一个窗口类如果成员变量集中在m_h和m_str,说明它主要管理句柄和字符串资源;如果大量m_b和m_n,说明它以状态数值为主。这套前缀在你正式阅读代码前,已经把类大概是什么样剧透给你了。这就是它们真正的价值。