본문 바로가기

C/기초와 문법

[C] 함수 응용

반응형

🔊 해당 포스팅은 인프런의 널널한 개발자님의 독하게 시작하는 C 프로그래밍 강의를 듣고 개인적인 복습 목적 하에 작성된 글입니다. 해당 포스팅에 사용된 모든 자료는 필자가 직접 재구성하였음을 알립니다.
 

C언어를 배워보자!


이번 포스팅에서는 이전에 배웠던 C언어에서 함수를 정의하는 방법에서 더 나아가 좀 더 응용하는 내용에 대해서 알아보도록 하자.

1. 함수에 매개변수를 전달하는 2가지 기법

함수에 매개변수를 전달하는 방법으로는 크게 2가지가 존재한다. 첫 번째로는 우리가 흔히 사용해왔던 call by value 방식이다. 예시코드는 아래와 같다.

 

#include <stdio.h>

int Multiply(int a, int b) {
    return a * b;
}

int main(void) {
    int res = 0;
    res = Multiply(10, 99);
    printf("Result: %d\n", res);
    return 0;
}

 

두번째 방법으로는 매개변수에 포인터를 넣는 방식이다. 이를 call by reference 방식이라고도 한다. 예시 소스코드는 아래와 같다.

 

#include <stdio.h>

int Multiply(int* a, int* b) {
    return *a * *b;
}

int main(void) {
    int a = 10, b = 99;
    int res = 0;
    res = Multiply(&a, &b);
    printf("Result: %d\n", res);
    return 0;
}

 

참고로 함수에 전달되는 매개변수들은 쓰레드의 스택 프레임 메모리 또는 CPU 레지스터에 저장된다. 32비트 운영체제에서는 스택 프레임에 저장이 되고, 64비트의 운영체제에서는 CPU 레지스터에도 전달이 된 후 메모리에 스택 프레임 메모리에 저장된다는 점을 알아두자. 그리고 매개변수가 여러 개일 경우, 가장 오른쪽에 있는 매개변수부터 스택 프레임 메모리 또는 레지스터에 저장된다는 순서도 기억해두자.

2. 스택 프레임 메모리와 지역변수 주소 반환 문제

다음으로 알아볼 것은 함수를 만드는데, 이 함수가 지역변수의 메모리 주소를 반환할 때에 대한 문제에 대해서 다루어보자. 그에 앞어서 만약 어떤 함수 안에서 malloc() 함수를 이용해서 Heap 메모리로부터 메모리를 동적할당 받는다고 해보자. 이 때 해당 함수가 종료되고 나면 동적할당 받은 메모리는 여전히 살아있을까? 정답은 살아있다.

 

#include <stdio.h>
#include <stdlib.h>

char* MallocBuffer(char* pszBuffer) {
    pszBuffer = (char*)malloc(sizeof(char) * 5);

    pszBuffer[0] = 'Z';
    pszBuffer[1] = 'e';
    pszBuffer[2] = 'd';
    pszBuffer[3] = 'd';
    pszBuffer[4] = '\0';
    return pszBuffer;
}

int main(void) {
    char* pszBuffer = NULL;
    pszBuffer = MallocBuffer(pszBuffer);
    puts(pszBuffer);

    free(pszBuffer);
    return 0;
}

 

동적 할당 메모리 케이스와는 다르게 함수 내에서 지역변수의 주소를 반환하는 것은 문제가 될 수 있다. 우선 아래 소스코드를 실행시켜보고 출력이 어떻게 나오는지 보자.

 

#include <stdio.h>

int* Func(void) {
    int a = 10;
    return &a;
}


int main(void) {
    int* pszData = 0;
    pszData = Func();

    printf("%d\n", *pszData);
    return 0;
}

 

위 소스코드의 출력은 10이 나온다. 소스코드를 보면 Func 이라는 함수는 함수 내부에서 정의한 a 라는 지역변수의 주소를 반환한다. 그리고 이 Func 이라는 함수를 main 함수에서 호출하고 반환한 값을 출력한다. 결과만 보면 10으로 잘 나온 것 같다. 하지만 이는 치명적인 오류가 존재한다. 왜냐하면 위 Func 함수가 종료되는 순간 스택 프레임 메모리에서 지역변수 a 값이 소멸되기 때문이다. 엇? 그런데 소멸되었으면 10이 정상적으로 출력이 되면 안되는거 아닌가? 할 수 있다. 여기서 "소멸되었다" 라는 것이 반드시 해당 값이 0이나 다른 값으로 초기화(clear) 된다는 뜻이 아니다. 현재 위 소스코드에서 지역변수 a가 스택 메모리에서 소멸되고 난 뒤 다른 지역변수가 선언되거나 하는 등 스택 메모리에서 지역변수 a가 저장되었던 곳이 overwrite 되는 로직이 없기 때문에 그대로 10이 남아있다는 것이다. 그림으로 표시하면 아래와 같다.

 

 

Func 함수가 종료되기 전에 연두색 부분으로 지역변수 a가 스택 메모리에 저장되어있고 해당 메모리에는 10이라는 값이 저장되어 있던 상태였다. 그리고 Func 함수가 종료되고 난 뒤, a가 스택 메모리에서 소멸되었지만, 메모리에는 여전히 10이라는 값이 저장되어 있는 상태이다. 만약 이 상태에서 새로운 함수가 호출되는데, 그 새로운 함수 내에서 지역변수가 할당되면 저 스택 프레임 내의 10이라는 데이터가 어떻게 변할까? 아래 FuncNew 라는 함수가 추가된 예시 소스코드를 살펴보자.

 

#include <stdio.h>

int* Func(void) {
    int a = 10;
    return &a;
}

void FuncNew(void) {
    int b = 99;
}

int main(void) {
    int* pszData = 0;
    pszData = Func();
    FuncNew();

    printf("%d\n", *pszData);
    return 0;
}

 

FuncNew 라는 새로운 함수는 아무값도 반환하지 않도록 void로 설정하고 단지 지역변수 b만 선언하였다. 그리고 main 함수 내에서 pszData를 출력하기 전에 FuncNew 함수를 호출하기만 해보자. 위 소스코드의 결과는 99가 나온다. 즉, 아까 위에서 말한 스택 프레임 내에 지역변수 a가 소멸되고 난 뒤 FuncNew 함수 내에서 지역변수 b를 할당함으로써 b에 할당된 99가 덮어쓰기 된 것이다. 그림으로 표현하면 다음과 같아진다.

 

 

따라서, 만약 어떤 함수를 정의했는데, 해당 함수가 지역변수의 주소를 반환한다면 반드시 위와 같은 점을 주의하고 잘 사용해야 한다.

3. 메모리 동적 할당과 해제하는 함수의 분리

이번 목차에서는 메모리를 동적 할당받고 해당 메모리를 해제하는 함수가 서로 다른 함수로 분리되었을 때에 대해 알아본다. 사실 이 상황은 2번 목차에서 어떤 함수 내에서 메모리를 동적할당 받고 해당 함수가 종료된다고 하여도 동적할당 받은 메모리를 여전히 살아있다라고 설명할 때 든 상황과 동일하다. 먼저 아래 예시코드를 살펴보자.

 

#include <stdio.h>
#include <stdlib.h>

char* GetName(void) {
    char* pszName = NULL;

    pszName = (char*)calloc(32, sizeof(char));
    printf("이름을 입력하세요: ");

    fgets(pszName, sizeof(char) * 32, stdin);
    return pszName;
}

int main(void) {
    char* pszName = NULL;

    pszName = GetName();
    printf("당신의 이름은 %s입니다\n", pszName);
    
    free(pszName);
}

 

GetName 이라는 함수 내에서 pszName 이라는 문자열 포인터 변수로 동적 메모리를 할당받았다. 그리고 난 뒤 pszName을 반환하고 GetName 함수는 종료된다. 이 GetName 함수를 main 함수에서 호출하게 된다. 하지만 GetName 함수 안에서 동적할당 받은 메모리를 main 함수에서 반납(free) 해주고 있다. 

 

이렇게 메모리 동적 할당받는 역할은 GetName 함수가, 할당 받은 메모리를 반납하는 것은 main 함수가 하는 분리된 상황은 주의를 신중히 가해서 코드를 작성해야 한다. 왜냐하면 자칫하면 GetName 함수에서 동적할당 받은 메모리를 main 함수에서 반납하지 않으면 그대로 런타임동안 계속 메모리 누수가 발생할 것이기 때문이다.

 

추가적으로, GetName 함수처럼 포인터 변수를 반환하는 것에 대해서 주의할 점이 또 있다. 포인터 변수를 반환했다는 것은 말 그대로 어떤 데이터가 담겨있는 메모리의 주소 값을 반환한다는 것이다. 하지만 이는 메모리 주소 값 '만' 반환하는 것이지 해당 메모리가 얼마나 크기로 할당되어 있는지에 대해서는 알려주지 못한다. 따라서 동적 할당받는 케이스 뿐만 아니라 어찌 되었건 어떤 함수가 메모리 주소를 반환한다고 했을 때, 다른 함수에서 그 반환된 메모리 주소의 크기가 얼마인지 필요할 수도 있지 않을까?에 대해서 고려하면서 코드를 작성해야 한다.

반응형