fixed 문 — 가변 변수를 고정해 포인터 연산에 사용하기

fixed 문 — 가변 변수를 고정해 포인터 연산에 사용하기

C#에서 포인터를 다룰 때, 가비지 컬렉터가 메모리를 옮겨버리면 참조하던 주소가 의미를 잃어버리는 문제가 있어요. fixed 문은 그런 가변(moveable) 변수를 한 곳에 고정(pin)하고, 그 변수를 가리키는 포인터를 선언할 수 있게 해주는 문장입니다.

출처: Microsoft Learn

본문

fixed 문은 가비지 컬렉터가 가변 변수를 다른 곳으로 옮기지 못하게 막고, 그 변수를 가리키는 포인터를 선언해 줍니다. 고정된(pinned) 변수의 주소는 이 문장이 실행되는 동안 변하지 않아요. 그리고 이렇게 선언한 포인터는 해당 fixed 문 안에서만 쓸 수 있고, 읽기 전용이라 값을 바꿀 수도 없습니다:

unsafe
{
    byte[] bytes = [1, 2, 3];
    fixed (byte* pointerToFirst = bytes)
    {
        Console.WriteLine($"The address of the first array element: {(long)pointerToFirst:X}.");
        Console.WriteLine($"The value of the first array element: {*pointerToFirst}.");
    }
}
// Output is similar to:
// The address of the first array element: 2173F80B5C8.
// The value of the first array element: 1.

[!NOTE] fixed 문을 쓰려면 코드를 AllowUnsafeBlocks 컴파일러 옵션을 켜고 컴파일해야 해요.

C# 15의 메모리 안전성(memory safety) 미리 보기 기능을 쓰면 unsafe 컨텍스트 밖에서도 fixed를 쓸 수 있지만, 포인터 역참조처럼 고정된 메모리에 실제로 접근하는 연산은 여전히 unsafe 컨텍스트가 필요합니다.

선언한 포인터는 여러 가지 방법으로 초기화할 수 있어요.

  • 배열로 초기화 — 이 문서 맨 앞의 예시처럼 하면 됩니다. 이때 초기화된 포인터는 배열의 첫 번째 요소의 주소를 가리켜요.
  • 변수의 주소로 초기화 — 주소 연산자 &를 사용해요. 아래 예시처럼요:
unsafe
{
    int[] numbers = [10, 20, 30];
    fixed (int* toFirst = &numbers[0], toLast = &numbers[^1])
    {
        Console.WriteLine(toLast - toFirst);  // output: 2
    }
}

객체의 필드도 고정할 수 있는 또 다른 가변 변수의 예시예요. 초기화된 포인터가 어떤 객체의 필드나 배열 요소의 주소를 가리키는 경우, fixed 문은 문장 본문이 실행되는 동안 가비지 컬렉터가 해당 객체 인스턴스를 옮기거나 해제하지 않는다는 것을 보장해 줍니다.

  • GetPinnableReference 메서드를 구현한 타입의 인스턴스로 초기화 — 그 메서드는 반드시 비관리형(unmanaged) 타입에 대한 ref 변수를 반환해야 해요. .NET의 Span<T>ReadOnlySpan<T> 타입이 이 패턴을 사용하죠. 그래서 Span 인스턴스도 다음과 같이 고정할 수 있습니다:
unsafe
{
    int[] numbers = [10, 20, 30, 40, 50];
    Span<int> interior = numbers.AsSpan()[1..^1];
    fixed (int* p = interior)
    {
        for (int i = 0; i < interior.Length; i++)
        {
            Console.Write(p[i]);  
        }
        // output: 203040
    }
}

자세한 내용은 Span<T>.GetPinnableReference API 참조를 확인해 보세요.

  • 문자열(string)로 초기화 — 아래 예시처럼요:
unsafe
{
    var message = "Hello!";
    fixed (char* p = message)
    {
        Console.WriteLine(*p);  // output: H
    }
}

한편 스택에 메모리를 할당하면 가비지 컬렉션의 대상이 아니어서 굳이 고정할 필요가 없어요. 그럴 때는 stackalloc을 쓰면 됩니다.

더 알아보기