C 언어(19) - 함수 호출의 원리

C 언어 강의, 함수 호출 원리

Featured image

🔚 짧게 하는 복습

✅ 1. 함수를 정의하고 호출할 수 있다.

✅ 2. main함수의 존재 이유에 대해 안다.

✅ 3. 함수를 쓰는 이유에 대해 안다.

혹시 기억이 안 난다면, 다시 돌아가자


우리는 저번 시간 main함수, 여러 자료형의 함수의 정의 및 호출을 배웠다.

배열과 포인터를 배우고 온 여러분은 “함수 이거 뭐 생각보다 쉽네”라고 생각할 수 있다.

아래의 코드를 한번 보자.

같은 이름의 변수가 선언이 2번이나 됐는데 오류가 나지 않는다. 그렇다면 마지막 출력값은 무엇일까?

또한, 이런 코드도 있다.

당연히 이 정도 코드는 해석하지…. 라고 생각했던 여러분은 의외의 결과를 볼 것이다.

분명 값을 바꾸는 함수인데 값이 바뀌지 않는다.

이게 도대체 무슨 일일까??

이러한 기괴한 상황들을 해석하기 위해서는 함수 호출의 과정을 더더욱 살펴볼 필요가 있다.


메모리의 영역

함수 호출을 설명하기 전, 컴퓨터의 메모리에 대해 조금 더 자세하게 알 필요가 있다.

우리는 이때까지 메모리의 세부적인 부분을 다루었다. 무슨 말이냐면 정해진 공간에서 변수가 어떻게 저장이 되고, 포인터가 어떻게 가리키고…. 좁은 범위에서 보았다는 의미이다.

우리는 변수가 저장되는 부분만 이해했지, 코드는 저장이 되는지 안 되는지도 모르고 리터럴 상수 등은 어디에 있는지 하나도 모르기 때문이다.

그래서 일단 넓은 범위의 메모리에 대해서 이야기해 보겠다.

위 그림이 메모리의 전체적인 구조이다. 보다시피 메모리는 총 4가지 영역으로 나뉘는데 텍스트, 데이터, 스택, 힙 영역으로 나뉜다.

텍스트 영역이란 우리가 작성한 코드상수가 위치하며, 이 영역은 읽기 전용이어서 실행 중에 코드를 수정할 수 없다.(함수도 여기 저장된다.)

데이터 영역힙 영역은 아직은 우리가 배운 것에 해당하는 것이 없어서 몰라도 된다.(참고로 전역 변수는 여기 데이터 영역에 저장된다.)

스택 영역은 함수 호출과 관련된 변수, 함수의 인자, 반환 주소 등이 저장되는 영역이다.

우리가 중요하게 볼 것은 스택 영역인데, 스택 영역은 함수가 하나 호출될 때마다 영역이 아래에서 위로 할당된다.

또한, 함수 하나가 종료될 시, 할당되었던 영역은 사라진다.

매개변수가 없는 함수의 실행

예시를 통해 자세하게 살펴보자.

#include<stdio.h>

void change1(){
    int n = 5;
}
int main() {
    int n = 4;

    change1();

    printf("%d", n);

    return 0;
}
  1. main 함수부터 찾는다.
  2. 스택 영역에 main함수를 위한 공간이 생긴다. 이를 스택 프레임이라고하며, 여기에는 매개변수, 함수에서 정의된 변수, 반환 주소가 저장된다.
  3. main함수가 위에서 아래로 실행된다. int n=4가 실행되면 main 함수의 스택 프레임 내에서, 시작 주소가 정해지고 4바이트 공간에 4라는 값이 이진수로 저장된다.
  4. change1()을 만나면, 텍스트 영역으로 가서 해당 함수의 주소를 찾는다.
  5. 스택 영역change1의 스택 프레임이 생성된다.
  6. 매개변수는 없으니 할당 될 것이 없고, change1이 종료되면 다시 돌아올 반환 주소가 저장된다.
  7. change1의 위에서 아래로 실행된다. int n=5가 실행되면 change1의 스택 프레임 내에서, 시작 주소가 정해지고 4바이트 공간에 5라는 값이 이진수로 저장된다.
  8. 반환 값이 없으니 그냥 종료한다. change1의 스택 프레임이 제거된다.
  9. 반환 주소를 가지고 있으니, main 함수에서 실행하던 부분으로 다시 돌아온다.
  10. main 함수의 스택 프레임에는 n이라는 변수가 4라는 수가 저장되어 있으니 4를 출력한다.
  11. 함수가 종료된다. main 함수의 스택 프레임도 제거된다.

이렇게 보니, 변수가 함수마다 같은 이름으로 선언이 돼 있어도 헷갈릴 일이 없다는 것을 알 수 있다.


매개변수가 있는 함수

이번에는 두 번째 예제를 같이 살펴보겠다.

#include<stdio.h>

void change2(int n, int m){
    int temp = n;
    n = m;
    m = temp;
}

int main() {
    int n = 4;
    int m = 5;

    change2(n, m);

    printf("%d %d", n, m);

    return 0;
}

  1. main 함수부터 찾는다.
  2. 스택 영역에 main함수를 위한 공간이 생긴다. 이를 스택 프레임이라고하며, 여기에는 매개변수, 함수에서 정의된 변수, 반환 주소가 저장된다.
  3. main함수가 위에서 아래로 실행된다. int n=4가 실행되면 main 함수의 스택 프레임 내에서, 시작 주소가 정해지고 4바이트 공간에 4라는 값이 이진수로 저장된다.
  4. main함수가 위에서 아래로 실행된다. int m=5가 실행되면 main 함수의 스택 프레임 내에서, n과 겹치지 않게 시작 주소가 정해지고 4바이트 공간에 5라는 값이 이진수로 저장된다.
  5. change2()을 만나면, 텍스트 영역으로 가서 해당 함수의 주소를 찾는다.
  6. 스택 영역change2의 스택 프레임이 생성된다. (이 부분이 가장 중요하다. 값이 복사된다)
  7. 매개변수의 영역의 n, m(매개변수 1, 2)에 넘겨준 값이 각각 복사되어 저장된다, change2이 종료되면 다시 돌아올 반환 주소가 저장된다.
  8. change2의 위에서 아래로 실행된다. int temp = n이 실행되면 change2의 스택 프레임 내에서, 시작 주소가 정해지고 4바이트 공간에 n의 값이 복사되어 이진수로 저장된다.
  9. 반환 값이 없으니 그냥 종료한다. change2의 스택 프레임이 제거된다.
  10. 반환 주소를 가지고 있으니, main 함수에서 실행하던 부분으로 다시 돌아온다.
  11. main 함수의 스택 프레임에는 n이라는 변수, m이라는 변수에 각각 4, 5라는 수가 저장되어 있으니 4, 5를 출력한다.
  12. 함수가 종료된다. main 함수의 스택 프레임도 제거된다.

어라라…. 이거 굉장히 익숙한 순간이다. (여기에 가보자)

마지막 예제에서 인사이트를 얻을 수 있다. 값을 복사하는 방법으로는 다른 변수 간에 영향을 줄 수 없다.

이는 다른 스택 프레임의 변수들끼리도 마찬가지다.

하지만 값의 주소를 복사해서 포인터를 이용한다면…?? 바로 실행해보자.


포인터 매개변수가 있는 함수

우리의 예측이 맞았다. 값이 드디어 바뀌는 것을 알 수 있다.

자세한 과정은 과제로 맡기겠다. 여기서 핵심은 포인터로 매개변수를 설정한 후 간접 참조를 이용하면 원래의 값에 영향을 줄 수 있다는 점이다.


📖 오늘의 핵심(다 알기 전까지는 넘어가지 말자❗)

✅ 1. 메모리 구조의 영역을 안다.

✅ 2. 함수 호출과 종료에 따른 스택 영역의 변화를 안다.

✅ 3. 포인터 매개변수가 있는 함수는 왜 다른 스택 프레임 사이 값의 영향을 줄 수 있는지 안다.

⚠️ 포인터 매개변수를 사용하지 않으면, 호출된 함수에서 main 함수의 값에는 영향을 줄 수 없다.

⚠️ Call by value, Call by reference라는 용어가 있다. 많은 강의에서 포인터 매개변수를 이용한 함수를 Call by reference라고 하는데, 이는 잘못된 정보이다. (읽어볼 거리 참고)

💣 과제,

  1. 포인터 매개변수가 있는 함수의 스택 메모리 영역의 변화를 그려보자(난이도 中)

  2. 1차원 배열을 매개변수로 넘기는 방법에 대해 생각해보자. (난이도 中)

  3. 다차원 배열이나 포인터 배열을 매개변수로 넘기는 방법에 대해 생각해보자. (난이도 上)

  4. 그렇다면 1차원 배열을 반환값으로 가지는 방법에 대해 생각해보자. (난이도 中)

  5. 그렇다면 다차원 배열이나 포인터 배열을 반환값으로 가지는 방법에 대해 생각해보자. (난이도 上)

🔜 더 공부해보기,

  1. 읽어볼 거리(1) - Call by value, Call by reference란? (매우 중요함)