如果我使用AllocHGlobal分配了一些内存,是否需要使用FreeHGlobal释放它?

15

我写了一个辅助方法,

internal static IntPtr StructToPtr(object obj)
{
    var ptr = Marshal.AllocHGlobal(Marshal.SizeOf(obj));
    Marshal.StructureToPtr(obj, ptr, false);
    return ptr;
}

它接受一个 struct 并将其转换为一个 IntPtr。我使用它的方式如下:

public int Copy(Texture texture, Rect srcrect, Rect dstrect)
{
    return SDL.RenderCopy(_ptr, texture._ptr, Util.StructToPtr(srcrect), Util.StructToPtr(dstrect));
}
问题在于我只需要那个IntPtr一刹那,这样我就可以将它传递给C DLL了。
[DllImport("SDL2.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "SDL_RenderCopy")]
internal static extern int RenderCopy(IntPtr renderer, IntPtr texture, IntPtr srcrect, IntPtr dstrect);

我其实不想担心释放它的问题;否则我的一行代码就会变成六行:

public int Copy(Texture texture, Rect? srcrect=null, Rect? dstrect=null)
{
    var srcptr = Util.StructToPtr(srcrect);
    var dstptr = Util.StructToPtr(dstrect);
    var result = SDL.RenderCopy(_ptr, texture._ptr, srcptr, dstptr);
    Marshal.FreeHGlobal(srcptr);
    Marshal.FreeHGlobal(dstptr);
    return result;
}

有没有更好的方法来做到这一点?C#会清理它分配的任何内存吗?

如果不行,那么我是否可以将对SDL.RenderCopy的调用包装在一些using语句中,以便我不必进行所有这些临时变量和显式释放的无聊操作?

4个回答

30

是的,C#不会自动释放由Marshal.AllocHGlobal分配的内存。 必须使用Marshal.FreeHGlobal调用来释放该内存,否则它将泄漏。

您可以创建一个智能指针来包装IntPtr

class StructWrapper : IDisposable {
    public IntPtr Ptr { get; private set; }

    public StructWrapper(object obj) {
         if (Ptr != null) {
             Ptr = Marshal.AllocHGlobal(Marshal.SizeOf(obj));
             Marshal.StructureToPtr(obj, Ptr, false);
         }
         else {
             Ptr = IntPtr.Zero;
         }
    }

    ~StructWrapper() {
        if (Ptr != IntPtr.Zero) {
            Marshal.FreeHGlobal(Ptr);
            Ptr = IntPtr.Zero;
        }
    }

    public void Dispose() {
       Marshal.FreeHGlobal(Ptr);
       Ptr = IntPtr.Zero;
       GC.SuppressFinalize(this);
    }

    public static implicit operator IntPtr(StructWrapper w) {
        return w.Ptr;
    }
}

使用这个包装器,您可以通过将对象包装在using语句中手动释放内存,或者允许它在终结器运行时被释放。

使用这个封装器,您可以通过将对象包装在using语句中手动释放内存,或者允许它在终结器运行时被释放。

8
唯一让这更好的事情就是进行 implicit 转换成 IntPtr,这样就无需使用 .Ptr 就能将其传递给他的 API 调用。 - Jonathon Reinhart
虽然,如果你只需要在这个函数中完成它,我不会介绍这个。因为如果我按照你的做法,你希望尽可能快。 - Keith Nicholas
2
我会做的另一个更改是允许构造函数接受null并将Ptr初始化为IntPtr.Zero - mpen
1
我假设我们刚刚开发的这个结构是CC许可证? :-) - Jonathon Reinhart
7
这个答案实际上只是重新发明了SafeHandle,但SafeHandle的好处在于将释放内存的过程放在了约束执行区域中,因此不会被像“Thread.Abort()”这样的东西中断,并且可以直接从P/Invoke调用中返回。 - Scott Chamberlain
6
目前(2015/05/09)ctor的写法为if (Ptr != null),我认为这是错误的。ctor应该检查obj是否为空,并相应地设置Ptr。请参考Mark于2013年7月10日4:40提出的建议。 - Kent

24
很多人不知道这一点(这就是为什么你会得到很多说你不能的答案),但.NET中有一些专门用于此类情况的内置功能:SafeHandle
事实上,.NET 2.0页面中的其中一个派生类使用了AllocHGlobal的示例。当SafeUnmanagedMemoryHandle的终结器被调用时,它将自动为您调用FreeHGlobal。(如果您想获得确定性清理而不仅仅是等待终结器处理它,您需要显式地调用Close()Dispose()。)
这只需要你做出一些代码更改:
internal static SafeUnmanagedMemoryHandle StructToPtr(object obj)
{
    var ptr = Marshal.AllocHGlobal(Marshal.SizeOf(obj));
    Marshal.StructureToPtr(obj, ptr, false);
    return new SafeUnmanagedMemoryHandle(ptr, true);
}

[DllImport("SDL2.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "SDL_RenderCopy")]
internal static extern int RenderCopy(IntPtr renderer, IntPtr texture, SafeUnmanagedMemoryHandle srcrect, SafeUnmanagedMemoryHandle dstrect);

一旦你这样做了,你原来的 Copy 示例将会按照你预期的方式正常工作。

public int Copy(Texture texture, Rect srcrect, Rect dstrect)
{
    return SDL.RenderCopy(_ptr, texture._ptr, Util.StructToPtr(srcrect), Util.StructToPtr(dstrect));
}
当这两个指针超出作用域并完成后,它们将在此之后被清理。我不知道 _ptr 如何被使用或者Texture 是否是你所控制的类,但它们也可以潜在地被替换为 SafeHandle


更新:如果你想要学习更多关于如何正确处理非托管资源(并获得一个比MSDN给出的示例更好的实现 IDisposable 的更好模式的示例),我强烈推荐阅读由 Stephen Cleary 撰写的文章 "IDisposable: What Your Mother Never Told You About Resource Deallocation"。他在文章中深入介绍了如何正确编写自己的 SafeHandles。


附录

这里是一个示例的副本,以防链接失效:

using System;
using System.Security.Permissions;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

namespace SafeHandleExamples
{
    class Example
    {
        public static void Main()
        {
            IntPtr ptr = Marshal.AllocHGlobal(10);

            Console.WriteLine("Ten bytes of unmanaged memory allocated.");

            SafeUnmanagedMemoryHandle memHandle = new SafeUnmanagedMemoryHandle(ptr, true);

            if (memHandle.IsInvalid)
            {
                Console.WriteLine("SafeUnmanagedMemoryHandle is invalid!.");
            }
            else
            {
                Console.WriteLine("SafeUnmanagedMemoryHandle class initialized to unmanaged memory.");
            }

            Console.ReadLine();
        }
    }


    // Demand unmanaged code permission to use this class.
    [SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
    sealed class SafeUnmanagedMemoryHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
        // Set ownsHandle to true for the default constructor.
        internal SafeUnmanagedMemoryHandle() : base(true) { }

        // Set the handle and set ownsHandle to true.
        internal SafeUnmanagedMemoryHandle(IntPtr preexistingHandle, bool ownsHandle)
            : base(ownsHandle)
        {
            SetHandle(preexistingHandle);
        }

        // Perform any specific actions to release the 
        // handle in the ReleaseHandle method.
        // Often, you need to use Pinvoke to make
        // a call into the Win32 API to release the 
        // handle. In this case, however, we can use
        // the Marshal class to release the unmanaged
        // memory.
        override protected bool ReleaseHandle()
        {
            // "handle" is the internal
            // value for the IntPtr handle.

            // If the handle was set,
            // free it. Return success.
            if (handle != IntPtr.Zero)
            {

                // Free the handle.
                Marshal.FreeHGlobal(handle);

                // Set the handle to zero.
                handle = IntPtr.Zero;

                // Return success.
                return true;
            }

            // Return false. 
            return false;
        }
    }
}

1
+1 太棒了。考虑到 AllocHGlobal 的常见用途,为什么 SafeUnmanagedMemoryHandle 不是 BCL 的一部分呢? - Jonathon Reinhart
不知道,他们也在2.0版本之后的MSDN中删除了示例。 - Scott Chamberlain
有趣。我看到 SafeHandle 用于像 CreateFile 这样的 PInvoked 调用,但我想知道为什么他们要摒弃这样一个东西。我甚至不确定这里谁会知道。 - Jonathon Reinhart
这真的很棒。我将调整shf301的类以使用SafeHandle。不过MSDN上的示例有点奇怪; 如果“ReleaseHandle”仅在“根据IsInvalid属性定义的句柄有效时”才被调用,为什么要显式检查if(handle!= IntPtr.Zero)?([src](http://msdn.microsoft.com/en-us/library/system.runtime.interopservices.safehandle.releasehandle.aspx)) - mpen

1

是的,你必须释放它,并且你编写的这个6行程序的方式非常高效。当你跳出垃圾回收器时,这就是你所做的权衡。


1
他目前的代码唯一的问题是SDL.RenderCopy可能会抛出异常(不确定,没有查看文档),如果发生这种情况,他将会泄漏内存,因为释放内存的方法永远不会被执行。 - Cody Gray
1
不确定它是否会发生,但如果确实发生了,那将是一个问题。 - Keith Nicholas
所有的SDL方法在失败时返回0。 - mpen

0

很遗憾,没有内置的自动方式来完成这个任务。如果你调用了AllocHGlobal,你必须使用FreeHGlobal显式释放它(除非你可以接受可能导致大量内存泄漏的情况)。

这块内存必须使用Marshal.FreeHGlobal方法释放。

我过去的做法是将我的AllocHGlobal分配封装在一个IDisposable包装类中,在Dispose()方法中调用指针上的FreeHGlobal。这样,我可以将它们放在using语句中。


2
咳嗽声:是的,有一种自动方式 咳嗽声。 - Scott Chamberlain

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接