7

用 C++ 编写无 memleak 代码对我来说不是问题,我只是坚持使用RAII习语。

用 C# 编写无 memleak 代码也不是很难,垃圾收集器会处理它。

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

这就是我所拥有的:

用 C# 编写的 Windows 服务,它在内部使用 C++ 库(例如 OpenCV)。C++ 类通过 C++/CLI 包装类访问。例如,我有一个MatW 用于图像对象的 C++/CLI 包装类cv::Mat,它将 a 作为构造函数参数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);
    }
}

现在,这是我的问题:

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

我知道这是很多问题,但我只想摆脱我的内存泄漏,所以我列出了我认为可能出错的所有内容......

4

2 回答 2

6

我的代码有什么问题吗?

不在您发布的内容中-您using正确地应用了陈述。因此,您的代码示例不是内存泄漏的原因。

我必须在我的 C# 代码中使用 using 吗?当使用多个包装器对象时,它会使代码变得丑陋,因为 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#using 模式。

上面的代码是将像 List^ 这样的对象从 C++/CLI 返回到 C# 的正确方法吗?

差不多!

使用 gcnew 并不意味着垃圾收集器会处理这些对象,而我不必关心如何以及何时释放它们?

您不必释放内存。但是如果类实现了IDisposable,那么这与内存分配完全不同。您必须Dispose在放弃对象之前手动调用。

小心终结器——这些是连接到 GC 的一种方式,以便在 GC 收集您的对象时运行您自己的清理代码。但它们并不真正适合应用程序代码中的一般用途。它们从您无法控制的线程运行,有时您无法控制,并且以您无法控制的顺序运行。因此,如果一个对象的终结器尝试使用终结器访问另一个对象,则第二个对象可能已经被终结。无法控制这些事件的顺序。SafeHandle现在涵盖了终结器的大部分原始用途。

于 2012-10-03T08:37:43.270 回答
4

您在 ref 类中没有终结器。

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

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

因此,带有~的“析构函数”的行为类似于 c++ 析构函数,而不是 C# 析构函数。C++/CLI ref 类中的“析构函数”被编译成 .Net方法。 C# 析构函数/终结器的等价物是终结器方法,它使用! (感叹号)。Dispose()

所以,为了避免内存泄漏,你需要定义一个终结器:

 !MatW()
    {
        delete nativeMat;
    }

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

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

于 2012-10-03T07:42:26.543 回答