반응형

0. 시작하기

1. Call-By-Value & Call-By-Reference란?

C언어에 익숙한 사람이라면, Call-By-Value와 Call-By-Address라는 것에 대해서 들어봤을 겁니다.

 

이것은 함수에 전달되는 인자가 '값(Value)'인지 '주소(Address)'인지에 따라서 결정됩니다.

 

그럼 Call-By-Reference는? 인자가 '참조자(Reference)'인 것을 의미합니다.

 

Address나 Reference나 작동방식에 있어서는 큰 차이가 없다고 생각합니다.

 

 

 

2. Call-By-Value ( CPP/CallByValue.cpp )

 

Call-By-Value는 일반적으로 많이 사용되는 방식입니다.

 

함수의 매개변수로 값이 전달되는 것을 의미하며, 보통 반환값이 존재하거나 전역변수와 같은 값을 통제하는데 사용하는 함수에서 사용합니다.

 

만약 Call-By-Value로 swap함수를 만든다면 어떤 문제가 발생하는지 알아보겠습니다.

 

예제코드를 아래와 같이 만들어보았습니다.

 

#include <iostream>
using namespace std;

void swap(int x, int y);

int main(void){
    int x = 10;
    int y = 20;

    swap(x, y);

    cout << x << " " << y << endl;
}

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

    cout << "a : " << a << "\nb : " << b << endl;
}

실행결과

 

실행해보면 swap이라는 함수 내에 전달된 x와 y값은 서로 위치가 바뀌어서 20, 10의 순서로 출력이 된것을 볼 수 있습니다.

 

하지만, main함수에서 변수 x, y를 출력해보면 함수에 들어가기 전과 동일한 10과 20이라는 값을 갖고있는 것을 볼 수 있습니다.

 

이것은 메인함수에서 swap 함수로 인자를 전달할 때, x와 y라는 변수의 값(10, 20 이라는 값)만을 전달한 것이기 때문입니다.

 

즉, 위의 코드에서 swap(x, y)는 swap(10, 20)과 동일한 작동을 한다는 것을 의미합니다. x와 y의 주소값등은 전달되지 않기 때문에 함수 내에서 아무리 값을 변경해도 결과적으로 x와 y의 값은 변함이 없다는 것을 말합니다.

 

이게 Call-By-Value : 값에 의한 호출을 말합니다.

 

 

 

3. Call-By-Reference ( CPP/CallByReference.cpp )

 

다음은 Call-By-Reference에 대해서 다뤄보겠습니다. 위에서 Call-By-Value를 다뤄봤기에 쉽게 추론하실 수 있을 것이라고 생각합니다.

 

예제코드를 먼저 보겠습니다.

 

#include <iostream>
using namespace std;

void swap(int &x, int &y);

int main(void){
    int x = 10;
    int y = 20;

    swap(x, y);

    cout << x << " " << y << endl;
}

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

    cout << "a : " << a << "\nb : " << b << endl;
}

실행결과

 

차이는 매개변수를 참조자로 선언해 주었다는 것만 존재합니다. 즉, Reference(참조자)를 이용하여 값을 '호출'하는 것이 아니라 실제 변수를 참조할 수 있는 참조자를 '호출'하는 것 이게 Call-By-Reference : 참조에 의한 호출을 의미합니다.

 

어렵지 않은 내용이라고 생각합니다.

 

 

 

 

4. 특이한 참조자의 사용 ( CPP/Reference4.cpp )

이제부터는 참조자의 여러가지 특이한 케이스들을 다뤄보겠습니다.

 

먼저 참조자는 선언시 참조할 '변수'를 지정해주어야 한다는 특징이 있습니다.

 

즉, 아래와 같은 코드를 수행하면 애러가 발생합니다.

 

#include <iostream>

int main(void){
    int &x = 20;

    return 0;
}

실행결과

 

참조자는 항상 변수를 참조하기 때문에 정수형 데이터를 참조자의 값으로 갖을 수 없다는 내용을 갖습니다.

 

그런데 const라는 녀석을 만나면 문제가 해결됩니다.

 

#include <iostream>

int main(void){
    const int &x = 10;

    return 0;
}

위와깉이 코드를 작성해서 수행하면 아무런 애러도 발생하지 않습니다.

 

왜? 그걸 위해서는 const함수가 무엇인지를 먼저 알아야 한다고 생각합니다.

 

  • const : constant의 약자로 '상수'를 정의하는데 사용합니다. '상수'란 코드내에서 절대로 변하지 않는 고정된 값을 의미합니다.

그럼 왜 const함수를 사용하면 애러가 없는걸까요? 이것은 여기서 10이 단순한 '값'이 아닌 특별한 '데이터'가 되었다는 것이 핵심입니다.

 

 

어려운 이야기일 수 있습니다. 풀어서 말하자면, 애러가 났던 코드에서 참조자는 10이라는 값을 전달받았지만, 아래의 참조자는 '메모리상에 상수 10이라는 값을 갖은 공간'을 전달받은 것을 의미한다고 생각하시면 좋겠습니다.

 

같은 10이라는 값이지만, 전자는 메모리에 존재하지 않는 10, 후자는 메모리에 상수로 지정된 10이라는 값

 

물론 후자의 경우, 특이하게도 '변수'가 없는 참조자 형태를 갖습니다. 때문에 어떻게보면 의도하지 않은 사용이 될 수도 있습니다.

 

하지만 덕분에 아래와 같은 코드를 사용도 가능해졌습니다.

 

#inlcude <iostream>
using namespace std;

int sub(const int &x, const int &y){
	return x-y;
}

int main(void){
	int result = sub(10,5);
    cout << result << endl;
    
    return 0;
}

 

 

 

5. 참조자의 문제점

편리한 참조자가 무슨 문제를 갖고있는가? 이것은 맨 처음에 다뤘던 swap코드를 다시 한번 보며 말씀드리겠습니다.

 

#include <iostream>
using namespace std;

void swap(int &x, int &y);

int main(void){
    int x = 10;
    int y = 20;

    swap(x, y);

    cout << x << " " << y << endl;
}

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

    cout << "a : " << a << "\nb : " << b << endl;
}

만약 위와같은 코드가 존재할 때, main함수 부분만은 보고 x, y 값이 [ Call-By-Reference ]라는 것을 알 수 있습니까?

 

swap함수를 정의한 것을 보지 않는다면, 메인함수에서는 해당 함수가 Call-By-Reference인지 Call-By-Value인지 알 수 없습니다.

 

큰 문제가 아니라고 생각할 수 있지만, 여러사람이 한 코드를 공유한다면 이것은 큰 문제가 될 수 있습니다.

 

당연히 Call-By-Value라고 생각하고 사용한 함수가 Call-By-Reference였고, 이에따라 함수의 인자로 전달한 값이 변하게되는 문제가 발생할 수 있습니다.

 

 

 

 

반응형
반응형

0. 시작하기

 

1. 포인터 ( CPP/Pointer.cpp )

참조자에 대해서 설명하기 위해서는 역시 포인터를 먼저 말하고 가야한다고 생각해서 적게되었습니다.

 

C언어를 입문하는 사람이 가장 힘들어하는 부분이 '포인터'라고 합니다.

 

포인터는 변수의 메모리상 주소를 가리키는 녀석을 말합니다.

 

조금 더 디테일하게 보겠습니다.

 

#include <iostream>
using namespace std;

int main(void){
    int a = 10;
    int *pt1 = &a;
    int **pt2 = &pt1;

    cout << a << endl;      // a의 값
    cout << &a << endl;     // a의 주소값

    cout << "\n---------------\n" << endl;

    cout << pt1 << endl;    // pt1의 값
    cout << *pt1 << endl;   // pt1이 가리키는 값
    cout << &pt1 << endl;   // pt1의 주소값

    cout << "\n---------------\n" << endl;

    cout << pt2 << endl;    // pt2의 값
    cout << *pt2 << endl;   // pt2가 가리키는 값
    cout << &pt2 << endl;   // pt2의 주소값

    return 0;
}

 

위와같은 코드를 하나 작성해보았습니다. 그리고 실행해보면 아래와 같은 결과를 볼 수 있습니다.

 

실행결과

 

 

이를 그림으로 표현하면 아래와 같습니다.

 

메모리상 데이터와 주소

어려워 보이는 개념일 수 있습니다만, 간단하게 생각해봅시다.

 

1개의 포인터(*)는 변수의 주소를 직접 갖고 있다는 의미입니다.

 

2개의 포인터(*)(=더블포인터)는 변수의 주소를 갖고있는 포인터의 주소를 갖고있다는 것을 말합니다.

 

포인터의 갯수가 3개, 4개가 되도 마찬가지입니다.(물론 3,4개씩 쓸일이 있을까 싶기는 합니다.)

 

C언어 기준으로 본다면, 포인터는 아래와 같습니다.

 

  • int *pt : 변수의 주소값을 저장할 'pt'라는 포인터를 만듭니다.
  • int **ptt : 변수의 주소값을 갖고있는 포인터의 주소값을 가리킬 (더블)포인터를 만듭니다.
  • print(*pt) : 포인터 pt가 가리키는 값을 출력합니다.
  • print(&pt) : 포인터 pt의 주소값을 출력합니다.

위의 4개만 알고있다면 포인터를 쉽게 사용은 못해도, 포인터를 이용한 다양한 알고리즘, 자료구조들을 이해하는데는 충분합니다.

 

 

2. 참조자 ( CPP/Reference1.cpp ~ Reference3.cpp )

그럼 본론으로 돌아가서 C++의 참조자에 대해서 언급해보겠습니다.

 

위에서 보았듯이 C++에서도 C언어와 마찬가지로 포인터(*)를 사용할 수 있는 것을 볼 수 있었습니다. 하지만, 프로그래밍을 할 때, 포인터를 사용한다는 것은 익숙하지 않은 프로그래머에게는 상당히 부담스러울 수 있는 일입니다.(난이도 때문)

 

이러한 문제 때문이었는지 C++에서는 '참조자'라는 특별한 친구를 만들어 주었습니다.

 

참조자는 변수에 대한 별명을 만들어주는 것이라 볼 수 있습니다.

 

예제 코드 몇개를 살펴 보겠습니다.

 

#include <iostream>   // CPP/Reference1.cpp
using namespace std;

int main(void){
    int x = 10;
    int &y = x;  // int y = x 사용시 결과는 11, 11

    x++;
    y++;

    cout << x << endl;
    cout << y << endl;

    return 0;
}

실행결과

 

만약 [ int y = x ]라고 해주었다면, 결과는 11, 11이 출력되었을 것입니다.

 

왜냐하면 변수라는 것은 [ 데이터 ]를 저장하는 공간이기 때문입니다. 데이터, 즉 값이라는 것을 저장하는 변수는 특정한 숫자나 특정한 문자열을 저장할 뿐이지 다른 변수의 주소값등을 저장하지는 못합니다.

 

따라서 위의 코드에서 [ int &y = x ]라는 부분이 특별한 기능을 한다는 것을 알 수 있습니다.

 

변수 선언과정에서 변수명 앞에 '&' 를 붙일 경우, 이것은 변수가 아닌 '참조자'라는 것으로 선언되었다는 의미를 갖습니다.

 

참조자는 위에서 언급한것과 같이 '변수'에 대한 '별명'을 붙여준다는 것을 말합니다.

 

위의 예제를 조금 수정해서 아래와 같이 실행해 보았습니다.

 

#include <iostream>  // CPP/Reference2.cpp

using namespace std;

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

    cout << &x << endl;
    cout << &y << endl;
    cout << &z << endl;

    return 0;
}

실행결과

위의 코드를 보게되면 '변수x'와 '참조자y'는 동일한 주소값을 갖고있는 것을 볼 수 있습니다.

 

그림으로 그려본다면 아래와 같은 모습임을 예측할 수 있습니다.

 

 

중요한것은 [변수x]와 [참조자 y, z]가 존재하는 형태라는 것입니다.

 

여러개의 변수가 하나의 데이터 공간을 공유하는 것은 아니라는 것을 꼭! 기억했으면 좋겠습니다.

( y 나 z가 없는 x는 존재할 수 있지만, x가 없는 y 또는 z 는 존재할 수 없습니다.)

 

 

참조자가 어떤 것인지는 대충 아셨을 것이라고 생각합니다. 그럼 이제 참조자가 어떤식으로 사용되는지 알아보겠습니다.

 

참조자의 가장 큰 특징은 딱 두가지입니다.

 

  • 참조자는 선언과 함께 참조할 변수가 정해져야 합니다. ( int &x; 와 같이 선언 불가)
  • 참조자는 함수 호출시 매개변수로 갖을 수 있다.

제가 설명력이 부족해서 이해하기 힘드실테니, 예제코드를 통해서 알아보겠습니다.

 

#include <iostream>

using namespace std;

void swap(int &a, int &b);

int main(void){
    int x = 10;
    int y = 20;

    swap(x,y);

    cout << x << endl;
    cout << y << endl;

    return 0;
}


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

실행결과

 

일반적으로 swap 함수를 사용할 경우 포인터를 사용해주었었습니다. 왜냐하면 함수의 경우 일반적으로 [Call-By-Value]를 원칙으로 하기 때문입니다.

( Call-By-Value, Call-By-Reference 에 대한 내용은 다음페이지에서 다루겠습니다. )

 

위의 swap 코드를 보게되면 [ int &a, int &b ]의 형태로 '참조자'를 이용한 것을 볼 수 있습니다.

 

따라서 이 swap 함수에서의 a, b라는 값은 메인함수의 [ x, y ]라는 변수의 '별명'이 되는 것을 볼 수 있습니다.

 

여기서 혹시 [ int &a ] 형태로 참조자는 선언할 수 없냐고 했는데 사용가능한건가? 하는 의문이 있을 수 있습니다.

 

결과는 당연히 가능합니다. 왜냐하면 함수의 매개변수는 함수가 코드내에서 실행되는 순간 할당되기 때문입니다.

 

위의 함수를 '인라인' 된것처럼 본다면 다음과 같은 코드가 될 것입니다.

 

#include <iostream>

using namespace std;

int main(void){
    int x = 10;
    int y = 20;

    // Swap 함수 영역 시작
    swap(int &a = x, int &b = y){
    	int temp = a;
        a = b;
        b = temp
    }
    // Swap 함수 영역 끝

    cout << x << endl;
    cout << y << endl;

    return 0;
}

'실제 코드가 아닌 추상적인 코드입니다'

 

 

위와같이 함수는 실행되는 과정에서 각각의 매개변수에 값을 할당하기 때문에 참조자만을 선언하는 것이 아니라는 것을 알 수 있습니다.

 

참조자는 정말 중요한 개념이고 다양한 응용이 있기 때문에 다음 페이지에서 조금 더 알아보도록 하겠습니다.

반응형

+ Recent posts