2007年9月5日星期三

VC编译器高级编译技巧

首先,转载一下关于内存泄漏检测的相关技巧,参见:最快速度找到内存泄漏
其主要内容摘录如下:

inline void EnableMemLeakCheck()
{
_CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG)
| _CRTDBG_LEAK_CHECK_DF);
}

#ifdef _DEBUG
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endifvoid main()
{
EnableMemLeakCheck();
_CrtSetBreakAlloc(
52);
int* leak = new int[10];
}

原来这里从定义了new,恩,真的很好。可是,我又使用了他的内存组件AutoFreeAlloc(参见C++内存管理变革(3):另类内存管理AutoFreeAlloc规格AutoFreeAlloc细节)。这个组件也是真的很好,真的很好。可是里面对于new,也有类似“重定义”的做法。代码如下:

#define STD_NEW(alloc, Type) \

::new((alloc).allocate(sizeof(Type), \

std::DestructorTraits::destruct)) Type
如果把这里的new替换成我们的new(_NORMAL_BLOCK, __FILE__, __LINE__)的话,在有的情况下会产生编译错误。换句话说,两者不能共存。

这叫什么问题??貌似是宏定义之间的冲突?好像又不是。网上找了老半天,看到一段本地new与托管new共存的代码,参见
经典与现代的结合:在MFC中集成RAD.NET框架
主要内容如下:
  在MFC非托管类中定义托管成员变量

  在MFC类中使用托管对象,提供对象的声明和初始化方法与传统的方法略有不同。以在文档类CtestDoc中添加一个托管成员变量为例,声明托管对象的代码如下:

以下是引用片段:
public:
gcroot m_ptestDocObj;

  gcroot类型安全包装模板可以将托管参考类型指针作为成员变量嵌入到非托管类中,该变量就可以像其他类型的变量一样使用了。在CtestDoc的成员函数InitialDocument中创建这个对象,代码如下:

以下是引用片段:
BOOL CtestDoc::InitialDocument()
{
#pragma push_macro("new")
#undef new
m_ptestDocObj = new test::testDocObject();
#pragma pop_macro("new")
}

  由于testDocObject是一个托管参考类型,它总被分配在CLR堆上,所以自然不能使用在afx.h中定义的new操作符来直接初始化该对象以避免该托管对象在非托管的本地C++堆上创建导致的错误。在托管对象中声明MFC对象,与常规方法一致。


原来如此,VC支持对宏定义的push与pop啊。尝试在用到STD_NEW的地方,就把原来的new定义push起来,undef。然后再把其pop出来。这样,果然编译的很顺利了。

这里还有微软关于pop的文档 pop_macro
主要内容摘录如下:
pop_macro
#pragma pop_macro("macro_name")

Sets the value of the macro_name macro to the value on the top of the stack for this macro. You must first issue a push_macro for macro_name before you can do a pop_macro.

Example

// pragma_directives_pop_macro.cpp
// compile with: /W1
#include
#define X 1
#define Y 2

int main() {
printf("%d",X);
printf("\n%d",Y);
#define Y 3 // C4005
#pragma push_macro("Y")
#pragma push_macro("X")
printf("\n%d",X);
#define X 2 // C4005
printf("\n%d",X);
#pragma pop_macro("X")
printf("\n%d",X);
#pragma pop_macro("Y")
printf("\n%d",Y);
}

Program Output

1
2
1
2
1
3
    对了,我了解那个pop还是从这里了解到的:C++ Q&A 专栏... 性能监视,托管扩展,和锁定工具栏
 我有一个用 C++ 写的库并且我
正在尝试用托管扩展将我的某些类暴露给 Microsoft .NET 框架。这时编译器报错 C3828:“placement arguments
not allowed while creating instances of managed classes”。我必须要在我的库中插入下面的代码
才能解决这个问题: #pragma push_macro("new")
#undef new
// managed stuff here
#pragma pop_macro("new")
  但我始终不明白为什么要这样做,因为我的其它模块不需要它们。此外,敲入 pragmas 相当不方便。有没有什么更好的方法使我不必总是要敲入 push_macro
和 pop_macro?

Jordie Marslan

这个问题与 C++ 无关,它与 MFC 有关。C++ 的一个 比较晦涩难懂的特点是你可以重载 new 操作符,并且你甚至可以给它附加参数。通常,操作符 new 只接受拟分配对象的大小:

void* operator new(size_t nAlloc)
{
return malloc(nAlloc);
}   但你也可以随心所欲附加参数来重载 new 操作符,只要在调用 new 时候提供这些参数即可。在各种应用程序向导(App Wizards)中,这 是 MFC 所做的事情。一个典型的 MFC 程序(.cpp)文件顶部都有下面这样的代码行,通常都由应用程序向导生成: #ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif MFC 将 new 重定义为 DEBUG_NEW。但 DEBUG_NEW 是什么? afx.h 道出了原委: // (simplified)
#ifdef _DEBUG
# define DEBUG_NEW new(THIS_FILE, __LINE__)
#else
# define DEBUG_NEW new
#endif 在 debug 生成模式中,MFC 重载了操作符 new 以获取两个额外的参数,比如: void* operator new(size_t nSize,
LPCSTR lpszFileName, int nLine);   重载的版本与普通的 new 同样都有表示对象大小的 size 参数,但还增加了两个参数:源文件名称和行数。因此,无论何时,只要你写: pfoo = new CFoo(..); 预处理程序便会将它转变为: pfoo = new (sizeof(CFoo), THIS_FILE, __LINE__) CFoo(...);   __FILE__(用来初始化 THIS_FILE)和 __LINE__ 是专用的预处理符号,它保存当前被编译的模块文件名称和行数。 其主要用途是当你的应用程序泄漏时,MFC 能显示一个消息。如: Shame on you! You didn''t free the CFoo object in foo.cpp, line 127!   这对于调试来说,是个巨大的福音,但是当你使用托管C++时,它会导致混乱,因为公共语言运行时(CLR)用于托管对象的 new 操作符并不能理解 这些额外的参数(placement arguments)。但是 MFC 用 #define 重定义了 new,这导致预处理程序做了一个直接的词汇替换。当托管扩展看到了 额外的参数,它们便会歇斯底里。这就是为什么你必须用 #pragmas push_macro/pop_macro 来临时反定义 MFC 已经定义的东西, 然后再次重定义它以恢复到 MFC 中。
  对于这个问题,我能想到的唯一的解决办法就是删除 MFC 对 new 的重定义,同时用某个称为mfcnew 的东西来替代它,如 Figure 2 所示。 其缺点是当分配常规 C++ 对象时,你必须记住使用 mfcnew 类型,而分配托管对象时使用常规的 new 声明,如下所示: pfoo = mfcnew CFoo(...);
CManagedClass *pmc = new CManagedClass();   如果你忘记使用 mfcnew,你便无法获得自动的内存泄漏报告。如果你认为这样是相当麻烦的(我就是这么觉得),那么你会很高兴知道这些问题将在 Visual C++® 2005中得到修正。这正相反:它有一个 gcnew 操作符,你必须用它来分配托管对象。这样就不可能再与普通操作符 new 相冲突了。 以我之见,这是一个更好的方案,因为强制程序员了解何时分配与本地堆对象相对的托管对象是有好处的。此外,它也使得将来在托管堆上分配本地对象成为可能,反之亦然,通过 自动创建相应的使用 GCHandle 或其它必需的代理类。
  你可能想知道为什么这个额外的参数被称为“placement arguments”,这是因为最初的目的是允许你在内存的特定位置分配对象。例如,你可能创建一个操作符 new(size_t size, void* p),它只返回被传递的指针 void* operator new(size_t size, void* p)
{
return p;
} 这时你可以调用 new(p) CFoo ; 如果你想要在 p 位置创建 CFoo 对象,而不是让 malloc 为你分配内存,就可以使用一个指针 p。
  无论如何,你是对的,使用 push/pop_macro 是个痛苦,而且它还使你的代码看起来丑陋。眼下并没有太多的解决办法——尽管另外一个更加简单的方案 是将所有的托管代码和本地代码隔离开来,放到单独的源文件中去并从托管模块中删除 DEBUG_NEW 部分。如果你的代码已经紧密混合时——换句话说,如果你从相同的函数或代码 块创建托管和非托管对象时,这个方法将不再可行。

补记:文章我写到一半的时候,我发现,原来Winx的作者已经给出解决办法了,都怪我当时看的太快,忽略了而已。。。
参见WINX的STD_NEW与MFC共存问题
作者给出办法大致如下:

解决方案

我推荐的解决方案是,删除MFC在源代码文件中的调试代码中的如下语句:

#define new DEBUG_NEW

不过这带来另一个问题,在该源代码文件中发生的内存泄漏,MFC程序无法检测到了。

要解决这个问题也很简单,如果要用到 new 的地方,直接用 DEBUG_NEW 代替好了。例如:

Type* a = new Type(arg1, arg2);

改为

Type* a = DEBUG_NEW Type(arg1, arg2);

个人推荐

我个人的推荐是,不只是在要使用 WINX 的 STD_NEW 时候这样做,而是,所有代码中都使用DEBUG_NEW,而不是直接使用new。



OK,全文完。

没有评论: