본문 바로가기

[C++] 레퍼런스 사용 시 주의 사항

Kwonriver 2023. 1. 27.
728x90

 

C++에서 레퍼런스(Reference, 참조)란 변수에 대한 앨리언스(Alias, 별명)이다. 변수에 대한 별명이기 때문에 일반적인 자료형 뿐 아니라 포인터 자료형에 대해서도 사용이 가능하다. 레퍼런스 변수의 값을 변경하는 경우 해당 레퍼런스가 가리키는 원본의 값이 변경된다.

 

int x = 10;
int& xRef = x;
xRef = 15;	// x 는 15로 변경되었다.

레퍼런스는 변수의 주소를 가져오거나 변수에 대한 역참조 연산을 자동으로 수행하는 특수 포인터로 볼 수 있다. xRef 변수를 사용하는 것은 x를 사용하는 것과 동일하다. 즉, &xRef 연산으로 xRef의 주소를 찾는 경우 x의 주소 값인 &x가 반환된다. 다르게 말하면 &xRef == &x가 true가 된다.

 

이러한 레퍼런스를 사용할 때 주의해야할 점이 몇가지 있는데 알아보자.

 


레퍼런스 변수 생성

레퍼런스 변수는 반드시 생성과 동시에 초기화해야 한다. 

int x = 5;
int &xRef = x;

int y = 10;
int &yRef;	// 컴파일 에러

위 코드에서 xRef는 생성과 동시에 x 변수로 초기화를 진행하였다. 그러나 yRef는 추후 y를  대입하기 위해 초기화하지 않았는데 이런 경우 빌드 단에서 컴파일 에러가 발생한다. 

 

레퍼런스 변수를 초기화할 때 리터럴(Literal) 값은 사용할 수 없다. 즉, int & xRef = 5; 동작을 진행하면 컴파일 에러가 발생한다. 이는 5라는 리터럴 정수 값은 상수로 규정되는데 int &는 변수이기 때문에 상수의 변경 가능성 때문에 초기화되지 않는다. 따라서 리터럴 값을 레퍼런스 변수에 저장하기 위해서는 레퍼런스 변수도 수정 불가능한 상수로 변경해야한다. 이 때 const를 사용한다.

 

int& xLiteral = 5;	// 컴파일 에러
const int& yLiteral = 5; // 정상

 

함수 반환 값과 같은 임시 객체에 대해서도 const를 사용하면 레페런스 변수에 저장할 수 있다. 사실상 이름을 레퍼런스 상수라고 불러야하는게 아닐까.  

 

std::string GetProgramingFirstString(void)
( 
    return "Hello World";
}

{
    const std::string MyString = GetProgrammingFistString();    // 정상
    std::string MyStringErr = GetProgrammingFirstString();      // 컴파일 에러
}

 


레퍼런스 대상 변경

위에서 레퍼런스 변수는 반드시 생성과 동시에 초기화해야 한다고 하였다. 그렇다면 레퍼런스 변수가 가리키고 있는 대상을 변경하고자 할 때는 어떻게 할까. 결론적으로 한 번 초기화된 페퍼런스 변수의 대상을 변경할 수 없다. 이유인 즉슨 대입 연산자(=)를 통해 값을 대입하면 레퍼런스가 가리키는 대상이 변경되는 것이 아닌 대상의 값이 변경되기 때문이다. 

 

int x = 5;
int y = 10;
int& xRef = x;
xRef = y; // xRef가 가리키는 대상이 y로 변경되는 것이 아닌 x의 값에 y의 값이 대입된다. 즉, x 와 xRef는 10이 된다.

 

한 번 생성 후 초기화된 레퍼런스 변수는 가리키는 대상을 바꿀 수 없으니 바꾸고자 하는 행동 자체를 지향해야한다. 레퍼런스 변수에 레퍼런스 변수를 대입하는 방법을 사용하더라도 각각이 가리키는 값을 대입한다. 주소를 사용하여도 가리키는 대상을 변경할 수 없다. 애초에 그런 규칙이니 괜한 노력하지 말자.

 

int x = 10, y = 15;
int& xRef = x;
int& yRef = y;
xRef = yRef;    // x에 y의 값인 15가 대입된다.
xRef = &y;      // 컴파일 에러
xRef = &yRef;   // 컴파일 에러

 


포인터 레퍼런스와 레퍼런스 포인터

레퍼런스는 모든 데이터타입에 대하여 생성이 가능하기 때문에 포인터도 레퍼런스 변수를 생성할 수 있다. 다만 레퍼런스가 어디에 붙냐에 따라서 기능이 달라진다.

 

포인터 레퍼런스는 포인터 변수에 대한 레퍼런스를 생성한 것이다. 아래 코드는 int*에 대한 레퍼런스 변수를 생성한 것이다.

int* pMyInt;
int*& pRef = pMyInt;
pRef = new int;
delete pRef;

 

레퍼런스 포인터는 레퍼런스의 주소값을 의미한다. 레퍼런스 변수에 &를 붙여 만드는데 이는 레퍼런스가 가리키는 변수의 주소와 같다.

int x = 5;
int& xRef = x;
int* pX = &xRef;    // 레퍼런스 포인터(&xRef)
*pX = 100;          // x에 100을 대입.

 

다만 포인터와 레퍼런스는 엄연히 다른 타입이기 때문에 둘을 직접 비교하는 경우 컴파일 에러가 발생할 수 있다. 위 코드에서 pX == xRef 연산을 수행하는 경우 서로 타입이 달라 컴파일 에러가 발생한다. 따라서 레퍼런스 포인터를 사용하여 pX == &xRef 등으로 비교해야 한다.

 

특별히 주의할 점은 레퍼런스가 모든 데이터 타입에 대하여 만들 수 있다보니 레퍼런스에 대한 레퍼런스를 생성할 수 있을 것이라 착각할 수 있는데 이는 불가능하다. 즉, int&&나 int&*는 생성이 불가능하다.

 


클래스 내의 레퍼런스 멤버 변수

클래스의 멤버변수로 레퍼런스를 사용할 수 있다. 단, 레퍼런스의 특성 상 생성과 동시에 초기화가 진행되어야하기 때문에 반드시 생성자 이니셜라이저를 사용하여 초기화하여야 한다. 

 

class Coordinate
{
public:
    Coordinate(int& x) : xRef(x) {}
private:
    int& xRef;
}
728x90