다양한 반환 값들(값, 주소, 참조, 구조체, 튜플) Returning values 본문
다양한 반환 값들(값, 주소, 참조, 구조체, 튜플) Returning values
함수로 들어가는 인자들을 매개변수에게 어떻게 넣어줄 것인가에 대해서 공부했었다.
이번에는 계산한 결과 혹은 함수가 수행한 결과를 어떻게 돌려받을 것인가에 대해서 살펴보겠다.
Return by value
값을 돌려받는 방법에 대해서 먼저 살펴보겠다.
값으로 반환(return by value)은 가장 간단하고 안전하다.
값이 반환되면 복사본이 호출자에게 반환된다.
값으로 전달처럼 리터럴, 변수 및 표현식을 반환할 수 있다.
값으로 반환의 또 다른 장점은 범위 지정 문제를 걱정할 필요 없다는 것이다.
함수 내에서 선언된 지역 변수를 반환할 수 있다.
함수가 반환되기 전에 지역 변수가 평가되고,
값의 복사본이 호출자에게 반환되기 때문에 함수의 지역 변수가 함수의 끝에서 범위를 벗어날 때 아무런 문제가 없다.
예제를 보자
getValue라는 함수가 있고 매개변수는 int x 로 지정이 되어있다.
main에 있는 getValue에서 3 리터럴을 넣고 호출을 하면
이 리터럴 3이 함수에 있는 int x 변수가 선언됨과 동시에 복사가 된다
int x의 범위는 getValue함수의 { } 중괄호 범위가 된다.
그다음 이 매개변수 x에 들어있는 값 3은
함수에 있는 value라는 int값을 결정할 때 사용이 된다.
3 * 2 니까 6이 된다.
int value가 선언이 되고 거기에 6이 복사해서 초기화된다.
그리고 이 함수의 return 타입은 정수다
그래서 value가 리턴이 되면서 함수 밖으로 전달이 된다.
이때 main에 있는 변수 value는 정수형으로 main 영역 안에 선언이 되고
함수가 끝남과 동시에 돌려받은 3 * 2 = 6이라는 값이 value에 복사해서 들어간다.
값으로 반환은 함수 내에서 선언된 (지역)변수를 반환하거나
값으로 전달된 매개 변수를 반환할 때 가장 적절하다. 가장 간단하고 안전하다.
단점은 복사나 변수의 생성 이런 것들이 여러 번 일어난다는 것이다.
아무래도 속도가 조금 떨어질 것이다.
간단한 경우에는 크게 문제가 안되고
structure나 class를 사용할 때 데이터가 엄청나게 많다면 값으로 반환은 느리다.
또는 array를 쓰는데 그 array에 있는 많은 데이터들이 전부 매번 복사가 되는 방식이라면 곤란하다.
값으로 반환을 사용해야 하는 경우:
- 함수 내에서 선언된 (지역) 변수를 반환할 때
- 값으로 전달된 매개 변수를 반환할 때
값으로 반환을 사용해야 하지 않아야 하는 경우:
- 배열이나 포인터를 반환할 때 (주소로 반환 사용)
- 구조체 또는 클래스를 반환할 때 (참조로 반환 사용)
Return by address
주소로 반환(return by address)은 호출자에게 변수의 주소를 반환한다.
그러나 함수 안에서 선언된 지역 변수의 주소를 반환하려고 하면 프로그램에서 정의되지 않은 동작이 발생한다.
다음 예제를 보자.
이전 예제에서 int에 * 찍어주면은 포인터로 바뀌고
return은 & 붙여서 주소로 바꿔주면 된다.
그러면 받는 쪽에서는 두 가지 방법이 있다.
첫 번째는 주소로 받아버리는 방법이 있다.
두 번째는
value는 값으로 받고
getValue()를 de-referencing을 해서 받아버리는 방법이 있다.
이때 이렇게 de-referencing 해서 값으로 받는 것은 크게 문제가 없다.
하지만 권장하지 않는다.
왜냐하면
이 value가 영역을 벗어남과 동시에 사라져 버리기 때문이다.
사라질 변수를 de-referencing 하는 게 아무래도 안전하지는 않다.
이게 경우에 따라서는 문제가 되기도 하고 안되기도 하다.
한번 찍어보면
당장은 원하는 값이 나왔다고 하더라도 이런 코딩은 가급적 안 하시는 게 좋다.
다른 방법도 얼마든지 많은데 굳이 이렇게 할 필요 없다.
그리고
이렇게 주소로 받는 방법은 더 위험하다.
왜냐하면 이 변수의 주소를 가지고 있는데
얘는 이미 영역을 넘어서면서 사라진다.
변수는 사라졌는데 메모리 주소만 알고 있는 꼴이 된다. 굉장히 위험하다.
실행시켜보면
그래서 사실 좀 더 위험하다
그런데 아주 특이한 방식으로 return by address를 사용하기도 한다..
간혹 array를 만들 때
이 부분을 함수로 뽑아내고 싶을 때가 생긴다.
어떤 메모리를 생성을 하고 그 메모리의 포인터를 함수의 리턴 값으로 돌려받는 패턴이 있다.
나중에 디자인 패턴 같은 거 공부할 텐데
factory pattern(공장 패턴 : 게임팩에서 다룬다) 같은 많이 사용되는 패턴에서 흔하게 사용되는 방식이다.
size가 파라미터로 들어오고
return 하게 되면은 new int [size]가 주소를 반환하게 된다.
위 예제 같은 간단한 경우에는 잘 쓰이지 않지만 뒤에 가면 아주 많이 쓰일 수 있다.
그거는 객체 지향 배우고 나서 다시 설명드리겠다.
이것 또한 위험이 있다.
이전에 new와 delete 설명드리면서
new가 있으면 그것과 쌍이 되는 delete이 있어야 된다고 말씀드렸다.
new를 한 함수가 여기 있는데 delete을 어디서 해야 할지 막막할 수 있다.
현재 구조에서 delete는
new는 allocateMemory 함수에 있고 delete은 main에 있고 이렇게 따로 있으면 좀 정신이 없다.
이런 식으로 동적 메모리 할당하는 방법은 프로그래머를 힘들게 만들 가능성이 있다.
어쨌거나 이런 경우에 대해서는 기본적인 문법이 이렇다는 거 설명드린 것
실제 현업에서 사용하실 때는 훨씬 더 편한 방법 사용한다.
주소로 반환을 사용해야 하는 경우:
- 동적 할당된 메모리를 반환할 때
- 주소로 전달된 매개 변수를 반환할 때
주소로 반환을 사용해야 하지 않아야 하는 경우:
- 함수 내에서 선언된 (지역) 변수를 반환할 때 (값으로 반환 사용)
- 참조로 전달된 구조체 또는 클래스를 반환할 때 (참조로 반환 사용)
Return by Reference
변수가 참조로 반환(return by reference)되면 변수에 대한 참조가 호출자에게 반환된다.
그런 다음 호출자는 이 참조를 사용해서 변수를 계속 수정할 수 있다.
참조 반환은 빠르므로 구조체와 클래스를 반환할 때 유용하다.
그러나 주소로 반환처럼 참조로 함수 지역 변수를 반환하면 안 된다.
다음 예제를 보자.
이렇게 되면 getValue()가 레퍼런스를 반환해서
레퍼런스가 잠깐 남아있는 상태에서
그 레퍼런스가 가리키고 있는 변수의 실제값이 main에 있는 value로 복사해서 들어온다.
찍어보면
그렇다면 문제가 되는 경우는 뭘까?
받는 쪽에서도 레퍼런스로 받아버리면
main에 있는 &value는 함수에 있는 value에 대한 레퍼런스다.
왜냐하면
그리고 마찬가지로 함수에 있는 value는
이 영역이 넘어가면 사라지는 애다.
문제가 생길 가능성이 높다.
문제가 없나 한번 찍어보자
지금은 값이 잘 나온다.
그런데 한번 더 찍어보자
두 번째 출력할 때 문제가 생긴다.
함수에 있던 value의 메모리가 사라진 거다.
return 값 받는 동안만 임시로 좀 잡아주고 있다가
그걸 받았으니까 난 할 일 다 했다! 하면서 사라진 거다.
os로 메모리가 돌아간 거다.
그런데 이 레퍼런스가 아직 살아있으니까
값을 de-referencing 하려고 시도를 한 거고
메모리가 죽었으니 쓰레기 값이 나와버린 비극적인 사태가 발생을 한 거다.
이렇게 사용하는 것은 매우 안 좋다.
그래서 간혹 이렇게 const를 넣는 경우도 있는데
이렇게 메모리가 일시적으로 생겼다가 사라지는 지역 변수일 경우에는
이렇게 return 해버리면 안 된다.
문제는 또 return 할 때는 이게 레퍼런스인지 value인지 구분이 안 가고
여기 return type 정의하는 부분에서 확인을 해야 하기 때문에 실수할 가능성이 조금 더 높다.
그리고 이렇게 de-referencing을 추가적으로 했을 때 문제가 생기는 건
아까 말씀드린 return by address에서도 똑같이 나타날 수 있다.
그래서 쓰지 않는 게 좋을 수 있다고 말씀드린 거다.
그럼 return by reference는 쓸모가 없나? 그건 또 아니다
오히려 더 편하게 쓰는 경우가 있다.
어떤 경우가 있는지 보여드리겠다.
my_array의 30번째 값을 10으로 바꾸는 코드를 만들었다.
그리고 my_array 같은 사용자 정의 데이터 타입의 array를 일부를 고치는데
별도의 함수가 고치게 만들 수도 있다.
함수에 파라미터로 array<int, 100> my_array와 idx를 넣어주고
return으로 my_array [idx]를 해주고
main에서
get(my_array, 30) = 1024;라고 만든 다음에
my_array[30] 이 값을 다시 한번 찍어보자.
이 경우에는 여기에 메모리가 명확하게 잡혀있다.
전역 변수로 나가 있어도 똑같이 작동할 거다.
메모리가 이렇게 잡혀있는 상황에서
그러면서 마치
이런 패턴으로 사용하는 경우가 아주 많다.
메모리는 이렇게 어딘가 안전하게 저장되어 있어서
함수가 실행되기 전이나 후나 메모리는 확실하게 잡혀있는 상태인 거다
그 상태에서 레퍼런스만 보내주고
그걸 가지고 변수처럼 편리하게 작업할 수 있게 해 주는 거다.
예를 들면
수학 라이브러리를 구현하실 때 라이브러리에 들어있는 클래스나 사용자 정의 데이터형 등
좀 수학식과 비슷하게 코딩이 되도록 구현을 하신다면 이런 기술을 많이 사용하실 수 있다.
참조로 반환을 사용해야 하는 경우:
- 참조 매개 변수를 반환할 때
- 함수에 전달된 배열의 요소를 반환할 때
- 함수의 끝에서 소멸하지 않는 구조체나 클래스를 반환할 때
참조로 반환을 사용해야 하지 않아야 하는 경우:
- 함수 내에서 선언된 (지역) 변수를 반환할 때 (값으로 반환 사용)
- 기본 배열이나 포인터 값을 반환할 때 (주소로 반환 사용)
Return by Structure
return 값을 여러 개를 return 하고 싶을 때가 있을 거다
예를 들어서 정수 3개를 return 받고 싶다고 하면
structure로 받는 것이 일반적인 방법이었다.
리턴 값에 int 같은 자료형 대신에 딱 S를 적어주고
사용자 정의 데이터형을 리턴 값으로 받을 수 있다.
이런 식으로 구조체를 통해서 return 값을 받아버리면은
값을 여러 개를 한꺼번에 받는 효과가 나타난다.
이렇게 처리를 할 수가 있다.
이 방식의 단점은 명확하다
함수 한 개를 만들 때마다 구조체 하나씩 만들어줘야 된다.
이러면 구현 overload가 꽤 크다.
옛날 라이브러리는 이렇게 짜여 있다.
특히 게임이나 그래픽스 하시려고 direct x 프로그래밍하게 되면
direct x 라이브러리를 사용하시게 되면 그 내부에 구조체가 엄청나게 많다.
함수 호출할 때도 구조체에 포인터로 넣고
받을 때도 구조체로 받거나 구조체에 포인터로 받거나 하는 경우가 많다.
아무튼 이런 방식이 많이 사용이 됐었다.
C 스타일 코딩을 꼭 하셔야 하는 상황이라면 (C++를 사용하실 수 없는 경우라면)
어쩔 수 없이 저런 방법을 사용하셔야 할 수도 있다.
대신에 아무래도 C니까 속도는 더 빠를 거다.
Return by Tuple
다른 방법을 또 하나 알려드리자면 튜플을 사용하는 거다.
#include <tuple> 해주고
만약 정수형과 double 형 두 개의 return 값을 받고 싶다면
std::tuple<int, double> 이렇게 return 타입을 정해주면 된다.
std::tuple<int, double> 자체가 사용자 정의 자료형처럼 되는 거다.
그리고 return을 해줄 때도
int a = 10; double d = 3.14;
이렇게 계산을 하고
std::make_tuple 이라는게 있다
안에(a, d)를 넣어버리면 되고
받는 쪽에서는 tuple로 받으면 된다.
std::get<0>(my_tp); 는 a를 나타내고
std::get<1>(my_tp); 는 d를 나타낸다.
실행시켜보면
두 개 이상의 값을 받을 수 있다.
그리고 여기에 struct가 들어갈 수도 있다.
만약에 structure가 공통적으로 정의가 되어있다면 안쓸 이유가 없다
그런데 이것도 여전히 좀 불편하다
받는 쪽에서 이렇게 한다는 게 좀 불편하고 get<0>, get<1> 이렇게 하는 거 좀 불편하다
그래서 C++ 17을 써보면
여기서 바꿔주면 됨
바꿔 주게 되면은
함수에서 리런 할 때 turple로 a와 d를 보내고 있다.
걔네를
auto [a, d] = getTuple(); 여기서 각각 변수를 선언하면서 받아주는 것이다.
a와 d에 마우스 갖다 대 보면
이렇게 뜬다.
여기까지 다양한 방식으로 return 값을 돌려받는 방법에 대해 말씀드렸다.
추후에 클래스를 배우고 나면 그때 return value optimization에 대해서도 추가적으로 말씀드리겠다.
'💘 C++ > 함수' 카테고리의 다른 글
함수 오버로딩 (Function Overloading) (0) | 2022.08.06 |
---|---|
인라인 함수 (Inline Functions) (0) | 2022.07.31 |
주소에 의한 인수 전달 (Passing Arguments by address(Call by Address)) (2) | 2022.07.27 |
참조에 의한 인수 전달 (Passing Arguments by Reference(Call by Reference)) (0) | 2022.07.26 |
값에 의한 인수 전달 (Passing Arguments by Value(Call by Value)) (0) | 2022.07.24 |