SturdyCobble's Study Note

[프로그래밍] 6.6 레퍼런스(Reference)와 포인터(Pointer) 본문

휴지통/['19.06-'20.07]프로그래밍101

[프로그래밍] 6.6 레퍼런스(Reference)와 포인터(Pointer)

StudyingCobble 2020. 7. 29. 20:31

NOTICE : 독학인 만큼, 잘못된 내용이 있을 수 있습니다. 내용 지적은 언제나 환영합니다.

더 이상 이 블로그에는 글을 올리지는 않을 예정입니다. 그렇지만 댓글을 달아주시면 최대한 답변드리고자 노력하겠습니다.


이 글은 C++의 레퍼런스에 대해 다룹니다. C/C++의 포인터도 약간 다룹니다.

 

 C++에서 Reference는 Pointer와 같이 변수를 간접적으로 접근할 수 있게 하는 역할을 합니다. 하지만, 그 자체에 메모리 주소를 저장하는 것이 아니라, 하나의 별명을 만드는 것처럼 작동합니다. 사실 포인터를 통해 내부적으로 레퍼런스가 구현될 수 있지만, 레퍼런스 만의 장점이 있어서 종종 이용됩니다.

int var = 10;
int& ref_var = var;

cout<< var << "," << ref_var << endl;
cout<< &var << "," << &ref_var << endl;

위와 같이 &를 이용합니다. 이때, Reference를 만들기 위한 &와, 주소를 가져오는 &가 서로 다르다는 사실에 유의해야 합니다. 레퍼런스의 경우 포인터처럼 *을 이용할 필요도 없습니다. 또한, 원본 변수와 같은 메모리상 주소를 나타내므로, 둘 중에 어느 것을 수정하여도 다른 값이 자동으로 변경되게 됩니다. '별명'이라는 개념으로 이해하면 편합니다.

 

 레퍼런스의 특징은 초기화하지 않으면 에러가 발생한다는 점입니다. 어떻게 보면 당연합니다. 레퍼런스는 기존의 변수와 같은 주소를 가리켜야 하므로, 초기화를 하지 않으면 변수인데 가리키는 주소가 없는 상태가 될테니 말입니다. 따라서 원본 변수와 같은 자료형을 가져야 하는 것은 당연한 이치입니다.

 

이미 생성된 레퍼런스는 그 가리키는 변수를 변경할 수 없습니다. ref_var = var2이런 식으로 대입하는 것은 가리키는 변수를 바꾸는 것이 아니라 var2가 가지는 값을 대입하는 식이 됩니다. 연장선상에서 생각해보면, 상수또한 대입할 수 없습니다.

 

 

 포인터와 레퍼런스는 사용되는 장소에 있어서 매우 유사합니다. 다음의 예시는 포인터와 레퍼런스를 함수의 인수와 리턴값으로 활용한 예시들입니다.

# include <iostream>
using namespace std;

void funcReference(int& rf_var) {
  rf_var = rf_var + 10;
}

void funcPointer(int* pt_var) {
  *pt_var = *pt_var + 10;
}

int main(){
  int var0 = 2020;
  funcReference(var0);
  cout << var0 << endl;
  funcPointer(&var0);
  cout << var0 << endl;
  return 0;
}
/*
--------RESULT---------------
2030
2040
*/

레퍼런스를 이용하는 경우 포인터와 다르게 &연산자와 *를 사용할 필요가 없습니다. 하지만, 한 가지 단점은 *같은 것을 붙이지 않기에 헷갈릴 우려도 있습니다. 따라서 이름에 reference임을 같이 표시하면 좋습니다.

# include <iostream>
using namespace std;

int& refFindSmallerOrLeft(int& ref_var1, int&ref_var2) {
  if (ref_var1 > ref_var2) { return ref_var2; }
  else { return ref_var1; }
}

int* ptrFindSmallerOrLeft(int* ptr_var1, int* ptr_var2) {
  if (*ptr_var1 > *ptr_var2) { return ptr_var2; }
  else { return ptr_var1; }
}

int main(){
  int var1 = 2020;
  int var2 = -2020;
  refFindSmallerOrLeft(var1, var2)=-2021;
  cout << var1 << " " << var2 << endl;
  *ptrFindSmallerOrLeft(&var1, &var2)=-2022;
  cout << var1 << " " << var2 << endl;
  return 0;
}
/*
--------RESULT---------------
2020 -2021
2020 -2022
*/

 위와 같이 리턴값으로 활용할수도 있습니다. 다만, 지역변수를 반환하려고 하면 다음과 같은 값이 나옵니다.

int& refLocalFood() {
  int localVar = 10;
  return localVar;
}

int* ptrLocalFood() {
  int localVar = 10;
  return &localVar;
}

int main(){
  cout << &refLocalFood() << endl;
  cout << ptrLocalFood() << endl;
  return 0;
}
==============RESULT===================
0
0

실제로 주소가 0이라는게 아니라, 이미 메모리 상에서 사라진 변수이기에 0으로 나타납니다.

 

 

 

  포인터와 다르게, 레퍼런스는 포인터나 다시 레퍼런스를 가지지 않습니다. 사실 이렇게 사용할 이유도 별로 없긴 합니다. 다만 포인터의 레퍼런스는 가능합니다. 따라서 이중 포인터를 포인터의 레퍼런스의 형태로 바꾸어 볼 수 있습니다. 이 때, 자료형이 T라고 한다면, 이에 대한 포인터는 T* ptr;로 선언할 것이고, 이에 대한 레퍼런스를 다시 취한다면 T*& ref_ptr;이 됩니다.

void funcThx(char*& var) {
  var="I'm Fine. Thank you. And you?";
}


int main(){
  char* str0 = "How are you?";
  cout << str0 << endl;
  funcThx(str0);
  cout << str0 << endl;
  return 0;
}
/*
--------RESULT---------------
How are you?
I'm Fine. Thank you. And you?
*/

 

  배열이나 함수에 대한 레퍼런스도 가능합니다. 다만 연산자 우선순위에 주의하여야 합니다.

void justFunc(int num) {
  cout << "Num: " << num << endl;
}


int main(){
  int arr[10]={0,1,2,3,4,5,6,7,8,9};
  void (&ref_func)(int)=justFunc;      // NOT  void &ref_func(int)=justFunc
  int (&ref_arr)[10]=arr;
  for(int i=0 ;i<10 ;i++)
    ref_func(ref_arr[i]);
  return 0;
}
/*
--------RESULT---------------
Num: 0
Num: 1
Num: 2
Num: 3
Num: 4
Num: 5
Num: 6
Num: 7
Num: 8
Num: 9
*/

 

 Java나 Python에는 직접적으로 위와 같은 레퍼런스 자료형이 있는 것은 아니지만, 거의 대부분의 자료형이 Reference Type의 형태로 처리됩니다. Java의 경우 일부 Primitive Type을 제외한 자료형이 그렇습니다.

Object o1 = new Object();
Object o2 = o1;

 위와 같은 형태로 코드를 작성하면, o1, o2는 객체가 됩니다. 이때, o2=o1이라는 식은 단순히 o1의 값을 대입하는 것을 넘어서 같은 객체를 참조하게 됩니다. C++의 레퍼런스와 매우 비슷하지만, 여기서는 값을 변경할 수 있습니다.

 

 또한, Java의 경우 각 Primitive 자료형(예를 들어 int에 대응하는 Int)에 대응하는 참조 자료형이 존재합니다. 이 부분에 대해서는 다음 글에서 다룰 예정입니다.

 

 Python의 경우도 비슷합니다. 다만, 일반적인 int와 같은 자료형에 대해서는 그 값만을 복사하게 됩니다. 그러나 주소를 복사하는 대표적인 예시가 리스트입니다.

a = [1,2,3,4,5]
b = a
b.append(6)
print(a)
print(b)
-------Result---
[1, 2, 3, 4, 5, 6]
[1, 2, 3, 4, 5, 6]

다만, int형도 객체로서 처리되므로 다음과 같은 결과가 나오게 됩니다.

sa = [1,2,3,4,5]
sb = sa
sb.append(6)
print(id(sa)==id(sb))
a = 10
b = a
print(id(a)==id(b))
b = b + 11
print(id(a)==id(b))
===================Result===============
True
True
False

 즉, 대입되어 값이 같을 때에는 같은 주소를 가리킵니다.

 

이에 관한 내용은 다른 글에서 다시 다룰 예정입니다. (아니면 이미 다루었을 수도 있습니다.)

 

 

 

 

다음 글에서는 Java와 Python의 참조 자료형들과 얕은 복사와 깊은 복사, 그리고 Java의 Primitive Type에 대응하는 Wrapper Class에 대해 알아볼 예정입니다. (물론 상황에 따라 내용이 변경될 수 있습니다.)

 

Comments