본문 바로가기

참조 변수 (Reference Variable) 본문

💘 C++/행렬, 문자열, 포인터, 참조

참조 변수 (Reference Variable)

Hyonii 2022. 7. 17. 17:19

참조형 변수 (Reference variable)

지금까지 두 가지 변수 타입을 공부했다.

 

  • 일반 변수 : 직접 값을 보유
  • 포인터 : 다른 값의 주소(또는 null)를 보유

참조형(reference)은 C++에서 지원하는 세 번째 변수 타입이다.

참조형은 다른 객체 또는 값의 별칭으로 사용되는 C++ 타입이다.

 

C++은 세 가지 종류의 참조형을 지원한다.

 

  1.  non-const 값 참조형
  2.  const 값 참조형
  3.  r-value 참조형

포인터를 사용하다 보면 * 을 붙여주는 게 조금 귀찮을 수 있다.

de-referencing 할 때 일일이 *을 붙여줘야 하는데 조금 불편하다.

특정한 경우에 포인터보다 조금 사용하기 편한 게 있다.

그게 바로 참조 변수이다.

 

우리가 보통 포인터를 쓸 때에는

 

int value = 5;를 먼저 선언하고

포인터를 사용한다면 int *ptr = &value; 이렇게 할 수 있다.

 

사실 ptr에는 nullptr을 미리 넣어놓고

그 다음에 ptr = &value; 이렇게 할 수 있다.

왜냐하면 메모리 값이니까

메모리 값으로 0을 넣는 것도 가능하다.

실제로 쓸 수 없는 메모리 주소이기는 하지만 나름 주소이긴 하다.

예를 들어 가짜 전화번호를 적어주는 것과 비슷하다고 생각하면 된다.

 

포인터가 지금부터 설명드릴 참조와는 어떻게 다른지 비교해보며 공부해보자.


References to non-const values

non-const 값에 대한 참조형은 자료형 뒤에 앰퍼샌드(&)를 사용하여 선언한다.

 

- 자료형 &별명 = 기존 변수명;

 

참조는 int &ref = value; 이렇게 하면 내부적으로 ref가 value 하고 같은 variable인 것처럼 작동한다.

마치 같은 메모리를 사용하는 것처럼,

value가 딱 잡고 있는 메모리를 같이 사용하는 것처럼 작동한다.

 

cout으로 ref를 찍어보고

그다음 ref에다가 10을 대입하고 value와 ref를 찍어보자.

 

ref에 10을 대입했는데 value 에도 값이 바뀐 것을 볼 수 있다.

 

마치 포인터에서 *ptr = 10; 이렇게 해준 것과 똑같이 작동하고 있다.

그런데 보시면 ref는 앞에다가 *을 붙여줄 필요도 없고 뭔가 문법적으로 더 깔끔해 보인다.

 

일단 참조는 별명처럼 사용할 수 있다.

value라는 변수의 별명, 또 다른 이름인 것처럼 ref를 사용할 수 있다고 보면 된다.

 

그다음으로 주소도 찍어보자.

value의 주소, ref의 주소, 포인터의 주소, 포인터 변수 자체의 주소도 찍어보자

 

value의 주소가 나온 것은 os가 그렇게 줬겠거니 생각하면 된다.

그런데 여기서 ref의 주소가 value의 주소와 같다.

여기서 알 수 있는 것은 reference는 한 변수의 또 다른 이름인 것이다.

진짜 그냥 이름만 다른 거라고 생각하면 된다.

그리고 포인터 변수에 담고 있는 주소는 value의 주소인 것이 맞다.

마지막으로 포인터 변수 자체의 주소를 출력해보면 다른 값이 나온다.

이를 통해 포인터 변수도 변수라는 것 확인할 수 있다.

 

일단 여기서 중요하게 봐야 할 것은

cout << &value << endl;

cout << &ref << endl;

이 두 줄이 같은 값을 낸다는 것이다.

주소도 같다고 나온다는 거 알아두자!

자기 자신이 별도의 주소를 갖고 있는 것이 아니라 value의 주소를 공유한다고 보면 된다.

이와 다르게 포인터는 포인터 변수 자체가 다른 주소를 갖고 있다.

 


References must be initialized

참조형은 선언과 동시에 반드시 초기화해야 한다.

 

초기화 안하면 하라고 컴파일 에러 뜬다.

별명은 원래 이름이 꼭 하나 있어야 별명을 짓는 것과 비슷한 느낌이다.

 

null 값을 저장할 수 있는 포인터와 다르게, null 참조는 없다.

꼭 이렇게 초기화를 해줘야 한다는 특징이 있다.

 

그리고 오른쪽에 리터럴이 못 들어간다.

 

이런 숫자 못 들어간다.

왜냐하면 리터럴은 공식적인 메모리 주소를 갖지 못한다.

대신 변수가 들어갈 수 있다.

L-value가 들어갈 수 있다.


다음으로 const 예제를 보자. 

 

지금은 문제될게 없다.

여기서 이번에는 const int y를 넣어보자

 

이렇게 하면 오류난다.

왜냐하면 ref에서 y의 값을 마음대로 바꿔버릴 수도 있기 때문에 아예 컴파일러가 막아버리고 있다.

non-const 값에 대한 참조는 non-const 값으로만 초기화할 수 있다.

const 값 또는 r-value로 초기화할 수 없다.

 

const를 그냥 레퍼런스에 넣으려고 하니까 안된다?

그럼 눈치껏 레퍼런스에 const를 넣으면 되겠구나 알아차릴 수 있다.

 

이렇게 하면 된다.


References can not be reassigned

레퍼런스가 re-assign이 되는지 한번 해보자

 

이때 ref1에다가 value2를 다시 넣을 수 있을까?

넣어서 출력해보면

 

5와 10이 들어간 것을 확인할 수 있다.

여기서 헷갈렸다. 재할당이 된 건가? 싶었다.

하지만

 

주소를 출력해보면

여전히 ref1의 주소는 value1의 주소와 같다.

 

초기화된 후에는 다른 변수를 참조하도록 변경할 수 없다.

 

이 부분 이해가 잘 안돼서 참고한 페이지 ⬇

https://stackoverflow.com/questions/9293674/can-we-reassign-the-reference-in-c

 

Can we reassign the reference in C++?

I have read everywhere that a reference has to be initialized then and there and can't be re-initialized again. To test my understanding, I have written the following small program. It seems as if I

stackoverflow.com


References as function parameters

지금부터 설명드릴 부분이 중요하다.

참조형은 함수 매개 변수로 가장 많이 사용된다.

이때 매개 변수는 인수의 별칭으로 사용되며, 복사본이 만들어지지 않는다.

이렇게 하면 복사하는데 비용이 많이 드는 경우 성능이 향상될 수 있다.

 

함수에 포인터 인수 전달하면 함수 안에서 포인터를 역참조하여 인수의 값을 직접 수정할 수 있었다.

이런 점에서 참조형은 유사하게 작동한다. 

참조형 매개 변수는 인수의 별칭으로 사용되므로 참조 매개 변수를 사용하는 함수는 전달된 인수를 수정할 수 있다.

 

예제를 살펴보자

 

n 이 있다.

그다음 함수를 하나 만들어서 n의 값을 내부적으로 바꿔보자.

 

함수를 만들었다.

실행시켜보면 어떻게 나올까?

 

함수 안에 있는 n의 값과 main에서의 n의 값이 다르게 나온다.

값이 다르게 나오는 이유는

 

서로 완전히 다른 애들이기 때문이다.

다만 

 

여기서 n 이 들어갈 때 5라는 값이 들어가고

 

int n에는 복사가 되어서  처음에는 5인 것처럼 나온다.

하지만 함수 안에서 바꿔줘도 main에서 영향받지 않는다.

 

여기서 레퍼런스를 써보자!

간단하다

 

여기다가 &만 붙여주면 된다.

다시 실행시켜보자

 

함수에서 바뀐 값이 그대로 main에 영향을 준다.

주소를 찍어보자

 

주소가 아예 같다.

인수 n이 함수에 전달되면 함수 매개변수 &n 이 인수 n에 대한 참조로 설정된다.

이것은 함수가 &n을 통해 n의 값을 변경할 수 있게 한다.

변수 n 자체가 참조형일 필요는 없다.

 

포인터로 넘길 때는 포인터 변수의 주소는 또 다르다.

포인터가 우리가 넘겨주고자 하는 변수의 주소를 그대로 복사해서 넣어주는 것이다.

 

레퍼런스를 쓰게 되면 아예 변수 자체가 넘어간다.

레퍼런스는 아주 편리하고 좋은 개념이다.

 

포인터로 넣었을 때는 변수 자체를 복사를 한번 해야 한다.

그런데 레퍼런스로 넘기면 변수 자체가 넘어가는 것이기 때문에 복사할 필요가 없다.

주소조차도 복사 할 필요 없다.

그래서 효율이 더 높다.


그렇다면 반대로 어떤 경우에는 main에 있는 n의 값이 doSomething 함수에 들어가는데

이 함수가 n의 값을 바꾸지 못하게 막고 싶을 때는 어떻게 할까?

이런 경우에는 앞에 const로 막아버릴 수 있다.

 

이렇게 const를 앞에 붙여줬더니

바로 못 바꾸게 오류가 뜬다.

 

경고가 뜨고 있다.

 

요즘 프로그래밍은 이렇게 하는 추세이다.

예전에는 return 값을 하나만 받을 수 있었기 때문에

파라미터에다가 return 값의 레퍼런스나 포인터를 집어넣는 방식으로

return 값을 여러 개 받는 함수를 간접적으로 구현했었다.

그런데 최근 C++ 17부터는 여러 개 값을 받는 것이 어렵지 않게 되어서

코드를 깔끔하게 정리하고자 할 때는 그냥 return 값을 여러개 받아버린다.

입력은 순수하게 입력만 받아라. 즉, 입력으로 넣는 것은 doSomething 함수가 고치지 않게 해라 하는 경우가 꽤 많아졌다.

 


Using references to pass C-Style arrays to functions

C 스타일 배열에서 가장 귀찮은 문제 중 하나는 대부분이 평가될 때 포인터로 변환된다는 점이다.

그러나 C 스타일 배열을 참조로 전달하면 이러한 변환이 발생하지 않는다.

 

array를 함수에 넣어줄 때도 레퍼런스를 사용할 수 있다.

 

length가 5이고 arr가 있고 printElements 함수에 arr을 넣어주고 있다.

 

여기 &가 붙어있다

그리고 이 경우에는 element의 수가 반드시 들어가야 한다.

명시적으로 매개 변수에서 배열의 크기를 정의해야 한다.

조금 불편하지만 어차피 나중에는 아예 std::vector 같은 것을 레퍼런스로 넘겨버리면 정말 편해진다.

 

그리고 이때 앞에 const를 붙여줄 수도 있다.

 

 


References as shortcuts

참조형의 또 다른 장점은 중첩된 데이터에 쉽게 접근할 수 있게 한다는 것이다.

 

structure를 만들어 보자

 

struct Something을 한 개 만들고 그 안에 v1, v2가 있다.

그다음 struct가 한 개 더 있는데 그 안에 Something이 들어 있다.

 

그렇다면 main에서 Other를 하나 정의하고 

이 안에 들어있는 st에 들어있는 v1을 접근하려면

 

이런 식으로 접근했어야 했다.

 

그런데 만약 이름이 굉장히 길고 안쪽으로 들어가고 들어가고 하는 게 많아지면

타이핑하기도 힘들고 외우기도 힘들고 쓰기고 힘들고 느려진다.

이럴 때 레퍼런스를 쓸 수 있다.

 

v1이 지금 int 다

 

이렇게 하면 간단하게 사용할 수 있다.

 

지금은 예제라서 이름이 짧고 간단한거다

실전에서는 쓸모가 정말 많고 아주 유용하다.

특히 v1 같은 게 여러 번 쓰일 경우에는 레퍼런스를 꼭 쓰는 것이 좋다.

그래야 속도도 느려지지 않는다.

 


References vs Pointers

참조형과 포인터는 흥미로운 관계가 있다.

참조형은 접근할 때 암시적으로 역참조되는 포인터와 같은 역할을 한다.

(참조형은 내부적으로 포인터로 구현되어있다)

 

이렇게 하면 문제없이 잘 된다.

이때 ref와 *ptr이 기능상 동일하게 평가된다.

 

*ptr=10; 과 ref = 10; 이 두 개가 동일하게 작동한다.

 

참조형은 선언과 동시에 유효한 객체로 초기화해야 하고,

일단 초기화되면 변경할 수 없으므로 포인터보다 사용하는 것이 훨씬 안전하다.

(널 포인터를 역참조하면 위험하다)

 

주어진 문제가 참조형과 포인터 둘 다로 해결할 수 있다면 참조형을 사용하는 게 더 좋다.

 


요약 (Summary)

참조형을 통해 다른 객체나 값에 대한 별칭을 정의할 수 있다.

non-const 값에 대한 참조는 non-const 값으로만 초기화할 수 있다.

일단 초기화되면 참조를 재할당할 수 없다.

 

참조형은 인수의 값을 수정하려는 경우나 인수의 비싼 복사본을 만들지 않으려는 경우 함수 매개변수로 자주 사용된다.

 

여기까지 참조에 대해서 설명드렸다.

참조는 특히 객체지향 프로그래밍 하다 보면 아주 많이 쓰게 될 것이다.

 

Comments