들어가며
1720년에는 마르세이유 역병이 창궐했다.
1820년에는 콜레라, 1920년에는 림프절 페스트,
그리고 2020년에는 코로나 바이러스.
이쯤 되면 지구가 친환경을 직접 실천하는 것이 아닐까 싶다.
100년 주기로 이렇게 기승인 것을 보면,
역병 스킬 쿨타임이 꽤나 긴 것 같다.
진짜 Call-By-Reference
전 글에서 말했듯이 이번엔 진짜 'Call-By-Reference' 방식을 학습할 것이다.
이름에 맞게 참조자를 사용한 함수 호출 방식 말이다.
전에도 말했다시피 핵심은 '함수 외부에 선언된 변수에 접근' 이지 않은가?
일단 빠르게 코드와 부딪쳐보자.
int SwapByRef(int& ref1, int& ref2)
{
int temp = ref1;
ref1 = ref2;
ref2 = temp;
} // Call-By-Reference
함수의 매개변수 위치에 참조자가 있는 것을 확인할 수 있다.
'아니 잠깐 필자 양반, 매개변수 저거 뭐여. 선언과 동시에 초기화해야 한다며?'
그렇다!
하지만 과정을 잘 생각해 보라.
'매개 변수'는 함수가 호출되어야 초기화가 진행되는 변수들이다.
감이 좀 오는가?
그렇다!
위의 매개변수 선언은 초기화가 이뤄지지 않은 것이 아니라
함수 호출 시에 전달되는 인자로 초기화하겠다는 의미의 선언이다.
이해가 되었으리라 믿고 main으로 넘어가 보자.
int main()
{
int val1 = 10;
int val2 = 20;
SwapByRef(val1, val2);
cout << val1 << endl;
cout << val2 << endl;
return 0;
}
딱히 특별한 것은 없어 보인다.
하지만 아까 포착한 포인트를 다시 한번 살펴보면
우리가 예측할 수 있는 다음과 같은 일이 벌어진다.
위 그림에서 보이는 대로 매개변수로 선언된 참조자 ref1과 ref2는
main 함수에서 선언된 변수 val1과 val2로 초기화되어
같은 메모리 상의 공간에 접근할 수 있게 된다.
그리고 SwapByRef 함수 내에서는
이 두 참조자를 통해서 실제 값의 교환이 이루어진다.
이제 좀 참조자를 어떻게 사용하는지 감이 오는가?
이것이 진짜 'Call-By-Reference' 방식의 함수 호출이다.
주소(포인터)를 이용하든지, (Call-By-Address)
참조자(레퍼런스)를 이용하든지
그냥 다 Call-By-Reference 방식의 일종으로 생각하면 편하다.
참조자 방식의 Call-By-Reference의 주의사항
포인터는 잘못 사용할 가능성이 높다.
상대적으로 참조자가 포인터 보다 활용이 더 쉽기 때문에,
참조자 기반의 함수가 더 좋다고 생각할 수도 있다.
하지만 단점도 있지 않겠는가?
함수의 매개변수가 참조자이고 이 함수에 인자가 전달된다고 가정해보자.
참조자는 인자로 받은 변수의 값을 변경할 수 있다.
따라서 코드의 길이가 길어지고 구성이 복잡해질수록,
함수의 호출 문장만 보고는 이 함수가 뭐하는 녀석인지 알기 어렵다.
이는 곧 함수 원형을 확인해야하는 귀찮은 단계에 다다른다.
왜냐하면 참조자가 함수 내부에서 어떤 역할과 기능을 하는지 알 수 없기 때문이다.
상당히 귀찮지 않지 않을 수가 없지 아니한가?
따라서 위 문장처럼 명료성이 떨어져 유지 보수가 어려워진다.
그런 이유로 사람들은 참조자 보다는 포인터를 사용하는 경우가 더 많다.
하지만 반드시 사용해야 하는 상황이 절대 없으리란 보장은 없지 않은가?
그렇기에 사용하는 것이 바로 'const' 키워드이다.
사용법은 다음과 같다.
void happyFunc(const int& ref) {······}
의미는 다음과 같다.
'참조자 ref로 값은 안 건드릴 거니까 함부로 접근하지 말어. 알갓어?'
정도가 되겠다.
그리고 이런 암묵적인 규칙도 알아두면 좋다.
'여러분덜, 함수 내에서 참조자로 값 안 바꿀거면 'const' 꼭 붙이셈 ㅇㅇ'
이를 통하여 함수의 원형만 봐도 그 참조자를 통한
값의 변경이 이뤄지지 않음을 알 수 있게 해주겠다는 뜻이다.
만약 하지 않았다면?
그럼 함수의 몸체를 일일히 다 확인하는 매우 번거롭고 귀찮은 사태가 벌어진다.
'히히 한번 엿되보라지!' 라는 심보가 아니라면 유의하도록 하자.
참조형 반환
함수의 반환형에 참조형이 선언될 수 있다.
이건 또 뭔 소린가 싶은가?
다음의 코드가 가장 대표적인 경우이다.
int& RefRetFuncOne(int& ref)
{
ref++;
return ref;
}
'아, 매개변수도 참조자고 이걸 반환해서 반환형이 참조자인가?'
뭔소린가 싶은가?
나도 그렇다.
저게 뭔 소린가 싶다.
하지만 저 생각이 오류를 범하고 있다는 것은 확실하다.
다음의 코드에 경우는 어떠한가?
int RefRetFuncOne(int& ref)
{
ref++;
return ref;
}
위 코드의 경우처럼 참조자를 받환해도 반환형은 참조형이 아닐 수 있기 때문이다.
자, 둘의 차이점이 무엇인지 대충 예상이 가는가?
그렇다면 다음의 전반적인 코드를 보고 제대로 이해해 보도록 하자.
#include <iostream>
using namespace std;
int& RefRetFuncOne(int& ref)
{
ref++;
return ref;
}
int main()
{
int num1 = 1;
int& num2 = RefRetFuncOne(num1); // int& num2 = ref;
num1++;
num2++;
cout << num1 << endl;
cout << num2 << endl;
return 0;
}
어떠한가? 예상대로의 결과인가?
아니면 머릿속의 중앙 처리 장치가 버벅대는가?
그림으로 설명하면 다음과 같다.
위 그림에서 보이듯이, 참조형으로 반환된 값을 참조자에 저장하면
참조 관계가 중첩되어 참조자가 하나 더 생기는 꼴이 된다.
즉, 저 코드 내부의 num1과 관련한 참조자 관계는 다음과 같다.
int num1 = 1; // 일반 변수 선언 및 초기화
int& ref = num1; // 인자 전달 과정에서 매개변수 초기화
int& num2 = ref; // 함수의 참조자 반환값으로 참조자 초기화
// num1 = ref = num2
여러모로 신분 세탁을 거하게 한 모습이다.
이렇게 된다면 당연하게도 가독성과 명료성이 떨어지는건
어찌보면 당연하게 느껴진다.
참조형 반환(진) - 상
모두가 알지만 모두가 알아야 하는 사실이 있다.
함수 'RefRetFuncOne'의 매개변수로 선언된 참조자 'ref'는
'지역 변수' 와 동일한 성격을 가진다는 것이다.
그러니까, 함수 'RefRetFuncOne'이 반환하게 되면
함수 내부에 지역적으로 선언된 매개변수이자 참조자인 ref는 소멸된다는 것이다.
그러나, 참조자는 소멸되었으나 원래의 변수는 영향을 받지 않는다.
즉, 함수의 반환으로 인한 참조자의 소멸의 관계는 다음과 같다.
마치 프로토스의 질럿의 최후처럼 작렬히 소멸한 모습이다.
그럼 다음의 코드를
int& num2 = RefRetFuncOne(num1);
다음의 코드로
int num2 = RefRetFuncOne(num1);
변경한 후에 실행시킨다면?
우선 두 코드의 차이는 무엇일까.
함수의 반환값을 저장하는 대상이
참조자인가 변수인가의 차이다.
그럼 다음의 코드를 해석하고 흐름을 파악해 보라.
#include <iostream>
using namespace std;
int& RefRetFuncOne(int& ref)
{
ref++;
return ref;
}
int main()
{
int num1 = 1;
int num2 = RefRetFuncOne(num1);
num1+=1;
num2+=10;
cout << num1 << endl;
cout << num2 << endl;
return 0;
}
일부로 명확하게 파악하라고 연산 과정도 살짝 손을 보았다.
그럼 이 코드가 의미하는 바는 무엇이고, 변수나 함수의 관계는 어떠한가?
그에 대한 답은 다음과 같을 것 같은가?
유감이지만 알려주지 않을 것이다.
다음 글에서 알려줄 것이다.
마치며
생각보다 참조자에 대한 개념과 내용이 많았다.
때문에 글을 쓰는 양과 요구되는 시간이 많아져서
꽤나 호흡이 길어지고 있다.
뭔가 필력도 예전같지 않은 것 같다.
최근 코로나 바이러스 때문인지 몰라도
강제로 히키코모리 짓을 하고 있어서 그런지
텐션이 뚝뚝 떨어지는 것 같다.
어흐흑.
참고 및 출처
-
윤성우의 열혈 C++ 프로그래밍