티스토리 뷰

주의 사항!

  • 이 글은 제가 직접 공부하는 중에 작성되고 있습니다.
  • 따라서 제가 이해하는 그대로의 내용이 포함됩니다.
  • 따라서 이 글은 사실과는 다른 내용이 포함될 수 있습니다.

 

참조자의 이해

지금부터 설명하는 '참조자'라는 것은 성격상 포인터와 비교되기 쉽습니다. 그러나 참조자는 포인터를 이해하지 못해도 이해할 수 있는 개념입니다.

 

지금까지 변수를 선언할 때 다음과 같이 선언했습니다.

int org = 200;

 

위의 선언으로 인해 다음과 같은 일련의 과정들이 진행됩니다.

  • 컴파일 단계에서 int형 데이터를 저장할 메모리 공간을 할당합니다.
  • 해당 공간의 이름을 "org"라고 부릅니다.(주소와 헷갈리시면 안 됩니다.)
  • 해당 공간의 쓰레기 값을 데이터 200으로 초기화합니다.

 

참조자는 해당 메모리 공간을 'org'가 아닌 다른 이름으로도 부를 수 있게 해 줍니다. 참조자는 다음과 같이 선언합니다.

int& ref = org;

 

위의 예제에서 사용된 '&'는 메모리 공간의 주소를 구하는 '주소 연산자'가 아닙니다. 똑같은 기호이지만 자세히 보면 '주소 연산자'라고 하기에는 사용법이 다르다는 것을 알 수 있을 것입니다. 참조자의 선언에서 사용되는 '&'는 '참조 선언자'라고 합니다. 이 연산자는 해당 변수가 참조자로서 선언됨을 알려주는 역할을 합니다.

 

위의 코드로 인해 참조자 'ref'를 선언하게 되었고, 'ref'는 'org'를 부르는 다른 이름이 되었습니다. 그럼 만약 다음 예제와 같은 일을 수행한다면 어떤 일이 일어날까요?

#include <iostream>

int main()
{
	int org = 100;
	int& ref = org;

	ref = 333;

	std::cout << org << std::endl;
	std::cout << ref << std::endl;

	return 0;
}

/*
실행 결과

333
333

*/

 

'ref'라고 부르는 메모리 공간에 있는 데이터를 333으로 바꾸어 저장했습니다. 그리고 'org'이라고 부르는 메모리 공간의 데이터를 출력하고, 이어서 'ref'라고 부르는 메모리 공간의 데이터를 출력했습니다. 부르는 이름만 다르지 두 공간은 사실 같은 메모리 공간이기 때문에 해당 공간에 저장된 333을 출력합니다. 'org'과 'ref'에 각각 주소 연산자를 사용해 주소를 구할 때에도 역시 구해지는 주소는 같습니다.

#include <iostream>

int main()
{
	int org = 100;
	int& ref = org;

	ref = 333;

	std::cout << &org << std::endl;
	std::cout << &ref << std::endl;

	return 0;
}

/*
실행 결과

0097F974
0097F974

*/

 

lvalue 참조자(lvalue reference)

참조자(reference)는 C++11부터 lvalue 참조자와 rvalue 참조자로 구분됩니다. lvalue 참조자와 rvalue 참조자는 서로 탄생 배경이나 사용 목적이 다릅니다. 사실 앞에서 설명한 내용이 모두 lvalue 참조자에 해당되는 내용입니다. rvalue 참조자에 대한 설명은 잠시 뒤로 미루겠습니다.

 

lvalue 참조자를 사용하는 방법은 여러 가지 경우가 있습니다. 우선 앞에서도 설명했지만 변수를 참조하는 참조자를 선언하는 방법입니다.

#include <iostream>

int main()
{
	int org = 100;
	int& ref = org;
	
	std::cout << org << std::endl;
	std::cout << ref << std::endl;

	return 0;
}

/*
실행 결과

100
100

*/

 

물론 다음과 같이 참조자를 참조하는 참조자도 선언할 수 있습니다.

#include <iostream>

int main()
{
	int org = 100;
	int& ref = org;
	int& copy = ref;
	
	std::cout << org << std::endl;
	std::cout << ref << std::endl;
	std::cout << copy << std::endl;

	return 0;
}

/*
실행 결과

100
100
100

*/

 

아이덴티티가 존재하는 함수 역시 함수 포인터처럼 참조자로 선언하여 호출할 수 있습니다.

 

혹시 아이덴티티가 무엇인지 모르겠다면 아래의 '더보기'를 클릭하여 아이덴티티에 대한 설명을 참고하시기 바랍니다.

더보기

아이덴티티(Identity)란 번역하면 '정체성'을 의미합니다. 즉, 어떤 변수나 함수가 아이덴티티를 가지고 있다는 얘기는 그 변수나 함수가 자기 정체성을 가지고 있다는 의미일 것입니다. 

 

아이덴티티를 프로그래밍 관점에서 가장 쉽게 설명하자면 '이름'에 빗댈 수 있을 것 같습니다. 다음과 같은 리터럴이나 임시 객체는 아이덴티티를 가지지 못합니다.

10;         //리터럴
int(10);    //임시 객체

 

왜냐하면 이들은 메모리 공간에 저장되지도 않고('저장소'를 가지지도 않고), 또 이들을 추후 불러낼 수 있는 '이름'도 가지지 않기 때문입니다. 그렇기 때문에 이들은 한 번 사용하고 나면 버려지게 됩니다. 하지만 다음과 같이 '저장소'와 '이름'을 가지게 되면 아이덴티티가 존재한다고 표현합니다.

int age = 10;
Account myAccount = new Account("name", 30000);

 

위의 'age'와 'myAccount'는 이들을 저장하고 있는 메모리 공간도 가지고 있고, 언제든지 이들을 호출할 수 있는 '이름'도 가지고 있습니다.

 

#include <iostream>

int orgFunc() { return 10; }

int main()
{
	int(&refFunc)() = orgFunc;
	std::cout << refFunc() << std::endl;

	return 0;
}

/*
실행 결과

10

*/

 

lvalue 참조자(lvalue reference)의 존재 이유

C++에 lvalue 참조자의 개념을 도입하게 된 이유에도 여러 가지가 있습니다.

 

첫 번째로 객체 지향의 개념을 지원하기 위해서 도입되었습니다. 자바와 C#과 같은 객체 지향 언어는 오로지 다음과 같은 방법으로만 변수(또는 객체)를 생성할 수 있습니다.

Account a = new Account();

 

즉, 자바와 C#에서 사용하는 모든 변수는 포인터도, 일반 변수도 아닌 참조자와 매우 유사한 기능을 제공하고 있습니다.

반면 참조자가 있기 전 C++에는 일반 변수와 포인터만 존재했습니다. 일반 변수는 함수를 호출할 때 매개 변수를 통해 값 복사만을 가능하게 하기 때문에 다음과 같은 방법으로는 우리가 원하는 swap() 함수를 정의할 수가 없게 됩니다.

#include <iostream>

void swap(int a, int b)
{
	int temp = a;
	a = b;
	b = temp;
}

int main()
{
	int a = 11, b = 333;
	std::cout << "a : " << a << ", b : " << b << std::endl;

	swap(a, b);

	std::cout << "a : " << a << ", b : " << b << std::endl;

	return 0;
}

/*
실행 결과

a : 11, b : 333
a : 11, b : 333

*/

 

물론 이는 다음과 같이 포인터를 사용해서 해결이 가능하긴 합니다.

#include <iostream>

void swap(int* a, int* b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}

int main()
{
	int a = 11, b = 333;
	std::cout << "a : " << a << ", b : " << b << std::endl;

	swap(&a, &b);

	std::cout << "a : " << a << ", b : " << b << std::endl;

	return 0;
}

/*
실행 결과

a : 11, b : 333
a : 333, b : 11

*/

 

하지만 포인터는 NULL문제나 out of range와 같은 큰 문제를 가지고 있어 안정성이 떨어진다는 단점이 있습니다. 따라서 C++에서는 포인터의 사용을 자제하고 있습니다. 

 

두 번째 이유로는 참조자의 사용으로 프로그램의 성능이 향상될 수 있기 때문입니다. 다음의 예제를 보겠습니다.

#include <iostream>

int sum(int a, int b)
{
	return (a + b);
}

int main()
{
	int num1 = 1100, num2 = 33;
	int result = sum(num1, num2);

	std::cout << result << std::endl;

	return 0;
}

/*
실행 결과

1133

*/

 

위의 예제를 보면 두 정수를 입력받아 합연산 후 결과 값을 반환하는 sum() 함수가 정의되어 있습니다. 매우 간단한 함수이지만 이 함수가 매개 변수를 받는 과정을 들여다보면 불필요한 연산을 수행함을 할 수 있습니다.

 

sum() 함수에는 매개 변수로서 a와 b가 선언되어 있습니다. 그리고 함수를 호출할 때 주어진 인자로 a와 b는 초기화됩니다. 즉 add 함수의 호출로 인해 새로운 int형 변수 a와 b가 선언되고 이들이 각각 num1과 num2의 값으로 초기화되는 과정이 추가되는 것입니다. 이는 불필요한 과정입니다. 그냥 num1과 num2의 값을 바로 가져와 계산할 수는 없을까요?

 

다음과 같이 add 함수의 매개 변수를 참조자로 선언하게 되면 새로운 변수를 선언하지 않고 num1과 num2의 메모리 공간을 참조하여 해당 값을 바로 불러올 수 있게 됩니다. 불필요한 연산이 사라지는 것입니다.

#include <iostream>

int sum(int& a, int& b)
{
	return (a + b);
}

int main()
{
	int num1 = 1100, num2 = 33;
	int result = sum(num1, num2);

	std::cout << result << std::endl;

	return 0;
}

/*
실행 결과

1133

*/

 

물론 이는 참조자가 아닌 포인터로도 구현할 수 있지만, 앞서 설명했다시피 포인터의 사용은 자제하는 것이 좋습니다. 그리고 위 예제의 경우에는 값의 복사가 일어난다고 해도 int형 변수 두 개에 대해 값을 복사해오는 것이기 때문에 프로그램에 미치는 영향은 미미할 것입니다. 그래서 굳이 참조자를 사용하지 않아도 문제 될 것이 없다고 생각할 수 있습니다. 물론 그렇기는 하나 만약 객체를 다루기 시작하면 조금 까다로워집니다. 그리고 영향이 미미해도 반대로 말하자면 굳이 참조자를 사용하지 않을 이유도 없으므로 참조자를 사용하는 습관을 들이는 것이 좋습니다.

 

참조자 주의 사항

참조자는 선언과 동시에 변수(또는 객체)로 초기화해야 합니다. 다음은 그 예시입니다.

int org = 10;

int& ref = org;    //옳은 방법

int& copy;         //선언과 동시에 초기화가 되지 않음
copy = org;

 

또 참조자는 아이덴티티가 없는 대상 또는 NULL포인터는 참조할 수 없습니다. 아래는 그 예시입니다.

int& ref = 20;         //리터럴 참조 불가
int& ref;              //참조 대상이 없음
int& ref = int(10);    //임시 객체 참조 불가
int& ref = NULL;       //NULL 포인터는 참조 불가

 

참조는 배열 내 개별 요소들은 참조할 수 있지만 포인터와 달리 배열 전체를 참조할 수는 없습니다.

int arr[] = { 1, 2, 3, 4, 5, 6 };

int& ref = arr[2];    //개별 요소 참조 가능
int& refArr = arr;    //배열 전체 참조 불가능
int* arrPtr = arr;    //포인터는 배열 전체 참조 가능

 

참조자와 함수

우선 아래의 코드를 보겠습니다.

int num = 20;
int &ref = num;

 

실제로 위와 같이 변수를 선언하자마자 참조자 선언까지 하는 일은 극히 드뭅니다. 위의 예시는 참조자의 이해를 위해 사용했던 것일 뿐 실제 참조자는 함수에서 활용할 때 유용합니다. 다음과 같은 코드를 살펴보겠습니다.

#include <iostream>

void simpleFunc(int& val)
{
	val += 20;
}

int main(void)
{
	int num = 20;

	simpleFunc(num);

	std::cout << num << std::endl;

	return 0;
}

/*
실행결과

40

*/

 

처음 num를 20으로 선언했으나 출력될 때는 40으로 출력되었습니다. simpleFunc 함수를 호출하면서 인수로 num를 주었습니다. 그리고 이 함수는 매개 변수를 참조자의 형태로 받습니다. 함수 내에서 val는 참조자로서 num와 같은 메모리 공간을 공유하므로 val에 20을 더하는 연산이 num에 20을 더하는 연산과 동일하게 수행됩니다.

 

참조자는 함수의 반환형에도 선언할 수 있습니다. 아래의 예제를 보겠습니다.

#include <iostream>

int& refRetFuncOne(int& ref)
{
	ref++;
	return ref;
}

int main(void)
{
	int num1 = 1;
	int& num2 = refRetFuncOne(num1);
	num1++;
	num2++;

	std::cout << "num1 : " << num1 << std::endl;
	std::cout << "num2 : " << num2 << std::endl;

	return 0;
}

 

위 예제의 실행 결과가 어떻게 될지 한 번 생각해보고 밑의 '더보기'를 클릭해 결과를 확인합니다.

더보기
/*
실행결과

num1 : 4
num2 : 4

*/

 

  • 우선 num1을 1로 선언했습니다.
  • 그리고 refRutFuncOne 함수에 num1을 인수로 주어 호출합니다.
  • 함수의 매개변수에 참조자가 선언되어 있습니다. 따라서 ref는 num1과 같은 메모리 공간을 공유합니다.
  • ref의 값을 1 증가시켰기 때문에 결과적으로 num1의 값이 1 증가하고 2가 된 것과 같습니다.
  • 그리고 ref를 반환하는데 반환형이 참조형입니다.
  • 그리고 이것을 num2에 대입하는데 num2도 &연산자가 붙어 참조형입니다.
  • 결과적으로 num1을 참조한 참조자를 다시 참조했으므로 num2는 num1을 참조한 것과 같습니다.
  • 이후 num1을 1 증가 시켜 3이 되고, 다시 num2를 1 증가시킴으로써 4가 됩니다.
  • num1과 num2는 같은 메모리 공간을 공유하므로 둘을 출력하면 똑같은 4가 출력됩니다.

 

이번엔 참조형이 아닌 변수에 함수의 반환 값을 저장하는 것으로 위 예제를 조금 수정해보겠습니다.

#include <iostream>

using std::cout;
using std::cin;
using std::endl;

int& refRetFuncOne(int& ref)
{
	ref++;
	return ref;
}

int main(void)
{
	int num1 = 1;
	int num2 = refRetFuncOne(num1);
	num1++;
	num2 += 100;

	cout << "num1 : " << num1 << endl;
	cout << "num2 : " << num2 << endl;

	return 0;
}

 

위 예제의 실행결과를 생각해보고, 아래의 '더보기'를 클릭하여 결과를 확인합니다.

더보기
/*
실행결과

num1 : 3
num2 : 102

*/

 

  • 이번에는 함수의 반환값을 참조형이 아닌 변수 num2에 저장합니다.
  • num1을 1로 초기화하면서 선언했고, 이를 함수에 인수로 주어 호출합니다.
  • 함수 내에서 ref가 num1을 참조하므로 ref를 1 증가시키는 연산은 num1을 1 증가시키는 연산과 같게 됩니다.
  • 따라서 num1 은 2가 됩니다.
  • 그리고 ref를 반환하는데 참조형으로 반환합니다.
  • 참조형으로 반환했지만 해당 값을 저장하는 것은 일반적인 변수입니다.
  • ref가 저장하고 있는 값을 num2에 저장하므로 num2는 2가 됩니다. num1과는 관계없는 변수가 되었습니다.
  • 이후 num1은 1을 더해 3이 되고, num2는 100을 더해 102가 됩니다.

 

이번에는 함수의 반환형이 참조형이 아닌 int형이 되도록 조금 수정해보겠습니다.

#include <iostream>
#include "functionDeclaration.h"

using std::cout;
using std::cin;
using std::endl;

int refRetFuncOne(int& ref)
{
	ref++;
	return ref;
}

int main(void)
{
	int num1 = 1;
	int num2 = refRetFuncOne(num1);
	num1++;
	num2 += 100;

	cout << "num1 : " << num1 << endl;
	cout << "num2 : " << num2 << endl;

	return 0;
}

 

위 예제의 실행결과를 생각해보고, 아래의 '더보기'를 클릭하여 결과를 확인합니다.

더보기
/*
실행결과

num1 : 3
num2 : 102

*/

 

  • 실행결과는 앞선 예제와 같습니다.
  • 함수에서 반환형이 기본자료형이기 때문에 ref를 반환하면 ref가 가지고 있는 데이터를 반환합니다.
  • 그리고 num2는 이 데이터를 저장합니다.
  • 따라서 num2와 num1은 서로 아무런 관계 없는 변수가 됩니다.

 

이번에는 함수의 반환형이 기본자료형이고 num2가 참조형인 경우의 예제를 보겠습니다.

#include <iostream>
#include "functionDeclaration.h"

using std::cout;
using std::cin;
using std::endl;

int refRetFuncOne(int& ref)
{
	ref++;
	return ref;
}

int main(void)
{
	int num1 = 1;
	int& num2 = refRetFuncOne(num1);
	num1++;
	num2 += 100;

	cout << "num1 : " << num1 << endl;
	cout << "num2 : " << num2 << endl;

	return 0;
}

 

위 예제의 실행결과를 생각해보고, 아래의 '더보기'를 클릭하여 결과를 확인합니다.

더보기

위 예제의 연산은 불가능합니다.

 

num2가 참조형으로 선언되었으므로 우변에는 참조할 변수가 와야 합니다. 그런데 함수가 반환하는 자료형이 기본자료형입니다. 따라서 ref를 반환할 때 ref가 가진 데이터를 반환하므로 참조자 선언이 불가능하게 됩니다.

 

간단히 정리하면 이렇습니다. 함수의 반환형이 참조형일 경우 다음과 같은 연산이 가능합니다.

int num2 = refRetFuncOne(num1);   //가능
int& num2 = refRetFuncOne(num1);  //가능

 

하지만 함수의 반환형이 기본자료형인 경우 연산은 다음과 같이 됩니다.

int num2 = refRetFuncOne(num1);   //가능
int& num2 = refRetFuncOne(num1);  //불가능

 

이것 하나만 기억하면 될 듯합니다. "함수의 반환 값을 참조하고자 할 때는 반드시 함수의 반환형을 참조형으로 선언해야 한다". 하지만 함수의 반환형으로 참조형을 선언할 때는 주의해야 할 점이 있습니다. 함수 안에서 선언된 지역 변수를 반환하는 일은 없어야 합니다. 다음 예를 들어보겠습니다.

int& retuRefFunc(int n)
{
	int num = 20;
	num += n;

	return num;
}

 

위 함수는 함수 안에서 선언된 지역변수인 num를 참조형으로 반환하고 있습니다. 만약 이 함수가 다음과 같이 사용되었다면 어떻게 될까요?

int& ref = retuRefFunc(10);

 

함수 안에서 선언된 num의 값은 30이 되고, 이를 참조형으로 반환하므로 ref는 num와 같은 메모리 공간을 공유하게 되고 ref 또한 30이 됩니다. 그런데 num는 함수 안에서 선언된 지역변수입니다. 함수가 종료되면 해당 함수에서 선언된 지역변수를 저장하는 메모리는 모두 사라집니다. 따라서 함수가 종료될 때 num는 사라집니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함