참조변수와 포인터는 같은 것일까
종종 우리는 참조변수와 포인터를 동급으로 취급한다. 객체지향언어에서 다형성의 정의는 다음과 같다. ‘다형성은 기반 클래스의 참조변수로 여러 파생 클래스의 객체를 가리키는 것’. 그리고 그에 대한 예시로 우리는 포인터를 쓴다.
#include <iostream>
class A
{
public:
virtual void say(void) {std::cout << "A" << std::endl;}
};
class B : public A
{
public:
void say(void) {std::cout << "B" << std::endl;}
};
int main(void)
{
A *pointer = new B();
pointer->say();
return (0);
}
C++
복사
다형성의 정의에서 ‘참조변수’ 라는 키워드가 등장하는데, 예제 코드에서는 포인터를 사용한다. 위 코드의 작성자는 참조변수와 포인터를 동급으로 취급하고 있는 것이다.
이상한 일이다. 실제로 ‘참조변수’ 와 ‘포인터’ 는 서로 다른 개념이다. 사용하는 방식, 키워드 그리고 모양새가 다르다. 그러나 많은 사람들이 이 둘을 때로는 같게, 때로는 다르다고 말한다. 나는 이 둘을 어떻게 다루어야 하는 것인가?
참조변수에 대해 알아보자
참조변수에 대해 알아보기 전에 몇 가지 단어에 대해 정의를 하고 시작하자. 먼저 ‘변수’ 는 할당된 메모리 공간의 이름이다. 우리는 이 ‘이름’ 을 가지고 해당 메모리 공간에 접근한다. 간단한 예시는 다음과 같다.
int a; // 메모리 공간이 생겼고, a 라는 이름이 붙었다.
a = 10; // a 공간에 접근해서 10 이라는 값을 채웠다.
C++
복사
‘참조자’ 는 할당된 메모리 공간에 또 다른 ‘이름’ 을 붙이는 기능을 한다. 어떤 변수가 참조하는 공간의 또 다른 이름을 붙이는 것이다. 이에 대한 예시는 다음과 같다.
int a = 10; // a 라 불리는 메모리 공간이 생겼고, 10으로 초기화 되었다.
int &ref = a; // a 라 불리는 메모리 공간의 또 다른 이름 ref 이 선언과 동시에 초기화 됨.
C++
복사
코드를 한번 뜯어 보자. 먼저 ‘&’ 키워드 바로 ‘참조자’ 이다. 이것의 기능은 변수(메모리 공간의 이름)에 또 다른 이름을 만드는 기능을 한다. 해석하면 어떤 메모리 공간의 또 다른 이름을 ‘ref’ 라고 짓는 것이다.
이제 쭉 해석을 해보면, ‘int &ref = a;’ 는 “메모리 공간 a 에 또 다른 이름을 ‘ref’ 라고 짓는다.” 이다. 목록화를 시켜보겠다.
•
int: 참조 변수의 유형
•
&: 참조자, 메모리 공간에 또 다른 이름을 만드는 기능을 한다.
•
ref: 메모리 공간의 또 다른 이름이다.
•
a: 변수 a는 또 다른 이름 ref로 불리며, 둘 다 같은 공간을 참조한다.
포인터에 대해 알아보자
포인터는 어떤 유형의 데이터를 담은 메모리의 주소를 담는 공간에 붙여진 ‘이름’ 이다. 다시 말하면, 데이터를 담은 메모리의 주소를 가진 ‘변수’ 인 것이다.
int a = 10;
int *p = &a; // 포인터 p 가 가지는 값은 a 의 주소값이다. (&: 주소연산자)
C++
복사
이제 우리는 포인터 변수 p 를 통해 변수 a 가 가진 값을 변화시킬 수 있다. p 는 변수 a 의 메모리 주소를 가지고 있으므로, 메모리 주소로 접근해 a 의 값을 바꿀 수 있는 것이다.
다형성에서의 포인터와 참조변수
위 설명에 따르면, 참조자는 어떤 변수의 또 다른 ‘이름’ 인 것이고, 포인터는 어떤 변수의 ‘메모리 주소’ 를 가지는 ‘변수’인 것이다. 참조자는 메모리 공간 자체에 이름이 새로 붙지만, 포인터는 특정 메모리 주소를 저장하기 위해 새로운 메모리 공간을 가진다는 차이점도 있다.
어쨌든 이렇게 근본적으로 다른 개념인 포인터와 참조변수를 우리는 왜 종종 동급 취급 하는 것일까? 아래 다형성의 예제를 통해 이 둘이 동급으로 취급되는 이유를 알아보자.
#include <iostream>
class A
{
public:
virtual void what(void) {std::cout << "A" << std::endl;}
};
class B : public A
{
public:
void what(void) {std::cout << "B" << std::endl;}
};
int main(void)
{
B b;
A* pointer = &b;
A& reference = b;
pointer->what();
reference.what();
return (0);
}
C++
복사
"B"
"B"
C++
복사
class B 는 class A 를 상속한다. B 의 what 함수는 A 의 함수를 오버라이딩 하였다. 메인 함수에서 class B 의 객체는 b 이고, 이를 가리키는 포인터 ‘pointer’와 또 다른 별명인 ‘reference’ 를 선언하였습니다.
객체지향언어의 다형성에 따라서 가상 함수로 선언된 what() 은 class A 의 참조변수에 따라 파생 class B 의 함수를 호출하는 것이 가능합니다. 그렇기 때문에 위 코드에서 pointer→what() 은 “B” 라는 결과물을 내놓습니다.
그런데 이상한 것이, 포인터 ‘pointer’ 또한 파생 클래스 B 의 함수를 호출하는 것이 가능합니다. C++ 에서의 참조변수는 ‘reference varaible’ 즉, 레퍼런스 변수 ‘reference’ 를 의미합니다. 이게 ‘pointer’ 를 의미하지는 않습니다. 그러나 ‘참조변수’ 와 ‘포인터’ 가 같은 동작을 하고 있습니다. 이 때문에 우리는 종종 포인터와 레퍼런스를 같다고 생각합니다. 둘 다 참조변수의 역할을 하고 있기 때문이지요.
결론을 내리겠습니다. ‘레퍼런스’ 와 ‘포인터’ 의 정의는 다릅니다. 그러나 참조변수의 역할을 포인터 또한 할 수 있기 때문에 이 둘은 어떤 부분에서 동등하게 취급될 수 있습니다.