call by value와 call by reference(C++ 참조변수와 자바)

mylime·2025년 3월 2일
post-thumbnail

서론


웹 개발자를 준비하고 있다면 call by value와 call by reference의 차이를 잘 알고있어야한다. java를 자주 사용했다면 call by value의 개념은 익숙할테지만, call by reference는 잘 와닿지 않는 경우가 많을 것이다. 글쓴이도 실제로 와닿지 않았다. (사실 글쓴이는 학부시절 C++을 배웠지만.. 몇 년 전이라 기억이 나지 않았다) 그래서 예제를 보면서 해당 개념을 알고, 둘의 차이점을 확실하게 비교하고자 글을 쓴다.



C++의 참조(&) 알아보기


가장 먼저 call by reference에 사용되는 reference(참조) 에 대한 개념을 알아야한다. 실제 코드를 보면 이해가 쉽기 때문에, 참조의 개념을 사용하는 C++의 예제를 가져와봤다.

C++을 사용해봤다면 참조 변수를 잘 알거다. 사용 예시는 다음과 같다.

int original = 10;
int copy = original;
int &ref = original; 

copy변수에는 original변수에 대한 값이 들어가고, ref변수에는 original 변수의 참조가 들어간다. 참조의 개념에서 reference는 original 변수와 개념적으로 완전히 동일하다고 볼 수 있다.


세 변수의 주소값을 찍어보면, ref변수는 original변수와 완전히 동일한 주소를 가진다. 반면 copy변수는 값만 같을 뿐, 다른 주소를 할당받는다는 걸 알 수 있다.

✨여기서 유의해야할 점은, 참조는 "같은 주소값을 가지고 있는 것"과는 완전히 다르다는 거다. 이는 아래에서 더 살펴보자.


ref = 20; 
cout << "----ref를 통해 값 변경----" << endl;
cout << "original: " << original << endl;  // 20
cout << "copy: " << copy << endl;   // 10
cout << "ref: " << ref << endl << endl;            // 20

ref를 통해 값을 바꿨다. reforiginal과 완전히 같은 변수이름으로 취급되기 때문에 original값도 같이 변경된다. 반면 단순히 값만 복사한 copy변수는 이전 값이 계속 유지되는 걸 볼 수 있다.



original = 30;
cout << "----원본변수 값 변경----" << endl;
cout << "original: " << original << endl;  // 30
cout << "ref: " << ref << endl << endl;            // 30

이후 원본변수 값을 바꿔도 ref의 값이 바뀌는 걸 알 수 있다.



참조가 어떻게 가능한거지?


자바를 주 언어로 개발해왔다면 참조가 잘 와닿지 않을 수 있다. 같은 주소값을 가지는 것도 아니고 어떻게 변수가 완전히 동일하게 취급되는 걸까?

C++에서 참조자는 할당된 하나의 메모리 공간에 다른 이름을 붙이는 것을 말한다. 변수를 대신할 수 있는 다른 이름, 별명이라고 볼 수 있다.


int original = 10;
int &ref = original; 

정수의 값을 가지는 int로 original 변수를 선언하면, 메모리 어느 공간에 original이라는 이름을 부여하게 된다. 그리고 해당 메모리 공간에 10을 선언한다.

ref는 이 메모리 공간을 가리키는 또다른 새로운 이름이 된다. 새로운 메모리가 할당되지 않고, 별명이 부여되는 느낌으로 볼 수 있다.


+) 참조자를 선언할 때는 다음 조건을 지켜야한다

  1. 이미 선언되어져 있는 변수에 대해서만 가능
  2. 상수에 대한 참조는 유효하지 않음
  3. 아무것도 참조하지 않는 형태의 선언도 유효하지 않음(NULL 초기화 불가능)

+) 컴파일러는 symbol table에 모든 변수 이름과 메모리 위치를 기록한다. 참조 변수를 만들면, 컴파일러는 참조 변수의 심볼을 원본 변수와 같은 메모리 위치를 가리키도록 설정한다.



참조 사용 예시: swap


#include <iostream>
using namespace std;

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

int main() {
    int num1 = 5;
    int num2 = 10;

    cout << "----변경 전----" << endl;
    cout << "num1: " << num1 << endl;
    cout << "num2: " << num2 << endl;
    cout << "num1 주소값: " << &num1 << endl;
    cout << "num2 주소값: " << &num2 << endl << endl;
    
    swap(num1, num2);

    cout << "----변경 후----" << endl;
    cout << "num1: " << num1 << endl;
    cout << "num2: " << num2 << endl;
    cout << "num1 주소값: " << &num1 << endl;
    cout << "num2 주소값: " << &num2 << endl;
}

call by value방식으로 매개변수를 만들 경우에는 ab에 대한 메모리 공간을 새로 할당해야한다.
하지만 참조로 매개변수를 받음으로써 새로운 메모리 공간을 할당하지 않고 값을 변경할 수 있게 된다.


swap이 잘 진행되었고, 주소값은 변하지 않았다는 걸 알 수 있다.
그리고 a변수가 새로운 메모리를 할당받지 않았고, num1변수와 완전히 동일한 주소 가리킨다는 것을 볼 수 있다.



call by value


call by value는 단순하게 생각하면 된다. 그냥 매개변수에 대한 새로운 메모리가 할당되고, 값이 복사되는 개념이다.

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

int main() {
	...
    
	swap(num1, num2);
    
	,,,
}

일단 예제의 swap 함수는 제대로 동작하지 않는다. 이유는 간단하다.

a와 num1, b와 num2는 완전히 다른 변수이고, 값만 같기 때문이다.
a와 num1은 가리키는 메모리 공간(주소값)도 다르고 완전히 독립적인 변수이다. 그래서 변경될 때 영향을 전혀 받지 않는다.



주소값을 값으로 가지는 배열에서의 예시


위에서는 primitive type 변수인 int에 대한 참조를 이야기했다. 단순해서 이해가 쉬웠겠지만, 조금 개념이 복잡해지는건 배열을 매개변수로 받을 때다.

int[] 배열은 값 자체를 가지는 게 아니라 해당 배열의 주소를 값으로 담고있다. 이를 매개변수로 받을 때의 call by reference와 call by value 예시를 알아보자.


call by value, java의 예시

public class Main {
    static void change(int[] arr) {
        arr[0] = 7;
        arr = new int[]{4,5,6};
    }

    public static void main(String[] args) {
        int[] original = new int[]{1,2,3};

        change(original);

        for (int value: original) {
            System.out.print(value + " ");
        }
    }
}

이 코드에서 arroriginal은 같은 값(배열의 시작주소)을 가지고 있다.

하지만 arr과 orignal은 완전히 독립적인 변수고, 가지고 있는 값(주소값)만 같다. 그래서 주소값에 있는 배열의 값을 변경할 때는 잘 작동하지만, 해당 변수 자체의 값을 바꿀 때는 서로 영향을 주지 않는다.


call by reference, C++의 예시

#include <iostream>
using namespace std;

void change(int* &arr) {
    arr[0] = 7;
    arr = new int[3]{4,5,6};
}

int main() {
    int* original = new int[3]{1,2,3};

    change(original);

    cout << original[0] << " " << original[1] << " " << original[2] << endl;
}

이 예시가 중요하다.

change 함수의 arr변수는 original과 완전히 같은 변수로 취급된다. 메모리 공간의 이름이 original과 arr 2개가 있는 것과 같다.

그래서 arr이 가리키는 메모리 공간의 0번 인덱스 값을 7로 바꾸는 것도 잘 동작하고, 메모리 공간의 값을 {4,5,6} 으로 바꾸는 것도 잘 동작한다. 그리고 해당 메모리 공간을 가리키는 original의 값도 변경된 것을 볼 수 있다.

original과 arr은 완전히 같은 메모리를 가리키고, 같은 이름이라고 생각하면 이해가 쉽다.


주소값을 가리키는 포인터의 경우, 값을 담을 변수를 새로 할당받아 주소값을 담는다. 하지만 참조의 경우 symbol table에 메모리의 값을 가리키는 이름을 추가하는 개념이다. 그래서 주소값을 저장하는 새로운 변수를 할당받지 않는다. 참조를 선언하면 완전히 동일한 변수처럼 사용이 되므로, 별명을 붙인다고 말하기도 한다.

좀 더 알아보고 싶다면 symbol table을 더 잘 설명한 다음 블로그를 참고하면 좋을 것 같다.
https://blog.naver.com/lcg2004/60202567805



마치며..


개념만 공부할 때는 잘 와닿지 않았는데, 직접 사용해보니 크게 와닿았다.
C++의 경우 다음 웹사이트에서 설치없이 실행해볼 수 있으니, 참조에 대한 개념을 더 알고싶다면 IDE를 힘들게 설치하지 말고 여기서 체험해봐도 좋을 것 같다. https://www.mycompiler.io/ko/new/cpp



참고자료


https://copycode.tistory.com/82
https://blog.naver.com/lcg2004/60202567805

profile
안녕하세요

0개의 댓글