본문 바로가기

프로그래밍/C++

[C++] 이명제조기, 참조자 - 2

들어가며

자 이번 시간을 끝으로 개념적인 참조자의 설명은 끝이 난다.

그 말은 즉, 아직 참조자에 대해 알아야 할 것들이 많이 남아 있다는 소리다.

지금까지 설명한 것만 가지고 참조자를 어디다가 실용적이게 잘 써먹어 봐라!

라고 한다면 너무 무책임 한 것이 아닌가.

그렇기에 우린 좀 더 참조자에 대해서 알아볼 필요가 있다.

분명 어딘가에 써먹지 않겠는가?

 

참조자의 개수의 제한

결론부터 말하자면, 그런 거 없다.

즉, 다음과 같이 여러 개의 참조자를 선언하는 것도 가능하다.

int  num1 = 1;
int& num2 = num1;
int& num3 = num1;

 

이렇게 된다면 num1의 메모리 공간에 num2와 num3라는 이명이 붙은 것이 되고,

num2와 num3를 이용하여 num1의 메모리 공간에 접근할 수 있게 되는 것이다.

그림 5 - 하나의 변수, 여러 개의 참조자

 

참조자, 참조자참조자, 참조자참조자참조자

뭔 소린가 싶은가?

자 예를 들어보자.

우리가 흔히 떡국에 넣어 먹는 떡을 '떡국떡'이라고 한다.

그럼 그 떡을 넣은 국은 '떡국떡국'이 되어야 합당하지 않은가?

그럼 그 '떡국떡국'에 넣는 떡이 '떡국떡국떡'이 되고,

이렇게 명제가 순환하여 회귀하게 된다.

뭔 소린가 싶은가?

참조자에 대한 참조자는 참조자참조자가 되어야 합당하지 않은가?

미안하다.


참조자를 대상으로도 참조자를 선언할 수 있다.

즉, 다음과 같은 참조자의 중첩 코드도 실행이 가능하다.

int  num1 = 1;
int& num2 = num1;
int& num3 = num2;
int& num4 = num3;

 

이렇게 된다면 num1의 메모리 공간의 상태는 다음과 같다.

그림 6 - 참조자의 중첩

하나의 변수에 대해 여러 개의 참조자를 만드는 것이나,

참조자에 대한 참조자를 중첩으로 만드는 것이나,

결과적으로는 같은 형태를 가지고 있는 것이 된다.

하지만 뭐든 과하면 좋지 않다. 과유불급(過猶不及)이라는 말이 있지 않은가?

필요 이상으로 참조자를 선언하는 것은 바람직하지 않으며,

참조자를 대상으로 또 다른 참조자를 생성하는 상황이 흔하지도 않다.

 

참조자의 선언 가능 범위

참조자를 사용할 때 주의해야 할 점이 몇 가지 있다.

참고로, 여기서 말하는 '변수'는 '배열의 요소'도 내포한다.

   1. 참조자는 변수에 대해서만 선언이 가능하다.

자, 이 주의사항에 대해 전면적으로 맥이는 코드는 다음과 같다.

int& ref1 = 10;    // 유효하지 않은 선언, 컴파일 오류
int& ref2 = NULL;  // 유효하지 않은 선언, 컴파일 오류

 

우선 참조자를 선언하고서 변수가 아닌 것으로 초기화하고 있기 때문에 이 코드는 유효하지 않다.

비슷한 맥락으로 참조자를 선언하면서 NULL로 초기화하는 것도 불가능 하다.

참조자는 변수에 이명을 부여하는 것이지 않는가?

따라서 변수가 아닌 것을 대상으로는 참조자를 선언할 수 없다는 것은 합당하다.

 

그리고 다음 주의사항은 다음과 같다.

   2. 선언됨과 동시에 누군가를 참조해야 한다.

이명은 있는데, 그 이명을 붙일 사람이 없다.

그럼 그 이명은 무슨 의미가 있겠는가?

그렇기 때문에 다음과 같은 코드는 유효하지 않다.

int& ref;

 

참조자를 선언과 동시에 참조(초기화)하지 않고 있기 때문이다.

 

그리고 다음 주의사항이다.

   3. 참조자를 선언한 후 참조 대상을 변경할 수 없다.

당연하게도 불가능하니 코드로도 나타낼 수 없다.

그림으로 설명하면 다음과 같다.

그림 7 - 참조 대상 변경 불가

이런 식으로 이미 어떠한 변수를 참조하고 있는 참조자가

다른 변수로 참조 대상을 변경할 수 없다.

 

배열의 참조자

위에서 현재 설명하는 개념에서의 '변수'는 '배열의 요소'를 포함한다고 했다.

다시 한번 말하지만, '배열'이 아닌 '배열의 요소'를 말한다.

이와 관련해서는 다음의 코드를 분석해보며 이해해보도록 하자.

#include <iostream>

using namespace std;

int main()
{
    int arr[3] = { 1, 3, 5 };
    int& ref1 = arr[0];
    int& ref2 = arr[1];
    int& ref3 = arr[2];

    cout << ref1 << endl;
    cout << ref2 << endl;
    cout << ref3 << endl;
    return 0;
}

 

해당 코드를 실행시키면 확인할 수 있듯이

'배열 요소''변수'로 간주되어 참조자의 선언이 가능하다.

 

포인터의 참조자

포인터도 엄연히 '포인터 변수'이지 않은가?

따라서 당연히 그 포인터 변수에도 참조자 선언이 가능하다.

이와 관련해서 다음 코드를 분석하여 이해하도록 하자.

#include <iostream>

using namespace std;

int main()
{
    int num = 10;
    int* ptr = &num;            // 변수 num에 대한 포인터
    int** dptr = &ptr;          // 포인터 변수 ptr에 대한 포인터 (더블 포인터)

    int& ref = num;             // 변수 num에 대한 참조자
    int* (&pref) = ptr;         // 포인터 변수 ptr에 대한 참조자
    int** (&dpref) = dptr;      // 더블 포인터 변수 dptr에 대한 참조자

    cout << ref << endl;        // ref -> num 으로 접근
    cout << *pref << endl;      // (pref -> ptr) -> num으로 접근
    cout << **dpref << endl;    // (dpref -> dptr) -> ptr -> num으로 접근

    return 0;
}

 

'어······. 뭔 소린지 대충 모르겠는 뎁쇼······?'

너무 심각하게 생각할 필요는 없다.

단지 아직 IT업계에 취직할 생각을 하면 안되는 단계일 뿐이다.

주석을 하나하나 찬찬히 읽어가며 접근 경로를 따라가면서 이해하면 훨씬 수월하다.

 

마치며

이제 참조자의 선언 가능 유무와 참조자의 선언 방법에 대한 규칙이 잘 정리되었으리라 믿는다.

어떠한가, 참조자. 생각보다 쉬운 개념이지 않은가?

함께 공부하는 입장의 필자도 이 녀석을 어디다 써먹을지 감이 안 잡힌다.

그래도 알아놔서 나쁠 것도 없고,

우리가 공부하면서 '참조자'라는 녀석을 들어본 적 있는 만큼

잦은 빈도로 사용되는 개념이라 믿어 의심치 않는다.

그럼 다음에는 '참조자와 함수'에 관하여 공부해보도록 하겠다.


참고 및 출처

  • 윤성우 열혈 C++ 프로그래밍