[Python, C++] call by value, call by reference, call by address

3 minute read

함수 호출 방식에 대해 정리했습니다.

  • 추천 독자
    • 파이썬 함수 내에서 변수를 조작했는데 의도와 다르게 원본 값이 바뀌거나, 반대로 바뀌지 않은 경험이 있으신 분
    • 값, 참조, 주소에 의한 호출의 차이가 궁금하신 분

함수 호출 방식의 종류

  1. 참조에 의한 호출 (call by reference)
  2. 값에 의한 호출 (call by value)
  3. 주소에 의한 호출 (call by address)

파이썬에서는 call by assignment를 사용하는데, 불변 객체는 값에 의한 호출, 가변 객체는 참조에 의한 호출 방식으로 할당하여 사용됩니다. (1, 2번 혼합)

1. 참조에 의한 호출

my_param = ["before"]

def my_function(p):
    print(id(p))
    p.append("after")
    print(id(p))

print(my_param, id(my_param))
my_function(my_param)
print(my_param, id(my_param))

# ['before'] 4372546816
# 4372546816
# 4372546816 -- 전달받은 '그' 리스트에 추가
# ['before', 'after'] 4372546816
  • 함수에 파라미터를 전달하면 전달된 그 자체를 사용합니다.
  • 함수 내에서 파라미터로 전달된 변수가 변경되면 함수 밖에서도 변경됩니다.

파이썬에서는 mutable object(list, dict, set)가 전달되는 경우 위처럼 작동합니다.


2. 값에 의한 호출

my_param = "before"

def my_function(p):
    print(id(p))
    p = "after"
    print(id(p))

print(my_param, id(my_param))
my_function(my_param)
print(my_param, id(my_param))

# before 4372546736
# 4372546736 -- 변경 전까지는 id 값이 scope 외부와 동일 (참조로 사용)
# 4372547824 -- 변경이 일어나면 id 값이 달라짐
# before 4372546736
  • 함수에 파라미터를 전달하면 전달된 복사하여 사용합니다.
  • 함수 scope 내부에서만 사용되며, 함수 밖의 변수에는 영향을 미치지 못 합니다.

파이썬에서는 immutable object(int, float, str, tuple)가 전달되어 값이 변경되는 경우 위처럼 작동합니다. 값이 변경되지 않으면(그리고 변경되기 전까지는) 아래의 참조에 의한 호출로 작동합니다.


3. 주소에 의한 호출

메모리 공간 주소를 가리키는 포인터가 있는 언어에서 사용할 수 있습니다. 주소에 의한 호출은 원본 변수의 값을 변경한다는 점에서 참조에 의한 호출과 동일한 결과를 냅니다.

포인터를 사용할 수 없는 파이썬에서는 구현이 불가능하므로 여기서부터는 C++로 예제로 작성하겠습니다. 그리고 하는 김에 C++의 참조에 의한 호출과 비교해 보겠습니다.

#include <stdio.h>

// 주소에 의한 호출 (c, cpp에서 동작)
void  call_by_address(int* x, int* y) // 포인터를 전달
{
    int tmp = *x;
    *x = *y;
    *y = tmp;
    printf("주소: x = %d , y = %d \n", x, y);
    printf("값: *x = %d , *y = %d \n", *x, *y);
}

// call_by_address(&a, &b); // 주소를 직접 넘김
  • 주소를 값으로 전달(포인터 *a, *b로 주소 자체를 전달)하여 사용합니다.
// 참조에 의한 호출 (cpp에서 동작)
void  call_by_reference(int& x, int& y) // 참조변수를 전달
{ 
    int tmp = x;
    x = y;
    y = tmp;
    printf("주소: &x = %d, &y = %d \n", &x, &y);
    printf("값: x = %d, y = %d \n", x, y);
}

// call_by_reference(a, b); // 변수를 넘기고, 함수 내에서 변수의 주소를 참조
  • 참조에 의한 호출은 참조 변수인 앰퍼샌드(&)를 사용합니다.
    • a, b를 주고 함수 내에서 &로 참조
  • c에서는 지원하지 않습니다.
int main(void)
{
    int a = 20, b = 30;

    printf("최초값: a = %d , b = %d \n", a, b);
    printf("최초주소: &a = %d , &b = %d \n", &a, &b);
    
    puts("call by address");
    call_by_address(&a, &b); // 주소를 직접 넘김
    printf("변경값: a = %d , b = %d \n", a, b);

    puts("call by reference");
    call_by_reference(a, b); // 변수를 넘기고, 함수 내에서 변수의 주소를 참조
    printf("변경값: a = %d , b = %d \n", a, b);

    return 0; 
}

// 최초값: a = 20 , b = 30 
// 최초주소: &a = -1697730104 , &b = -1697730100

// call by address 
// 주소: x = -1697730104 , y = -1697730100 
// 값: *x = 30 , *y = 20

// 변경값: a = 30 , b = 20

// call by reference  
// 주소: &x = -1697730104, &y = -1697730100
// 값: x = 20, y = 30

// 변경값: a = 20 , b = 30
  • 주소에 의한 호출, 참조에 의한 호출 방식 모두 새로운 공간을 할당하지 않고 기존 공간을 이용하기 때문에 복사하는 시간을 아낄 수 있습니다.
  • 다만, 주소에 의한 호출은 주소 자체를 값으로 전달하기 때문에 호출자의 입장에서 신경쓸 것이 하나 늘어납니다.
  • 반면, 참조에 의한 호출은 함수 내에서 참조 변수를 사용하기 때문에 함수를 호출하는 측에서 조금 더 편하게 쓸 수 있습니다.

Tags:

Updated:

Leave a comment