
1. 참조자(References)
1.1 참조자의 이해
참조자는 성격상 포인터와 비유되기 쉬운 개념으로, 참조자를 이해하기 위해서 먼저 변수의 개념을 살펴봐야 한다. 우선, 변수란 할당된 메모리 공간에 붙여진 이름으로, 사용자는 변수를 통해 해당 메모리 공간에 접근할 수 있다. 그런데 C++는 &연산자를 사용하여 할당된 메모리 공간에 둘 이상의 이름을 부여할 수 있는데 이것을 가리켜 참조자(Reference)라고 한다.
int num1 = 2021;
int& num2 = num1; /* 변수 num1에 대한 참조자 num2를 선언합니다. */
int* ptr = &num1 /* 변수 num1의 주소를 포인터 ptr에 저장합니다. */
참조자의 이해를 위해서 변수 num1과 참조자 num2를 선언하였다. 위의 예제에서 &연산자는 두 가지 의미로 사용되었다. 4행의 &연산자는 이미 선언된 변수 num1 앞에 붙어 있는데, 이것은 변수 num1의 주소를 반환하는 역할을 한다. 그러나 2행의 &연산자는 새로 선언되는 변수 앞에 붙어 있는데, 이것은 변수 num1을 참조하는 참조자 num2를 선언하는 역할을 한다. 위의 예제를 실행하면 참조자 num2는 변수 num1의 참조자가 되며, 결과적으로 변수 num1에 num2라는 이름이 하나 더 붙은 꼴이 된다. 즉, 참조자는 참조하는 변수를 대신할 수 있는 또 다른 이름인 것이다.
Reference.cpp
#include <iostream>
using namespace std;
int main(void) {
int num1 = 2002;
int& num2 = num1; /* 변수 num1의 참조자 num2를 선언합니다. */
num2 = 2021;
cout << "변수: " << num1 << endl;
cout << "참조자: " << num2 << endl;
return 0;
}
Reference.cpp 실행 결과
2021
2021
[한줄 요약] 참조자란?
자신이 참조하는 변수를 대신할 수 있는 또 하나의 이름을 말한다.
1.2 참조자와 변수
변수와 비슷한 참조자를 선언하기 위해서 몇 가지 규칙을 숙지해야 한다. 변수를 대상으로 하는 참조자의 선언 규칙은 다음과 같다. 첫 번째, 참조자의 수에는 제한이 없다. 하나의 변수를 참조하는 여러 참조자를 선언할 수 있다. 두 번째, 참조자를 대상으로 참조자를 선언할 수 있다. 하지만 지나치게 참조자를 많이 선언하는 것은 바람직하지 않으며 참조자를 대상으로 또 다른 참조를 하는 일이 흔히 필요하지는 않다. 세 번째, 참조자는 변수에 대해서만 선언할 수 있다. 참조자는 변수에 또 다른 이름을 붙이는 것이기 때문에 const 키워드를 사용하지 않으면 상수를 대상으로 선언할 수 없다. 네 번째, 참조자는 선언과 동시에 변수를 참조해야 한다. 참조자를 미리 선언했다가 나중에 변수를 참조할 수 없으며, 포인터 변수처럼 NULL로 초기화할 수도 없다.
- 참조자 수에는 제한이 없다.
- 참조자를 대상으로 참조자를 선언할 수 있다.
int num1 = 2021;
int& num2 = num1;
int& num3 = num2;
- 참조자는 변수에 대해서만 선언할 수 있다.
- 참조자는 선언과 동시에 변수를 참조해야 한다.
int& num1 = 20; /* 컴파일 오류! 참조자는 상수를 참조할 수 없습니다. */
int& num2; /* 컴파일 오류! 참조자는 선언과 동시에 초기화되어야 합니다. */
int& num3 = NULL; /* 컴파일 오류! 참조자는 NULL을 참조할 수 없습니다. */
- 참조자는 한 번 초기화되면 참조하는 대상을 변경할 수 없다.
- 상수 또는 상수화된 변수에 대한 참조자 선언은 const 키워드를 사용해야 한다.
const int num = 2021;
const int& ref1 = num;
int& ref2 = num /* 컴파일 오류! 상수화된 num을 참조자 ref2로 변경할 수 없습니다. */
[한줄 요약] 참조자의 선언 및 초기화
referenceType& referenceName = variableName;
2. 참조자와 함수
2.1 참조자를 이용한 Call by Reference
C언어에서 Call by Reference는 주소 값을 전달받아 함수 외부에 선언된 변수에 접근하는 함수의 호출을 말한다. 주소 값이 함수 외부에 선언된 변수를 참조하는데 사용되기 때문에 포인터를 매개 변수로 사용한다. 한편, C++는 함수 외부에 선언된 변수에 접근하는 함수 호출로 또 다른 Call by Reference를 제공하는데, 바로 참조자를 이용한 Call by Reference이다.
SwapByReference.cpp
#include <iostream>
using namespace std;
void SwapByReference(int& ref1, int& ref2) {
int temp = ref1;
ref1 = ref2;
ref2 = temp;
}
int main(void) {
int num1 = 10;
int num2 = 20;
SwapByReference(num1, num2);
cout << "num1: " << num1 << endl;
cout << "num2: " << num2 << endl;
}
앞서 참조자와 변수에서 참조자는 선언과 동시에 변수로 초기화되어야 한다고 언급하였다. SwapByReference 함수의 매개 변수는 함수가 호출되어 인자가 전달되어야 초기화되는 매개 변수이므로, 참조자 ref1과 ref2는 함수가 호출될 때 전달되는 인자로 초기화된다.
SwapByReference.cpp 실행 결과
20
10
위의 예제에서 매개 변수로 선언된 참조자 ref1과 ref2는 main 함수에서 선언된 변수 num1과 num2의 또 다른 이름이 된다. 그리고 SwapByReference 함수 안에서 두 참조자를 통해 값의 교환이 이루어지기 때문에 결과적으로 변수 num1과 num2 값의 서로 바뀌게 된다.
[한줄 요약] 참조자를 이용한 Call by Reference
매개 변수를 참조자로 선언하여 함수 외부의 변수에 접근하고, 변수의 값을 변경할 수 있다.
2.2 참조형 반환
함수의 매개 변수를 참조자로 선언하는 것처럼, 함수의 반환형 역시 참조형으로 선언할 수 있다. 단, 함수의 반환형과 반환된 값을 저장하는 변수의 자료형에 따라서 컴파일 오류와 경고를 나타내는 상황이 발생한다.
자료형 | 함수 반환형 | 함수 매개 변수형 |
예 |
1. 값으로 저장 | 일반형 | 일반형 | int x = int Function(int y); |
2. 참조자로 저장 | const int& x = int Function(int y); | ||
3. 값으로 저장 | 일반형 | 참조형 | int x = int Function(int& y); |
4. 참조자로 저장 | const int& x = int Function(int& y); | ||
5. 값으로 저장 | 참조형 | 일반형 | int x = int& Function(int y); |
6. 참조자로 저장 | int& x = int& Function(int y); | ||
7. 값으로 저장 | 참조형 | 참조형 | int x = int& Function(int& y); |
8. 참조자로 저장 | int& x = int& Function(int& y); |
① 함수의 반환형이 일반형이고, 이것을 참조자로 저장할 경우(2번, 4번)
함수에서 기본 자료형으로 반환된 값은 상수이므로 2번과 4번은 상수를 참조하라는 의미를 갖는다. 그런데 이것은 항상 변수를 참조해야 하는 참조자의 규칙에 위배되고 "비const 참조에 대한 초기값은 Lvalue여야 합니다."라는 오류를 발생시킨다. 따라서 굳이 상수를 참조하기 위해서는 const 키워드를 선언해야 한다.
② 함수의 반환형이 참조형이고, 함수의 반환 값을 참조자로 저장하는 경우(6번, 8번)
사용자가 함수 안에서 지역 변수를 선언하고 선언된 지역 변수를 참조형으로 반환하는 경우, 함수가 종료됨에 따라서 사용자가 선언한 지역 변수는 메모리에서 자연스럽게 사라진다. 그런데 6번과 8번은 메모리에서 사라진 지역 변수를 참조자로 참조하라는 의미를 갖는다. 메모리에서 사라진 지역 변수를 다시 참조할 수 없으므로 "지역 변수 또는 임시: x의 주소를 반환하고 있습니다." 경고가 발생한다. 이때 참조자가 참조하는 값이 의도한 것과 전혀 다른 쓰레기 값을 참조하므로 주의해야 한다.
ReferenceReturn.cpp
#include <iostream>
using namespace std;
int FunctionOne(int x) {
x += 1;
return x;
}
int FunctionTwo(int& x) {
x += 1;
return x;
}
int& FunctionThree(int x) {
x += 1;
return x;
}
int& FunctionFour(int& x) {
x += 1;
return x;
}
int main(void) {
int x = 100;
int a1 = FunctionOne(x);
const int& b1 = FunctionOne(x);// 비const 참조에 대한 초기값은 Lvalue여야 합니다.
cout << a1 << endl;
cout << b1 << endl;
int a2 = FunctionTwo(x);
const int& b2 = FunctionTwo(x);// 비const 참조에 대한 초기값은 Lvalue여야 합니다.
cout << a2 << endl;
cout << b2 << endl;
int a3 = FunctionThree(x);
int& b3 = FunctionThree(x);// 지역 변수 또는 임시: x의 주소를 반환하고 있습니다.
cout << a3 << endl;
cout << b3 << endl;
int a4 = FunctionFour(x);
int& b4 = FunctionFour(x);
cout << a4 << endl;
cout << b4 << endl;
return 0;
}
ReferenceReturn.cpp 실행 결과
101
101
101
102
103
135783
103
104
2.3 const 참조자
참조자를 이용한 Call by Reference는 포인터를 이용한 것보다 함수를 정의하기 더 쉽다는 장점이 있다. 그러나 참조자를 이용한 함수의 정의는 한 가지 단점이 있는데 이와 관련해서 다음 예제를 살펴보자.
int num = 2021;
SampleFunction(num);
cout << num << endl;
// void SampleFunction(int num) { ... }일 경우, num은 2021입니다.
// void SampleFunction(int& ref) { ... }일 경우, num은 알 수 없습니다.
C언어의 관점에서 바라보면 위 예제의 실행 결과는 2021일 것이다. 포인터를 매개 변수로 하는 Call by Reference 함수가 아니므로 Call by Value 함수이고, 이것은 함수 외부에 선언된 변수의 값을 변경하지 못하기 때문이다. 그러나 C++의 관점은 조금 다르다. 6행의 SampleFunction 함수처럼 참조자를 매개 변수로 하는 Call by Reference일 경우, 실행 결과가 2021이라고 장담할 수 없다. 예를 들어, 다른 사람의 어떤 프로그램을 분석하는 과정에서 이런 함수를 발견했다고 치자. 우리는 함수 호출을 보고 함수가 어떻게 동작할지 어느 정도 예상할 수 있어야 한다. 그러나 C++의 경우 함수의 선언을 살펴봐야 하고, 참조자가 매개 변수로 선언되어 있다면 함수의 정의까지 모두 확인하여 참조자를 통한 외부에 선언된 변수의 값이 변경되는지 확인해야 한다. 즉, C++는 함수의 호출문만 보고 외부에 선언된 변수의 값이 변경되지 않는다는 것을 알 수 없는데, 이것은 분명한 단점으로 다가온다.
이 단점은 const 키워드를 선언한 참조자를 선언하여 해결할 수 있다. const 참조자는 참조자를 이용하여 함수 외부에 선언된 변수의 값을 변경하지 않겠다는 것을 말한다. 함수 안에서 참조자를 통한 변수의 값을 변경하지 않을 경우, 참조자를 const 키워드로 선언해서 함수의 원형을 보았을 때 변수의 값이 변경되지 않는다는 것을 보장할 수 있다.
funtionType functionName(const referenceType& referenceName) { ... }
[한줄 요약] const 참조자
함수의 매개 변수를 const 참조자로 선언하면 참조자를 이용한 값의 변경을 할 수 없게 되어 더 안전해진다.
3. 참조자 Call by Reference, const 포인터와 const 참조자 문제 풀이
3.1 참조자 기반의 Call by Reference 구현
열혈 C++ 프로그래밍 | 문제 02-1 | 참조자 기반의 Call-by-reference 구현
문제 1 참조자를 이용해서 다음 요구사항에 부합하는 함수를 각각 정의하여라. ▶ 인자로 전달된 int형 변수의 값을 1씩 증가시키는 함수 ▶ 인자로 전달된 int형 변수의 부호를 바꾸는 함수 그
continue96.tistory.com
3.2 const 포인터와 const 참조자
열혈 C++ 프로그래밍 | 문제 02-2 | const 포인터와 const 참조자
문제 1 const 포인터에 대한 복습을 겸할 수 있는 문제를 제시하겠다. 다음 상수 선언을 보자. const int num = 2021; 포인터 변수를 선언해서 위 변수를 가리키게 해보자. 그리고 이 포인터 변수를 참
continue96.tistory.com
'Object Oriented Programming(C++) > 열혈 C++ 프로그래밍' 카테고리의 다른 글
C++ | 02-3 C++에서 C언어 표준함수 호출하기(C Standard Library) (0) | 2021.08.02 |
---|---|
C++ | 02-2 malloc과 free를 대신하는 new와 delete (0) | 2021.08.02 |
C++ | 01-5 이름공간(Namespace) (0) | 2021.07.31 |
C++ | 01-4 인라인 함수(Inline Function) (0) | 2021.07.24 |
C++ | 01-3 매개 변수의 디폴트 값(Default Value) (0) | 2021.07.24 |
댓글