모두의 코드
씹어먹는 C++ - <2 - 2. C++ 은 C 친구일까?>

이번 강좌에서는

안녕하세요 여러분! 오랜만에 찾아온 Psi 입니다. 사실 이전 강좌에서 부터 강조해왔지만 C 언어에서 되던 것이 C++ 에서는 거의 100% 된다고 보셔도 무방합니다.

즉 기초적인 문법이 거의 똑같다는 것이지요. 이전 강좌에서는 기본적인 구문들, 예를 들어 변수의 정의 방법이나, 조건문(if, else, switch), 반복문(for, while, do-while) 등등을 살펴 보았는데요, 이번 강좌는 C++ 와 C 언어의 경계가 되는 강좌로 여러가지 중요한 내용을 배우게 됩니다. 그럼면서 여러분을 이끌고 자연스럽게 C++ 의 세계로 들어가도록 하겠습니다.

 함수 사용하기

#include <iostream>
using namespace std;

void print_square(int arg);
int main() {
  int i;

  cout << "제곱할 수 ? : ";
  cin >> i;

  print_square(i);

  return 0;
}

void print_square(int arg) { cout << "전달된 인자 : " << arg * arg << endl; }

성공적으로 컴파일 하였다면

와 같이 나옵니다.

위 소스를 보면, 사실 굳이 설명이 필요 없이 이해가 잘 되실 것입니다. C++ 에서 C 와 입출력 방법이 다를 뿐 다 똑같습니다. 일단, 아래의 코드에서 우리는 void 형의 (리턴값이 없는) 함수 print_square 을 선언합니다.

void print_square(int arg);

그리고 이 함수는

void print_square(int arg) { cout << "전달된 인자 : " << arg * arg << endl; }

와 같은 작업을 수행하지요. 즉 arg* arg 를 출력하는 것입니다. 그리고 main 함수에서 인자로 i 를 전달했지요.

print_square(i);

따라서 argi 의 값이 들어가서 i * i 인, 우리의 실행 결과의 경우 12 를 전달해서 144 가 출력되게 되는 것이지요. 매우 간단합니다. 사실 C 하고 전혀 다를 바가 없어요!

 레퍼런스의 도입

#include <iostream>

using namespace std;
int change_val(int *p) {
  *p = 3;

  return 0;
}
int main() {
  int number = 5;

  cout << number << endl;
  change_val(&number);
  cout << number << endl;
}

성공적으로 컴파일 하였다면

 와 같이 나옵니다.

위 소스 코드 역시 저의 C 언어 강좌를 잘 따라오신 분이라면 무리없이 이해하실 수 있는 코드 입니다. 즉 change_val 함수의 인자 pnumber 의 주소값을 전달하여, *p 를 통해 number 를 참조하여 number 의 값을 3 으로 바꾸었습니다. 그런데, 말이죠. 여러분 모두 & 키를 입력 하시는데에 진물이 나셨을 것입니다.

이런 분들 위해서 C++ 에서 새롭게 생겨난 개념이 있습니다. 바로 레퍼런스 입니다.

#include <iostream>

using namespace std;
int change_val(int &p) {
  p = 3;

  return 0;
}
int main() {
  int number = 5;

  cout << number << endl;
  change_val(number);
  cout << number << endl;
}

  성공적으로 컴파일 하였다면

앞선 결과와 동일하게 나오는 것을 알 수 있습니다. 위 소스코드를 찬찬히  살펴보자면 일단 change_val 함수에서 number 앞에 & 를 붙이지 않았습니다. 그리고, change_val 함수에서도 *p = 3; 대신에 p = 3; 으로 바뀌었습니다. 그 대신에 change_val 에서 인자로 int &p 를 받고 있습니다. 이 것이 바로 레퍼런스 입니다.

레퍼런스(reference) 가 무엇인지 알기 위해 일단 사전에 그 의미를 검색해보았습니다.

MENTIONING SB/STH |[C , U] ~ (to sb/sth) (… 에 대해) 말하기, 언급; 언급 대상, 언급한 것
LOOKING FOR INFORMATION |[U] (정보를 얻기 위해)찾아봄, 참고, 참조

그랬더니 두번째 뜻으로 참고 라는 말이 나오네요. 맞습니다. 레퍼런스란, 어떤 다른 변수의 참고, 즉 다른 이름 이란 의미를 가지고 있습니다. 그래서 흔히 C++ 에서 레퍼런스를 참조자 라고도 합니다. 위 경우 pnumber 변수의 다른 이름이 되는 것입니다. 따라서 p = 3; 이란 명령은 number = 3; 과 정확히 일치하는 명령이 되겠지요.

레퍼런스를 정의하는 방법은 아래와 같습니다.

int& ref = number;

즉, int 타입의 변수의 레퍼런스를 만들기 위해서는 int& 로 하면 되고, 그 오른쪽에 참고하고 싶은 것을 써주면 되지요. 어떤 특정 타입 T 에 대해 참조자를 만들고 싶다면 T& 와 같이 정의하면 됩니다. 아무튼, 이렇게 한다면 여러분은 number 에 다른 이름인 ref 를 부여한 것이나 마찬가지 입니다. 즉 별명을 지어준 것이라 생각하세요. 참조자의 가장 중요한 특성으로 반드시 정의 시 초기화 되어야 한다 입니다. 다시 말해, 아래와 같은 문장은 존재할 수 없다는 것이지요.

int &ref;

왜냐하면 참조자라는 것이 반드시 다른 어떤누구의 별명 이 되어야 하는 것인데, 그 '누구' 가 존재하지 않는다면 참조자가 존재할 필요가 없어질테니까요. 물론 어떤 사람들은 '포인터 역시 다른 어떤 변수를 가리켜야 하는데 왜 초기화 하지 않고 정의할 수 있냐' 라고 물을 수 있습니다.

하지만 포인터 자체는 '메모리 값을 보관하는 변수' 자체로 활용될 수 있지만 참조자는 그렇지 않습니다. 참조자는 포인터 처럼 어떠한 메모리 공간에 할당되어서 자신을 참조하는 주소값을 보관하는 것이 아닙니다. 컴파일 시에 원래 가리키던 변수의 주소값으로 다 치환되버리죠. (*(주소값) 으로)

참고로 말하자면 레퍼런스는 한 번 초기화 되면 다른 변수의 별명이 될 수 없습니다. 예를 들어서

int a = 10;
int &ref = a;
int b = 3;
ref = b;

를 하면 ref = b; 에서 refb 를 가리키는 것이 아니라, a = b;, 즉 a 에 3 이 대입되는 것입니다. 물론

&ref = b;

와 같은 문장은 &a = b; 즉, "a 의 주소값을 3 으로 변경한다?" 라는 말이 안되는 문장이고

ref &= b;

ref = ref &b;, 즉 a = a & b; 와 같은 문장으로 역시 전혀 의미가 다릅니다. 아무튼 레퍼런스는 포인터로 치면 int const * 와 같은 형태라 말할 수 있습니다. 즉 한 번 별명이 된다면 영원히 바뀔 수 없는 것이지요. (물론 포인터와 레퍼런스는 엄연히 다른 것입니다!)

  일부 C 언어를 배운 사람의 경우 레퍼런스와 포인터가 헷갈릴 수 있습니다. 예를 들어 아래와 같은 코드를 보세요.

int number = 10;
int& ref = number;
int* p = &number;

ref++;
p++;

p ++ 의 경우 C 언어를 배운 사람이라면 p 의 주소값이 4 만큼 증가되어서 (int 의 크기가 4 이니까) 아마 이상한 것을 가리키고 있겠지요. 반면에 ref++ 의 경우 ref 가 아까 'number 의 다른 이름' 이라고 했으므로 number++ 과 동일합니다. 즉 number 가 11 이 됩니다. 간단하지요? 이렇게 참조자를 사용한다면 귀찮았던 포인터 관련 연산들을 모두 생략할 수 있게 됩니다. 그냥 원래 변수라고 생각하면 되는 일이니까요.

  이제 다시 원래 소스를 다시 살펴보자면

change_val(number);

위와 같이 change_val 함수를 호출하였고 인자로 number 을 전달하였습니다. 따라서

int change_val(int &p) {
  p = 3;

  return 0;
}

위 문장에서 int &p = number;pnumber 의 별명이 됩니다. 따라서 p = 3; 이라 하는 것은 mainnumber = 3; 을 하는 것과 정확히 동일한 작업입니다.

// 참조자 이해하기

#include <iostream>
using namespace std;

int main() {
  int x;
  int& y = x;
  int& z = y;

  x = 1;
  cout << "x : " << x << " y : " << y << " z : " << z << endl;

  y = 2;
  cout << "x : " << x << " y : " << y << " z : " << z << endl;

  z = 3;
  cout << "x : " << x << " y : " << y << " z : " << z << endl;
}

  성공적으로 컴파일 하였다면

  사실 위 소스를 타이핑 하면서 고개를 갸우뚱 하시는 분들이 있을 지도 모릅니다. 왜냐하면 여러분들은 그간 C 언어의 '악마의 포인터 세계' 에서 사셨기 때문이지요. 하지만 여기서 강조하지만, 포인터와 레퍼런스는 비슷하면서도 다른 녀석들입니다. 먼저 다음과 같은 부분은 쉽게 이해하셨겠지요.

int x;
int& y = x;

"음. x 에 대한 레퍼런스로 y 를 정의하였구나. 즉 yx 의 또다른 별명이 되겠지." 라고 다들 생각하셨겠지요. 그러면서 여러분은 아마 제가 앞서 말한 "어떤 특정 타입 T 에 대해 참조자를 만들고 싶다면 T& 와 같이 정의하면 됩니다." 를 머리속에 떠올리면서 다음 소스를 보고 의문을 가졌을 것입니다.

int& y = x;
int& z = y;

"yint& 이므로 zint&& 가 되야 하는데 왜 int& 이지?". 좋은 질문입니다. 물론 이 이야기가 포인터였다면 정확히 들어맞을 터 입니다. 포인터였다면

int x;
int* y = &x;
int** z = &y;

  와 같이 소스를 작성했어야 맞겠죠. 하지만 포인터와 레퍼런스는 다릅니다. 앞선 소스를 다시 살펴봅시다.

int& y = x;
int& z = y;

yx 의 레퍼런스 입니다. 즉 우리가 코드 상에서 y 라고 표시한 것은 y 를 그대로 x 로 바꾸어도 의미가 변하지 않는 다는 것이지요. 다시 말해

int& z = x;
int& z = y;

위 두 문장은 정확히 같은 문장이라는 것입니다. 따라서 앞선 논의에 따라서 역시 int& 가 되어야 합니다. 여러분은 레퍼런스를 포인터의 개념으로 생각하시면 안됩니다. 그냥 어떤 변수의 다른 이름이라고 생각하시면 편할 것입니다. 따라서 위 세 문장의 정의식은 결국 x 의 다른 이름인 yz 를 만들어준 것 뿐입니다.

x = 1;
cout << "x : " << x << " y : " << y << " z : " << z << endl;

y = 2;
cout << "x : " << x << " y : " << y << " z : " << z << endl;

z = 3;
cout << "x : " << x << " y : " << y << " z : " << z << endl;

결과적으로 위 문장들은 모두 1,1,12,2,23,3,3 을 출력하게 되겠지요.

사실 여러분들은 도대체 왜 레퍼런스를 도입했는지 의문을 가질지도 모릅니다. 왜냐하면 어차피 레퍼런스로 할 수 있는거 포인터로 모두 할 수 있기 때문이죠. 하지만 나중에 가면 레퍼런스의 진정한 위력.. 이라기 보단 편리함을 몸소 느낄 수 있을 것입니다.

아니면 이미 느꼈을 지도 모르죠. 지난 강좌에서 변수 입력시 배웠던 cin 을 기억하시나요? 아마 사용자로 부터 변수에 값을 입력 받을 때 다음과 같이 했었을 것입니다.

cin >> user_input;

그런데 무언가 이상하지 않으세요? 예전에 scanf 로 이용할 때 분명히

scanf("%d", &user_input);

와 같이 항상 주소값을 전달해 주었는데 말이죠. 왜냐하면 어떤 변수의 값을 다른 함수에서 바꾸기 위해서는 항상 포인터로 전달하였기 때문이니까요. 하지만 여기서는 cin 이라는 것에 그냥 user_input 을 전달했는데 잘 작동합니다. 왜 그럴까요? 바로 레퍼런스 형태로 전달하였기 때문이지요. 귀찮은 &user_input 앞에 붙일 필요가 없게 되는 것입니다. 레퍼런스의 편리함은 이쯤에서 맛보기로 끝내도록 하고 계속 강좌를 진행해봅시다.

 상수에 대한 참조자

#include <iostream>
using namespace std;

int main() {
  int &ref = 4;

  cout << ref << endl;
}

위와 같은 소스를 살펴봅시다. 일단 컴파일 해보면 아래와 같은 오류가 나타날 것입니다.

error C2440: 'initializing' : cannot convert from 'int' to 'int &'

왜 오류가 나타날까요? 아마 여러분들은 다 알고 계시겠지요. 위 상수 값 자체는 '리터럴' 이기 때문에 (리터럴이 무엇인지 모르겠으면 여기로) 상수이고 따라서 위와 같이 레퍼런스로 참조한다면

ref = 5;

로 리터럴의 값을 바꿀 수 있는 여지가 생기기 때문에 참조할 수 없습니다. 하지만 아래와 같이

const int &ref = 4;

상수 참조자로 선언한다면 리터럴도 참조 할 수 있게 되는 것입니다. 예컨대

int a = ref;

a = 4; 와는 문장과 동일합니다. 마찬가지 이유로 상수를 참조하기 위해서는 상수 레퍼런스를 선언하시면 됩니다.

 레퍼런스의 배열과 배열의 레퍼런스

아마도 예전 C 강좌에서 포인터 가지고 한 이야기를 레퍼런스를 가지고 다시 한 번 재탕하는 기분입니다. 하지만 이 주제는 많은 C++ 초보자들의 머리를 아프게하는 문제이기도 하지요. 일단은, 레퍼런스의 배열이 과연 가능한 것인지에 대해 부터 생각해봅시다. 앞서 말했듯이 레퍼런스는 반드시 정의와 함께 초기화를 해주어야 한다고 했습니다. 따라서 여러분의 머리속에는 다음과 같이 레퍼런스의 배열을 정의하는 것을 떠올렸을 것입니다.

int a, b;
int& arr[2] = {a, b};

  그런데 말이죠. 컴파일 하기도 전에 빨간줄이 그어지네요. 아무튼 컴파일을 해보면

 error C2234: 'arr' : arrays of references are illegal

레퍼런스의 배열을 불법(illegal) 이라고 하네요. 얼마나 불법인지 한 번 C++ 규정을 찾아 보았더니 C++ 표준안 8.3.2/4 를 보면 놀랍게도

There shall be no references to references,no arrays of references, and no pointers to references
레퍼런스의 레퍼런스,레퍼런스의 배열, 레퍼런스의 포인터는 존재할 수 없다.

정말로 '불법' 인 것이 맞군요. 이것이 말이 돼냐 안돼냐를 떠나기 전에 C++ 규정에서 레퍼런스의 배열을 선언하는 것을 막아버리고 있습니다. 그러면 도대체 왜 안될까요? 왠지 위에서

int& arr[2] = {a, b};

로 해서 "arr[0]a 를 의미하고 arr[1]b 를 의미하고.." 로 만들면 안될까요. 여러분은 먼저 '레퍼런스의 포인터는 존재할 수 없다' 에 대해 생각해보도록 합시다. 레퍼런스의 포인터는 왜 존재하지 않을까요. 당연한 이야기 입니다. 위에서 말했듯이 레퍼런스는 메모리 상에 특정 공간을 차지하는 것이 아니라 컴파일 시에 원래 레퍼런스가 참조하던 변수의 주소값으로 대체된다고 하였습니다. 따라서 메모리 공간을 차지하지 않는 것의 포인터를 생각한다는 것은 말이 안되는 것입니다.

그런데 말이죠. arr 은 무엇을 의미하는 것일까요. 바로 arr 배열의 첫 번째 레퍼런스를 가리키는 '포인터' 가 되는 것입니다. 이는 바로 앞에서 말한 내용에 정확히 모순이 되는 것입니다. 따라서 마찬가지로 레퍼런스의 배열도 존재할 수 없게 됩니다.

하지만 배열의 레퍼런스는 어떨까요?

#include <iostream>
using namespace std;

int main() {
  int arr[3] = {1, 2, 3};
  int(&ref)[3] = arr;

  ref[0] = 2;
  ref[1] = 3;
  ref[2] = 1;

  cout << arr[0] << arr[1] << arr[2] << endl;
  return 0;
}

  성공적으로 컴파일 하였다면

  먼저 가장 중요한 첫 두줄을 살펴봅시다.

int arr[3] = {1, 2, 3};
int (&ref)[3] = arr;

위와 같이 refarr 을 가리키도록 하였습니다. 위와 같이 하면 ref[0] 부터 ref[2] 가 각각 arr[0] 부터 arr[2] 의 레퍼런스가 됩니다. 사실 배열의 레퍼런스는 잘 사용되지 않습니다. 왜냐하면 위와 같이 배열의 크기를 명확히 명시해 주어야 합니다. int (&ref)[3] 이라면 반드시 크기가 3 인 int 배열을 가리켜야 하고 int (&ref)[5] 라면 크기가 5 인 int 배열을 가리켜야 하겠지요.

하지만 포인터를 사용하면 굳이 그럴 필요 없이 단순히 int *P 하나로 모든 1 차원 배열들을 가리킬 수 있으니, 배열을 가리킬 필요가 있을 경우 레퍼런스 보다는 포인터를 사용하는 것을 훨씬 더 권장합니다. 참고로 그 이상 차원의 배열들도 마찬가지로 아래와 같이 레퍼런스를 사용하여 정의할 수 있습니다.

int arr[3][2] = {1, 2, 3, 4, 5, 6};
int (&ref)[3][2] = arr;

  역시 일차원 배열을 했을 때와 동일합니다.

레퍼런스를 리턴하는 함수

C++ 를 처음 배우신 분들이 가장 많이 헷갈려 하는 부분이 바로 레퍼런스를 반환하는 함수 입니다. 아래의 코드를 살펴볼까요 C++ 를 처음 배우신 분들이 가장 많이 헷갈려 하는 부분이 바로 레퍼런스를 반환하는 함수 입니다. 아래의 코드를 살펴볼까요.

#include <iostream>
using namespace std;

int fn1(int &a) { return a; }

int main() {
  int x = 1;
  cout << fn1(x)++ << endl;
}

당연히도 위 코드는 컴파일 되지 않습니다. 왜냐하면 fn1(x) 를 했을 때, "아 이제, ax 의 별명으로 해야지~" 라고 한 후에, 이를 리턴하면서 그냥 평범한 int 로 리턴하였기 때문에 임시로 복사된 x 의 '값' 이 반환되는 것입니다 (이를 우측값이라 하는데, 나중에 자세히 다루도록 하겠습니다). 당연히도 이 값은 임시로 생성된 것이므로, 읽기만 가능하지 수정은 불가능 합니다.

#include <iostream>
using namespace std;

int fn1(int &a) { return a; }

int &fn2(int &a) { return a; }
int main() {
  int x = 1;
  cout << fn2(x)++ << endl;
  cout << "x :: " << x << endl;
}

성공적으로 컴파일 하였다면

아주 깔끔하게 잘 나옵니다.놀라운 점은, x 의 값이 바뀌었다는 점입니다. 그 이유는 fn2 을 살펴보면 알 수 있습니다.

int& fn2(int& a) { return a; }

fn2 를 보면 인자로 레퍼런스를 받아서, 다시 그것을 그대로 리턴합니다. 쉽게말해,

fn2(x)

를 했을 때, fn2 내부에서 "아 이제 ax 의 별명 (레퍼런스) 이다!", 이렇게 된 것이고, 다시 함수를 리턴할 때 "나 x 의 별명을 리턴함!" 이렇게 되는 것이지요. 따라서, fn2(x) ++ 은 마치 x++ 을 한 문장과 동일하게 된 것입니다.

만약에 C 언어 였다면, x 의 포인터를 리턴하고, 그걸 받아서 다시 역참조 해서 (*)++ 을 해줬어야 하겠지요. 하지만 레퍼런스의 도입 덕분에 정말 편리해졌습니다.

생각해보기

문제 1

레퍼런스가 메모리 상에 반드시 존재해야 하는 경우는 어떤 경우가 있을까요? 그리고 메모리 상에 존재할 필요가 없는 경우는 또 어떤 경우가 있을 까요? (난이도 : 上)

강좌를 보다가 조금이라도 궁금한 것이나 이상한 점이 있다면꼭 댓글을 남겨주시기 바랍니다. 그 외에도 강좌에 관련된 것이라면 어떠한 것도 질문해 주셔도 상관 없습니다. 생각해 볼 문제도 정 모르겠다면 댓글을 달아주세요.

현재 여러분이 보신 강좌는<<씹어먹는 C++ - <2 - 2. C++ 은 C 친구일까?>>> 입니다. 이번 강좌의모든 예제들의 코드를 보지 않고 짤 수준까지 강좌를 읽어 보시기 전까지 다음 강좌로 넘어가지 말아주세요


 다음 강좌 보러가기
프로필 사진 없음
댓글에 글쓴이에게 큰 힘이 됩니다