본문 바로가기

[게임 개발자를 위한 C++ 문법] 포인터와 레퍼런스

@iamrain2025. 8. 13. 14:49

포인터

변수는 "값"을 담는다.

 

하나의 변수를 다른 변수에 대입하면 새로운 메모리 공간에 동일한 값이 복제된다.

이 둘은 서로 각각의 공간을 가지기 때문에, 한 쪽이 바뀌더라도 다른 쪽에 영향이 없다.

 

그런데 여기서 한 번 생각해보면, 변수를 복사할 때 당연히 비용이 발생한다.

복사할 변수가 작다면 비용이 적게들지만, 크다면 많이든다.

 

이러한 복사 비용 때문에 C++에서는 값을 직접 복사하는 대신 변수의 주소를 가리켜서 동일한 데이터에 접근할 수 있게 한다.

그것이 바로 포인터다.

 

포인터는 `int* a = &b`와 같은 형태로 선언되는데 `&` 연산자가 주소 값을 넣는 연산자이다.

주소 값을 넣는다고 했는데, 왜 타입이 함께 선언될까? 포인터는 변수의 주소값 중에서도 시작 주소를 저장한다. 그리고 타입이 선언되었기 때문에 해당 데이터의 크기를 알 수 있어서 시작 주소로부터 얼마까지가 데이터인지 알 수 있는 것이다.

 

포인터에 주소 값이 저장되어 있다면, 주소에 있는 실제 값은 어떻게 가져올 수 있을까? 역참조 연산자 `*`를 사용하면 된다.

int x = 3;
int* ptr = &x;

위와 같은 코드가 있을 때, `x`의 시작 주소 값을 200이라고 하겠다.

`cout << ptr << '\n';`을 수행하면 200이 나온다. `ptr`은 시작 주소를 가지기 때문이다.

3을 가져오고 싶다면? `cout << *ptr << '\n';`으로 작성하면 된다. 이처럼 읽어오는 것 뿐만 아니라 `*ptr = 40`과 같이 주소에 있는 값도 변경할 수 있다.

배열과 포인터

배열에서 배열의 이름은 시작 주소를 가지고 있다. 배열을 호출하면 배열의 첫 번째 원소의 주소로 해석하는 것이다.

하드웨어적인 부분에서 배열은 메모리에 연속적으로 할당된다. 따라서 시작 위치를 알면 나머지도 모두 알 수 있는 것이다.

 

포인터와 비슷하게 보이지만 배열은 다른 주소값을 할당할 수 없다. 또한, 포인터 변수의 크기는 타입과 무관하게 운영체제에서 관리하는 메모리 주소의 크기이지만, 배열은 배열 원소 타입의 크기에 갯수를 곱한 것이다.

포인터 배열과 배열 포인터

  • 포인터 배열
    포인터를 원소로 갖는 배열. `int* ptrArr[4];`는 크기가 4이고, 원소가 `int*`인 배열.
  • 배열 포인터
    배열 전체를 가리키는 포인터. 다차원 배열을 제어할 때 사용.

포인터 연산

포인터는 주소값을 담기 때문에 산술 연산 시 메모리 주소의 이동으로 해석된다.

  • `ptr + 1`
    `ptr`이 가리키는 주소에서 한 단위 메모리 주소가 이동. 포인터 자료형 크기에 따라서 단위가 결정된다.
  • `(*ptr) + 1`
    `ptr`이 가리키는 변수의 값에 1을 더한다. 원본값에는 아무런 변화가 없다.
  • `*(ptr + 1)`
    `ptr[i]`와 동일. 실제 배열 인덱스 연산자 `[ ]`는 내부적으로 포인터 연산을 통해 구현되어 있다.
    한 단위 이동한 메모리 주소에 대한 역참조이다.
포인터 연산은 `+`와 `-`는 가능하지만, `*`와 `/`는 불가능하다.

레퍼런스

포인터를 사용하면 주소 값을 직접 다뤄야해서 복잡해질 수 있다. 이를 해결하기 위해 C++에서는 레퍼런스 문법을 도입했다.

 

레퍼런스는 일반 변수와 거의 동일하게 사용할 수 있지만 내부적으로는 해당 변수를 직접 가르켜 주는 역할을 한다.

특정 변수에 별명을 부여하는 식으로 사용하는데, 특정 변수의 레퍼런스를 연결하면 해당 변수는 2개의 이름을 가지는 것이다.

 

`int& ref = val;`와 같이 데이터타입 뒤에 `&`를 붙여준다. 이렇게하면 `ref`의 값이 변경될 때 `var`의 값도 함께 변경된다.

한 가지 주의할 점은 레퍼런스는 선언과 동시에 초기화를 해야한다.

포인터와 레퍼런스 차이점

  • 선언과 초기화 시점
    포인터는 선언을 하고 나중에 `=` 연산자를 통해 대상을 변경할 수 있다.
    반면에 레퍼런스는 선언과 동시에 초기화해야 하며, 초기화 이후에 다른 대상을 연결할 수 없다.
  • NULL
    레퍼런스는 항상 다른 변수와 연결되어 있기 때문에 `NULL`이 있을 수 없다.
    반면에 포인터는 유효한 대상이 없음을 나타내기 위해 `NULL` 혹은 `nullptr`을 가질 수 있다.
`NULL`보다는 `nullptr` 사용이 권장된다.
`NULL`은 `(void*) 0`, void포인터 영이라서 0인지, 0번째 주소 값인지 애매한 상황이 발생할 수 있다.
이를 해결하기 위한 것이 널 포인터 `nullptr`이다.
  • 간접 참조 문법의 유무
    포인터는 주소 값을 담기 때문에 접근할 때 `*` 연산을 통해 역참조를 하고 주소를 가져올 때 `&` 연산을 사용한다.
    레퍼런스는 변수 자체의 별명이므로 일반 변수와 연산하는 방법이 동일하다. 값을 표현할 때 일반 변수처럼 표현할 수 있다.

상수 레퍼런스

레퍼런스에 상수 제약을 걸어서 읽기 전용으로 사용할 수 있다.

상수 레퍼런스를 사용하면 값을 복사하지 않고도 기존 변수를 보호할 수 있다.

 

`const int& cref = x;`와 같이 작성하면 복사 과정 없이 `x`의 값을 읽을 수는 있지만 `x` 값을 수정할 수는 없다.

iamrain
@iamrain :: Annals of Unreal

iamrain 님의 블로그 입니다.

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차