포인터 관련 연산자 - 변수의 주소를 얻고, 메모리 위치에 접근하고 역참조하기
포인터 관련 연산자 - 변수의 주소를 얻고, 메모리 위치에 접근하고 역참조하기
C#에서 포인터를 다뤄야 하는 상황이라면, 그 핵심은 결국 몇 가지 연산자에 달려 있어요. 변수의 주소를 얻고(&), 포인터가 가리키는 값을 꺼내고(*), 포인터 값을 비교하고, 포인터와 정수를 더하거나 뺄 수 있게 해 주는 것이 바로 포인터 연산자입니다. 지금부터 이 연산자들을 하나씩 짚어볼게요.
출처: Microsoft Learn
본문
포인터를 다룰 때 쓰는 연산자는 다음과 같아요.
- 단항
&(주소 연산자, address-of) 연산자: 변수의 주소를 얻어요. - 단항
*(포인터 역참조, pointer indirection) 연산자: 포인터가 가리키는 변수를 얻어요. ->(멤버 접근, member access)와[](요소 접근, element access) 연산자.- 산술 연산자
+,-,++,--. - 비교 연산자
==,!=,<,>,<=,>=.
포인터 타입 자체에 대한 설명이 필요하다면 Pointer types 문서를 참고하세요.
[!NOTE] 포인터를 쓰는 대부분의 작업은 unsafe 컨텍스트가 필요하고, unsafe 코드를 컴파일하려면 AllowUnsafeBlocks 컴파일러 옵션을 켜야 해요. C# 15에 도입된 memory safety 미리 보기 기능을 활성화하면
unsafe컨텍스트 밖에서도 주소 연산자&를 쓸 수 있어요. 다만 이 기능을 켜더라도, 가리키는 메모리를 읽거나 쓰는 포인터 역참조·멤버 접근·요소 접근 연산자는 여전히unsafe컨텍스트가 필요해요.
주소 연산자 &
단항 & 연산자는 피연산자의 주소를 반환해요.
:::code language="csharp" source="snippets/shared/PointerOperators.cs" id="AddressOf":::
& 연산자의 피연산자는 반드시 고정(fixed) 변수여야 해요. 고정 변수란 가비지 컬렉터가 건드리지 않는 저장 위치에 있는 변수를 말해요. 위 예시에서 지역 변수 number는 스택에 있어서 고정 변수예요. 반면 가비지 컬렉터가 옮길 수 있는 저장 위치에 있는 변수는 이동(movable) 변수라고 불러요. 객체의 필드나 배열 요소가 대표적인 이동 변수죠. 이동 변수의 주소를 얻고 싶다면, fixed 문으로 그 변수를 "고정(pin)"하면 돼요. 이렇게 얻은 주소는 fixed 문의 블록 안에서만 유효해요. 다음 예시가 fixed 문과 & 연산자를 함께 쓰는 방법을 보여 줍니다.
:::code language="csharp" source="snippets/shared/PointerOperators.cs" id="AddressOfFixed":::
상수나 값의 주소는 얻을 수 없어요.
고정 변수와 이동 변수에 대한 더 자세한 내용은 C# 언어 사양의 Fixed and moveable variables 섹션을 참고하세요.
참고로 이항 & 연산자는 불리언 피연산자의 논리곱(AND) 또는 정수 피연산자의 비트 논리곱을 계산해요. 포인터에서 쓰는 단항 &와는 별개의 역할이죠.
포인터 역참조 연산자 *
단항 포인터 역참조 연산자 *는 피연산자가 가리키는 변수에 접근해요. 흔히 역참조(dereference) 연산자라고도 부르죠. * 연산자의 피연산자는 포인터 타입이어야 해요.
:::code language="csharp" source="snippets/shared/PointerOperators.cs" id="PointerIndirection":::
void* 타입의 표현식에는 * 연산자를 적용할 수 없어요.
이항 * 연산자는 숫자 피연산자의 곱을 계산해요. 포인터 역참조와는 완전히 다른, 일반적인 곱셈 연산자예요.
포인터 멤버 접근 연산자 ->
-> 연산자는 포인터 역참조와 멤버 접근을 합쳐 놓은 거예요. x가 T* 타입의 포인터이고 y가 타입 T의 접근 가능한 멤버라면, 다음과 같은 표현식
x->y
은 아래와 완전히 동일해요.
(*x).y
즉 "x가 가리키는 대상의 y 멤버"에 접근하는 셈이죠. 다음 예시는 -> 연산자의 사용법을 보여 줍니다.
:::code language="csharp" source="snippets/shared/PointerOperators.cs" id="MemberAccess":::
void* 타입의 표현식에는 -> 연산자를 쓸 수 없어요.
포인터 요소 접근 연산자 []
포인터 타입의 표현식 p에 대해, p[n] 형태의 포인터 요소 접근은 *(p + n)으로 평가돼요. 값 n은 int, uint, long, ulong 중 하나로 암시적으로 변환 가능한 타입이어야 해요. 포인터에서 + 연산자의 동작에 대한 자세한 내용은 포인터에 정수 값 더하고 빼기 섹션을 참고하세요.
다음 예시는 포인터와 [] 연산자를 이용해 배열 요소에 접근하는 방법을 보여 줍니다.
:::code language="csharp" source="snippets/shared/PointerOperators.cs" id="ElementAccess":::
위 예시에서 stackalloc 식이 스택에 메모리 블록을 할당해요.
[!NOTE] 포인터 요소 접근 연산자는 범위를 벗어난(out-of-bounds) 접근을 검사하지 않아요. 이 점을 꼭 유의하세요.
void* 타입의 표현식에는 [] 연산자로 포인터 요소 접근을 할 수 없어요.
[] 연산자는 배열 요소 또는 인덱서 접근에도 쓰이니까, 문맥에 따라 의미가 달라진다는 점도 알고 계시면 좋아요.
포인터 산술 연산자
포인터로는 다음과 같은 산술 연산을 할 수 있어요.
- 포인터에 정수 값을 더하거나 뺀다.
- 두 포인터를 뺀다.
- 포인터를 증가시키거나 감소시킨다.
다만 void* 타입의 포인터로는 이런 연산을 할 수 없어요.
숫자 타입에서 지원되는 산술 연산에 대한 내용은 Arithmetic operators 문서를 참고하세요.
포인터에 정수 값 더하고 빼기
T* 타입의 포인터 p와 int, uint, long, ulong 중 하나로 암시적으로 변환 가능한 타입의 식 n이 있을 때, 덧셈과 뺄셈은 다음과 같이 동작해요.
p + n과n + p는 모두T*타입의 포인터를 반환해요.p가 가리키는 주소에n * sizeof(T)를 더해서 얻는 포인터예요.p - n은T*타입의 포인터를 반환해요.p가 가리키는 주소에서n * sizeof(T)를 빼서 얻는 포인터예요.
sizeof 연산자는 타입의 크기를 바이트 단위로 얻어요.
다음 예시는 포인터에서 + 연산자를 사용하는 방법을 보여 줍니다.
:::code language="csharp" source="snippets/shared/PointerOperators.cs" id="AddNumber":::
포인터 뺄셈
T* 타입의 두 포인터 p1과 p2에 대해, p1 - p2는 p1과 p2가 가리키는 주소의 차이를 sizeof(T)로 나눈 값을 반환해요. 결과 타입은 long이에요. 다시 말해 p1 - p2는 ((long)(p1) - (long)(p2)) / sizeof(T)처럼 계산돼요.
다음 예시는 포인터 뺄셈을 보여 줍니다.
:::code language="csharp" source="snippets/shared/PointerOperators.cs" id="SubtractPointers":::
포인터 증가와 감소
++ 증가 연산자는 포인터 피연산자에 1을 더하고, -- 감소 연산자는 1을 뺍니다.
두 연산자 모두 후위(p++, p--)와 전위(++p, --p) 두 형태를 지원해요. p++와 p--의 결과는 연산 이전의 p 값이고, ++p와 --p의 결과는 연산 이후의 p 값이에요.
다음 예시는 후위와 전위 증가 연산자 각각의 동작을 보여 줍니다.
:::code language="csharp" source="snippets/shared/PointerOperators.cs" id="Increment":::
포인터 비교 연산자
==, !=, <, >, <=, >= 연산자는 void*를 포함한 모든 포인터 타입의 피연산자를 비교하는 데 쓸 수 있어요. 이 연산자들은 두 피연산자가 가리키는 주소를 마치 부호 없는 정수처럼 취급해 비교해요.
이 연산자들이 다른 타입의 피연산자에 대해 어떻게 동작하는지 궁금하다면 Equality operators와 Comparison operators 문서를 참고하세요.
연산자 우선 순위
포인터 관련 연산자는 우선 순위가 높은 순서부터 낮은 순서로 다음과 같이 나뉘어요.
- (Primary) 연산자: 후위 증가
x++, 후위 감소x--, 그리고->와[]연산자. - (Unary) 연산자: 전위 증가
++x, 전위 감소--x, 그리고 주소&와 역참조*연산자. - (Additive) 연산자: 이항
+와-연산자. - (Relational and type-testing) 연산자: 비교
<,>,<=,>=연산자. - (Equality) 연산자:
==와!=연산자.
괄호 ()를 쓰면 연산자 우선 순위가 정해 놓은 평가 순서를 바꿀 수 있어요.
우선 순위 레벨별로 정렬한 C# 연산자 전체 목록은 C# operators 문서의 Operator precedence 섹션을 참고하세요.
연산자 오버로딩
사용자 정의 타입에서는 포인터 관련 연산자인 &, *, ->, []를 오버로딩할 수 없어요.
C# 언어 사양
더 자세한 내용은 C# 언어 사양의 다음 섹션들을 참고하세요.
- Fixed and moveable variables
- The address-of operator
- Pointer indirection
- Pointer member access
- Pointer element access
- Pointer arithmetic
- Pointer increment and decrement
- Pointer comparison