C++/CLI代码中的内存泄漏.我做错了什么

memory leaks in C++/CLI code.. what did I do wrong?

本文关键字:泄漏 错了 什么 内存 CLI 代码 C++      更新时间:2023-10-16

用C++编写无内存泄漏的代码对我来说不是问题,我只是坚持RAII习惯用法。

在C#中编写无内存泄漏的代码也不是很难,垃圾收集器会处理它

不幸的是,编写C++/CLI代码对我来说是个问题。我以为我已经理解了它的工作原理,但我仍然有很大的问题,希望你能给我一些提示。

这就是我所拥有的:

用C#编写的Windows服务,在内部使用C++库(例如OpenCV)。使用C++/CLI包装类访问C++类。例如,我有一个cv::Mat映像对象的MatWC++/CLI包装类,它将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服务C#类中的方法的调用:

Bitmap bmpimg = ...
using (MatW img = new MatW(bmpimg))
{
using (PeopleDetectorW peopleDetector = new PeopleDetector())
{
List<string> people = peopleDetector.detect(img);
}
}

现在,我的问题是:

  • 我的代码有什么问题吗
  • 我的C#代码中必须使用using吗?当有多个包装器对象在使用时,这会使代码变得丑陋,因为using语句必须嵌套
  • 在使用了这些对象之后,我可以使用Dispose()
  • 我能不麻烦把它交给垃圾收集器吗?(无using,无Dispose())
  • 上面的代码是将List<string^>^之类的对象从C++/CLI返回到C#的正确方法吗
  • 使用gcnew是否意味着垃圾收集器将处理对象,而我不必关心如何以及何时释放它们

我知道有很多问题,但我只想消除我的内存泄漏,所以我列出了我认为可能出错的一切。。。

我的代码有什么问题吗?

不在您发布的内容中-您正确应用了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语句封装了整个模式。

我能不麻烦,把它留给垃圾收集器吗?(不使用,不处置())

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涵盖了终结器的大多数原始用途。

ref类中没有终结器。

在C++/CLI中,当在堆栈上创建类的实例(C++样式),然后它超出作用域时,或者使用delete运算符时,都会调用析构函数。终结器由GC在完成对象时调用。

在C#中,GC处理所有对象的销毁(没有删除运算符),因此析构函数和终结器之间没有区别。

因此,带有~的">析构函数"的行为类似于c++析构函数,而一点也不像c#析构函数。C++/CLI ref类中的">析构函数"被编译到.NetDispose()方法中
与C#析构函数/终结器等价的是用定义的终结器方法(感叹号)。

因此,为了避免内存泄漏,您需要定义一个终结器:

!MatW()
{
delete nativeMat;
}
~MatW()
{
this->!MatW();
}

请参阅MSDN文章visual C++中的析构函数和终结器使用