C++ 的“SMAT”子系统





5.00/5 (1投票)
一种出乎意料地简单的方法,可以避免 C++ 中 new/delete 分配的内存泄漏
Sensible Memory Allocation Tracking(智能内存分配跟踪)简介
在本文中,我将介绍“SMAT”子系统,“SMAT”是“Sensible Memory Allocation Tracking”的缩写。它仅包含一个必需的 C++ 源文件、一个必需的 C++ 头文件以及两个完全可选的 C++ 头文件。将前面提到的两个必需的 C++ 源文件添加到任何(相对较新)的 Visual C++ 项目中,基本上就可以自动跟踪(在该项目中)通过(全局)C++“new”和“delete”运算符进行的所有内存分配。
它通过为标准的 C++ “new”和“delete”运算符提供宏替换来实现这一点,您可以在进行内存分配时使用这些宏替换,以自动跟踪所有分配的内存。
用宏替换 C++ 中的 new 和 delete 运算符来跟踪内存分配的想法可能**不是**一个新想法(抱歉玩文字游戏)。然而,由于我还没有在互联网上看到任何可以做到这一点,同时又*快速易于安装和使用、不显眼、免费且可下载*的东西,所以我决定自己开发一个,并在此谦虚地展示。如果事实证明其他人已经开发了与我在这里展示的类似的东西(而且我并不知道他们没有),那么就称之为对那个(旧的?)想法的“回顾”。
安装和使用非常简单。只需将前面提到的两个必需的 C++ 源文件(“SMAT.cpp”和“SMAT.h”)包含到您拥有的任何 Visual C++ 项目中,然后在您的 DEBUG 版本中全局 #define 符号常量“USE_SMAT”来启用它,然后您就完成了(除了 new 和 delete 的替换,我稍后会讲到)。
该系统是独立的、用户可扩展的、线程安全的,并且对生产版本**没有**任何影响。我称之为“Sensible Memory Allocation Tracking”子系统(或简称“SMAT”),因为它基于一个非常简单的理念,即跟踪任何东西都是计算机非常擅长的事情,所以让您的计算机跟踪您自己的程序的内存分配在基本层面上非常有意义(即,它是“智能的”)。事实上,这个想法运作得非常好,如果使用得当,SMAT 子系统几乎可以**消除**在任何 Visual C++ 项目中因(使用)new 和 delete 运算符而产生的内存泄漏。
但这真的**仅此而已**。例如,它**不会**帮助解决通过使用 Microsoft 的 COM/DCOM 接口而产生的内存泄漏,因为 COM/DCOM 有其自己(非常具体)的内存分配规则、函数和方法,这些规则、函数和方法与 C++ 的 new 和 delete 运算符是分开且不同的。因此,它也**不会**帮助解决“智能指针”(它们并非真正是指针,而只是大而全的类)。
但是,尽管如此,您**可以**在 C 语言程序(而不是 C++)中使用此子系统来跟踪由 malloc() 等函数进行的分配,只需对 SMAT 的单个必需 C++ 头文件进行一些小的更改即可。我还没有这样做,实际上也不打算在本文中进一步探讨,但这确实不会太难。
一些背景视角
如您所知,当您(实际上是您的程序,但我们不必过于纠结于措辞)使用“new”运算符分配内存时,您会得到一个指向已分配内存的指针(一个真实的指针)。然后,您(通过指针)随心所欲地使用已分配的内存,直到不再需要它,这时您应该使用“delete”运算符将其释放。理论上这一切都很好。
该场景的问题在于,即使您非常擅长跟踪程序分配的所有零散内存,也没有任何东西或任何人会告诉您“漏掉了一个”——您未能释放一块或多块(或成千上万块)内存。内存本身不会抗议——它会毫不犹豫地认为您仍然需要它。程序也不会有任何异议——它只关心您是否使用了无效的指针。即使是操作系统也不在乎,除非您最终分配了所有可用内存,而这种情况几乎从未发生(尤其是在 64 位编译中)。
这时,SMAT 子系统就派上用场了。SMAT **会**告诉您是否未能释放已分配的内存块。或者两个。或者一百个。如果您尝试释放未分配的指针,它也会发出警报,但这更多是附加好处而非主要优势。
如何使用 SMAT
如前所述,SMAT 子系统为“new
”和“delete
”运算符提供了宏替换,这些宏替换(几乎)像运算符本身一样易于使用。具体而言,
TypeNme *Objct = new TypeNme; // <== becomes ==>
// TypeNme *Objct = NEW_SINGLE( TypeNme );
TypeNme *Array = new TypeNme[ Num ]; // <== becomes ==>
// TypeNme *Array = NEW_ARRAY( TypeNme, Num );
ClassNme *Klass = new ClassNme( p, q ); // <== becomes ==> ClassNme *Klass =
// NEW_OBJECT( ClassNme, ClassNme( p, q ) );
并且(假设上面的代码已执行)
delete Objct; // <== becomes ==> FREE_SINGLE( TypeNme, Objct );
delete [] Array; // <== becomes ==> FREE_ARRAY( TypeNme, Array );
delete Klass; // <== becomes ==> FREE_SINGLE( ClassNme, Klass );
从上面可以看出,使用 SMAT 宏而不是它们所替换的内容的主要区别在于,**释放**已分配内存的 SMAT 宏需要指定类型,而标准的 C++ delete 运算符则不需要。因此,当**释放**内存时,SMAT 宏实际上稍微难用一些。但我相信,使用 SMAT 的好处远远超过了这一点额外的麻烦。此外,在释放内存时必须指定内存类型,这实际上可能能提高程序读者和编写者的清晰度。
那么,使用这些宏(而不是相应的原生 C++ 运算符)如何有助于消除内存泄漏呢?
好问题。答案 contained in SMAT 的另一个宏,称为“CHK4LEFTOVERS
”。这个宏的作用类似于一个 C 函数,它接受一个参数,并返回 `void`(没有返回值)。这个宏的作用是生成一个文本文件,其中详细列出了您的程序分配但从未释放的所有内存块。该文本文件的内容看起来会像这样:
Memory allocated at line 396 in file, "C:\Team_A\Dev\Saucer\flight.cpp" was never freed.
Memory allocated at line 37 in file, "C:\Team_A\Dev\Saucer\rotation.cpp" was never freed.
Memory allocated at line 396 in file, "C:\Team_A\Dev\Saucer\flight.cpp" was never freed.
Memory allocated at line 3457 in file, "C:\Team_B\Dev\Saucer\escape.cpp" was never freed.
Memory allocated at line 157 in file, "C:\Team_B\Dev\Saucer\MOI.cpp" was never freed.
Memory allocated at line 3457 in file, "C:\Team_B\Dev\Saucer\escape.cpp" was never freed.
在上面的输出文本文件中(我们称之为“SMAT Report”,因为没有更好的名字),对于您的程序通过 new 运算符分配但从未通过 delete 运算符释放的每个指针,都会有一行文本。从那里,您可以查找分配每个指针的代码以进行进一步调查。
“CHK4LEFTOVERS
”宏接受一个参数:所需的 SMAT Report 的完整路径名(作为 C++ `TCHAR` 字符串),每次调用时它都会创建或覆盖该文件。但是,如果在调用“CHK4LEFTOVERS
”宏时所有分配的指针都已正确释放,则根本不会生成 SMAT Report(没有剩余指针,就没有报告)。
SMAT 子系统的 C++ 宏列表中最后一个(很可能也是最不重要的)是 `INIT_SMAT()` 宏。它的作用当然是初始化 SMAT 子系统。在使用任何其他 SMAT 宏**之前**必须调用它,并且它接受一个参数,该参数是指向用户定义的回调函数的函数指针。“ON_SCREW_UP
”函数指针的特定类型在 SMAT.h 文件中定义为:
typedef void (*ON_SCREW_UP)( const TCHAR *SrcFile, unsigned LineNum, void *Not_Allocated );
此函数指针参数可以在调用 `INIT_SMAT()` 宏时设置为 `NULL`。如果不是 `NULL`,当您的程序尝试释放从未分配过的指针时,SMAT 子系统将调用此参数指向的函数,这**总是**一个致命错误。因此,该函数本身不应返回。如果它返回了,在它返回后会立即生成一个异常。传递给此函数的第一个参数将指定“出错”发生在源代码的哪个位置,最后一个参数指定了被错误释放的未分配指针(通过该代码)。
如果您决定定义这样一个回调函数,请务必使其定义依赖于是否 #defined 了符号常量“USE_SMAT
”,否则您将最终在生产代码中出现一个多余的函数定义。考虑到链接器可能会将其优化掉,这并非灾难性的,但仍属不当。
更详细的使用说明
实现 SMAT - 步骤 1
总而言之,一旦您成功地将 SMAT 子系统的两个必需 C++ 源文件添加到您的 C++ 项目中(我假设您知道如何做),您的程序必须做的**第一**件事就是调用 `INIT_SMAT()` 宏。应尽早完成此操作,并且只需执行一次。**不要**调用多次。如果您下载了本文的演示项目,您将看到以下用于调用 `INIT_SMAT()` 宏的良好(但非最佳)示例:
#ifdef USE_SMAT // <== Yes, you should use this to conditionalize
// all of your SMAT supporting code:
/*------------------------------------------------------------------------*/
extern __declspec( noreturn ) void On_Bad_Free( const TCHAR *, unsigned, void * );
/*------------------------------------------------------------------------*/
static __declspec( noreturn ) void On_Bad_Free( const TCHAR *SrcFile, unsigned LineNum, void * )
{
static TCHAR BadFreeMsg[] =
_T("Attempt to delete an unallocated pointer at line %u in file, \"%s\".");
TCHAR ErrMsg[ Elems( BadFreeMsg ) + 1024 ];
_stprintf( ErrMsg, BadFreeMsg, LineNum, SrcFile );
GenError( ERR_KERNEL, ErrMsg );
}
#endif /* USE_SMAT */
/*------------------------------------------------------------------------*/
static bool InitApplication( HINSTANCE hInstance )
{
AppInst = hInstance;
AppDlg = GetDesktopWindow(); // <== ..until we have an App window,
// use the desktop's for error reporting..
if ( ERROR_HAPPENED ) return( false ); // <== Returns here after
// a fatal error..
if ( !INIT_SMAT( On_Bad_Free ) ) GenWinKernelErr(); // <== Initializes the SMAT
// (or does nothing).
return( true );
}
在上面的代码中,如果未 #defined “USE_SMAT
”符号常量,例如在您的所有生产代码中,那么 `INIT_SMAT()` 宏调用将解析为常量布尔值 true,因此整行代码将被优化掉。实际上,上面的示例并非最佳示例,因为它显然可以针对生产版本进行进一步优化,但这只是一个演示,因此在此特定实例中,清晰性优先于效率。
实现 SMAT - 步骤 2
在此之后,您要实现的 SMAT 子系统到您的 C++ 项目的**第二个**任务是决定程序将在何时何地调用前面提到的“CHK4LEFTOVERS
”宏。您可以在调用 `INIT_SMAT()` 宏后的任何时间调用它,但至少一开始最好的做法是将调用放在程序的最后一行代码,如下面的代码片段所示,它摘自本文的下载演示项目:
int APIENTRY _tWinMain( HINSTANCE hInstance, HINSTANCE, LPTSTR, int nCmdShow )
{
if ( InitApplication( hInstance ) ) MakeTopLvlDlg( IDD_Main_Dlg, MainDlg, nCmdShow );
CHK4LEFTOVERS( _T("C:\\EgSMAT.dbg") ); // <== Iff USE_SMAT is #defined,
// this line will generate a
return( 0 ); // text file listing all allocated memory
// that was NOT freed..
}
实现 SMAT - 步骤 3
最后但同样重要的是,您将必须用相应的 SMAT 子系统宏替换程序使用全局 C++ new 运算符进行的所有内存分配。如果正在分配数组,则应使用 `NEW_ARRAY()` 宏。如果不是,则应使用 `NEW_SINGLE()` 宏,除非它是类。如果正在分配类,则应使用 `NEW_OBJECT()` 宏。除非今天是星期二。开玩笑。
您还必须用相应的 SMAT 宏替换所有内存释放(即使用全局 C++ delete 运算符)。如果释放数组,则应使用 `FREE_ARRAY()` 宏,否则应使用 `FREE_SINGLE()` 宏。
此时,非常重要的一点是,尽管您实际上只需要替换您想要跟踪的内存分配(而不是全部),但您选择用 SMAT 宏跟踪的分配**必须**用 SMAT 宏释放,否则会导致错误结果。具体来说,如果您使用,例如,`NEW_SINGLE()` 宏分配某些内存,然后直接使用 C++ delete 运算符释放该已分配内存,则“CHK4LEFTOVERS
”宏将报告该已分配内存从未被释放。基本上,这是因为 SMAT 子系统的“NEW_
”宏系列会将新分配的指针添加到内部列表中,而 SMAT 子系统的“FREE_
”宏系列将从中删除该指针。事实上,SMAT 子系统就是这样区分“剩余”分配(即内存泄漏)的。
因此,最初,最好先在少数几个内存分配上“测试”SMAT 子系统,以熟悉它。随着您对 SMAT 宏的使用越来越熟练,您可能会想将它们用于越来越多的分配,最好最终将 SMAT 用于**所有** C++ 分配。该系统实际上就是为此设计的,这就是为什么它能够相对轻松地处理数万次内存分配。
工作原理
如前所述,SMAT 子系统的工作原理原则上极其简单。当请求内存分配时,SMAT 会分配内存,但在返回指针之前,它会将该指针的副本保存到一个内部已排序的(已分配指针的)列表中,以及一些关于分配在源代码中哪个位置的信息,我称之为分配的“元数据”。当请求释放已分配内存时,SMAT 会首先从其内部列表中删除指定的指针,然后释放内存。最后,当调用“CHK4LEFTOVERS
”宏时,该函数只需报告(在“元数据”中包含的)仍然存在于(即尚未从)其内部已排序的已分配指针列表中所有指针的上述“相关信息”。
好的,也许用英语阐述不是那么“极其简单”,但仍然如此。分配意味着添加到列表中,删除意味着从列表中移除,如果列表在调用“CHK4LEFTOVERS
”宏时不为空,那么就有内存泄漏,这些泄漏会在 SMAT Report 中报告。所以目标是运行您的 DEBUG 可执行文件直到完成,而不会生成任何 SMAT Reports。那么您的代码就可以称为没有内存泄漏(至少,所有 SMAT 跟踪的)。
实际上,SMAT 子系统维护着**两个**已分配指针列表,而不仅仅是一个——一个用于数组分配,另一个用于所有其他非数组分配(我简单地称之为“单个”分配,如“NEW_SINGLE()
”和“FREE_SINGLE()
”)。所有这些真正意味着的是,在分配和释放内存时,您必须知道您是在处理数组,这是 C++ new 和 delete 运算符本身已要求的必要信息,与它们不无关系。
内部,每个已分配指针列表都是一个红黑树,它本质上是一个负载均衡的二进制排序链表,经过优化以实现快速查找。从另一个角度看,您也可以将红黑树描述为一个完全独立的内存数据库内核,但我们暂时不要过于诗意。几乎所有维护红黑树结构的程序代码都来自公共领域(我稍作修改),但为此提供相同代码并使其至少听起来应该是可理解的功劳,则要归功于这个出色的 URL 资源,我强烈推荐给任何希望进一步研究红黑树机制的人。
列表本身对最终用户是隐藏的,事实上,如果您不想要额外的内存开销(尽管很小,但随便),列表不必是红黑树。如果您的项目全局 #defines 符号常量“NO_RED_BLACK
”,那么内部已分配指针列表将恢复为简单的单向链表,所有查找都将顺序进行(速度会慢得多)。
注意事项和扩展
与任何 API 一样,都会有注意事项。不要这样做:
if ( TCHAR *Ptr = NEW_ARRAY( TCHAR, 100 ) )
{
Use_The( Ptr );
FREE_SINGLE( TCHAR, Ptr );
}
如果您这样做,将调用前面提到的 `ON_SCREW_UP` 函数,您的程序将终止。
这实际上只是一个过于啰嗦的方式来指出 SMAT 宏的使用协议非常简单:如果您使用 `NEW_ARRAY()` 分配,您必须使用 `FREE_ARRAY()` 释放;如果您使用 `NEW_SINGLE()` 或 `NEW_OBJECT()` 分配,您必须使用 `FREE_SINGLE()` 释放——所以这也不是什么高深的学问。
另一个,有点更微妙的错误,SMAT 子系统新手可能会犯的错误,如以下代码片段所示:
if ( TCHAR *Ptr = NEW_ARRAY( TCHAR, 100 ) )
{
Use_The( Ptr );
FREE_ARRAY( TCHAR *, Ptr );
}
在上面的代码中,`FREE_ARRAY()` 宏调用中指定的类型是错误的。它指定的是正在释放的指针的类型,**而不是**正在释放的对象或对象的类型。传递给 SMAT 宏的所有类型参数都被假定为指定正在分配或释放的对象或对象的类型。由于 C++ new 运算符也是如此,所以这并不奇怪。
SMAT 扩展
如引言(上文)中所述,SMAT 子系统是“用户可扩展的”。这与其说是一个“特性”,不如说是一个绝对的必要性。请允许我解释。
C++ 编程中普遍使用结构和类来组织数据,其中大部分内存通常是在运行时分配的。这些极其常见的内存块经常包含指向其他已分配内存块(其他结构)的指针,而这些内存块又可以包含指向更多已分配内存块的指针,依此类推。关键在于,分配一个结构或类几乎不是一个简单地分配一块内存的问题。这就是为什么有能力的 C++ 程序员通常会创建一个类构造函数(用于类)或一个独立函数(用于结构),来完成分配某个特定类或结构所需的所有(或大部分)零散内存块的繁重工作。
现在,假设您的程序经常通过使用一个非常优雅的“分配函数”和一个同样优雅的“释放函数”来分配(和释放)数千个此类结构(或类)。当程序完成时,您想知道是否所有用您的“分配函数”分配的结构都已使用您的“释放函数”释放。如果您漏掉了一个,您想知道它是在您的程序代码的哪个位置分配的,以便您可以更密切地跟踪它的生命周期。很合理。
但是没有 SMAT 扩展,您就无法做到这一点。例如,如果结构包含二十个指向已分配内存的指针,所有这些指针都使用 SMAT 宏分配,并且您的程序未能释放其中一个或多个结构,那么 SMAT Report 将为每个未释放的结构(假设结构本身已分配)报告**二十一个**内存泄漏,这不是您想要的。更糟糕的是,所有 21 个内存泄漏(每个结构)都将由 SMAT 报告为源自您(非常优雅的)“分配函数”**内部**,**而不是**源自**调用**该函数的程序代码,而这正是您真正想要的。
要解决这个问题,您必须使用 SMAT 扩展。根本上,SMAT 扩展是一个您定义(或已定义)的函数,它分配一个或多个内存块(使用 SMAT 宏),这与前面讨论的“分配函数”类似。但是,在声明、定义和调用 SMAT 扩展时必须使用特殊的 SMAT 宏,因为它必须在调试版本和发布版本中都能工作(假设您的调试版本 ` #define "USE_SMAT"` 而您的发布版本不这样做)。
SMAT 扩展可以返回您想要的任何内容,并接受任意数量的参数。然而,不带参数的 SMAT 扩展比带参数的扩展具有不同的协议。所以可以说,从使用角度来看,SMAT 扩展有两种类型:带参数的和不带参数的。
声明 SMAT 扩展
假设您已经有一个不带参数的现有分配函数,其声明如下:
extern SUM_BLK_TYPE *New_SumBlk( void );
如果您想将此函数变成 SMAT 扩展,则需要将声明更改为:
extern SUM_BLK_TYPE *New_SumBlk( SMX_VOID );
另一方面,如果您的现有分配函数**确实**带有参数,例如:
extern SUM_BLK_TYPE *New_SumBlk( CNTRL *, unsigned );
那么声明将需要更改为:
extern SUM_BLK_TYPE *New_SumBlk( SMX_PARAMS( CNTRL *, unsigned ) );
所以规则是:如果函数声明有一个非 void 的形式参数列表,那么它们**都**必须( unaltered)传递给“SMX_PARAMS
”宏,如上面的示例所示。这包括可变参数函数。否则,必须使用“SMX_VOID
”宏。
定义 SMAT 扩展
无论您的 SMAT 扩展是否带任何参数,其定义都必须**紧接**在以下代码之前:
#include "Inherit_On.h"
这告诉 SMAT 子系统,在以下 SMAT 扩展定义中发出的所有 SMAT 分配宏和扩展调用将“继承”分配元数据,该元数据指定了调用 SMAT 扩展的源代码,**而不是**调用 SMAT 分配宏或扩展的源代码(这是默认行为)。如果这听起来有点令人困惑,请不要担心;您不必知道它是如何工作的就能使用它。
此外,在 SMAT 扩展**正文****之后**,必须发出以下预处理器指令:
#include "Inherit_Off.h"
这当然会撤销“Inherit_On.h”头文件所做的任何操作,从而将所有 SMAT 分配宏恢复到其默认行为。它可以包含在单个 SMAT 扩展定义之后,或包含在**一组** SMAT 扩展定义之后,只要它们都连续定义在模块中,并且前面有包含“Inherit_On.h”头文件的指令。
一旦解决了这个问题,您仍然需要(稍微)更改每个 SMAT 定义的声明部分,具体细节如下。
假设您已经有一个不带参数的现有分配函数,其定义的声明部分(即其“头部”)是:
SUM_BLK_TYPE *New_SumBlk()
如果您想将此函数变成 SMAT 扩展,则需要将定义更改为:
SUM_BLK_TYPE *New_SumBlk( SMX_META )
另一方面,如果您的现有分配函数**确实**带有参数,例如:
SUM_BLK_TYPE *New_SumBlk( CNTRL *Cntrl, unsigned NumItems )
那么定义“头部”将需要更改为:
SUM_BLK_TYPE *New_SumBlk( SMX_PARAMS( CNTRL *Cntrl, unsigned NumItems ) )
所以这里的规则与前面提到的 SMAT 扩展**声明**的规则几乎相同,只是不带参数的函数必须使用“SMX_META
”宏,而不是声明中使用的“SMX_VOID
”宏。
调用 SMAT 扩展
假设前面示例的 SMAT 扩展已在范围内声明并定义,您将在某个时候调用它。
如果我们的前面示例的 SMAT 扩展不带任何参数,对其的调用将如下所示:
SUM_BLK_TYPE *Gimme = New_SumBlk( SMX_XTEND );
或者,如果我们的示例 SMAT 扩展**确实**带参数,则调用需要如下所示:
SUM_BLK_TYPE *Gimme = New_SumBlk( SMX_PASS( &MyCntrl, 42 ) );
所以长话短说是,如果 SMAT 扩展带有参数,那么它们都必须( verbatim)传递给 `SMX_PASS()` 宏,然后将其输出传递给 SMAT 扩展。如果 SMAT 扩展不带参数,则必须使用 `SMX_XTEND` 宏,如上所示。
关于 SMAT 扩展的最后说明
尽管将一个或多个自己的分配函数转换为“SMAT 扩展”的机制乍一看可能比它带来的好处要麻烦,但实际上并非如此。这是因为,一旦转换完成,您将拥有一个分配函数,它:
- 将在您的生产版本中 unchanged 执行,并且
- 将被 SMAT 子系统视为低级“块分配器”,就像其自身的 `NEW_SINGLE()` 和 `NEW_ARRAY()` 宏一样,因此
- 将正确地将分配元数据设置为**调用**该函数的程序代码,如果这些分配从未被释放,这是您需要知道的信息。
如果您想看到一个真正优秀的实际工作中的 SMAT 扩展示例,本文的下载演示项目包含两个,它们都是可变参数的。
第一个称为“MkeNewStrng
”,它分配并返回一个 `TCHAR` 字符串,该字符串包含所有传递给它的 `TCHAR` 字符串的连接,最后一个(必须**始终**是 `NULL`)除外。第二个称为“EssPrintEff
”,它基本上实现了 `_sctprintf()` 函数的功能,但还分配并返回一个指向格式化结果的指针。
另一方面(我有好多只手),完全有可能在**不**利用 SMAT 的任何扩展功能的情况下,从 SMAT 子系统中获得很多收益。由您选择。
软件包内容
整个子系统包含在四个(4)个 MSVS 兼容的 C++ 源文件中,其中两个很小,只有在创建 SMAT 扩展函数(参见上文)时才需要。这四个文件是:
- SMAT.h - 如果您使用 `stdafx.h`,请将其添加到其中,否则添加到任何需要它的地方。
- SMAT.cpp - 此模块中的所有源代码都依赖于符号常量“
USE_SMAT
”,因此如果未 #defined,实际上将生成任何内容。 - Inherit_On.h - 非常小的单用途文件,其唯一用途如 **定义 SMAT 扩展** 部分(上文)所述。
- Inherit_Off.h - 同上。
列表中的后两个文件仅存在,因为我无法弄清楚如何创建一个可以调用预处理器指令的宏。我相信我会被某个预处理器专家痛骂,他们会说:“你这个白痴,你本来可以用 Blah dee Blah 来做这件事!!”。随便吧。乐于接受建议。
最终想法
当我最初想到写这篇文章,并随后意识到提供一个“演示”项目可能是一件明智的事情时,我就知道我不想提供一个除此之外毫无用处的项目。所以本文的下载演示项目,我称之为“EgSMAT
”,实际上做了一些 quasi-useful 的事情:它列出了您系统中任何可访问驱动器上的所有文件,这些文件无法通过常规方法打开读取。结果通常……有点意思。
最后,在我使用这个调试子系统(以各种形式)超过十几年之后,我对其实用性深信不疑,所以我决定与世界分享它,这样我就可以变得富有和出名,并在 Yvonne Strahovski 旁边买房子。不过我得承认,这可能不会……一夜之间实现……;>
历史
- 2020年4月5日:初始版本