15-4.레퍼런스

15-4-가.변수의 별명

레퍼런스(Reference)는 C++에서 새로 추가된 기능이며 변수의 별명(alias)을 정의한다. 별명을 붙이게 되면 한 대상에 대해 두 개의 이름이 생기게 되고 본래 이름은 물론이고 별명으로도 변수를 사용할 수 있다. 레퍼런스를 선언하는 기본 형식은 다음과 같다.

 

type &변수=초기값;

 

포인터 변수를 선언할 때 구두점 *를 사용하는데 비해 레퍼런스를 선언할 때는 구두점 &를 사용한다. 포인터가 기본 타입에 대한 유도형이듯이 레퍼런스도 유도형이라는 점에서 동일하되 특정 대상체에 대한 별명이므로 선언할 때 어떤 대상체에 대한 별명인지를 반드시 밝혀야 한다는 점이 다르다. 다음이 레퍼런스를 사용하는 가장 간단한 예제이다.

 

: Ref1

#include <Turboc.h>

 

void main()

{

     int i=3;

     int &ri=i;

 

     printf("i=%d, ri=%d\n",i,ri);

     ri++;

     printf("i=%d, ri=%d\n",i,ri);

     printf("i번지=%x, ri번지=%x\n",&i,&ri);

}

 

정수형 변수 i를 3으로 초기화했으며 정수형 레퍼런스 ri를 i로 초기화했다. int &ri=i; 선언에 의해 정수형 변수 i에 대해 ri라는 별명을 만든 것이다. 이후 ri는 i와 완전히 동일한 대상을 가리키며 둘 중 하나를 변경하면 나머지 하나도 바뀐다. 실행 결과는 다음과 같다.

 

i=3, ri=3

i=4, ri=4

i번지=12ff7c, ri번지=12ff7c

 

i와 ri의 값을 출력했는데 둘 다 똑같은 값 3을 가진다. 이 상태에서 ri를 1 증가시킨 후 값을 출력해 보면 ri만 증가하는 것이 아니라 i와 ri가 같이 증가되어 둘 다 4가 된다. ri가 i의 별명이기 때문에 ri에 대입되는 값은 i에도 똑같이 대입되며 반대로 i의 값을 바꾸면 ri도 같이 변경된다. 두 변수가 가리키는 실제 번지를 출력해 보면 동일한 위치를 가리키고 있음을 확인할 수 있다.

보다시피 레퍼런스는 대상체와 동일한 주소를 가지는 완전한 별명이다. ri는 i와 이름만 다를 뿐이지 같은 변수인 것이다. T형 변수 v의 별명 r을 하나 만들고 싶다면 언제든지 T &r=v;로 선언하면 된다. 별명이란 일상 생활에서 사용하는 용어와 일치하므로 개념적으로 이해하기 쉽다. 다음은 레퍼런스를 선언하고 사용할 때의 일반적인 주의 사항이다.

 

 레퍼런스와 대상체는 타입이 완전히 일치해야 한다. 레퍼런스가 대상 변수의 완전한 별명이 되려면 같은 타입을 가져야 한다. 다음 예를 보자.

 

int i;

int &ri=i;                      // 가능

double &rd=i;               // 에러

short &rs=i;                 // 에러

unsigned &ru=i;           // 에러

 

정수형(int) 변수 i의 레퍼런스는 반드시 정수형이어야 한다. 실수형 레퍼런스로는 i의 별명을 만들 수 없으며 심지어 int형과 호환되는 short, unsigned 형으로도 별명을 만들 수 없다.

 레퍼런스는 생성 직후부터 별명으로 동작하기 때문에 선언할 때 초기식으로 반드시 대상체를 지정해야 한다. 포인터의 경우는 일단 선언해 놓고 나중에 가리킬 변수의 번지를 대입받을 수 있지만 레퍼런스는 그렇지 못하다.

 

int *pi;

pi=&i;

int &ri;      // 에러

ri=i;

 

아무 것도 가리키지 않는 널 레퍼런스를 인정하지 않기 때문에 int &ri;라는 선언문이 에러로 처리된다. 선언할 때부터 누구의 별명인지에 대한 지정이 있어야 한다. 단, 다음의 경우는 예외적으로 초기값이 없는 레퍼런스를 선언할 수 있다.

 

① 함수의 인수 목록에 사용되는 레퍼런스 형식 인수. 함수가 호출될 때 실인수에 대한 별명으로 초기화된다. 이런 예는 바로 다음 항에서 살펴볼 것이다.

② 클래스의 멤버로 선언될 때. 이때는 클래스의 생성자에서 반드시 초기화해야 한다. 만약 생성자에서 레퍼런스 멤버를 초기화하지 않으면 에러로 처리된다.

③ 변수를 extern 선언할 때. 이때는 레퍼런스의 초기식이 외부에 선언되어 있다는 뜻이므로 초기값을 주지 않아도 된다. extern int &ri; 선언문은 ri가 어떤 변수에 대한 별명으로 외부에 선언되어 있다는 뜻이다.

 

이런 예외적인 경우라 하더라도 레퍼런스의 대상체 지정이 함수 호출 시점이나 객체 생성 시점으로 연기되는 것뿐이지 대상체가 없는 레퍼런스를 허용하는 것은 아니다. 레퍼런스가 실제 메모리에 생성될 때는 반드시 누구의 별명인지 지정되어 있어야 한다.

 레퍼런스는 일단 선언되면 초기식에서 지정한 대상체의 별명으로 계속 사용된다. 그래서 선언된 후 중간에 참조 대상을 변경할 수 없으며 파괴될 때까지 같은 대상체만 가리킬 수 있다. 다음 예를 보자.

 

: Ref2

#include <Turboc.h>

 

void main()

{

     int i=3,j=7;

     int &ri=i;

 

     printf("i=%d, ri=%d, j=%d\n",i,ri,j);

     ri=j;

     printf("i=%d, ri=%d, j=%d\n",i,ri,j);

}

 

ri는 i의 레퍼런스로 초기화되었으므로 이후부터 ri는 i의 별명으로 사용된다. 중간에 ri=j 대입문으로 ri의 대상체를 j로 변경해 봤는데 결과는 다음과 같다.

 

i=3, ri=3, j=7

i=7, ri=7, j=7

 

최초 i와 ri는 3이고 j는 7이다. 이 상태에서 ri=j; 대입문에 의해 ri가 j를 가리키도록 했으므로 i는 3이고 ri와 j는 7이 될 것 같지만 그렇지 않고 모든 변수들이 일제히 7로 바뀌어 버렸다. 왜냐하면 ri는 i의 별명으로 초기화되었으므로 ri=j 대입문은 곧 i=j가 되기 때문이다. 이 대입문은 ri의 대상체를 j로 바꾸는 것이 아니라 ri가 가리키는 본래 변수 i에 j의 값을 대입하는 명령으로 해석된다.

레퍼런스에 대한 대입 연산자(=)는 레퍼런스의 대상체를 바꾸는 것이 아니라 대상체의 값을 변경하는 것으로 정의되어 있다. 즉 실행중에 = 연산자로 레퍼런스의 대상체를 변경할 수 없으며 그래서 선언할 때 한 번만 초기화할 수 있는 것이다. 반면 포인터는 = 연산자로 가리키는 대상을 얼마든지 변경할 수 있으며 그래서 선언할 때 꼭 초기화하지 않아도 된다.

 레퍼런스에 대한 모든 연산은 대상체에 대한 연산으로 해석된다. 그래서 다음 연산문들은 모두 문법적으로 합당하다.

 

int i=3,j;

int &ri=i;

int *pi;

 

ri++;

ri*=5;

j=ri >> 4;

j=ri % 2;

pi=&ri;

 

ri가 정수형 레퍼런스이므로 ri에 대한 모든 연산문은 정수형에 대한 연산이다. 따라서 정수형 변수 i에 대해 사용할 수 있는 모든 연산자를 다 사용할 수 있으며 연산의 효과는 정수형에 대한 연산과 동일하다. 증가, 복합 대입, 쉬프트, 나머지 연산 등은 물론이고 주소 연산자 &도 사용할 수 있다. 이에 비해 포인터의 경우는 곱셈, 나머지, 쉬프트 등의 연산이 허용되지 않는다.

 레퍼런스의 대상체는 실제 메모리 번지를 점유하고 있는 좌변값이어야 한다. 다음 선언문은 에러로 처리된다.

 

int &ri=123;

 

아무리 타입이 일치하더라도 상수값은 좌변값이 아니기 때문에 레퍼런스의 대상체가 될 수 없다. 만약 이 선언문이 가능하다면 ri=456; 대입문으로 상수 123이 456으로 바뀔 수 있다는 얘기가 되어 버린다. 단, 상수 지시 레퍼런스인 경우는 상수를 대상체로 취할 수 있다.

 

const int &ri=123;

 

이렇게 되면 ri는 123이라는 상수값을 가지며 이후 이 값은 변경할 수 없으므로 좌변값으로 사용되지 않는다. 이 선언문은 일단 가능은 하지만 전혀 실용성이 없다. 왜냐하면 const int ri=123;이라는 정수형 상수를 만드는 것과 아무런 차이가 없기 때문이다.