참조자의 활용에는 함수가 큰 위치를 차지한다. 따라서 함수와 관련해서 참조자를 이야기
Call-by-value & call-by-reference
C언어를 공부하면서 배운 함수의 두 가지 호출방식
- Call-by-value : 값을 인자로 전달하는 함수의 호출방식
- Call-by-reference : 주소 값을 인자로 전달하는 함수의 호출방식
Call-by-value
int Adder(int num1, int num2)
{
return num1+num2;
}
두 개의 정수를 인자로 요구.
Call-by-value의 형태로 정의된 함수의 내부에서는, 함수외부에 선언된 변수에 접근이 불가능하다.
따라서 두 변수에 저장된 값을 서로 바꿔서 저장할 목적으로 다음과 가팅 함수를 정의하면 원하는 결과를 얻을 수 없다.
void swapbyvalue(int num1, int num2)
{
int temp = num1;
num1 = num2;
num2 = temp;
// call-by-value
}
위 함수를 대상으로 다음의 main 함수를 실행하면
#include <iostream>
using namespace std;
void swapbyvalue(int num1, int num2)
{
int temp = num1;
num1 = num2;
num2 = temp;
// call-by-value
}
int main(void)
{
int val1 = 20;
int val2 = 30;
swapbyvalue(val1, val2);
cout << "val1 : " << val1 << endl;
cout << "val2 : " << val2 << endl;
return 0;
}
값이 서로 바뀌지 않았다.
그래서 필요한 것이 다음과 같은 call-by-reference 기반의 함수
#include <iostream>
using namespace std;
void swapbyvalue(int *ptr1, int *ptr2)
{
int temp = *ptr1;
*ptr1 = *ptr2;
*ptr2 = temp;
// call-by-reference
}
int main(void)
{
int val1 = 20;
int val2 = 30;
swapbyvalue(&val1, &val2);
cout << "val1 : " << val1 << endl;
cout << "val2 : " << val2 << endl;
return 0;
}
값이 바뀐 것을 볼 수 있다.
Call-by-address? call-by-reference!
int * simplefunc(int * ptr)
{
...
}
위 함수의 정의가 call-by-value? 아니면 call-by-reference?
이는 call-by-value가 될 수도 있고 call-by-reference가 될 수도 있다.
다음과 같이 정의가 되면 call-by-value
int * simplefunc(int * ptr)
{
return ptr+1; // 주소 값을 증가시켜서 반환
}
이 함수의 연산 주체는 값(value)이다. 다만 그 값이 주소 값 일뿐다. 주소 값에 1을 더한 결과를 반환하는 연산을 하니(포인터 연산의 결과로 4가 증가한다).
흔히 우리가 생각하는, 주소 값을 이용해서 함수 외부에 선언된 변수에 접근하는 Call-by-reference와는 거리가 멀다.
반면 아래와 같이 정의되면 call-by-reference다.
#include <iostream>
int* simplefunc(int* ptr)
{
if(ptr == NULL)
return NULL;
*ptr = 20;
return ptr;
}
이 함수에서는 주소 값을 이용해서 함수 외부에 선언된 변수를 '참조(reference)' 했으니, 이는 call-by-reference이다.
"주소 값을 전달받아서, 함수 외부에 선언된 변수에 접근하는 형태의 함수 호출"
즉, 주소 값이 외부 변수의 참조도구로 사용되는 함수의 호출을 뜻함.
이럿듯 주소 값이 전달되었다는 사실이 중요한 것이 아니고, 주소 값이 참조의 도구로 사용되었다는 사실이 중요한 것이며, 이것이 call-by-value와 call-by-reference를 구분하는 기준.
C++에서는 함수 외부에 선언된 변수의 접근방법이 두 가지가 존재한다.
- 주소 값을 이용한 Call-by-reference
- 참조자를 이용한 Call-by-reference
이렇듯 C++에서는 두 가지 방식으로 Call-by-reference 함수의 정의가 가능.
위의 내용들을 이유로 call-by-address라는 표현을 가급적 사용하지 않은 것임
참조자를 이용한 Call-by-reference
참조자를 이용해서 함수를 정의해도 call-by-reference가 가능, 즉 참조자를 이용해서 함수 내에서 함수외부에 선언된 변수에 접근할 수 있다는 말
#include <iostream>
using namespace std;
void swapref(int& ref1, int& ref2) // main 함수에서 선언된 변수의 참조자가 됨
{
int temp = ref1;
ref1 = ref2;
ref2 = temp;
}
int main()
{
int val1 = 10;
int val2 = 20;
swapref(val1, val2); // 매개변수로 참조자 선언
cout << "val1 : " << val1 << endl;
cout << "val2 : " << val2 << endl;
return 0;
}
매개변수는 함수가 호출되어야 초기화가 진행되는 변수들이기 때문에 위와 같이 정의해도 문제가 발생하지 않는다.
위 그림에서 보이는 대로 매개변수로 선언된 참조자 ref1과 ref2는 main 함수에서 선언된 변수 val1과 val2의 또 다른 이름이 된다.
그리고 swapref 함수를 통해서 값의 교환 과정을 거치기 때문에, 그 결과는 실제로 val1과 val2의 값의 교환으로 이어진다.
참조자를 이용한 Call-by-reference의 황당함과 const 참조자
포인터는 잘못 사용할 확률이 높고, 참조자의 활용이 상대적으로 포인터의 활용보다 쉽기 때문에, 참조자 기반의 함수정의가 더 좋은 선택이라 생각할 수 있다.
그러나 참조자 기반의 함수정의에 좋은 점만 있는 것은 아니다.
예시
int num=24;
func(num);
cout<<num<<endl;
C언어의 관점에서는 100% 24 출력된다.
그러나, C++에서는 얼마나 출력될 지 알 수 없다.
함수가 아래와 같이 정의되어 있으면 24가 출력될 수 있지만
void func(int val)
만약 아래와 같이 정의되어 있으면, 참조자를 이용해서 num에 저장된 값을 바꿀 수 있기 때문이다.
void func(int &val)
- 이는 참조자의 단점이 됨
- 코드를 분석하는 과정에서 함수의 호출문장만 보고도 함수의 특성을 어느 정도 판단할 수 있어야 함
- 그러나 참조자를 사용하는 경우, 함수의 원형을 확인해야 하고, 확인결과 참조자가 매개변수의 선언에 와있다면, 함수의 몸체까지 문장 단위로 확인을 해서 참조자를 통한 값의 변경이 일어나는지를 확인해야 한다.
- 그렇다면 이러한 단점을 어떻게 해결?
- 완벽한 해결은 불가능, 해결하려면 참조자 기반의 함수정의를 하지 말아야 함
- 그러나 const 키워드를 이용하면 이러한 단점을 어느 정도는 극복할 수 있음
void func(const int &val)
참조자 ref에 const 선언이 추가
"함수 func 내에서 참조자 ref를 이용한 값의 변경은 하지 않겠다."
여기서 const 선언으로 인해서, 참조자 ref에 값을 저장하는 경우 컴파일 에러가 발생.
따라서 함수 내에서 값의 변경이 이뤄지지 않음을 알 수 있음
"함수 내에서, 참조자를 통한 값의 변경을 진행하지 않을 경우, 참조자를 const로 선언해서, 함수의 원형만 봐도 값의 변경이 이뤄지지 않음을 알 수 있게 한다."
사소한 것 같지만 중요한 원칙과 습관
예제
#include <iostream>
using namespace std;
void swapref(const int& ref1, const int& ref2)
{
int temp = ref1;
ref1 = ref2;
ref2 = temp;
}
int main()
{
int val1 = 10;
int val2 = 20;
swapref(val1, val2);
cout << "val1 : " << val1 << endl;
cout << "val2 : " << val2 << endl;
return 0;
}
컴파일 에러 발생
반환형이 참조형(Reference Type)인 경우
함수의 반환형에도 참조형이 선언될 수 있다.
int& refretfunc1(int &ref)
{
ref++;
return ref;
}
위의 함수에서는 매개변수로 참조자가 선언되었는데, 이 참조자를 그대로 반환하고 있다.
"매개변수가 참조자인데, 이를 반환하니까 반환형이 참조형인 거구나!" => 잘못된 판단
다음과 같이 참조자를 반환해도 반환형은 참조형일 수 있기 때문
int refretfunc1(int &ref)
{
ref++;
return ref;
}
예제(반환형 참조자, 대입 참조자)
#include <iostream>
using namespace std;
int& refretfunc1(int& ref)
{
ref++;
return ref;
}
int main(void)
{
int num1 = 1;
int& num2 = refretfunc1(num1); // num2는 num1을 참조함
num1++;
num2++;
cout << num1 << endl;
cout << num2 << endl;
}
참조형으로 반환된 값을 참조자에 저장하면, 아래와 같이 참조의 관계가 하나 더 추가된다.
int num1=1;
int &ref=num1; // 인자의 전달과정에서 일어난 일
int &num2=ref; // 함수의 반환과 반환 값의 저장에서 일어난 일
함수 retretfunc의 매개변수로 선언된 참조자 ref는 지역변수와 동일한 성격을 가진다.
즉, retrrtfunc이 반환을 하면, 참조자 ref는 소멸된다.
그러나 참조자는 참조자일뿐, 그 자체로 변수는 아니기 때문에 참조자가 참조하는 변수는 소멸되지 않는다.
즉, 함수의 반환으로 인한 참조자의 소멸결과는 다음과 같다.
그렇다면 다음 예제(반환형은 참조자, 대입은 변수)
#include <iostream>
using namespace std;
int& refret1(int& ref)
{
ref++;
return ref;
}
int main(void)
{
int num1 = 1;
int num2 = refret1(num1);
num1++;
num2 += 100;
cout << num1 << endl;
cout << num2 << endl;
}
int num1=1;
int &ref=num1; // 인자의 전달과정에서 일어난 일
int num2=ref; // 함수의 반환과 반환 값의 저장에서 일어난 일
반환형이 참조형인 경우, 반환 값을 무엇으러 저장하느냐에 따라서 그 결과에 차이가 있으므로 적절한 선택을 해야 한다.
그렇다면 다음 예제(참조자 반환, 반환형은 기본형, 대입은 변수)
#include <iostream>
using namespace std;
int refret(int& ref)
{
ref++;
return ref;
}
int main(void)
{
int num1 = 1;
int num2 = refret(num1);
num1++; // 3
num2 += 100; // 102
cout << num1 << endl; //
cout << num2 << endl; //
}
실험 결과를 보면 바로 이전 코드와 차이가 없다. 뿐만 아니라, 실행과정에서 일어나는 일도 위 그림과 동일하다.
하지만 다음의 차이가 있다.
반환형이 참조형인 함수는 반환 값을 다음과 같이 두 가지 형태로 저장할 수 있다.
int num2 = refretfunc(num1); // (O)
int &num2 = refretfunc(num1); // (O)
하지만 기본자료형으로 선언된 함수의 반환 값은 반드시 변수에 저장해야 한다.
반환 값은 상수나 다름없기 때문이다.
int num2 = refretfunc(num1); // (O)
int &num2 = refretfunc(num1); // (X)
잘못된 참조의 반환
아래는 예상되는 문제가 있다.
int& retureffunc(int n)
{
int num = 20;
num+=n;
return num;
}
지역변수 num에 저장된 값을 반환하지 않고 num을 참조의 형태로 반환하고 있다.
따라서 다음의 형태로 함수를 호출하고 나면
int &ref = retureffunc(10);
지역변수 num에 ref라는 또 하나의 이름이 붙게 된다.
그러나 함수가 반환이 되면, 정작 지역변수 num은 소멸이 된다.
따라서 위의 함수처럼 지역변수를 참조형으로 반환하는 일은 없어야 한다.
const 참조자의 또 다른 특징
아래는 어떤 논리적인 문제점?
const int num=20;
int &ref = num;
ref+=10;
cout<<num<<endl;
const 선언을 통해서 변수 num을 상수화했는데, 참조자 ref를 통해서 값을 변경한다?
이것을 허용한다면, 사실상 변수 num의 상수화는 의미가 없다.
상수화되었다면 어떠한 경로를 통하더라도 값의 변경을 허용하면 안된다.
컴파일에러를 일으킴
따라서 상수화된 변수에 대한 참조자 선언은 다음과 같이 한다.
const int num=20;
const int &ref = num;
이렇데 선언이 되면 ref를 통한 값의 변경이 불가능, 상수화에 대한 논리적인 문제점 발생 X
그리고 const 참조자는 다음과 같이 상수도 참조가 가능하다.
const int &num=20;
참조자가 상수를 참조?
int num = 20+30;
여기서, 20이나 30과 같은 프로그램상에서 표현되는 숫자를 리터럴(literal) 또는 리터럴 상수(lieteral constant).
"암시적으로 존재하는 값. 다음 행으로 넘어가면 존재하지 않는 상수"
덧셈연산을 위해서는 20도, 그리고 30도 모두 메모리 공간에 저장되어야 한다.
하지만 메모리 공간에 저장되었다고 해서 재 참조가 가능한 값은 아니다.
즉, 다음 행으로 넘어가면 소멸되는 상수라고 해도 틀리지 않는다.
그런데 상수를 참조한다는 것이 이치에 맞음?
const int &ref = 30;
이는 숫자 30이 메모리 공간에 계속 남아있을 때에나 성립이 가능한 문장.
그래서 C++에서는 위의 문장이 성립할 수 있도록, const 참조자를 이용해서 상수를 참조할 때 '임시변수'라는 것을 만든다.
그리고 이 장소에 상수 30을 저장하고선 참조자가 이를 참조하게끔 한다.
이렇듯 임시로 생성한 변수를 상수화하여 이를 참조자가 참조하게끔 하는 구조.
결과적으로 상수화된 변수를 참조하는 형태
아래와 같이 활용 가능하다
int Adder(const int &num1, const int &num2)
{
return num1 + num2;
}
위와 같이 정의된 함수에 인자의 전달을 목적으로 변수를 선언한다는 것은 매우 번거로운 일이 아닐 수 없다.
그러나 임시변수의 생성을 통한 const 참조자의 상수참조를 허용함으로써 위의 함수는 아래와 같이 호출 가능
cout << Adder(3, 4) << endl;
참고 : [윤성우 열혈 C++ 프로그래밍] - 대부분의 내용 및 코드는 이 책에서 개인 공부 정리 목적으로 참고하였습니다.
'Language&Framework&Etc > C++' 카테고리의 다른 글
C언어 기반의 C++ 2(02-6) C++에서 C언어의 표준함수 호출하기 (0) | 2020.12.23 |
---|---|
C언어 기반의 C++ 2(02-5) malloc & free를 대신하는 new & delete (0) | 2020.12.23 |
C언어 기반의 C++ 2(02-3) 참조자(Reference)의 이해 (0) | 2020.12.21 |
C언어 기반의 C++ 2(02-2) 새로운 자료형 bool (0) | 2020.12.20 |
C언어 기반의 C++ 2(02-1) 시작에 앞서 (0) | 2020.12.20 |