[개발자의 교양] C언어에는 call by reference가 없다고???

SUNGKYUM KIM·2023년 3월 3일
2

개발자의 교양

목록 보기
1/1
post-thumbnail

때는 3학년, 프로그래밍 언어 수업을 들을 때 였다.

교수님 : !%$#@#^#%(&!… C언어에는 그래서 call by reference는 없는거야~
나 : ?????????

이게 도대체 무슨 충격적인 말인가. 분명 1학년 C언어 수업을 들을 때 포인터의 개념을 배우면서 “call by reference는 너무 너무 중요해요~” 라는 말을 100번은 넘게 들었던 것 같은데 말이다.

알면 좋고 몰라도 상관없는 [개발자의 교양] 시리즈, 첫 번째 주제는 C언어와 Call by reference 이다.

Call by reference?

#include <stdio.h>

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

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

    printf("Before swap: x=%d, y=%d\n", x, y);
    swap(x, y);
    printf("After swap: x=%d, y=%d\n", x, y);

    return 0;
}

모르긴 몰라도 C언어 기본 강의나 혹 책에서는 포인터에 대한 개념을 설명할 때 가장 많이 사용하는 예시일텐데 C언어를 배운 사람이라면 코드를 보면서 불편함에 몸서리 칠 것이다. 위 코드를 바르게 고친다면

#include <stdio.h>

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

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

    printf("Before swap: x=%d, y=%d\n", x, y);
    swap(&x, &y);
    printf("After swap: x=%d, y=%d\n", x, y);

    return 0;
}

이렇게 될 것인데, 이 과정에서 우리는 자연스레 Call by reference 혹은 Pass by reference 라는 개념을 접하게 된다.

잘못된 예시와는 다르게 swap 함수를 부를 때 & 연산자를 통해 주소값을 넘겨주게 되고 넘어온 파라매터의 값을 복사하여 저장하는 int 형 포인터 변수 a는 자신이 저장하고 있는 주소값에 * 연산자를 통해 접근하여 직접 값을 변경할 수 있게 된다.

방금 문장에는 C언어의 Call by reference 라고 흔히 부르는 방식의 동작원리를 차근차근 풀어 설명한 것인데 딱봐도 간단해 보이지는 않는다.

call by reference를 우리의 위대한 chat GPT님께서 어떻게 말씀하시는지 들어보자.

위대한 chat GPT 님께서 Call by Reference는 참조를 전달한다고 말씀하신다.

참조’를….전달…?

이쯤에서 우리는 의문을 가져야 한다. C언어는 주소값을 전달한다고 했는데 그럼 참조와 주소는 뭐가 다른걸까?

참조값과 주소값

참조값(reference value)주소값(address value)은 비슷한 의미를 가지고 있지만, 조금 다른 개념이다.

어떠한 변수가 ‘선언’되면, 해당 변수는 메모리 주소에 ‘할당’된다. 주소값은 이 할당된 메모리 주소를 나타내는 값으로 C언어에서는 보통 4byte의 메모리 공간에 저장하게 된다. 이는 말그대로 4byte 짜리 값(value)이기 때문에 필연적으로 저장하기 위한 별도의 변수가 필요하고 이 주소값을 저장할 수 있는 변수가 바로 포인터인 것이다. 주소값은 이처럼 값(value)이기 때문에 우리는 이 값에 특정한 숫자를 더하거나 빼서 변하게 만들 수 있다.

이와 다르게 ‘참조값’은 변수가 지닌 그 값 자체를 말하고, 그리고 ‘참조’는 그 값의 주소이다. 말이 쉬운 듯 어려운데 예시와 함께 이해해보자.

int a = 10; 
int b = 10;

a 변수와 b변수는 각각 10이라는 값을 가지고 있다. 더 정확하게는 2진수 형태로 숫자 10을 메모리 공간 어딘가에 ‘소유’하고 있다.

printf("%d", a==b);

그럼 위 코드의 결과는..? 당연히 1(True)이다. 그런데 만약 참조값에 대한 비교였다면(물론 C언어는 지원하지 않지만) 위 결과는 0(False)가 되어야 할 것이다.

이게 뭔소리냐 하면 a에 저장된 10은 b에 저장된 10과 메모리 상에 다른 위치에 존재한다는 것.

만약 a변수는 메모리상에 0x01에 저장되어있고 b변수는 0x02에 저장되어 있다면 a와 b는 서로 다른 것을 '참조'하고 있는 것이며 따라서 0x01의 10과 0x02의 10은 엄밀히 말해 다른 값이라는 것. 러프하게 말하면 주소와 값을 합한 개념이라고 생각해도 될 것이다.

그리고 0x01(아니면 0x02)에 올라간 10이라는 정수값을 알아낼 수 있는 열쇠가 바로 ‘참조’이고 이것을 우리는 일일히 0x01이라고 작성할 수 없으니(실제 주소는 더 복잡하니까) 간편하게 a라는 ‘이름’으로 참조가 가리키는 값을 알아내는 것이다.

a = a + 1
printf("%d" , a);
// 11

중요한 것은 참조값은 변할 수 있지만 참조는 변할 수 없다는 것. 위 코드에서 참조값은 (0x01의)11이 되었지만 참조는 그대로 0x01이다.

그러니 a/2, a*10, a-128, a%19 … 별 연산을 백날 천날 해봐야 a의 주소는 그대로라는 것.

(여담이지만 C++에서 참조자라는 개념이 들어오면서 이 참조 자체를 복사할 수 있는 방법이 생겼다. 따라서, a를 참조한 참조자 c변수가 있다고 한다면 c는 a와 (내부적으로)완전히 같은 변수로 ‘이름’은 다르지만 같은 참조값을 지닌다는 의미에서 별칭(alias)라고 부르기도한다.)

그럼 C언어에서 Call by Reference가 된다는 건 뭐야?

아까의 코드로 돌아가보자

swap(&x, &y);

이 부분에서 우리는 x와 y에 &연산자를 붙여서 상수값인 주소를 넘겨주고 있다(변수의 주소는 바꿀 수 없다는 것).

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

그런데 이 함수에서는 파라매터에 int *a, int *b라는 포인터 변수를 통해 해당 값을 전달받고 있다. 이게 무슨 말이냐면, 결국 넘어오는 값은 변수 x와 y가 머무르고 있는 해당 주소값을 복사한 값이며 이를 각각 a와b가 전달받을 뿐이라는 뜻이다.

그래서 해당 주소값을 통해 메모리에 접근하기 위해 에스터리스크(*)연산자를 통해 굳이 ‘접근’하는 과정을 추가하여 스왑하는 과정을 치룬 것.

아까 우리 위대한 chat GPT님께서 뭐라고 말씀하셨는지 기억하는가ㅏ? call by reference는 ‘참조’를 넘겨야 하는데 그냥 하나의 값(value)으로 ‘복사’해서 넘겼을 뿐이니 엄밀히 말하면 C언어에는 오직 Call by Value (혹 구분하기 위해 Call by Address라고 부르기도 한다) 뿐이 없다고 이야기 하는 것이다.

C언어 창시자는 뭐라던데?

이쯤에서 그럼 만든 사람이 뭐라는지 들어보면 되는거 아니야? 라는 생각이 드는 사람이 있을 것이다.

C언어 창시자 데니스 리치(Dennis Ritchie)와 브라이언 커니핸(Brian W. Kernighan)이 작성한 <C Programming Language>를 보면(커니핸은 자신이 C언어에 기여한 바는 없다고 말하긴 했다)

void strcpy(char *s, char *t){
    while((*s = *t) != '\0'){
        s++;
        t++;
    }
}

위 코드 예시와 함께

Because arguments are passed by value, strcpy can use the parameters s and t in any way it pleases

라고 설명하는데 보면 알겠지만 분명 passed by value, 즉 값에 의한 전달을 말하고 있다. 실제로 그들은 해당 책에서는 call by reference라는 표현을 쓰지 않고 오직 call by value 혹은 pass by value라는 표현만 사용하는 것을 알 수 있다.

또한 데니스 리치의 "The Development of the C Language"이라는 논문에서는 C 언어의 매개변수 전달 방식을 설명하면서 "C 언어는 값을 복사하는 값을 전달(call-by-value) 방식만 지원한다"고 명시하고 있다.

또한 위키의 평가전략(Evaluation Strategy) 라는 문서의 Call by reference 라는 항목에서는

This typically means that the function can modify (i.e., assign to) the variable used as argument—something that will be seen by its caller.

라고 말하며 함수를 부르는 caller의 인수를 직접 수정할 수 있어야 Call by reference라고 명시하고 있으며 포인터를 이용한 간접접근은 인수의 값을 직접 수정하는게 아니기에 이 기준에서 볼 때 Call by reference라고 보기는 어려울 것이고 그 아래에는

Call by reference can be simulated in languages that use call by value and don't exactly support call by reference, by making use of references (objects that refer to other objects), such as pointers (objects representing the memory addresses of other objects).

위와 같이 말하고 있는데, 해석하면 “Call by Reference는 Call by Value 밖에 지원하지 않는 언어에서는 Pointer를 사용해서 시뮬레이트(컴퓨터를 활용한 모의 실험에서 비롯된 단어 임을 생각해보자)한다”라는 의미인데 이렇게 보나 저렇게 보나 분명 C언어 자체에는 Call by Reference는 없다고 보는 것이 타당할 것이다.

뭐야 끝난거 아니야? 그럼 앞으로 C언어에 Call by Reference가 있다고 하는 강사들, 교수님들은 사짜니까 무시하고 앞으로 그렇게 말하는 사람들이 있으면 참교육해줘야지

라는 생각이 들었다면 결론까지 차분히 읽어주길 바란다.

결론 : 뭣이 중헌디

물론 근본주의의 관점으로 본다면 C언어에 Call by Reference가 있다고 말하는 사람들은 ‘틀린’ 게 맞지만, 그들을 ‘잘못’했다고 마냥 폄하하자는 것이 이 글의 목적이 아니다. Simulate던지 Support 던지 분명 C언어에서는 불리는 함수에게 넘어온 값이 value라 할 지라도 그 참조값을 변경할 수 있는 포인터라는 개념이 존재한다는 것은 누군가는 ‘원본을 수정’하는 문제에 대해서 깊은 고민을 했던 결과가 아닐까?

이 포인터는 분명 C언어를 배우는 많은 사람들을 좌절시키는 요소 중 하나이지만 (특히 python을 먼저 접한 사람들에게는 더..) CPU와 메모리에 대한 더 깊은 이해를 가져다 주는 것도 사실이라고 생각한다. 그리고 C언어의 위대한 업적들 또한 우리는 부정할 수 없을 것이다.

우리는 컴퓨터 과학을 발전시키기 위한 노력을 한 수많은 사람들의 업적 위에 서 있다(덕분에 많은 개발자들이 먹고 사는 것이 아닌가). 컴퓨터 과학의 아버지라고 불리우는 엘런 튜링 뿐만 아니라 교수님을 포함한 수많은 연구자들, 나아가 개발문화의 발전을 위해 노력하는 지식공유자들에게 경의를 표한다.

긴 글을 읽어주신 당신에게도 너무나 감사하다는 말과 함께 밥먹고 커피 한 잔 할 때 즐거운 대화 주제 중 한가지가 되길 바라는 마음으로 포스팅을 마친다.

Reference

profile
Code For Christ

1개의 댓글

comment-user-thumbnail
2023년 3월 17일

좋은 글 감사합니다 👍

답글 달기