在C ++中编写无memleak的代码对我来说不是问题,我只是保持RAII成语。
在C#中编写无memleak的代码也不是很难,垃圾收集器会处理它。
不幸的是,编写C ++ / CLI代码对我来说是个问题。我以为我已经理解它是如何工作的,但我仍然有很大的问题,我希望你能给我一些提示。
这就是我所拥有的:
用C#编写的Windows服务,内部使用C ++库(例如OpenCV)。使用C ++ / CLI包装类访问C ++类。
例如,我有一个MatW
C ++ / CLI包装类用于cv::Mat
图像对象,它将构造函数参数作为System::Drawing::Bitmap
:
public ref class MatW
{
public:
MatW(System::Drawing::Bitmap ^bmpimg)
{
cv::Size imgsize(bmpimg->Width, bmpimg->Height);
nativeMat = new Mat(imgsize, CV_8UC3);
// code to copy data from Bitmap to Mat
// ...
}
~MatW()
{
delete nativeMat;
}
cv::Mat* ptr() { return nativeMat; }
private:
cv::Mat *nativeMat;
};
另一个C ++类可能是例如
class PeopleDetector
{
public:
void detect(const cv::Mat &img, std::vector<std::string> &people);
}
它的包装类:
public ref class PeopleDetectorW
{
public:
PeopleDetectorW() { nativePeopleDetector = new PeopleDetector(); }
~PeopleDetectorW() { delete nativePeopleDetector; }
System::Collections::Generic::List<System::String^>^ detect(MatW^ img)
{
std::vector<std::string> people;
nativePeopleDetector->detect(*img->ptr(), people);
System::Collections::Generic::List<System::String^>^ peopleList = gcnew System::Collections::Generic::List<System::String^>();
for (std::vector<std::string>::iterator it = people.begin(); it != people.end(); ++it)
{
System::String^ p = gcnew System::String(it->c_str());
peopleList->Add(p);
}
return peopleList;
}
以下是我的Windows Service C#类中对该方法的调用:
Bitmap bmpimg = ...
using (MatW img = new MatW(bmpimg))
{
using (PeopleDetectorW peopleDetector = new PeopleDetector())
{
List<string> people = peopleDetector.detect(img);
}
}
现在,这是我的问题:
using
吗?当使用多个包装器对象时,它会使代码变得丑陋,因为using
语句必须嵌套Dispose()
吗?using
,没有Dispose()
)List<string^>^
之类的对象从C ++ / CLI返回到C#的正确方法吗?gcnew
并不意味着垃圾收集器会处理这些对象而我不必关心如何以及何时释放它们?我知道这是很多问题,但我想要的只是摆脱我的内存泄漏,所以我列出了我认为可能出错的所有内容......
答案 0 :(得分:5)
我的代码有什么问题吗?
不是您发布的内容 - 您正确应用using
语句。因此,您的代码示例不是导致内存泄漏的原因。
我必须在我的C#代码中使用吗?当使用多个包装器对象时,它会使代码变得丑陋,因为using语句必须嵌套
你不必,但你不必在语法上嵌套它们。这相当于:
Bitmap bmpimg = ...
using (MatW img = new MatW(bmpimg))
using (PeopleDetectorW peopleDetector = new PeopleDetector())
{
List<string> people = peopleDetector.detect(img);
}
我可以在使用对象后使用Dispose()吗?
你可以,但是你需要try
/ finally
来确保始终调用Dispose
,即使抛出异常也是如此。 using
语句封装了整个模式。
我可以不打扰并将其留给垃圾收集器吗? (没有使用,没有Dispose())
C ++ RAII通常应用于所有类型的临时状态清理,包括递减在构造函数中递增的计数器等等。而GC在后台线程中运行。它不适合RAII可以处理的所有确定性清理方案。 RAII的CLR等价物是IDisposable
,它的C#语言接口是using
。在C ++中,它的语言接口是(自然地)析构函数(变为Dispose
方法)和delete
运算符(变成对Dispose
的调用)。在“堆栈”中声明的Ref类对象实际上只相当于C#
使用模式。
上面的代码是将类似List ^的对象从C ++ / CLI返回到C#的正确方法吗?
差不多!
使用gcnew并不意味着垃圾收集器会处理对象,我不必关心如何以及何时释放它们?
您不必释放内存。但是如果类实现IDisposable
,那么这与内存分配完全不同。在放弃对象之前,您必须手动调用Dispose
。
警惕终结者 - 这是一种挂钩GC的方式,以便在GC收集对象时运行自己的清理代码。但它们并不适合应用程序代码中的一般用途。它们从您无法控制的线程运行,在您无法控制的时间运行,并且按照您无法控制的顺序运行。因此,如果一个对象的终结器尝试使用终结器访问另一个对象,则第二个对象可能已经完成。无法控制这些事件的顺序。目前,终结者的大多数原始用途都由SafeHandle涵盖。
答案 1 :(得分:2)
你在ref类中没有终结器。
在C ++ / CLI中,当您在堆栈上创建类的实例(C ++样式),然后它超出范围,或者使用delete
运算符时,将调用析构函数。
当最终确定对象时,终结器由 GC 调用。
在C#中, GC 处理所有对象的破坏(没有删除操作符),因此析构函数和终结器之间没有区别。
所以带有〜的“析构函数”就像c ++析构函数一样,完全不像C#析构函数。
C ++ / CLI ref类中的“析构函数”被编译为.Net Dispose()
方法。
与C#析构函数/终结符等效的是使用!(感叹号)定义的终结器方法。
因此,为避免内存泄漏,您需要定义终结器:
!MatW()
{
delete nativeMat;
}
~MatW()
{
this->!MatW();
}
请参阅MSDN文章 Destructors and Finalizers in visual C++ 使用